diff --git a/.bomr/bomr.yaml b/.bomr/bomr.yaml deleted file mode 100644 index 4fedb4a0cf0b..000000000000 --- a/.bomr/bomr.yaml +++ /dev/null @@ -1,28 +0,0 @@ -bomr: - bom: spring-boot-project/spring-boot-dependencies/pom.xml - upgrade: - github: - organization: spring-projects - repository: spring-boot - issue-labels: - - 'type: dependency-upgrade' - policy: same-minor-version - prohibited: - - project: derby - versions: - # 10.15 requires Java 9 - - '[10.15,)' - verify: - ignored-dependencies: - # Avoid conflicting transitive requirements for - # io.grpc:grpc-core:jar:[1.0.1,1.0.1] (Jetty), - # io.grpc:grpc-core:jar:[1.14.0,1.14.0] (Micrometer's Azure Registry), and - # io.grpc:grpc-core:jar:[1.15.0,1.15.0] (Micrometer's Stackdriver Registry) - - 'io.micrometer:micrometer-registry-azure-monitor' - - 'org.eclipse.jetty.gcloud:jetty-gcloud-session-manager' - - 'org.eclipse.jetty:jetty-home' - repositories: - # Caffeine Simulator's dependencies - - 'https://maven.imagej.net/content/repositories/public/' - # Spring Data GemFire's GemFire dependencies - - 'https://repo.spring.io/gemstone-release-pivotal-cache' diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 79c06893c9ca..266a6152d621 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -7,7 +7,7 @@ categories as some of them do not apply here. STOP!! Please ask questions about how to use something, or to understand why something isn't working as you expect it to, on Stack Overflow using the spring-boot tag. - Security Vulnerability -STOP!! Please don't raise security vulnerabilities here. Head over to https://pivotal.io/security to learn how to disclose them responsibly. +STOP!! Please don't raise security vulnerabilities here. Head over to https://spring.io/security-policy to learn how to disclose them responsibly. - Managed Dependency Upgrade You DO NOT need to raise an issue for a managed dependency version upgrade as there's a semi-automatic process for checking managed dependencies for new versions before a release. BUT pull requests for upgrades that are more involved than just a version property change are still most welcome. - With an Immediate Pull Request diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index f97d9f541a2f..8ef2b756d14b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,11 +1,14 @@ + + + Spring Boot Maven Plugin + + org.springframework.boot + spring-boot-maven-plugin + 2.2.0.GRADLE-SNAPSHOT + spring-boot + false + true + + + build-info + Generate a {@code build-info.properties} file based on the content of the current +{@link MavenProject}. + false + true + false + false + false + true + generate-resources + org.springframework.boot.maven.BuildInfoMojo + java + per-lookup + once-per-session + 1.4.0 + true + + + additionalProperties + java.util.Map + false + true + Additional properties to store in the build-info.properties. Each entry is prefixed +by {@code build.} in the generated build-info.properties. + + + outputFile + java.io.File + false + true + The location of the generated build-info.properties. + + + project + org.apache.maven.project.MavenProject + true + false + The Maven project. + + + session + org.apache.maven.execution.MavenSession + true + false + The Maven session. + + + time + java.lang.String + 2.2.0 + false + true + The value used for the {@code build.time} property in a form suitable for +{@link Instant#parse(CharSequence)}. Defaults to {@code session.request.startTime}. +To disable the {@code build.time} property entirely, use {@code 'off'}. + + + + + + + + + + org.sonatype.plexus.build.incremental.BuildContext + buildContext + + + + + help + Display help information on spring-boot-maven-plugin.<br> +Call <code>mvn spring-boot:help -Ddetail=true -Dgoal=&lt;goal-name&gt;</code> to display parameter details. + false + false + false + false + false + true + org.springframework.boot.maven.HelpMojo + java + per-lookup + once-per-session + true + + + detail + boolean + false + true + If <code>true</code>, display all settable properties for each goal. + + + goal + java.lang.String + false + true + The name of the goal for which to show help. If unspecified, all goals will be displayed. + + + indentSize + int + false + true + The number of spaces per indentation level, should be positive. + + + lineLength + int + false + true + The maximum length of a display line, should be positive. + + + + ${detail} + ${goal} + ${indentSize} + ${lineLength} + + + + repackage + Repackages existing JAR and WAR archives so that they can be executed from the command +line using {@literal java -jar}. With <code>layout=NONE</code> can also be used simply +to package a JAR with nested dependencies (and no main class, so not executable). + compile+runtime + false + true + false + false + false + true + package + org.springframework.boot.maven.RepackageMojo + java + per-lookup + once-per-session + 1.0.0 + compile+runtime + true + + + attach + boolean + 1.4.0 + false + true + Attach the repackaged archive to be installed and deployed. + + + classifier + java.lang.String + 1.0.0 + false + true + Classifier to add to the repackaged archive. If not given, the main artifact will +be replaced by the repackaged archive. If given, the classifier will also be used +to determine the source archive to repackage: if an artifact with that classifier +already exists, it will be used as source and replaced. If no such artifact exists, +the main artifact will be used as source and the repackaged archive will be +attached as a supplemental artifact with that classifier. Attaching the artifact +allows to deploy it alongside to the original one, see <a href= +"https://maven.apache.org/plugins/maven-deploy-plugin/examples/deploying-with-classifiers.html" +>the Maven documentation for more details</a>. + + + embeddedLaunchScript + java.io.File + 1.3.0 + false + true + The embedded launch script to prepend to the front of the jar if it is fully +executable. If not specified the 'Spring Boot' default script will be used. + + + embeddedLaunchScriptProperties + java.util.Properties + 1.3.0 + false + true + Properties that should be expanded in the embedded launch script. + + + excludeDevtools + boolean + 1.3.0 + false + true + Exclude Spring Boot devtools from the repackaged archive. + + + excludeGroupIds + java.lang.String + 1.1.0 + false + true + Comma separated list of groupId names to exclude (exact match). + + + excludes + java.util.List + 1.1.0 + false + true + Collection of artifact definitions to exclude. The {@link Exclude} element defines +a {@code groupId} and {@code artifactId} mandatory properties and an optional +{@code classifier} property. + + + executable + boolean + 1.3.0 + false + true + Make a fully executable jar for *nix machines by prepending a launch script to the +jar. +<p> +Currently, some tools do not accept this format so you may not always be able to +use this technique. For example, {@code jar -xf} may silently fail to extract a jar +or war that has been made fully-executable. It is recommended that you only enable +this option if you intend to execute it directly, rather than running it with +{@code java -jar} or deploying it to a servlet container. + + + finalName + java.lang.String + 1.0.0 + false + false + Name of the generated archive. + + + includeSystemScope + boolean + 1.4.0 + false + true + Include system scoped dependencies. + + + includes + java.util.List + 1.2.0 + false + true + Collection of artifact definitions to include. The {@link Include} element defines +a {@code groupId} and {@code artifactId} mandatory properties and an optional +{@code classifier} property. + + + layout + org.springframework.boot.maven.RepackageMojo$LayoutType + 1.0.0 + false + true + The type of archive (which corresponds to how the dependencies are laid out inside +it). Possible values are JAR, WAR, ZIP, DIR, NONE. Defaults to a guess based on the +archive type. + + + layoutFactory + org.springframework.boot.loader.tools.LayoutFactory + 1.5.0 + false + true + The layout factory that will be used to create the executable archive if no +explicit layout is set. Alternative layouts implementations can be provided by 3rd +parties. + + + mainClass + java.lang.String + 1.0.0 + false + true + The name of the main class. If not specified the first compiled class found that +contains a 'main' method will be used. + + + outputDirectory + java.io.File + 1.0.0 + true + true + Directory containing the generated archive. + + + project + org.apache.maven.project.MavenProject + 1.0.0 + true + false + The Maven project. + + + requiresUnpack + java.util.List + 1.1.0 + false + true + A list of the libraries that must be unpacked from fat jars in order to run. +Specify each library as a {@code <dependency>} with a {@code <groupId>} and a +{@code <artifactId>} and they will be unpacked at runtime. + + + skip + boolean + 1.2.0 + false + true + Skip the execution. + + + + + ${spring-boot.repackage.excludeDevtools} + ${spring-boot.excludeGroupIds} + ${spring-boot.excludes} + + + + ${spring-boot.includes} + ${spring-boot.repackage.layout} + + + ${spring-boot.repackage.skip} + + + + org.apache.maven.project.MavenProjectHelper + projectHelper + + + + + run + Run an executable archive application. + test + false + true + false + false + false + true + validate + test-compile + org.springframework.boot.maven.RunMojo + java + per-lookup + once-per-session + 1.0.0 + false + + + addResources + boolean + 1.0.0 + false + true + Add maven resources to the classpath directly, this allows live in-place editing of +resources. Duplicate resources are removed from {@code target/classes} to prevent +them to appear twice if {@code ClassLoader.getResources()} is called. Please +consider adding {@code spring-boot-devtools} to your project instead as it provides +this feature and many more. + + + agent + java.io.File[] + 1.0.0 + since 2.2.0 in favor of {@code agents} + false + true + Path to agent jar. NOTE: a forked process is required to use this feature. + + + agents + java.io.File[] + 2.2.0 + false + true + Path to agent jars. NOTE: a forked process is required to use this feature. + + + arguments + java.lang.String[] + 1.0.0 + false + true + Arguments that should be passed to the application. On command line use commas to +separate multiple arguments. + + + classesDirectory + java.io.File + 1.0.0 + true + true + Directory containing the classes and resource files that should be packaged into +the archive. + + + environmentVariables + java.util.Map + 2.1.0 + false + true + List of Environment variables that should be associated with the forked process +used to run the application. NOTE: a forked process is required to use this +feature. + + + excludeGroupIds + java.lang.String + 1.1.0 + false + true + Comma separated list of groupId names to exclude (exact match). + + + excludes + java.util.List + 1.1.0 + false + true + Collection of artifact definitions to exclude. The {@link Exclude} element defines +a {@code groupId} and {@code artifactId} mandatory properties and an optional +{@code classifier} property. + + + folders + java.lang.String[] + 1.0.0 + false + true + Additional folders besides the classes directory that should be added to the +classpath. + + + fork + boolean + 1.2.0 + false + true + Flag to indicate if the run processes should be forked. Disabling forking will +disable some features such as an agent, custom JVM arguments, devtools or +specifying the working directory to use. + + + includes + java.util.List + 1.2.0 + false + true + Collection of artifact definitions to include. The {@link Include} element defines +a {@code groupId} and {@code artifactId} mandatory properties and an optional +{@code classifier} property. + + + jvmArguments + java.lang.String + 1.1.0 + false + true + JVM arguments that should be associated with the forked process used to run the +application. On command line, make sure to wrap multiple values between quotes. +NOTE: a forked process is required to use this feature. + + + mainClass + java.lang.String + 1.0.0 + false + true + The name of the main class. If not specified the first compiled class found that +contains a 'main' method will be used. + + + noverify + boolean + 1.0.0 + false + true + Flag to say that the agent requires -noverify. + + + optimizedLaunch + boolean + 2.2.0 + false + true + Whether the JVM's launch should be optimized. + + + profiles + java.lang.String[] + 1.3.0 + false + true + The spring profiles to activate. Convenience shortcut of specifying the +'spring.profiles.active' argument. On command line use commas to separate multiple +profiles. + + + project + org.apache.maven.project.MavenProject + 1.0.0 + true + false + The Maven project. + + + skip + boolean + 1.3.2 + false + true + Skip the execution. + + + systemPropertyVariables + java.util.Map + 2.1.0 + false + true + List of JVM system properties to pass to the process. NOTE: a forked process is +required to use this feature. + + + useTestClasspath + java.lang.Boolean + 1.3.0 + false + true + Flag to include the test classpath when running. + + + workingDirectory + java.io.File + 1.5.0 + false + true + Current working directory to use for the application. If not specified, basedir +will be used. NOTE: a forked process is required to use this feature. + + + + ${spring-boot.run.addResources} + ${spring-boot.run.agent} + ${spring-boot.run.agents} + ${spring-boot.run.arguments} + + ${spring-boot.excludeGroupIds} + ${spring-boot.excludes} + ${spring-boot.run.folders} + ${spring-boot.run.fork} + ${spring-boot.includes} + ${spring-boot.run.jvmArguments} + ${spring-boot.run.main-class} + ${spring-boot.run.noverify} + ${spring-boot.run.optimizedLaunch} + ${spring-boot.run.profiles} + + ${spring-boot.run.skip} + ${spring-boot.run.useTestClasspath} + ${spring-boot.run.workingDirectory} + + + + start + Start a spring application. Contrary to the {@code run} goal, this does not block and +allows other goal to operate on the application. This goal is typically used in +integration test scenario where the application is started before a test suite and +stopped after. + test + false + true + false + false + false + true + pre-integration-test + org.springframework.boot.maven.StartMojo + java + per-lookup + once-per-session + 1.3.0 + false + + + addResources + boolean + 1.0.0 + false + true + Add maven resources to the classpath directly, this allows live in-place editing of +resources. Duplicate resources are removed from {@code target/classes} to prevent +them to appear twice if {@code ClassLoader.getResources()} is called. Please +consider adding {@code spring-boot-devtools} to your project instead as it provides +this feature and many more. + + + agent + java.io.File[] + 1.0.0 + since 2.2.0 in favor of {@code agents} + false + true + Path to agent jar. NOTE: a forked process is required to use this feature. + + + agents + java.io.File[] + 2.2.0 + false + true + Path to agent jars. NOTE: a forked process is required to use this feature. + + + arguments + java.lang.String[] + 1.0.0 + false + true + Arguments that should be passed to the application. On command line use commas to +separate multiple arguments. + + + classesDirectory + java.io.File + 1.0.0 + true + true + Directory containing the classes and resource files that should be packaged into +the archive. + + + environmentVariables + java.util.Map + 2.1.0 + false + true + List of Environment variables that should be associated with the forked process +used to run the application. NOTE: a forked process is required to use this +feature. + + + excludeGroupIds + java.lang.String + 1.1.0 + false + true + Comma separated list of groupId names to exclude (exact match). + + + excludes + java.util.List + 1.1.0 + false + true + Collection of artifact definitions to exclude. The {@link Exclude} element defines +a {@code groupId} and {@code artifactId} mandatory properties and an optional +{@code classifier} property. + + + folders + java.lang.String[] + 1.0.0 + false + true + Additional folders besides the classes directory that should be added to the +classpath. + + + fork + boolean + 1.2.0 + false + true + Flag to indicate if the run processes should be forked. Disabling forking will +disable some features such as an agent, custom JVM arguments, devtools or +specifying the working directory to use. + + + includes + java.util.List + 1.2.0 + false + true + Collection of artifact definitions to include. The {@link Include} element defines +a {@code groupId} and {@code artifactId} mandatory properties and an optional +{@code classifier} property. + + + jmxName + java.lang.String + false + true + The JMX name of the automatically deployed MBean managing the lifecycle of the +spring application. + + + jmxPort + int + false + true + The port to use to expose the platform MBeanServer if the application is forked. + + + jvmArguments + java.lang.String + 1.1.0 + false + true + JVM arguments that should be associated with the forked process used to run the +application. On command line, make sure to wrap multiple values between quotes. +NOTE: a forked process is required to use this feature. + + + mainClass + java.lang.String + 1.0.0 + false + true + The name of the main class. If not specified the first compiled class found that +contains a 'main' method will be used. + + + maxAttempts + int + false + true + The maximum number of attempts to check if the spring application is ready. +Combined with the "wait" argument, this gives a global timeout value (30 sec by +default) + + + noverify + boolean + 1.0.0 + false + true + Flag to say that the agent requires -noverify. + + + profiles + java.lang.String[] + 1.3.0 + false + true + The spring profiles to activate. Convenience shortcut of specifying the +'spring.profiles.active' argument. On command line use commas to separate multiple +profiles. + + + project + org.apache.maven.project.MavenProject + 1.0.0 + true + false + The Maven project. + + + skip + boolean + 1.3.2 + false + true + Skip the execution. + + + systemPropertyVariables + java.util.Map + 2.1.0 + false + true + List of JVM system properties to pass to the process. NOTE: a forked process is +required to use this feature. + + + useTestClasspath + java.lang.Boolean + 1.3.0 + false + true + Flag to include the test classpath when running. + + + wait + long + false + true + The number of milli-seconds to wait between each attempt to check if the spring +application is ready. + + + workingDirectory + java.io.File + 1.5.0 + false + true + Current working directory to use for the application. If not specified, basedir +will be used. NOTE: a forked process is required to use this feature. + + + + ${spring-boot.run.addResources} + ${spring-boot.run.agent} + ${spring-boot.run.agents} + ${spring-boot.run.arguments} + + ${spring-boot.excludeGroupIds} + ${spring-boot.excludes} + ${spring-boot.run.folders} + ${spring-boot.run.fork} + ${spring-boot.includes} + ${spring-boot.run.jvmArguments} + ${spring-boot.run.main-class} + ${spring-boot.run.noverify} + ${spring-boot.run.profiles} + + ${spring-boot.run.skip} + ${spring-boot.run.useTestClasspath} + ${spring-boot.run.workingDirectory} + + + + stop + Stop a spring application that has been started by the "start" goal. Typically invoked +once a test suite has completed. + false + true + false + false + false + true + post-integration-test + org.springframework.boot.maven.StopMojo + java + per-lookup + once-per-session + 1.3.0 + false + + + fork + java.lang.Boolean + 1.3.0 + false + true + Flag to indicate if process to stop was forked. By default, the value is inherited +from the {@link MavenProject}. If it is set, it must match the value used to +{@link StartMojo start} the process. + + + jmxName + java.lang.String + false + true + The JMX name of the automatically deployed MBean managing the lifecycle of the +application. + + + jmxPort + int + false + true + The port to use to lookup the platform MBeanServer if the application has been +forked. + + + project + org.apache.maven.project.MavenProject + 1.4.1 + true + false + The Maven project. + + + skip + boolean + 1.3.2 + false + true + Skip the execution. + + + + ${spring-boot.stop.fork} + + ${spring-boot.stop.skip} + + + + + diff --git a/buildSrc/src/test/resources/spring-configuration-metadata.json b/buildSrc/src/test/resources/spring-configuration-metadata.json new file mode 100644 index 000000000000..e975b1e3f4f2 --- /dev/null +++ b/buildSrc/src/test/resources/spring-configuration-metadata.json @@ -0,0 +1,9 @@ +{ + "properties": [ + { + "name": "example.counter", + "type": "java.lang.Integer", + "defaultValue": 0 + } + ] +} diff --git a/ci/README.adoc b/ci/README.adoc index 3f20aab939e3..5d4431a9cfa2 100644 --- a/ci/README.adoc +++ b/ci/README.adoc @@ -11,7 +11,7 @@ The pipeline can be deployed using the following command: [source] ---- -$ fly -t spring-boot set-pipeline -p spring-boot -c ci/pipeline.yml -l ci/parameters.yml +$ fly -t spring-boot set-pipeline -p spring-boot-2.5.x -c ci/pipeline.yml -l ci/parameters.yml ---- NOTE: This assumes that you have credhub integration configured with the appropriate diff --git a/ci/config/changelog-generator.yml b/ci/config/changelog-generator.yml new file mode 100644 index 000000000000..67db9ca4f9d7 --- /dev/null +++ b/ci/config/changelog-generator.yml @@ -0,0 +1,23 @@ +changelog: + repository: spring-projects/spring-boot + sections: + - title: ":star: New Features" + labels: + - "type: enhancement" + - title: ":lady_beetle: Bug Fixes" + labels: + - "type: bug" + - "type: regression" + - title: ":notebook_with_decorative_cover: Documentation" + labels: + - "type: documentation" + - title: ":hammer: Dependency Upgrades" + sort: "title" + labels: + - "type: dependency-upgrade" + issues: + ports: + - label: "status: forward-port" + bodyExpression: 'Forward port of issue #(\d+).*' + - label: "status: back-port" + bodyExpression: 'Back port of issue #(\d+).*' diff --git a/ci/config/gradle-plugin-publishing/build.gradle b/ci/config/gradle-plugin-publishing/build.gradle new file mode 100644 index 000000000000..e3a34c3ed2b2 --- /dev/null +++ b/ci/config/gradle-plugin-publishing/build.gradle @@ -0,0 +1,13 @@ +plugins { + id "com.gradle.plugin-publish" version "0.15.0" +} +tasks.register("publishExisting", com.gradle.publish.PublishExistingTask) { + pluginId = "org.springframework.boot" + fileRepositoryRoot = new File("${repositoryRoot}") + pluginVersion = "${bootVersion}" + pluginCoordinates = "org.springframework.boot:spring-boot-gradle-plugin:${bootVersion}" + displayName = "Spring Boot Gradle Plugin" + pluginDescription = "Spring Boot Gradle Plugin" + website = "https://spring.io/projects/spring-boot" + vcsUrl = "https://github.com/spring-projects/spring-boot" +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-cli/src/it/resources/jar-command/public/public.txt b/ci/config/gradle-plugin-publishing/settings.gradle similarity index 100% rename from spring-boot-project/spring-boot-cli/src/it/resources/jar-command/public/public.txt rename to ci/config/gradle-plugin-publishing/settings.gradle diff --git a/ci/images/README.adoc b/ci/images/README.adoc index 84eae1609edd..92fa3c2ec51c 100644 --- a/ci/images/README.adoc +++ b/ci/images/README.adoc @@ -11,11 +11,11 @@ $ docker build --no-cache -f /Dockerfile . For example ---- -$ docker build --no-cache -f spring-boot-ci-image/Dockerfile . +$ docker build --no-cache -f ci-image/Dockerfile . ---- To test run: ---- -$ docker run -it --entrypoint /bin/bash ✈ +$ docker run -it --entrypoint /bin/bash ---- diff --git a/ci/images/ci-image-jdk11/Dockerfile b/ci/images/ci-image-jdk11/Dockerfile new file mode 100644 index 000000000000..9eca829ae35e --- /dev/null +++ b/ci/images/ci-image-jdk11/Dockerfile @@ -0,0 +1,12 @@ +FROM ubuntu:focal-20220316 + +ADD setup.sh /setup.sh +ADD get-jdk-url.sh /get-jdk-url.sh +ADD get-docker-url.sh /get-docker-url.sh +RUN ./setup.sh java11 + +ENV JAVA_HOME /opt/openjdk +ENV PATH $JAVA_HOME/bin:$PATH +ADD docker-lib.sh /docker-lib.sh + +ENTRYPOINT [ "switch", "shell=/bin/bash", "--", "codep", "/bin/docker daemon" ] diff --git a/ci/images/ci-image-jdk17/Dockerfile b/ci/images/ci-image-jdk17/Dockerfile new file mode 100644 index 000000000000..52d1de8b3d64 --- /dev/null +++ b/ci/images/ci-image-jdk17/Dockerfile @@ -0,0 +1,12 @@ +FROM ubuntu:focal-20220316 + +ADD setup.sh /setup.sh +ADD get-jdk-url.sh /get-jdk-url.sh +ADD get-docker-url.sh /get-docker-url.sh +RUN ./setup.sh java8 java17 + +ENV JAVA_HOME /opt/openjdk +ENV PATH $JAVA_HOME/bin:$PATH +ADD docker-lib.sh /docker-lib.sh + +ENTRYPOINT [ "switch", "shell=/bin/bash", "--", "codep", "/bin/docker daemon" ] diff --git a/ci/images/ci-image-jdk18/Dockerfile b/ci/images/ci-image-jdk18/Dockerfile new file mode 100644 index 000000000000..96366af97e8a --- /dev/null +++ b/ci/images/ci-image-jdk18/Dockerfile @@ -0,0 +1,12 @@ +FROM ubuntu:focal-20220316 + +ADD setup.sh /setup.sh +ADD get-jdk-url.sh /get-jdk-url.sh +ADD get-docker-url.sh /get-docker-url.sh +RUN ./setup.sh java8 java18 + +ENV JAVA_HOME /opt/openjdk +ENV PATH $JAVA_HOME/bin:$PATH +ADD docker-lib.sh /docker-lib.sh + +ENTRYPOINT [ "switch", "shell=/bin/bash", "--", "codep", "/bin/docker daemon" ] diff --git a/ci/images/ci-image/Dockerfile b/ci/images/ci-image/Dockerfile new file mode 100644 index 000000000000..a425bf3bf1f0 --- /dev/null +++ b/ci/images/ci-image/Dockerfile @@ -0,0 +1,15 @@ +FROM ubuntu:focal-20220316 + +ADD setup.sh /setup.sh +ADD get-jdk-url.sh /get-jdk-url.sh +ADD get-docker-url.sh /get-docker-url.sh +RUN ./setup.sh java8 + +ENV JAVA_HOME /opt/openjdk +ENV PATH $JAVA_HOME/bin:$PATH +ADD docker-lib.sh /docker-lib.sh + +ADD build-release-scripts.sh /build-release-scripts.sh +ADD releasescripts /release-scripts +RUN ./build-release-scripts.sh +ENTRYPOINT [ "switch", "shell=/bin/bash", "--", "codep", "/bin/docker daemon" ] diff --git a/ci/images/docker-lib.sh b/ci/images/docker-lib.sh index bcc0f33a013b..eac9c2eee9a4 100644 --- a/ci/images/docker-lib.sh +++ b/ci/images/docker-lib.sh @@ -68,12 +68,8 @@ start_docker() { server_args="${server_args}" - for registry in $3; do - server_args="${server_args} --insecure-registry ${registry}" - done - - if [ -n "$4" ]; then - server_args="${server_args} --registry-mirror $4" + if [ -n "$1" ]; then + server_args="${server_args} --registry-mirror https://$1" fi try_start() { diff --git a/ci/images/get-docker-url.sh b/ci/images/get-docker-url.sh new file mode 100755 index 000000000000..fae9032a9e7f --- /dev/null +++ b/ci/images/get-docker-url.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -e + +version="20.10.14" +echo "https://download.docker.com/linux/static/stable/x86_64/docker-$version.tgz"; diff --git a/ci/images/get-jdk-url.sh b/ci/images/get-jdk-url.sh index 7ea427f33df0..4d11c3418e7c 100755 --- a/ci/images/get-jdk-url.sh +++ b/ci/images/get-jdk-url.sh @@ -3,13 +3,16 @@ set -e case "$1" in java8) - echo "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u232-b09/OpenJDK8U-jdk_x64_linux_hotspot_8u232b09.tar.gz" + echo "https://github.com/adoptium/temurin8-binaries/releases/download/jdk8u322-b06/OpenJDK8U-jdk_x64_linux_hotspot_8u322b06.tar.gz" ;; java11) - echo "https://github.com/AdoptOpenJDK/openjdk11-binaries/releases/download/jdk-11.0.5%2B10/OpenJDK11U-jdk_x64_linux_hotspot_11.0.5_10.tar.gz" + echo "https://github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.14.1%2B1/OpenJDK11U-jdk_x64_linux_hotspot_11.0.14.1_1.tar.gz" ;; - java13) - echo "https://github.com/AdoptOpenJDK/openjdk13-binaries/releases/download/jdk-13.0.1%2B9/OpenJDK13U-jdk_x64_linux_hotspot_13.0.1_9.tar.gz" + java17) + echo "https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.2%2B8/OpenJDK17U-jdk_x64_linux_hotspot_17.0.2_8.tar.gz" + ;; + java18) + echo "https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18%2B36/OpenJDK18U-jdk_x64_linux_hotspot_18_36.tar.gz" ;; *) echo $"Unknown java version" diff --git a/ci/images/releasescripts/pom.xml b/ci/images/releasescripts/pom.xml index 05bd6dde8e93..746f0593f72f 100644 --- a/ci/images/releasescripts/pom.xml +++ b/ci/images/releasescripts/pom.xml @@ -1,25 +1,29 @@ - + 4.0.0 org.springframework.boot spring-boot-starter-parent - 2.2.0.RELEASE - + 2.2.4.RELEASE + io.spring.concourse.releasescripts release-scripts 0.0.1-SNAPSHOT releasescripts Utility that can be used when releasing Java projects - 1.8 - 0.0.15 + 0.0.26 - + + org.bouncycastle + bcpg-jdk15to18 + 1.68 + org.springframework.boot spring-boot-starter @@ -60,7 +64,6 @@ - spring-boot-release-scripts @@ -86,5 +89,4 @@ - - + \ No newline at end of file diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/Application.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/Application.java index 7e6495b96c25..9a4c7cb9d46d 100644 --- a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/Application.java +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/Application.java @@ -1,5 +1,5 @@ /* -* Copyright 2012-2019 the original author or authors. +* Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,8 +18,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; @SpringBootApplication +@ConfigurationPropertiesScan public class Application { public static void main(String[] args) { diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/artifactory/ArtifactoryService.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/artifactory/ArtifactoryService.java index 370f345f79f9..2bb2cd7603ed 100644 --- a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/artifactory/ArtifactoryService.java +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/artifactory/ArtifactoryService.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,10 +20,10 @@ import io.spring.concourse.releasescripts.ReleaseInfo; import io.spring.concourse.releasescripts.artifactory.payload.BuildInfoResponse; -import io.spring.concourse.releasescripts.artifactory.payload.DistributionRequest; +import io.spring.concourse.releasescripts.artifactory.payload.BuildInfoResponse.Status; import io.spring.concourse.releasescripts.artifactory.payload.PromotionRequest; -import io.spring.concourse.releasescripts.bintray.BintrayService; -import io.spring.concourse.releasescripts.system.ConsoleLogger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.http.MediaType; @@ -42,25 +42,19 @@ @Component public class ArtifactoryService { + private static final Logger logger = LoggerFactory.getLogger(ArtifactoryService.class); + private static final String ARTIFACTORY_URL = "https://repo.spring.io"; private static final String PROMOTION_URL = ARTIFACTORY_URL + "/api/build/promote/"; private static final String BUILD_INFO_URL = ARTIFACTORY_URL + "/api/build/"; - private static final String DISTRIBUTION_URL = ARTIFACTORY_URL + "/api/build/distribute/"; - private static final String STAGING_REPO = "libs-staging-local"; private final RestTemplate restTemplate; - private final BintrayService bintrayService; - - private static final ConsoleLogger console = new ConsoleLogger(); - - public ArtifactoryService(RestTemplateBuilder builder, ArtifactoryProperties artifactoryProperties, - BintrayService bintrayService) { - this.bintrayService = bintrayService; + public ArtifactoryService(RestTemplateBuilder builder, ArtifactoryProperties artifactoryProperties) { String username = artifactoryProperties.getUsername(); String password = artifactoryProperties.getPassword(); if (StringUtils.hasLength(username)) { @@ -78,20 +72,21 @@ public void promote(String targetRepo, ReleaseInfo releaseInfo) { PromotionRequest request = getPromotionRequest(targetRepo); String buildName = releaseInfo.getBuildName(); String buildNumber = releaseInfo.getBuildNumber(); - console.log("Promoting " + buildName + "/" + buildNumber + " to " + request.getTargetRepo()); + logger.info("Promoting " + buildName + "/" + buildNumber + " to " + request.getTargetRepo()); RequestEntity requestEntity = RequestEntity .post(URI.create(PROMOTION_URL + buildName + "/" + buildNumber)).contentType(MediaType.APPLICATION_JSON) .body(request); try { this.restTemplate.exchange(requestEntity, String.class); + logger.debug("Promotion complete"); } catch (HttpClientErrorException ex) { boolean isAlreadyPromoted = isAlreadyPromoted(buildName, buildNumber, request.getTargetRepo()); if (isAlreadyPromoted) { - console.log("Already promoted."); + logger.info("Already promoted."); } else { - console.log("Promotion failed."); + logger.info("Promotion failed."); throw ex; } } @@ -99,38 +94,24 @@ public void promote(String targetRepo, ReleaseInfo releaseInfo) { private boolean isAlreadyPromoted(String buildName, String buildNumber, String targetRepo) { try { + logger.debug("Checking if already promoted"); ResponseEntity entity = this.restTemplate .getForEntity(BUILD_INFO_URL + buildName + "/" + buildNumber, BuildInfoResponse.class); - BuildInfoResponse.Status status = entity.getBody().getBuildInfo().getStatuses()[0]; + Status[] statuses = entity.getBody().getBuildInfo().getStatuses(); + BuildInfoResponse.Status status = (statuses != null) ? statuses[0] : null; + if (status == null) { + logger.debug("Returned no status object"); + return false; + } + logger.debug("Returned repository " + status.getRepository() + " expecting " + targetRepo); return status.getRepository().equals(targetRepo); } catch (HttpClientErrorException ex) { + logger.debug("Client error, assuming not promoted"); return false; } } - /** - * Deploy builds from Artifactory to Bintray. - * @param sourceRepo the source repo in Artifactory. - */ - public void distribute(String sourceRepo, ReleaseInfo releaseInfo) { - DistributionRequest request = new DistributionRequest(new String[] { sourceRepo }); - RequestEntity requestEntity = RequestEntity - .post(URI.create(DISTRIBUTION_URL + releaseInfo.getBuildName() + "/" + releaseInfo.getBuildNumber())) - .contentType(MediaType.APPLICATION_JSON).body(request); - try { - this.restTemplate.exchange(requestEntity, Object.class); - } - catch (HttpClientErrorException ex) { - console.log("Failed to distribute."); - throw ex; - } - if (!this.bintrayService.isDistributionComplete(releaseInfo)) { - throw new DistributionTimeoutException("Distribution timed out."); - } - - } - private PromotionRequest getPromotionRequest(String targetRepo) { return new PromotionRequest("staged", STAGING_REPO, targetRepo); } diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/artifactory/payload/BuildInfoResponse.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/artifactory/payload/BuildInfoResponse.java index 1ec9ee57e923..149d2c56fd5d 100644 --- a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/artifactory/payload/BuildInfoResponse.java +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/artifactory/payload/BuildInfoResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,12 @@ package io.spring.concourse.releasescripts.artifactory.payload; +import java.util.Arrays; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + /** * Represents the response from Artifactory's buildInfo endpoint. * @@ -54,7 +60,7 @@ public void setStatuses(Status[] statuses) { } public String getName() { - return name; + return this.name; } public void setName(String name) { @@ -83,6 +89,14 @@ public String getVersion() { public void setVersion(String version) { this.version = version; + + } + + public Set getArtifactDigests(Predicate predicate) { + return Arrays.stream(this.modules).flatMap((module) -> { + Artifact[] artifacts = module.getArtifacts(); + return (artifacts != null) ? Arrays.stream(artifacts) : Stream.empty(); + }).filter(predicate).map(Artifact::getSha256).collect(Collectors.toSet()); } } @@ -105,6 +119,8 @@ public static class Module { private String id; + private Artifact[] artifacts; + public String getId() { return this.id; } @@ -113,6 +129,38 @@ public void setId(String id) { this.id = id; } + public Artifact[] getArtifacts() { + return this.artifacts; + } + + public void setArtifacts(Artifact[] artifacts) { + this.artifacts = artifacts; + } + + } + + public static class Artifact { + + private String name; + + private String sha256; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public String getSha256() { + return this.sha256; + } + + public void setSha256(String sha256) { + this.sha256 = sha256; + } + } } diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/bintray/BintrayProperties.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/bintray/BintrayProperties.java deleted file mode 100644 index 7612ff77761f..000000000000 --- a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/bintray/BintrayProperties.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.spring.concourse.releasescripts.bintray; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * {@link ConfigurationProperties @ConfigurationProperties} for the Bintray API. - * - * @author Madhura Bhave - */ -@ConfigurationProperties(prefix = "bintray") -public class BintrayProperties { - - private String username; - - private String apiKey; - - private String repo; - - private String subject; - - public String getUsername() { - return this.username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getApiKey() { - return this.apiKey; - } - - public void setApiKey(String apiKey) { - this.apiKey = apiKey; - } - - public String getRepo() { - return this.repo; - } - - public void setRepo(String repo) { - this.repo = repo; - } - - public String getSubject() { - return this.subject; - } - - public void setSubject(String subject) { - this.subject = subject; - } - -} diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/bintray/BintrayService.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/bintray/BintrayService.java deleted file mode 100644 index 2cc1fe779ed2..000000000000 --- a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/bintray/BintrayService.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.spring.concourse.releasescripts.bintray; - -import java.net.URI; -import java.util.Objects; -import java.util.concurrent.TimeUnit; - -import io.spring.concourse.releasescripts.ReleaseInfo; -import io.spring.concourse.releasescripts.sonatype.SonatypeProperties; -import io.spring.concourse.releasescripts.sonatype.SonatypeService; -import io.spring.concourse.releasescripts.system.ConsoleLogger; -import org.awaitility.core.ConditionTimeoutException; - -import org.springframework.boot.web.client.RestTemplateBuilder; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.RequestEntity; -import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; -import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.client.RestTemplate; - -import static org.awaitility.Awaitility.waitAtMost; - -/** - * Central class for interacting with Bintray's REST API. - * - * @author Madhura Bhave - */ -@Component -public class BintrayService { - - private static final String BINTRAY_URL = "https://api.bintray.com/"; - - private static final String GRADLE_PLUGIN_REQUEST = "[ { \"name\": \"gradle-plugin\", \"values\": [\"org.springframework.boot:org.springframework.boot:spring-boot-gradle-plugin\"] } ]"; - - private final RestTemplate restTemplate; - - private final BintrayProperties bintrayProperties; - - private final SonatypeProperties sonatypeProperties; - - private final SonatypeService sonatypeService; - - private static final ConsoleLogger console = new ConsoleLogger(); - - public BintrayService(RestTemplateBuilder builder, BintrayProperties bintrayProperties, - SonatypeProperties sonatypeProperties, SonatypeService sonatypeService) { - this.bintrayProperties = bintrayProperties; - this.sonatypeProperties = sonatypeProperties; - this.sonatypeService = sonatypeService; - String username = bintrayProperties.getUsername(); - String apiKey = bintrayProperties.getApiKey(); - if (StringUtils.hasLength(username)) { - builder = builder.basicAuthentication(username, apiKey); - } - this.restTemplate = builder.build(); - } - - public boolean isDistributionComplete(ReleaseInfo releaseInfo) { - RequestEntity allFilesRequest = getRequest(releaseInfo, 1); - Object[] allFiles = waitAtMost(5, TimeUnit.MINUTES).with().pollDelay(20, TimeUnit.SECONDS).until(() -> { - try { - return this.restTemplate.exchange(allFilesRequest, Object[].class).getBody(); - } - catch (HttpClientErrorException ex) { - if (ex.getStatusCode() != HttpStatus.NOT_FOUND) { - throw ex; - } - return null; - } - }, Objects::nonNull); - RequestEntity publishedFilesRequest = getRequest(releaseInfo, 0); - try { - waitAtMost(40, TimeUnit.MINUTES).with().pollDelay(20, TimeUnit.SECONDS).until(() -> { - Object[] publishedFiles = this.restTemplate.exchange(publishedFilesRequest, Object[].class).getBody(); - return allFiles.length == publishedFiles.length; - }); - } - catch (ConditionTimeoutException ex) { - return false; - } - return true; - } - - private RequestEntity getRequest(ReleaseInfo releaseInfo, int includeUnpublished) { - return RequestEntity.get(URI.create(BINTRAY_URL + "packages/" + this.bintrayProperties.getSubject() + "/" - + this.bintrayProperties.getRepo() + "/" + releaseInfo.getGroupId() + "/versions/" - + releaseInfo.getVersion() + "/files?include_unpublished=" + includeUnpublished)).build(); - } - - /** - * Add attributes to Spring Boot's Gradle plugin. - * @param releaseInfo the release information - */ - public void publishGradlePlugin(ReleaseInfo releaseInfo) { - RequestEntity requestEntity = RequestEntity - .post(URI.create(BINTRAY_URL + "packages/" + this.bintrayProperties.getSubject() + "/" - + this.bintrayProperties.getRepo() + "/" + releaseInfo.getGroupId() + "/versions/" - + releaseInfo.getVersion() + "/attributes")) - .contentType(MediaType.APPLICATION_JSON).body(GRADLE_PLUGIN_REQUEST); - try { - this.restTemplate.exchange(requestEntity, Object.class); - } - catch (HttpClientErrorException ex) { - console.log("Failed to add attribute to gradle plugin."); - throw ex; - } - } - - /** - * Sync artifacts from Bintray to Maven Central. - * @param releaseInfo the release information - */ - public void syncToMavenCentral(ReleaseInfo releaseInfo) { - console.log("Calling Bintray to sync to Sonatype"); - if (this.sonatypeService.artifactsPublished(releaseInfo)) { - return; - } - RequestEntity requestEntity = RequestEntity - .post(URI.create(String.format(BINTRAY_URL + "maven_central_sync/%s/%s/%s/versions/%s", - this.bintrayProperties.getSubject(), this.bintrayProperties.getRepo(), releaseInfo.getGroupId(), - releaseInfo.getVersion()))) - .contentType(MediaType.APPLICATION_JSON).body(this.sonatypeProperties); - try { - this.restTemplate.exchange(requestEntity, Object.class); - } - catch (HttpClientErrorException ex) { - console.log("Failed to sync."); - throw ex; - } - } - -} diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/CommandProcessor.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/CommandProcessor.java index 6b7de70dff36..50a0b2132a7e 100644 --- a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/CommandProcessor.java +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/CommandProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,9 @@ import java.util.Collections; import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; @@ -32,6 +35,8 @@ @Component public class CommandProcessor implements ApplicationRunner { + private static final Logger logger = LoggerFactory.getLogger(CommandProcessor.class); + private final List commands; public CommandProcessor(List commands) { @@ -40,11 +45,14 @@ public CommandProcessor(List commands) { @Override public void run(ApplicationArguments args) throws Exception { + logger.debug("Running command processor"); List nonOptionArgs = args.getNonOptionArgs(); Assert.state(!nonOptionArgs.isEmpty(), "No command argument specified"); String request = nonOptionArgs.get(0); - this.commands.stream().filter((c) -> c.getName().equals(request)).findFirst() - .orElseThrow(() -> new IllegalStateException("Unknown command '" + request + "'")).run(args); + Command command = this.commands.stream().filter((candidate) -> candidate.getName().equals(request)).findFirst() + .orElseThrow(() -> new IllegalStateException("Unknown command '" + request + "'")); + logger.debug("Found command " + command.getClass().getName()); + command.run(args); } } diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/DistributeCommand.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/DistributeCommand.java deleted file mode 100644 index 3d9e97ee94ad..000000000000 --- a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/DistributeCommand.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.spring.concourse.releasescripts.command; - -import java.io.File; -import java.nio.file.Files; -import java.util.List; - -import com.fasterxml.jackson.databind.ObjectMapper; -import io.spring.concourse.releasescripts.ReleaseInfo; -import io.spring.concourse.releasescripts.ReleaseType; -import io.spring.concourse.releasescripts.artifactory.ArtifactoryService; -import io.spring.concourse.releasescripts.artifactory.payload.BuildInfoResponse; - -import org.springframework.boot.ApplicationArguments; -import org.springframework.stereotype.Component; -import org.springframework.util.Assert; - -/** - * Command used to deploy builds from Artifactory to Bintray. - * - * @author Madhura Bhave - */ -@Component -public class DistributeCommand implements Command { - - private final ArtifactoryService service; - - private final ObjectMapper objectMapper; - - public DistributeCommand(ArtifactoryService service, ObjectMapper objectMapper) { - this.service = service; - this.objectMapper = objectMapper; - } - - @Override - public void run(ApplicationArguments args) throws Exception { - List nonOptionArgs = args.getNonOptionArgs(); - Assert.state(!nonOptionArgs.isEmpty(), "No command argument specified"); - Assert.state(nonOptionArgs.size() == 3, "Release type or build info not specified"); - String releaseType = nonOptionArgs.get(1); - ReleaseType type = ReleaseType.from(releaseType); - if (!ReleaseType.RELEASE.equals(type)) { - return; - } - String buildInfoLocation = nonOptionArgs.get(2); - byte[] content = Files.readAllBytes(new File(buildInfoLocation).toPath()); - BuildInfoResponse buildInfoResponse = this.objectMapper.readValue(content, BuildInfoResponse.class); - ReleaseInfo releaseInfo = ReleaseInfo.from(buildInfoResponse.getBuildInfo()); - this.service.distribute(type.getRepo(), releaseInfo); - } - -} diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/PromoteCommand.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/PromoteCommand.java index 230059d88f1f..dc8f80ceba62 100644 --- a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/PromoteCommand.java +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/PromoteCommand.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,8 @@ import io.spring.concourse.releasescripts.ReleaseType; import io.spring.concourse.releasescripts.artifactory.ArtifactoryService; import io.spring.concourse.releasescripts.artifactory.payload.BuildInfoResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.boot.ApplicationArguments; import org.springframework.stereotype.Component; @@ -38,6 +40,8 @@ @Component public class PromoteCommand implements Command { + private static final Logger logger = LoggerFactory.getLogger(PromoteCommand.class); + private final ArtifactoryService service; private final ObjectMapper objectMapper; @@ -49,6 +53,7 @@ public PromoteCommand(ArtifactoryService service, ObjectMapper objectMapper) { @Override public void run(ApplicationArguments args) throws Exception { + logger.debug("Running 'promote' command"); List nonOptionArgs = args.getNonOptionArgs(); Assert.state(!nonOptionArgs.isEmpty(), "No command argument specified"); Assert.state(nonOptionArgs.size() == 3, "Release type or build info location not specified"); diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/PublishGradlePlugin.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/PublishGradlePlugin.java deleted file mode 100644 index 68af1dce26c8..000000000000 --- a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/PublishGradlePlugin.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.spring.concourse.releasescripts.command; - -import java.io.File; -import java.nio.file.Files; -import java.util.List; - -import com.fasterxml.jackson.databind.ObjectMapper; -import io.spring.concourse.releasescripts.ReleaseInfo; -import io.spring.concourse.releasescripts.ReleaseType; -import io.spring.concourse.releasescripts.artifactory.payload.BuildInfoResponse; -import io.spring.concourse.releasescripts.bintray.BintrayService; - -import org.springframework.boot.ApplicationArguments; -import org.springframework.stereotype.Component; -import org.springframework.util.Assert; - -/** - * Command used to add attributes to the gradle plugin. - * - * @author Madhura Bhave - */ -@Component -public class PublishGradlePlugin implements Command { - - private static final String PUBLISH_GRADLE_PLUGIN_COMMAND = "publishGradlePlugin"; - - private final BintrayService service; - - private final ObjectMapper objectMapper; - - public PublishGradlePlugin(BintrayService service, ObjectMapper objectMapper) { - this.service = service; - this.objectMapper = objectMapper; - } - - @Override - public String getName() { - return PUBLISH_GRADLE_PLUGIN_COMMAND; - } - - @Override - public void run(ApplicationArguments args) throws Exception { - List nonOptionArgs = args.getNonOptionArgs(); - Assert.state(!nonOptionArgs.isEmpty(), "No command argument specified"); - Assert.state(nonOptionArgs.size() == 3, "Release type or build info not specified"); - String releaseType = nonOptionArgs.get(1); - ReleaseType type = ReleaseType.from(releaseType); - if (!ReleaseType.RELEASE.equals(type)) { - return; - } - String buildInfoLocation = nonOptionArgs.get(2); - byte[] content = Files.readAllBytes(new File(buildInfoLocation).toPath()); - BuildInfoResponse buildInfoResponse = this.objectMapper.readValue(content, BuildInfoResponse.class); - ReleaseInfo releaseInfo = ReleaseInfo.from(buildInfoResponse.getBuildInfo()); - this.service.publishGradlePlugin(releaseInfo); - } - -} diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/PublishToCentralCommand.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/PublishToCentralCommand.java new file mode 100644 index 000000000000..2dccc1876e48 --- /dev/null +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/PublishToCentralCommand.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.concourse.releasescripts.command; + +import java.io.File; +import java.nio.file.Files; +import java.util.List; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.spring.concourse.releasescripts.ReleaseInfo; +import io.spring.concourse.releasescripts.ReleaseType; +import io.spring.concourse.releasescripts.artifactory.payload.BuildInfoResponse; +import io.spring.concourse.releasescripts.artifactory.payload.BuildInfoResponse.BuildInfo; +import io.spring.concourse.releasescripts.sonatype.SonatypeService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.boot.ApplicationArguments; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +/** + * Command used to publish a release to Maven Central. + * + * @author Andy Wilkinson + */ +@Component +public class PublishToCentralCommand implements Command { + + private static final Logger logger = LoggerFactory.getLogger(PublishToCentralCommand.class); + + private final SonatypeService sonatype; + + private final ObjectMapper objectMapper; + + public PublishToCentralCommand(SonatypeService sonatype, ObjectMapper objectMapper) { + this.sonatype = sonatype; + this.objectMapper = objectMapper; + } + + @Override + public String getName() { + return "publishToCentral"; + } + + @Override + public void run(ApplicationArguments args) throws Exception { + List nonOptionArgs = args.getNonOptionArgs(); + Assert.state(nonOptionArgs.size() == 4, + "Release type, build info location, or artifacts location not specified"); + String releaseType = nonOptionArgs.get(1); + ReleaseType type = ReleaseType.from(releaseType); + if (!ReleaseType.RELEASE.equals(type)) { + return; + } + String buildInfoLocation = nonOptionArgs.get(2); + logger.debug("Loading build-info from " + buildInfoLocation); + byte[] content = Files.readAllBytes(new File(buildInfoLocation).toPath()); + BuildInfoResponse buildInfoResponse = this.objectMapper.readValue(content, BuildInfoResponse.class); + BuildInfo buildInfo = buildInfoResponse.getBuildInfo(); + ReleaseInfo releaseInfo = ReleaseInfo.from(buildInfo); + String artifactsLocation = nonOptionArgs.get(3); + this.sonatype.publish(releaseInfo, new File(artifactsLocation).toPath()); + } + +} diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/PublishToSdkmanCommand.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/PublishToSdkmanCommand.java new file mode 100644 index 000000000000..1938610dc0cd --- /dev/null +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/PublishToSdkmanCommand.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.concourse.releasescripts.command; + +import java.util.List; + +import io.spring.concourse.releasescripts.ReleaseType; +import io.spring.concourse.releasescripts.sdkman.SdkmanService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.boot.ApplicationArguments; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +/** + * Command used to publish to SDKMAN. + * + * @author Madhura Bhave + */ +@Component +public class PublishToSdkmanCommand implements Command { + + private static final Logger logger = LoggerFactory.getLogger(PublishToSdkmanCommand.class); + + private static final String PUBLISH_TO_SDKMAN_COMMAND = "publishToSdkman"; + + private final SdkmanService service; + + public PublishToSdkmanCommand(SdkmanService service) { + this.service = service; + } + + @Override + public String getName() { + return PUBLISH_TO_SDKMAN_COMMAND; + } + + @Override + public void run(ApplicationArguments args) throws Exception { + logger.debug("Running 'push to SDKMAN' command"); + List nonOptionArgs = args.getNonOptionArgs(); + Assert.state(!nonOptionArgs.isEmpty(), "No command argument specified"); + Assert.state(nonOptionArgs.size() >= 3, "Release type or version not specified"); + String releaseType = nonOptionArgs.get(1); + ReleaseType type = ReleaseType.from(releaseType); + if (!ReleaseType.RELEASE.equals(type)) { + return; + } + String version = nonOptionArgs.get(2); + boolean makeDefault = false; + if (nonOptionArgs.size() == 4) { + makeDefault = Boolean.parseBoolean(nonOptionArgs.get(3)); + } + this.service.publish(version, makeDefault); + } + +} diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/SyncToCentralCommand.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/SyncToCentralCommand.java deleted file mode 100644 index 23c20482eb60..000000000000 --- a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/SyncToCentralCommand.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.spring.concourse.releasescripts.command; - -import java.io.File; -import java.nio.file.Files; -import java.util.List; - -import com.fasterxml.jackson.databind.ObjectMapper; -import io.spring.concourse.releasescripts.ReleaseInfo; -import io.spring.concourse.releasescripts.ReleaseType; -import io.spring.concourse.releasescripts.artifactory.payload.BuildInfoResponse; -import io.spring.concourse.releasescripts.bintray.BintrayService; - -import org.springframework.boot.ApplicationArguments; -import org.springframework.stereotype.Component; -import org.springframework.util.Assert; - -/** - * Command used to sync artifacts to Maven Central. - * - * @author Madhura Bhave - */ -@Component -public class SyncToCentralCommand implements Command { - - private static final String SYNC_TO_CENTRAL_COMMAND = "syncToCentral"; - - private final BintrayService service; - - private final ObjectMapper objectMapper; - - public SyncToCentralCommand(BintrayService service, ObjectMapper objectMapper) { - this.service = service; - this.objectMapper = objectMapper; - } - - @Override - public String getName() { - return SYNC_TO_CENTRAL_COMMAND; - } - - @Override - public void run(ApplicationArguments args) throws Exception { - List nonOptionArgs = args.getNonOptionArgs(); - Assert.state(!nonOptionArgs.isEmpty(), "No command argument specified"); - Assert.state(nonOptionArgs.size() == 3, "Release type or build info not specified"); - String releaseType = nonOptionArgs.get(1); - ReleaseType type = ReleaseType.from(releaseType); - if (!ReleaseType.RELEASE.equals(type)) { - return; - } - String buildInfoLocation = nonOptionArgs.get(2); - byte[] content = Files.readAllBytes(new File(buildInfoLocation).toPath()); - BuildInfoResponse buildInfoResponse = this.objectMapper.readValue(content, BuildInfoResponse.class); - ReleaseInfo releaseInfo = ReleaseInfo.from(buildInfoResponse.getBuildInfo()); - this.service.syncToMavenCentral(releaseInfo); - } - -} diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sdkman/SdkmanProperties.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sdkman/SdkmanProperties.java new file mode 100644 index 000000000000..575d3cf1b703 --- /dev/null +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sdkman/SdkmanProperties.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.concourse.releasescripts.sdkman; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * {@link ConfigurationProperties @ConfigurationProperties} for SDKMAN. + * + * @author Madhura Bhave + */ +@ConfigurationProperties(prefix = "sdkman") +public class SdkmanProperties { + + private String consumerKey; + + private String consumerToken; + + public String getConsumerKey() { + return this.consumerKey; + } + + public void setConsumerKey(String consumerKey) { + this.consumerKey = consumerKey; + } + + public String getConsumerToken() { + return this.consumerToken; + } + + public void setConsumerToken(String consumerToken) { + this.consumerToken = consumerToken; + } + +} diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sdkman/SdkmanService.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sdkman/SdkmanService.java new file mode 100644 index 000000000000..4064d7af134d --- /dev/null +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sdkman/SdkmanService.java @@ -0,0 +1,148 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.concourse.releasescripts.sdkman; + +import java.net.URI; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.MediaType; +import org.springframework.http.RequestEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +/** + * Central class for interacting with SDKMAN's API. + * + * @author Madhura Bhave + */ +@Component +public class SdkmanService { + + private static final Logger logger = LoggerFactory.getLogger(SdkmanService.class); + + private static final String SDKMAN_URL = "https://vendors.sdkman.io/"; + + private static final String DOWNLOAD_URL = "https://repo.spring.io/simple/libs-release-local/org/springframework/boot/spring-boot-cli/" + + "%s/spring-boot-cli-%s-bin.zip"; + + private static final String SPRING_BOOT = "springboot"; + + private final RestTemplate restTemplate; + + private final SdkmanProperties properties; + + private final String CONSUMER_KEY_HEADER = "Consumer-Key"; + + private final String CONSUMER_TOKEN_HEADER = "Consumer-Token"; + + public SdkmanService(RestTemplateBuilder builder, SdkmanProperties properties) { + this.restTemplate = builder.build(); + this.properties = properties; + } + + public void publish(String version, boolean makeDefault) { + release(version); + if (makeDefault) { + makeDefault(version); + } + broadcast(version); + } + + private void broadcast(String version) { + BroadcastRequest broadcastRequest = new BroadcastRequest(version); + RequestEntity broadcastEntity = RequestEntity.post(URI.create(SDKMAN_URL + "announce/struct")) + .header(CONSUMER_KEY_HEADER, this.properties.getConsumerKey()) + .header(CONSUMER_TOKEN_HEADER, this.properties.getConsumerToken()) + .contentType(MediaType.APPLICATION_JSON).body(broadcastRequest); + this.restTemplate.exchange(broadcastEntity, String.class); + logger.debug("Broadcast complete"); + } + + private void makeDefault(String version) { + logger.debug("Making this version the default"); + Request request = new Request(version); + RequestEntity requestEntity = RequestEntity.put(URI.create(SDKMAN_URL + "default")) + .header(CONSUMER_KEY_HEADER, this.properties.getConsumerKey()) + .header(CONSUMER_TOKEN_HEADER, this.properties.getConsumerToken()) + .contentType(MediaType.APPLICATION_JSON).body(request); + this.restTemplate.exchange(requestEntity, String.class); + logger.debug("Make default complete"); + } + + private void release(String version) { + ReleaseRequest releaseRequest = new ReleaseRequest(version, String.format(DOWNLOAD_URL, version, version)); + RequestEntity releaseEntity = RequestEntity.post(URI.create(SDKMAN_URL + "release")) + .header(CONSUMER_KEY_HEADER, this.properties.getConsumerKey()) + .header(CONSUMER_TOKEN_HEADER, this.properties.getConsumerToken()) + .contentType(MediaType.APPLICATION_JSON).body(releaseRequest); + this.restTemplate.exchange(releaseEntity, String.class); + logger.debug("Release complete"); + } + + static class Request { + + private final String candidate = SPRING_BOOT; + + private final String version; + + Request(String version) { + this.version = version; + } + + public String getCandidate() { + return this.candidate; + } + + public String getVersion() { + return this.version; + } + + } + + static class ReleaseRequest extends Request { + + private final String url; + + ReleaseRequest(String version, String url) { + super(version); + this.url = url; + } + + public String getUrl() { + return this.url; + } + + } + + static class BroadcastRequest extends Request { + + private final String hashtag = SPRING_BOOT; + + BroadcastRequest(String version) { + super(version); + } + + public String getHashtag() { + return this.hashtag; + } + + } + +} diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sonatype/ArtifactCollector.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sonatype/ArtifactCollector.java new file mode 100644 index 000000000000..04ab265f2511 --- /dev/null +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sonatype/ArtifactCollector.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.concourse.releasescripts.sonatype; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.core.io.PathResource; + +/** + * Collects artifacts to be deployed. + * + * @author Andy Wilkinson + */ +class ArtifactCollector { + + private final Predicate excludeFilter; + + ArtifactCollector(List exclude) { + this.excludeFilter = excludeFilter(exclude); + } + + private Predicate excludeFilter(List exclude) { + Predicate patternFilter = exclude.stream().map(Pattern::compile).map(Pattern::asPredicate) + .reduce((path) -> false, Predicate::or).negate(); + return (path) -> patternFilter.test(path.toString()); + } + + Collection collectArtifacts(Path root) { + try (Stream artifacts = Files.walk(root)) { + return artifacts.filter(Files::isRegularFile).filter(this.excludeFilter) + .map((artifact) -> deployableArtifact(artifact, root)).collect(Collectors.toList()); + } + catch (IOException ex) { + throw new RuntimeException("Could not read artifacts from '" + root + "'"); + } + } + + private DeployableArtifact deployableArtifact(Path artifact, Path root) { + return new DeployableArtifact(new PathResource(artifact), root.relativize(artifact).toString()); + } + +} diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sonatype/DeployableArtifact.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sonatype/DeployableArtifact.java new file mode 100644 index 000000000000..45bfa9c3d242 --- /dev/null +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sonatype/DeployableArtifact.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.concourse.releasescripts.sonatype; + +import org.springframework.core.io.Resource; + +/** + * An artifact that can be deployed. + * + * @author Andy Wilkinson + */ +class DeployableArtifact { + + private final Resource resource; + + private final String path; + + DeployableArtifact(Resource resource, String path) { + this.resource = resource; + this.path = path; + } + + Resource getResource() { + return this.resource; + } + + String getPath() { + return this.path; + } + +} \ No newline at end of file diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sonatype/SonatypeProperties.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sonatype/SonatypeProperties.java index 165bcfea381c..4f9d0a4c5409 100644 --- a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sonatype/SonatypeProperties.java +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sonatype/SonatypeProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,10 @@ package io.spring.concourse.releasescripts.sonatype; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + import com.fasterxml.jackson.annotation.JsonProperty; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -34,6 +38,32 @@ public class SonatypeProperties { @JsonProperty("password") private String passwordToken; + /** + * URL of the Nexus instance used to publish releases. + */ + private String url; + + /** + * ID of the staging profile used to publish releases. + */ + private String stagingProfileId; + + /** + * Time between requests made to determine if the closing of a staging repository has + * completed. + */ + private Duration pollingInterval = Duration.ofSeconds(15); + + /** + * Number of threads used to upload artifacts to the staging repository. + */ + private int uploadThreads = 8; + + /** + * Regular expression patterns of artifacts to exclude + */ + private List exclude = new ArrayList<>(); + public String getUserToken() { return this.userToken; } @@ -50,4 +80,44 @@ public void setPasswordToken(String passwordToken) { this.passwordToken = passwordToken; } + public String getUrl() { + return this.url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getStagingProfileId() { + return this.stagingProfileId; + } + + public void setStagingProfileId(String stagingProfileId) { + this.stagingProfileId = stagingProfileId; + } + + public Duration getPollingInterval() { + return this.pollingInterval; + } + + public void setPollingInterval(Duration pollingInterval) { + this.pollingInterval = pollingInterval; + } + + public int getUploadThreads() { + return this.uploadThreads; + } + + public void setUploadThreads(int uploadThreads) { + this.uploadThreads = uploadThreads; + } + + public List getExclude() { + return this.exclude; + } + + public void setExclude(List exclude) { + this.exclude = exclude; + } + } diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sonatype/SonatypeService.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sonatype/SonatypeService.java index ce2ff317d2ac..da92b0c207b8 100644 --- a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sonatype/SonatypeService.java +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sonatype/SonatypeService.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,30 @@ package io.spring.concourse.releasescripts.sonatype; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonCreator.Mode; +import com.fasterxml.jackson.annotation.JsonProperty; import io.spring.concourse.releasescripts.ReleaseInfo; -import io.spring.concourse.releasescripts.system.ConsoleLogger; +import org.apache.logging.log4j.util.Strings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.http.HttpStatus; @@ -31,15 +53,26 @@ * Central class for interacting with Sonatype. * * @author Madhura Bhave + * @author Andy Wilkinson */ @Component public class SonatypeService { - private static final String SONATYPE_REPOSITORY_URI = "https://oss.sonatype.org/service/local/repositories/releases/content/org/springframework/boot/spring-boot/"; + private static final Logger logger = LoggerFactory.getLogger(SonatypeService.class); + + private static final String NEXUS_REPOSITORY_PATH = "/service/local/repositories/releases/content/org/springframework/boot/spring-boot/"; + + private static final String NEXUS_STAGING_PATH = "/service/local/staging/"; + + private final ArtifactCollector artifactCollector; private final RestTemplate restTemplate; - private static final ConsoleLogger console = new ConsoleLogger(); + private final String stagingProfileId; + + private final Duration pollingInterval; + + private final int threads; public SonatypeService(RestTemplateBuilder builder, SonatypeProperties sonatypeProperties) { String username = sonatypeProperties.getUserToken(); @@ -47,21 +80,45 @@ public SonatypeService(RestTemplateBuilder builder, SonatypeProperties sonatypeP if (StringUtils.hasLength(username)) { builder = builder.basicAuthentication(username, password); } - this.restTemplate = builder.build(); + this.restTemplate = builder.rootUri(sonatypeProperties.getUrl()).build(); + this.stagingProfileId = sonatypeProperties.getStagingProfileId(); + this.pollingInterval = sonatypeProperties.getPollingInterval(); + this.threads = sonatypeProperties.getUploadThreads(); + + this.artifactCollector = new ArtifactCollector(sonatypeProperties.getExclude()); } /** - * Checks if artifacts are already published to Maven Central. - * @return true if artifacts are published + * Publishes the release by creating a staging repository and deploying to it the + * artifacts at the given {@code artifactsRoot}. The repository is then closed and, + * upon successfully closure, it is released. * @param releaseInfo the release information + * @param artifactsRoot the root directory of the artifacts to stage */ - public boolean artifactsPublished(ReleaseInfo releaseInfo) { + public void publish(ReleaseInfo releaseInfo, Path artifactsRoot) { + if (artifactsPublished(releaseInfo)) { + return; + } + logger.info("Creating staging repository"); + String buildId = releaseInfo.getBuildNumber(); + String repositoryId = createStagingRepository(buildId); + Collection artifacts = this.artifactCollector.collectArtifacts(artifactsRoot); + logger.info("Staging repository {} created. Deploying {} artifacts", repositoryId, artifacts.size()); + deploy(artifacts, repositoryId); + logger.info("Deploy complete. Closing staging repository"); + close(repositoryId); + logger.info("Staging repository closed"); + release(repositoryId, buildId); + logger.info("Staging repository released"); + } + + private boolean artifactsPublished(ReleaseInfo releaseInfo) { try { - ResponseEntity entity = this.restTemplate - .getForEntity(String.format(SONATYPE_REPOSITORY_URI + "%s/spring-boot-%s.jar.sha1", - releaseInfo.getVersion(), releaseInfo.getVersion()), Object.class); + ResponseEntity entity = this.restTemplate + .getForEntity(String.format(NEXUS_REPOSITORY_PATH + "%s/spring-boot-%s.jar.sha1", + releaseInfo.getVersion(), releaseInfo.getVersion()), byte[].class); if (HttpStatus.OK.equals(entity.getStatusCode())) { - console.log("Already published to Sonatype."); + logger.info("Already published to Sonatype."); return true; } } @@ -71,4 +128,179 @@ public boolean artifactsPublished(ReleaseInfo releaseInfo) { return false; } + private String createStagingRepository(String buildId) { + Map body = new HashMap<>(); + body.put("data", Collections.singletonMap("description", buildId)); + PromoteResponse response = this.restTemplate.postForObject( + String.format(NEXUS_STAGING_PATH + "profiles/%s/start", this.stagingProfileId), body, + PromoteResponse.class); + String repositoryId = response.data.stagedRepositoryId; + return repositoryId; + } + + private void deploy(Collection artifacts, String repositoryId) { + ExecutorService executor = Executors.newFixedThreadPool(this.threads); + try { + CompletableFuture.allOf(artifacts.stream() + .map((artifact) -> CompletableFuture.runAsync(() -> deploy(artifact, repositoryId), executor)) + .toArray(CompletableFuture[]::new)).get(60, TimeUnit.MINUTES); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted during artifact deploy"); + } + catch (ExecutionException ex) { + throw new RuntimeException("Deploy failed", ex); + } + catch (TimeoutException ex) { + throw new RuntimeException("Deploy timed out", ex); + } + finally { + executor.shutdown(); + } + } + + private void deploy(DeployableArtifact deployableArtifact, String repositoryId) { + try { + this.restTemplate.put( + NEXUS_STAGING_PATH + "deployByRepositoryId/" + repositoryId + "/" + deployableArtifact.getPath(), + deployableArtifact.getResource()); + logger.info("Deloyed {}", deployableArtifact.getPath()); + } + catch (HttpClientErrorException ex) { + logger.error("Failed to deploy {}. Error response: {}", deployableArtifact.getPath(), + ex.getResponseBodyAsString()); + throw ex; + } + } + + private void close(String stagedRepositoryId) { + Map body = new HashMap<>(); + body.put("data", Collections.singletonMap("stagedRepositoryId", stagedRepositoryId)); + this.restTemplate.postForEntity(String.format(NEXUS_STAGING_PATH + "profiles/%s/finish", this.stagingProfileId), + body, Void.class); + logger.info("Close requested. Awaiting result"); + while (true) { + StagingRepository repository = this.restTemplate + .getForObject(NEXUS_STAGING_PATH + "repository/" + stagedRepositoryId, StagingRepository.class); + if (!repository.transitioning) { + if ("open".equals(repository.type)) { + logFailures(stagedRepositoryId); + throw new RuntimeException("Close failed"); + } + return; + } + try { + Thread.sleep(this.pollingInterval.toMillis()); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted while waiting for staging repository to close", ex); + } + } + } + + private void logFailures(String stagedRepositoryId) { + try { + StagingRepositoryActivity[] activities = this.restTemplate.getForObject( + NEXUS_STAGING_PATH + "repository/" + stagedRepositoryId + "/activity", + StagingRepositoryActivity[].class); + List failureMessages = Stream.of(activities).flatMap((activity) -> activity.events.stream()) + .filter((event) -> event.severity > 0).flatMap((event) -> event.properties.stream()) + .filter((property) -> "failureMessage".equals(property.name)) + .map((property) -> " " + property.value).collect(Collectors.toList()); + if (failureMessages.isEmpty()) { + logger.error("Close failed for unknown reasons"); + } + logger.error("Close failed:\n{}", Strings.join(failureMessages, '\n')); + } + catch (Exception ex) { + logger.error("Failed to determine causes of close failure", ex); + } + } + + private void release(String stagedRepositoryId, String buildId) { + Map data = new HashMap<>(); + data.put("stagedRepositoryIds", Arrays.asList(stagedRepositoryId)); + data.put("description", "Releasing " + buildId); + data.put("autoDropAfterRelease", true); + Map body = Collections.singletonMap("data", data); + this.restTemplate.postForEntity(NEXUS_STAGING_PATH + "bulk/promote", body, Void.class); + } + + private static final class PromoteResponse { + + private final Data data; + + @JsonCreator(mode = Mode.PROPERTIES) + private PromoteResponse(@JsonProperty("data") Data data) { + this.data = data; + } + + private static final class Data { + + private final String stagedRepositoryId; + + @JsonCreator(mode = Mode.PROPERTIES) + Data(@JsonProperty("stagedRepositoryId") String stagedRepositoryId) { + this.stagedRepositoryId = stagedRepositoryId; + } + + } + + } + + private static final class StagingRepository { + + private final String type; + + private final boolean transitioning; + + private StagingRepository(String type, boolean transitioning) { + this.type = type; + this.transitioning = transitioning; + } + + } + + private static final class StagingRepositoryActivity { + + private final List events; + + @JsonCreator + private StagingRepositoryActivity(@JsonProperty("events") List events) { + this.events = events; + } + + private static class Event { + + private final List properties; + + private final int severity; + + @JsonCreator + public Event(@JsonProperty("name") String name, @JsonProperty("properties") List properties, + @JsonProperty("severity") int severity) { + this.properties = properties; + this.severity = severity; + } + + private static class Property { + + private final String name; + + private final String value; + + @JsonCreator + private Property(@JsonProperty("name") String name, @JsonProperty("value") String value) { + this.name = name; + this.value = value; + } + + } + + } + + } + } diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/system/ConsoleLogger.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/system/ConsoleLogger.java deleted file mode 100644 index 1b8e6d29fa7b..000000000000 --- a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/system/ConsoleLogger.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.spring.concourse.releasescripts.system; - -import org.slf4j.helpers.MessageFormatter; - -/** - * Simple console logger used to output progress messages. - * - * @author Madhura Bhave - */ -public class ConsoleLogger { - - public void log(String message, Object... args) { - System.err.println(MessageFormatter.arrayFormat(message, args).getMessage()); - } - -} diff --git a/ci/images/releasescripts/src/main/resources/application.properties b/ci/images/releasescripts/src/main/resources/application.properties index 8b137891791f..56dfa61a1909 100644 --- a/ci/images/releasescripts/src/main/resources/application.properties +++ b/ci/images/releasescripts/src/main/resources/application.properties @@ -1 +1,4 @@ - +spring.main.banner-mode=off +sonatype.exclude[0]=build-info\\.json +sonatype.exclude[1]=org/springframework/boot/spring-boot-docs/.* +logging.level.io.spring.concourse=DEBUG \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/artifactory/ArtifactoryServiceTests.java b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/artifactory/ArtifactoryServiceTests.java index d0b8a2b34db7..19ab95e12556 100644 --- a/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/artifactory/ArtifactoryServiceTests.java +++ b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/artifactory/ArtifactoryServiceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,14 +17,12 @@ package io.spring.concourse.releasescripts.artifactory; import io.spring.concourse.releasescripts.ReleaseInfo; -import io.spring.concourse.releasescripts.bintray.BintrayService; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.autoconfigure.web.client.RestClientTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.core.io.ClassPathResource; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -35,10 +33,6 @@ import org.springframework.web.client.HttpClientErrorException; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; import static org.springframework.test.web.client.match.MockRestRequestMatchers.content; import static org.springframework.test.web.client.match.MockRestRequestMatchers.header; import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; @@ -58,9 +52,6 @@ class ArtifactoryServiceTests { @Autowired private ArtifactoryService service; - @MockBean - private BintrayService bintrayService; - @Autowired private ArtifactoryProperties properties; @@ -74,9 +65,7 @@ void tearDown() { @Test void promoteWhenSuccessful() { - this.server - .expect(requestTo( - "https://repo.spring.io/api/build/promote/" + "example-build" + "/" + "example-build-1")) + this.server.expect(requestTo("https://repo.spring.io/api/build/promote/example-build/example-build-1")) .andExpect(method(HttpMethod.POST)) .andExpect(content().json( "{\"status\": \"staged\", \"sourceRepo\": \"libs-staging-local\", \"targetRepo\": \"libs-milestone-local\"}")) @@ -89,11 +78,9 @@ void promoteWhenSuccessful() { @Test void promoteWhenArtifactsAlreadyPromoted() { - this.server - .expect(requestTo( - "https://repo.spring.io/api/build/promote/" + "example-build" + "/" + "example-build-1")) + this.server.expect(requestTo("https://repo.spring.io/api/build/promote/example-build/example-build-1")) .andRespond(withStatus(HttpStatus.CONFLICT)); - this.server.expect(requestTo("https://repo.spring.io/api/build/" + "example-build" + "/" + "example-build-1")) + this.server.expect(requestTo("https://repo.spring.io/api/build/example-build/example-build-1")) .andRespond(withJsonFrom("build-info-response.json")); this.service.promote("libs-release-local", getReleaseInfo()); this.server.verify(); @@ -101,11 +88,9 @@ void promoteWhenArtifactsAlreadyPromoted() { @Test void promoteWhenCheckForArtifactsAlreadyPromotedFails() { - this.server - .expect(requestTo( - "https://repo.spring.io/api/build/promote/" + "example-build" + "/" + "example-build-1")) + this.server.expect(requestTo("https://repo.spring.io/api/build/promote/example-build/example-build-1")) .andRespond(withStatus(HttpStatus.CONFLICT)); - this.server.expect(requestTo("https://repo.spring.io/api/build/" + "example-build" + "/" + "example-build-1")) + this.server.expect(requestTo("https://repo.spring.io/api/build/example-build/example-build-1")) .andRespond(withStatus(HttpStatus.FORBIDDEN)); assertThatExceptionOfType(HttpClientErrorException.class) .isThrownBy(() -> this.service.promote("libs-release-local", getReleaseInfo())); @@ -113,72 +98,25 @@ void promoteWhenCheckForArtifactsAlreadyPromotedFails() { } @Test - void promoteWhenPromotionFails() { - this.server - .expect(requestTo( - "https://repo.spring.io/api/build/promote/" + "example-build" + "/" + "example-build-1")) + void promoteWhenCheckForArtifactsAlreadyPromotedReturnsNoStatus() { + this.server.expect(requestTo("https://repo.spring.io/api/build/promote/example-build/example-build-1")) .andRespond(withStatus(HttpStatus.CONFLICT)); - this.server.expect(requestTo("https://repo.spring.io/api/build/" + "example-build" + "/" + "example-build-1")) - .andRespond(withJsonFrom("staged-build-info-response.json")); + this.server.expect(requestTo("https://repo.spring.io/api/build/example-build/example-build-1")) + .andRespond(withJsonFrom("no-status-build-info-response.json")); assertThatExceptionOfType(HttpClientErrorException.class) - .isThrownBy(() -> this.service.promote("libs-release-local", getReleaseInfo())); - this.server.verify(); - } - - @Test - void distributeWhenSuccessful() throws Exception { - ReleaseInfo releaseInfo = getReleaseInfo(); - given(this.bintrayService.isDistributionComplete(releaseInfo)).willReturn(true); - this.server - .expect(requestTo( - "https://repo.spring.io/api/build/distribute/" + "example-build" + "/" + "example-build-1")) - .andExpect(method(HttpMethod.POST)) - .andExpect(content().json( - "{\"sourceRepos\": [\"libs-release-local\"], \"targetRepo\" : \"spring-distributions\", \"async\":\"true\"}")) - .andExpect(header("Authorization", "Basic " + Base64Utils.encodeToString(String - .format("%s:%s", this.properties.getUsername(), this.properties.getPassword()).getBytes()))) - .andExpect(header("Content-Type", MediaType.APPLICATION_JSON.toString())).andRespond(withSuccess()); - this.service.distribute("libs-release-local", releaseInfo); + .isThrownBy(() -> this.service.promote("libs-milestone-local", getReleaseInfo())); this.server.verify(); - verify(this.bintrayService, times(1)).isDistributionComplete(releaseInfo); } @Test - void distributeWhenFailure() throws Exception { - ReleaseInfo releaseInfo = getReleaseInfo(); - this.server - .expect(requestTo( - "https://repo.spring.io/api/build/distribute/" + "example-build" + "/" + "example-build-1")) - .andExpect(method(HttpMethod.POST)) - .andExpect(content().json( - "{\"sourceRepos\": [\"libs-release-local\"], \"targetRepo\" : \"spring-distributions\", \"async\":\"true\"}")) - .andExpect(header("Authorization", "Basic " + Base64Utils.encodeToString(String - .format("%s:%s", this.properties.getUsername(), this.properties.getPassword()).getBytes()))) - .andExpect(header("Content-Type", MediaType.APPLICATION_JSON.toString())) - .andRespond(withStatus(HttpStatus.FORBIDDEN)); + void promoteWhenPromotionFails() { + this.server.expect(requestTo("https://repo.spring.io/api/build/promote/example-build/example-build-1")) + .andRespond(withStatus(HttpStatus.CONFLICT)); + this.server.expect(requestTo("https://repo.spring.io/api/build/example-build/example-build-1")) + .andRespond(withJsonFrom("staged-build-info-response.json")); assertThatExceptionOfType(HttpClientErrorException.class) - .isThrownBy(() -> this.service.distribute("libs-release-local", releaseInfo)); - this.server.verify(); - verifyNoInteractions(this.bintrayService); - } - - @Test - void distributeWhenGettingPackagesTimesOut() throws Exception { - ReleaseInfo releaseInfo = getReleaseInfo(); - given(this.bintrayService.isDistributionComplete(releaseInfo)).willReturn(false); - this.server - .expect(requestTo( - "https://repo.spring.io/api/build/distribute/" + "example-build" + "/" + "example-build-1")) - .andExpect(method(HttpMethod.POST)) - .andExpect(content().json( - "{\"sourceRepos\": [\"libs-release-local\"], \"targetRepo\" : \"spring-distributions\", \"async\":\"true\"}")) - .andExpect(header("Authorization", "Basic " + Base64Utils.encodeToString(String - .format("%s:%s", this.properties.getUsername(), this.properties.getPassword()).getBytes()))) - .andExpect(header("Content-Type", MediaType.APPLICATION_JSON.toString())).andRespond(withSuccess()); - assertThatExceptionOfType(DistributionTimeoutException.class) - .isThrownBy(() -> this.service.distribute("libs-release-local", releaseInfo)); + .isThrownBy(() -> this.service.promote("libs-release-local", getReleaseInfo())); this.server.verify(); - verify(this.bintrayService, times(1)).isDistributionComplete(releaseInfo); } private ReleaseInfo getReleaseInfo() { diff --git a/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/bintray/BintrayServiceTests.java b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/bintray/BintrayServiceTests.java deleted file mode 100644 index e622ed3adb45..000000000000 --- a/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/bintray/BintrayServiceTests.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.spring.concourse.releasescripts.bintray; - -import io.spring.concourse.releasescripts.ReleaseInfo; -import io.spring.concourse.releasescripts.sonatype.SonatypeProperties; -import io.spring.concourse.releasescripts.sonatype.SonatypeService; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.test.autoconfigure.web.client.RestClientTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.core.io.ClassPathResource; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.test.web.client.ExpectedCount; -import org.springframework.test.web.client.MockRestServiceServer; -import org.springframework.test.web.client.response.DefaultResponseCreator; -import org.springframework.util.Base64Utils; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.springframework.test.web.client.match.MockRestRequestMatchers.content; -import static org.springframework.test.web.client.match.MockRestRequestMatchers.header; -import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; -import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; -import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus; -import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; - -/** - * Tests for {@link BintrayService}. - * - * @author Madhura Bhave - */ -@RestClientTest(BintrayService.class) -@EnableConfigurationProperties({ BintrayProperties.class, SonatypeProperties.class }) -class BintrayServiceTests { - - @Autowired - private BintrayService service; - - @Autowired - private BintrayProperties properties; - - @Autowired - private SonatypeProperties sonatypeProperties; - - @MockBean - private SonatypeService sonatypeService; - - @Autowired - private MockRestServiceServer server; - - @AfterEach - void tearDown() { - this.server.reset(); - } - - @Test - void isDistributionComplete() throws Exception { - this.server - .expect(requestTo(String.format( - "https://api.bintray.com/packages/%s/%s/%s/versions/%s/files?include_unpublished=%s", - this.properties.getSubject(), this.properties.getRepo(), "example", "1.1.0.RELEASE", 1))) - .andRespond(withStatus(HttpStatus.NOT_FOUND)); - setupGetPackageFiles(1, "all-package-files.json"); - setupGetPackageFiles(0, "published-files.json"); - setupGetPackageFiles(0, "all-package-files.json"); - assertThat(this.service.isDistributionComplete(getReleaseInfo())).isTrue(); - this.server.verify(); - } - - private void setupGetPackageFiles(int includeUnpublished, String path) { - this.server - .expect(requestTo(String.format( - "https://api.bintray.com/packages/%s/%s/%s/versions/%s/files?include_unpublished=%s", - this.properties.getSubject(), this.properties.getRepo(), "example", "1.1.0.RELEASE", - includeUnpublished))) - .andExpect(method(HttpMethod.GET)) - .andExpect(header("Authorization", "Basic " + Base64Utils.encodeToString( - String.format("%s:%s", this.properties.getUsername(), this.properties.getApiKey()).getBytes()))) - .andRespond(withJsonFrom(path)); - } - - @Test - void publishGradlePluginWhenSuccessful() { - this.server - .expect(requestTo(String.format("https://api.bintray.com/packages/%s/%s/%s/versions/%s/attributes", - this.properties.getSubject(), this.properties.getRepo(), "example", "1.1.0.RELEASE"))) - .andExpect(method(HttpMethod.POST)) - .andExpect(content().json( - "[ { \"name\": \"gradle-plugin\", \"values\": [\"org.springframework.boot:org.springframework.boot:spring-boot-gradle-plugin\"] } ]")) - .andExpect(header("Authorization", "Basic " + Base64Utils.encodeToString( - String.format("%s:%s", this.properties.getUsername(), this.properties.getApiKey()).getBytes()))) - .andExpect(header("Content-Type", MediaType.APPLICATION_JSON.toString())).andRespond(withSuccess()); - this.service.publishGradlePlugin(getReleaseInfo()); - this.server.verify(); - } - - @Test - void syncToMavenCentralWhenSuccessful() { - ReleaseInfo releaseInfo = getReleaseInfo(); - given(this.sonatypeService.artifactsPublished(releaseInfo)).willReturn(false); - this.server - .expect(requestTo(String.format("https://api.bintray.com/maven_central_sync/%s/%s/%s/versions/%s", - this.properties.getSubject(), this.properties.getRepo(), "example", "1.1.0.RELEASE"))) - .andExpect(method(HttpMethod.POST)) - .andExpect(content().json(String.format("{\"username\": \"%s\", \"password\": \"%s\"}", - this.sonatypeProperties.getUserToken(), this.sonatypeProperties.getPasswordToken()))) - .andExpect(header("Authorization", "Basic " + Base64Utils.encodeToString( - String.format("%s:%s", this.properties.getUsername(), this.properties.getApiKey()).getBytes()))) - .andExpect(header("Content-Type", MediaType.APPLICATION_JSON.toString())).andRespond(withSuccess()); - this.service.syncToMavenCentral(releaseInfo); - this.server.verify(); - } - - @Test - void syncToMavenCentralWhenArtifactsAlreadyPublished() { - ReleaseInfo releaseInfo = getReleaseInfo(); - given(this.sonatypeService.artifactsPublished(releaseInfo)).willReturn(true); - this.server.expect(ExpectedCount.never(), - requestTo(String.format("https://api.bintray.com/maven_central_sync/%s/%s/%s/versions/%s", - this.properties.getSubject(), this.properties.getRepo(), "example", "1.1.0.RELEASE"))); - this.service.syncToMavenCentral(releaseInfo); - this.server.verify(); - } - - private ReleaseInfo getReleaseInfo() { - ReleaseInfo releaseInfo = new ReleaseInfo(); - releaseInfo.setBuildName("example-build"); - releaseInfo.setBuildNumber("example-build-1"); - releaseInfo.setGroupId("example"); - releaseInfo.setVersion("1.1.0.RELEASE"); - return releaseInfo; - } - - private DefaultResponseCreator withJsonFrom(String path) { - return withSuccess(getClassPathResource(path), MediaType.APPLICATION_JSON); - } - - private ClassPathResource getClassPathResource(String path) { - return new ClassPathResource(path, getClass()); - } - -} \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/CommandProcessorTests.java b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/CommandProcessorTests.java deleted file mode 100644 index c21953c559b4..000000000000 --- a/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/CommandProcessorTests.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.spring.concourse.releasescripts.command; - -import java.util.Arrays; -import java.util.Collections; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.DefaultApplicationArguments; - -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; - -/** - * Tests for {@link CommandProcessor}. - * - * @author Madhura Bhave - */ -class CommandProcessorTests { - - private static final String[] NO_ARGS = {}; - - @Test - void runWhenNoArgumentThrowsException() { - CommandProcessor processor = new CommandProcessor(Collections.singletonList(mock(Command.class))); - assertThatIllegalStateException().isThrownBy(() -> processor.run(new DefaultApplicationArguments(NO_ARGS))) - .withMessage("No command argument specified"); - } - - @Test - void runWhenUnknownCommandThrowsException() { - Command fooCommand = mock(Command.class); - given(fooCommand.getName()).willReturn("foo"); - CommandProcessor processor = new CommandProcessor(Collections.singletonList(fooCommand)); - DefaultApplicationArguments args = new DefaultApplicationArguments(new String[] { "bar", "go" }); - assertThatIllegalStateException().isThrownBy(() -> processor.run(args)).withMessage("Unknown command 'bar'"); - } - - @Test - void runDelegatesToCommand() throws Exception { - Command fooCommand = mock(Command.class); - given(fooCommand.getName()).willReturn("foo"); - Command barCommand = mock(Command.class); - given(barCommand.getName()).willReturn("bar"); - CommandProcessor processor = new CommandProcessor(Arrays.asList(fooCommand, barCommand)); - DefaultApplicationArguments args = new DefaultApplicationArguments(new String[] { "bar", "go" }); - processor.run(args); - verify(fooCommand, never()).run(any()); - verify(barCommand).run(args); - } - -} \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/DistributeCommandTests.java b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/DistributeCommandTests.java deleted file mode 100644 index c8949f224099..000000000000 --- a/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/DistributeCommandTests.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.spring.concourse.releasescripts.command; - -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.spring.concourse.releasescripts.ReleaseInfo; -import io.spring.concourse.releasescripts.ReleaseType; -import io.spring.concourse.releasescripts.artifactory.ArtifactoryService; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import org.springframework.boot.DefaultApplicationArguments; -import org.springframework.core.io.ClassPathResource; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; - -/** - * Tests for {@link DistributeCommand}. - * - * @author Madhura Bhave - */ -class DistributeCommandTests { - - @Mock - private ArtifactoryService service; - - private DistributeCommand command; - - private ObjectMapper objectMapper; - - @BeforeEach - void setup() { - MockitoAnnotations.initMocks(this); - this.objectMapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - this.command = new DistributeCommand(this.service, objectMapper); - } - - @Test - void distributeWhenReleaseTypeNotSpecifiedShouldThrowException() { - Assertions.assertThatIllegalStateException() - .isThrownBy(() -> this.command.run(new DefaultApplicationArguments("distribute"))); - } - - @Test - void distributeWhenReleaseTypeMilestoneShouldDoNothing() throws Exception { - this.command.run(new DefaultApplicationArguments("distribute", "M", getBuildInfoLocation())); - verifyNoInteractions(this.service); - } - - @Test - void distributeWhenReleaseTypeRCShouldDoNothing() throws Exception { - this.command.run(new DefaultApplicationArguments("distribute", "RC", getBuildInfoLocation())); - verifyNoInteractions(this.service); - } - - @Test - void distributeWhenReleaseTypeReleaseShouldCallService() throws Exception { - ArgumentCaptor captor = ArgumentCaptor.forClass(ReleaseInfo.class); - this.command.run(new DefaultApplicationArguments("distribute", "RELEASE", getBuildInfoLocation())); - verify(this.service).distribute(eq(ReleaseType.RELEASE.getRepo()), captor.capture()); - ReleaseInfo releaseInfo = captor.getValue(); - assertThat(releaseInfo.getBuildName()).isEqualTo("example"); - assertThat(releaseInfo.getBuildNumber()).isEqualTo("example-build-1"); - assertThat(releaseInfo.getGroupId()).isEqualTo("org.example.demo"); - assertThat(releaseInfo.getVersion()).isEqualTo("2.2.0"); - } - - private String getBuildInfoLocation() throws Exception { - return new ClassPathResource("build-info-response.json", ArtifactoryService.class).getFile().getAbsolutePath(); - } - -} \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/PromoteCommandTests.java b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/PromoteCommandTests.java deleted file mode 100644 index f1c776094a7a..000000000000 --- a/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/PromoteCommandTests.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.spring.concourse.releasescripts.command; - -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.spring.concourse.releasescripts.ReleaseInfo; -import io.spring.concourse.releasescripts.ReleaseType; -import io.spring.concourse.releasescripts.artifactory.ArtifactoryService; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import org.springframework.boot.DefaultApplicationArguments; -import org.springframework.core.io.ClassPathResource; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.verify; - -/** - * @author Madhura Bhave - */ -class PromoteCommandTests { - - @Mock - private ArtifactoryService service; - - private PromoteCommand command; - - private ObjectMapper objectMapper; - - @BeforeEach - void setup() { - MockitoAnnotations.initMocks(this); - this.objectMapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - this.command = new PromoteCommand(this.service, this.objectMapper); - } - - @Test - void runWhenReleaseTypeNotSpecifiedShouldThrowException() { - assertThatIllegalStateException() - .isThrownBy(() -> this.command.run(new DefaultApplicationArguments("promote"))); - } - - @Test - void runWhenReleaseTypeMilestoneShouldCallService() throws Exception { - this.command.run(new DefaultApplicationArguments("promote", "M", getBuildInfoLocation())); - verify(this.service).promote(eq(ReleaseType.MILESTONE.getRepo()), any(ReleaseInfo.class)); - } - - @Test - void runWhenReleaseTypeRCShouldCallService() throws Exception { - this.command.run(new DefaultApplicationArguments("promote", "RC", getBuildInfoLocation())); - verify(this.service).promote(eq(ReleaseType.RELEASE_CANDIDATE.getRepo()), any(ReleaseInfo.class)); - } - - @Test - void runWhenReleaseTypeReleaseShouldCallService() throws Exception { - this.command.run(new DefaultApplicationArguments("promote", "RELEASE", getBuildInfoLocation())); - verify(this.service).promote(eq(ReleaseType.RELEASE.getRepo()), any(ReleaseInfo.class)); - } - - @Test - void runWhenBuildInfoNotSpecifiedShouldThrowException() { - assertThatIllegalStateException() - .isThrownBy(() -> this.command.run(new DefaultApplicationArguments("promote", "M"))); - } - - @Test - void runShouldParseBuildInfoProperly() throws Exception { - ArgumentCaptor captor = ArgumentCaptor.forClass(ReleaseInfo.class); - this.command.run(new DefaultApplicationArguments("promote", "RELEASE", getBuildInfoLocation())); - verify(this.service).promote(eq(ReleaseType.RELEASE.getRepo()), captor.capture()); - ReleaseInfo releaseInfo = captor.getValue(); - assertThat(releaseInfo.getBuildName()).isEqualTo("example"); - assertThat(releaseInfo.getBuildNumber()).isEqualTo("example-build-1"); - assertThat(releaseInfo.getGroupId()).isEqualTo("org.example.demo"); - assertThat(releaseInfo.getVersion()).isEqualTo("2.2.0"); - } - - private String getBuildInfoLocation() throws Exception { - return new ClassPathResource("build-info-response.json", ArtifactoryService.class).getFile().getAbsolutePath(); - } - -} \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/PublishGradlePluginTests.java b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/PublishGradlePluginTests.java deleted file mode 100644 index e7ceb1c8b466..000000000000 --- a/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/PublishGradlePluginTests.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.spring.concourse.releasescripts.command; - -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.spring.concourse.releasescripts.ReleaseInfo; -import io.spring.concourse.releasescripts.artifactory.ArtifactoryService; -import io.spring.concourse.releasescripts.bintray.BintrayService; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import org.springframework.boot.DefaultApplicationArguments; -import org.springframework.core.io.ClassPathResource; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; - -/** - * Tests for {@link PublishGradlePlugin}. - * - * @author Madhura Bhave - */ -class PublishGradlePluginTests { - - @Mock - private BintrayService service; - - private PublishGradlePlugin command; - - private ObjectMapper objectMapper; - - @BeforeEach - void setup() { - MockitoAnnotations.initMocks(this); - this.objectMapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - this.command = new PublishGradlePlugin(this.service, objectMapper); - } - - @Test - void runWhenReleaseTypeNotSpecifiedShouldThrowException() throws Exception { - Assertions.assertThatIllegalStateException() - .isThrownBy(() -> this.command.run(new DefaultApplicationArguments("publishGradlePlugin"))); - } - - @Test - void runWhenReleaseTypeMilestoneShouldDoNothing() throws Exception { - this.command.run(new DefaultApplicationArguments("publishGradlePlugin", "M", getBuildInfoLocation())); - verifyNoInteractions(this.service); - } - - @Test - void runWhenReleaseTypeRCShouldDoNothing() throws Exception { - this.command.run(new DefaultApplicationArguments("publishGradlePlugin", "RC", getBuildInfoLocation())); - verifyNoInteractions(this.service); - } - - @Test - void runWhenReleaseTypeReleaseShouldCallService() throws Exception { - ArgumentCaptor captor = ArgumentCaptor.forClass(ReleaseInfo.class); - this.command.run(new DefaultApplicationArguments("promote", "RELEASE", getBuildInfoLocation())); - verify(this.service).publishGradlePlugin(captor.capture()); - ReleaseInfo releaseInfo = captor.getValue(); - assertThat(releaseInfo.getBuildName()).isEqualTo("example"); - assertThat(releaseInfo.getBuildNumber()).isEqualTo("example-build-1"); - assertThat(releaseInfo.getGroupId()).isEqualTo("org.example.demo"); - assertThat(releaseInfo.getVersion()).isEqualTo("2.2.0"); - } - - private String getBuildInfoLocation() throws Exception { - return new ClassPathResource("build-info-response.json", ArtifactoryService.class).getFile().getAbsolutePath(); - } - -} \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/SyncToCentralCommandTests.java b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/SyncToCentralCommandTests.java deleted file mode 100644 index 2f79452c3ac2..000000000000 --- a/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/SyncToCentralCommandTests.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.spring.concourse.releasescripts.command; - -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.spring.concourse.releasescripts.ReleaseInfo; -import io.spring.concourse.releasescripts.artifactory.ArtifactoryService; -import io.spring.concourse.releasescripts.bintray.BintrayService; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import org.springframework.boot.DefaultApplicationArguments; -import org.springframework.core.io.ClassPathResource; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; - -/** - * Tests for {@link SyncToCentralCommand}. - * - * @author Madhura Bhave - */ -class SyncToCentralCommandTests { - - @Mock - private BintrayService service; - - private SyncToCentralCommand command; - - private ObjectMapper objectMapper; - - @BeforeEach - void setup() { - MockitoAnnotations.initMocks(this); - this.objectMapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - this.command = new SyncToCentralCommand(this.service, objectMapper); - } - - @Test - void runWhenReleaseTypeNotSpecifiedShouldThrowException() throws Exception { - Assertions.assertThatIllegalStateException() - .isThrownBy(() -> this.command.run(new DefaultApplicationArguments("syncToCentral"))); - } - - @Test - void runWhenReleaseTypeMilestoneShouldDoNothing() throws Exception { - this.command.run(new DefaultApplicationArguments("syncToCentral", "M", getBuildInfoLocation())); - verifyNoInteractions(this.service); - } - - @Test - void runWhenReleaseTypeRCShouldDoNothing() throws Exception { - this.command.run(new DefaultApplicationArguments("syncToCentral", "RC", getBuildInfoLocation())); - verifyNoInteractions(this.service); - } - - @Test - void runWhenReleaseTypeReleaseShouldCallService() throws Exception { - ArgumentCaptor captor = ArgumentCaptor.forClass(ReleaseInfo.class); - this.command.run(new DefaultApplicationArguments("syncToCentral", "RELEASE", getBuildInfoLocation())); - verify(this.service).syncToMavenCentral(captor.capture()); - ReleaseInfo releaseInfo = captor.getValue(); - assertThat(releaseInfo.getBuildName()).isEqualTo("example"); - assertThat(releaseInfo.getBuildNumber()).isEqualTo("example-build-1"); - assertThat(releaseInfo.getGroupId()).isEqualTo("org.example.demo"); - assertThat(releaseInfo.getVersion()).isEqualTo("2.2.0"); - } - - private String getBuildInfoLocation() throws Exception { - return new ClassPathResource("build-info-response.json", ArtifactoryService.class).getFile().getAbsolutePath(); - } - -} \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/sdkman/SdkmanServiceTests.java b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/sdkman/SdkmanServiceTests.java new file mode 100644 index 000000000000..d7a9e7d4d568 --- /dev/null +++ b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/sdkman/SdkmanServiceTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.concourse.releasescripts.sdkman; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.autoconfigure.web.client.RestClientTest; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.test.web.client.MockRestServiceServer; + +import static org.springframework.test.web.client.match.MockRestRequestMatchers.content; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.header; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +/** + * Tests for {@link SdkmanService}. + * + * @author Madhura Bhave + */ +@EnableConfigurationProperties(SdkmanProperties.class) +@RestClientTest(SdkmanService.class) +class SdkmanServiceTests { + + @Autowired + private SdkmanService service; + + @Autowired + private SdkmanProperties properties; + + @Autowired + private MockRestServiceServer server; + + @AfterEach + void tearDown() { + this.server.reset(); + } + + @Test + void publishWhenMakeDefaultTrue() throws Exception { + setupExpectation("https://vendors.sdkman.io/release", + "{\"candidate\": \"springboot\", \"version\": \"1.2.3\", \"url\": \"https://repo.spring.io/simple/libs-release-local/org/springframework/boot/spring-boot-cli/1.2.3/spring-boot-cli-1.2.3-bin.zip\"}"); + setupExpectation("https://vendors.sdkman.io/default", "{\"candidate\": \"springboot\", \"version\": \"1.2.3\"}", + HttpMethod.PUT); + setupExpectation("https://vendors.sdkman.io/announce/struct", + "{\"candidate\": \"springboot\", \"version\": \"1.2.3\", \"hashtag\": \"springboot\"}"); + this.service.publish("1.2.3", true); + this.server.verify(); + } + + @Test + void publishWhenMakeDefaultFalse() throws Exception { + setupExpectation("https://vendors.sdkman.io/release", + "{\"candidate\": \"springboot\", \"version\": \"1.2.3\", \"url\": \"https://repo.spring.io/simple/libs-release-local/org/springframework/boot/spring-boot-cli/1.2.3/spring-boot-cli-1.2.3-bin.zip\"}"); + setupExpectation("https://vendors.sdkman.io/announce/struct", + "{\"candidate\": \"springboot\", \"version\": \"1.2.3\", \"hashtag\": \"springboot\"}"); + this.service.publish("1.2.3", false); + this.server.verify(); + } + + private void setupExpectation(String url, String body) { + setupExpectation(url, body, HttpMethod.POST); + } + + private void setupExpectation(String url, String body, HttpMethod method) { + this.server.expect(requestTo(url)).andExpect(method(method)).andExpect(content().json(body)) + .andExpect(header("Consumer-Key", "sdkman-consumer-key")) + .andExpect(header("Consumer-Token", "sdkman-consumer-token")) + .andExpect(header("Content-Type", MediaType.APPLICATION_JSON.toString())).andRespond(withSuccess()); + } + +} diff --git a/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/sonatype/SonatypeServiceTests.java b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/sonatype/SonatypeServiceTests.java index ee477feeaffb..6c9133c7c677 100644 --- a/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/sonatype/SonatypeServiceTests.java +++ b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/sonatype/SonatypeServiceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,17 @@ package io.spring.concourse.releasescripts.sonatype; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + import io.spring.concourse.releasescripts.ReleaseInfo; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -23,11 +34,20 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.autoconfigure.web.client.RestClientTest; +import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.test.web.client.ExpectedCount; import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.test.web.client.RequestMatcher; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.hamcrest.Matchers.equalTo; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.header; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.jsonPath; import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus; @@ -38,16 +58,13 @@ * * @author Madhura Bhave */ -@RestClientTest(SonatypeService.class) +@RestClientTest(components = SonatypeService.class, properties = "sonatype.url=https://nexus.example.org") @EnableConfigurationProperties(SonatypeProperties.class) class SonatypeServiceTests { @Autowired private SonatypeService service; - @Autowired - private SonatypeProperties properties; - @Autowired private MockRestServiceServer server; @@ -57,24 +74,119 @@ void tearDown() { } @Test - void artifactsPublishedWhenPublishedShouldReturnTrue() { + void publishWhenAlreadyPublishedShouldNotPublish() { this.server.expect(requestTo(String.format( - "https://oss.sonatype.org/service/local/repositories/releases/content/org/springframework/boot/spring-boot/%s/spring-boot-%s.jar.sha1", - "1.1.0.RELEASE", "1.1.0.RELEASE"))).andExpect(method(HttpMethod.GET)).andRespond(withSuccess()); - boolean published = this.service.artifactsPublished(getReleaseInfo()); - assertThat(published).isTrue(); + "/service/local/repositories/releases/content/org/springframework/boot/spring-boot/%s/spring-boot-%s.jar.sha1", + "1.1.0.RELEASE", "1.1.0.RELEASE"))).andExpect(method(HttpMethod.GET)) + .andRespond(withSuccess().body("ce8d8b6838ecceb68962b9150b18682f4237ccf71".getBytes())); + Path artifactsRoot = new File("src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo") + .toPath(); + this.service.publish(getReleaseInfo(), artifactsRoot); this.server.verify(); } @Test - void artifactsPublishedWhenNotPublishedShouldReturnFalse() { + void publishWithSuccessfulClose() throws IOException { this.server.expect(requestTo(String.format( - "https://oss.sonatype.org/service/local/repositories/releases/content/org/springframework/boot/spring-boot/%s/spring-boot-%s.jar.sha1", + "/service/local/repositories/releases/content/org/springframework/boot/spring-boot/%s/spring-boot-%s.jar.sha1", "1.1.0.RELEASE", "1.1.0.RELEASE"))).andExpect(method(HttpMethod.GET)) .andRespond(withStatus(HttpStatus.NOT_FOUND)); - boolean published = this.service.artifactsPublished(getReleaseInfo()); - assertThat(published).isFalse(); - this.server.verify(); + this.server.expect(requestTo("/service/local/staging/profiles/1a2b3c4d/start")) + .andExpect(method(HttpMethod.POST)).andExpect(header("Content-Type", "application/json")) + .andExpect(header("Accept", "application/json, application/*+json")) + .andExpect(jsonPath("$.data.description").value("example-build-1")) + .andRespond(withStatus(HttpStatus.CREATED).contentType(MediaType.APPLICATION_JSON).body( + "{\"data\":{\"stagedRepositoryId\":\"example-6789\", \"description\":\"example-build\"}}")); + Path artifactsRoot = new File("src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo") + .toPath(); + try (Stream artifacts = Files.walk(artifactsRoot)) { + Set uploads = artifacts.filter(Files::isRegularFile) + .map((artifact) -> artifactsRoot.relativize(artifact)) + .filter((artifact) -> !artifact.startsWith("build-info.json")) + .map((artifact) -> requestTo( + "/service/local/staging/deployByRepositoryId/example-6789/" + artifact.toString())) + .collect(Collectors.toCollection(HashSet::new)); + AnyOfRequestMatcher uploadRequestsMatcher = anyOf(uploads); + assertThat(uploadRequestsMatcher.candidates).hasSize(150); + this.server.expect(ExpectedCount.times(150), uploadRequestsMatcher).andExpect(method(HttpMethod.PUT)) + .andRespond(withSuccess()); + this.server.expect(requestTo("/service/local/staging/profiles/1a2b3c4d/finish")) + .andExpect(method(HttpMethod.POST)).andExpect(header("Content-Type", "application/json")) + .andExpect(header("Accept", "application/json, application/*+json")) + .andRespond(withStatus(HttpStatus.CREATED)); + this.server.expect(ExpectedCount.times(2), requestTo("/service/local/staging/repository/example-6789")) + .andExpect(method(HttpMethod.GET)) + .andExpect(header("Accept", "application/json, application/*+json")) + .andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON) + .body("{\"type\":\"open\", \"transitioning\":true}")); + this.server.expect(requestTo("/service/local/staging/repository/example-6789")) + .andExpect(method(HttpMethod.GET)) + .andExpect(header("Accept", "application/json, application/*+json")) + .andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON) + .body("{\"type\":\"closed\", \"transitioning\":false}")); + this.server.expect(requestTo("/service/local/staging/bulk/promote")).andExpect(method(HttpMethod.POST)) + .andExpect(header("Content-Type", "application/json")) + .andExpect(header("Accept", "application/json, application/*+json")) + .andExpect(jsonPath("$.data.description").value("Releasing example-build-1")) + .andExpect(jsonPath("$.data.autoDropAfterRelease").value(true)) + .andExpect(jsonPath("$.data.stagedRepositoryIds").value(equalTo(Arrays.asList("example-6789")))) + .andRespond(withSuccess()); + this.service.publish(getReleaseInfo(), artifactsRoot); + this.server.verify(); + assertThat(uploadRequestsMatcher.candidates).hasSize(0); + } + } + + @Test + void publishWithCloseFailureDueToRuleViolations() throws IOException { + this.server.expect(requestTo(String.format( + "/service/local/repositories/releases/content/org/springframework/boot/spring-boot/%s/spring-boot-%s.jar.sha1", + "1.1.0.RELEASE", "1.1.0.RELEASE"))).andExpect(method(HttpMethod.GET)) + .andRespond(withStatus(HttpStatus.NOT_FOUND)); + this.server.expect(requestTo("/service/local/staging/profiles/1a2b3c4d/start")) + .andExpect(method(HttpMethod.POST)).andExpect(header("Content-Type", "application/json")) + .andExpect(header("Accept", "application/json, application/*+json")) + .andExpect(jsonPath("$.data.description").value("example-build-1")) + .andRespond(withStatus(HttpStatus.CREATED).contentType(MediaType.APPLICATION_JSON).body( + "{\"data\":{\"stagedRepositoryId\":\"example-6789\", \"description\":\"example-build\"}}")); + Path artifactsRoot = new File("src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo") + .toPath(); + try (Stream artifacts = Files.walk(artifactsRoot)) { + Set uploads = artifacts.filter(Files::isRegularFile) + .map((artifact) -> artifactsRoot.relativize(artifact)) + .filter((artifact) -> !"build-info.json".equals(artifact.toString())) + .map((artifact) -> requestTo( + "/service/local/staging/deployByRepositoryId/example-6789/" + artifact.toString())) + .collect(Collectors.toCollection(HashSet::new)); + AnyOfRequestMatcher uploadRequestsMatcher = anyOf(uploads); + assertThat(uploadRequestsMatcher.candidates).hasSize(150); + this.server.expect(ExpectedCount.times(150), uploadRequestsMatcher).andExpect(method(HttpMethod.PUT)) + .andRespond(withSuccess()); + this.server.expect(requestTo("/service/local/staging/profiles/1a2b3c4d/finish")) + .andExpect(method(HttpMethod.POST)).andExpect(header("Content-Type", "application/json")) + .andExpect(header("Accept", "application/json, application/*+json")) + .andRespond(withStatus(HttpStatus.CREATED)); + this.server.expect(ExpectedCount.times(2), requestTo("/service/local/staging/repository/example-6789")) + .andExpect(method(HttpMethod.GET)) + .andExpect(header("Accept", "application/json, application/*+json")) + .andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON) + .body("{\"type\":\"open\", \"transitioning\":true}")); + this.server.expect(requestTo("/service/local/staging/repository/example-6789")) + .andExpect(method(HttpMethod.GET)) + .andExpect(header("Accept", "application/json, application/*+json")) + .andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON) + .body("{\"type\":\"open\", \"transitioning\":false}")); + this.server.expect(requestTo("/service/local/staging/repository/example-6789/activity")) + .andExpect(method(HttpMethod.GET)) + .andExpect(header("Accept", "application/json, application/*+json")) + .andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON).body(new FileSystemResource( + new File("src/test/resources/io/spring/concourse/releasescripts/sonatype/activity.json")))); + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> this.service.publish(getReleaseInfo(), artifactsRoot)) + .withMessage("Close failed"); + this.server.verify(); + assertThat(uploadRequestsMatcher.candidates).hasSize(0); + } } private ReleaseInfo getReleaseInfo() { @@ -86,4 +198,39 @@ private ReleaseInfo getReleaseInfo() { return releaseInfo; } -} \ No newline at end of file + private AnyOfRequestMatcher anyOf(Set candidates) { + return new AnyOfRequestMatcher(candidates); + } + + private static class AnyOfRequestMatcher implements RequestMatcher { + + private final Object monitor = new Object(); + + private final Set candidates; + + private AnyOfRequestMatcher(Set candidates) { + this.candidates = candidates; + } + + @Override + public void match(ClientHttpRequest request) throws IOException, AssertionError { + synchronized (this.monitor) { + Iterator iterator = this.candidates.iterator(); + while (iterator.hasNext()) { + try { + iterator.next().match(request); + iterator.remove(); + return; + } + catch (AssertionError ex) { + // Continue + } + } + throw new AssertionError( + "No matching request matcher was found for request to '" + request.getURI() + "'"); + } + } + + } + +} diff --git a/ci/images/releasescripts/src/test/resources/application.yml b/ci/images/releasescripts/src/test/resources/application.yml index 88fe8a8f347a..9c2cbc9d5b25 100644 --- a/ci/images/releasescripts/src/test/resources/application.yml +++ b/ci/images/releasescripts/src/test/resources/application.yml @@ -9,3 +9,8 @@ bintray: sonatype: user-token: sonatype-user password-token: sonatype-password + polling-interval: 1s + staging-profile-id: 1a2b3c4d +sdkman: + consumer-key: sdkman-consumer-key + consumer-token: sdkman-consumer-token diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/artifactory/filtered-build-info-response.json b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/artifactory/filtered-build-info-response.json new file mode 100644 index 000000000000..9f9935114de5 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/artifactory/filtered-build-info-response.json @@ -0,0 +1,59 @@ +{ + "buildInfo": { + "version": "1.0.1", + "name": "example", + "number": "example-build-1", + "started": "2019-09-10T12:18:05.430+0000", + "durationMillis": 0, + "artifactoryPrincipal": "user", + "url": "https://my-ci.com", + "modules": [ + { + "id": "org.example.demo:demo:2.2.0", + "artifacts": [ + { + "type": "jar", + "sha1": "ayyyya9151a22cb3145538e523dbbaaaaaaaa", + "sha256": "aaaaaaaaa85f5c5093721f3ed0edda8ff8290yyyyyyyyyy", + "md5": "aaaaaacddea1724b0b69d8yyyyyyy", + "name": "demo-2.2.0.jar" + } + ] + }, + { + "id": "org.example.demo:demo:2.2.0:zip", + "artifacts": [ + { + "type": "zip", + "sha1": "ayyyya9151a22cb3145538e523dbbaaaaaaab", + "sha256": "aaaaaaaaa85f5c5093721f3ed0edda8ff8290yyyyyyyyyz", + "md5": "aaaaaacddea1724b0b69d8yyyyyyz", + "name": "demo-2.2.0.zip" + } + ] + }, + { + "id": "org.example.demo:demo:2.2.0:doc", + "artifacts": [ + { + "type": "jar", + "sha1": "ayyyya9151a22cb3145538e523dbbaaaaaaba", + "sha256": "aaaaaaaaa85f5c5093721f3ed0edda8ff8290yyyyyyyyzy", + "md5": "aaaaaacddea1724b0b69d8yyyyyzy", + "name": "demo-2.2.0.doc" + } + ] + } + ], + "statuses": [ + { + "status": "staged", + "repository": "libs-release-local", + "timestamp": "2019-09-10T12:42:24.716+0000", + "user": "user", + "timestampDate": 1568119344716 + } + ] + }, + "uri": "https://my-artifactory-repo.com/api/build/example/example-build-1" +} \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/artifactory/no-status-build-info-response.json b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/artifactory/no-status-build-info-response.json new file mode 100644 index 000000000000..6183ca80018c --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/artifactory/no-status-build-info-response.json @@ -0,0 +1,26 @@ +{ + "buildInfo": { + "version": "1.0.1", + "name": "example", + "number": "example-build-1", + "started": "2019-09-10T12:18:05.430+0000", + "durationMillis": 0, + "artifactoryPrincipal": "user", + "url": "https://my-ci.com", + "modules": [ + { + "id": "org.example.demo:demo:2.2.0", + "artifacts": [ + { + "type": "jar", + "sha1": "ayyyya9151a22cb3145538e523dbbaaaaaaaa", + "sha256": "aaaaaaaaa85f5c5093721f3ed0edda8ff8290yyyyyyyyyy", + "md5": "aaaaaacddea1724b0b69d8yyyyyyy", + "name": "demo-2.2.0.jar" + } + ] + } + ] + }, + "uri": "https://my-artifactory-repo.com/api/build/example/example-build-1" +} \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/bintray/all-package-files.json b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/bintray/all-package-files.json deleted file mode 100644 index cb4839cd1346..000000000000 --- a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/bintray/all-package-files.json +++ /dev/null @@ -1,35 +0,0 @@ -[ - { - "name": "nutcracker-1.1-sources.jar", - "path": "org/jfrog/powerutils/nutcracker/1.1/nutcracker-1.1-sources.jar", - "package": "jfrog-power-utils", - "version": "1.1", - "repo": "jfrog-jars", - "owner": "jfrog", - "created": "ISO8601 (yyyy-MM-dd'T'HH:mm:ss.SSSZ)", - "size": 1234, - "sha1": "602e20176706d3cc7535f01ffdbe91b270ae5012" - }, - { - "name": "nutcracker-1.1.pom", - "path": "org/jfrog/powerutils/nutcracker/1.1/nutcracker-1.1.pom", - "package": "jfrog-power-utils", - "version": "1.1", - "repo": "jfrog-jars", - "owner": "jfrog", - "created": "ISO8601 (yyyy-MM-dd'T'HH:mm:ss.SSSZ)", - "size": 1234, - "sha1": "602e20176706d3cc7535f01ffdbe91b270ae5012" - }, - { - "name": "nutcracker-1.1.jar", - "path": "org/jfrog/powerutils/nutcracker/1.1/nutcracker-1.1.jar", - "package": "jfrog-power-utils", - "version": "1.1", - "repo": "jfrog-jars", - "owner": "jfrog", - "created": "ISO8601 (yyyy-MM-dd'T'HH:mm:ss.SSSZ)", - "size": 1234, - "sha1": "602e20176706d3cc7535f01ffdbe91b270ae5012" - } -] \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/bintray/published-files.json b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/bintray/published-files.json deleted file mode 100644 index 05d2bfc0e87d..000000000000 --- a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/bintray/published-files.json +++ /dev/null @@ -1,13 +0,0 @@ -[ - { - "name": "nutcracker-1.1-sources.jar", - "path": "org/jfrog/powerutils/nutcracker/1.1/nutcracker-1.1-sources.jar", - "package": "jfrog-power-utils", - "version": "1.1", - "repo": "jfrog-jars", - "owner": "jfrog", - "created": "ISO8601 (yyyy-MM-dd'T'HH:mm:ss.SSSZ)", - "size": 1234, - "sha1": "602e20176706d3cc7535f01ffdbe91b270ae5012" - } -] \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/activity.json b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/activity.json new file mode 100644 index 000000000000..92e0e141371d --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/activity.json @@ -0,0 +1,362 @@ +[ + { + "events": [ + { + "name": "repositoryCreated", + "properties": [ + { + "name": "id", + "value": "orgspringframework-7161" + }, + { + "name": "user", + "value": "user" + }, + { + "name": "ip", + "value": "127.0.0.1" + } + ], + "severity": 0, + "timestamp": "2021-02-08T14:31:13.523Z" + } + ], + "name": "open", + "started": "2021-02-08T14:31:00.662Z", + "stopped": "2021-02-08T14:31:14.855Z" + }, + { + "events": [ + { + "name": "rulesEvaluate", + "properties": [ + { + "name": "id", + "value": "5e9e8e6f8d20a3" + }, + { + "name": "rule", + "value": "no-traversal-paths-in-archive-file" + }, + { + "name": "rule", + "value": "profile-target-matching-staging" + }, + { + "name": "rule", + "value": "sbom-report" + }, + { + "name": "rule", + "value": "checksum-staging" + }, + { + "name": "rule", + "value": "javadoc-staging" + }, + { + "name": "rule", + "value": "pom-staging" + }, + { + "name": "rule", + "value": "signature-staging" + }, + { + "name": "rule", + "value": "sources-staging" + } + ], + "severity": 0, + "timestamp": "2021-02-08T14:31:37.327Z" + }, + { + "name": "ruleEvaluate", + "properties": [ + { + "name": "typeId", + "value": "no-traversal-paths-in-archive-file" + } + ], + "severity": 0, + "timestamp": "2021-02-08T14:31:41.254Z" + }, + { + "name": "rulePassed", + "properties": [ + { + "name": "typeId", + "value": "no-traversal-paths-in-archive-file" + } + ], + "severity": 0, + "timestamp": "2021-02-08T14:31:47.498Z" + }, + { + "name": "ruleEvaluate", + "properties": [ + { + "name": "typeId", + "value": "javadoc-staging" + } + ], + "severity": 0, + "timestamp": "2021-02-08T14:31:53.438Z" + }, + { + "name": "rulePassed", + "properties": [ + { + "name": "typeId", + "value": "javadoc-staging" + } + ], + "severity": 0, + "timestamp": "2021-02-08T14:31:54.623Z" + }, + { + "name": "ruleEvaluate", + "properties": [ + { + "name": "typeId", + "value": "pom-staging" + } + ], + "severity": 0, + "timestamp": "2021-02-08T14:31:58.091Z" + }, + { + "name": "ruleFailed", + "properties": [ + { + "name": "typeId", + "value": "pom-staging" + }, + { + "name": "failureMessage", + "value": "Invalid POM: /org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom: Project name missing, Project description missing, Project URL missing, License information missing, SCM URL missing, Developer information missing" + }, + { + "name": "failureMessage", + "value": "Invalid POM: /org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom: Project name missing, Project description missing, Project URL missing, License information missing, SCM URL missing, Developer information missing" + }, + { + "name": "failureMessage", + "value": "Invalid POM: /org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom: Project name missing, Project description missing, Project URL missing, License information missing, SCM URL missing, Developer information missing" + } + ], + "severity": 1, + "timestamp": "2021-02-08T14:31:59.403Z" + }, + { + "name": "ruleEvaluate", + "properties": [ + { + "name": "typeId", + "value": "profile-target-matching-staging" + } + ], + "severity": 0, + "timestamp": "2021-02-08T14:32:05.322Z" + }, + { + "name": "rulePassed", + "properties": [ + { + "name": "typeId", + "value": "profile-target-matching-staging" + } + ], + "severity": 0, + "timestamp": "2021-02-08T14:32:06.492Z" + }, + { + "name": "ruleEvaluate", + "properties": [ + { + "name": "typeId", + "value": "checksum-staging" + } + ], + "severity": 0, + "timestamp": "2021-02-08T14:32:12.415Z" + }, + { + "name": "rulePassed", + "properties": [ + { + "name": "typeId", + "value": "checksum-staging" + } + ], + "severity": 0, + "timestamp": "2021-02-08T14:32:13.568Z" + }, + { + "name": "ruleEvaluate", + "properties": [ + { + "name": "typeId", + "value": "signature-staging" + } + ], + "severity": 0, + "timestamp": "2021-02-08T14:32:18.288Z" + }, + { + "name": "ruleFailed", + "properties": [ + { + "name": "typeId", + "value": "signature-staging" + }, + { + "name": "failureMessage", + "value": "Missing Signature: '/org/springframework/example/module-one/1.0.0/module-one-1.0.0-javadoc.jar.asc' does not exist for 'module-one-1.0.0-javadoc.jar'." + }, + { + "name": "failureMessage", + "value": "Missing Signature: '/org/springframework/example/module-one/1.0.0/module-one-1.0.0.jar.asc' does not exist for 'module-one-1.0.0.jar'." + }, + { + "name": "failureMessage", + "value": "Missing Signature: '/org/springframework/example/module-one/1.0.0/module-one-1.0.0-sources.jar.asc' does not exist for 'module-one-1.0.0-sources.jar'." + }, + { + "name": "failureMessage", + "value": "Missing Signature: '/org/springframework/example/module-one/1.0.0/module-one-1.0.0.module.asc' does not exist for 'module-one-1.0.0.module'." + }, + { + "name": "failureMessage", + "value": "Missing Signature: '/org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom.asc' does not exist for 'module-one-1.0.0.pom'." + }, + { + "name": "failureMessage", + "value": "Missing Signature: '/org/springframework/example/module-two/1.0.0/module-two-1.0.0.module.asc' does not exist for 'module-two-1.0.0.module'." + }, + { + "name": "failureMessage", + "value": "Missing Signature: '/org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom.asc' does not exist for 'module-two-1.0.0.pom'." + }, + { + "name": "failureMessage", + "value": "Missing Signature: '/org/springframework/example/module-two/1.0.0/module-two-1.0.0-sources.jar.asc' does not exist for 'module-two-1.0.0-sources.jar'." + }, + { + "name": "failureMessage", + "value": "Missing Signature: '/org/springframework/example/module-two/1.0.0/module-two-1.0.0.jar.asc' does not exist for 'module-two-1.0.0.jar'." + }, + { + "name": "failureMessage", + "value": "Missing Signature: '/org/springframework/example/module-two/1.0.0/module-two-1.0.0-javadoc.jar.asc' does not exist for 'module-two-1.0.0-javadoc.jar'." + }, + { + "name": "failureMessage", + "value": "Missing Signature: '/org/springframework/example/module-three/1.0.0/module-three-1.0.0.module.asc' does not exist for 'module-three-1.0.0.module'." + }, + { + "name": "failureMessage", + "value": "Missing Signature: '/org/springframework/example/module-three/1.0.0/module-three-1.0.0-javadoc.jar.asc' does not exist for 'module-three-1.0.0-javadoc.jar'." + }, + { + "name": "failureMessage", + "value": "Missing Signature: '/org/springframework/example/module-three/1.0.0/module-three-1.0.0-sources.jar.asc' does not exist for 'module-three-1.0.0-sources.jar'." + }, + { + "name": "failureMessage", + "value": "Missing Signature: '/org/springframework/example/module-three/1.0.0/module-three-1.0.0.jar.asc' does not exist for 'module-three-1.0.0.jar'." + }, + { + "name": "failureMessage", + "value": "Missing Signature: '/org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom.asc' does not exist for 'module-three-1.0.0.pom'." + } + ], + "severity": 1, + "timestamp": "2021-02-08T14:32:19.443Z" + }, + { + "name": "ruleEvaluate", + "properties": [ + { + "name": "typeId", + "value": "sources-staging" + } + ], + "severity": 0, + "timestamp": "2021-02-08T14:32:24.175Z" + }, + { + "name": "rulePassed", + "properties": [ + { + "name": "typeId", + "value": "sources-staging" + } + ], + "severity": 0, + "timestamp": "2021-02-08T14:32:28.940Z" + }, + { + "name": "ruleEvaluate", + "properties": [ + { + "name": "typeId", + "value": "sbom-report" + } + ], + "severity": 0, + "timestamp": "2021-02-08T14:32:34.906Z" + }, + { + "name": "rulePassed", + "properties": [ + { + "name": "typeId", + "value": "sbom-report" + }, + { + "name": "successMessage", + "value": "Successfully requested SBOM report" + } + ], + "severity": 0, + "timestamp": "2021-02-08T14:32:36.520Z" + }, + { + "name": "rulesFailed", + "properties": [ + { + "name": "id", + "value": "5e9e8e6f8d20a3" + }, + { + "name": "failureCount", + "value": "2" + } + ], + "severity": 1, + "timestamp": "2021-02-08T14:32:42.068Z" + }, + { + "name": "repositoryCloseFailed", + "properties": [ + { + "name": "id", + "value": "orgspringframework-7161" + }, + { + "name": "cause", + "value": "com.sonatype.nexus.staging.StagingRulesFailedException: One or more rules have failed" + } + ], + "severity": 1, + "timestamp": "2021-02-08T14:32:43.218Z" + } + ], + "name": "close", + "started": "2021-02-08T14:31:34.943Z", + "startedByIpAddress": "127.0.0.1", + "startedByUserId": "user", + "stopped": "2021-02-08T14:32:47.138Z" + } +] diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/build-info.json b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/build-info.json new file mode 100644 index 000000000000..bfe0d3be186b --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/build-info.json @@ -0,0 +1,35 @@ +{ + "buildInfo": { + "version": "1.0.1", + "name": "example", + "number": "example-build-1", + "started": "2019-09-10T12:18:05.430+0000", + "durationMillis": 0, + "artifactoryPrincipal": "user", + "url": "https://my-ci.com", + "modules": [ + { + "id": "org.example.demo:demo:2.2.0", + "artifacts": [ + { + "type": "jar", + "sha1": "ayyyya9151a22cb3145538e523dbbaaaaaaaa", + "sha256": "aaaaaaaaa85f5c5093721f3ed0edda8ff8290yyyyyyyyyy", + "md5": "aaaaaacddea1724b0b69d8yyyyyyy", + "name": "demo-2.2.0.jar" + } + ] + } + ], + "statuses": [ + { + "status": "staged", + "repository": "libs-release-local", + "timestamp": "2019-09-10T12:42:24.716+0000", + "user": "user", + "timestampDate": 1568119344716 + } + ] + }, + "uri": "https://my-artifactory-repo.com/api/build/example/example-build-1" +} \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-javadoc.jar b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-javadoc.jar new file mode 100644 index 000000000000..a142d391c0af Binary files /dev/null and b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-javadoc.jar differ diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-javadoc.jar.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-javadoc.jar.asc new file mode 100644 index 000000000000..85d6a7cb1e5a --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-javadoc.jar.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT3qtgf8CeDvxzi7lPghlrZtmYTTjaic4FZsGRWTPky3H24i4wSRDhG0L5sj4uPK +eLLlITr5a9j26UCas9HRSthiC8+EgIMAhSN0X482SQhUZHAW67ErIvaHlwL+ixMD +0T5pmsW8PKN3lV1TFMhNYSEC2GRG/4GF+3yQA8LR+BgeEu/E5nmysIH8vuQMkOD6 +3pKA8VKNBml591j6UTqxoHtPX+rThaziz3Hy3+ekf5iWslllTTGPd2SWqTvnj2Ae +GvRzsbli+FEM0Aj/v8jUQnQzOz891QSvWR+fMfCqZimiJMc+GBzJ9umbcyQsB5tY +e26mAoYd9KEpGXMKN4biHbJZNp1GGw== +=x/MY +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-javadoc.jar.md5 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-javadoc.jar.md5 new file mode 100644 index 000000000000..95fa4af1641f --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-javadoc.jar.md5 @@ -0,0 +1 @@ +e84da489be91de821c95d41b8f0e0a0a \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-javadoc.jar.md5.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-javadoc.jar.md5.asc new file mode 100644 index 000000000000..1cbf6ccffa07 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-javadoc.jar.md5.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT1PSwf+InTdlh+BVVy3RFszEL5vN7waSPYMImXhgd5EZ1d4Lxd6EeOwtNgWNKpG +E+Ps/Kw0jZDvoD49WJlcUjzDHBNHcE7C/L3GAWHV6WwklhtQaJ4EegsynWdSXz6k +fqJY6r58aGKGjpKPutRWAjvfcdC170+ZRsc2oi9xrAgHCpvXzTjq4+O9Ah0t5jwW +jcZ/Xubcw4vjsw774OucHbtwGsvRN5SDJ3IONOH8WCwhUP5vEEKvA6MYX0KGoTdS +3wTCyZTzU3qtTWxcbTCpiJIWbYwRR7TzLB/uydWHlAMzuz6coIiBpYsGiO6wkmfg +W+QvcE7wyW2jtb22pCImLyObyZ21VA== +=VjDv +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-javadoc.jar.sha1 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-javadoc.jar.sha1 new file mode 100644 index 000000000000..2a2834e1ab41 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-javadoc.jar.sha1 @@ -0,0 +1 @@ +8992b17455ce660da9c5fe47226b7ded9e872637 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-javadoc.jar.sha1.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-javadoc.jar.sha1.asc new file mode 100644 index 000000000000..4cd212d6e1e9 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-javadoc.jar.sha1.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT2HQgf+MTUEnwzXK4zi76VI7C5cchGan26lIA2Ebq4vtYGKDqNSISOAAdWs9+nT +U6ZA6OIFo5qdeD6F/45s91IoDbxbhMDMFEsSijKASqiuZN5TZM1U2h2kWFAl/sEl +EI1RTygn+xDw/ah4V3/duuMFC+jRgvJ/LgemIF4KBvECWaTQKNu0fu5d4dPXMpp+ +jrxMEZPQZsivpOvklzV8O7wAkf/ZQhJdcB2m8uOfSPlJ91a4EEtXF9/GzzkXUi1P +bzt4NsmOag3227B3mO1Bc6yZdDBNu8wQ9apiJVCpqsxB9Dz0PCL4dHNa1u9g6Xo6 +ElRgneV4HZp+LB125VoNabKuNH00bw== +=2yDl +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-javadoc.jar.sha256 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-javadoc.jar.sha256 new file mode 100644 index 000000000000..4f27f01046dd --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-javadoc.jar.sha256 @@ -0,0 +1 @@ +10ce8a82f7e53c67f2af8d4dc4b8cdb4d0630d0e1d21818da4d5b3ca2de08385 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-javadoc.jar.sha256.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-javadoc.jar.sha256.asc new file mode 100644 index 000000000000..e29629cbec07 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-javadoc.jar.sha256.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT0ScQf7Bip+quFq1CzDCTDxUhdTTOIpQcCfMKo1Jegpa2Hlm63XuK+6zVI9u6S+ +dBXPdYeneMOqMPQUAaw3M06ZqohaDOt8Ti1EiQFGXCdOvbonTA52Lrd4EEZxwNnK +BdPuIh/8qCfozm5KbZe1bFyGVRAdNyf27KvzHgfBTirLtI+3MiOdL4bvNZbWRPfh +J84Ko+Ena8jPFgyz6nJv2Q2U/V3dCooLJAXs2vEG6owwk5J9zvSysWpHaJbXas5v +KXO9TOBBjf3+vxb1WVQa8ZYUU3+FIFes0RFVgOWghJXIooOcWrwOV2Q8z9qWXwoK +mMZ2oLS+z/7clXibK45KeRUeCX5DvQ== +=5oO1 +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-javadoc.jar.sha512 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-javadoc.jar.sha512 new file mode 100644 index 000000000000..eb02a04a89d2 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-javadoc.jar.sha512 @@ -0,0 +1 @@ +2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-javadoc.jar.sha512.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-javadoc.jar.sha512.asc new file mode 100644 index 000000000000..f35b726baff6 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-javadoc.jar.sha512.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT3d+AgAwQvlwnKQLLuiZADGL+I922/YG317N2Re1EjC6WlMRZUKXH54fckRTyPm +4ZLyxVHy8LlUD2Q10g69opb7HRd/tV0miBJhn5OU1wIM3hqTgxNp9EFckK4md45k +osnhQJNDsFToxJL8zPP+KRs/aWPZs+FrRcH6k26lwLl2gTfyBDsaU11HFRVEN9yi +X41obVyKiVNlc9efSSvlLtRBSVt0VhAFhck+3t61H6D9H09QxaDGAqmduDua3Tg3 +t5eqURuDfv3TfSztYgK3JBmG/6gVMsZodCgyC+8rhDDs6vSoDG30apx5Leg2rPbj +xuk2wi/WNzc94IgY9tVS3tAfT2k6yQ== +=6+Cv +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-sources.jar b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-sources.jar new file mode 100644 index 000000000000..a142d391c0af Binary files /dev/null and b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-sources.jar differ diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-sources.jar.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-sources.jar.asc new file mode 100644 index 000000000000..85d6a7cb1e5a --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-sources.jar.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT3qtgf8CeDvxzi7lPghlrZtmYTTjaic4FZsGRWTPky3H24i4wSRDhG0L5sj4uPK +eLLlITr5a9j26UCas9HRSthiC8+EgIMAhSN0X482SQhUZHAW67ErIvaHlwL+ixMD +0T5pmsW8PKN3lV1TFMhNYSEC2GRG/4GF+3yQA8LR+BgeEu/E5nmysIH8vuQMkOD6 +3pKA8VKNBml591j6UTqxoHtPX+rThaziz3Hy3+ekf5iWslllTTGPd2SWqTvnj2Ae +GvRzsbli+FEM0Aj/v8jUQnQzOz891QSvWR+fMfCqZimiJMc+GBzJ9umbcyQsB5tY +e26mAoYd9KEpGXMKN4biHbJZNp1GGw== +=x/MY +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-sources.jar.md5 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-sources.jar.md5 new file mode 100644 index 000000000000..95fa4af1641f --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-sources.jar.md5 @@ -0,0 +1 @@ +e84da489be91de821c95d41b8f0e0a0a \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-sources.jar.md5.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-sources.jar.md5.asc new file mode 100644 index 000000000000..1cbf6ccffa07 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-sources.jar.md5.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT1PSwf+InTdlh+BVVy3RFszEL5vN7waSPYMImXhgd5EZ1d4Lxd6EeOwtNgWNKpG +E+Ps/Kw0jZDvoD49WJlcUjzDHBNHcE7C/L3GAWHV6WwklhtQaJ4EegsynWdSXz6k +fqJY6r58aGKGjpKPutRWAjvfcdC170+ZRsc2oi9xrAgHCpvXzTjq4+O9Ah0t5jwW +jcZ/Xubcw4vjsw774OucHbtwGsvRN5SDJ3IONOH8WCwhUP5vEEKvA6MYX0KGoTdS +3wTCyZTzU3qtTWxcbTCpiJIWbYwRR7TzLB/uydWHlAMzuz6coIiBpYsGiO6wkmfg +W+QvcE7wyW2jtb22pCImLyObyZ21VA== +=VjDv +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-sources.jar.sha1 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-sources.jar.sha1 new file mode 100644 index 000000000000..2a2834e1ab41 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-sources.jar.sha1 @@ -0,0 +1 @@ +8992b17455ce660da9c5fe47226b7ded9e872637 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-sources.jar.sha1.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-sources.jar.sha1.asc new file mode 100644 index 000000000000..4cd212d6e1e9 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-sources.jar.sha1.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT2HQgf+MTUEnwzXK4zi76VI7C5cchGan26lIA2Ebq4vtYGKDqNSISOAAdWs9+nT +U6ZA6OIFo5qdeD6F/45s91IoDbxbhMDMFEsSijKASqiuZN5TZM1U2h2kWFAl/sEl +EI1RTygn+xDw/ah4V3/duuMFC+jRgvJ/LgemIF4KBvECWaTQKNu0fu5d4dPXMpp+ +jrxMEZPQZsivpOvklzV8O7wAkf/ZQhJdcB2m8uOfSPlJ91a4EEtXF9/GzzkXUi1P +bzt4NsmOag3227B3mO1Bc6yZdDBNu8wQ9apiJVCpqsxB9Dz0PCL4dHNa1u9g6Xo6 +ElRgneV4HZp+LB125VoNabKuNH00bw== +=2yDl +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-sources.jar.sha256 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-sources.jar.sha256 new file mode 100644 index 000000000000..4f27f01046dd --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-sources.jar.sha256 @@ -0,0 +1 @@ +10ce8a82f7e53c67f2af8d4dc4b8cdb4d0630d0e1d21818da4d5b3ca2de08385 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-sources.jar.sha256.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-sources.jar.sha256.asc new file mode 100644 index 000000000000..e29629cbec07 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-sources.jar.sha256.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT0ScQf7Bip+quFq1CzDCTDxUhdTTOIpQcCfMKo1Jegpa2Hlm63XuK+6zVI9u6S+ +dBXPdYeneMOqMPQUAaw3M06ZqohaDOt8Ti1EiQFGXCdOvbonTA52Lrd4EEZxwNnK +BdPuIh/8qCfozm5KbZe1bFyGVRAdNyf27KvzHgfBTirLtI+3MiOdL4bvNZbWRPfh +J84Ko+Ena8jPFgyz6nJv2Q2U/V3dCooLJAXs2vEG6owwk5J9zvSysWpHaJbXas5v +KXO9TOBBjf3+vxb1WVQa8ZYUU3+FIFes0RFVgOWghJXIooOcWrwOV2Q8z9qWXwoK +mMZ2oLS+z/7clXibK45KeRUeCX5DvQ== +=5oO1 +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-sources.jar.sha512 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-sources.jar.sha512 new file mode 100644 index 000000000000..eb02a04a89d2 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-sources.jar.sha512 @@ -0,0 +1 @@ +2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-sources.jar.sha512.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-sources.jar.sha512.asc new file mode 100644 index 000000000000..f35b726baff6 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0-sources.jar.sha512.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT3d+AgAwQvlwnKQLLuiZADGL+I922/YG317N2Re1EjC6WlMRZUKXH54fckRTyPm +4ZLyxVHy8LlUD2Q10g69opb7HRd/tV0miBJhn5OU1wIM3hqTgxNp9EFckK4md45k +osnhQJNDsFToxJL8zPP+KRs/aWPZs+FrRcH6k26lwLl2gTfyBDsaU11HFRVEN9yi +X41obVyKiVNlc9efSSvlLtRBSVt0VhAFhck+3t61H6D9H09QxaDGAqmduDua3Tg3 +t5eqURuDfv3TfSztYgK3JBmG/6gVMsZodCgyC+8rhDDs6vSoDG30apx5Leg2rPbj +xuk2wi/WNzc94IgY9tVS3tAfT2k6yQ== +=6+Cv +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.jar b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.jar new file mode 100644 index 000000000000..a142d391c0af Binary files /dev/null and b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.jar differ diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.jar.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.jar.asc new file mode 100644 index 000000000000..85d6a7cb1e5a --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.jar.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT3qtgf8CeDvxzi7lPghlrZtmYTTjaic4FZsGRWTPky3H24i4wSRDhG0L5sj4uPK +eLLlITr5a9j26UCas9HRSthiC8+EgIMAhSN0X482SQhUZHAW67ErIvaHlwL+ixMD +0T5pmsW8PKN3lV1TFMhNYSEC2GRG/4GF+3yQA8LR+BgeEu/E5nmysIH8vuQMkOD6 +3pKA8VKNBml591j6UTqxoHtPX+rThaziz3Hy3+ekf5iWslllTTGPd2SWqTvnj2Ae +GvRzsbli+FEM0Aj/v8jUQnQzOz891QSvWR+fMfCqZimiJMc+GBzJ9umbcyQsB5tY +e26mAoYd9KEpGXMKN4biHbJZNp1GGw== +=x/MY +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.jar.md5 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.jar.md5 new file mode 100644 index 000000000000..95fa4af1641f --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.jar.md5 @@ -0,0 +1 @@ +e84da489be91de821c95d41b8f0e0a0a \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.jar.md5.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.jar.md5.asc new file mode 100644 index 000000000000..1cbf6ccffa07 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.jar.md5.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT1PSwf+InTdlh+BVVy3RFszEL5vN7waSPYMImXhgd5EZ1d4Lxd6EeOwtNgWNKpG +E+Ps/Kw0jZDvoD49WJlcUjzDHBNHcE7C/L3GAWHV6WwklhtQaJ4EegsynWdSXz6k +fqJY6r58aGKGjpKPutRWAjvfcdC170+ZRsc2oi9xrAgHCpvXzTjq4+O9Ah0t5jwW +jcZ/Xubcw4vjsw774OucHbtwGsvRN5SDJ3IONOH8WCwhUP5vEEKvA6MYX0KGoTdS +3wTCyZTzU3qtTWxcbTCpiJIWbYwRR7TzLB/uydWHlAMzuz6coIiBpYsGiO6wkmfg +W+QvcE7wyW2jtb22pCImLyObyZ21VA== +=VjDv +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.jar.sha1 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.jar.sha1 new file mode 100644 index 000000000000..2a2834e1ab41 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.jar.sha1 @@ -0,0 +1 @@ +8992b17455ce660da9c5fe47226b7ded9e872637 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.jar.sha1.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.jar.sha1.asc new file mode 100644 index 000000000000..4cd212d6e1e9 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.jar.sha1.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT2HQgf+MTUEnwzXK4zi76VI7C5cchGan26lIA2Ebq4vtYGKDqNSISOAAdWs9+nT +U6ZA6OIFo5qdeD6F/45s91IoDbxbhMDMFEsSijKASqiuZN5TZM1U2h2kWFAl/sEl +EI1RTygn+xDw/ah4V3/duuMFC+jRgvJ/LgemIF4KBvECWaTQKNu0fu5d4dPXMpp+ +jrxMEZPQZsivpOvklzV8O7wAkf/ZQhJdcB2m8uOfSPlJ91a4EEtXF9/GzzkXUi1P +bzt4NsmOag3227B3mO1Bc6yZdDBNu8wQ9apiJVCpqsxB9Dz0PCL4dHNa1u9g6Xo6 +ElRgneV4HZp+LB125VoNabKuNH00bw== +=2yDl +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.jar.sha256 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.jar.sha256 new file mode 100644 index 000000000000..4f27f01046dd --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.jar.sha256 @@ -0,0 +1 @@ +10ce8a82f7e53c67f2af8d4dc4b8cdb4d0630d0e1d21818da4d5b3ca2de08385 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.jar.sha256.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.jar.sha256.asc new file mode 100644 index 000000000000..e29629cbec07 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.jar.sha256.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT0ScQf7Bip+quFq1CzDCTDxUhdTTOIpQcCfMKo1Jegpa2Hlm63XuK+6zVI9u6S+ +dBXPdYeneMOqMPQUAaw3M06ZqohaDOt8Ti1EiQFGXCdOvbonTA52Lrd4EEZxwNnK +BdPuIh/8qCfozm5KbZe1bFyGVRAdNyf27KvzHgfBTirLtI+3MiOdL4bvNZbWRPfh +J84Ko+Ena8jPFgyz6nJv2Q2U/V3dCooLJAXs2vEG6owwk5J9zvSysWpHaJbXas5v +KXO9TOBBjf3+vxb1WVQa8ZYUU3+FIFes0RFVgOWghJXIooOcWrwOV2Q8z9qWXwoK +mMZ2oLS+z/7clXibK45KeRUeCX5DvQ== +=5oO1 +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.jar.sha512 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.jar.sha512 new file mode 100644 index 000000000000..eb02a04a89d2 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.jar.sha512 @@ -0,0 +1 @@ +2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.jar.sha512.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.jar.sha512.asc new file mode 100644 index 000000000000..f35b726baff6 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.jar.sha512.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT3d+AgAwQvlwnKQLLuiZADGL+I922/YG317N2Re1EjC6WlMRZUKXH54fckRTyPm +4ZLyxVHy8LlUD2Q10g69opb7HRd/tV0miBJhn5OU1wIM3hqTgxNp9EFckK4md45k +osnhQJNDsFToxJL8zPP+KRs/aWPZs+FrRcH6k26lwLl2gTfyBDsaU11HFRVEN9yi +X41obVyKiVNlc9efSSvlLtRBSVt0VhAFhck+3t61H6D9H09QxaDGAqmduDua3Tg3 +t5eqURuDfv3TfSztYgK3JBmG/6gVMsZodCgyC+8rhDDs6vSoDG30apx5Leg2rPbj +xuk2wi/WNzc94IgY9tVS3tAfT2k6yQ== +=6+Cv +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.module b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.module new file mode 100644 index 000000000000..6a92fa0a1afb --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.module @@ -0,0 +1,101 @@ +{ + "formatVersion": "1.1", + "component": { + "group": "org.springframework.example", + "module": "module-one", + "version": "1.0.0", + "attributes": { + "org.gradle.status": "release" + } + }, + "createdBy": { + "gradle": { + "version": "6.5.1", + "buildId": "mvqepqsdqjcahjl7cii6b6ucoe" + } + }, + "variants": [ + { + "name": "apiElements", + "attributes": { + "org.gradle.category": "library", + "org.gradle.dependency.bundling": "external", + "org.gradle.jvm.version": 8, + "org.gradle.libraryelements": "jar", + "org.gradle.usage": "java-api" + }, + "files": [ + { + "name": "module-one-1.0.0.jar", + "url": "module-one-1.0.0.jar", + "size": 261, + "sha512": "2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00", + "sha256": "10ce8a82f7e53c67f2af8d4dc4b8cdb4d0630d0e1d21818da4d5b3ca2de08385", + "sha1": "8992b17455ce660da9c5fe47226b7ded9e872637", + "md5": "e84da489be91de821c95d41b8f0e0a0a" + } + ] + }, + { + "name": "runtimeElements", + "attributes": { + "org.gradle.category": "library", + "org.gradle.dependency.bundling": "external", + "org.gradle.jvm.version": 8, + "org.gradle.libraryelements": "jar", + "org.gradle.usage": "java-runtime" + }, + "files": [ + { + "name": "module-one-1.0.0.jar", + "url": "module-one-1.0.0.jar", + "size": 261, + "sha512": "2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00", + "sha256": "10ce8a82f7e53c67f2af8d4dc4b8cdb4d0630d0e1d21818da4d5b3ca2de08385", + "sha1": "8992b17455ce660da9c5fe47226b7ded9e872637", + "md5": "e84da489be91de821c95d41b8f0e0a0a" + } + ] + }, + { + "name": "javadocElements", + "attributes": { + "org.gradle.category": "documentation", + "org.gradle.dependency.bundling": "external", + "org.gradle.docstype": "javadoc", + "org.gradle.usage": "java-runtime" + }, + "files": [ + { + "name": "module-one-1.0.0-javadoc.jar", + "url": "module-one-1.0.0-javadoc.jar", + "size": 261, + "sha512": "2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00", + "sha256": "10ce8a82f7e53c67f2af8d4dc4b8cdb4d0630d0e1d21818da4d5b3ca2de08385", + "sha1": "8992b17455ce660da9c5fe47226b7ded9e872637", + "md5": "e84da489be91de821c95d41b8f0e0a0a" + } + ] + }, + { + "name": "sourcesElements", + "attributes": { + "org.gradle.category": "documentation", + "org.gradle.dependency.bundling": "external", + "org.gradle.docstype": "sources", + "org.gradle.usage": "java-runtime" + }, + "files": [ + { + "name": "module-one-1.0.0-sources.jar", + "url": "module-one-1.0.0-sources.jar", + "size": 261, + "sha512": "2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00", + "sha256": "10ce8a82f7e53c67f2af8d4dc4b8cdb4d0630d0e1d21818da4d5b3ca2de08385", + "sha1": "8992b17455ce660da9c5fe47226b7ded9e872637", + "md5": "e84da489be91de821c95d41b8f0e0a0a" + } + ] + } + ] +} diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.module.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.module.asc new file mode 100644 index 000000000000..54d7598382da --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.module.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT1HBQf/fCBHR+fpZjkcgonkAVWcGvRx5kRHlsCISs64XMw90++DTawoKxr9/TvY +fltQlq/xaf+2O2Xzh9HIymtZBeKp7a4fWQ2AHf/ygkGyIKvy8h+mu3MGDdmHZeA4 +fn9FGjaE0a/wYJmCEHJ1qJ4GaNq47gzRTu76jzZNafnNRlq1rlyVu2txnlks6xDr +oE8EnRT86Y67Ku8YArjkhZSHhf/tzSSwdTAgBinh6eba5tW5ueRXfsheqgtpJMov +hiDIVxuAlJoHy2cQ8L9+8geg0OSXLwQ9BXrBsDCLvrDauU735/Hv/NGrWE95kemw +Ay9jCXhXFWKkzCw2ps3QHTTpTK4aVw== +=1QME +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.module.md5 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.module.md5 new file mode 100644 index 000000000000..7da5a1024ebb --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.module.md5 @@ -0,0 +1 @@ +b5b2aedb082633674ef9308a2ac21934 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.module.md5.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.module.md5.asc new file mode 100644 index 000000000000..f525064e6acc --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.module.md5.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT1SLQgApB6OWW9cgtaofOu3HwgsVxaxLYPsDf057m2O5pI6uV5Ikyt97B1txjTB +9EXcy4gsfu7rxwgRHIEPCQkQKhhZioscT1SPFN0yopCwsJEvxrJE018ojyaIem/L +KVcbtiBVMj3GZCbS0DHpwZNx2u7yblyBqUGhCMKLkYqVL7nUHJKtECECs5jbJnb9 +xXGFe0xlZ/IbkHv5QXyStgUYCah7ayWQDvjN7UJrpJL1lmTD0rjWLilkeKsVu3/k +11cZb5YdOmrL9a+8ql1jXPkma3HPjoIPRC5LB2BnloduwEPsiiLGG7Cs8UFEJNjQ +m5w+l4dDd03y5ioaW8fI/meAKpBm4g== +=gwLM +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.module.sha1 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.module.sha1 new file mode 100644 index 000000000000..f4d48063e987 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.module.sha1 @@ -0,0 +1 @@ +b7cb6c2ce7fbb98b8eb502c3ef8fcab0dd4880fc \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.module.sha1.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.module.sha1.asc new file mode 100644 index 000000000000..0c9e8cb57007 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.module.sha1.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT2y5AgAlI4H5hwDIgVmXtRq/ri7kxEJnC9L9FOv8aE9YasHAruaU1YR5m17Jncl +4guJHc+gSd3BiSx1rsI6PNxLACabw4Vy56eCRpmiFWeIkoCETBUk8AN25Q/1tzgw +hHmIRgOkF9PzSBWDTUNsyx/7E9P2QSiJOkMAGGuMKGDpYTR9zmaluzwfY+BI/VoW +BbZpdzt02OGQosWmA7DlwkXUwip6iBjga79suUFIsyH0hmRW2q/nCeJ04ttzXUog +NTNkpEwMYpZAzQXE7ks7WJJlAPkVYPWy/j5YCV7xTFb9I/56ux+/wRUaGU5fumSR +lr3PNoYNToC/4GLX6Kc2OH0e1LXNTQ== +=s02D +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.module.sha256 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.module.sha256 new file mode 100644 index 000000000000..cdc919db3db6 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.module.sha256 @@ -0,0 +1 @@ +4ef7e1ba5fda72b0168c9aab4746ec6ee57fb73020c49fe2f49251187aaab074 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.module.sha256.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.module.sha256.asc new file mode 100644 index 000000000000..94ab1b8db014 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.module.sha256.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT1/vwgAhUTLKjxmry4W3cVdfX/D/vxDTLAp5OxwJy36CZmJwsVuN9TLjPo4tRqq +woiopR2oSTaJqld2pe98WlIeDJJRe4ta1Uwvg7k4Sf6YaZXm01Wufk4a835sFUwY +BTWmnFYX0+dp5mLyXZmZjrAr5Q2bowRuqZd2DAYiNY/E5MH2T7OAJE2hCOHUpCaB +JVeP7HcbaGYR3NX/mLq0t8+xjTPXQk/OHijuusuLQxfLZvZiaikDoOHUD6l0dlRw +xcLTghG5+jd1q7noKAbUVgoEOshstfomCHZpPMj11c7KIuG1+3wRMdm+F67lkcJ5 +eDW2fmF+6LYr+WlEi33rDIyTk3GhlQ== +=mHUe +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.module.sha512 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.module.sha512 new file mode 100644 index 000000000000..76ab9ddba9cd --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.module.sha512 @@ -0,0 +1 @@ +29b1bc06a150e4764826e35e2d2541933b0583ce823f5b00c02effad9f37f02f0d2eef1c81214d69eaf74220e1f77332c5e6a91eb413a3022b5a8a1d7914c4c3 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.module.sha512.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.module.sha512.asc new file mode 100644 index 000000000000..8041ff51f1f1 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.module.sha512.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT0QLAf/ffTpTfH4IebklGJIKZC8ZjRt4CgwpR431qNeWkY25cHmWFj48x2u9dmS +ZpxN572d3PPjcMigT/9wM05omiU+4DHxGgHq/Xj6GXN1DNaENcu7uoye96thjKPv +jz98tPIRMC9hYr3m/K1CJ3+ZG0++7JorCZRpodH/MhklRWXOvNszs81VWtgvMnpd +h9r0PuoaYBl6bIl19o7E3JJU6dKgwfre4b+a1RSYI+A8bmJOKMgHytAKi+804r0P +4R2WuQT4q+dSmkMtgp65vJ9giv/xuFrd1bT4n+qcDkwE8pTcWvsB4w1RkDOKs4fK +/ta5xBQ1hiKAd6nJffke1b0MBrZOrA== +=ZMpE +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom new file mode 100644 index 000000000000..cd3ade53cfa6 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom @@ -0,0 +1,49 @@ + + + + + + + + 4.0.0 + org.springframework.example + module-one + 1.0.0 + module-one + Example module + https://spring.io/projects/spring-boot + + Pivotal Software, Inc. + https://spring.io + + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0 + + + + + Pivotal + info@pivotal.io + Pivotal Software, Inc. + https://www.spring.io + + + + + scm:git:git://github.com/spring-projects/spring-boot.git + + + scm:git:ssh://git@github.com/spring-projects/spring-boot.git + + https://github.com/spring-projects/spring-boot + + + GitHub + + https://github.com/spring-projects/spring-boot/issues + + + diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom.asc new file mode 100644 index 000000000000..eba13f46b597 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT04rwgAwJHic8GGHFZ+UAJYLW/OxJOVyd0ebx4yT5zAyTjyvxnrlKmKZ6GP/NhZ +htJQnZez85lUKA0TsMvl/6H2iEhKOns6HgqY3PLFkKNRKOq601phtD9HCkxDibWB +UDT01I0q2xNOljD03lhfytefnSnZ96AaySol2v5DBIZsOKWGir0/8KJCpEQJHjCF +TwNk8lNF3moGlO4zUfoBbkSZ+J0J8Bq5QI3nIAWFYxHcrZ2YGsAZd48kux8x2V3C +c6QsYEonmztqxop76a7K8Gv+MDmo/u/vqM8z5C63/WpOoDtRG+F5vtPkhCrR6M5f +ygubQUy5TL+dWdHE8zgA2O9hZuoHEg== +=bkxG +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom.md5 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom.md5 new file mode 100644 index 000000000000..d82ed4d3a5d3 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom.md5 @@ -0,0 +1 @@ +48776112e8bc3ca36b6392b0e9d6d619 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom.md5.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom.md5.asc new file mode 100644 index 000000000000..78c3a0a5f668 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom.md5.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT0XEAf+O9a/29MIWBtj1oLxIT1LLdzTU68qt5+qW+58SNQmMxu0MaESW4GZOc3p +mTV0EJyxUkCLJyoqOY4/GhqBAm33mMZSY8BQtvUZPYxpbJwBo+pE8YfnH3n1v20P +4pS4oJKekXAhTqShpx5oFjCK4J3chaz+Xc8Ldm1DXakCRc1bc/YYZ+87sy2z+PXk +PmN3KPcc/XjH4GPjmVUR8vR1TGUjUMQGvbAdrgkjFyaCGNvyreuHLsAFWrFFbIOn +/mB++enkXhmjWbiyvmvWQvtU0QFA4sRGYww0Lup1GRQ+00IqHF1QRMskqujAwmok ++TuB3Zc9WuAERPre+Qr1DEevClNwAQ== +=3beu +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom.sha1 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom.sha1 new file mode 100644 index 000000000000..28dc2dadd344 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom.sha1 @@ -0,0 +1 @@ +a5cc75e17f8eccfc4ac30bfbb09f42d5f34ecbb1 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom.sha1.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom.sha1.asc new file mode 100644 index 000000000000..9d1d2ea54f3b --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom.sha1.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT2aVAf+MQhSBr1jzcQE5mo1bMOXa4owaRr+dRir5Q4Gr7Fz4NuGoQEIzoL7XP5r +0zIjebzworxCaw+JNyaIxniHNBeK3sPHTLeW8bCrdJLkhE9RtGdEHLyPYXwPuFin +xVw3VQHWiA0uPM+JaekgdPDtK5wGFQ/AK3pc6vR108oT0kV4zQEqgRnvLqV9Q5zZ +UPHBi5kypu1BmCW4upYL1dmjASWPn9Q8cNpHcX/NJPNJ9zW0yxAAtq4wLfh7PQml +3EaHEYllsf8v1vMv00+zZNhc6O4BBP1qrRiaYHDAJhJjn6ctV9GFhJ2Ttxh/NmSy +H679tlC2PeRjGMi8bOHBshcikn5KUw== +=4aJI +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom.sha256 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom.sha256 new file mode 100644 index 000000000000..8d1625bf07c2 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom.sha256 @@ -0,0 +1 @@ +3b31f2abf79368001e2fab02997446ac62db714b9db9cb78c4c542aa962485dc \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom.sha256.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom.sha256.asc new file mode 100644 index 000000000000..e572b776de94 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom.sha256.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT0nDQgAlfchq7/W/wubx3IR3tQs0tKiix3nZIc97zuH6sR8+r+CJe78wbmSE9Oo +/z96wfzeZYNIKh2v+dBLHF7OfcPGBE7tiX07jfCa6KzjjY3hFBhW+muMP/aBRb+4 +itSs6F3lkZOPW2+hpSdFQ6U8Rm81cAlZv7Zk2XswwTQkJo8GcNL1w/5wAVpNK0yG +VinZr8YRMFs6OYQxLqGSypDLAmv9rOaJ7aCdaKnQwYES65kC7tbe0SRZGQoDe8n4 +XLzpvC8rM9MXZDEN4qI+ZAANOJNVsXUmDZLDSe4ak48u/cTOokY8I6bR2k/XOhbu +L+D4W7oKAE9HmzlTMusosyjNOBQAmQ== +=Wjji +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom.sha512 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom.sha512 new file mode 100644 index 000000000000..6edd0b3e98f8 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom.sha512 @@ -0,0 +1 @@ +05bd8fd394a15b9dcc1bfaece0a63b0fdc2c3625a7e0aa5230fd3b5b75a8f8934a0af550b44437aa1486909058e84703e63fdec6f637d639d565b55bdaf1fa6c \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom.sha512.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom.sha512.asc new file mode 100644 index 000000000000..896fc8f31e59 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom.sha512.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT19rwf/a6sZxSDNTxN72VvsrKsHq+wMes5UUcQ+L7e5QLjaCTx2ayW2FdHMBaNi +IDBBE9kxnxa/S6G6nSRARUjXowsEYZGUNLLvUjNZ4Z3g2R9XyGPaz3Ky9yWpRm36 +E0lFqf8aaCLpzwV2z7cfeVNYsd2gnHakphK/UiZzXFz+GYzqby/0m5Kk8Zs7rK6V +/ji0bYWUi8t1jli8MfTHQtM8EUHG0nXRfEKilyoYkO3UsTEh/UN1VRpJ5DgcRC8L +Zbd2zPnV15MPUzZvz3kkycUulQdhOqTDjUod9P/WoASwjDuKCG2/kquwOvnoHXJ9 +9Ju+ca0s9y0jbotIygYxJXZVev3EiA== +=oWIp +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-javadoc.jar b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-javadoc.jar new file mode 100644 index 000000000000..a142d391c0af Binary files /dev/null and b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-javadoc.jar differ diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-javadoc.jar.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-javadoc.jar.asc new file mode 100644 index 000000000000..85d6a7cb1e5a --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-javadoc.jar.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT3qtgf8CeDvxzi7lPghlrZtmYTTjaic4FZsGRWTPky3H24i4wSRDhG0L5sj4uPK +eLLlITr5a9j26UCas9HRSthiC8+EgIMAhSN0X482SQhUZHAW67ErIvaHlwL+ixMD +0T5pmsW8PKN3lV1TFMhNYSEC2GRG/4GF+3yQA8LR+BgeEu/E5nmysIH8vuQMkOD6 +3pKA8VKNBml591j6UTqxoHtPX+rThaziz3Hy3+ekf5iWslllTTGPd2SWqTvnj2Ae +GvRzsbli+FEM0Aj/v8jUQnQzOz891QSvWR+fMfCqZimiJMc+GBzJ9umbcyQsB5tY +e26mAoYd9KEpGXMKN4biHbJZNp1GGw== +=x/MY +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-javadoc.jar.md5 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-javadoc.jar.md5 new file mode 100644 index 000000000000..95fa4af1641f --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-javadoc.jar.md5 @@ -0,0 +1 @@ +e84da489be91de821c95d41b8f0e0a0a \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-javadoc.jar.md5.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-javadoc.jar.md5.asc new file mode 100644 index 000000000000..1cbf6ccffa07 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-javadoc.jar.md5.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT1PSwf+InTdlh+BVVy3RFszEL5vN7waSPYMImXhgd5EZ1d4Lxd6EeOwtNgWNKpG +E+Ps/Kw0jZDvoD49WJlcUjzDHBNHcE7C/L3GAWHV6WwklhtQaJ4EegsynWdSXz6k +fqJY6r58aGKGjpKPutRWAjvfcdC170+ZRsc2oi9xrAgHCpvXzTjq4+O9Ah0t5jwW +jcZ/Xubcw4vjsw774OucHbtwGsvRN5SDJ3IONOH8WCwhUP5vEEKvA6MYX0KGoTdS +3wTCyZTzU3qtTWxcbTCpiJIWbYwRR7TzLB/uydWHlAMzuz6coIiBpYsGiO6wkmfg +W+QvcE7wyW2jtb22pCImLyObyZ21VA== +=VjDv +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-javadoc.jar.sha1 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-javadoc.jar.sha1 new file mode 100644 index 000000000000..2a2834e1ab41 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-javadoc.jar.sha1 @@ -0,0 +1 @@ +8992b17455ce660da9c5fe47226b7ded9e872637 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-javadoc.jar.sha1.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-javadoc.jar.sha1.asc new file mode 100644 index 000000000000..4cd212d6e1e9 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-javadoc.jar.sha1.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT2HQgf+MTUEnwzXK4zi76VI7C5cchGan26lIA2Ebq4vtYGKDqNSISOAAdWs9+nT +U6ZA6OIFo5qdeD6F/45s91IoDbxbhMDMFEsSijKASqiuZN5TZM1U2h2kWFAl/sEl +EI1RTygn+xDw/ah4V3/duuMFC+jRgvJ/LgemIF4KBvECWaTQKNu0fu5d4dPXMpp+ +jrxMEZPQZsivpOvklzV8O7wAkf/ZQhJdcB2m8uOfSPlJ91a4EEtXF9/GzzkXUi1P +bzt4NsmOag3227B3mO1Bc6yZdDBNu8wQ9apiJVCpqsxB9Dz0PCL4dHNa1u9g6Xo6 +ElRgneV4HZp+LB125VoNabKuNH00bw== +=2yDl +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-javadoc.jar.sha256 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-javadoc.jar.sha256 new file mode 100644 index 000000000000..4f27f01046dd --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-javadoc.jar.sha256 @@ -0,0 +1 @@ +10ce8a82f7e53c67f2af8d4dc4b8cdb4d0630d0e1d21818da4d5b3ca2de08385 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-javadoc.jar.sha256.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-javadoc.jar.sha256.asc new file mode 100644 index 000000000000..e29629cbec07 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-javadoc.jar.sha256.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT0ScQf7Bip+quFq1CzDCTDxUhdTTOIpQcCfMKo1Jegpa2Hlm63XuK+6zVI9u6S+ +dBXPdYeneMOqMPQUAaw3M06ZqohaDOt8Ti1EiQFGXCdOvbonTA52Lrd4EEZxwNnK +BdPuIh/8qCfozm5KbZe1bFyGVRAdNyf27KvzHgfBTirLtI+3MiOdL4bvNZbWRPfh +J84Ko+Ena8jPFgyz6nJv2Q2U/V3dCooLJAXs2vEG6owwk5J9zvSysWpHaJbXas5v +KXO9TOBBjf3+vxb1WVQa8ZYUU3+FIFes0RFVgOWghJXIooOcWrwOV2Q8z9qWXwoK +mMZ2oLS+z/7clXibK45KeRUeCX5DvQ== +=5oO1 +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-javadoc.jar.sha512 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-javadoc.jar.sha512 new file mode 100644 index 000000000000..eb02a04a89d2 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-javadoc.jar.sha512 @@ -0,0 +1 @@ +2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-javadoc.jar.sha512.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-javadoc.jar.sha512.asc new file mode 100644 index 000000000000..f35b726baff6 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-javadoc.jar.sha512.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT3d+AgAwQvlwnKQLLuiZADGL+I922/YG317N2Re1EjC6WlMRZUKXH54fckRTyPm +4ZLyxVHy8LlUD2Q10g69opb7HRd/tV0miBJhn5OU1wIM3hqTgxNp9EFckK4md45k +osnhQJNDsFToxJL8zPP+KRs/aWPZs+FrRcH6k26lwLl2gTfyBDsaU11HFRVEN9yi +X41obVyKiVNlc9efSSvlLtRBSVt0VhAFhck+3t61H6D9H09QxaDGAqmduDua3Tg3 +t5eqURuDfv3TfSztYgK3JBmG/6gVMsZodCgyC+8rhDDs6vSoDG30apx5Leg2rPbj +xuk2wi/WNzc94IgY9tVS3tAfT2k6yQ== +=6+Cv +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-sources.jar b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-sources.jar new file mode 100644 index 000000000000..a142d391c0af Binary files /dev/null and b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-sources.jar differ diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-sources.jar.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-sources.jar.asc new file mode 100644 index 000000000000..85d6a7cb1e5a --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-sources.jar.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT3qtgf8CeDvxzi7lPghlrZtmYTTjaic4FZsGRWTPky3H24i4wSRDhG0L5sj4uPK +eLLlITr5a9j26UCas9HRSthiC8+EgIMAhSN0X482SQhUZHAW67ErIvaHlwL+ixMD +0T5pmsW8PKN3lV1TFMhNYSEC2GRG/4GF+3yQA8LR+BgeEu/E5nmysIH8vuQMkOD6 +3pKA8VKNBml591j6UTqxoHtPX+rThaziz3Hy3+ekf5iWslllTTGPd2SWqTvnj2Ae +GvRzsbli+FEM0Aj/v8jUQnQzOz891QSvWR+fMfCqZimiJMc+GBzJ9umbcyQsB5tY +e26mAoYd9KEpGXMKN4biHbJZNp1GGw== +=x/MY +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-sources.jar.md5 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-sources.jar.md5 new file mode 100644 index 000000000000..95fa4af1641f --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-sources.jar.md5 @@ -0,0 +1 @@ +e84da489be91de821c95d41b8f0e0a0a \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-sources.jar.md5.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-sources.jar.md5.asc new file mode 100644 index 000000000000..1cbf6ccffa07 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-sources.jar.md5.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT1PSwf+InTdlh+BVVy3RFszEL5vN7waSPYMImXhgd5EZ1d4Lxd6EeOwtNgWNKpG +E+Ps/Kw0jZDvoD49WJlcUjzDHBNHcE7C/L3GAWHV6WwklhtQaJ4EegsynWdSXz6k +fqJY6r58aGKGjpKPutRWAjvfcdC170+ZRsc2oi9xrAgHCpvXzTjq4+O9Ah0t5jwW +jcZ/Xubcw4vjsw774OucHbtwGsvRN5SDJ3IONOH8WCwhUP5vEEKvA6MYX0KGoTdS +3wTCyZTzU3qtTWxcbTCpiJIWbYwRR7TzLB/uydWHlAMzuz6coIiBpYsGiO6wkmfg +W+QvcE7wyW2jtb22pCImLyObyZ21VA== +=VjDv +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-sources.jar.sha1 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-sources.jar.sha1 new file mode 100644 index 000000000000..2a2834e1ab41 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-sources.jar.sha1 @@ -0,0 +1 @@ +8992b17455ce660da9c5fe47226b7ded9e872637 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-sources.jar.sha1.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-sources.jar.sha1.asc new file mode 100644 index 000000000000..4cd212d6e1e9 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-sources.jar.sha1.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT2HQgf+MTUEnwzXK4zi76VI7C5cchGan26lIA2Ebq4vtYGKDqNSISOAAdWs9+nT +U6ZA6OIFo5qdeD6F/45s91IoDbxbhMDMFEsSijKASqiuZN5TZM1U2h2kWFAl/sEl +EI1RTygn+xDw/ah4V3/duuMFC+jRgvJ/LgemIF4KBvECWaTQKNu0fu5d4dPXMpp+ +jrxMEZPQZsivpOvklzV8O7wAkf/ZQhJdcB2m8uOfSPlJ91a4EEtXF9/GzzkXUi1P +bzt4NsmOag3227B3mO1Bc6yZdDBNu8wQ9apiJVCpqsxB9Dz0PCL4dHNa1u9g6Xo6 +ElRgneV4HZp+LB125VoNabKuNH00bw== +=2yDl +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-sources.jar.sha256 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-sources.jar.sha256 new file mode 100644 index 000000000000..4f27f01046dd --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-sources.jar.sha256 @@ -0,0 +1 @@ +10ce8a82f7e53c67f2af8d4dc4b8cdb4d0630d0e1d21818da4d5b3ca2de08385 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-sources.jar.sha256.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-sources.jar.sha256.asc new file mode 100644 index 000000000000..e29629cbec07 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-sources.jar.sha256.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT0ScQf7Bip+quFq1CzDCTDxUhdTTOIpQcCfMKo1Jegpa2Hlm63XuK+6zVI9u6S+ +dBXPdYeneMOqMPQUAaw3M06ZqohaDOt8Ti1EiQFGXCdOvbonTA52Lrd4EEZxwNnK +BdPuIh/8qCfozm5KbZe1bFyGVRAdNyf27KvzHgfBTirLtI+3MiOdL4bvNZbWRPfh +J84Ko+Ena8jPFgyz6nJv2Q2U/V3dCooLJAXs2vEG6owwk5J9zvSysWpHaJbXas5v +KXO9TOBBjf3+vxb1WVQa8ZYUU3+FIFes0RFVgOWghJXIooOcWrwOV2Q8z9qWXwoK +mMZ2oLS+z/7clXibK45KeRUeCX5DvQ== +=5oO1 +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-sources.jar.sha512 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-sources.jar.sha512 new file mode 100644 index 000000000000..eb02a04a89d2 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-sources.jar.sha512 @@ -0,0 +1 @@ +2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-sources.jar.sha512.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-sources.jar.sha512.asc new file mode 100644 index 000000000000..f35b726baff6 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0-sources.jar.sha512.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT3d+AgAwQvlwnKQLLuiZADGL+I922/YG317N2Re1EjC6WlMRZUKXH54fckRTyPm +4ZLyxVHy8LlUD2Q10g69opb7HRd/tV0miBJhn5OU1wIM3hqTgxNp9EFckK4md45k +osnhQJNDsFToxJL8zPP+KRs/aWPZs+FrRcH6k26lwLl2gTfyBDsaU11HFRVEN9yi +X41obVyKiVNlc9efSSvlLtRBSVt0VhAFhck+3t61H6D9H09QxaDGAqmduDua3Tg3 +t5eqURuDfv3TfSztYgK3JBmG/6gVMsZodCgyC+8rhDDs6vSoDG30apx5Leg2rPbj +xuk2wi/WNzc94IgY9tVS3tAfT2k6yQ== +=6+Cv +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.jar b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.jar new file mode 100644 index 000000000000..a142d391c0af Binary files /dev/null and b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.jar differ diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.jar.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.jar.asc new file mode 100644 index 000000000000..85d6a7cb1e5a --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.jar.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT3qtgf8CeDvxzi7lPghlrZtmYTTjaic4FZsGRWTPky3H24i4wSRDhG0L5sj4uPK +eLLlITr5a9j26UCas9HRSthiC8+EgIMAhSN0X482SQhUZHAW67ErIvaHlwL+ixMD +0T5pmsW8PKN3lV1TFMhNYSEC2GRG/4GF+3yQA8LR+BgeEu/E5nmysIH8vuQMkOD6 +3pKA8VKNBml591j6UTqxoHtPX+rThaziz3Hy3+ekf5iWslllTTGPd2SWqTvnj2Ae +GvRzsbli+FEM0Aj/v8jUQnQzOz891QSvWR+fMfCqZimiJMc+GBzJ9umbcyQsB5tY +e26mAoYd9KEpGXMKN4biHbJZNp1GGw== +=x/MY +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.jar.md5 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.jar.md5 new file mode 100644 index 000000000000..95fa4af1641f --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.jar.md5 @@ -0,0 +1 @@ +e84da489be91de821c95d41b8f0e0a0a \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.jar.md5.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.jar.md5.asc new file mode 100644 index 000000000000..1cbf6ccffa07 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.jar.md5.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT1PSwf+InTdlh+BVVy3RFszEL5vN7waSPYMImXhgd5EZ1d4Lxd6EeOwtNgWNKpG +E+Ps/Kw0jZDvoD49WJlcUjzDHBNHcE7C/L3GAWHV6WwklhtQaJ4EegsynWdSXz6k +fqJY6r58aGKGjpKPutRWAjvfcdC170+ZRsc2oi9xrAgHCpvXzTjq4+O9Ah0t5jwW +jcZ/Xubcw4vjsw774OucHbtwGsvRN5SDJ3IONOH8WCwhUP5vEEKvA6MYX0KGoTdS +3wTCyZTzU3qtTWxcbTCpiJIWbYwRR7TzLB/uydWHlAMzuz6coIiBpYsGiO6wkmfg +W+QvcE7wyW2jtb22pCImLyObyZ21VA== +=VjDv +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.jar.sha1 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.jar.sha1 new file mode 100644 index 000000000000..2a2834e1ab41 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.jar.sha1 @@ -0,0 +1 @@ +8992b17455ce660da9c5fe47226b7ded9e872637 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.jar.sha1.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.jar.sha1.asc new file mode 100644 index 000000000000..4cd212d6e1e9 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.jar.sha1.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT2HQgf+MTUEnwzXK4zi76VI7C5cchGan26lIA2Ebq4vtYGKDqNSISOAAdWs9+nT +U6ZA6OIFo5qdeD6F/45s91IoDbxbhMDMFEsSijKASqiuZN5TZM1U2h2kWFAl/sEl +EI1RTygn+xDw/ah4V3/duuMFC+jRgvJ/LgemIF4KBvECWaTQKNu0fu5d4dPXMpp+ +jrxMEZPQZsivpOvklzV8O7wAkf/ZQhJdcB2m8uOfSPlJ91a4EEtXF9/GzzkXUi1P +bzt4NsmOag3227B3mO1Bc6yZdDBNu8wQ9apiJVCpqsxB9Dz0PCL4dHNa1u9g6Xo6 +ElRgneV4HZp+LB125VoNabKuNH00bw== +=2yDl +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.jar.sha256 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.jar.sha256 new file mode 100644 index 000000000000..4f27f01046dd --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.jar.sha256 @@ -0,0 +1 @@ +10ce8a82f7e53c67f2af8d4dc4b8cdb4d0630d0e1d21818da4d5b3ca2de08385 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.jar.sha256.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.jar.sha256.asc new file mode 100644 index 000000000000..e29629cbec07 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.jar.sha256.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT0ScQf7Bip+quFq1CzDCTDxUhdTTOIpQcCfMKo1Jegpa2Hlm63XuK+6zVI9u6S+ +dBXPdYeneMOqMPQUAaw3M06ZqohaDOt8Ti1EiQFGXCdOvbonTA52Lrd4EEZxwNnK +BdPuIh/8qCfozm5KbZe1bFyGVRAdNyf27KvzHgfBTirLtI+3MiOdL4bvNZbWRPfh +J84Ko+Ena8jPFgyz6nJv2Q2U/V3dCooLJAXs2vEG6owwk5J9zvSysWpHaJbXas5v +KXO9TOBBjf3+vxb1WVQa8ZYUU3+FIFes0RFVgOWghJXIooOcWrwOV2Q8z9qWXwoK +mMZ2oLS+z/7clXibK45KeRUeCX5DvQ== +=5oO1 +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.jar.sha512 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.jar.sha512 new file mode 100644 index 000000000000..eb02a04a89d2 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.jar.sha512 @@ -0,0 +1 @@ +2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.jar.sha512.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.jar.sha512.asc new file mode 100644 index 000000000000..f35b726baff6 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.jar.sha512.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT3d+AgAwQvlwnKQLLuiZADGL+I922/YG317N2Re1EjC6WlMRZUKXH54fckRTyPm +4ZLyxVHy8LlUD2Q10g69opb7HRd/tV0miBJhn5OU1wIM3hqTgxNp9EFckK4md45k +osnhQJNDsFToxJL8zPP+KRs/aWPZs+FrRcH6k26lwLl2gTfyBDsaU11HFRVEN9yi +X41obVyKiVNlc9efSSvlLtRBSVt0VhAFhck+3t61H6D9H09QxaDGAqmduDua3Tg3 +t5eqURuDfv3TfSztYgK3JBmG/6gVMsZodCgyC+8rhDDs6vSoDG30apx5Leg2rPbj +xuk2wi/WNzc94IgY9tVS3tAfT2k6yQ== +=6+Cv +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.module b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.module new file mode 100644 index 000000000000..8618f194c99c --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.module @@ -0,0 +1,101 @@ +{ + "formatVersion": "1.1", + "component": { + "group": "org.springframework.example", + "module": "module-three", + "version": "1.0.0", + "attributes": { + "org.gradle.status": "release" + } + }, + "createdBy": { + "gradle": { + "version": "6.5.1", + "buildId": "mvqepqsdqjcahjl7cii6b6ucoe" + } + }, + "variants": [ + { + "name": "apiElements", + "attributes": { + "org.gradle.category": "library", + "org.gradle.dependency.bundling": "external", + "org.gradle.jvm.version": 8, + "org.gradle.libraryelements": "jar", + "org.gradle.usage": "java-api" + }, + "files": [ + { + "name": "module-three-1.0.0.jar", + "url": "module-three-1.0.0.jar", + "size": 261, + "sha512": "2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00", + "sha256": "10ce8a82f7e53c67f2af8d4dc4b8cdb4d0630d0e1d21818da4d5b3ca2de08385", + "sha1": "8992b17455ce660da9c5fe47226b7ded9e872637", + "md5": "e84da489be91de821c95d41b8f0e0a0a" + } + ] + }, + { + "name": "runtimeElements", + "attributes": { + "org.gradle.category": "library", + "org.gradle.dependency.bundling": "external", + "org.gradle.jvm.version": 8, + "org.gradle.libraryelements": "jar", + "org.gradle.usage": "java-runtime" + }, + "files": [ + { + "name": "module-three-1.0.0.jar", + "url": "module-three-1.0.0.jar", + "size": 261, + "sha512": "2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00", + "sha256": "10ce8a82f7e53c67f2af8d4dc4b8cdb4d0630d0e1d21818da4d5b3ca2de08385", + "sha1": "8992b17455ce660da9c5fe47226b7ded9e872637", + "md5": "e84da489be91de821c95d41b8f0e0a0a" + } + ] + }, + { + "name": "javadocElements", + "attributes": { + "org.gradle.category": "documentation", + "org.gradle.dependency.bundling": "external", + "org.gradle.docstype": "javadoc", + "org.gradle.usage": "java-runtime" + }, + "files": [ + { + "name": "module-three-1.0.0-javadoc.jar", + "url": "module-three-1.0.0-javadoc.jar", + "size": 261, + "sha512": "2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00", + "sha256": "10ce8a82f7e53c67f2af8d4dc4b8cdb4d0630d0e1d21818da4d5b3ca2de08385", + "sha1": "8992b17455ce660da9c5fe47226b7ded9e872637", + "md5": "e84da489be91de821c95d41b8f0e0a0a" + } + ] + }, + { + "name": "sourcesElements", + "attributes": { + "org.gradle.category": "documentation", + "org.gradle.dependency.bundling": "external", + "org.gradle.docstype": "sources", + "org.gradle.usage": "java-runtime" + }, + "files": [ + { + "name": "module-three-1.0.0-sources.jar", + "url": "module-three-1.0.0-sources.jar", + "size": 261, + "sha512": "2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00", + "sha256": "10ce8a82f7e53c67f2af8d4dc4b8cdb4d0630d0e1d21818da4d5b3ca2de08385", + "sha1": "8992b17455ce660da9c5fe47226b7ded9e872637", + "md5": "e84da489be91de821c95d41b8f0e0a0a" + } + ] + } + ] +} diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.module.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.module.asc new file mode 100644 index 000000000000..f7112bc8e54a --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.module.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT3OcAf+OJv0t0rhNnJcF656mem5qv3fvcJKkPqyKF9I0TiP33W61/ntrGezdaDX +tLde1MFRto3HS0/U0t6NqfMNTXYcQ5vH/qqnIRWP7Iv/t7f+mum6pOcYkxJhhXFT +1pH0l4iqVQOBUiAJhOpUh0utLNWdZcEv+DdxgtFbFyaEDmg46Cpy9YtAH6XKEh5d +ZZeiX/+XC+Ufx1bReDLHvFjUyQa/Lv8rEthX2eBmAXkoPwJG0LA9xF6X8leB0DI/ +9a1KiNcmRSSUarLpqV/hE6oQggGeMLVoJ+51klunRAfiXw6h2m9gRlnWikLjC+23 +/E2m+7Gb0Kc4izXIdHTqS2fYPMHsyw== +=h486 +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.module.md5 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.module.md5 new file mode 100644 index 000000000000..c5a7b486933a --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.module.md5 @@ -0,0 +1 @@ +90fa60dcc829042dd1208c174752caeb \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.module.md5.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.module.md5.asc new file mode 100644 index 000000000000..83f03d877951 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.module.md5.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT0xjQgAwJcUWVwcl3PI7FhRUoPaPfqaoG3bUPwLePYuf++qPCNUDOmnq0aXtqbr +Ul9SxQRDy9D7ygCWVCTVXRjg0HHZQT/ZYB7lhaDLxMEpV25q9acJAZ4qzbn8vRAG +FqlqYaSlIDducapPUGWAOF/xwhf5k8tIGO5p0hY4wdU3b+0YU1w5DavYOetTZ26Z +jwVagOj/6WFIHnu6PwXGkynqxui8dnsld23eamOZYsfR19weTNh0GT3ncl8y03eP +Wy6CkFxzN0kvSdze0nAfO9dygpRxh7nNbz/uJhTFP4pDwz5iE8FBiUXZLkxTZ8YJ +lvIKuS+JnUTEQcd00/nAL4cncMiJIQ== +=lQP9 +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.module.sha1 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.module.sha1 new file mode 100644 index 000000000000..4b25265d7253 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.module.sha1 @@ -0,0 +1 @@ +9090f5fcb4c79a89b3a46f2cb8071381e0787a03 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.module.sha1.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.module.sha1.asc new file mode 100644 index 000000000000..cfa8f7e8880f --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.module.sha1.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT3urQf/W31jKnVjCIckj7XFbeucazmVr0K73LNpg0eQwqqz877KKmBDV8qn8b3o +MTBDgUn/9LMJzUSWRFV+CkM0cgAG0s8vmzeymtH6RWv+ikHh/3Ky4sYxd9Pa3Ipo +zeeIqyJk1dysfcLLsP1ml6ayh8VM/DK+DDc4CU9wrEGAUDeVIFiTw7DrMIB7PcdG +ru7z6J/jcIA55RiJMDvuqhS+Obx/JUrmqDrrK8Npp9stRP+RpZpF1AKGgg1dfLo1 +bKw+KYuMhK7Kq7nIg9GqZvhr46oOKko5NF2l+GfVR14Gdb330/88t/IxwJvsUiCC +sWQTrGJb062N5oHGtdoZ3mXLo7bnuw== +=+snH +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.module.sha256 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.module.sha256 new file mode 100644 index 000000000000..346a5ff6e495 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.module.sha256 @@ -0,0 +1 @@ +2e8d3db6ece5719d7be27bcfdefa1f890da9a19f73390c7122797db1081d245b \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.module.sha256.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.module.sha256.asc new file mode 100644 index 000000000000..41d0ccfe35c6 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.module.sha256.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT14OggAlf9eyYFV3HRC7LoeM1Q9LrkYZUIDIUkukUxDxBTGPLf739qZtHgUl6lC +yUCQqGswhuuwR8s7ht2MDMp8isjs1j7inpAQA3kYgHOCUMjYlIyhPdIxHtQ8WD+S +CwW2nHtf7tXFrhKFecqolKyp+qZYWx1anMmbLggyaXWZmiIwhIHLxIogbyjVLdkD +9qUAKCUpEvyNqogyYYtAjJERRzw9RN4lwnpm/uEkKtFQVoxui2VQr/DEbzooXu8A +mqKkUBbgf9uxH5s+pUuUgbl+XZnPLGzJV6NcFe/jpsvEzHkUQzAsVnNnCWAPreY8 +RTfj2eGleFWESIiMFUAp6U0an5GoOQ== +=T+Cv +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.module.sha512 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.module.sha512 new file mode 100644 index 000000000000..1817ab28cb61 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.module.sha512 @@ -0,0 +1 @@ +ac3e2a0dfb3b8ddaa79468f85698ff97e9b88e401849e2e733073b711a1673ba2e37be61d224fd69ec2e1c59ed0342e6b3757bc3931c2e297d24bcae9370fa3b \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.module.sha512.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.module.sha512.asc new file mode 100644 index 000000000000..1f6bfda96678 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.module.sha512.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT1HGQgAuINmQJ5vpFWWmXbIrEVf5+fTKq72R7gXdJ9XHYgQdSyKoeUUy3FElqfI +55gyiLk1OMMy6Pd1HKi0bczOUOlz8K34uMXcT+ctm41Lp6243FfLm4iy1x/DWlHb +IWksIG1TRf7g0b//OiBbbaesjnc5QK1rft6T4KiEPD9NtOi/8ON7vVu0S9oERUGO +32Zwu/wGZeKztUoXVQ/zZk5UA9hYE/7C5bX3dRBS038luv7YZKe3313PfVj29vdx +bsfRIcH/qIe//WL3OTTbaOvSgOs8qvJHPN8NmdH70GbZ2W9jTe7KrIlb8FBEaPEj +BbLov9td9qxXlRxyBhLYRB7MN4rsKw== +=qIiQ +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom new file mode 100644 index 000000000000..d67dcd644a31 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom @@ -0,0 +1,49 @@ + + + + + + + + 4.0.0 + org.springframework.example + module-three + 1.0.0 + module-three + Example module + https://spring.io/projects/spring-boot + + Pivotal Software, Inc. + https://spring.io + + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0 + + + + + Pivotal + info@pivotal.io + Pivotal Software, Inc. + https://www.spring.io + + + + + scm:git:git://github.com/spring-projects/spring-boot.git + + + scm:git:ssh://git@github.com/spring-projects/spring-boot.git + + https://github.com/spring-projects/spring-boot + + + GitHub + + https://github.com/spring-projects/spring-boot/issues + + + diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom.asc new file mode 100644 index 000000000000..eb100ce746aa --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT0T5QgAgXfcX/6hkGNWRp4xIbpC0P9wi21WBmlWeM1l1vPjlogcPB5fIQ15tnxL +dyXVJjhdBXG70m5UkOtR5LbO+6Y7soEsocfuN/wdjNP/JUk2xW4HTj87F16r3EhV +s1nrydd/nZxsIemTY1irOrCk4yEOWlAO91VOGFI4UoGGE6oeMiTFje6vbNidGT3Y +RD1VrxbVasI38HHggQ+odrdp+rk8AwAUJq8g96KyRO5d+O6NQCf4cTe6S5+kJKG1 +ETQ0yASHiD5pzcpQiEQu+wclgAunVAzr5Ql/SnOcZEjUoVOLix7Ttcv5KcXjZhY9 +9VQyULZ1MzcrSEoRoOv8k8fT7swvLg== +=KgwJ +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom.md5 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom.md5 new file mode 100644 index 000000000000..e156ed030237 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom.md5 @@ -0,0 +1 @@ +a819cb79241de23f94f3626ae83be12f \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom.md5.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom.md5.asc new file mode 100644 index 000000000000..f55b2670d4e8 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom.md5.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT2Umgf+K57ogLEAGx/40dFOV0yiGmvCXwywMCVbnMXCA85Ceti0TGFY6T0EaJXy +wF7QQ0SW56svIxX/U58IVocWuaVRJA7tCZF1u9DCvafdYDJeM4iHHVu6GzM1Tng2 +JFYV4q5MtT712rCrcf7ZH3MntYawsGjBiF9IHWwvSNUyf53W7L4VSWcpv0tfPLra +EeC7ztnnDXgi32FSpXvu27mDPbrQLibihUZBjoZ4uuRU2wB6HICJ90JjoYtK6JoC +ToEZY4jFLkEmQ8dy0KUa5rhUDWJ+Bq+bYHhwMXl9HQUKZuqjvlmHCRHIsJgdU+Cl +i5NPJkXhCZOs9tD3hf3NdeD9ef72kQ== +=PraK +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom.sha1 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom.sha1 new file mode 100644 index 000000000000..81ed28e231d7 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom.sha1 @@ -0,0 +1 @@ +e562eacb5f9cf14ccbb80c8be0396710fc6a546c \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom.sha1.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom.sha1.asc new file mode 100644 index 000000000000..53461b47a075 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom.sha1.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT2j8QgAiciViDfKLAv2rYMyBJbyQjK29fpG78NMsKw+j3zWwJEPlPuZhIT0/KWQ +3ipcYbtBoKYrKSG54uzPflGAQoostMYV+XtJ+0fjICsNDpKjfhuDWojaWkxnF1KD +NcWSiapNO6iX0s62yaL/netVVsHsE5fVr//IG6WDTrJK2GEkOQAoca2W8ixI3G0s +kTIJEmCMA9ZOUMKBwwtJ0NPEZPxe1N/R6SuGWGdkWlrqPRmA6lnY153zH3vJ6pqF +RM1Phwpt46l3o20D5wOhqU8jvV7b5HUZ50sHV0sJOMUbwvFyrhIOzJLMixk1WJhR +lnudbpWPssTJO3Fiv67b/iADaBaIbA== +=4CYs +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom.sha256 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom.sha256 new file mode 100644 index 000000000000..381f84d9d480 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom.sha256 @@ -0,0 +1 @@ +f043a098eee190d59e2b4b450c2de21dcb3b7fce224abefd89e0e5b44b939fa7 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom.sha256.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom.sha256.asc new file mode 100644 index 000000000000..b9b35e5371e6 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom.sha256.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT3Pwwf+IikjoDdjeMQfmERTN4Gjirx9+fler+Gr5JYC78OxLrB8uq0tn11wEJMQ +ZDQe83OYjEkFQhPn6yQ5bc9edZTJztQJcGpVe7NkYffS4geo+ahuksaQWMF9opEc +OqBB5fTWVt18qGFTI2F3CEDIo38muTgkzndFuzmcbVhAcknF5ybcVDIFpZNlnqe/ +xflD6lupXWXQC4nE4n2EVhNnmkiF9nKRJWwEJ1hoy1gwTxSYmnO/BLn8ob4tswnc +gHnj4Yp7qoV/SnRnfNDHMPQMfzAwq1jucjjPOt3xkw17UIGBnrP1zAxeZFmarCTO +YjJt5PUNwtqOVHlHzwgn3b3FGRJe3A== +=AU99 +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom.sha512 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom.sha512 new file mode 100644 index 000000000000..fb6a445480ea --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom.sha512 @@ -0,0 +1 @@ +67549e36f07c9f4b83cfa9f00fa647705128eabd03dec3df0066177e89940652fc7e13b8c0b60652c811e167c3ffaba0ab95a4f315a10f69572c12e4e944b6e6 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom.sha512.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom.sha512.asc new file mode 100644 index 000000000000..71232b747c6f --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom.sha512.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT0bBgf+Nm9vooXNKE/z4cNeFqiHwLb+gMxvqlKnl/+03KbuFvlDUwdqSxfnHQZ3 +qfCBtIe6MN/0I5FCNRCcFxiCjCPDqSMAcvRPU5UOG2M2W0uvcqMwKO6KRGBLd65J +MulDxoe7LrEk6KwNYfecxCtXBvLwzwQd3A10tcQOVl66neF/g+9jEc+MHJPa1xi5 +4pDOo3TQ4EpGfB8eF9Z+7YKc9hPYBFsm+n3P6SYcVAiRUiNBE8gCOvvZE9mOTQAo +yC80AfDjXe/YBsd3a79hVW+ESQAKfK8S1RzO+c5GhIZCIJFdSJDOeww86O4U0n5g +6hIXRNWUFUEueAvQ2dYHZujQSBxNig== +=t5De +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-javadoc.jar b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-javadoc.jar new file mode 100644 index 000000000000..a142d391c0af Binary files /dev/null and b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-javadoc.jar differ diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-javadoc.jar.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-javadoc.jar.asc new file mode 100644 index 000000000000..624356b34ae0 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-javadoc.jar.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX +xT1IkAf+N8ts9K88PC/dISZMWSrgRbSugd6VRG6pto6jsp9cJBzTzQ6C8psMkXcN +qX0fnLBn4Zn1dovxRIrA8QeT/vxHMl1X0Foe6SUTMjK34Ofq4V1FVlhuJIi0YZrP +L7B4cKTkv1ndwSVgE23zkynfaIPiPl1uZOwDmlpArokqnjSiUq9NndtKf87NwekW +hbf7brgfZddeDj9xhAn5hz2pHUhx/uH9tOX3JlZgE+yATZsGm6Z9BSf4Lur0W85P +hrJ+MfuYPzZ3n7okuaQdMT3QIqe3FO+dfGZKwakw2NWfgWP3LZsQ5rbyxBlyH7CG +JA/VAIIqe99ftHcBFRGB6C9hMn4FcA== +=prNt +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-javadoc.jar.md5 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-javadoc.jar.md5 new file mode 100644 index 000000000000..95fa4af1641f --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-javadoc.jar.md5 @@ -0,0 +1 @@ +e84da489be91de821c95d41b8f0e0a0a \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-javadoc.jar.md5.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-javadoc.jar.md5.asc new file mode 100644 index 000000000000..333ba4509b3f --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-javadoc.jar.md5.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX +xT3jzQf8DUis2+v7616JWRpUziEnpnvms7+PkgovWttcpbO0RlLr1s0Uno0D0jAt +7KUGNN4//n8hKsojZ6ZI+7pzsk/0NathOQRNYcdb+AFf71T3yJhef1d5GmXHA7iV +wA6AfrFTEQ7SaimZXEGGpFXzb3rPsVnryOEbTOXno3B7nNjZTUpjkW/APkvJueUk +BIFCWH9rL1txRKWhKg8f6YT+l6HQFn+qu1Z3/MoqxCn6HvUxExA1mwNbzfvNaDTt +l04jNVG6NqZyGhivuDnpyonnmwKySryKVvGrWn6b5SfgPPQYQJTWK3c7npSPjKpO +ydzWOvISS55vBKLbB7g69g8ah3FHEw== +=fcEr +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-javadoc.jar.sha1 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-javadoc.jar.sha1 new file mode 100644 index 000000000000..2a2834e1ab41 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-javadoc.jar.sha1 @@ -0,0 +1 @@ +8992b17455ce660da9c5fe47226b7ded9e872637 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-javadoc.jar.sha1.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-javadoc.jar.sha1.asc new file mode 100644 index 000000000000..66ae31b24c26 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-javadoc.jar.sha1.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX +xT0ovgf/aejEen2MEvJTF1Tjg24rK1jZAYqmgGi8H2b26h7ZEd/le2jWs6VpPmvv +DX3pyaKFyOZXpU8SOkoPgi021VIt9LLWPlxMgcWlb6EWg8xw70PXISbUFy3IcxRi +I14uAUXoFgIOT6jPt659kdXLNtYRsS3nQcBgJTIz6axHk2t5tD3TRf4xcLCyuVGv +/obkTwpLr2jdPBxgTe+oDPjCnOyI6YeN0dKq4aiGBI/xECNpitbzmYQA2FQ+WvsG +qq+1n/eAZAzAUWumxLna9ov1O0f6cY9d9hxWMTe2L4/a7B6KezF0CPnShFaC6pCV +98aamE5QxBmSeQtmRdI75WFHJ1h0Vg== +=0M4U +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-javadoc.jar.sha256 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-javadoc.jar.sha256 new file mode 100644 index 000000000000..4f27f01046dd --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-javadoc.jar.sha256 @@ -0,0 +1 @@ +10ce8a82f7e53c67f2af8d4dc4b8cdb4d0630d0e1d21818da4d5b3ca2de08385 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-javadoc.jar.sha256.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-javadoc.jar.sha256.asc new file mode 100644 index 000000000000..825ed5de96dd --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-javadoc.jar.sha256.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX +xT104Qf+NG6IUBLo7eTPtPzWyNs9bShrJR759atbY9kBOrRfyoBM/hWrcr6pG4e5 +BmBPULkuBWLVM+Ra52B2S96848oiaLLEuiWlMudWRsWCxyVknjm9EKlMHA3VdQtu +8grrPKS4mIwSvdzAEEeqR+mwtWXHlz+jc/R9NeQhNmmcNZv1nkyzCuoNCH/HMTl4 +/ei6enpYrYNnMrNz9TOMQ67sCtZEm6TaxlqS+9h/V9TEnq6+1qXEt4c+AsqQdMWH +3BZREzXHFocQciSEXfL6m07pnNlnvCcjsM2SAqTeTqQupEmqFGkhL2blE1VMdplW +fDCC/ee5JYVyyUXpzydSjCYwFbO3NQ== +=gSEv +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-javadoc.jar.sha512 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-javadoc.jar.sha512 new file mode 100644 index 000000000000..eb02a04a89d2 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-javadoc.jar.sha512 @@ -0,0 +1 @@ +2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-javadoc.jar.sha512.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-javadoc.jar.sha512.asc new file mode 100644 index 000000000000..7df8aa894e07 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-javadoc.jar.sha512.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX +xT0XdAf/feMRtW2BUz84x43aYLaERIEPx2TsqMUwVcov6sp5MWaSAJEX2vrscCRB +K/7x4N3dxnrckc3sBF1hs+zrRwySYU1FyGVIxQxdeURnUWxCva0uOWj91jcUOIkA +gpmOZZj51b4SkB6GZjtvN/Z3B4xzEPmTPfKFiBZPhuYW4HiC8FHM1JnlW6h2xoj6 +Bja//qoj9ccfRjiMnnI0iPgZNiyR8n8+EJGi0ykizxkiT6cWI84kZ+JQYooDHbCf +NgPt2NjcGzd6SGrQW8/0td0N+xDRfLpTrbfTmlC5hikXmS0e79BV6W0eYcWcgDni +r8WjbDmomHHFDhT8p1R+tGtd8p2txg== +=BHvx +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-sources.jar b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-sources.jar new file mode 100644 index 000000000000..a142d391c0af Binary files /dev/null and b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-sources.jar differ diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-sources.jar.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-sources.jar.asc new file mode 100644 index 000000000000..624356b34ae0 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-sources.jar.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX +xT1IkAf+N8ts9K88PC/dISZMWSrgRbSugd6VRG6pto6jsp9cJBzTzQ6C8psMkXcN +qX0fnLBn4Zn1dovxRIrA8QeT/vxHMl1X0Foe6SUTMjK34Ofq4V1FVlhuJIi0YZrP +L7B4cKTkv1ndwSVgE23zkynfaIPiPl1uZOwDmlpArokqnjSiUq9NndtKf87NwekW +hbf7brgfZddeDj9xhAn5hz2pHUhx/uH9tOX3JlZgE+yATZsGm6Z9BSf4Lur0W85P +hrJ+MfuYPzZ3n7okuaQdMT3QIqe3FO+dfGZKwakw2NWfgWP3LZsQ5rbyxBlyH7CG +JA/VAIIqe99ftHcBFRGB6C9hMn4FcA== +=prNt +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-sources.jar.md5 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-sources.jar.md5 new file mode 100644 index 000000000000..95fa4af1641f --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-sources.jar.md5 @@ -0,0 +1 @@ +e84da489be91de821c95d41b8f0e0a0a \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-sources.jar.md5.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-sources.jar.md5.asc new file mode 100644 index 000000000000..1cbf6ccffa07 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-sources.jar.md5.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT1PSwf+InTdlh+BVVy3RFszEL5vN7waSPYMImXhgd5EZ1d4Lxd6EeOwtNgWNKpG +E+Ps/Kw0jZDvoD49WJlcUjzDHBNHcE7C/L3GAWHV6WwklhtQaJ4EegsynWdSXz6k +fqJY6r58aGKGjpKPutRWAjvfcdC170+ZRsc2oi9xrAgHCpvXzTjq4+O9Ah0t5jwW +jcZ/Xubcw4vjsw774OucHbtwGsvRN5SDJ3IONOH8WCwhUP5vEEKvA6MYX0KGoTdS +3wTCyZTzU3qtTWxcbTCpiJIWbYwRR7TzLB/uydWHlAMzuz6coIiBpYsGiO6wkmfg +W+QvcE7wyW2jtb22pCImLyObyZ21VA== +=VjDv +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-sources.jar.sha1 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-sources.jar.sha1 new file mode 100644 index 000000000000..2a2834e1ab41 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-sources.jar.sha1 @@ -0,0 +1 @@ +8992b17455ce660da9c5fe47226b7ded9e872637 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-sources.jar.sha1.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-sources.jar.sha1.asc new file mode 100644 index 000000000000..4cd212d6e1e9 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-sources.jar.sha1.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT2HQgf+MTUEnwzXK4zi76VI7C5cchGan26lIA2Ebq4vtYGKDqNSISOAAdWs9+nT +U6ZA6OIFo5qdeD6F/45s91IoDbxbhMDMFEsSijKASqiuZN5TZM1U2h2kWFAl/sEl +EI1RTygn+xDw/ah4V3/duuMFC+jRgvJ/LgemIF4KBvECWaTQKNu0fu5d4dPXMpp+ +jrxMEZPQZsivpOvklzV8O7wAkf/ZQhJdcB2m8uOfSPlJ91a4EEtXF9/GzzkXUi1P +bzt4NsmOag3227B3mO1Bc6yZdDBNu8wQ9apiJVCpqsxB9Dz0PCL4dHNa1u9g6Xo6 +ElRgneV4HZp+LB125VoNabKuNH00bw== +=2yDl +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-sources.jar.sha256 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-sources.jar.sha256 new file mode 100644 index 000000000000..4f27f01046dd --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-sources.jar.sha256 @@ -0,0 +1 @@ +10ce8a82f7e53c67f2af8d4dc4b8cdb4d0630d0e1d21818da4d5b3ca2de08385 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-sources.jar.sha256.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-sources.jar.sha256.asc new file mode 100644 index 000000000000..825ed5de96dd --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-sources.jar.sha256.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX +xT104Qf+NG6IUBLo7eTPtPzWyNs9bShrJR759atbY9kBOrRfyoBM/hWrcr6pG4e5 +BmBPULkuBWLVM+Ra52B2S96848oiaLLEuiWlMudWRsWCxyVknjm9EKlMHA3VdQtu +8grrPKS4mIwSvdzAEEeqR+mwtWXHlz+jc/R9NeQhNmmcNZv1nkyzCuoNCH/HMTl4 +/ei6enpYrYNnMrNz9TOMQ67sCtZEm6TaxlqS+9h/V9TEnq6+1qXEt4c+AsqQdMWH +3BZREzXHFocQciSEXfL6m07pnNlnvCcjsM2SAqTeTqQupEmqFGkhL2blE1VMdplW +fDCC/ee5JYVyyUXpzydSjCYwFbO3NQ== +=gSEv +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-sources.jar.sha512 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-sources.jar.sha512 new file mode 100644 index 000000000000..eb02a04a89d2 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-sources.jar.sha512 @@ -0,0 +1 @@ +2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-sources.jar.sha512.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-sources.jar.sha512.asc new file mode 100644 index 000000000000..f35b726baff6 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0-sources.jar.sha512.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT3d+AgAwQvlwnKQLLuiZADGL+I922/YG317N2Re1EjC6WlMRZUKXH54fckRTyPm +4ZLyxVHy8LlUD2Q10g69opb7HRd/tV0miBJhn5OU1wIM3hqTgxNp9EFckK4md45k +osnhQJNDsFToxJL8zPP+KRs/aWPZs+FrRcH6k26lwLl2gTfyBDsaU11HFRVEN9yi +X41obVyKiVNlc9efSSvlLtRBSVt0VhAFhck+3t61H6D9H09QxaDGAqmduDua3Tg3 +t5eqURuDfv3TfSztYgK3JBmG/6gVMsZodCgyC+8rhDDs6vSoDG30apx5Leg2rPbj +xuk2wi/WNzc94IgY9tVS3tAfT2k6yQ== +=6+Cv +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.jar b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.jar new file mode 100644 index 000000000000..a142d391c0af Binary files /dev/null and b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.jar differ diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.jar.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.jar.asc new file mode 100644 index 000000000000..624356b34ae0 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.jar.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX +xT1IkAf+N8ts9K88PC/dISZMWSrgRbSugd6VRG6pto6jsp9cJBzTzQ6C8psMkXcN +qX0fnLBn4Zn1dovxRIrA8QeT/vxHMl1X0Foe6SUTMjK34Ofq4V1FVlhuJIi0YZrP +L7B4cKTkv1ndwSVgE23zkynfaIPiPl1uZOwDmlpArokqnjSiUq9NndtKf87NwekW +hbf7brgfZddeDj9xhAn5hz2pHUhx/uH9tOX3JlZgE+yATZsGm6Z9BSf4Lur0W85P +hrJ+MfuYPzZ3n7okuaQdMT3QIqe3FO+dfGZKwakw2NWfgWP3LZsQ5rbyxBlyH7CG +JA/VAIIqe99ftHcBFRGB6C9hMn4FcA== +=prNt +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.jar.md5 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.jar.md5 new file mode 100644 index 000000000000..95fa4af1641f --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.jar.md5 @@ -0,0 +1 @@ +e84da489be91de821c95d41b8f0e0a0a \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.jar.md5.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.jar.md5.asc new file mode 100644 index 000000000000..333ba4509b3f --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.jar.md5.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX +xT3jzQf8DUis2+v7616JWRpUziEnpnvms7+PkgovWttcpbO0RlLr1s0Uno0D0jAt +7KUGNN4//n8hKsojZ6ZI+7pzsk/0NathOQRNYcdb+AFf71T3yJhef1d5GmXHA7iV +wA6AfrFTEQ7SaimZXEGGpFXzb3rPsVnryOEbTOXno3B7nNjZTUpjkW/APkvJueUk +BIFCWH9rL1txRKWhKg8f6YT+l6HQFn+qu1Z3/MoqxCn6HvUxExA1mwNbzfvNaDTt +l04jNVG6NqZyGhivuDnpyonnmwKySryKVvGrWn6b5SfgPPQYQJTWK3c7npSPjKpO +ydzWOvISS55vBKLbB7g69g8ah3FHEw== +=fcEr +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.jar.sha1 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.jar.sha1 new file mode 100644 index 000000000000..2a2834e1ab41 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.jar.sha1 @@ -0,0 +1 @@ +8992b17455ce660da9c5fe47226b7ded9e872637 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.jar.sha1.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.jar.sha1.asc new file mode 100644 index 000000000000..66ae31b24c26 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.jar.sha1.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX +xT0ovgf/aejEen2MEvJTF1Tjg24rK1jZAYqmgGi8H2b26h7ZEd/le2jWs6VpPmvv +DX3pyaKFyOZXpU8SOkoPgi021VIt9LLWPlxMgcWlb6EWg8xw70PXISbUFy3IcxRi +I14uAUXoFgIOT6jPt659kdXLNtYRsS3nQcBgJTIz6axHk2t5tD3TRf4xcLCyuVGv +/obkTwpLr2jdPBxgTe+oDPjCnOyI6YeN0dKq4aiGBI/xECNpitbzmYQA2FQ+WvsG +qq+1n/eAZAzAUWumxLna9ov1O0f6cY9d9hxWMTe2L4/a7B6KezF0CPnShFaC6pCV +98aamE5QxBmSeQtmRdI75WFHJ1h0Vg== +=0M4U +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.jar.sha256 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.jar.sha256 new file mode 100644 index 000000000000..4f27f01046dd --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.jar.sha256 @@ -0,0 +1 @@ +10ce8a82f7e53c67f2af8d4dc4b8cdb4d0630d0e1d21818da4d5b3ca2de08385 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.jar.sha256.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.jar.sha256.asc new file mode 100644 index 000000000000..825ed5de96dd --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.jar.sha256.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX +xT104Qf+NG6IUBLo7eTPtPzWyNs9bShrJR759atbY9kBOrRfyoBM/hWrcr6pG4e5 +BmBPULkuBWLVM+Ra52B2S96848oiaLLEuiWlMudWRsWCxyVknjm9EKlMHA3VdQtu +8grrPKS4mIwSvdzAEEeqR+mwtWXHlz+jc/R9NeQhNmmcNZv1nkyzCuoNCH/HMTl4 +/ei6enpYrYNnMrNz9TOMQ67sCtZEm6TaxlqS+9h/V9TEnq6+1qXEt4c+AsqQdMWH +3BZREzXHFocQciSEXfL6m07pnNlnvCcjsM2SAqTeTqQupEmqFGkhL2blE1VMdplW +fDCC/ee5JYVyyUXpzydSjCYwFbO3NQ== +=gSEv +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.jar.sha512 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.jar.sha512 new file mode 100644 index 000000000000..eb02a04a89d2 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.jar.sha512 @@ -0,0 +1 @@ +2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.jar.sha512.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.jar.sha512.asc new file mode 100644 index 000000000000..f35b726baff6 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.jar.sha512.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT3d+AgAwQvlwnKQLLuiZADGL+I922/YG317N2Re1EjC6WlMRZUKXH54fckRTyPm +4ZLyxVHy8LlUD2Q10g69opb7HRd/tV0miBJhn5OU1wIM3hqTgxNp9EFckK4md45k +osnhQJNDsFToxJL8zPP+KRs/aWPZs+FrRcH6k26lwLl2gTfyBDsaU11HFRVEN9yi +X41obVyKiVNlc9efSSvlLtRBSVt0VhAFhck+3t61H6D9H09QxaDGAqmduDua3Tg3 +t5eqURuDfv3TfSztYgK3JBmG/6gVMsZodCgyC+8rhDDs6vSoDG30apx5Leg2rPbj +xuk2wi/WNzc94IgY9tVS3tAfT2k6yQ== +=6+Cv +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.module b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.module new file mode 100644 index 000000000000..23e5ef1229ad --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.module @@ -0,0 +1,101 @@ +{ + "formatVersion": "1.1", + "component": { + "group": "org.springframework.example", + "module": "module-two", + "version": "1.0.0", + "attributes": { + "org.gradle.status": "release" + } + }, + "createdBy": { + "gradle": { + "version": "6.5.1", + "buildId": "mvqepqsdqjcahjl7cii6b6ucoe" + } + }, + "variants": [ + { + "name": "apiElements", + "attributes": { + "org.gradle.category": "library", + "org.gradle.dependency.bundling": "external", + "org.gradle.jvm.version": 8, + "org.gradle.libraryelements": "jar", + "org.gradle.usage": "java-api" + }, + "files": [ + { + "name": "module-two-1.0.0.jar", + "url": "module-two-1.0.0.jar", + "size": 261, + "sha512": "2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00", + "sha256": "10ce8a82f7e53c67f2af8d4dc4b8cdb4d0630d0e1d21818da4d5b3ca2de08385", + "sha1": "8992b17455ce660da9c5fe47226b7ded9e872637", + "md5": "e84da489be91de821c95d41b8f0e0a0a" + } + ] + }, + { + "name": "runtimeElements", + "attributes": { + "org.gradle.category": "library", + "org.gradle.dependency.bundling": "external", + "org.gradle.jvm.version": 8, + "org.gradle.libraryelements": "jar", + "org.gradle.usage": "java-runtime" + }, + "files": [ + { + "name": "module-two-1.0.0.jar", + "url": "module-two-1.0.0.jar", + "size": 261, + "sha512": "2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00", + "sha256": "10ce8a82f7e53c67f2af8d4dc4b8cdb4d0630d0e1d21818da4d5b3ca2de08385", + "sha1": "8992b17455ce660da9c5fe47226b7ded9e872637", + "md5": "e84da489be91de821c95d41b8f0e0a0a" + } + ] + }, + { + "name": "javadocElements", + "attributes": { + "org.gradle.category": "documentation", + "org.gradle.dependency.bundling": "external", + "org.gradle.docstype": "javadoc", + "org.gradle.usage": "java-runtime" + }, + "files": [ + { + "name": "module-two-1.0.0-javadoc.jar", + "url": "module-two-1.0.0-javadoc.jar", + "size": 261, + "sha512": "2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00", + "sha256": "10ce8a82f7e53c67f2af8d4dc4b8cdb4d0630d0e1d21818da4d5b3ca2de08385", + "sha1": "8992b17455ce660da9c5fe47226b7ded9e872637", + "md5": "e84da489be91de821c95d41b8f0e0a0a" + } + ] + }, + { + "name": "sourcesElements", + "attributes": { + "org.gradle.category": "documentation", + "org.gradle.dependency.bundling": "external", + "org.gradle.docstype": "sources", + "org.gradle.usage": "java-runtime" + }, + "files": [ + { + "name": "module-two-1.0.0-sources.jar", + "url": "module-two-1.0.0-sources.jar", + "size": 261, + "sha512": "2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00", + "sha256": "10ce8a82f7e53c67f2af8d4dc4b8cdb4d0630d0e1d21818da4d5b3ca2de08385", + "sha1": "8992b17455ce660da9c5fe47226b7ded9e872637", + "md5": "e84da489be91de821c95d41b8f0e0a0a" + } + ] + } + ] +} diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.module.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.module.asc new file mode 100644 index 000000000000..3cf7219f0929 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.module.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX +xT2SwAf/SDu2XlOfyL+oTidHItm3DmLsRY0xU2d5siCuasxxpzIPG+O5f3o7VeNS +pKUctb+Vbx7Za+tPYts4ztG+bqVUVZbtn9ERWCXAvuuAnbJMxIl4D7HXahZPJKtl +UrpKgA+45p2NLB9MK5B9QkmZInxF0ex3IUkc6e3MN8pmcefcjjDpoEvWKlc+ocEA +/ySwMcH38FRYB6XbwsAjdXm7jiLpA9ZA5MdfZzjmm3nRBDzujBjU/Pv1+PFPH4Lh +rfAS5+HOvWLQwt5kKyr8w3GzfbWT7FF7z024x0rT6mo0chOMe33Ng/AOYSGFzisJ +OrJieiEosNjdcFbfuvspQFsI0cRU/A== +=dgpO +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.module.md5 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.module.md5 new file mode 100644 index 000000000000..ef64fa1aa54d --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.module.md5 @@ -0,0 +1 @@ +9a7d415ecef034f2fd4a0b251055ec4b \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.module.md5.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.module.md5.asc new file mode 100644 index 000000000000..a31ae245d37d --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.module.md5.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX +xT375Af+L1KawLLikLiiC6R2BCxufjcLHEzxh0RLjgDHpHXy8s/Bh5GgkTT4x/Hn +gGJNT3Yz18rzpZaLnvNR/J9wOFEzLxRsgl1rumvTrrwhbIjac774oj3Z8Zv+W1T8 +KN1mtfUSLDZSRbmY0YByvPVtag1+FaIifxmIIFLny+xDzRVD1OZ38gOaxz91nqmd +pgjR2eVmeYLX3oAIApVopYdKWXNwOMzdBQbNroPRKCOesmTqQi0sjuvgN7r5JoxN +9vVzF1SFnAKnw/LQqL0KMrRzCBd+ncUk7A6D6RB0MM0V+TB9am9CsatxflRgQY+c +vzu/BHY8k6lh44TECvAuNuSr01CkPA== +=awJx +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.module.sha1 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.module.sha1 new file mode 100644 index 000000000000..dc6c2e7bcd60 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.module.sha1 @@ -0,0 +1 @@ +9df95def633421b7415f807b0b5848c6975de806 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.module.sha1.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.module.sha1.asc new file mode 100644 index 000000000000..df2673f47c2f --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.module.sha1.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX +xT1ubAgAtWXXzqRDIC+DhAaY3IfHyM/Dmlj58jLhTzta4xKQe9HykEjBlpkPSscp +9R+O2aL4xfBUmKtVLORKoGN3oUPhdU8a5vfgI2itdDPWLkOfFE64OJtIOZKp4ST4 +i00Jsqd4GFryS3r+i5FL5MCCv+zG/OkylMIfcH1XVxynvwrJVY49Do+TmW4MOIFf +4uDOd29XmEc8vCJBd3VZu0epHqcXhdiQy0ekdl3NdUimzRuXAckNhGNMoLWYhKaw +voErlAtfDHMbYU1DebguEaiLi98N6IxX1aO0Lleg3JNveD7pLCjEEf7AW+7TYoz1 +QBvHABpVHzZ7Rg5VhZNIIrQ38zyZdg== +=kz39 +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.module.sha256 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.module.sha256 new file mode 100644 index 000000000000..dc5ad0866352 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.module.sha256 @@ -0,0 +1 @@ +773b2d6350eb671af0b79328bd5334fba565f9041ca823fe7b55097dadf3dcf5 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.module.sha256.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.module.sha256.asc new file mode 100644 index 000000000000..2557a7a64e2f --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.module.sha256.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX +xT3njggAl61MfkbtByxMKX8fQo5Jqp+vtvyZoaDZj+6FKr0xust5Wr4Fi+4+7fkj +KyXFCTZUTd0xokRNpC8bxeZhhVkeG0pq6DrTr5BTvJLMJwPWVvrtn+bzDN1FvMia +iZqcOlWtbwTdcosmBxXtxwI1gavwFhHUGudBzrBs85qkMkDz9BH8Egb+z/owFfPh +lB9NSzezj4axgr745Ov5gYCwZp44iDBTcLZDWSLGMTuC6VdrLTQVsNLxouGI/67E +0oqLmlaqfWZEJktTMk0LHG5ymy0g40Gm8r2kuxRnFEDVJwXSfJRiTvxifB+YoEHp +RAcpReQe8+iSStuGEKYmfwmyXTdXAA== +=759w +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.module.sha512 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.module.sha512 new file mode 100644 index 000000000000..7e12e81cba75 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.module.sha512 @@ -0,0 +1 @@ +6294828fb9ce277d60e92c607d00b739e9266c3ebdca173fb32881c04f2d365d43bf9d77c0a07cf1dbcd09ad302f7458e938197f59ff5614a5d1d4e4146a0cf6 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.module.sha512.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.module.sha512.asc new file mode 100644 index 000000000000..5c37a3450966 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.module.sha512.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX +xT1H5wgAnmHJRXH+f1cZLlLKqMv7JMbuSFha3XsDPeL/XIEXmD9zj/R8IE7EKmpS +uwkswZaOeIyc95J4FasxiZm5CExxzRSTQfXtK2lOl/7Gp84D+D6XXI28CUIRnOfo +SeOyCFk2U3a6uTsRgi1FSnJRvLCs+0tB+bByKuVgGbfQdF0mtQ9rCxlqKKVa/dz6 +ertOXtz1A7fiLV44ovZG27GOciRJbogBmWfmNGPaQ+Ry8b8ICPf3SDdNSpp/nG2i +YZxzIyX9xabzPGg7M1d4ClhrpJQRjJD1hJRIHCkwC8f8Y544iQ/MuAwd3hNIfjWP +GJjgOl0iYjO8LPVaRdrHFkBUntVdHQ== +=tlE4 +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom new file mode 100644 index 000000000000..b8ef79e38cde --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom @@ -0,0 +1,49 @@ + + + + + + + + 4.0.0 + org.springframework.example + module-two + 1.0.0 + module-two + Example module + https://spring.io/projects/spring-boot + + Pivotal Software, Inc. + https://spring.io + + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0 + + + + + Pivotal + info@pivotal.io + Pivotal Software, Inc. + https://www.spring.io + + + + + scm:git:git://github.com/spring-projects/spring-boot.git + + + scm:git:ssh://git@github.com/spring-projects/spring-boot.git + + https://github.com/spring-projects/spring-boot + + + GitHub + + https://github.com/spring-projects/spring-boot/issues + + + diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom.asc new file mode 100644 index 000000000000..f3703ea391be --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT2kvAgAoc2k0ljj7L3Pj4rPz73K1SO3pDHdf+6S6pU7E4ao9FEFZBcB7YJjEmmQ +U724HqU15PIkaJKI/v4Z612E1gMSMIQ8A0LnsFR9yQdvrsK1Ijv+CdPCdyvZsBfP +3MgmWaRUOToK3BAAVV5y0dfUNFUyeKKxHNclJd6H0HUK02of8I7LBn/5ULK4QRaQ +Lm3bUIT3PtjUfND+DK3QlczZ+YgOkIwTkLywYCYxblm9XJjWCRXaZI1MdUlA6SMs +uEqtglQ9zEJgyue/JtWsIkAlzUbdyjo34Cg5HEZJ6RNzboXlRNFm83fcKyPhSy7V +0xikP1INbKuZSU1ZE7/rRYIQ7ChK0g== +=96NH +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom.md5 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom.md5 new file mode 100644 index 000000000000..463446fec94b --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom.md5 @@ -0,0 +1 @@ +057258909d16c0f9ed33c8a83e4ef165 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom.md5.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom.md5.asc new file mode 100644 index 000000000000..df9d49af1f43 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom.md5.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX +xT1eXQgAhIYkhiSoV4lFMPT9GuN4OjkoQC+v9ev8HkmIdq8lpoB+StIYzI35hKLU +kfm5d2aeVo7ifDdXh642p9oEXRfuDPfaLd9u8SZZBAdo4rolQZr4bl+JaUFzR79i +nRozXQeJF1UrDUKMi0+YGQxlosTbdx7Romj2UdfEmL2ACetxxR3rQExgZl4O/OUm +PHJtIrzO1xdbVxtKelILJ4D/PauqEqcqzC2gI5vObZJcRgxDU/wc2CVN9jruv07h +UW+8IEFV8vexoHo+Kq/F9xaTW2b7oXnvfOJgWRbh3zGpxSluJwVINDyX39/ym/Dr +FIO6BPWFKQPEUW2cY6C69jj7S+v8Ig== +=PYoG +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom.sha1 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom.sha1 new file mode 100644 index 000000000000..2b3f269aad0f --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom.sha1 @@ -0,0 +1 @@ +1a742c42aef877b6a2808a1b5c35fbe3189ce274 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom.sha1.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom.sha1.asc new file mode 100644 index 000000000000..ea7c57f9c6ed --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom.sha1.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEyBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX +xT0bmAf4+WOg0xkoyRDXf/1hFOXlimKqyF1K7I6PXx1dFokRr+tvOtfFZucCOf+f +1hCvnBiPTQwwMPCgll0reTsH2nHfDVUcbugpxVDC3Yza0x3gHudBhPC7yv+osNIu +sVlnMRYbG1RQGjE6BxHoBk9pdOcwgN7zk2Y4LfAbOKMTo7dhAjZavRx3aShEUwHy +P9/kfxcWCL0tOSzWg5XpZuxFEdVMWNJvshFvP0j2/Nlr6ZL5o/AwtyZKMiZ8QcUb +0satLj501JYI6pM2cm8N17T94+jCsQXZic/hUCNteXA4XbRcDBR2wQLd08/Ht0U4 +rHzZQNr5Ft5R5ScshTEVBwjcd/Kx +=drUj +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom.sha256 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom.sha256 new file mode 100644 index 000000000000..334afc14508c --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom.sha256 @@ -0,0 +1 @@ +7e855e618d4aba3eb4e63bbfc9960e4da319d8bdef0cca213a11e9b2d1991635 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom.sha256.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom.sha256.asc new file mode 100644 index 000000000000..433766e66069 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom.sha256.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX +xT3w3QgAvHS75cBMEEZGWiCQ+lbwDMWY7rjQBwkpO1Sj6WxpKcWD2F4YmNni1z1k +L71SDuLP0a+IbYcoDCkss7vxH6hx6Lm53e+2WwhaVGCO1A6N3a56rFyEFlATrn31 +mcRjLrN4wzysqqbeamzSt+R0UoWj7yiihtBuz7tkhjGP+df2qCrXNeuetrhukFOz +P5RLd4PURYMMUMqqNZ8JNnRhdCVdSVUpfM+BDolNDaswDrvOI3jzjXD/6HCt0fcN +Pt484kFDqGEx0iXvv+7shiExs31gex+fsn2ta9yOGYluF/8Rc4z+X0/59MewoCgC +EelPT4oi4zirrIWzGp1bRF+jDi6feA== +=stbf +-----END PGP SIGNATURE----- diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom.sha512 b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom.sha512 new file mode 100644 index 000000000000..e7b24fdd95be --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom.sha512 @@ -0,0 +1 @@ +5b06587734aa146b452e78e34abffb38b8c820abf623975409ae003b5d90aded345fa8f92ed9d7a505a16db324c0649d2e16c7597bad8dc63f0a5c00123789e1 \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom.sha512.asc b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom.sha512.asc new file mode 100644 index 000000000000..d8d2cc73e369 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo/org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom.sha512.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- + +iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX +xT3p3QgAp5doWrX6eMPD1I09NHMt29LI8wFa/xld7rof8OQLHDN55TLseqOvBU4V +E82s5cm4Uk0ndnth/VUHqsJK6SsNX8/0N2bvOtWgUWBYdClcy6ZBWXjQIDFfCdqX +LPqQN4nOT3ZZMrzTZhLsAJkbzvVaOzEtUYZWw1ZAIT8nPkud24stuuxKUtsAxfeD +QqcgKng/sPx6DS2+NSmmzyCF9eSL70NBcBF6+RJ+4X0YtZRtX+wsic2MnKnVAnyX +hPejxguJYhwWbn1yRdVWknCdffpiT09IC/7AS/yc8s1DdbS6XEae8uFl0OB5z5dx +nnaHUvlFrAjDGsrYeW5h1ZkM8VwBxA== +=2HWi +-----END PGP SIGNATURE----- diff --git a/ci/images/setup.sh b/ci/images/setup.sh index d888d964fd77..7cc0c6744d6c 100755 --- a/ci/images/setup.sh +++ b/ci/images/setup.sh @@ -5,11 +5,14 @@ set -ex # UTILS ########################################################### +export DEBIAN_FRONTEND=noninteractive apt-get update -apt-get install --no-install-recommends -y ca-certificates net-tools libxml2-utils git curl libudev1 libxml2-utils iptables iproute2 jq +apt-get install --no-install-recommends -y tzdata ca-certificates net-tools libxml2-utils git curl libudev1 libxml2-utils iptables iproute2 jq +ln -fs /usr/share/zoneinfo/UTC /etc/localtime +dpkg-reconfigure --frontend noninteractive tzdata rm -rf /var/lib/apt/lists/* -curl https://raw.githubusercontent.com/spring-io/concourse-java-scripts/v0.0.2/concourse-java.sh > /opt/concourse-java.sh +curl https://raw.githubusercontent.com/spring-io/concourse-java-scripts/v0.0.4/concourse-java.sh > /opt/concourse-java.sh ########################################################### @@ -23,13 +26,23 @@ curl -L ${JDK_URL} | tar zx --strip-components=1 test -f /opt/openjdk/bin/java test -f /opt/openjdk/bin/javac +if [[ $# -eq 2 ]]; then + cd / + TOOLCHAIN_JDK_URL=$( ./get-jdk-url.sh $2 ) + + mkdir -p /opt/openjdk-toolchain + cd /opt/openjdk-toolchain + curl -L ${TOOLCHAIN_JDK_URL} | tar zx --strip-components=1 + test -f /opt/openjdk-toolchain/bin/java + test -f /opt/openjdk-toolchain/bin/javac +fi ########################################################### # DOCKER ########################################################### - cd / -curl -L https://download.docker.com/linux/static/stable/x86_64/docker-19.03.2.tgz | tar zx +DOCKER_URL=$( ./get-docker-url.sh ) +curl -L ${DOCKER_URL} | tar zx mv /docker/* /bin/ chmod +x /bin/docker* @@ -38,3 +51,9 @@ curl -L https://github.com/progrium/entrykit/releases/download/v${ENTRYKIT_VERSI chmod +x entrykit && \ mv entrykit /bin/entrykit && \ entrykit --symlink + +########################################################### +# GRADLE ENTERPRISE +########################################################### +mkdir ~/.gradle +echo 'systemProp.user.name=concourse' > ~/.gradle/gradle.properties \ No newline at end of file diff --git a/ci/images/spring-boot-ci-image/Dockerfile b/ci/images/spring-boot-ci-image/Dockerfile deleted file mode 100644 index d128866ad993..000000000000 --- a/ci/images/spring-boot-ci-image/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -FROM ubuntu:bionic-20190807 - -ADD setup.sh /setup.sh -ADD get-jdk-url.sh /get-jdk-url.sh -RUN ./setup.sh java8 - -ENV JAVA_HOME /opt/openjdk -ENV PATH $JAVA_HOME/bin:$PATH -ADD docker-lib.sh /docker-lib.sh - -ADD build-release-scripts.sh /build-release-scripts.sh -ADD releasescripts /release-scripts -RUN ./build-release-scripts.sh -ENTRYPOINT [ "switch", "shell=/bin/bash", "--", "codep", "/bin/docker daemon" ] diff --git a/ci/images/spring-boot-jdk11-ci-image/Dockerfile b/ci/images/spring-boot-jdk11-ci-image/Dockerfile deleted file mode 100644 index 84e8297b214a..000000000000 --- a/ci/images/spring-boot-jdk11-ci-image/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM ubuntu:bionic-20190807 - -ADD setup.sh /setup.sh -ADD get-jdk-url.sh /get-jdk-url.sh -RUN ./setup.sh java11 - -ENV JAVA_HOME /opt/openjdk -ENV PATH $JAVA_HOME/bin:$PATH -ADD docker-lib.sh /docker-lib.sh - -ENTRYPOINT [ "switch", "shell=/bin/bash", "--", "codep", "/bin/docker daemon" ] diff --git a/ci/images/spring-boot-jdk13-ci-image/Dockerfile b/ci/images/spring-boot-jdk13-ci-image/Dockerfile deleted file mode 100644 index 41218723ef54..000000000000 --- a/ci/images/spring-boot-jdk13-ci-image/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM ubuntu:bionic-20190807 - -ADD setup.sh /setup.sh -ADD get-jdk-url.sh /get-jdk-url.sh -RUN ./setup.sh java13 - -ENV JAVA_HOME /opt/openjdk -ENV PATH $JAVA_HOME/bin:$PATH -ADD docker-lib.sh /docker-lib.sh - -ENTRYPOINT [ "switch", "shell=/bin/bash", "--", "codep", "/bin/docker daemon" ] diff --git a/ci/parameters.yml b/ci/parameters.yml index 2860a65c606b..6e2424434023 100644 --- a/ci/parameters.yml +++ b/ci/parameters.yml @@ -3,12 +3,11 @@ email-from: "ci@spring.io" email-to: ["spring-boot-dev@pivotal.io"] github-repo: "https://github.com/spring-projects/spring-boot.git" github-repo-name: "spring-projects/spring-boot" +homebrew-tap-repo: "https://github.com/spring-io/homebrew-tap.git" docker-hub-organization: "springci" artifactory-server: "https://repo.spring.io" -branch: "master" +branch: "2.5.x" +milestone: "2.5.x" build-name: "spring-boot" -pipeline-name: "spring-boot" concourse-url: "https://ci.spring.io" -bintray-subject: "spring" -bintray-repo: "jars" task-timeout: 2h00m diff --git a/ci/pipeline.yml b/ci/pipeline.yml index 78b193ec40a9..cae473867f8f 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -1,50 +1,142 @@ +anchors: + git-repo-resource-source: &git-repo-resource-source + uri: ((github-repo)) + username: ((github-username)) + password: ((github-ci-release-token)) + branch: ((branch)) + registry-image-resource-source: ®istry-image-resource-source + username: ((docker-hub-username)) + password: ((docker-hub-password)) + tag: ((milestone)) + gradle-enterprise-task-params: &gradle-enterprise-task-params + GRADLE_ENTERPRISE_ACCESS_KEY: ((gradle_enterprise_secret_access_key)) + GRADLE_ENTERPRISE_CACHE_USERNAME: ((gradle_enterprise_cache_user.username)) + GRADLE_ENTERPRISE_CACHE_PASSWORD: ((gradle_enterprise_cache_user.password)) + docker-hub-task-params: &docker-hub-task-params + DOCKER_HUB_USERNAME: ((docker-hub-username)) + DOCKER_HUB_PASSWORD: ((docker-hub-password)) + github-task-params: &github-task-params + GITHUB_REPO: spring-boot + GITHUB_ORGANIZATION: spring-projects + GITHUB_PASSWORD: ((github-ci-release-token)) + GITHUB_USERNAME: ((github-username)) + MILESTONE: ((milestone)) + sontatype-task-params: &sonatype-task-params + SONATYPE_USER_TOKEN: ((sonatype-username)) + SONATYPE_PASSWORD_TOKEN: ((sonatype-password)) + SONATYPE_URL: ((sonatype-url)) + SONATYPE_STAGING_PROFILE_ID: ((sonatype-staging-profile-id)) + artifactory-task-params: &artifactory-task-params + ARTIFACTORY_SERVER: ((artifactory-server)) + ARTIFACTORY_USERNAME: ((artifactory-username)) + ARTIFACTORY_PASSWORD: ((artifactory-password)) + sdkman-task-params: &sdkman-task-params + SDKMAN_CONSUMER_KEY: ((sdkman-consumer-key)) + SDKMAN_CONSUMER_TOKEN: ((sdkman-consumer-token)) + build-project-task-params: &build-project-task-params + privileged: true + timeout: ((task-timeout)) + file: git-repo/ci/tasks/build-project.yml + params: + BRANCH: ((branch)) + DOCKER_HUB_MIRROR: ((docker-hub-mirror)) + <<: *gradle-enterprise-task-params + <<: *docker-hub-task-params + artifactory-repo-put-params: &artifactory-repo-put-params + signing_key: ((signing-key)) + signing_passphrase: ((signing-passphrase)) + repo: libs-snapshot-local + folder: distribution-repository + build_uri: "https://ci.spring.io/teams/${BUILD_TEAM_NAME}/pipelines/${BUILD_PIPELINE_NAME}/jobs/${BUILD_JOB_NAME}/builds/${BUILD_NAME}" + build_number: "${BUILD_PIPELINE_NAME}-${BUILD_JOB_NAME}-${BUILD_NAME}" + disable_checksum_uploads: true + threads: 8 + artifact_set: + - include: + - "/**/spring-boot-docs-*.zip" + properties: + "zip.type": "docs" + "zip.deployed": "false" + slack-fail-params: &slack-fail-params + text: > + :concourse-failed: + [$TEXT_FILE_CONTENT] + text_file: git-repo/build/build-scan-uri.txt + silent: true + icon_emoji: ":concourse:" + username: concourse-ci + slack-success-params: &slack-success-params + text: > + :concourse-succeeded: + [$TEXT_FILE_CONTENT] + text_file: git-repo/build/build-scan-uri.txt + silent: true + icon_emoji: ":concourse:" + username: concourse-ci + homebrew-tap-repo-resource-source: &homebrew-tap-repo-resource-source + uri: ((homebrew-tap-repo)) + username: ((github-username)) + password: ((github-ci-release-token)) + branch: main + gradle-publish-params: &gradle-publish-params + GRADLE_PUBLISH_KEY: ((gradle-publish-key)) + GRADLE_PUBLISH_SECRET: ((gradle-publish-secret)) + docker-hub-mirror-vars: &docker-hub-mirror-vars + docker-hub-mirror: ((docker-hub-mirror)) + docker-hub-mirror-username: ((docker-hub-mirror-username)) + docker-hub-mirror-password: ((docker-hub-mirror-password)) resource_types: +- name: registry-image + type: registry-image + source: + repository: concourse/registry-image-resource + tag: 1.5.0 - name: artifactory-resource - type: docker-image + type: registry-image source: repository: springio/artifactory-resource - tag: 0.0.5 + tag: 0.0.17 - name: pull-request - type: docker-image + type: registry-image source: - repository: jtarchie/pr + repository: teliaoss/github-pr-resource + tag: v0.23.0 - name: github-status-resource - type: docker-image + type: registry-image source: repository: dpb587/github-status-resource tag: master - name: slack-notification - type: docker-image + type: registry-image source: repository: cfcommunity/slack-notification-resource tag: latest +- name: github-release + type: registry-image + source: + repository: concourse/github-release-resource + tag: 1.5.5 resources: - name: git-repo type: git - icon: github-circle + icon: github source: - uri: ((github-repo)) - username: ((github-username)) - password: ((github-password)) - branch: ((branch)) + <<: *git-repo-resource-source - name: git-repo-windows type: git + icon: github source: - uri: ((github-repo)) - username: ((github-username)) - password: ((github-password)) - branch: ((branch)) + <<: *git-repo-resource-source git_config: - name: core.autocrlf value: true - icon: github-circle - name: git-pull-request type: pull-request icon: source-pull source: access_token: ((github-ci-pull-request-token)) - repo: ((github-repo-name)) - base: ((branch)) + repository: ((github-repo-name)) + base_branch: ((branch)) ignore_paths: ["ci/*"] - name: github-pre-release type: github-release @@ -65,35 +157,35 @@ resources: pre_release: false - name: ci-images-git-repo type: git - icon: github-circle + icon: github source: uri: ((github-repo)) branch: ((branch)) paths: ["ci/images/*"] -- name: spring-boot-ci-image - type: docker-image +- name: ci-image + type: registry-image icon: docker source: - repository: ((docker-hub-organization))/spring-boot-ci-image - username: ((docker-hub-username)) - password: ((docker-hub-password)) - tag: ((branch)) -- name: spring-boot-jdk11-ci-image - type: docker-image + <<: *registry-image-resource-source + repository: ((docker-hub-organization))/spring-boot-ci +- name: ci-image-jdk11 + type: registry-image icon: docker source: - repository: ((docker-hub-organization))/spring-boot-jdk11-ci-image - username: ((docker-hub-username)) - password: ((docker-hub-password)) - tag: ((branch)) -- name: spring-boot-jdk13-ci-image - type: docker-image + <<: *registry-image-resource-source + repository: ((docker-hub-organization))/spring-boot-ci-jdk11 +- name: ci-image-jdk17 + type: registry-image icon: docker source: - repository: ((docker-hub-organization))/spring-boot-jdk13-ci-image - username: ((docker-hub-username)) - password: ((docker-hub-password)) - tag: ((branch)) + <<: *registry-image-resource-source + repository: ((docker-hub-organization))/spring-boot-ci-jdk17 +- name: ci-image-jdk18 + type: registry-image + icon: docker + source: + <<: *registry-image-resource-source + repository: ((docker-hub-organization))/spring-boot-ci-jdk18 - name: artifactory-repo type: artifactory-resource icon: package-variant @@ -118,14 +210,22 @@ resources: access_token: ((github-ci-status-token)) branch: ((branch)) context: jdk11-build -- name: repo-status-jdk13-build +- name: repo-status-jdk17-build + type: github-status-resource + icon: eye-check-outline + source: + repository: ((github-repo-name)) + access_token: ((github-ci-status-token)) + branch: ((branch)) + context: jdk17-build +- name: repo-status-jdk18-build type: github-status-resource icon: eye-check-outline source: repository: ((github-repo-name)) access_token: ((github-ci-status-token)) branch: ((branch)) - context: jdk13-build + context: jdk18-build - name: slack-alert type: slack-notification icon: slack @@ -140,135 +240,160 @@ resources: days: [Wednesday] - name: daily type: time + icon: clock-outline source: { interval: "24h" } +- name: homebrew-tap-repo + type: git + icon: github + source: + <<: *homebrew-tap-repo-resource-source jobs: -- name: build-spring-boot-ci-images +- name: build-ci-images plan: - get: ci-images-git-repo trigger: true + - get: git-repo - in_parallel: - - put: spring-boot-ci-image + - task: build-ci-image + privileged: true + file: git-repo/ci/tasks/build-ci-image.yml + output_mapping: + image: ci-image + vars: + ci-image-name: ci-image + <<: *docker-hub-mirror-vars + - task: build-ci-image-jdk11 + privileged: true + file: git-repo/ci/tasks/build-ci-image.yml + output_mapping: + image: ci-image-jdk11 + vars: + ci-image-name: ci-image-jdk11 + <<: *docker-hub-mirror-vars + - task: build-ci-image-jdk17 + privileged: true + file: git-repo/ci/tasks/build-ci-image.yml + output_mapping: + image: ci-image-jdk17 + vars: + ci-image-name: ci-image-jdk17 + <<: *docker-hub-mirror-vars + - task: build-ci-image-jdk18 + privileged: true + file: git-repo/ci/tasks/build-ci-image.yml + output_mapping: + image: ci-image-jdk18 + vars: + ci-image-name: ci-image-jdk18 + <<: *docker-hub-mirror-vars + - in_parallel: + - put: ci-image + params: + image: ci-image/image.tar + - put: ci-image-jdk11 params: - build: ci-images-git-repo/ci/images - dockerfile: ci-images-git-repo/ci/images/spring-boot-ci-image/Dockerfile - - put: spring-boot-jdk11-ci-image + image: ci-image-jdk11/image.tar + - put: ci-image-jdk17 params: - build: ci-images-git-repo/ci/images - dockerfile: ci-images-git-repo/ci/images/spring-boot-jdk11-ci-image/Dockerfile - - put: spring-boot-jdk13-ci-image + image: ci-image-jdk17/image.tar + - put: ci-image-jdk18 params: - build: ci-images-git-repo/ci/images - dockerfile: ci-images-git-repo/ci/images/spring-boot-jdk13-ci-image/Dockerfile + image: ci-image-jdk18/image.tar - name: detect-jdk-updates plan: - get: git-repo - get: every-wednesday trigger: true - - get: spring-boot-ci-image + - get: ci-image - in_parallel: - task: detect-jdk8-update + image: ci-image file: git-repo/ci/tasks/detect-jdk-updates.yml params: - GITHUB_REPO: spring-boot - GITHUB_ORGANIZATION: spring-projects - GITHUB_PASSWORD: ((github-password)) - GITHUB_USERNAME: ((github-username)) + <<: *github-task-params JDK_VERSION: java8 - image: spring-boot-ci-image - task: detect-jdk11-update + image: ci-image file: git-repo/ci/tasks/detect-jdk-updates.yml params: - GITHUB_REPO: spring-boot - GITHUB_ORGANIZATION: spring-projects - GITHUB_PASSWORD: ((github-password)) - GITHUB_USERNAME: ((github-username)) + <<: *github-task-params JDK_VERSION: java11 - image: spring-boot-ci-image - - task: detect-jdk13-update + - task: detect-jdk17-update + image: ci-image file: git-repo/ci/tasks/detect-jdk-updates.yml params: - GITHUB_REPO: spring-boot - GITHUB_ORGANIZATION: spring-projects - GITHUB_PASSWORD: ((github-password)) - GITHUB_USERNAME: ((github-username)) - JDK_VERSION: java13 - image: spring-boot-ci-image + <<: *github-task-params + JDK_VERSION: java17 + - task: detect-jdk18-update + image: ci-image + file: git-repo/ci/tasks/detect-jdk-updates.yml + params: + <<: *github-task-params + JDK_VERSION: java18 +- name: detect-ubuntu-image-updates + plan: + - get: git-repo + - get: every-wednesday + trigger: true + - get: ci-image + - do: + - task: detect-ubuntu-image-updates + image: ci-image + file: git-repo/ci/tasks/detect-ubuntu-image-updates.yml + params: + <<: *github-task-params +- name: detect-docker-updates + plan: + - get: git-repo + - get: every-wednesday + trigger: true + - get: ci-image + - do: + - task: detect-docker-updates + image: ci-image + file: git-repo/ci/tasks/detect-docker-updates.yml + params: + <<: *github-task-params - name: build serial: true public: true plan: - - get: spring-boot-ci-image + - get: ci-image - get: git-repo trigger: true - put: repo-status-build params: { state: "pending", commit: "git-repo" } - do: - task: build-project - privileged: true - timeout: ((task-timeout)) - image: spring-boot-ci-image - file: git-repo/ci/tasks/build-project.yml - - in_parallel: - - task: build-smoke-tests - timeout: ((task-timeout)) - image: spring-boot-ci-image - file: git-repo/ci/tasks/build-smoke-tests.yml - - task: build-integration-tests - timeout: ((task-timeout)) - image: spring-boot-ci-image - file: git-repo/ci/tasks/build-integration-tests.yml - - task: build-deployment-tests - timeout: ((task-timeout)) - image: spring-boot-ci-image - file: git-repo/ci/tasks/build-deployment-tests.yml + image: ci-image + <<: *build-project-task-params on_failure: do: - put: repo-status-build params: { state: "failure", commit: "git-repo" } - put: slack-alert params: - text: ":concourse-failed: " - silent: true - icon_emoji: ":concourse:" - username: concourse-ci + <<: *slack-fail-params - put: repo-status-build params: { state: "success", commit: "git-repo" } - put: artifactory-repo - params: &artifactory-params - repo: libs-snapshot-local - folder: distribution-repository - build_uri: "https://ci.spring.io/teams/${BUILD_TEAM_NAME}/pipelines/${BUILD_PIPELINE_NAME}/jobs/${BUILD_JOB_NAME}/builds/${BUILD_NAME}" - build_number: "${BUILD_PIPELINE_NAME}-${BUILD_JOB_NAME}-${BUILD_NAME}" - disable_checksum_uploads: true - exclude: - - "**/*.effective-pom" - - "**/spring-boot-configuration-docs/**" - - "**/spring-boot-test-support/**" - artifact_set: - - include: - - "/**/spring-boot-docs-*.zip" - properties: - "zip.type": "docs" - "zip.deployed": "false" + params: + <<: *artifactory-repo-put-params + get_params: + threads: 8 on_failure: do: - put: slack-alert params: - text: ":concourse-failed: " - silent: true - icon_emoji: ":concourse:" - username: concourse-ci + <<: *slack-fail-params - put: slack-alert params: - text: ":concourse-succeeded: " - silent: true - icon_emoji: ":concourse:" - username: concourse-ci + <<: *slack-success-params - name: build-pull-requests serial: true public: true plan: - - get: spring-boot-ci-image + - get: ci-image - get: git-repo resource: git-pull-request trigger: true @@ -279,22 +404,9 @@ jobs: path: git-repo status: pending - task: build-project - timeout: ((task-timeout)) - image: spring-boot-ci-image + image: ci-image file: git-repo/ci/tasks/build-pr-project.yml - - in_parallel: - - task: build-smoke-tests - timeout: ((task-timeout)) - image: spring-boot-ci-image - file: git-repo/ci/tasks/build-smoke-tests.yml - - task: build-integration-tests - timeout: ((task-timeout)) - image: spring-boot-ci-image - file: git-repo/ci/tasks/build-integration-tests.yml - - task: build-deployment-tests - timeout: ((task-timeout)) - image: spring-boot-ci-image - file: git-repo/ci/tasks/build-deployment-tests.yml + timeout: ((task-timeout)) on_success: put: git-pull-request params: @@ -309,94 +421,91 @@ jobs: serial: true public: true plan: - - get: spring-boot-jdk11-ci-image + - get: ci-image-jdk11 - get: git-repo trigger: true - put: repo-status-jdk11-build params: { state: "pending", commit: "git-repo" } - do: - task: build-project - privileged: true - timeout: ((task-timeout)) - image: spring-boot-jdk11-ci-image - file: git-repo/ci/tasks/build-project.yml - - in_parallel: - - task: build-smoke-tests - timeout: ((task-timeout)) - image: spring-boot-jdk11-ci-image - file: git-repo/ci/tasks/build-smoke-tests.yml - - task: build-integration-tests - timeout: 1h30m - image: spring-boot-jdk11-ci-image - file: git-repo/ci/tasks/build-integration-tests.yml - - task: build-deployment-tests - timeout: 1h30m - image: spring-boot-jdk11-ci-image - file: git-repo/ci/tasks/build-deployment-tests.yml + image: ci-image-jdk11 + <<: *build-project-task-params on_failure: do: - put: repo-status-jdk11-build params: { state: "failure", commit: "git-repo" } - put: slack-alert params: - text: ":concourse-failed: " - silent: true - icon_emoji: ":concourse:" - username: concourse-ci + <<: *slack-fail-params - put: repo-status-jdk11-build params: { state: "success", commit: "git-repo" } - put: slack-alert params: - text: ":concourse-succeeded: " - silent: true - icon_emoji: ":concourse:" - username: concourse-ci -- name: jdk13-build + <<: *slack-success-params +- name: jdk17-build serial: true public: true plan: - - get: spring-boot-jdk13-ci-image + - get: ci-image-jdk17 - get: git-repo trigger: true - - put: repo-status-jdk13-build + - put: repo-status-jdk17-build params: { state: "pending", commit: "git-repo" } - do: - task: build-project + image: ci-image-jdk17 privileged: true - timeout: 1h30m - image: spring-boot-jdk13-ci-image + timeout: ((task-timeout)) file: git-repo/ci/tasks/build-project.yml - - in_parallel: - - task: build-smoke-tests - timeout: 1h30m - image: spring-boot-jdk13-ci-image - file: git-repo/ci/tasks/build-smoke-tests.yml - - task: build-integration-tests - timeout: 1h30m - image: spring-boot-jdk13-ci-image - file: git-repo/ci/tasks/build-integration-tests.yml - - task: build-deployment-tests - timeout: 1h30m - image: spring-boot-jdk13-ci-image - file: git-repo/ci/tasks/build-deployment-tests.yml + params: + BRANCH: ((branch)) + TOOLCHAIN_JAVA_VERSION: 17 + <<: *gradle-enterprise-task-params + <<: *docker-hub-task-params on_failure: do: - - put: repo-status-jdk13-build + - put: repo-status-jdk17-build params: { state: "failure", commit: "git-repo" } - put: slack-alert params: - text: ":concourse-failed: " - silent: true - icon_emoji: ":concourse:" - username: concourse-ci - - put: repo-status-jdk13-build + <<: *slack-fail-params + - put: repo-status-jdk17-build params: { state: "success", commit: "git-repo" } - put: slack-alert params: - text: ":concourse-succeeded: " - silent: true - icon_emoji: ":concourse:" - username: concourse-ci + <<: *slack-success-params +- name: jdk18-build + serial: true + public: true + plan: + - get: ci-image-jdk18 + - get: git-repo + trigger: true + - put: repo-status-jdk18-build + params: { state: "pending", commit: "git-repo" } + - do: + - task: build-project + image: ci-image-jdk18 + privileged: true + timeout: ((task-timeout)) + file: git-repo/ci/tasks/build-project.yml + params: + BRANCH: ((branch)) + TOOLCHAIN_JAVA_VERSION: 18 + <<: *gradle-enterprise-task-params + <<: *docker-hub-task-params + on_failure: + do: + - put: repo-status-jdk18-build + params: { state: "failure", commit: "git-repo" } + - put: slack-alert + params: + <<: *slack-fail-params + - put: repo-status-jdk18-build + params: { state: "success", commit: "git-repo" } + - put: slack-alert + params: + <<: *slack-success-params - name: windows-build serial: true plan: @@ -411,78 +520,87 @@ jobs: tags: - WIN64 timeout: ((task-timeout)) + params: + BRANCH: ((branch)) + <<: *gradle-enterprise-task-params on_failure: do: - put: slack-alert params: - text: ":concourse-failed: " - silent: true - icon_emoji: ":concourse:" - username: concourse-ci + <<: *slack-fail-params - put: slack-alert params: - text: ":concourse-succeeded: " - silent: true - icon_emoji: ":concourse:" - username: concourse-ci + <<: *slack-success-params - name: stage-milestone serial: true plan: - - get: spring-boot-ci-image + - get: ci-image - get: git-repo trigger: false - task: stage - image: spring-boot-ci-image + image: ci-image file: git-repo/ci/tasks/stage.yml params: RELEASE_TYPE: M + <<: *gradle-enterprise-task-params + <<: *docker-hub-task-params - put: artifactory-repo params: - <<: *artifactory-params + <<: *artifactory-repo-put-params repo: libs-staging-local + get_params: + threads: 8 - put: git-repo params: repository: stage-git-repo - name: stage-rc serial: true plan: - - get: spring-boot-ci-image + - get: ci-image - get: git-repo trigger: false - task: stage - image: spring-boot-ci-image + image: ci-image file: git-repo/ci/tasks/stage.yml params: RELEASE_TYPE: RC + <<: *gradle-enterprise-task-params + <<: *docker-hub-task-params - put: artifactory-repo params: - <<: *artifactory-params + <<: *artifactory-repo-put-params repo: libs-staging-local + get_params: + threads: 8 - put: git-repo params: repository: stage-git-repo - name: stage-release serial: true plan: - - get: spring-boot-ci-image + - get: ci-image - get: git-repo trigger: false - task: stage - image: spring-boot-ci-image + image: ci-image file: git-repo/ci/tasks/stage.yml params: RELEASE_TYPE: RELEASE + <<: *gradle-enterprise-task-params + <<: *docker-hub-task-params - put: artifactory-repo params: - <<: *artifactory-params + <<: *artifactory-repo-put-params repo: libs-staging-local + get_params: + threads: 8 - put: git-repo params: repository: stage-git-repo - name: promote-milestone serial: true plan: - - get: spring-boot-ci-image + - get: ci-image - get: git-repo trigger: false - get: artifactory-repo @@ -492,28 +610,28 @@ jobs: download_artifacts: false save_build_info: true - task: promote - image: spring-boot-ci-image + image: ci-image file: git-repo/ci/tasks/promote.yml params: RELEASE_TYPE: M - ARTIFACTORY_SERVER: ((artifactory-server)) - ARTIFACTORY_USERNAME: ((artifactory-username)) - ARTIFACTORY_PASSWORD: ((artifactory-password)) - - task: generate-release-notes - file: git-repo/ci/tasks/generate-release-notes.yml + <<: *artifactory-task-params + - task: generate-changelog + file: git-repo/ci/tasks/generate-changelog.yml params: RELEASE_TYPE: M GITHUB_USERNAME: ((github-username)) GITHUB_TOKEN: ((github-ci-release-token)) + vars: + <<: *docker-hub-mirror-vars - put: github-pre-release params: - name: generated-release-notes/tag - tag: generated-release-notes/tag - body: generated-release-notes/release-notes.md + name: generated-changelog/tag + tag: generated-changelog/tag + body: generated-changelog/changelog.md - name: promote-rc serial: true plan: - - get: spring-boot-ci-image + - get: ci-image - get: git-repo trigger: false - get: artifactory-repo @@ -523,52 +641,65 @@ jobs: download_artifacts: false save_build_info: true - task: promote - image: spring-boot-ci-image + image: ci-image file: git-repo/ci/tasks/promote.yml params: RELEASE_TYPE: RC - ARTIFACTORY_SERVER: ((artifactory-server)) - ARTIFACTORY_USERNAME: ((artifactory-username)) - ARTIFACTORY_PASSWORD: ((artifactory-password)) - - task: generate-release-notes - file: git-repo/ci/tasks/generate-release-notes.yml + <<: *artifactory-task-params + - task: generate-changelog + file: git-repo/ci/tasks/generate-changelog.yml params: RELEASE_TYPE: RC GITHUB_USERNAME: ((github-username)) GITHUB_TOKEN: ((github-ci-release-token)) + vars: + <<: *docker-hub-mirror-vars - put: github-pre-release params: - name: generated-release-notes/tag - tag: generated-release-notes/tag - body: generated-release-notes/release-notes.md + name: generated-changelog/tag + tag: generated-changelog/tag + body: generated-changelog/changelog.md - name: promote-release serial: true plan: - - get: spring-boot-ci-image + - get: ci-image - get: git-repo trigger: false - get: artifactory-repo trigger: false passed: [stage-release] params: - download_artifacts: false + download_artifacts: true save_build_info: true + threads: 8 - task: promote - image: spring-boot-ci-image + image: ci-image file: git-repo/ci/tasks/promote.yml params: RELEASE_TYPE: RELEASE - ARTIFACTORY_SERVER: ((artifactory-server)) - ARTIFACTORY_USERNAME: ((artifactory-username)) - ARTIFACTORY_PASSWORD: ((artifactory-password)) - BINTRAY_SUBJECT: ((bintray-subject)) - BINTRAY_REPO: ((bintray-repo)) - BINTRAY_USERNAME: ((bintray-username)) - BINTRAY_API_KEY: ((bintray-api-key)) -- name: sync-to-maven-central + <<: *artifactory-task-params + <<: *sonatype-task-params +- name: publish-gradle-plugin + serial: true + plan: + - get: ci-image + - get: git-repo + - get: artifactory-repo + trigger: true + passed: [promote-release] + params: + download_artifacts: true + save_build_info: true + threads: 8 + - task: publish-gradle-plugin + image: ci-image + file: git-repo/ci/tasks/publish-gradle-plugin.yml + params: + <<: *gradle-publish-params +- name: create-github-release serial: true plan: - - get: spring-boot-ci-image + - get: ci-image - get: git-repo - get: artifactory-repo trigger: true @@ -576,33 +707,62 @@ jobs: params: download_artifacts: false save_build_info: true - - task: sync-to-maven-central - image: spring-boot-ci-image - file: git-repo/ci/tasks/sync-to-maven-central.yml - params: - BINTRAY_USERNAME: ((bintray-username)) - BINTRAY_API_KEY: ((bintray-api-key)) - SONATYPE_USER_TOKEN: ((sonatype-user-token)) - SONATYPE_PASSWORD_TOKEN: ((sonatype-user-token-password)) - BINTRAY_SUBJECT: ((bintray-subject)) - BINTRAY_REPO: ((bintray-repo)) - - task: generate-release-notes - file: git-repo/ci/tasks/generate-release-notes.yml + - task: generate-changelog + file: git-repo/ci/tasks/generate-changelog.yml params: RELEASE_TYPE: RELEASE GITHUB_USERNAME: ((github-username)) GITHUB_TOKEN: ((github-ci-release-token)) + vars: + <<: *docker-hub-mirror-vars - put: github-release params: - name: generated-release-notes/tag - tag: generated-release-notes/tag - body: generated-release-notes/release-notes.md + name: generated-changelog/tag + tag: generated-changelog/tag + body: generated-changelog/changelog.md +- name: publish-to-sdkman + serial: true + plan: + - get: ci-image + - get: git-repo + - get: artifactory-repo + passed: [create-github-release] + params: + download_artifacts: false + save_build_info: true + - task: publish-to-sdkman + image: ci-image + file: git-repo/ci/tasks/publish-to-sdkman.yml + params: + <<: *sdkman-task-params + RELEASE_TYPE: RELEASE + BRANCH: ((branch)) + LATEST_GA: false +- name: update-homebrew-tap + serial: true + plan: + - get: ci-image + - get: git-repo + - get: homebrew-tap-repo + - get: artifactory-repo + passed: [create-github-release] + params: + download_artifacts: false + save_build_info: true + - task: update-homebrew-tap + image: ci-image + file: git-repo/ci/tasks/update-homebrew-tap.yml + params: + LATEST_GA: false + - put: homebrew-tap-repo + params: + repository: updated-homebrew-tap-repo groups: -- name: "Build" - jobs: ["build", "jdk11-build", "jdk13-build", "windows-build"] -- name: "Release" - jobs: ["stage-milestone", "stage-rc", "stage-release", "promote-milestone", "promote-rc", "promote-release", "sync-to-maven-central"] -- name: "CI Images" - jobs: ["build-spring-boot-ci-images", "detect-jdk-updates"] -- name: "Build Pull Requests" +- name: "builds" + jobs: ["build", "jdk11-build", "jdk17-build", "jdk18-build", "windows-build"] +- name: "releases" + jobs: ["stage-milestone", "stage-rc", "stage-release", "promote-milestone", "promote-rc", "promote-release", "create-github-release", "publish-gradle-plugin", "publish-to-sdkman", "update-homebrew-tap"] +- name: "ci-images" + jobs: ["build-ci-images", "detect-docker-updates", "detect-jdk-updates", "detect-ubuntu-image-updates"] +- name: "pull-requests" jobs: ["build-pull-requests"] diff --git a/ci/scripts/build-deployment-tests.sh b/ci/scripts/build-deployment-tests.sh deleted file mode 100755 index c93fd0900837..000000000000 --- a/ci/scripts/build-deployment-tests.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -set -e - -source $(dirname $0)/common.sh -repository=$(pwd)/distribution-repository - -pushd git-repo > /dev/null -run_maven -f spring-boot-tests/spring-boot-deployment-tests/pom.xml clean install -U -Dfull -Drepository=file://${repository} -popd > /dev/null diff --git a/ci/scripts/build-integration-tests.sh b/ci/scripts/build-integration-tests.sh deleted file mode 100755 index 953eaebb1c74..000000000000 --- a/ci/scripts/build-integration-tests.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -set -e - -source $(dirname $0)/common.sh -repository=$(pwd)/distribution-repository - -pushd git-repo > /dev/null -run_maven -f spring-boot-tests/spring-boot-integration-tests/pom.xml clean install -U -Dfull -Drepository=file://${repository} -popd > /dev/null diff --git a/ci/scripts/build-pr-project.sh b/ci/scripts/build-pr-project.sh new file mode 100755 index 000000000000..9a80167ac9b3 --- /dev/null +++ b/ci/scripts/build-pr-project.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e + +source $(dirname $0)/common.sh + +pushd git-repo > /dev/null +./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --max-workers=4 --continue build +popd > /dev/null diff --git a/ci/scripts/build-project-windows.bat b/ci/scripts/build-project-windows.bat index efade5d1acfa..b260a9b1fad9 100755 --- a/ci/scripts/build-project-windows.bat +++ b/ci/scripts/build-project-windows.bat @@ -1,6 +1,4 @@ -SET "JAVA_HOME=C:\opt\jdk-8" -SET PATH=%PATH%;C:\Program Files\Git\usr\bin -cd git-repo - -echo ".\mvnw clean install" > build.log -.\mvnw clean install -U >> build.log 2>&1 || (sleep 1 && tail -n 3000 build.log && exit 1) \ No newline at end of file +SET "JAVA_HOME=C:\opt\jdk-8" +SET PATH=%PATH%;C:\Program Files\Git\usr\bin +cd git-repo +.\gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --max-workers=4 build diff --git a/ci/scripts/build-project.sh b/ci/scripts/build-project.sh index da3c44e4723a..56e0ad28e83d 100755 --- a/ci/scripts/build-project.sh +++ b/ci/scripts/build-project.sh @@ -5,6 +5,9 @@ source $(dirname $0)/common.sh repository=$(pwd)/distribution-repository pushd git-repo > /dev/null -run_maven -N clean verify -run_maven -f spring-boot-project/pom.xml clean deploy -U -Dfull -DaltDeploymentRepository=distribution::default::file://${repository} +if [[ -d /opt/openjdk-toolchain ]]; then + ./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --max-workers=4 -PdeploymentRepository=${repository} build publishAllPublicationsToDeploymentRepository -PtoolchainVersion=${TOOLCHAIN_JAVA_VERSION} -Porg.gradle.java.installations.auto-detect=false -Porg.gradle.java.installations.auto-download=false -Porg.gradle.java.installations.paths=/opt/openjdk-toolchain/ +else + ./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --max-workers=4 -PdeploymentRepository=${repository} build publishAllPublicationsToDeploymentRepository +fi popd > /dev/null diff --git a/ci/scripts/build-smoke-tests.sh b/ci/scripts/build-smoke-tests.sh deleted file mode 100755 index cfb839c0efa7..000000000000 --- a/ci/scripts/build-smoke-tests.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -set -e - -source $(dirname $0)/common.sh -repository=$(pwd)/distribution-repository - -pushd git-repo > /dev/null -run_maven -f spring-boot-tests/spring-boot-smoke-tests/pom.xml clean install -U -Dfull -Drepository=file://${repository} -popd > /dev/null diff --git a/ci/scripts/common.sh b/ci/scripts/common.sh index 0c0f901c5072..ca7724f35dc2 100644 --- a/ci/scripts/common.sh +++ b/ci/scripts/common.sh @@ -5,4 +5,8 @@ if [[ -d $PWD/embedmongo && ! -d $HOME/.embedmongo ]]; then ln -s "$PWD/embedmongo" "$HOME/.embedmongo" fi +if [[ -n $DOCKER_HUB_USERNAME ]]; then + docker login -u $DOCKER_HUB_USERNAME -p $DOCKER_HUB_PASSWORD +fi + cleanup_maven_repo "org.springframework.boot" diff --git a/ci/scripts/detect-docker-updates.sh b/ci/scripts/detect-docker-updates.sh new file mode 100755 index 000000000000..3d15719b56ec --- /dev/null +++ b/ci/scripts/detect-docker-updates.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +latest_version=$(curl -I -s https://github.com/moby/moby/releases/latest | grep -i "location:" | awk '{n=split($0, parts, "/"); print substr(parts[n],2);}' | awk '{$1=$1;print}' | tr -d '\r' | tr -d '\n' ) + +if [[ $latest_version =~ (beta|rc) ]]; then + echo "Skip pre-release versions" + exit 0; +fi + +title_prefix="Upgrade CI to Docker" +milestone_number=$( curl -s https://api.github.com/repos/${GITHUB_ORGANIZATION}/${GITHUB_REPO}/milestones\?state\=open | jq -c --arg MILESTONE "$MILESTONE" '.[] | select(.title==$MILESTONE)' | jq -r '.number') +existing_upgrade_issues=$( curl -s https://api.github.com/repos/${GITHUB_ORGANIZATION}/${GITHUB_REPO}/issues\?labels\=type:%20task\&state\=open\&creator\=spring-builds\&milestone\=${milestone_number} | jq -c --arg TITLE_PREFIX "$title_prefix" '.[] | select(.title | startswith($TITLE_PREFIX))' ) + +latest="https://download.docker.com/linux/static/stable/x86_64/docker-$latest_version.tgz" +current=$( git-repo/ci/images/get-docker-url.sh ) + +if [[ $current = $latest ]]; then + echo "Already up-to-date" + exit 0; +fi + +ISSUE_TITLE="$title_prefix $latest_version" + +if [[ ${existing_upgrade_issues} = "" ]]; then + curl \ + -s \ + -u ${GITHUB_USERNAME}:${GITHUB_PASSWORD} \ + -H "Content-type:application/json" \ + -d "{\"title\":\"${ISSUE_TITLE}\",\"milestone\":\"${milestone_number}\",\"body\": \"${latest}\",\"labels\":[\"type: task\"]}" \ + -f \ + -X \ + POST "https://api.github.com/repos/${GITHUB_ORGANIZATION}/${GITHUB_REPO}/issues" > /dev/null || { echo "Failed to create issue" >&2; exit 1; } +else + echo "Issue already exists." +fi \ No newline at end of file diff --git a/ci/scripts/detect-jdk-updates.sh b/ci/scripts/detect-jdk-updates.sh index f848df73b1f7..3d67c7773ea6 100755 --- a/ci/scripts/detect-jdk-updates.sh +++ b/ci/scripts/detect-jdk-updates.sh @@ -1,25 +1,40 @@ #!/bin/bash +report_error() { + echo "Script exited with error $1 on line $2" + exit 1; +} + +trap 'report_error $? $LINENO' ERR + case "$JDK_VERSION" in java8) - BASE_URL="https://api.adoptopenjdk.net/v2/info/releases/openjdk8" + BASE_URL="https://api.adoptium.net/v3/assets/feature_releases/8/ga" ISSUE_TITLE="Upgrade Java 8 version in CI image" ;; java11) - BASE_URL="https://api.adoptopenjdk.net/v2/info/releases/openjdk11" + BASE_URL="https://api.adoptium.net/v3/assets/feature_releases/11/ga" ISSUE_TITLE="Upgrade Java 11 version in CI image" ;; - java13) - BASE_URL="https://api.adoptopenjdk.net/v2/info/releases/openjdk13" - ISSUE_TITLE="Upgrade Java 13 version in CI image" + java17) + BASE_URL="https://api.adoptium.net/v3/assets/feature_releases/17/ga" + ISSUE_TITLE="Upgrade Java 17 version in CI image" + ;; + java18) + BASE_URL="https://api.adoptium.net/v3/assets/feature_releases/18/ga" + ISSUE_TITLE="Upgrade Java 18 version in CI image" ;; *) echo $"Unknown java version" exit 1; esac -response=$( curl -s ${BASE_URL}\?openjdk_impl\=hotspot\&os\=linux\&arch\=x64\&release\=latest\&type\=jdk ) -latest=$( jq -r '.binaries[0].binary_link' <<< "$response" ) +response=$( curl -s ${BASE_URL}\?architecture\=x64\&heap_size\=normal\&image_type\=jdk\&jvm_impl\=hotspot\&os\=linux\&sort_order\=DESC\&vendor\=adoptium ) +latest=$( jq -r '.[0].binaries[0].package.link' <<< "$response" ) +if [[ ${latest} = "null" || ${latest} = "" ]]; then + echo "Could not parse JDK response: $response" + exit 1; +fi current=$( git-repo/ci/images/get-jdk-url.sh ${JDK_VERSION} ) @@ -28,15 +43,23 @@ if [[ $current = $latest ]]; then exit 0; fi -existing_tasks=$( curl -s https://api.github.com/repos/${GITHUB_ORGANIZATION}/${GITHUB_REPO}/issues\?labels\=type:%20task\&state\=open\&creator\=spring-buildmaster ) -existing_jdk_issues=$( echo "$existing_tasks" | jq -c --arg TITLE "$ISSUE_TITLE" '.[] | select(.title==$TITLE)' ) +milestone_response=$( curl -s https://api.github.com/repos/${GITHUB_ORGANIZATION}/${GITHUB_REPO}/milestones\?state\=open ) +milestone_result=$( jq -r -c --arg MILESTONE "$MILESTONE" '.[] | select(has("title")) | select(.title==$MILESTONE)' <<< "$milestone_response" ) +if [[ ${milestone_result} = "null" || ${milestone_result} = "" ]]; then + echo "Could not parse milestone: $milestone_response" + exit 1; +fi + +milestone_number=$( jq -r '.number' <<< "$milestone_result" ) +existing_tasks=$( curl -s https://api.github.com/repos/${GITHUB_ORGANIZATION}/${GITHUB_REPO}/issues\?labels\=type:%20task\&state\=open\&creator\=spring-builds\&milestone\=${milestone_number} ) +existing_jdk_issues=$( jq -r -c --arg TITLE "$ISSUE_TITLE" '.[] | select(has("title")) | select(.title==$TITLE)' <<< "$existing_tasks" ) if [[ ${existing_jdk_issues} = "" ]]; then curl \ -s \ -u ${GITHUB_USERNAME}:${GITHUB_PASSWORD} \ -H "Content-type:application/json" \ - -d "{\"title\":\"${ISSUE_TITLE}\",\"body\": \"${latest}\",\"labels\":[\"status: waiting-for-triage\",\"type: task\"]}" \ + -d "{\"title\":\"${ISSUE_TITLE}\",\"milestone\":\"${milestone_number}\",\"body\": \"${latest}\",\"labels\":[\"type: task\"]}" \ -f \ -X \ POST "https://api.github.com/repos/${GITHUB_ORGANIZATION}/${GITHUB_REPO}/issues" > /dev/null || { echo "Failed to create issue" >&2; exit 1; } diff --git a/ci/scripts/detect-ubuntu-image-updates.sh b/ci/scripts/detect-ubuntu-image-updates.sh new file mode 100755 index 000000000000..9557a5658f28 --- /dev/null +++ b/ci/scripts/detect-ubuntu-image-updates.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +ISSUE_TITLE="Upgrade Ubuntu version in CI images" + +ubuntu="focal" +latest=$( curl -s "https://hub.docker.com/v2/repositories/library/ubuntu/tags/?page_size=1&page=1&name=$ubuntu" | jq -c -r '.results[0].name' | awk '{split($0, parts, "-"); print parts[2]}' ) +current=$( grep "ubuntu:$ubuntu" git-repo/ci/images/ci-image/Dockerfile | awk '{split($0, parts, "-"); print parts[2]}' ) + +if [[ $current = $latest ]]; then + echo "Already up-to-date" + exit 0; +fi + +milestone_number=$( curl -s https://api.github.com/repos/${GITHUB_ORGANIZATION}/${GITHUB_REPO}/milestones\?state\=open | jq -c --arg MILESTONE "$MILESTONE" '.[] | select(.title==$MILESTONE)' | jq -r '.number') +existing_tasks=$( curl -s https://api.github.com/repos/${GITHUB_ORGANIZATION}/${GITHUB_REPO}/issues\?labels\=type:%20task\&state\=open\&creator\=spring-builds\&milestone\=${milestone_number} ) +existing_upgrade_issues=$( echo "$existing_tasks" | jq -c --arg TITLE "$ISSUE_TITLE" '.[] | select(.title==$TITLE)' ) + +if [[ ${existing_upgrade_issues} = "" ]]; then + curl \ + -s \ + -u ${GITHUB_USERNAME}:${GITHUB_PASSWORD} \ + -H "Content-type:application/json" \ + -d "{\"title\":\"${ISSUE_TITLE}\",\"milestone\":\"${milestone_number}\",\"body\": \"Upgrade to ubuntu:${ubuntu}-${latest}\",\"labels\":[\"type: task\"]}" \ + -f \ + -X \ + POST "https://api.github.com/repos/${GITHUB_ORGANIZATION}/${GITHUB_REPO}/issues" > /dev/null || { echo "Failed to create issue" >&2; exit 1; } +else + echo "Issue already exists." +fi diff --git a/ci/scripts/generate-changelog.sh b/ci/scripts/generate-changelog.sh new file mode 100755 index 000000000000..d3d2b97e5dba --- /dev/null +++ b/ci/scripts/generate-changelog.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -e + +CONFIG_DIR=git-repo/ci/config +version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' ) + +java -jar /github-changelog-generator.jar \ + --spring.config.location=${CONFIG_DIR}/changelog-generator.yml \ + ${version} generated-changelog/changelog.md + +echo ${version} > generated-changelog/version +echo v${version} > generated-changelog/tag diff --git a/ci/scripts/generate-release-notes.sh b/ci/scripts/generate-release-notes.sh deleted file mode 100755 index aa84b8dd5403..000000000000 --- a/ci/scripts/generate-release-notes.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash -set -e - -version=$( cat version/version ) - -milestone=${version} -if [[ $RELEASE_TYPE = "RELEASE" ]]; then - milestone=${version%.RELEASE} -fi - -java -jar /github-release-notes-generator.jar \ - --releasenotes.github.username=${GITHUB_USERNAME} \ - --releasenotes.github.password=${GITHUB_TOKEN} \ - --releasenotes.github.organization=spring-projects \ - --releasenotes.github.repository=spring-boot \ - ${milestone} generated-release-notes/release-notes.md - -echo ${version} > generated-release-notes/version -echo v${version} > generated-release-notes/tag diff --git a/ci/scripts/promote.sh b/ci/scripts/promote.sh index ce4f08b8bc10..14ad9861e6e1 100755 --- a/ci/scripts/promote.sh +++ b/ci/scripts/promote.sh @@ -2,13 +2,12 @@ source $(dirname $0)/common.sh +version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' ) export BUILD_INFO_LOCATION=$(pwd)/artifactory-repo/build-info.json -java -jar /spring-boot-release-scripts.jar promote $RELEASE_TYPE $BUILD_INFO_LOCATION > /dev/null || { exit 1; } +java -jar /spring-boot-release-scripts.jar publishToCentral $RELEASE_TYPE $BUILD_INFO_LOCATION artifactory-repo || { exit 1; } -java -jar /spring-boot-release-scripts.jar distribute $RELEASE_TYPE $BUILD_INFO_LOCATION > /dev/null || { exit 1; } - -java -jar /spring-boot-release-scripts.jar publishGradlePlugin $RELEASE_TYPE $BUILD_INFO_LOCATION > /dev/null || { exit 1; } +java -jar /spring-boot-release-scripts.jar promote $RELEASE_TYPE $BUILD_INFO_LOCATION || { exit 1; } echo "Promotion complete" echo $version > version/version diff --git a/ci/scripts/publish-gradle-plugin.sh b/ci/scripts/publish-gradle-plugin.sh new file mode 100755 index 000000000000..5ee841d430fb --- /dev/null +++ b/ci/scripts/publish-gradle-plugin.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +source $(dirname $0)/common.sh + +version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' ) + +git-repo/gradlew publishExisting -p git-repo/ci/config/gradle-plugin-publishing -Pgradle.publish.key=${GRADLE_PUBLISH_KEY} -Pgradle.publish.secret=${GRADLE_PUBLISH_SECRET} -PbootVersion=${version} -PrepositoryRoot=$(pwd)/artifactory-repo diff --git a/ci/scripts/publish-to-sdkman.sh b/ci/scripts/publish-to-sdkman.sh new file mode 100755 index 000000000000..00bb6adc26e5 --- /dev/null +++ b/ci/scripts/publish-to-sdkman.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +source $(dirname $0)/common.sh + +version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' ) + +java -jar /spring-boot-release-scripts.jar publishToSdkman $RELEASE_TYPE $version $LATEST_GA || { exit 1; } + +echo "Push to SDKMAN complete" diff --git a/ci/scripts/stage.sh b/ci/scripts/stage.sh index 2ff3a71bb375..bfc2690198e5 100755 --- a/ci/scripts/stage.sh +++ b/ci/scripts/stage.sh @@ -12,7 +12,7 @@ git clone git-repo stage-git-repo > /dev/null pushd stage-git-repo > /dev/null -snapshotVersion=$( get_revision_from_pom ) +snapshotVersion=$( awk -F '=' '$1 == "version" { print $2 }' gradle.properties ) if [[ $RELEASE_TYPE = "M" ]]; then stageVersion=$( get_next_milestone_release $snapshotVersion) nextVersion=$snapshotVersion @@ -27,26 +27,23 @@ else fi echo "Staging $stageVersion (next version will be $nextVersion)" +sed -i "s/version=$snapshotVersion/version=$stageVersion/" gradle.properties -set_revision_to_pom "$stageVersion" -git config user.name "Spring Buildmaster" > /dev/null -git config user.email "buildmaster@springframework.org" > /dev/null -git add pom.xml > /dev/null +git config user.name "Spring Builds" > /dev/null +git config user.email "spring-builds@users.noreply.github.com" > /dev/null +git add gradle.properties > /dev/null git commit -m"Release v$stageVersion" > /dev/null git tag -a "v$stageVersion" -m"Release v$stageVersion" > /dev/null -run_maven -f spring-boot-project/pom.xml clean deploy -U -Dfull -DaltDeploymentRepository=distribution::default::file://${repository} -run_maven -f spring-boot-tests/spring-boot-smoke-tests/pom.xml clean install -U -Dfull -Drepository=file://${repository} -run_maven -f spring-boot-tests/spring-boot-integration-tests/pom.xml clean install -U -Dfull -Drepository=file://${repository} -run_maven -f spring-boot-tests/spring-boot-deployment-tests/pom.xml clean install -U -Dfull -Drepository=file://${repository} +./gradlew --no-daemon --max-workers=4 -PdeploymentRepository=${repository} build publishAllPublicationsToDeploymentRepository git reset --hard HEAD^ > /dev/null if [[ $nextVersion != $snapshotVersion ]]; then echo "Setting next development version (v$nextVersion)" - set_revision_to_pom "$nextVersion" - git add pom.xml > /dev/null + sed -i "s/version=$snapshotVersion/version=$nextVersion/" gradle.properties + git add gradle.properties > /dev/null git commit -m"Next development version (v$nextVersion)" > /dev/null -fi; +fi echo "DONE" diff --git a/ci/scripts/sync-to-maven-central.sh b/ci/scripts/sync-to-maven-central.sh deleted file mode 100755 index 1c324ac508a9..000000000000 --- a/ci/scripts/sync-to-maven-central.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -export BUILD_INFO_LOCATION=$(pwd)/artifactory-repo/build-info.json - -java -jar /spring-boot-release-scripts.jar syncToCentral "RELEASE" $BUILD_INFO_LOCATION > /dev/null || { exit 1; } - -echo "Sync complete" -echo $version > version/version diff --git a/ci/scripts/update-homebrew-tap.sh b/ci/scripts/update-homebrew-tap.sh new file mode 100755 index 000000000000..591f5954139e --- /dev/null +++ b/ci/scripts/update-homebrew-tap.sh @@ -0,0 +1,19 @@ +#!/bin/bash +set -e + +version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' ) + +git clone homebrew-tap-repo updated-homebrew-tap-repo > /dev/null + +if [[ $LATEST_GA = true ]]; then +pushd updated-homebrew-tap-repo > /dev/null + curl https://repo.spring.io/libs-release-local/org/springframework/boot/spring-boot-cli/${version}/spring-boot-cli-${version}-homebrew.rb --output spring-boot-cli-${version}-homebrew.rb + rm spring-boot.rb + mv spring-boot-cli-*.rb spring-boot.rb + git config user.name "Spring Builds" > /dev/null + git config user.email "spring-builds@users.noreply.github.com" > /dev/null + git add spring-boot.rb > /dev/null + git commit -m "Upgrade to Spring Boot ${version}" > /dev/null + echo "DONE" +popd > /dev/null +fi diff --git a/ci/tasks/build-ci-image.yml b/ci/tasks/build-ci-image.yml new file mode 100644 index 000000000000..00f1804a44db --- /dev/null +++ b/ci/tasks/build-ci-image.yml @@ -0,0 +1,32 @@ +--- +platform: linux +image_resource: + type: registry-image + source: + repository: concourse/oci-build-task + tag: 0.9.1 + registry_mirror: + host: ((docker-hub-mirror)) + username: ((docker-hub-mirror-username)) + password: ((docker-hub-mirror-password)) +inputs: +- name: ci-images-git-repo +outputs: +- name: image +caches: +- path: ci-image-cache +params: + CONTEXT: ci-images-git-repo/ci/images + DOCKERFILE: ci-images-git-repo/ci/images/((ci-image-name))/Dockerfile + REGISTRY_MIRRORS: ((docker-hub-mirror)) + DOCKER_HUB_AUTH: ((docker-hub-auth)) +run: + path: /bin/sh + args: + - "-c" + - | + mkdir -p /root/.docker + cat > /root/.docker/config.json < + xsi:schemaLocation="http://www.eclipse.org/oomph/setup/jdt/1.0 http://git.eclipse.org/c/oomph/org.eclipse.oomph.git/plain/setups/models/JDT.ecore http://www.eclipse.org/buildship/oomph/1.0 https://raw.githubusercontent.com/eclipse/buildship/master/org.eclipse.buildship.oomph/model/GradleImport-1.0.ecore http://www.eclipse.org/oomph/predicates/1.0 http://git.eclipse.org/c/oomph/org.eclipse.oomph.git/plain/setups/models/Predicates.ecore http://www.eclipse.org/oomph/setup/workingsets/1.0 http://git.eclipse.org/c/oomph/org.eclipse.oomph.git/plain/setups/models/SetupWorkingSets.ecore http://www.eclipse.org/oomph/workingsets/1.0 http://git.eclipse.org/c/oomph/org.eclipse.oomph.git/plain/setups/models/WorkingSets.ecore" + name="spring.boot.2.5.x" + label="Spring Boot 2.5.x"> + name="io.spring.javaformat.eclipse.feature.feature.group"/> + name="org.eclipse.m2e.feature.feature.group"/> + name="org.eclipse.oomph.setup.maven.feature.group"/> + + name="org.springframework.boot.ide.main.feature.feature.group"/> + + + name="org.eclipse.buildship.feature.group"/> + name="org.eclipse.buildship.oomph.feature.group"/> + url="https://repo.spring.io/javaformat-eclipse-update-site/"/> + url="https://download.springsource.com/release/TOOLS/sts4/update/latest/"/> + Install the tools needed in the IDE to work with the source code for ${scope.project.label} + xsi:type="oomph:GradleImportTask"> - spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin - - + locateNestedProjects="true"/> @@ -122,19 +127,31 @@ name="spring-boot-tools"> + pattern="spring-boot-(tools|antlib|configuration-.*|loader|.*-tools|.*-layertools|.*-plugin|autoconfigure-processor|buildpack.*)"/> + xsi:type="predicates:OrPredicate"> + + + + xsi:type="predicates:OrPredicate"> + + + diff --git a/git/hooks/commit-msg b/git/hooks/commit-msg deleted file mode 120000 index dee7d3fc93f0..000000000000 --- a/git/hooks/commit-msg +++ /dev/null @@ -1 +0,0 @@ -../../git/hooks/forward-merge \ No newline at end of file diff --git a/git/hooks/forward-merge b/git/hooks/forward-merge index 2d0a3b199b6d..14872f472746 100755 --- a/git/hooks/forward-merge +++ b/git/hooks/forward-merge @@ -2,6 +2,10 @@ require 'json' require 'net/http' require 'yaml' +require 'logger' + +$log = Logger.new(STDOUT) +$log.level = Logger::WARN class ForwardMerge attr_reader :issue, :milestone, :message, :line @@ -13,49 +17,74 @@ class ForwardMerge end end -def find_forward_merge(message_file) - rev=`git rev-parse -q --verify MERGE_HEAD` +def find_forward_merges(message_file) + $log.debug "Searching for forward merge" + rev=`git rev-parse -q --verify MERGE_HEAD`.strip + $log.debug "Found #{rev} from git rev-parse" return nil unless rev message = File.read(message_file) + forward_merges = [] message.each_line do |line| - match = /^(?:Fixes|Closes) gh-(\d+) in ([\d\.]+(?:(?:M|RC)\d)?)$/.match(line) + $log.debug "Checking #{line} for message" + match = /^(?:Fixes|Closes) gh-(\d+) in (\d\.\d\.[\dx](?:[\.\-](?:M|RC)\d)?)$/.match(line) if match then issue = match[1] milestone = match[2] - return ForwardMerge.new(issue, milestone, message, line) + $log.debug "Matched reference to issue #{issue} in milestone #{milestone}" + forward_merges << ForwardMerge.new(issue, milestone, message, line) end end - return nil + $log.debug "No match in merge message" unless forward_merges + return forward_merges end -def find_milestone(username, password, repository, title) - uri = URI("https://api.github.com/repos/#{repository}/milestones") +def get_issue(username, password, repository, number) + $log.debug "Getting issue #{number} from GitHub repository #{repository}" + uri = URI("https://api.github.com/repos/#{repository}/issues/#{number}") http = Net::HTTP.new(uri.host, uri.port) http.use_ssl=true request = Net::HTTP::Get.new(uri.path) request.basic_auth(username, password) response = http.request(request) - milestones = JSON.parse(response.body) - milestones.each do |milestone| - return milestone['number'] if milestone['title'] == title - end - puts "Milestone #{title} not found" + $log.debug "Get HTTP response #{response.code}" + return JSON.parse(response.body) unless response.code != '200' + puts "Failed to retrieve issue #{number}: #{response.message}" exit 1 end -def get_issue(username, password, repository, number) - uri = URI("https://api.github.com/repos/#{repository}/issues/#{number}") +def find_milestone(username, password, repository, title) + $log.debug "Finding milestone #{title} from GitHub repository #{repository}" + uri = URI("https://api.github.com/repos/#{repository}/milestones") http = Net::HTTP.new(uri.host, uri.port) http.use_ssl=true request = Net::HTTP::Get.new(uri.path) request.basic_auth(username, password) response = http.request(request) - return JSON.parse(response.body) unless response.code != '200' - puts "Failed to retrieve issue #{number}: #{response.message}" + milestones = JSON.parse(response.body) + if title.end_with?(".x") + prefix = title.delete_suffix('.x') + $log.debug "Finding nearest milestone from candidates starting with #{prefix}" + titles = milestones.map { |milestone| milestone['title'] } + titles = titles.select{ |title| title.start_with?(prefix) unless title.end_with?('.x')} + titles = titles.sort_by { |v| Gem::Version.new(v) } + $log.debug "Considering candidates #{titles}" + if(titles.empty?) + puts "Cannot find nearest milestone for prefix #{title}" + exit 1 + end + title = titles.first + $log.debug "Found nearest milestone #{title}" + end + milestones.each do |milestone| + $log.debug "Considering #{milestone['title']}" + return milestone['number'] if milestone['title'] == title + end + puts "Milestone #{title} not found" exit 1 end def create_issue(username, password, repository, original, title, labels, milestone, milestone_name, dry_run) + $log.debug "Finding forward-merge issue in GitHub repository #{repository} for '#{title}'" uri = URI("https://api.github.com/repos/#{repository}/issues") http = Net::HTTP.new(uri.host, uri.port) http.use_ssl=true @@ -73,23 +102,34 @@ def create_issue(username, password, repository, original, title, labels, milest return "dry-run" end response = JSON.parse(http.request(request).body) + $log.debug "Created new issue #{response['number']}" return response['number'] end +$log.debug "Running forward-merge hook script" message_file=ARGV[0] -forward_merge = find_forward_merge(message_file) -exit 0 unless forward_merge + +forward_merges = find_forward_merges(message_file) +exit 0 unless forward_merges + +$log.debug "Loading config from ~/.spring-boot/forward_merge.yml" config = YAML.load_file(File.join(Dir.home, '.spring-boot', 'forward-merge.yml')) username = config['github']['credentials']['username'] password = config['github']['credentials']['password'] dry_run = config['dry_run'] repository = 'spring-projects/spring-boot' -existing_issue = get_issue(username, password, repository, forward_merge.issue) -title = existing_issue['title'] -labels = existing_issue['labels'].map { |label| label['name'] } -labels << "status: forward-port" -milestone = find_milestone(username, password, repository, forward_merge.milestone) -new_issue_number = create_issue(username, password, repository, forward_merge.issue, title, labels, milestone, forward_merge.milestone, dry_run) -puts "Created gh-#{new_issue_number} for forward port of gh-#{forward_merge.issue} into #{forward_merge.milestone}" -rewritten_message = forward_merge.message.sub(forward_merge.line, "Closes gh-#{new_issue_number}\n") -File.write(message_file, rewritten_message) + +forward_merges.each do |forward_merge| + existing_issue = get_issue(username, password, repository, forward_merge.issue) + title = existing_issue['title'] + labels = existing_issue['labels'].map { |label| label['name'] } + labels << "status: forward-port" + $log.debug "Processing issue '#{title}'" + + milestone = find_milestone(username, password, repository, forward_merge.milestone) + new_issue_number = create_issue(username, password, repository, forward_merge.issue, title, labels, milestone, forward_merge.milestone, dry_run) + + puts "Created gh-#{new_issue_number} for forward port of gh-#{forward_merge.issue} into #{forward_merge.milestone}" + rewritten_message = forward_merge.message.sub(forward_merge.line, "Closes gh-#{new_issue_number}\n") + File.write(message_file, rewritten_message) +end diff --git a/git/hooks/prepare-forward-merge b/git/hooks/prepare-forward-merge new file mode 100755 index 000000000000..163e3ed91518 --- /dev/null +++ b/git/hooks/prepare-forward-merge @@ -0,0 +1,71 @@ +#!/usr/bin/ruby +require 'json' +require 'net/http' +require 'yaml' +require 'logger' + +$main_branch = "2.5.x" + +$log = Logger.new(STDOUT) +$log.level = Logger::WARN + +def get_fixed_issues() + $log.debug "Searching for for forward merge" + rev=`git rev-parse -q --verify MERGE_HEAD`.strip + $log.debug "Found #{rev} from git rev-parse" + return nil unless rev + fixed = [] + message = `git log -1 --pretty=%B #{rev}` + message.each_line do |line| + $log.debug "Checking #{line} for message" + fixed << line.strip if /^(?:Fixes|Closes) gh-(\d+)/.match(line) + end + $log.debug "Found fixed issues #{fixed}" + return fixed; +end + +def rewrite_message(message_file, fixed) + current_branch = `git rev-parse --abbrev-ref HEAD`.strip + if current_branch == "main" + current_branch = $main_branch + end + rewritten_message = "" + message = File.read(message_file) + message.each_line do |line| + match = /^Merge.*branch\ '(.*)'(?:\ into\ (.*))?$/.match(line) + if match + from_branch = match[1] + if from_branch.include? "/" + from_branch = from_branch.partition("/").last + end + to_brach = match[2] + $log.debug "Rewriting merge message" + line = "Merge branch '#{from_branch}'" + (to_brach ? " into #{to_brach}\n" : "\n") + end + if fixed and line.start_with?("#") + $log.debug "Adding fixed" + rewritten_message << "\n" + fixed.each do |fixes| + rewritten_message << "#{fixes} in #{current_branch}\n" + end + fixed = nil + end + rewritten_message << line + end + return rewritten_message +end + +$log.debug "Running prepare-forward-merge hook script" + +message_file=ARGV[0] +message_type=ARGV[1] + +if message_type != "merge" + $log.debug "Not a merge commit" + exit 0; +end + +$log.debug "Searching for for forward merge" +fixed = get_fixed_issues() +rewritten_message = rewrite_message(message_file, fixed) +File.write(message_file, rewritten_message) diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000000..2035e90dfd7b --- /dev/null +++ b/gradle.properties @@ -0,0 +1,10 @@ +version=2.5.12 + +org.gradle.caching=true +org.gradle.parallel=true +org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 + +kotlinVersion=1.5.32 +tomcatVersion=9.0.60 + +kotlin.stdlib.default.dependency=false diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000000..e708b1c023ec Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties similarity index 91% rename from spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/gradle/wrapper/gradle-wrapper.properties rename to gradle/wrapper/gradle-wrapper.properties index 290541c73864..ec991f9aa12c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 000000000000..1b6c787337ff --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000000..107acd32c4e6 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/mvnw b/mvnw deleted file mode 100755 index d560832b5c4e..000000000000 --- a/mvnw +++ /dev/null @@ -1,305 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you 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. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir -# -# Optional ENV vars -# ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files -# ---------------------------------------------------------------------------- - -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi - -fi - -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" - fi - fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` - fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi - -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="`which java`" - fi -fi - -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi - -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi - - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` - fi - # end of workaround - done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" - fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; -fi - -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi -else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - if [ -n "$MVNW_REPOURL" ]; then - jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.3/maven-wrapper-0.5.3.jar" - else - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.3/maven-wrapper-0.5.3.jar" - fi - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - if $cygwin; then - wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` - fi - - if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget "$jarUrl" -O "$wrapperJarPath" - else - wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" - fi - elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - curl -o "$wrapperJarPath" "$jarUrl" -f - else - curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f - fi - - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - # For Cygwin, switch paths to Windows format before running javac - if $cygwin; then - javaClass=`cygpath --path --windows "$javaClass"` - fi - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") - fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi -fi -########################################################################################## -# End of extension -########################################################################################## - -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` -fi - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd deleted file mode 100755 index d06ac67f13e5..000000000000 --- a/mvnw.cmd +++ /dev/null @@ -1,172 +0,0 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM https://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.3/maven-wrapper-0.5.3.jar" - -FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - echo Found %WRAPPER_JAR% -) else ( - if not "%MVNW_REPOURL%" == "" ( - SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.3/maven-wrapper-0.5.3.jar" - ) - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %DOWNLOAD_URL% - - powershell -Command "&{"^ - "$webclient = new-object System.Net.WebClient;"^ - "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ - "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ - "}"^ - "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ - "}" - echo Finished downloading %WRAPPER_JAR% -) -@REM End of extension - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 766ea338a9e6..000000000000 --- a/pom.xml +++ /dev/null @@ -1,384 +0,0 @@ - - - 4.0.0 - org.springframework.boot - spring-boot-build - ${revision} - pom - Spring Boot Build - Spring Boot Build - - 2.2.1.BUILD-SNAPSHOT - ${basedir} - - - - - - default - - - !disable-spring-boot-default-profile - - - - 0.0.15 - 0.0.3.RELEASE - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - 3.0.0 - - - com.puppycrawl.tools - checkstyle - 8.22 - - - io.spring.javaformat - spring-javaformat-checkstyle - ${spring-javaformat.version} - - - io.spring.nohttp - nohttp-checkstyle - ${nohttp-checkstyle.version} - - - - - checkstyle-validation - validate - - ${disable.checks} - src/checkstyle/checkstyle.xml - src/checkstyle/checkstyle-suppressions.xml - true - main.basedir=${main.basedir} - UTF-8 - - - check - - - - nohttp-checkstyle-validation - validate - - ${disable.checks} - src/checkstyle/nohttp-checkstyle.xml - src/checkstyle/nohttp-checkstyle-suppressions.xml - main.basedir=${main.basedir} - UTF-8 - ${basedir} - **/* - **/.git/**/*,**/target/**/,**/.flattened-pom.xml,**/*.class,**/spring-boot-gradle-plugin/build/**,**/spring-boot-gradle-plugin/bin/** - - - check - - false - - - - - io.spring.javaformat - spring-javaformat-maven-plugin - ${spring-javaformat.version} - - - validate - - ${disable.checks} - - - validate - - - - - - - - spring-boot-project - - spring-boot-tests - - - - - m2e - - - m2e.version - - - - spring-boot-project - spring-boot-tests - - - - repository - - - repository - - - - - repository - ${repository} - - true - - - - - - repository - ${repository} - - true - - - - - - - - - central - https://repo.maven.apache.org/maven2 - - false - - - - spring-milestone - Spring Milestone - https://repo.spring.io/milestone - - false - - - - spring-snapshot - Spring Snapshot - https://repo.spring.io/snapshot - - true - - - - rabbit-milestone - Rabbit Milestone - https://dl.bintray.com/rabbitmq/maven-milestones - - false - - - - - - central - https://repo.maven.apache.org/maven2 - - false - - - - spring-release - Spring Release - https://repo.spring.io/release - - - spring-milestone - Spring Milestone - https://repo.spring.io/milestone - - false - - - - spring-snapshot - Spring Snapshot - https://repo.spring.io/snapshot - - true - - - - - - - - - org.eclipse.m2e - lifecycle-mapping - 1.0.0 - - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - [1,) - - check - - - - - - - - - org.apache.maven.plugins - maven-dependency-plugin - [1,) - - copy - copy-dependencies - unpack - - - - - - - - - org.apache.maven.plugins - maven-enforcer-plugin - [1,) - - enforce - - - - - - - - - org.apache.maven.plugins - maven-invoker-plugin - [1,) - - install - - - - - - - - - org.apache.maven.plugins - maven-plugin-plugin - [1,) - - descriptor - helpmojo - - - - - - - - - org.basepom.maven - duplicate-finder-maven-plugin - [1,) - - check - - - - - - - - - org.codehaus.mojo - build-helper-maven-plugin - [1,) - - reserve-network-port - - - - - - - - - org.codehaus.mojo - flatten-maven-plugin - [1,) - - flatten - - - - - - - - - org.jetbrains.kotlin - kotlin-maven-plugin - [1,) - - compile - test-compile - - - - - - - - - org.jooq - jooq-codegen-maven - [1,) - - generate - - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - [1,) - - build-info - - - - - - - - - - - - - - diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 000000000000..d79ea55896de --- /dev/null +++ b/settings.gradle @@ -0,0 +1,85 @@ +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + maven { + url 'https://repo.spring.io/release' + } + if (version.endsWith('-SNAPSHOT')) { + maven { url "https://repo.spring.io/snapshot" } + } + } + resolutionStrategy { + eachPlugin { + if (requested.id.id == "org.jetbrains.kotlin.jvm") { + useVersion "${kotlinVersion}" + } + if (requested.id.id == "org.jetbrains.kotlin.plugin.spring") { + useVersion "${kotlinVersion}" + } + } + } +} + +plugins { + id "com.gradle.enterprise" version "3.8.1" + id "io.spring.ge.conventions" version "0.0.9" +} + +rootProject.name="spring-boot-build" + +settings.gradle.projectsLoaded { + gradleEnterprise { + buildScan { + def toolchainVersion = settings.gradle.rootProject.findProperty('toolchainVersion') + if (toolchainVersion != null) { + value('Toolchain version', toolchainVersion) + tag("JDK-$toolchainVersion") + } + def buildDir = settings.gradle.rootProject.getBuildDir() + buildDir.mkdirs() + new File(buildDir, "build-scan-uri.txt").text = "build scan not generated" + buildScanPublished { scan -> + buildDir.mkdirs() + new File(buildDir, "build-scan-uri.txt").text = "<${scan.buildScanUri}|build scan>\n" + } + } + } +} + +include "spring-boot-project:spring-boot-dependencies" +include "spring-boot-project:spring-boot-parent" +include "spring-boot-project:spring-boot-tools:spring-boot-antlib" +include "spring-boot-project:spring-boot-tools:spring-boot-autoconfigure-processor" +include "spring-boot-project:spring-boot-tools:spring-boot-buildpack-platform" +include "spring-boot-project:spring-boot-tools:spring-boot-configuration-metadata" +include "spring-boot-project:spring-boot-tools:spring-boot-configuration-processor" +include "spring-boot-project:spring-boot-tools:spring-boot-gradle-plugin" +include "spring-boot-project:spring-boot-tools:spring-boot-jarmode-layertools" +include "spring-boot-project:spring-boot-tools:spring-boot-loader" +include "spring-boot-project:spring-boot-tools:spring-boot-loader-tools" +include "spring-boot-project:spring-boot-tools:spring-boot-maven-plugin" +include "spring-boot-project:spring-boot-tools:spring-boot-test-support" +include "spring-boot-project:spring-boot" +include "spring-boot-project:spring-boot-autoconfigure" +include "spring-boot-project:spring-boot-actuator" +include "spring-boot-project:spring-boot-actuator-autoconfigure" +include "spring-boot-project:spring-boot-cli" +include "spring-boot-project:spring-boot-devtools" +include "spring-boot-project:spring-boot-docs" +include "spring-boot-project:spring-boot-properties-migrator" +include "spring-boot-project:spring-boot-test" +include "spring-boot-project:spring-boot-test-autoconfigure" +include "spring-boot-tests:spring-boot-deployment-tests" +include "spring-boot-tests:spring-boot-integration-tests:spring-boot-configuration-processor-tests" +include "spring-boot-tests:spring-boot-integration-tests:spring-boot-launch-script-tests" +include "spring-boot-tests:spring-boot-integration-tests:spring-boot-loader-tests" +include "spring-boot-tests:spring-boot-integration-tests:spring-boot-server-tests" + +file("${rootDir}/spring-boot-project/spring-boot-starters").eachDirMatch(~/spring-boot-starter.*/) { + include "spring-boot-project:spring-boot-starters:${it.name}" +} + +file("${rootDir}/spring-boot-tests/spring-boot-smoke-tests").eachDirMatch(~/spring-boot-smoke-test.*/) { + include "spring-boot-tests:spring-boot-smoke-tests:${it.name}" +} diff --git a/spring-boot-project/pom.xml b/spring-boot-project/pom.xml deleted file mode 100644 index 2e893ff2819b..000000000000 --- a/spring-boot-project/pom.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-build - ${revision} - - spring-boot-project - pom - Spring Boot Project - Spring Boot Project - - ${basedir}/.. - - - spring-boot-dependencies - spring-boot-parent - spring-boot - spring-boot-actuator - spring-boot-actuator-autoconfigure - spring-boot-autoconfigure - spring-boot-devtools - spring-boot-properties-migrator - spring-boot-test - spring-boot-test-autoconfigure - spring-boot-tools - spring-boot-starters - spring-boot-cli - spring-boot-docs - - diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle new file mode 100644 index 000000000000..5dce26b89179 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle @@ -0,0 +1,247 @@ +plugins { + id "java-library" + id "org.asciidoctor.jvm.convert" + id "org.springframework.boot.auto-configuration" + id "org.springframework.boot.conventions" + id "org.springframework.boot.deployed" + id "org.springframework.boot.optional-dependencies" +} + +description = "Spring Boot Actuator AutoConfigure" + +configurations { + documentation +} + +repositories { + maven { + url "https://repo.spring.io/release" + mavenContent { + includeGroup "io.spring.asciidoctor" + includeGroup "io.spring.asciidoctor.backends" + } + } +} + +dependencies { + asciidoctorExtensions("org.springframework.restdocs:spring-restdocs-asciidoctor") + asciidoctorExtensions("io.spring.asciidoctor:spring-asciidoctor-extensions-section-ids") + + api(project(":spring-boot-project:spring-boot-actuator")) + + api(project(":spring-boot-project:spring-boot")) + api(project(":spring-boot-project:spring-boot-autoconfigure")) + + implementation("com.fasterxml.jackson.core:jackson-databind") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") + + optional("ch.qos.logback:logback-classic") + optional("com.datastax.oss:java-driver-core") { + exclude group: "org.slf4j", module: "jcl-over-slf4j" + } + optional("com.fasterxml.jackson.dataformat:jackson-dataformat-xml") + optional("com.github.ben-manes.caffeine:caffeine") + optional("com.hazelcast:hazelcast") + optional("com.hazelcast:hazelcast-spring") + optional("com.sun.mail:jakarta.mail") + optional("com.zaxxer:HikariCP") + optional("io.dropwizard.metrics:metrics-jmx") + optional("io.lettuce:lettuce-core") + optional("io.micrometer:micrometer-core") + optional("io.micrometer:micrometer-jersey2") + optional("io.micrometer:micrometer-registry-appoptics") + optional("io.micrometer:micrometer-registry-atlas") { + exclude group: "javax.inject", module: "javax.inject" + } + optional("io.micrometer:micrometer-registry-datadog") + optional("io.micrometer:micrometer-registry-dynatrace") + optional("io.micrometer:micrometer-registry-elastic") + optional("io.micrometer:micrometer-registry-ganglia") + optional("io.micrometer:micrometer-registry-graphite") + optional("io.micrometer:micrometer-registry-humio") + optional("io.micrometer:micrometer-registry-influx") + optional("io.micrometer:micrometer-registry-jmx") + optional("io.micrometer:micrometer-registry-kairos") + optional("io.micrometer:micrometer-registry-new-relic") + optional("io.micrometer:micrometer-registry-prometheus") + optional("io.micrometer:micrometer-registry-stackdriver") { + exclude group: "commons-logging", module: "commons-logging" + exclude group: "javax.annotation", module: "javax.annotation-api" + } + optional("io.prometheus:simpleclient_pushgateway") { + exclude group: "javax.xml.bind", module: "jaxb-api" + } + optional("io.micrometer:micrometer-registry-signalfx") + optional("io.micrometer:micrometer-registry-statsd") + optional("io.micrometer:micrometer-registry-wavefront") + optional("io.projectreactor.netty:reactor-netty-http") + optional("io.r2dbc:r2dbc-pool") + optional("io.r2dbc:r2dbc-spi") + optional("jakarta.jms:jakarta.jms-api") + optional("jakarta.persistence:jakarta.persistence-api") + optional("jakarta.servlet:jakarta.servlet-api") + optional("javax.cache:cache-api") + optional("net.sf.ehcache:ehcache") + optional("org.apache.activemq:activemq-broker") { + exclude group: "org.apache.geronimo.specs", module: "geronimo-jms_1.1_spec" + exclude group: "org.apache.geronimo.specs", module: "geronimo-j2ee-management_1.1_spec" + } + optional("org.apache.commons:commons-dbcp2") { + exclude group: "commons-logging", module: "commons-logging" + } + optional("org.apache.kafka:kafka-clients") + optional("org.apache.kafka:kafka-streams") + optional("org.apache.solr:solr-solrj") { + exclude group: "org.slf4j", module: "jcl-over-slf4j" + } + optional("org.apache.tomcat.embed:tomcat-embed-core") + optional("org.apache.tomcat.embed:tomcat-embed-el") + optional("org.apache.tomcat:tomcat-jdbc") + optional("org.aspectj:aspectjweaver") + optional("org.eclipse.jetty:jetty-server") { + exclude group: "javax.servlet", module: "javax.servlet-api" + } + optional("org.elasticsearch:elasticsearch") + optional("org.elasticsearch.client:elasticsearch-rest-client") { + exclude group: "commons-logging", module: "commons-logging" + } + optional("org.flywaydb:flyway-core") + optional("org.glassfish.jersey.core:jersey-server") + optional("org.glassfish.jersey.containers:jersey-container-servlet-core") + optional("org.hibernate:hibernate-core") { + exclude group: "javax.activation", module: "javax.activation-api" + exclude group: "javax.persistence", module: "javax.persistence-api" + exclude group: "javax.xml.bind", module: "jaxb-api" + exclude group: "org.jboss.spec.javax.transaction", module: "jboss-transaction-api_1.2_spec" + } + optional("org.hibernate:hibernate-micrometer") { + exclude group: "javax.activation", module: "javax.activation-api" + exclude group: "javax.persistence", module: "javax.persistence-api" + exclude group: "javax.xml.bind", module: "jaxb-api" + exclude group: "org.jboss.spec.javax.transaction", module: "jboss-transaction-api_1.2_spec" + } + optional("org.hibernate.validator:hibernate-validator") + optional("org.influxdb:influxdb-java") + optional("org.jolokia:jolokia-core") + optional("org.liquibase:liquibase-core") { + exclude group: "javax.xml.bind", module: "jaxb-api" + } + optional("org.mongodb:mongodb-driver-reactivestreams") + optional("org.mongodb:mongodb-driver-sync") + optional("org.neo4j.driver:neo4j-java-driver") + optional("org.quartz-scheduler:quartz") + optional("org.springframework:spring-jdbc") + optional("org.springframework:spring-jms") + optional("org.springframework:spring-messaging") + optional("org.springframework:spring-webflux") + optional("org.springframework:spring-webmvc") + optional("org.springframework.amqp:spring-rabbit") + optional("org.springframework.data:spring-data-cassandra") { + exclude group: "org.slf4j", module: "jcl-over-slf4j" + } + optional("org.springframework.data:spring-data-couchbase") + optional("org.springframework.data:spring-data-jpa") + optional("org.springframework.data:spring-data-ldap") + optional("org.springframework.data:spring-data-mongodb") + optional("org.springframework.data:spring-data-redis") + optional("org.springframework.data:spring-data-elasticsearch") { + exclude group: "commons-logging", module: "commons-logging" + } + optional("org.springframework.integration:spring-integration-core") + optional("org.springframework.kafka:spring-kafka") + optional("org.springframework.security:spring-security-config") + optional("org.springframework.security:spring-security-web") + optional("org.springframework.session:spring-session-core") + optional("redis.clients:jedis") + + testImplementation(project(":spring-boot-project:spring-boot-test")) + testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) + testImplementation("io.projectreactor:reactor-test") + testImplementation("io.r2dbc:r2dbc-h2") + testImplementation("com.squareup.okhttp3:mockwebserver") + testImplementation("com.jayway.jsonpath:json-path") + testImplementation("io.undertow:undertow-core") + testImplementation("io.undertow:undertow-servlet") { + exclude group: "org.jboss.spec.javax.annotation", module: "jboss-annotations-api_1.3_spec" + exclude group: "org.jboss.spec.javax.servlet", module: "jboss-servlet-api_4.0_spec" + } + testImplementation("jakarta.xml.bind:jakarta.xml.bind-api") + testImplementation("org.apache.logging.log4j:log4j-to-slf4j") + testImplementation("org.aspectj:aspectjrt") + testImplementation("org.assertj:assertj-core") + testImplementation("org.awaitility:awaitility") + testImplementation("org.eclipse.jetty:jetty-webapp") { + exclude group: "javax.servlet", module: "javax.servlet-api" + } + testImplementation("org.glassfish.jersey.ext:jersey-spring5") + testImplementation("org.glassfish.jersey.media:jersey-media-json-jackson") + testImplementation("org.hamcrest:hamcrest") + testImplementation("org.hsqldb:hsqldb") + testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.mockito:mockito-core") + testImplementation("org.mockito:mockito-junit-jupiter") + testImplementation("org.skyscreamer:jsonassert") + testImplementation("org.springframework:spring-orm") + testImplementation("org.springframework.data:spring-data-rest-webmvc") + testImplementation("org.springframework.integration:spring-integration-jmx") + testImplementation("org.springframework.restdocs:spring-restdocs-mockmvc") { + exclude group: "javax.servlet", module: "javax.servlet-api" + } + testImplementation("org.springframework.restdocs:spring-restdocs-webtestclient") + testImplementation("org.springframework.security:spring-security-test") + testImplementation("org.yaml:snakeyaml") + + testRuntimeOnly("jakarta.management.j2ee:jakarta.management.j2ee-api") + testRuntimeOnly("jakarta.transaction:jakarta.transaction-api") + testRuntimeOnly("org.springframework.security:spring-security-oauth2-jose") + testRuntimeOnly("org.springframework.security:spring-security-oauth2-resource-server") + testRuntimeOnly("org.springframework.security:spring-security-saml2-service-provider") +} + +test { + outputs.dir("${buildDir}/generated-snippets") +} + +task dependencyVersions(type: org.springframework.boot.build.constraints.ExtractVersionConstraints) { + enforcedPlatform(":spring-boot-project:spring-boot-dependencies") +} + +tasks.withType(org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask) { + dependsOn dependencyVersions + doFirst { + def versionConstraints = dependencyVersions.versionConstraints + def integrationVersion = versionConstraints["org.springframework.integration:spring-integration-core"] + def integrationDocs = String.format("https://docs.spring.io/spring-integration/docs/%s/reference/html/", integrationVersion) + attributes "spring-integration-docs": integrationDocs + } + dependsOn test + inputs.dir("${buildDir}/generated-snippets").withPathSensitivity(PathSensitivity.RELATIVE).withPropertyName("generatedSnippets") +} + +asciidoctor { + sources { + include "index.adoc" + } +} + +task asciidoctorPdf(type: org.asciidoctor.gradle.jvm.AsciidoctorTask) { + sources { + include "index.adoc" + } +} + +task zip(type: Zip) { + dependsOn asciidoctor, asciidoctorPdf + duplicatesStrategy "fail" + from(asciidoctorPdf.outputDir) { + into "pdf" + rename { "spring-boot-actuator-web-api.pdf" } + } + from(asciidoctor.outputDir) { + into "htmlsingle" + } +} + +artifacts { + documentation zip +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/pom.xml b/spring-boot-project/spring-boot-actuator-autoconfigure/pom.xml deleted file mode 100644 index 3768e9f4f2e6..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/pom.xml +++ /dev/null @@ -1,858 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-parent - ${revision} - ../spring-boot-parent - - spring-boot-actuator-autoconfigure - Spring Boot Actuator AutoConfigure - Spring Boot Actuator AutoConfigure - - ${basedir}/../.. - ${project.build.directory}/refdocs/ - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - - org.springframework.boot - spring-boot-actuator - - - org.springframework.boot - spring-boot-autoconfigure - - - com.fasterxml.jackson.core - jackson-databind - - - org.springframework - spring-core - - - org.springframework - spring-context - - - - ch.qos.logback - logback-classic - true - - - com.fasterxml.jackson.dataformat - jackson-dataformat-xml - true - - - com.github.ben-manes.caffeine - caffeine - true - - - com.hazelcast - hazelcast - true - - - com.hazelcast - hazelcast-spring - true - - - com.sun.mail - jakarta.mail - true - - - com.zaxxer - HikariCP - true - - - io.dropwizard.metrics - metrics-jmx - true - - - io.lettuce - lettuce-core - true - - - io.micrometer - micrometer-core - true - - - io.micrometer - micrometer-jersey2 - true - - - io.micrometer - micrometer-registry-appoptics - true - - - io.micrometer - micrometer-registry-atlas - true - - - io.micrometer - micrometer-registry-datadog - true - - - io.micrometer - micrometer-registry-dynatrace - true - - - io.micrometer - micrometer-registry-elastic - true - - - io.micrometer - micrometer-registry-ganglia - true - - - io.micrometer - micrometer-registry-graphite - true - - - io.micrometer - micrometer-registry-humio - true - - - io.micrometer - micrometer-registry-influx - true - - - io.micrometer - micrometer-registry-jmx - true - - - io.micrometer - micrometer-registry-kairos - true - - - io.micrometer - micrometer-registry-new-relic - true - - - io.micrometer - micrometer-registry-prometheus - true - - - io.prometheus - simpleclient_pushgateway - true - - - io.micrometer - micrometer-registry-signalfx - true - - - io.micrometer - micrometer-registry-statsd - true - - - io.micrometer - micrometer-registry-wavefront - true - - - io.projectreactor.netty - reactor-netty - true - - - io.searchbox - jest - true - - - jakarta.jms - jakarta.jms-api - true - - - jakarta.servlet - jakarta.servlet-api - true - - - jakarta.persistence - jakarta.persistence-api - true - - - jakarta.ws.rs - jakarta.ws.rs-api - true - - - javax.cache - cache-api - true - - - net.sf.ehcache - ehcache - true - - - org.apache.activemq - activemq-broker - true - - - geronimo-jms_1.1_spec - org.apache.geronimo.specs - - - - - org.apache.commons - commons-dbcp2 - true - - - org.apache.kafka - kafka-clients - true - - - org.apache.tomcat.embed - tomcat-embed-core - true - - - org.apache.tomcat.embed - tomcat-embed-el - true - - - org.apache.tomcat - tomcat-jdbc - true - - - org.aspectj - aspectjweaver - true - - - org.eclipse.jetty - jetty-server - true - - - javax.servlet - javax.servlet-api - - - - - org.elasticsearch - elasticsearch - true - - - org.elasticsearch.client - elasticsearch-rest-client - true - - - org.flywaydb - flyway-core - true - - - org.glassfish.jersey.core - jersey-server - true - - - javax.validation - validation-api - - - - - org.glassfish.jersey.containers - jersey-container-servlet-core - true - - - org.hibernate - hibernate-core - true - - - javax.activation - javax.activation-api - - - javax.xml.bind - jaxb-api - - - javax.persistence - javax.persistence-api - - - - - org.hibernate.validator - hibernate-validator - true - - - javax.validation - validation-api - - - - - org.influxdb - influxdb-java - true - - - org.jolokia - jolokia-core - true - - - org.infinispan - infinispan-spring5-embedded - true - - - org.liquibase - liquibase-core - true - - - org.mongodb - mongodb-driver-async - true - - - org.mongodb - mongodb-driver-reactivestreams - true - - - org.springframework - spring-jdbc - true - - - org.springframework - spring-jms - true - - - org.springframework - spring-messaging - true - - - org.springframework - spring-webflux - true - - - org.springframework - spring-webmvc - true - - - org.springframework.amqp - spring-rabbit - true - - - org.springframework.data - spring-data-cassandra - true - - - org.springframework.data - spring-data-couchbase - true - - - org.springframework.data - spring-data-ldap - true - - - org.springframework.data - spring-data-mongodb - true - - - org.springframework.data - spring-data-neo4j - true - - - org.springframework.data - spring-data-redis - true - - - org.springframework.data - spring-data-solr - true - - - - wstx-asl - org.codehaus.woodstox - - - - - org.springframework.integration - spring-integration-core - true - - - org.springframework.security - spring-security-config - true - - - org.springframework.security - spring-security-web - true - - - org.springframework.session - spring-session-core - true - - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - runtime - - - - org.springframework.boot - spring-boot-autoconfigure-processor - true - - - org.springframework.boot - spring-boot-configuration-processor - true - - - - org.springframework.boot - spring-boot-test - test - - - org.springframework.boot - spring-boot-test-support - test - - - io.projectreactor - reactor-test - test - - - com.squareup.okhttp3 - mockwebserver - test - - - org.hamcrest - hamcrest-core - - - - - com.jayway.jsonpath - json-path - test - - - io.undertow - undertow-core - test - - - io.undertow - undertow-servlet - test - - - org.jboss.spec.javax.servlet - jboss-servlet-api_3.1_spec - - - - - jakarta.validation - jakarta.validation-api - test - - - jakarta.xml.bind - jakarta.xml.bind-api - test - - - org.apache.logging.log4j - log4j-to-slf4j - test - - - org.aspectj - aspectjrt - test - - - org.eclipse.jetty - jetty-webapp - test - - - org.hsqldb - hsqldb - test - - - org.glassfish.jersey.ext - jersey-spring5 - test - - - org.glassfish.jersey.media - jersey-media-json-jackson - test - - - org.skyscreamer - jsonassert - test - - - org.springframework - spring-orm - test - - - org.springframework.data - spring-data-elasticsearch - test - - - org.springframework.data - spring-data-rest-webmvc - test - - - org.springframework.integration - spring-integration-jmx - test - - - org.springframework.restdocs - spring-restdocs-mockmvc - test - - - javax.servlet - javax.servlet-api - - - - - org.springframework.restdocs - spring-restdocs-webtestclient - test - - - org.springframework.security - spring-security-test - test - - - org.springframework.security - spring-security-oauth2-resource-server - test - - - org.springframework.security - spring-security-oauth2-jose - test - - - org.yaml - snakeyaml - test - - - redis.clients - jedis - true - - - - - full - - - full - - - - - - com.googlecode.maven-download-plugin - download-maven-plugin - - - unpack-doc-resources - generate-resources - - wget - - - ${spring-doc-resources.url} - true - ${refdocs.build.directory} - - - - - - org.apache.maven.plugins - maven-antrun-plugin - - - ant-contrib - ant-contrib - 1.0b3 - - - ant - ant - - - - - org.apache.ant - ant-nodeps - 1.8.1 - - - org.tigris.antelope - antelopetasks - 3.2.10 - - - - - set-up-maven-properties - prepare-package - - run - - - true - - - - - - - - - - - - - - - package-docs-zip - package - - run - - - - - - - - - - - - - - - - - org.asciidoctor - asciidoctor-maven-plugin - - - generate-html-documentation - prepare-package - - process-asciidoc - - - html5 - ${project.build.directory}/generated-docs/reference/html - highlight.js - book - - js/highlight - github - true - ./images - font - css/ - spring.css - warn - - - true - - DEBUG - - - - - - generate-pdf-documentation - prepare-package - - process-asciidoc - - - pdf - ${project.build.directory}/generated-docs/reference/pdf - - - - - ${refdocs.build.directory} - index.adoc - - ${version-type} - ${project.version} - ${project.build.directory}/generated-snippets/ - - - - - org.asciidoctor - asciidoctorj-pdf - 1.5.0-alpha.18 - - - - - org.apache.maven.plugins - maven-resources-plugin - - - copy-asciidoc-resources - generate-resources - - copy-resources - - - ${refdocs.build.directory} - - - src/main/asciidoc - false - - - - - - - - org.codehaus.mojo - build-helper-maven-plugin - - - attach-zip - - attach-artifact - - - - - ${project.build.directory}/${project.artifactId}-${project.version}-docs.zip - zip - docs - - - - - - - - - - - diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/anchor-rewrite.properties b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/anchor-rewrite.properties new file mode 100644 index 000000000000..74fbed4f091c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/anchor-rewrite.properties @@ -0,0 +1,134 @@ +overview=overview +overview-endpoint-urls=overview.endpoint-urls +overview-timestamps=overview.timestamps +audit-events=audit-events +audit-events-retrieving=audit-events.retrieving +audit-events-retrieving-query-parameters=audit-events.retrieving.query-parameters +audit-events-retrieving-response-structure=audit-events.retrieving.response-structure +beans=beans +beans-retrieving=beans.retrieving +beans-retrieving-response-structure=beans.retrieving.response-structure +caches=caches +caches-all=caches.all +caches-all-response-structure=caches.all.response-structure +caches-named=caches.named +caches-named-query-parameters=caches.named.query-parameters +caches-named-response-structure=caches.named.response-structure +caches-evict-all=caches.evict-all +caches-evict-named=caches.evict-named +caches-evict-named-request-structure=caches.evict-named.request-structure +conditions=conditions +conditions-retrieving=conditions.retrieving +conditions-retrieving-response-structure=conditions.retrieving.response-structure +configprops=configprops +configprops-retrieving=configprops.retrieving +configprops-retrieving-response-structure=configprops.retrieving.response-structure +configprops-retrieving-by-prefix=configprops.retrieving-by-prefix +configprops-retrieving-by-prefix-response-structure=configprops.retrieving-by-prefix.response-structure +env=env +env-entire=env.entire +env-entire-response-structure=env.entire.response-structure +env-single-property=env.single-property +env-single-response-structure=env.single-property.response-structure +flyway=flyway +flyway-retrieving=flyway.retrieving +flyway-retrieving-response-structure=flyway.retrieving.response-structure +health=health +health-retrieving=health.retrieving +health-retrieving-response-structure=health.retrieving.response-structure +health-retrieving-component=health.retrieving-component +health-retrieving-component-response-structure=health.retrieving-component.response-structure +health-retrieving-component-nested=health.retrieving-component-nested +health-retrieving-component-instance-response-structure=health.retrieving-component-nested.response-structure +heapdump=heapdump +heapdump-retrieving=heapdump.retrieving +http-trace=http-trace +http-trace-retrieving=http-trace.retrieving +http-trace-retrieving-response-structure=http-trace.retrieving.response-structure +info=info +info-retrieving=info.retrieving +info-retrieving-response-structure=info.retrieving.response-structure +info-retrieving-response-structure-build=info.retrieving.response-structure.build +info-retrieving-response-structure-git=info.retrieving.response-structure.git +integrationgraph=integrationgraph +integrationgraph-retrieving=integrationgraph.retrieving +integrationgraph-retrieving-response-structure=integrationgraph.retrieving.response-structure +integrationgraph-rebuilding=integrationgraph.rebuilding +liquibase=liquibase +liquibase-retrieving=liquibase.retrieving +liquibase-retrieving-response-structure=liquibase.retrieving.response-structure +log-file=logfile +logfile-retrieving=logfile.retrieving +logfile-retrieving-part=logfile.retrieving-part +loggers=loggers +loggers-all=loggers.all +loggers-all-response-structure=loggers.all.response-structure +loggers-single=loggers.single +loggers-single-response-structure=loggers.single.response-structure +loggers-group=loggers.group +loggers-group-response-structure=loggers.group.response-structure +loggers-setting-level=loggers.setting-level +loggers-setting-level-request-structure=loggers.setting-level.request-structure +loggers-group-setting-level=loggers.group-setting-level +loggers-group-setting-level-request-structure=loggers.group-setting-level.request-structure +loggers-clearing-level=loggers.clearing-level +mappings=mappings +mappings-retrieving=mappings.retrieving +mappings-retrieving-response-structure=mappings.retrieving.response-structure +mappings-retrieving-response-structure-dispatcher-servlets=mappings.retrieving.response-structure-dispatcher-servlets +mappings-retrieving-response-structure-servlets=mappings.retrieving.response-structure-servlets +mappings-retrieving-response-structure-servlet-filters=mappings.retrieving.response-structure-servlet-filters +mappings-retrieving-response-structure-dispatcher-handlers=mappings.retrieving.response-structure-dispatcher-handlers +metrics=metrics +metrics-retrieving-names=metrics.retrieving-names +metrics-retrieving-names-response-structure=metrics.retrieving-names.response-structure +metrics-retrieving-metric=metrics.retrieving-metric +metrics-retrieving-metric-query-parameters=metrics.retrieving-metric.query-parameters +metrics-retrieving-metric-response-structure=metrics.retrieving-metric.response-structure +metrics-drilling-down=metrics.drilling-down +prometheus=prometheus +prometheus-retrieving=prometheus.retrieving +prometheus-retrieving-query-parameters=prometheus.retrieving.query-parameters +prometheus-retrieving-names=prometheus.retrieving-names +quartz=quartz +quartz-report=quartz.report +quartz-report-response-structure=quartz.report.response-structure +quartz-job-groups=quartz.job-groups +quartz-job-groups-response-structure=quartz.job-groups.response-structure +quartz-trigger-groups=quartz.trigger-groups +quartz-trigger-groups-response-structure=quartz.trigger-groups.response-structure +quartz-job-group=quartz.job-group +quartz-job-group-response-structure=quartz.job-group.response-structure +quartz-trigger-group=quartz.trigger-group +quartz-trigger-group-response-structure=quartz.trigger-group.response-structure +quartz-job=quartz.job +quartz-job-response-structure=quartz.job.response-structure +quartz-trigger=quartz.trigger +quartz-trigger-common-response-structure=quartz.trigger.common-response-structure +quartz-trigger-cron-response-structure=quartz.trigger.cron-response-structure +quartz-trigger-simple-response-structure=quartz.trigger.simple-response-structure +quartz-trigger-daily-time-interval-response-structure=quartz.trigger.daily-time-interval-response-structure +quartz-trigger-calendar-interval-response-structure=quartz.trigger.calendar-interval-response-structure +quartz-trigger-custom-response-structure=quartz.trigger.custom-response-structure +scheduled-tasks=scheduled-tasks +scheduled-tasks-retrieving=scheduled-tasks.retrieving +scheduled-tasks-retrieving-response-structure=scheduled-tasks.retrieving.response-structure +sessions=sessions +sessions-retrieving=sessions.retrieving +sessions-retrieving-query-parameters=sessions.retrieving.query-parameters +sessions-retrieving-response-structure=sessions.retrieving.response-structure +sessions-retrieving-id=sessions.retrieving-id +sessions-retrieving-id-response-structure=sessions.retrieving-id.response-structure +sessions-deleting=sessions.deleting +shutdown=shutdown +shutdown-shutting-down=shutdown.shutting-down +shutdown-shutting-down-response-structure=shutdown.shutting-down.response-structure +startup=startup +startup-retrieving=startup.retrieving +startup-retrieving-snapshot=startup.retrieving.snapshot +startup-retrieving-drain=startup.retrieving.drain +startup-retrieving-response-structure=startup.retrieving.response-structure +threaddump=threaddump +threaddump-retrieving-json=threaddump.retrieving-json +threaddump-retrieving-json-response-structure=threaddump.retrieving-json.response-structure +threaddump-retrieving-text=threaddump.retrieving-text diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/auditevents.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/auditevents.adoc new file mode 100644 index 000000000000..2cba373c1d6f --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/auditevents.adoc @@ -0,0 +1,36 @@ +[[audit-events]] += Audit Events (`auditevents`) +The `auditevents` endpoint provides information about the application's audit events. + + + +[[audit-events.retrieving]] +== Retrieving Audit Events +To retrieve the audit events, make a `GET` request to `/actuator/auditevents`, as shown in the following curl-based example: + +include::{snippets}/auditevents/filtered/curl-request.adoc[] + +The preceding example retrieves `logout` events for the principal, `alice`, that occurred after 09:37 on 7 November 2017 in the UTC timezone. +The resulting response is similar to the following: + +include::{snippets}/auditevents/filtered/http-response.adoc[] + + + +[[audit-events.retrieving.query-parameters]] +=== Query Parameters +The endpoint uses query parameters to limit the events that it returns. +The following table shows the supported query parameters: + +[cols="2,4"] +include::{snippets}/auditevents/filtered/request-parameters.adoc[] + + + +[[audit-events.retrieving.response-structure]] +=== Response Structure +The response contains details of all of the audit events that matched the query. +The following table describes the structure of the response: + +[cols="2,1,3"] +include::{snippets}/auditevents/all/response-fields.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/beans.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/beans.adoc new file mode 100644 index 000000000000..d48cfff22ab7 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/beans.adoc @@ -0,0 +1,25 @@ +[[beans]] += Beans (`beans`) +The `beans` endpoint provides information about the application's beans. + + + +[[beans.retrieving]] +== Retrieving the Beans +To retrieve the beans, make a `GET` request to `/actuator/beans`, as shown in the following curl-based example: + +include::{snippets}/beans/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}/beans/http-response.adoc[] + + + +[[beans.retrieving.response-structure]] +=== Response Structure +The response contains details of the application's beans. +The following table describes the structure of the response: + +[cols="2,1,3"] +include::{snippets}/beans/response-fields.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/caches.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/caches.adoc new file mode 100644 index 000000000000..f818b01f35b4 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/caches.adoc @@ -0,0 +1,88 @@ +[[caches]] += Caches (`caches`) +The `caches` endpoint provides access to the application's caches. + + + +[[caches.all]] +== Retrieving All Caches +To retrieve the application's caches, make a `GET` request to `/actuator/caches`, as shown in the following curl-based example: + +include::{snippets}/caches/all/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}/caches/all/http-response.adoc[] + + + +[[caches.all.response-structure]] +=== Response Structure +The response contains details of the application's caches. +The following table describes the structure of the response: + +[cols="3,1,3"] +include::{snippets}/caches/all/response-fields.adoc[] + + + +[[caches.named]] +== Retrieving Caches by Name +To retrieve a cache by name, make a `GET` request to `/actuator/caches/\{name}`, as shown in the following curl-based example: + +include::{snippets}/caches/named/curl-request.adoc[] + +The preceding example retrieves information about the cache named `cities`. +The resulting response is similar to the following: + +include::{snippets}/caches/named/http-response.adoc[] + + + +[[caches.named.query-parameters]] +=== Query Parameters +If the requested name is specific enough to identify a single cache, no extra parameter is required. +Otherwise, the `cacheManager` must be specified. +The following table shows the supported query parameters: + +[cols="2,4"] +include::{snippets}/caches/named/request-parameters.adoc[] + + + +[[caches.named.response-structure]] +=== Response Structure +The response contains details of the requested cache. +The following table describes the structure of the response: + +[cols="3,1,3"] +include::{snippets}/caches/named/response-fields.adoc[] + + + +[[caches.evict-all]] +== Evict All Caches +To clear all available caches, make a `DELETE` request to `/actuator/caches` as shown in the following curl-based example: + +include::{snippets}/caches/evict-all/curl-request.adoc[] + + + +[[caches.evict-named]] +== Evict a Cache by Name +To evict a particular cache, make a `DELETE` request to `/actuator/caches/\{name}` as shown in the following curl-based example: + +include::{snippets}/caches/evict-named/curl-request.adoc[] + +NOTE: As there are two caches named `countries`, the `cacheManager` has to be provided to specify which `Cache` should be cleared. + + + +[[caches.evict-named.request-structure]] +=== Request Structure +If the requested name is specific enough to identify a single cache, no extra parameter is required. +Otherwise, the `cacheManager` must be specified. +The following table shows the supported query parameters: + +[cols="2,4"] +include::{snippets}/caches/evict-named/request-parameters.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/conditions.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/conditions.adoc new file mode 100644 index 000000000000..96d3daf2b640 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/conditions.adoc @@ -0,0 +1,25 @@ +[[conditions]] += Conditions Evaluation Report (`conditions`) +The `conditions` endpoint provides information about the evaluation of conditions on configuration and auto-configuration classes. + + + +[[conditions.retrieving]] +== Retrieving the Report +To retrieve the report, make a `GET` request to `/actuator/conditions`, as shown in the following curl-based example: + +include::{snippets}/conditions/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}/conditions/http-response.adoc[] + + + +[[conditions.retrieving.response-structure]] +=== Response Structure +The response contains details of the application's condition evaluation. +The following table describes the structure of the response: + +[cols="3,1,3"] +include::{snippets}/conditions/response-fields.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/configprops.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/configprops.adoc new file mode 100644 index 000000000000..b2775d87e71c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/configprops.adoc @@ -0,0 +1,49 @@ +[[configprops]] += Configuration Properties (`configprops`) +The `configprops` endpoint provides information about the application's `@ConfigurationProperties` beans. + + + +[[configprops.retrieving]] +== Retrieving All @ConfigurationProperties Beans +To retrieve all of the `@ConfigurationProperties` beans, make a `GET` request to `/actuator/configprops`, as shown in the following curl-based example: + +include::{snippets}/configprops/all/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}/configprops/all/http-response.adoc[] + + + +[[configprops.retrieving.response-structure]] +=== Response Structure +The response contains details of the application's `@ConfigurationProperties` beans. +The following table describes the structure of the response: + +[cols="2,1,3"] +include::{snippets}/configprops/all/response-fields.adoc[] + + + +[[configprops.retrieving-by-prefix]] +== Retrieving @ConfigurationProperties Beans By Prefix +To retrieve the `@ConfigurationProperties` beans mapped under a certain prefix, make a `GET` request to `/actuator/configprops/\{prefix}`, as shown in the following curl-based example: + +include::{snippets}/configprops/prefixed/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}/configprops/prefixed/http-response.adoc[] + +NOTE: The `\{prefix}` does not need to be exact, a more general prefix will return all beans mapped under that prefix stem. + + + +[[configprops.retrieving-by-prefix.response-structure]] +=== Response Structure +The response contains details of the application's `@ConfigurationProperties` beans. +The following table describes the structure of the response: + +[cols="2,1,3"] +include::{snippets}/configprops/prefixed/response-fields.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/env.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/env.adoc new file mode 100644 index 000000000000..ac5f2a7568e2 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/env.adoc @@ -0,0 +1,48 @@ +[[env]] += Environment (`env`) +The `env` endpoint provides information about the application's `Environment`. + + + +[[env.entire]] +== Retrieving the Entire Environment +To retrieve the entire environment, make a `GET` request to `/actuator/env`, as shown in the following curl-based example: + +include::{snippets}/env/all/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}/env/all/http-response.adoc[] + + + +[[env.entire.response-structure]] +=== Response Structure +The response contains details of the application's `Environment`. +The following table describes the structure of the response: + +[cols="3,1,3"] +include::{snippets}/env/all/response-fields.adoc[] + + + +[[env.single-property]] +== Retrieving a Single Property +To retrieve a single property, make a `GET` request to `/actuator/env/{property.name}`, as shown in the following curl-based example: + +include::{snippets}/env/single/curl-request.adoc[] + +The preceding example retrieves information about the property named `com.example.cache.max-size`. +The resulting response is similar to the following: + +include::{snippets}/env/single/http-response.adoc[] + + + +[[env.single-property.response-structure]] +=== Response Structure +The response contains details of the requested property. +The following table describes the structure of the response: + +[cols="3,1,3"] +include::{snippets}/env/single/response-fields.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/flyway.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/flyway.adoc new file mode 100644 index 000000000000..5d69b9d7594c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/flyway.adoc @@ -0,0 +1,25 @@ +[[flyway]] += Flyway (`flyway`) +The `flyway` endpoint provides information about database migrations performed by Flyway. + + + +[[flyway.retrieving]] +== Retrieving the Migrations +To retrieve the migrations, make a `GET` request to `/actuator/flyway`, as shown in the following curl-based example: + +include::{snippets}/flyway/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}/flyway/http-response.adoc[] + + + +[[flyway.retrieving.response-structure]] +=== Response Structure +The response contains details of the application's Flyway migrations. +The following table describes the structure of the response: + +[cols="2,1,3"] +include::{snippets}/flyway/response-fields.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/health.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/health.adoc new file mode 100644 index 000000000000..bf65b4686326 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/health.adoc @@ -0,0 +1,75 @@ +[[health]] += Health (`health`) +The `health` endpoint provides detailed information about the health of the application. + + + +[[health.retrieving]] +== Retrieving the Health of the Application +To retrieve the health of the application, make a `GET` request to `/actuator/health`, as shown in the following curl-based example: + +include::{snippets}/health/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}/health/http-response.adoc[] + + + +[[health.retrieving.response-structure]] +=== Response Structure +The response contains details of the health of the application. +The following table describes the structure of the response: + +[cols="2,1,3"] +include::{snippets}/health/response-fields.adoc[] + +NOTE: The response fields above are for the V3 API. +If you need to return V2 JSON you should use an accept header or `application/vnd.spring-boot.actuator.v2+json` + + + +[[health.retrieving-component]] +== Retrieving the Health of a Component +To retrieve the health of a particular component of the application's health, make a `GET` request to `/actuator/health/\{component}`, as shown in the following curl-based example: + +include::{snippets}/health/component/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}/health/component/http-response.adoc[] + + + +[[health.retrieving-component.response-structure]] +=== Response Structure +The response contains details of the health of a particular component of the application's health. +The following table describes the structure of the response: + +[cols="2,1,3"] +include::{snippets}/health/component/response-fields.adoc[] + + + +[[health.retrieving-component-nested]] +== Retrieving the Health of a Nested Component +If a particular component contains other nested components (as the `broker` indicator in the example above), the health of such a nested component can be retrieved by issuing a `GET` request to `/actuator/health/\{component}/\{subcomponent}`, as shown in the following curl-based example: + +include::{snippets}/health/instance/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}/health/instance/http-response.adoc[] + +Components of an application's health may be nested arbitrarily deep depending on the application's health indicators and how they have been grouped. +The health endpoint supports any number of `/\{component}` identifiers in the URL to allow the health of a component at any depth to be retrieved. + + + +[[health.retrieving-component-nested.response-structure]] +=== Response Structure +The response contains details of the health of an instance of a particular component of the application. +The following table describes the structure of the response: + +[cols="2,1,3"] +include::{snippets}/health/instance/response-fields.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/heapdump.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/heapdump.adoc similarity index 89% rename from spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/heapdump.adoc rename to spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/heapdump.adoc index ebbba69207db..7695819ef1d5 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/heapdump.adoc +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/heapdump.adoc @@ -1,18 +1,16 @@ [[heapdump]] = Heap Dump (`heapdump`) - The `heapdump` endpoint provides a heap dump from the application's JVM. -[[heapdump-retrieving]] +[[heapdump.retrieving]] == Retrieving the Heap Dump - To retrieve the heap dump, make a `GET` request to `/actuator/heapdump`. The response is binary data in https://docs.oracle.com/javase/8/docs/technotes/samples/hprof.html[HPROF] format and can be large. Typically, you should save the response to disk for subsequent analysis. When using curl, this can be achieved by using the `-O` option, as shown in the following example: -include::{snippets}heapdump/curl-request.adoc[] +include::{snippets}/heapdump/curl-request.adoc[] The preceding example results in a file named `heapdump` being written to the current working directory. diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/httptrace.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/httptrace.adoc new file mode 100644 index 000000000000..fc566a0121c7 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/httptrace.adoc @@ -0,0 +1,25 @@ +[[http-trace]] += HTTP Trace (`httptrace`) +The `httptrace` endpoint provides information about HTTP request-response exchanges. + + + +[[http-trace.retrieving]] +== Retrieving the Traces +To retrieve the traces, make a `GET` request to `/actuator/httptrace`, as shown in the following curl-based example: + +include::{snippets}/httptrace/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}/httptrace/http-response.adoc[] + + + +[[http-trace.retrieving.response-structure]] +=== Response Structure +The response contains details of the traced HTTP request-response exchanges. +The following table describes the structure of the response: + +[cols="2,1,3"] +include::{snippets}/httptrace/response-fields.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/info.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/info.adoc new file mode 100644 index 000000000000..a414cfed545c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/info.adoc @@ -0,0 +1,41 @@ +[[info]] += Info (`info`) +The `info` endpoint provides general information about the application. + + + +[[info.retrieving]] +== Retrieving the Info +To retrieve the information about the application, make a `GET` request to `/actuator/info`, as shown in the following curl-based example: + +include::{snippets}/info/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}/info/http-response.adoc[] + + + +[[info.retrieving.response-structure]] +=== Response Structure +The response contains general information about the application. +Each section of the response is contributed by an `InfoContributor`. +Spring Boot provides `build` and `git` contributions. + + + +[[info.retrieving.response-structure.build]] +==== Build Response Structure +The following table describe the structure of the `build` section of the response: + +[cols="2,1,3"] +include::{snippets}/info/response-fields-beneath-build.adoc[] + + + +[[info.retrieving.response-structure.git]] +==== Git Response Structure +The following table describes the structure of the `git` section of the response: + +[cols="2,1,3"] +include::{snippets}/info/response-fields-beneath-git.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/integrationgraph.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/integrationgraph.adoc new file mode 100644 index 000000000000..5709ca935833 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/integrationgraph.adoc @@ -0,0 +1,34 @@ +[[integrationgraph]] += Spring Integration graph (`integrationgraph`) +The `integrationgraph` endpoint exposes a graph containing all Spring Integration components. + + + +[[integrationgraph.retrieving]] +== Retrieving the Spring Integration Graph +To retrieve the information about the application, make a `GET` request to `/actuator/integrationgraph`, as shown in the following curl-based example: + +include::{snippets}/integrationgraph/graph/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}/integrationgraph/graph/http-response.adoc[] + + + +[[integrationgraph.retrieving.response-structure]] +=== Response Structure +The response contains all Spring Integration components used within the application, as well as the links between them. +More information about the structure can be found in the {spring-integration-docs}index-single.html#integration-graph[reference documentation]. + + + +[[integrationgraph.rebuilding]] +== Rebuilding the Spring Integration Graph +To rebuild the exposed graph, make a `POST` request to `/actuator/integrationgraph`, as shown in the following curl-based example: + +include::{snippets}/integrationgraph/rebuild/curl-request.adoc[] + +This will result in a `204 - No Content` response: + +include::{snippets}/integrationgraph/rebuild/http-response.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/liquibase.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/liquibase.adoc new file mode 100644 index 000000000000..7bfd46565da2 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/liquibase.adoc @@ -0,0 +1,25 @@ +[[liquibase]] += Liquibase (`liquibase`) +The `liquibase` endpoint provides information about database change sets applied by Liquibase. + + + +[[liquibase.retrieving]] +== Retrieving the Changes +To retrieve the changes, make a `GET` request to `/actuator/liquibase`, as shown in the following curl-based example: + +include::{snippets}/liquibase/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}/liquibase/http-response.adoc[] + + + +[[liquibase.retrieving.response-structure]] +=== Response Structure +The response contains details of the application's Liquibase change sets. +The following table describes the structure of the response: + +[cols="2,1,3"] +include::{snippets}/liquibase/response-fields.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/logfile.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/logfile.adoc new file mode 100644 index 000000000000..0fd16d6abd13 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/logfile.adoc @@ -0,0 +1,30 @@ +[[logfile]] += Log File (`logfile`) +The `logfile` endpoint provides access to the contents of the application's log file. + + + +[[logfile.retrieving]] +== Retrieving the Log File +To retrieve the log file, make a `GET` request to `/actuator/logfile`, as shown in the following curl-based example: + +include::{snippets}/logfile/entire/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}/logfile/entire/http-response.adoc[] + + + +[[logfile.retrieving-part]] +== Retrieving Part of the Log File +NOTE: Retrieving part of the log file is not supported when using Jersey. + +To retrieve part of the log file, make a `GET` request to `/actuator/logfile` by using the `Range` header, as shown in the following curl-based example: + +include::{snippets}/logfile/range/curl-request.adoc[] + +The preceding example retrieves the first 1024 bytes of the log file. +The resulting response is similar to the following: + +include::{snippets}/logfile/range/http-response.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/loggers.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/loggers.adoc new file mode 100644 index 000000000000..36cf215dac18 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/loggers.adoc @@ -0,0 +1,122 @@ +[[loggers]] += Loggers (`loggers`) +The `loggers` endpoint provides access to the application's loggers and the configuration of their levels. + + + +[[loggers.all]] +== Retrieving All Loggers +To retrieve the application's loggers, make a `GET` request to `/actuator/loggers`, as shown in the following curl-based example: + +include::{snippets}/loggers/all/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}/loggers/all/http-response.adoc[] + + + +[[loggers.all.response-structure]] +=== Response Structure +The response contains details of the application's loggers. +The following table describes the structure of the response: + +[cols="3,1,3"] +include::{snippets}/loggers/all/response-fields.adoc[] + + + +[[loggers.single]] +== Retrieving a Single Logger +To retrieve a single logger, make a `GET` request to `/actuator/loggers/{logger.name}`, as shown in the following curl-based example: + +include::{snippets}/loggers/single/curl-request.adoc[] + +The preceding example retrieves information about the logger named `com.example`. +The resulting response is similar to the following: + +include::{snippets}/loggers/single/http-response.adoc[] + + + +[[loggers.single.response-structure]] +=== Response Structure +The response contains details of the requested logger. +The following table describes the structure of the response: + +[cols="3,1,3"] +include::{snippets}/loggers/single/response-fields.adoc[] + + + +[[loggers.group]] +== Retrieving a Single Group +To retrieve a single group, make a `GET` request to `/actuator/loggers/{group.name}`, +as shown in the following curl-based example: + +include::{snippets}/loggers/group/curl-request.adoc[] + +The preceding example retrieves information about the logger group named `test`. +The resulting response is similar to the following: + +include::{snippets}/loggers/group/http-response.adoc[] + + + +[[loggers.group.response-structure]] +=== Response Structure +The response contains details of the requested group. +The following table describes the structure of the response: + +[cols="3,1,3"] +include::{snippets}/loggers/group/response-fields.adoc[] + + + +[[loggers.setting-level]] +== Setting a Log Level +To set the level of a logger, make a `POST` request to `/actuator/loggers/{logger.name}` with a JSON body that specifies the configured level for the logger, as shown in the following curl-based example: + +include::{snippets}/loggers/set/curl-request.adoc[] + +The preceding example sets the `configuredLevel` of the `com.example` logger to `DEBUG`. + + + +[[loggers.setting-level.request-structure]] +=== Request Structure +The request specifies the desired level of the logger. +The following table describes the structure of the request: + +[cols="3,1,3"] +include::{snippets}/loggers/set/request-fields.adoc[] + + + +[[loggers.group-setting-level]] +== Setting a Log Level for a Group +To set the level of a logger, make a `POST` request to `/actuator/loggers/{group.name}` with a JSON body that specifies the configured level for the logger group, as shown in the following curl-based example: + +include::{snippets}/loggers/setGroup/curl-request.adoc[] + +The preceding example sets the `configuredLevel` of the `test` logger group to `DEBUG`. + + + +[[loggers.group-setting-level.request-structure]] +=== Request Structure +The request specifies the desired level of the logger group. +The following table describes the structure of the request: + +[cols="3,1,3"] +include::{snippets}/loggers/set/request-fields.adoc[] + + + +[[loggers.clearing-level]] +== Clearing a Log Level +To clear the level of a logger, make a `POST` request to `/actuator/loggers/{logger.name}` with a JSON body containing an empty object, as shown in the following curl-based example: + +include::{snippets}/loggers/clear/curl-request.adoc[] + +The preceding example clears the configured level of the `com.example` logger. diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/mappings.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/mappings.adoc new file mode 100644 index 000000000000..350c5d16f547 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/mappings.adoc @@ -0,0 +1,68 @@ +[[mappings]] += Mappings (`mappings`) +The `mappings` endpoint provides information about the application's request mappings. + + + +[[mappings.retrieving]] +== Retrieving the Mappings +To retrieve the mappings, make a `GET` request to `/actuator/mappings`, as shown in the following curl-based example: + +include::{snippets}/mappings/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}/mappings/http-response.adoc[] + + + +[[mappings.retrieving.response-structure]] +=== Response Structure +The response contains details of the application's mappings. +The items found in the response depend on the type of web application (reactive or Servlet-based). +The following table describes the structure of the common elements of the response: + +[cols="2,1,3"] +include::{snippets}/mappings/response-fields.adoc[] + +The entries that may be found in `contexts.*.mappings` are described in the following sections. + + + +[[mappings.retrieving.response-structure-dispatcher-servlets]] +=== Dispatcher Servlets Response Structure +When using Spring MVC, the response contains details of any `DispatcherServlet` request mappings beneath `contexts.*.mappings.dispatcherServlets`. +The following table describes the structure of this section of the response: + +[cols="4,1,2"] +include::{snippets}/mappings/response-fields-dispatcher-servlets.adoc[] + + + +[[mappings.retrieving.response-structure-servlets]] +=== Servlets Response Structure +When using the Servlet stack, the response contains details of any `Servlet` mappings beneath `contexts.*.mappings.servlets`. +The following table describes the structure of this section of the response: + +[cols="2,1,3"] +include::{snippets}/mappings/response-fields-servlets.adoc[] + + + +[[mappings.retrieving.response-structure-servlet-filters]] +=== Servlet Filters Response Structure +When using the Servlet stack, the response contains details of any `Filter` mappings beneath `contexts.*.mappings.servletFilters`. +The following table describes the structure of this section of the response: + +[cols="2,1,3"] +include::{snippets}/mappings/response-fields-servlet-filters.adoc[] + + + +[[mappings.retrieving.response-structure-dispatcher-handlers]] +=== Dispatcher Handlers Response Structure +When using Spring WebFlux, the response contains details of any `DispatcherHandler` request mappings beneath `contexts.*.mappings.dispatcherHandlers`. +The following table describes the structure of this section of the response: + +[cols="4,1,2"] +include::{snippets}/mappings/response-fields-dispatcher-handlers.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/metrics.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/metrics.adoc new file mode 100644 index 000000000000..7f16cdd35a2c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/metrics.adoc @@ -0,0 +1,70 @@ +[[metrics]] += Metrics (`metrics`) +The `metrics` endpoint provides access to application metrics. + + + +[[metrics.retrieving-names]] +== Retrieving Metric Names +To retrieve the names of the available metrics, make a `GET` request to `/actuator/metrics`, as shown in the following curl-based example: + +include::{snippets}/metrics/names/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}/metrics/names/http-response.adoc[] + + + +[[metrics.retrieving-names.response-structure]] +=== Response Structure +The response contains details of the metric names. +The following table describes the structure of the response: + +[cols="3,1,2"] +include::{snippets}/metrics/names/response-fields.adoc[] + + + +[[metrics.retrieving-metric]] +== Retrieving a Metric +To retrieve a metric, make a `GET` request to `/actuator/metrics/{metric.name}`, as shown in the following curl-based example: + +include::{snippets}/metrics/metric/curl-request.adoc[] + +The preceding example retrieves information about the metric named `jvm.memory.max`. +The resulting response is similar to the following: + +include::{snippets}/metrics/metric/http-response.adoc[] + + + +[[metrics.retrieving-metric.query-parameters]] +=== Query Parameters +The endpoint uses query parameters to <> into a metric by using its tags. +The following table shows the single supported query parameter: + +[cols="2,4"] +include::{snippets}/metrics/metric-with-tags/request-parameters.adoc[] + + + +[[metrics.retrieving-metric.response-structure]] +=== Response structure +The response contains details of the metric. +The following table describes the structure of the response: + +include::{snippets}/metrics/metric/response-fields.adoc[] + + + +[[metrics.drilling-down]] +== Drilling Down +To drill down into a metric, make a `GET` request to `/actuator/metrics/{metric.name}` using the `tag` query parameter, as shown in the following curl-based example: + +include::{snippets}/metrics/metric-with-tags/curl-request.adoc[] + +The preceding example retrieves the `jvm.memory.max` metric, where the `area` tag has a value of `nonheap` and the `id` attribute has a value of `Compressed Class Space`. +The resulting response is similar to the following: + +include::{snippets}/metrics/metric-with-tags/http-response.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/prometheus.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/prometheus.adoc new file mode 100644 index 000000000000..a3fe04b63b6d --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/prometheus.adoc @@ -0,0 +1,47 @@ +[[prometheus]] += Prometheus (`prometheus`) +The `prometheus` endpoint provides Spring Boot application's metrics in the format required for scraping by a Prometheus server. + + + +[[prometheus.retrieving]] +== Retrieving All Metrics +To retrieve all metrics, make a `GET` request to `/actuator/prometheus`, as shown in the following curl-based example: + +include::{snippets}/prometheus/all/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}/prometheus/all/http-response.adoc[] + +The default response content type is `text/plain;version=0.0.4`. +The endpoint can also produce `application/openmetrics-text;version=1.0.0` when called with an appropriate `Accept` header, as shown in the following curl-based example: + +include::{snippets}/prometheus/openmetrics/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}/prometheus/openmetrics/http-response.adoc[] + + + +[[prometheus.retrieving.query-parameters]] +=== Query Parameters +The endpoint uses query parameters to limit the samples that it returns. +The following table shows the supported query parameters: + +[cols="2,4"] +include::{snippets}/prometheus/names/request-parameters.adoc[] + + + +[[prometheus.retrieving-names]] +== Retrieving Filtered Metrics +To retrieve metrics matching specific names, make a `GET` request to `/actuator/prometheus` with the `includedNames` query parameter, as shown in the following curl-based example: + +include::{snippets}/prometheus/names/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}/prometheus/names/http-response.adoc[] + diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/quartz.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/quartz.adoc new file mode 100644 index 000000000000..24b72d458843 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/quartz.adoc @@ -0,0 +1,257 @@ +[[quartz]] += Quartz (`quartz`) +The `quartz` endpoint provides information about jobs and triggers that are managed by the Quartz Scheduler. + + + +[[quartz.report]] +== Retrieving Registered Groups +Jobs and triggers are managed in groups. +To retrieve the list of registered job and trigger groups, make a `GET` request to `/actuator/quartz`, as shown in the following curl-based example: + +include::{snippets}/quartz/report/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}/quartz/report/http-response.adoc[] + + + +[[quartz.report.response-structure]] +=== Response Structure +The response contains the groups names for registered jobs and triggers. +The following table describes the structure of the response: + +[cols="3,1,3"] +include::{snippets}/quartz/report/response-fields.adoc[] + + + +[[quartz.job-groups]] +== Retrieving Registered Job Names +To retrieve the list of registered job names, make a `GET` request to `/actuator/quartz/jobs`, as shown in the following curl-based example: + +include::{snippets}/quartz/jobs/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}/quartz/jobs/http-response.adoc[] + + + +[[quartz.job-groups.response-structure]] +=== Response Structure +The response contains the registered job names for each group. +The following table describes the structure of the response: + +[cols="3,1,3"] +include::{snippets}/quartz/jobs/response-fields.adoc[] + + + +[[quartz.trigger-groups]] +== Retrieving Registered Trigger Names +To retrieve the list of registered trigger names, make a `GET` request to `/actuator/quartz/triggers`, as shown in the following curl-based example: + +include::{snippets}/quartz/triggers/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}/quartz/triggers/http-response.adoc[] + + + +[[quartz.trigger-groups.response-structure]] +=== Response Structure +The response contains the registered trigger names for each group. +The following table describes the structure of the response: + +[cols="3,1,3"] +include::{snippets}/quartz/triggers/response-fields.adoc[] + + + +[[quartz.job-group]] +== Retrieving Overview of a Job Group +To retrieve an overview of the jobs in a particular group, make a `GET` request to `/actuator/quartz/jobs/\{groupName}`, as shown in the following curl-based example: + +include::{snippets}/quartz/job-group/curl-request.adoc[] + +The preceding example retrieves the summary for jobs in the `samples` group. +The resulting response is similar to the following: + +include::{snippets}/quartz/job-group/http-response.adoc[] + + + +[[quartz.job-group.response-structure]] +=== Response Structure +The response contains an overview of jobs in a particular group. +The following table describes the structure of the response: + +[cols="3,1,3"] +include::{snippets}/quartz/job-group/response-fields.adoc[] + + + +[[quartz.trigger-group]] +== Retrieving Overview of a Trigger Group + +To retrieve an overview of the triggers in a particular group, make a `GET` request to `/actuator/quartz/triggers/\{groupName}`, as shown in the following curl-based example: + +include::{snippets}/quartz/trigger-group/curl-request.adoc[] + +The preceding example retrieves the summary for triggers in the `tests` group. +The resulting response is similar to the following: + +include::{snippets}/quartz/trigger-group/http-response.adoc[] + + + +[[quartz.trigger-group.response-structure]] +=== Response Structure +The response contains an overview of triggers in a particular group. +Trigger implementation specific details are available. +The following table describes the structure of the response: + +[cols="3,1,3"] +include::{snippets}/quartz/trigger-group/response-fields.adoc[] + + + +[[quartz.job]] +== Retrieving Details of a Job +To retrieve the details about a particular job, make a `GET` request to `/actuator/quartz/jobs/\{groupName}/\{jobName}`, as shown in the following curl-based example: + +include::{snippets}/quartz/job-details/curl-request.adoc[] + +The preceding example retrieves the details of the job identified by the `samples` group and `jobOne` name. +The resulting response is similar to the following: + +include::{snippets}/quartz/job-details/http-response.adoc[] + +If a key in the data map is identified as sensitive, its value is sanitized. + + + +[[quartz.job.response-structure]] +=== Response Structure +The response contains the full details of a job including a summary of the triggers associated with it, if any. +The triggers are sorted by next fire time and priority. +The following table describes the structure of the response: + +[cols="2,1,3"] +include::{snippets}/quartz/job-details/response-fields.adoc[] + + + +[[quartz.trigger]] +== Retrieving Details of a Trigger +To retrieve the details about a particular trigger, make a `GET` request to `/actuator/quartz/triggers/\{groupName}/\{triggerName}`, as shown in the following curl-based example: + +include::{snippets}/quartz/trigger-details-cron/curl-request.adoc[] + +The preceding example retrieves the details of trigger identified by the `samples` group and `example` name. + + + +[[quartz.trigger.common-response-structure]] +=== Common Response Structure +The response has a common structure and an additional object that is specific to the trigger's type. +There are five supported types: + +* `cron` for `CronTrigger` +* `simple` for `SimpleTrigger` +* `dailyTimeInterval` for `DailyTimeIntervalTrigger` +* `calendarInterval` for `CalendarIntervalTrigger` +* `custom` for any other trigger implementations + +The following table describes the structure of the common elements of the response: + +[cols="2,1,3"] +include::{snippets}/quartz/trigger-details-common/response-fields.adoc[] + + + +[[quartz.trigger.cron-response-structure]] +=== Cron Trigger Response Structure +A cron trigger defines the cron expression that is used to determine when it has to fire. +The resulting response for such a trigger implementation is similar to the following: + +include::{snippets}/quartz/trigger-details-cron/http-response.adoc[] + + +Much of the response is common to all trigger types. +The structure of the common elements of the response was <>. +The following table describes the structure of the parts of the response that are specific to cron triggers: + +[cols="2,1,3"] +include::{snippets}/quartz/trigger-details-cron/response-fields.adoc[] + + + +[[quartz.trigger.simple-response-structure]] +=== Simple Trigger Response Structure +A simple trigger is used to fire a Job at a given moment in time, and optionally repeated at a specified interval. +The resulting response for such a trigger implementation is similar to the following: + +include::{snippets}/quartz/trigger-details-simple/http-response.adoc[] + + +Much of the response is common to all trigger types. +The structure of the common elements of the response was <>. +The following table describes the structure of the parts of the response that are specific to simple triggers: + +[cols="2,1,3"] +include::{snippets}/quartz/trigger-details-simple/response-fields.adoc[] + + + +[[quartz.trigger.daily-time-interval-response-structure]] +=== Daily Time Interval Trigger Response Structure +A daily time interval trigger is used to fire a Job based upon daily repeating time intervals. +The resulting response for such a trigger implementation is similar to the following: + +include::{snippets}/quartz/trigger-details-daily-time-interval/http-response.adoc[] + + +Much of the response is common to all trigger types. +The structure of the common elements of the response was <>. +The following table describes the structure of the parts of the response that are specific to daily time interval triggers: + +[cols="2,1,3"] +include::{snippets}/quartz/trigger-details-daily-time-interval/response-fields.adoc[] + + + +[[quartz.trigger.calendar-interval-response-structure]] +=== Calendar Interval Trigger Response Structure +A calendar interval trigger is used to fire a Job based upon repeating calendar time intervals. +The resulting response for such a trigger implementation is similar to the following: + +include::{snippets}/quartz/trigger-details-calendar-interval/http-response.adoc[] + + +Much of the response is common to all trigger types. +The structure of the common elements of the response was <>. +The following table describes the structure of the parts of the response that are specific to calendar interval triggers: + +[cols="2,1,3"] +include::{snippets}/quartz/trigger-details-calendar-interval/response-fields.adoc[] + + + +[[quartz.trigger.custom-response-structure]] +=== Custom Trigger Response Structure +A custom trigger is any other implementation. +The resulting response for such a trigger implementation is similar to the following: + +include::{snippets}/quartz/trigger-details-custom/http-response.adoc[] + + +Much of the response is common to all trigger types. +The structure of the common elements of the response was <>. +The following table describes the structure of the parts of the response that are specific to custom triggers: + +[cols="2,1,3"] +include::{snippets}/quartz/trigger-details-custom/response-fields.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/scheduledtasks.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/scheduledtasks.adoc new file mode 100644 index 000000000000..bde0b5397f59 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/scheduledtasks.adoc @@ -0,0 +1,25 @@ +[[scheduled-tasks]] += Scheduled Tasks (`scheduledtasks`) +The `scheduledtasks` endpoint provides information about the application's scheduled tasks. + + + +[[scheduled-tasks.retrieving]] +== Retrieving the Scheduled Tasks +To retrieve the scheduled tasks, make a `GET` request to `/actuator/scheduledtasks`, as shown in the following curl-based example: + +include::{snippets}/scheduled-tasks/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}/scheduled-tasks/http-response.adoc[] + + + +[[scheduled-tasks.retrieving.response-structure]] +=== Response Structure +The response contains details of the application's scheduled tasks. +The following table describes the structure of the response: + +[cols="2,1,3"] +include::{snippets}/scheduled-tasks/response-fields.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/sessions.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/sessions.adoc new file mode 100644 index 000000000000..62b1ec91934f --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/sessions.adoc @@ -0,0 +1,69 @@ +[[sessions]] += Sessions (`sessions`) +The `sessions` endpoint provides information about the application's HTTP sessions that are managed by Spring Session. + + + +[[sessions.retrieving]] +== Retrieving Sessions +To retrieve the sessions, make a `GET` request to `/actuator/sessions`, as shown in the following curl-based example: + +include::{snippets}/sessions/username/curl-request.adoc[] + +The preceding examples retrieves all of the sessions for the user whose username is `alice`. +The resulting response is similar to the following: + +include::{snippets}/sessions/username/http-response.adoc[] + + + +[[sessions.retrieving.query-parameters]] +=== Query Parameters +The endpoint uses query parameters to limit the sessions that it returns. +The following table shows the single required query parameter: + +[cols="2,4"] +include::{snippets}/sessions/username/request-parameters.adoc[] + + + +[[sessions.retrieving.response-structure]] +=== Response Structure +The response contains details of the matching sessions. +The following table describes the structure of the response: + +[cols="3,1,3"] +include::{snippets}/sessions/username/response-fields.adoc[] + + + +[[sessions.retrieving-id]] +== Retrieving a Single Session +To retrieve a single session, make a `GET` request to `/actuator/sessions/\{id}`, as shown in the following curl-based example: + +include::{snippets}/sessions/id/curl-request.adoc[] + +The preceding example retrieves the session with the `id` of `4db5efcc-99cb-4d05-a52c-b49acfbb7ea9`. +The resulting response is similar to the following: + +include::{snippets}/sessions/id/http-response.adoc[] + + + +[[sessions.retrieving-id.response-structure]] +=== Response Structure +The response contains details of the requested session. +The following table describes the structure of the response: + +[cols="3,1,3"] +include::{snippets}/sessions/id/response-fields.adoc[] + + + +[[sessions.deleting]] +== Deleting a Session +To delete a session, make a `DELETE` request to `/actuator/sessions/\{id}`, as shown in the following curl-based example: + +include::{snippets}/sessions/delete/curl-request.adoc[] + +The preceding example deletes the session with the `id` of `4db5efcc-99cb-4d05-a52c-b49acfbb7ea9`. diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/shutdown.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/shutdown.adoc new file mode 100644 index 000000000000..7b498b43f59e --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/shutdown.adoc @@ -0,0 +1,25 @@ +[[shutdown]] += Shutdown (`shutdown`) +The `shutdown` endpoint is used to shut down the application. + + + +[[shutdown.shutting-down]] +== Shutting Down the Application +To shut down the application, make a `POST` request to `/actuator/shutdown`, as shown in the following curl-based example: + +include::{snippets}/shutdown/curl-request.adoc[] + +A response similar to the following is produced: + +include::{snippets}/shutdown/http-response.adoc[] + + + +[[shutdown.shutting-down.response-structure]] +=== Response Structure +The response contains details of the result of the shutdown request. +The following table describes the structure of the response: + +[cols="3,1,3"] +include::{snippets}/shutdown/response-fields.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/startup.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/startup.adoc new file mode 100644 index 000000000000..8675c75a28fc --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/startup.adoc @@ -0,0 +1,43 @@ +[[startup]] += Application Startup (`startup`) +The `startup` endpoint provides information about the application's startup sequence. + + + +[[startup.retrieving]] +== Retrieving the Application Startup Steps +The application startup steps can either be retrieved as a snapshot (`GET`) or drained from the buffer (`POST`). + + + +[[startup.retrieving.snapshot]] +=== Retrieving a snapshot of the Application Startup Steps +To retrieve the steps recorded so far during the application startup phase, make a `GET` request to `/actuator/startup`, as shown in the following curl-based example: + +include::{snippets}/startup-snapshot/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}/startup-snapshot/http-response.adoc[] + + + +[[startup.retrieving.drain]] +=== Draining the Application Startup Steps +To drain and return the steps recorded so far during the application startup phase, make a `POST` request to `/actuator/startup`, as shown in the following curl-based example: + +include::{snippets}/startup/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}/startup/http-response.adoc[] + + + +[[startup.retrieving.response-structure]] +=== Response Structure +The response contains details of the application startup steps. +The following table describes the structure of the response: + +[cols="2,1,3"] +include::{snippets}/startup/response-fields.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/threaddump.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/threaddump.adoc new file mode 100644 index 000000000000..566ff24444e4 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/threaddump.adoc @@ -0,0 +1,38 @@ +[[threaddump]] += Thread Dump (`threaddump`) +The `threaddump` endpoint provides a thread dump from the application's JVM. + + + +[[threaddump.retrieving-json]] +== Retrieving the Thread Dump as JSON +To retrieve the thread dump as JSON, make a `GET` request to `/actuator/threaddump` with an appropriate `Accept` header, as shown in the following curl-based example: + +include::{snippets}/threaddump/json/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}/threaddump/json/http-response.adoc[] + + + +[[threaddump.retrieving-json.response-structure]] +=== Response Structure +The response contains details of the JVM's threads. +The following table describes the structure of the response: + +[cols="3,1,2"] +include::{snippets}/threaddump/json/response-fields.adoc[] + + + +[[threaddump.retrieving-text]] +== Retrieving the Thread Dump as Text +To retrieve the thread dump as text, make a `GET` request to `/actuator/threaddump` that +accepts `text/plain`, as shown in the following curl-based example: + +include::{snippets}/threaddump/text/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}/threaddump/text/http-response.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/index.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/index.adoc new file mode 100644 index 000000000000..0168b94a4a6b --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/index.adoc @@ -0,0 +1,103 @@ +[[spring-boot-actuator-web-api-documentation]] += Spring Boot Actuator Web API Documentation +Andy Wilkinson; Stephane Nicoll +v{gradle-project-version} +:!version-label: +:doctype: book +:toc: left +:toclevels: 4 +:numbered: +:icons: font +:hide-uri-scheme: +:docinfo: shared,private +:attribute-missing: warn + + + +This API documentation describes Spring Boot Actuators web endpoints. + + + +[[overview]] +== Overview +Before you proceed, you should read the following topics: + +* <> +* <> + +NOTE: In order to get the correct JSON responses documented below, Jackson must be available. + + + +[[overview.endpoint-urls]] +=== URLs +By default, all web endpoints are available beneath the path `/actuator` with URLs of +the form `/actuator/\{id}`. The `/actuator` base path can be configured by using the +`management.endpoints.web.base-path` property, as shown in the following example: + +[source,properties,indent=0] +---- + management.endpoints.web.base-path=/manage +---- + +The preceding `application.properties` example changes the form of the endpoint URLs from +`/actuator/\{id}` to `/manage/\{id}`. For example, the URL `info` endpoint would become +`/manage/info`. + + + +[[overview.timestamps]] +=== Timestamps +All timestamps that are consumed by the endpoints, either as query parameters or in the +request body, must be formatted as an offset date and time as specified in +https://en.wikipedia.org/wiki/ISO_8601[ISO 8601]. + + + +include::endpoints/auditevents.adoc[leveloffset=+1] + +include::endpoints/beans.adoc[leveloffset=+1] + +include::endpoints/caches.adoc[leveloffset=+1] + +include::endpoints/conditions.adoc[leveloffset=+1] + +include::endpoints/configprops.adoc[leveloffset=+1] + +include::endpoints/env.adoc[leveloffset=+1] + +include::endpoints/flyway.adoc[leveloffset=+1] + +include::endpoints/health.adoc[leveloffset=+1] + +include::endpoints/heapdump.adoc[leveloffset=+1] + +include::endpoints/httptrace.adoc[leveloffset=+1] + +include::endpoints/info.adoc[leveloffset=+1] + +include::endpoints/integrationgraph.adoc[leveloffset=+1] + +include::endpoints/liquibase.adoc[leveloffset=+1] + +include::endpoints/logfile.adoc[leveloffset=+1] + +include::endpoints/loggers.adoc[leveloffset=+1] + +include::endpoints/mappings.adoc[leveloffset=+1] + +include::endpoints/metrics.adoc[leveloffset=+1] + +include::endpoints/prometheus.adoc[leveloffset=+1] + +include::endpoints/quartz.adoc[leveloffset=+1] + +include::endpoints/scheduledtasks.adoc[leveloffset=+1] + +include::endpoints/sessions.adoc[leveloffset=+1] + +include::endpoints/shutdown.adoc[leveloffset=+1] + +include::endpoints/startup.adoc[leveloffset=+1] + +include::endpoints/threaddump.adoc[leveloffset=+1] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/auditevents.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/auditevents.adoc deleted file mode 100644 index 91dac834d5e8..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/auditevents.adoc +++ /dev/null @@ -1,40 +0,0 @@ -[[audit-events]] -= Audit Events (`auditevents`) - -The `auditevents` endpoint provides information about the application's audit events. - - - -[[audit-events-retrieving]] -== Retrieving Audit Events - -To retrieve the audit events, make a `GET` request to `/actuator/auditevents`, as shown in the following curl-based example: - -include::{snippets}auditevents/filtered/curl-request.adoc[] - -The preceding example retrieves `logout` events for the principal, `alice`, that occurred after 09:37 on 7 November 2017 in the UTC timezone. -The resulting response is similar to the following: - -include::{snippets}auditevents/filtered/http-response.adoc[] - - - -[[audit-events-retrieving-query-parameters]] -=== Query Parameters - -The endpoint uses query parameters to limit the events that it returns. -The following table shows the supported query parameters: - -[cols="2,4"] -include::{snippets}auditevents/filtered/request-parameters.adoc[] - - - -[[audit-events-retrieving-response-structure]] -=== Response Structure - -The response contains details of all of the audit events that matched the query. -The following table describes the structure of the response: - -[cols="2,1,3"] -include::{snippets}auditevents/all/response-fields.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/beans.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/beans.adoc deleted file mode 100644 index 5417a906011b..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/beans.adoc +++ /dev/null @@ -1,28 +0,0 @@ -[[beans]] -= Beans (`beans`) - -The `beans` endpoint provides information about the application's beans. - - - -[[beans-retrieving]] -== Retrieving the Beans - -To retrieve the beans, make a `GET` request to `/actuator/beans`, as shown in the following curl-based example: - -include::{snippets}beans/curl-request.adoc[] - -The resulting response is similar to the following: - -include::{snippets}beans/http-response.adoc[] - - - -[[beans-retrieving-response-structure]] -=== Response Structure - -The response contains details of the application's beans. -The following table describes the structure of the response: - -[cols="2,1,3"] -include::{snippets}beans/response-fields.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/caches.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/caches.adoc deleted file mode 100644 index 3f2156a4b9a8..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/caches.adoc +++ /dev/null @@ -1,89 +0,0 @@ -[[caches]] -= Caches (`caches`) - -The `caches` endpoint provides access to the application's caches. - - - -[[caches-all]] -== Retrieving All Caches -To retrieve the application's caches, make a `GET` request to `/actuator/caches`, as shown in the following curl-based example: - -include::{snippets}caches/all/curl-request.adoc[] - -The resulting response is similar to the following: - -include::{snippets}caches/all/http-response.adoc[] - - - -[[caches-all-response-structure]] -=== Response Structure -The response contains details of the application's caches. -The following table describes the structure of the response: - -[cols="3,1,3"] -include::{snippets}caches/all/response-fields.adoc[] - - - -[[caches-named]] -== Retrieving Caches by Name -To retrieve a cache by name, make a `GET` request to `/actuator/caches/\{name}`, as shown in the following curl-based example: - -include::{snippets}caches/named/curl-request.adoc[] - -The preceding example retrieves information about the cache named `cities`. -The resulting response is similar to the following: - -include::{snippets}caches/named/http-response.adoc[] - - - -[[caches-named-query-parameters]] -=== Query Parameters -If the requested name is specific enough to identify a single cache, no extra parameter is required. -Otherwise, the `cacheManager` must be specified. -The following table shows the supported query parameters: - -[cols="2,4"] -include::{snippets}caches/named/request-parameters.adoc[] - - - -[[caches-named-response-structure]] -=== Response Structure -The response contains details of the requested cache. -The following table describes the structure of the response: - -[cols="3,1,3"] -include::{snippets}caches/named/response-fields.adoc[] - - - -[[caches-evict-all]] -== Evict All Caches -To clear all available caches, make a `DELETE` request to `/actuator/caches` as shown in the following curl-based example: - -include::{snippets}caches/evict-all/curl-request.adoc[] - - - -[[caches-evict-named]] -== Evict a Cache by Name -To evict a particular cache, make a `DELETE` request to `/actuator/caches/\{name}` as shown in the following curl-based example: - -include::{snippets}caches/evict-named/curl-request.adoc[] - -NOTE: As there are two caches named `countries`, the `cacheManager` has to be provided to specify which `Cache` should be cleared. - - - -[[caches-evict-named-request-structure]] -=== Request Structure -If the requested name is specific enough to identify a single cache, no extra parameter is required. -Otherwise, the `cacheManager` must be specified. -The following table shows the supported query parameters: - -[cols="2,4"] -include::{snippets}caches/evict-named/request-parameters.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/conditions.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/conditions.adoc deleted file mode 100644 index 26dace0267f5..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/conditions.adoc +++ /dev/null @@ -1,28 +0,0 @@ -[[conditions]] -= Conditions Evaluation Report (`conditions`) - -The `conditions` endpoint provides information about the evaluation of conditions on configuration and auto-configuration classes. - - - -[[conditions-retrieving]] -== Retrieving the Report - -To retrieve the report, make a `GET` request to `/actuator/conditions`, as shown in the following curl-based example: - -include::{snippets}conditions/curl-request.adoc[] - -The resulting response is similar to the following: - -include::{snippets}conditions/http-response.adoc[] - - - -[[conditions-retrieving-response-structure]] -=== Response Structure - -The response contains details of the application's condition evaluation. -The following table describes the structure of the response: - -[cols="3,1,3"] -include::{snippets}conditions/response-fields.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/configprops.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/configprops.adoc deleted file mode 100644 index 651b0ec51237..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/configprops.adoc +++ /dev/null @@ -1,28 +0,0 @@ -[[configprops]] -= Configuration Properties (`configprops`) - -The `configprops` endpoint provides information about the application's `@ConfigurationProperties` beans. - - - -[[configprops-retrieving]] -== Retrieving the `@ConfigurationProperties` Bean - -To retrieve the `@ConfigurationProperties` beans, make a `GET` request to `/actuator/configprops`, as shown in the following curl-based example: - -include::{snippets}configprops/curl-request.adoc[] - -The resulting response is similar to the following: - -include::{snippets}configprops/http-response.adoc[] - - - -[[configprops-retrieving-response-structure]] -=== Response Structure - -The response contains details of the application's `@ConfigurationProperties` beans. -The following table describes the structure of the response: - -[cols="2,1,3"] -include::{snippets}configprops/response-fields.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/env.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/env.adoc deleted file mode 100644 index d7b917ca1668..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/env.adoc +++ /dev/null @@ -1,53 +0,0 @@ -[[env]] -= Environment (`env`) - -The `env` endpoint provides information about the application's `Environment`. - - - -[[env-entire]] -== Retrieving the Entire Environment - -To retrieve the entire environment, make a `GET` request to `/actuator/env`, as shown in the following curl-based example: - -include::{snippets}env/all/curl-request.adoc[] - -The resulting response is similar to the following: - -include::{snippets}env/all/http-response.adoc[] - - - -[[env-entire-response-structure]] -=== Response Structure - -The response contains details of the application's `Environment`. -The following table describes the structure of the response: - -[cols="3,1,3"] -include::{snippets}env/all/response-fields.adoc[] - - - -[[env-single-property]] -== Retrieving a Single Property - -To retrieve a single property, make a `GET` request to `/actuator/env/{property.name}`, as shown in the following curl-based example: - -include::{snippets}env/single/curl-request.adoc[] - -The preceding example retrieves information about the property named `com.example.cache.max-size`. -The resulting response is similar to the following: - -include::{snippets}env/single/http-response.adoc[] - - - -[[env-single-response-structure]] -=== Response Structure - -The response contains details of the requested property. -The following table describes the structure of the response: - -[cols="3,1,3"] -include::{snippets}env/single/response-fields.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/flyway.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/flyway.adoc deleted file mode 100644 index 8fe81d6295d3..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/flyway.adoc +++ /dev/null @@ -1,28 +0,0 @@ -[[flyway]] -= Flyway (`flyway`) - -The `flyway` endpoint provides information about database migrations performed by Flyway. - - - -[[flyway-retrieving]] -== Retrieving the Migrations - -To retrieve the migrations, make a `GET` request to `/actuator/flyway`, as shown in the following curl-based example: - -include::{snippets}flyway/curl-request.adoc[] - -The resulting response is similar to the following: - -include::{snippets}flyway/http-response.adoc[] - - - -[[flyway-retrieving-response-structure]] -=== Response Structure - -The response contains details of the application's Flyway migrations. -The following table describes the structure of the response: - -[cols="2,1,3"] -include::{snippets}flyway/response-fields.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/health.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/health.adoc deleted file mode 100644 index a7c1998dde37..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/health.adoc +++ /dev/null @@ -1,75 +0,0 @@ -[[health]] -= Health (`health`) -The `health` endpoint provides detailed information about the health of the application. - - - -[[health-retrieving]] -== Retrieving the Health of the application -To retrieve the health of the application, make a `GET` request to `/actuator/health`, as shown in the following curl-based example: - -include::{snippets}health/curl-request.adoc[] - -The resulting response is similar to the following: - -include::{snippets}health/http-response.adoc[] - - - -[[health-retrieving-response-structure]] -=== Response Structure -The response contains details of the health of the application. -The following table describes the structure of the response: - -[cols="2,1,3"] -include::{snippets}health/response-fields.adoc[] - -NOTE: The response fields above are for the V3 API. -If you need to return V2 JSON you should use an accept header or `application/vnd.spring-boot.actuator.v2+json` - - - -[[health-retrieving-component]] -== Retrieving the Health of a component -To retrieve the health of a particular component of the application's health, make a `GET` request to `/actuator/health/\{component}`, as shown in the following curl-based example: - -include::{snippets}health/component/curl-request.adoc[] - -The resulting response is similar to the following: - -include::{snippets}health/component/http-response.adoc[] - - - -[[health-retrieving-component-response-structure]] -=== Response Structure -The response contains details of the health of a particular component of the application's health. -The following table describes the structure of the response: - -[cols="2,1,3"] -include::{snippets}health/component/response-fields.adoc[] - - - -[[health-retrieving-component-nested]] -== Retrieving the Health of a nested component -If a particular component contains other nested components (as the `broker` indicator in the example above), the health of such a nested component can be retrieved by issuing a `GET` request to `/actuator/health/\{component}/\{subcomponent}`, as shown in the following curl-based example: - -include::{snippets}health/instance/curl-request.adoc[] - -The resulting response is similar to the following: - -include::{snippets}health/instance/http-response.adoc[] - -Components of an application's health may be nested arbitrarily deep depending on the application's health indicators and how they have been grouped. -The health endpoint supports any number of `/\{component}` identifiers in the URL to allow the health of a component at any depth to be retrieved. - - - -[[health-retrieving-component-instance-response-structure]] -=== Response Structure -The response contains details of the health of an instance of a particular component of the application. -The following table describes the structure of the response: - -[cols="2,1,3"] -include::{snippets}health/instance/response-fields.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/httptrace.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/httptrace.adoc deleted file mode 100644 index ea1149f9efd5..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/httptrace.adoc +++ /dev/null @@ -1,28 +0,0 @@ -[[http-trace]] -= HTTP Trace (`httptrace`) - -The `httptrace` endpoint provides information about HTTP request-response exchanges. - - - -[[http-trace-retrieving]] -== Retrieving the Traces - -To retrieve the traces, make a `GET` request to `/actuator/httptrace`, as shown in the following curl-based example: - -include::{snippets}httptrace/curl-request.adoc[] - -The resulting response is similar to the following: - -include::{snippets}httptrace/http-response.adoc[] - - - -[[http-trace-retrieving-response-structure]] -=== Response Structure - -The response contains details of the traced HTTP request-response exchanges. -The following table describes the structure of the response: - -[cols="2,1,3"] -include::{snippets}httptrace/response-fields.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/info.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/info.adoc deleted file mode 100644 index f431c3a8d80a..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/info.adoc +++ /dev/null @@ -1,46 +0,0 @@ -[[info]] -= Info (`info`) - -The `info` endpoint provides general information about the application. - - - -[[info-retrieving]] -== Retrieving the Info - -To retrieve the information about the application, make a `GET` request to `/actuator/info`, as shown in the following curl-based example: - -include::{snippets}info/curl-request.adoc[] - -The resulting response is similar to the following: - -include::{snippets}info/http-response.adoc[] - - - -[[info-retrieving-response-structure]] -=== Response Structure - -The response contains general information about the application. -Each section of the response is contributed by an `InfoContributor`. -Spring Boot provides `build` and `git` contributions. - - - -[[info-retrieving-response-structure-build]] -==== `build` Response Structure - -The following table describe the structure of the `build` section of the response: - -[cols="2,1,3"] -include::{snippets}info/response-fields-beneath-build.adoc[] - - - -[[info-retrieving-response-structure-git]] -==== `git` Response Structure - -The following table describes the structure of the `git` section of the response: - -[cols="2,1,3"] -include::{snippets}info/response-fields-beneath-git.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/integrationgraph.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/integrationgraph.adoc deleted file mode 100644 index e2d3274729dc..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/integrationgraph.adoc +++ /dev/null @@ -1,35 +0,0 @@ -[[integrationgraph]] -= Spring Integration graph (`integrationgraph`) - -The `integrationgraph` endpoint exposes a graph containing all Spring Integration components. - - - -[[integrationgraph-retrieving]] -== Retrieving the Spring Integration graph -To retrieve the information about the application, make a `GET` request to `/actuator/integrationgraph`, as shown in the following curl-based example: - -include::{snippets}integrationgraph/graph/curl-request.adoc[] - -The resulting response is similar to the following: - -include::{snippets}integrationgraph/graph/http-response.adoc[] - - - -[[integrationgraph-retrieving-response-structure]] -=== Response Structure -The response contains all Spring Integration components used within the application, as well as the links between them. -More information about the structure can be found in the https://docs.spring.io/spring-integration/reference/html/#integration-graph[reference documentation]. - - - -[[integrationgraph-rebuilding]] -== Rebuilding the Spring Integration graph -To rebuild the exposed graph, make a `POST` request to `/actuator/integrationgraph`, as shown in the following curl-based example: - -include::{snippets}integrationgraph/rebuild/curl-request.adoc[] - -This will result in a `204 - No Content` response: - -include::{snippets}integrationgraph/rebuild/http-response.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/liquibase.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/liquibase.adoc deleted file mode 100644 index 1e84c8efac84..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/liquibase.adoc +++ /dev/null @@ -1,28 +0,0 @@ -[[liquibase]] -= Liquibase (`liquibase`) - -The `liquibase` endpoint provides information about database change sets applied by Liquibase. - - - -[[liquibase-retrieving]] -== Retrieving the Changes - -To retrieve the changes, make a `GET` request to `/actuator/liquibase`, as shown in the following curl-based example: - -include::{snippets}liquibase/curl-request.adoc[] - -The resulting response is similar to the following: - -include::{snippets}liquibase/http-response.adoc[] - - - -[[liquibase-retrieving-response-structure]] -=== Response Structure - -The response contains details of the application's Liquibase change sets. -The following table describes the structure of the response: - -[cols="2,1,3"] -include::{snippets}liquibase/response-fields.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/logfile.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/logfile.adoc deleted file mode 100644 index 99121fc6734d..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/logfile.adoc +++ /dev/null @@ -1,33 +0,0 @@ -[[log-file]] -= Log File (`logfile`) - -The `logfile` endpoint provides access to the contents of the application's log file. - - - -[[logfile-retrieving]] -== Retrieving the Log File - -To retrieve the log file, make a `GET` request to `/actuator/logfile`, as shown in the following curl-based example: - -include::{snippets}logfile/entire/curl-request.adoc[] - -The resulting response is similar to the following: - -include::{snippets}logfile/entire/http-response.adoc[] - - - -[[logfile-retrieving-part]] -== Retrieving Part of the Log File - -NOTE: Retrieving part of the log file is not supported when using Jersey. - -To retrieve part of the log file, make a `GET` request to `/actuator/logfile` by using the `Range` header, as shown in the following curl-based example: - -include::{snippets}logfile/range/curl-request.adoc[] - -The preceding example retrieves the first 1024 bytes of the log file. -The resulting response is similar to the following: - -include::{snippets}logfile/range/http-response.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/loggers.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/loggers.adoc deleted file mode 100644 index e4609c73e7ff..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/loggers.adoc +++ /dev/null @@ -1,134 +0,0 @@ -[[loggers]] -= Loggers (`loggers`) - -The `loggers` endpoint provides access to the application's loggers and the configuration of their levels. - - - -[[loggers-all]] -== Retrieving All Loggers - -To retrieve the application's loggers, make a `GET` request to `/actuator/loggers`, as shown in the following curl-based example: - -include::{snippets}loggers/all/curl-request.adoc[] - -The resulting response is similar to the following: - -include::{snippets}loggers/all/http-response.adoc[] - - - -[[loggers-all-response-structure]] -=== Response Structure - -The response contains details of the application's loggers. -The following table describes the structure of the response: - -[cols="3,1,3"] -include::{snippets}loggers/all/response-fields.adoc[] - - - -[[loggers-single]] -== Retrieving a Single Logger - -To retrieve a single logger, make a `GET` request to `/actuator/loggers/{logger.name}`, as shown in the following curl-based example: - -include::{snippets}loggers/single/curl-request.adoc[] - -The preceding example retrieves information about the logger named `com.example`. -The resulting response is similar to the following: - -include::{snippets}loggers/single/http-response.adoc[] - - - -[[loggers-single-response-structure]] -=== Response Structure - -The response contains details of the requested logger. -The following table describes the structure of the response: - -[cols="3,1,3"] -include::{snippets}loggers/single/response-fields.adoc[] - - - -[[loggers-group]] -== Retrieving a Single Group - -To retrieve a single group, make a `GET` request to `/actuator/loggers/{group.name}`, -as shown in the following curl-based example: - -include::{snippets}loggers/group/curl-request.adoc[] - -The preceding example retrieves information about the logger group named `test`. -The resulting response is similar to the following: - -include::{snippets}loggers/group/http-response.adoc[] - - - -[[loggers-group-response-structure]] -=== Response Structure - -The response contains details of the requested group. -The following table describes the structure of the response: - -[cols="3,1,3"] -include::{snippets}loggers/group/response-fields.adoc[] - - - -[[loggers-setting-level]] -== Setting a Log Level - -To set the level of a logger, make a `POST` request to `/actuator/loggers/{logger.name}` with a JSON body that specifies the configured level for the logger, as shown in the following curl-based example: - -include::{snippets}loggers/set/curl-request.adoc[] - -The preceding example sets the `configuredLevel` of the `com.example` logger to `DEBUG`. - - - -[[loggers-setting-level-request-structure]] -=== Request Structure - -The request specifies the desired level of the logger. -The following table describes the structure of the request: - -[cols="3,1,3"] -include::{snippets}loggers/set/request-fields.adoc[] - - - -[[loggers-group-setting-level]] -== Setting a Log Level for a Group - -To set the level of a logger, make a `POST` request to `/actuator/loggers/{group.name}` with a JSON body that specifies the configured level for the logger group, as shown in the following curl-based example: - -include::{snippets}loggers/setGroup/curl-request.adoc[] - -The preceding example sets the `configuredLevel` of the `test` logger group to `DEBUG`. - - - -[[loggers-group-setting-level-request-structure]] -=== Request Structure - -The request specifies the desired level of the logger group. -The following table describes the structure of the request: - -[cols="3,1,3"] -include::{snippets}loggers/set/request-fields.adoc[] - - - -[[loggers-clearing-level]] -== Clearing a Log Level - -To clear the level of a logger, make a `POST` request to `/actuator/loggers/{logger.name}` with a JSON body containing an empty object, as shown in the following curl-based example: - -include::{snippets}loggers/clear/curl-request.adoc[] - -The preceding example clears the configured level of the `com.example` logger. diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/mappings.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/mappings.adoc deleted file mode 100644 index 1d7371d9e252..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/mappings.adoc +++ /dev/null @@ -1,74 +0,0 @@ -[[mappings]] -= Mappings (`mappings`) - -The `mappings` endpoint provides information about the application's request mappings. - - - -[[mappings-retrieving]] -== Retrieving the Mappings - -To retrieve the mappings, make a `GET` request to `/actuator/mappings`, as shown in the following curl-based example: - -include::{snippets}mappings/curl-request.adoc[] - -The resulting response is similar to the following: - -include::{snippets}mappings/http-response.adoc[] - - - -[[mappings-retrieving-response-structure]] -=== Response Structure - -The response contains details of the application's mappings. -The items found in the response depend on the type of web application (reactive or Servlet-based). -The following table describes the structure of the common elements of the response: - -[cols="2,1,3"] -include::{snippets}mappings/response-fields.adoc[] - -The entries that may be found in `contexts.*.mappings` are described in the following sections. - - -[[mappings-retrieving-response-structure-dispatcher-servlets]] -=== Dispatcher Servlets Response Structure - -When using Spring MVC, the response contains details of any `DispatcherServlet` request mappings beneath `contexts.*.mappings.dispatcherServlets`. -The following table describes the structure of this section of the response: - -[cols="4,1,2"] -include::{snippets}mappings/response-fields-dispatcher-servlets.adoc[] - - - -[[mappings-retrieving-response-structure-servlets]] -=== Servlets Response Structure - -When using the Servlet stack, the response contains details of any `Servlet` mappings beneath `contexts.*.mappings.servlets`. -The following table describes the structure of this section of the response: - -[cols="2,1,3"] -include::{snippets}mappings/response-fields-servlets.adoc[] - - - -[[mappings-retrieving-response-structure-servlet-filters]] -=== Servlet Filters Response Structure - -When using the Servlet stack, the response contains details of any `Filter` mappings beneath `contexts.*.mappings.servletFilters`. -The following table describes the structure of this section of the response: - -[cols="2,1,3"] -include::{snippets}mappings/response-fields-servlet-filters.adoc[] - - - -[[mappings-retrieving-response-structure-dispatcher-handlers]] -=== Dispatcher Handlers Response Structure - -When using Spring WebFlux, the response contains details of any `DispatcherHandler` request mappings beneath `contexts.*.mappings.dispatcherHandlers`. -The following table describes the structure of this section of the response: - -[cols="4,1,2"] -include::{snippets}mappings/response-fields-dispatcher-handlers.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/metrics.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/metrics.adoc deleted file mode 100644 index db26c3fd95e1..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/metrics.adoc +++ /dev/null @@ -1,76 +0,0 @@ -[[metrics]] -= Metrics (`metrics`) - -The `metrics` endpoint provides access to application metrics. - - - -[[metrics-retrieving-names]] -== Retrieving Metric Names - -To retrieve the names of the available metrics, make a `GET` request to `/actuator/metrics`, as shown in the following curl-based example: - -include::{snippets}metrics/names/curl-request.adoc[] - -The resulting response is similar to the following: - -include::{snippets}metrics/names/http-response.adoc[] - - - -[[metrics-retrieving-names-response-structure]] -=== Response Structure - -The response contains details of the metric names. -The following table describes the structure of the response: - -[cols="3,1,2"] -include::{snippets}metrics/names/response-fields.adoc[] - - - -[[metrics-retrieving-metric]] -== Retrieving a Metric - -To retrieve a metric, make a `GET` request to `/actuator/metrics/{metric.name}`, as shown in the following curl-based example: - -include::{snippets}metrics/metric/curl-request.adoc[] - -The preceding example retrieves information about the metric named `jvm.memory.max`. -The resulting response is similar to the following: - -include::{snippets}metrics/metric/http-response.adoc[] - - - -[[metrics-retrieving-metric-query-parameters]] -=== Query Parameters - -The endpoint uses query parameters to <> into a metric by using its tags. -The following table shows the single supported query parameter: - -[cols="2,4"] -include::{snippets}metrics/metric-with-tags/request-parameters.adoc[] - - - -[[metrics-retrieving-metric-response-structure]] -=== Response structure - -The response contains details of the metric. -The following table describes the structure of the response: - -include::{snippets}metrics/metric/response-fields.adoc[] - - -[[metrics-drilling-down]] -== Drilling Down - -To drill down into a metric, make a `GET` request to `/actuator/metrics/{metric.name}` using the `tag` query parameter, as shown in the following curl-based example: - -include::{snippets}metrics/metric-with-tags/curl-request.adoc[] - -The preceding example retrieves the `jvm.memory.max` metric, where the `area` tag has a value of `nonheap` and the `id` attribute has a value of `Compressed Class Space`. -The resulting response is similar to the following: - -include::{snippets}metrics/metric-with-tags/http-response.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/prometheus.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/prometheus.adoc deleted file mode 100644 index 1d1006bc3764..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/prometheus.adoc +++ /dev/null @@ -1,17 +0,0 @@ -[[prometheus]] -= Prometheus (`prometheus`) - -The `prometheus` endpoint provides Spring Boot application's metrics in the format required for scraping by a Prometheus server. - - - -[[prometheus-retrieving]] -== Retrieving the Metrics - -To retrieve the metrics, make a `GET` request to `/actuator/prometheus`, as shown in the following curl-based example: - -include::{snippets}prometheus/curl-request.adoc[] - -The resulting response is similar to the following: - -include::{snippets}prometheus/http-response.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/scheduledtasks.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/scheduledtasks.adoc deleted file mode 100644 index a3a4f3a9a4c4..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/scheduledtasks.adoc +++ /dev/null @@ -1,28 +0,0 @@ -[[scheduled-tasks]] -= Scheduled Tasks (`scheduledtasks`) - -The `scheduledtasks` endpoint provides information about the application's scheduled tasks. - - - -[[scheduled-tasks-retrieving]] -== Retrieving the Scheduled Tasks - -To retrieve the scheduled tasks, make a `GET` request to `/actuator/scheduledtasks`, as shown in the following curl-based example: - -include::{snippets}scheduled-tasks/curl-request.adoc[] - -The resulting response is similar to the following: - -include::{snippets}scheduled-tasks/http-response.adoc[] - - - -[[scheduled-tasks-retrieving-response-structure]] -=== Response Structure - -The response contains details of the application's scheduled tasks. -The following table describes the structure of the response: - -[cols="2,1,3"] -include::{snippets}scheduled-tasks/response-fields.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/sessions.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/sessions.adoc deleted file mode 100644 index 8cc3508d9e99..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/sessions.adoc +++ /dev/null @@ -1,76 +0,0 @@ -[[sessions]] -= Sessions (`sessions`) - -The `sessions` endpoint provides information about the application's HTTP sessions that are managed by Spring Session. - - - -[[sessions-retrieving]] -== Retrieving Sessions - -To retrieve the sessions, make a `GET` request to `/actuator/sessions`, as shown in the following curl-based example: - -include::{snippets}sessions/username/curl-request.adoc[] - -The preceding examples retrieves all of the sessions for the user whose username is `alice`. -The resulting response is similar to the following: - -include::{snippets}sessions/username/http-response.adoc[] - - - -[[sessions-retrieving-query-parameters]] -=== Query Parameters - -The endpoint uses query parameters to limit the sessions that it returns. -The following table shows the single required query parameter: - -[cols="2,4"] -include::{snippets}sessions/username/request-parameters.adoc[] - - - -[[sessions-retrieving-response-structure]] -=== Response Structure - -The response contains details of the matching sessions. -The following table describes the structure of the response: - -[cols="3,1,3"] -include::{snippets}sessions/username/response-fields.adoc[] - - - -[[sessions-retrieving-id]] -== Retrieving a Single Session - -To retrieve a single session, make a `GET` request to `/actuator/sessions/\{id}`, as shown in the following curl-based example: - -include::{snippets}sessions/id/curl-request.adoc[] - -The preceding example retrieves the session with the `id` of `4db5efcc-99cb-4d05-a52c-b49acfbb7ea9`. -The resulting response is similar to the following: - -include::{snippets}sessions/id/http-response.adoc[] - - - -[[sessions-retrieving-id-response-structure]] -=== Response Structure - -The response contains details of the requested session. -The following table describes the structure of the response: - -[cols="3,1,3"] -include::{snippets}sessions/id/response-fields.adoc[] - - - -[[sessions-deleting]] -== Deleting a Session - -To delete a session, make a `DELETE` request to `/actuator/sessions/\{id}`, as shown in the following curl-based example: - -include::{snippets}sessions/delete/curl-request.adoc[] - -The preceding example deletes the session with the `id` of `4db5efcc-99cb-4d05-a52c-b49acfbb7ea9`. diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/shutdown.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/shutdown.adoc deleted file mode 100644 index 00aec5b1bf03..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/shutdown.adoc +++ /dev/null @@ -1,28 +0,0 @@ -[[shutdown]] -= Shutdown (`shutdown`) - -The `shutdown` endpoint is used to shut down the application. - - - -[[shutdown-shutting-down]] -== Shutting Down the Application - -To shut down the application, make a `POST` request to `/actuator/shutdown`, as shown in the following curl-based example: - -include::{snippets}shutdown/curl-request.adoc[] - -A response similar to the following is produced: - -include::{snippets}shutdown/http-response.adoc[] - - - -[[shutdown-shutting-down-response-structure]] -=== Response Structure - -The response contains details of the result of the shutdown request. -The following table describes the structure of the response: - -[cols="3,1,3"] -include::{snippets}shutdown/response-fields.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/threaddump.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/threaddump.adoc deleted file mode 100644 index fa61f6df0c9a..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/threaddump.adoc +++ /dev/null @@ -1,42 +0,0 @@ -[[threaddump]] -= Thread Dump (`threaddump`) - -The `threaddump` endpoint provides a thread dump from the application's JVM. - - - -[[threaddump-retrieving-json]] -== Retrieving the Thread Dump as JSON - -To retrieve the thread dump as JSON, make a `GET` request to `/actuator/threaddump` with an appropriate `Accept` header, as shown in the following curl-based example: - -include::{snippets}threaddump/json/curl-request.adoc[] - -The resulting response is similar to the following: - -include::{snippets}threaddump/json/http-response.adoc[] - - - -[[threaddump-retrieving-json-response-structure]] -=== Response Structure - -The response contains details of the JVM's threads. -The following table describes the structure of the response: - -[cols="3,1,2"] -include::{snippets}threaddump/json/response-fields.adoc[] - - - -[[threaddump-retrieving-text]] -== Retrieving the Thread Dump as Text - -To retrieve the thread dump as text, make a `GET` request to `/actuator/threaddump` that -accepts `text/plain`, as shown in the following curl-based example: - -include::{snippets}threaddump/text/curl-request.adoc[] - -The resulting response is similar to the following: - -include::{snippets}threaddump/text/http-response.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/index.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/index.adoc deleted file mode 100644 index d5d4ebf1ef3f..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/index.adoc +++ /dev/null @@ -1,74 +0,0 @@ -= Spring Boot Actuator Web API Documentation -Andy Wilkinson -:doctype: book -:toc: left -:toclevels: 4 -:source-highlighter: prettify -:numbered: -:icons: font -:hide-uri-scheme: -:docinfo: shared,private - -This API documentation describes Spring Boot Actuators web endpoints. - - - -[[overview]] -== Overview - -Before you proceed, you should read the following topics: - -* <> -* <> - - - -[[overview-endpoint-urls]] -=== URLs - -By default, all web endpoints are available beneath the path `/actuator` with URLs of -the form `/actuator/\{id}`. The `/actuator` base path can be configured by using the -`management.endpoints.web.base-path` property, as shown in the following example: - -[source,properties,indent=0] ----- - management.endpoints.web.base-path=/manage ----- - -The preceding `application.properties` example changes the form of the endpoint URLs from -`/actuator/\{id}` to `/manage/\{id}`. For example, the URL `info` endpoint would become -`/manage/info`. - - - -[[overview-timestamps]] -=== Timestamps - -All timestamps that are consumed by the endpoints, either as query parameters or in the -request body, must be formatted as an offset date and time as specified in -https://en.wikipedia.org/wiki/ISO_8601[ISO 8601]. - - - -include::endpoints/auditevents.adoc[leveloffset=+1] -include::endpoints/beans.adoc[leveloffset=+1] -include::endpoints/caches.adoc[leveloffset=+1] -include::endpoints/conditions.adoc[leveloffset=+1] -include::endpoints/configprops.adoc[leveloffset=+1] -include::endpoints/env.adoc[leveloffset=+1] -include::endpoints/flyway.adoc[leveloffset=+1] -include::endpoints/health.adoc[leveloffset=+1] -include::endpoints/heapdump.adoc[leveloffset=+1] -include::endpoints/httptrace.adoc[leveloffset=+1] -include::endpoints/info.adoc[leveloffset=+1] -include::endpoints/integrationgraph.adoc[leveloffset=+1] -include::endpoints/liquibase.adoc[leveloffset=+1] -include::endpoints/logfile.adoc[leveloffset=+1] -include::endpoints/loggers.adoc[leveloffset=+1] -include::endpoints/mappings.adoc[leveloffset=+1] -include::endpoints/metrics.adoc[leveloffset=+1] -include::endpoints/prometheus.adoc[leveloffset=+1] -include::endpoints/scheduledtasks.adoc[leveloffset=+1] -include::endpoints/sessions.adoc[leveloffset=+1] -include::endpoints/shutdown.adoc[leveloffset=+1] -include::endpoints/threaddump.adoc[leveloffset=+1] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/OnEndpointElementCondition.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/OnEndpointElementCondition.java index 0ad2e1d9ef2d..e1634475bbc1 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/OnEndpointElementCondition.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/OnEndpointElementCondition.java @@ -69,7 +69,8 @@ protected ConditionOutcome getEndpointOutcome(ConditionContext context, String e } protected ConditionOutcome getDefaultEndpointsOutcome(ConditionContext context) { - boolean match = Boolean.valueOf(context.getEnvironment().getProperty(this.prefix + "defaults.enabled", "true")); + boolean match = Boolean + .parseBoolean(context.getEnvironment().getProperty(this.prefix + "defaults.enabled", "true")); return new ConditionOutcome(match, ConditionMessage.forCondition(this.annotationType) .because(this.prefix + "defaults.enabled is considered " + match)); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityHealthContributorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityHealthContributorAutoConfiguration.java new file mode 100644 index 000000000000..4a028096f7d7 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityHealthContributorAutoConfiguration.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.availability; + +import org.springframework.boot.actuate.availability.AvailabilityStateHealthIndicator; +import org.springframework.boot.actuate.availability.LivenessStateHealthIndicator; +import org.springframework.boot.actuate.availability.ReadinessStateHealthIndicator; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.availability.ApplicationAvailability; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for + * {@link AvailabilityStateHealthIndicator}. + * + * @author Brian Clozel + * @since 2.3.2 + */ +@Configuration(proxyBeanMethods = false) +@AutoConfigureAfter(ApplicationAvailabilityAutoConfiguration.class) +public class AvailabilityHealthContributorAutoConfiguration { + + @Bean + @ConditionalOnMissingBean(name = "livenessStateHealthIndicator") + @ConditionalOnProperty(prefix = "management.health.livenessstate", name = "enabled", havingValue = "true") + public LivenessStateHealthIndicator livenessStateHealthIndicator(ApplicationAvailability applicationAvailability) { + return new LivenessStateHealthIndicator(applicationAvailability); + } + + @Bean + @ConditionalOnMissingBean(name = "readinessStateHealthIndicator") + @ConditionalOnProperty(prefix = "management.health.readinessstate", name = "enabled", havingValue = "true") + public ReadinessStateHealthIndicator readinessStateHealthIndicator( + ApplicationAvailability applicationAvailability) { + return new ReadinessStateHealthIndicator(applicationAvailability); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesAutoConfiguration.java new file mode 100644 index 000000000000..99e0e84d1004 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesAutoConfiguration.java @@ -0,0 +1,110 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.availability; + +import org.springframework.boot.actuate.availability.LivenessStateHealthIndicator; +import org.springframework.boot.actuate.availability.ReadinessStateHealthIndicator; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionMessage; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.boot.availability.ApplicationAvailability; +import org.springframework.boot.cloud.CloudPlatform; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.core.type.AnnotatedTypeMetadata; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for availability probes. + * + * @author Brian Clozel + * @author Phillip Webb + * @since 2.3.0 + */ +@Configuration(proxyBeanMethods = false) +@Conditional(AvailabilityProbesAutoConfiguration.ProbesCondition.class) +@AutoConfigureAfter({ AvailabilityHealthContributorAutoConfiguration.class, + ApplicationAvailabilityAutoConfiguration.class }) +public class AvailabilityProbesAutoConfiguration { + + @Bean + @ConditionalOnMissingBean(name = "livenessStateHealthIndicator") + public LivenessStateHealthIndicator livenessStateHealthIndicator(ApplicationAvailability applicationAvailability) { + return new LivenessStateHealthIndicator(applicationAvailability); + } + + @Bean + @ConditionalOnMissingBean(name = "readinessStateHealthIndicator") + public ReadinessStateHealthIndicator readinessStateHealthIndicator( + ApplicationAvailability applicationAvailability) { + return new ReadinessStateHealthIndicator(applicationAvailability); + } + + @Bean + public AvailabilityProbesHealthEndpointGroupsPostProcessor availabilityProbesHealthEndpointGroupsPostProcessor() { + return new AvailabilityProbesHealthEndpointGroupsPostProcessor(); + } + + /** + * {@link SpringBootCondition} to enable or disable probes. + *

+ * Probes are enabled if the dedicated configuration property is enabled or if the + * Kubernetes cloud environment is detected/enforced. + */ + static class ProbesCondition extends SpringBootCondition { + + private static final String ENABLED_PROPERTY = "management.endpoint.health.probes.enabled"; + + private static final String DEPRECATED_ENABLED_PROPERTY = "management.health.probes.enabled"; + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { + Environment environment = context.getEnvironment(); + ConditionMessage.Builder message = ConditionMessage.forCondition("Probes availability"); + ConditionOutcome outcome = onProperty(environment, message, ENABLED_PROPERTY); + if (outcome != null) { + return outcome; + } + outcome = onProperty(environment, message, DEPRECATED_ENABLED_PROPERTY); + if (outcome != null) { + return outcome; + } + if (CloudPlatform.getActive(environment) == CloudPlatform.KUBERNETES) { + return ConditionOutcome.match(message.because("running on Kubernetes")); + } + return ConditionOutcome.noMatch(message.because("not running on a supported cloud platform")); + } + + private ConditionOutcome onProperty(Environment environment, ConditionMessage.Builder message, + String propertyName) { + String enabled = environment.getProperty(propertyName); + if (enabled != null) { + boolean match = !"false".equalsIgnoreCase(enabled); + return new ConditionOutcome(match, message.because("'" + propertyName + "' set to '" + enabled + "'")); + } + return null; + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroup.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroup.java new file mode 100644 index 000000000000..c8a42ca3bbff --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroup.java @@ -0,0 +1,67 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.availability; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.boot.actuate.endpoint.SecurityContext; +import org.springframework.boot.actuate.health.HealthEndpointGroup; +import org.springframework.boot.actuate.health.HttpCodeStatusMapper; +import org.springframework.boot.actuate.health.StatusAggregator; + +/** + * {@link HealthEndpointGroup} used to support availability probes. + * + * @author Phillip Webb + * @author Brian Clozel + */ +class AvailabilityProbesHealthEndpointGroup implements HealthEndpointGroup { + + private final Set members; + + AvailabilityProbesHealthEndpointGroup(String... members) { + this.members = new HashSet<>(Arrays.asList(members)); + } + + @Override + public boolean isMember(String name) { + return this.members.contains(name); + } + + @Override + public boolean showComponents(SecurityContext securityContext) { + return false; + } + + @Override + public boolean showDetails(SecurityContext securityContext) { + return false; + } + + @Override + public StatusAggregator getStatusAggregator() { + return StatusAggregator.getDefault(); + } + + @Override + public HttpCodeStatusMapper getHttpCodeStatusMapper() { + return HttpCodeStatusMapper.DEFAULT; + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroups.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroups.java new file mode 100644 index 000000000000..0da69e7b5e44 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroups.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.availability; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.boot.actuate.health.HealthEndpointGroup; +import org.springframework.boot.actuate.health.HealthEndpointGroups; +import org.springframework.util.Assert; + +/** + * {@link HealthEndpointGroups} decorator to support availability probes. + * + * @author Phillip Webb + * @author Brian Clozel + */ +class AvailabilityProbesHealthEndpointGroups implements HealthEndpointGroups { + + private static final Map GROUPS; + static { + Map groups = new LinkedHashMap<>(); + groups.put("liveness", new AvailabilityProbesHealthEndpointGroup("livenessState")); + groups.put("readiness", new AvailabilityProbesHealthEndpointGroup("readinessState")); + GROUPS = Collections.unmodifiableMap(groups); + } + + private final HealthEndpointGroups groups; + + private final Set names; + + AvailabilityProbesHealthEndpointGroups(HealthEndpointGroups groups) { + Assert.notNull(groups, "Groups must not be null"); + this.groups = groups; + Set names = new LinkedHashSet<>(groups.getNames()); + names.addAll(GROUPS.keySet()); + this.names = Collections.unmodifiableSet(names); + } + + @Override + public HealthEndpointGroup getPrimary() { + return this.groups.getPrimary(); + } + + @Override + public Set getNames() { + return this.names; + } + + @Override + public HealthEndpointGroup get(String name) { + HealthEndpointGroup group = this.groups.get(name); + if (group == null) { + group = GROUPS.get(name); + } + return group; + } + + static boolean containsAllProbeGroups(HealthEndpointGroups groups) { + return groups.getNames().containsAll(GROUPS.keySet()); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroupsPostProcessor.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroupsPostProcessor.java new file mode 100644 index 000000000000..fcd36c99d9f1 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroupsPostProcessor.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.availability; + +import org.springframework.boot.actuate.health.HealthEndpointGroups; +import org.springframework.boot.actuate.health.HealthEndpointGroupsPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +/** + * {@link HealthEndpointGroupsPostProcessor} to add + * {@link AvailabilityProbesHealthEndpointGroups}. + * + * @author Phillip Webb + */ +@Order(Ordered.LOWEST_PRECEDENCE) +class AvailabilityProbesHealthEndpointGroupsPostProcessor implements HealthEndpointGroupsPostProcessor { + + @Override + public HealthEndpointGroups postProcessHealthEndpointGroups(HealthEndpointGroups groups) { + if (AvailabilityProbesHealthEndpointGroups.containsAllProbeGroups(groups)) { + return groups; + } + return new AvailabilityProbesHealthEndpointGroups(groups); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/package-info.java new file mode 100644 index 000000000000..bec5a50f5350 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/availability/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration that extends health endpoints so that they can be used as + * availability probes. + */ +package org.springframework.boot.actuate.autoconfigure.availability; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cassandra/CassandraHealthContributorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cassandra/CassandraHealthContributorAutoConfiguration.java index cf4f87aa89b5..23602f3ccb12 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cassandra/CassandraHealthContributorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cassandra/CassandraHealthContributorAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,46 +16,35 @@ package org.springframework.boot.actuate.autoconfigure.cassandra; -import java.util.Map; +import com.datastax.oss.driver.api.core.CqlSession; -import com.datastax.driver.core.Cluster; - -import org.springframework.boot.actuate.autoconfigure.health.CompositeHealthContributorConfiguration; +import org.springframework.boot.actuate.autoconfigure.cassandra.CassandraHealthContributorConfigurations.CassandraDriverConfiguration; import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; -import org.springframework.boot.actuate.cassandra.CassandraHealthIndicator; -import org.springframework.boot.actuate.health.HealthContributor; +import org.springframework.boot.actuate.cassandra.CassandraDriverHealthIndicator; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; -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.data.cassandra.CassandraDataAutoConfiguration; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.cassandra.core.CassandraOperations; +import org.springframework.context.annotation.Import; /** * {@link EnableAutoConfiguration Auto-configuration} for - * {@link CassandraHealthIndicator}. + * {@link CassandraDriverHealthIndicator}. * * @author Julien Dubois * @author Stephane Nicoll * @since 2.1.0 */ @Configuration(proxyBeanMethods = false) -@ConditionalOnClass({ Cluster.class, CassandraOperations.class }) -@ConditionalOnBean(CassandraOperations.class) +@ConditionalOnClass(CqlSession.class) @ConditionalOnEnabledHealthIndicator("cassandra") @AutoConfigureAfter({ CassandraAutoConfiguration.class, CassandraDataAutoConfiguration.class, CassandraReactiveHealthContributorAutoConfiguration.class }) -public class CassandraHealthContributorAutoConfiguration - extends CompositeHealthContributorConfiguration { - - @Bean - @ConditionalOnMissingBean(name = { "cassandraHealthIndicator", "cassandraHealthContributor" }) - public HealthContributor cassandraHealthContributor(Map cassandraOperations) { - return createContributor(cassandraOperations); - } +@Import({ CassandraDriverConfiguration.class, + CassandraHealthContributorConfigurations.CassandraOperationsConfiguration.class }) +@SuppressWarnings("deprecation") +public class CassandraHealthContributorAutoConfiguration { } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cassandra/CassandraHealthContributorConfigurations.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cassandra/CassandraHealthContributorConfigurations.java new file mode 100644 index 000000000000..b3ff97fe0e69 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cassandra/CassandraHealthContributorConfigurations.java @@ -0,0 +1,101 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.cassandra; + +import java.util.Map; + +import com.datastax.oss.driver.api.core.CqlSession; + +import org.springframework.boot.actuate.autoconfigure.health.CompositeHealthContributorConfiguration; +import org.springframework.boot.actuate.autoconfigure.health.CompositeReactiveHealthContributorConfiguration; +import org.springframework.boot.actuate.cassandra.CassandraDriverHealthIndicator; +import org.springframework.boot.actuate.cassandra.CassandraDriverReactiveHealthIndicator; +import org.springframework.boot.actuate.health.HealthContributor; +import org.springframework.boot.actuate.health.ReactiveHealthContributor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.cassandra.core.CassandraOperations; +import org.springframework.data.cassandra.core.ReactiveCassandraOperations; + +/** + * Health contributor options for Cassandra. + * + * @author Stephane Nicoll + */ +class CassandraHealthContributorConfigurations { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnBean(CqlSession.class) + static class CassandraDriverConfiguration + extends CompositeHealthContributorConfiguration { + + @Bean + @ConditionalOnMissingBean(name = { "cassandraHealthIndicator", "cassandraHealthContributor" }) + HealthContributor cassandraHealthContributor(Map sessions) { + return createContributor(sessions); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(CassandraOperations.class) + @ConditionalOnBean(CassandraOperations.class) + @Deprecated + static class CassandraOperationsConfiguration extends + CompositeHealthContributorConfiguration { + + @Bean + @ConditionalOnMissingBean(name = { "cassandraHealthIndicator", "cassandraHealthContributor" }) + HealthContributor cassandraHealthContributor(Map cassandraOperations) { + return createContributor(cassandraOperations); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnBean(CqlSession.class) + static class CassandraReactiveDriverConfiguration extends + CompositeReactiveHealthContributorConfiguration { + + @Bean + @ConditionalOnMissingBean(name = { "cassandraHealthIndicator", "cassandraHealthContributor" }) + ReactiveHealthContributor cassandraHealthContributor(Map sessions) { + return createContributor(sessions); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(ReactiveCassandraOperations.class) + @ConditionalOnBean(ReactiveCassandraOperations.class) + @Deprecated + static class CassandraReactiveOperationsConfiguration extends + CompositeReactiveHealthContributorConfiguration { + + @Bean + @ConditionalOnMissingBean(name = { "cassandraHealthIndicator", "cassandraHealthContributor" }) + ReactiveHealthContributor cassandraHealthContributor( + Map reactiveCassandraOperations) { + return createContributor(reactiveCassandraOperations); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cassandra/CassandraReactiveHealthContributorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cassandra/CassandraReactiveHealthContributorAutoConfiguration.java index 45ba39a5b0e2..c3f9e90cc67f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cassandra/CassandraReactiveHealthContributorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cassandra/CassandraReactiveHealthContributorAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,48 +13,37 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.boot.actuate.autoconfigure.cassandra; -import java.util.Map; +package org.springframework.boot.actuate.autoconfigure.cassandra; -import com.datastax.driver.core.Cluster; +import com.datastax.oss.driver.api.core.CqlSession; import reactor.core.publisher.Flux; -import org.springframework.boot.actuate.autoconfigure.health.CompositeReactiveHealthContributorConfiguration; +import org.springframework.boot.actuate.autoconfigure.cassandra.CassandraHealthContributorConfigurations.CassandraReactiveDriverConfiguration; import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; -import org.springframework.boot.actuate.cassandra.CassandraReactiveHealthIndicator; -import org.springframework.boot.actuate.health.ReactiveHealthContributor; +import org.springframework.boot.actuate.cassandra.CassandraDriverReactiveHealthIndicator; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -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.data.cassandra.CassandraReactiveDataAutoConfiguration; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.cassandra.core.ReactiveCassandraOperations; +import org.springframework.context.annotation.Import; /** * {@link EnableAutoConfiguration Auto-configuration} for - * {@link CassandraReactiveHealthIndicator}. + * {@link CassandraDriverReactiveHealthIndicator}. * * @author Artsiom Yudovin * @author Stephane Nicoll * @since 2.1.0 */ @Configuration(proxyBeanMethods = false) -@ConditionalOnClass({ Cluster.class, ReactiveCassandraOperations.class, Flux.class }) -@ConditionalOnBean(ReactiveCassandraOperations.class) +@ConditionalOnClass({ CqlSession.class, Flux.class }) @ConditionalOnEnabledHealthIndicator("cassandra") @AutoConfigureAfter(CassandraReactiveDataAutoConfiguration.class) -public class CassandraReactiveHealthContributorAutoConfiguration extends - CompositeReactiveHealthContributorConfiguration { - - @Bean - @ConditionalOnMissingBean(name = { "cassandraHealthIndicator", "cassandraHealthContributor" }) - public ReactiveHealthContributor cassandraHealthContributor( - Map reactiveCassandraOperations) { - return createContributor(reactiveCassandraOperations); - } +@Import({ CassandraReactiveDriverConfiguration.class, + CassandraHealthContributorConfigurations.CassandraReactiveOperationsConfiguration.class }) +@SuppressWarnings("deprecation") +public class CassandraReactiveHealthContributorAutoConfiguration { } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/AccessLevel.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/AccessLevel.java index b609dda14806..7c23ba93d219 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/AccessLevel.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/AccessLevel.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,6 +38,9 @@ public enum AccessLevel { */ FULL; + /** + * The request attribute used to store the {@link AccessLevel}. + */ public static final String REQUEST_ATTRIBUTE = "cloudFoundryAccessLevel"; private final List ids; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryAuthorizationException.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryAuthorizationException.java index 93f33f1b06a3..48525ee8a55a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryAuthorizationException.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryAuthorizationException.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -58,24 +58,54 @@ public Reason getReason() { */ public enum Reason { + /** + * Access Denied. + */ ACCESS_DENIED(HttpStatus.FORBIDDEN), + /** + * Invalid Audience. + */ INVALID_AUDIENCE(HttpStatus.UNAUTHORIZED), + /** + * Invalid Issuer. + */ INVALID_ISSUER(HttpStatus.UNAUTHORIZED), + /** + * Invalid Key ID. + */ INVALID_KEY_ID(HttpStatus.UNAUTHORIZED), + /** + * Invalid Signature. + */ INVALID_SIGNATURE(HttpStatus.UNAUTHORIZED), + /** + * Invalid Token. + */ INVALID_TOKEN(HttpStatus.UNAUTHORIZED), + /** + * Missing Authorization. + */ MISSING_AUTHORIZATION(HttpStatus.UNAUTHORIZED), + /** + * Token Expired. + */ TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED), + /** + * Unsupported Token Signing Algorithm. + */ UNSUPPORTED_TOKEN_SIGNING_ALGORITHM(HttpStatus.UNAUTHORIZED), + /** + * Service Unavailable. + */ SERVICE_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE); private final HttpStatus status; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscoverer.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscoverer.java index 7f5105895640..a01fe72a9246 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscoverer.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscoverer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -58,21 +58,21 @@ public CloudFoundryWebEndpointDiscoverer(ApplicationContext applicationContext, } @Override - protected boolean isExtensionExposed(Object extensionBean) { - if (isHealthEndpointExtension(extensionBean) && !isCloudFoundryHealthEndpointExtension(extensionBean)) { + protected boolean isExtensionTypeExposed(Class extensionBeanType) { + if (isHealthEndpointExtension(extensionBeanType) && !isCloudFoundryHealthEndpointExtension(extensionBeanType)) { // Filter regular health endpoint extensions so a CF version can replace them return false; } return true; } - private boolean isHealthEndpointExtension(Object extensionBean) { - return MergedAnnotations.from(extensionBean.getClass()).get(EndpointWebExtension.class) + private boolean isHealthEndpointExtension(Class extensionBeanType) { + return MergedAnnotations.from(extensionBeanType).get(EndpointWebExtension.class) .getValue("endpoint", Class.class).map(HealthEndpoint.class::isAssignableFrom).orElse(false); } - private boolean isCloudFoundryHealthEndpointExtension(Object extensionBean) { - return MergedAnnotations.from(extensionBean.getClass()).isPresent(EndpointCloudFoundryExtension.class); + private boolean isCloudFoundryHealthEndpointExtension(Class extensionBeanType) { + return MergedAnnotations.from(extensionBeanType).isPresent(EndpointCloudFoundryExtension.class); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryReactiveHealthEndpointWebExtension.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryReactiveHealthEndpointWebExtension.java index 589f952bc770..67f1708b5175 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryReactiveHealthEndpointWebExtension.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryReactiveHealthEndpointWebExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,12 +19,12 @@ import reactor.core.publisher.Mono; import org.springframework.boot.actuate.autoconfigure.cloudfoundry.EndpointCloudFoundryExtension; +import org.springframework.boot.actuate.endpoint.ApiVersion; import org.springframework.boot.actuate.endpoint.SecurityContext; import org.springframework.boot.actuate.endpoint.annotation.EndpointExtension; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.Selector; import org.springframework.boot.actuate.endpoint.annotation.Selector.Match; -import org.springframework.boot.actuate.endpoint.http.ApiVersion; import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; import org.springframework.boot.actuate.health.HealthComponent; import org.springframework.boot.actuate.health.HealthEndpoint; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityService.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityService.java index bb82d7c2038a..7f9a83b3f15e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityService.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityService.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,10 +20,10 @@ import java.util.List; import java.util.Map; -import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.SslProvider; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import reactor.core.publisher.Mono; +import reactor.netty.http.Http11SslContextSpec; import reactor.netty.http.client.HttpClient; import org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel; @@ -66,14 +66,13 @@ class ReactiveCloudFoundrySecurityService { } protected ReactorClientHttpConnector buildTrustAllSslConnector() { - HttpClient client = HttpClient.create() - .secure((sslContextSpec) -> sslContextSpec.sslContext(createSslContext())); + HttpClient client = HttpClient.create().secure((spec) -> spec.sslContext(createSslContextSpec())); return new ReactorClientHttpConnector(client); } - private SslContextBuilder createSslContext() { - return SslContextBuilder.forClient().sslProvider(SslProvider.JDK) - .trustManager(InsecureTrustManagerFactory.INSTANCE); + private Http11SslContextSpec createSslContextSpec() { + return Http11SslContextSpec.forClient().configure( + (builder) -> builder.sslProvider(SslProvider.JDK).trustManager(InsecureTrustManagerFactory.INSTANCE)); } /** diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration.java index a0950f72441b..04f24deeb771 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -64,6 +64,7 @@ import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.WebSecurityConfigurer; import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.servlet.DispatcherServlet; @@ -158,18 +159,23 @@ private CorsConfiguration getCorsConfiguration() { * specific paths. The Cloud foundry endpoints are protected by their own security * interceptor. */ - @ConditionalOnClass(WebSecurity.class) - @Order(SecurityProperties.IGNORED_ORDER) + @ConditionalOnClass({ WebSecurityCustomizer.class, WebSecurity.class }) @Configuration(proxyBeanMethods = false) - public static class IgnoredPathsWebSecurityConfigurer implements WebSecurityConfigurer { + public static class IgnoredCloudFoundryPathsWebSecurityConfiguration { - @Override - public void init(WebSecurity builder) throws Exception { - builder.ignoring().requestMatchers(new AntPathRequestMatcher("/cloudfoundryapplication/**")); + @Bean + IgnoredCloudFoundryPathsWebSecurityCustomizer ignoreCloudFoundryPathsWebSecurityCustomizer() { + return new IgnoredCloudFoundryPathsWebSecurityCustomizer(); } + } + + @Order(SecurityProperties.IGNORED_ORDER) + static class IgnoredCloudFoundryPathsWebSecurityCustomizer implements WebSecurityCustomizer { + @Override - public void configure(WebSecurity builder) throws Exception { + public void customize(WebSecurity web) { + web.ignoring().requestMatchers(new AntPathRequestMatcher("/cloudfoundryapplication/**")); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryHealthEndpointWebExtension.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryHealthEndpointWebExtension.java index e91a1fbe4b54..7ec0b9772e97 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryHealthEndpointWebExtension.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryHealthEndpointWebExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,12 +17,12 @@ package org.springframework.boot.actuate.autoconfigure.cloudfoundry.servlet; import org.springframework.boot.actuate.autoconfigure.cloudfoundry.EndpointCloudFoundryExtension; +import org.springframework.boot.actuate.endpoint.ApiVersion; import org.springframework.boot.actuate.endpoint.SecurityContext; import org.springframework.boot.actuate.endpoint.annotation.EndpointExtension; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.Selector; import org.springframework.boot.actuate.endpoint.annotation.Selector.Match; -import org.springframework.boot.actuate.endpoint.http.ApiVersion; import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; import org.springframework.boot.actuate.health.HealthComponent; import org.springframework.boot.actuate.health.HealthEndpoint; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryInfoEndpointWebExtension.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryInfoEndpointWebExtension.java index 73b2bbd968dc..9d3502d48e3e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryInfoEndpointWebExtension.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryInfoEndpointWebExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.actuate.autoconfigure.cloudfoundry.servlet; import java.util.Map; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryWebEndpointServletHandlerMapping.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryWebEndpointServletHandlerMapping.java index bb2331e7bc22..472535cf6b29 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryWebEndpointServletHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryWebEndpointServletHandlerMapping.java @@ -25,6 +25,9 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + import org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel; import org.springframework.boot.actuate.autoconfigure.cloudfoundry.SecurityResponse; import org.springframework.boot.actuate.endpoint.EndpointId; @@ -51,6 +54,8 @@ */ class CloudFoundryWebEndpointServletHandlerMapping extends AbstractWebMvcEndpointHandlerMapping { + private static final Log logger = LogFactory.getLog(CloudFoundryWebEndpointServletHandlerMapping.class); + private final CloudFoundrySecurityInterceptor securityInterceptor; private final EndpointLinksResolver linksResolver; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/SkipSslVerificationHttpRequestFactory.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/SkipSslVerificationHttpRequestFactory.java index 1d40a1d0f352..c613c16a2ee9 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/SkipSslVerificationHttpRequestFactory.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/SkipSslVerificationHttpRequestFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -62,7 +62,7 @@ private SSLSocketFactory createSslSocketFactory() throws Exception { return context.getSocketFactory(); } - private class SkipHostnameVerifier implements HostnameVerifier { + private static class SkipHostnameVerifier implements HostnameVerifier { @Override public boolean verify(String s, SSLSession sslSession) { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointAutoConfiguration.java index d9fe6c792fef..4225492220c4 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,9 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint; +import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpointWebExtension; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -30,6 +32,7 @@ * * @author Phillip Webb * @author Stephane Nicoll + * @author Chris Bono * @since 2.0.0 */ @Configuration(proxyBeanMethods = false) @@ -46,7 +49,19 @@ public ConfigurationPropertiesReportEndpoint configurationPropertiesReportEndpoi if (keysToSanitize != null) { endpoint.setKeysToSanitize(keysToSanitize); } + String[] additionalKeysToSanitize = properties.getAdditionalKeysToSanitize(); + if (additionalKeysToSanitize != null) { + endpoint.keysToSanitize(additionalKeysToSanitize); + } return endpoint; } + @Bean + @ConditionalOnMissingBean + @ConditionalOnBean(ConfigurationPropertiesReportEndpoint.class) + public ConfigurationPropertiesReportEndpointWebExtension configurationPropertiesReportEndpointWebExtension( + ConfigurationPropertiesReportEndpoint configurationPropertiesReportEndpoint) { + return new ConfigurationPropertiesReportEndpointWebExtension(configurationPropertiesReportEndpoint); + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointProperties.java index 7170f64ab242..d4a2b1383583 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,12 @@ public class ConfigurationPropertiesReportEndpointProperties { */ private String[] keysToSanitize; + /** + * Keys that should be sanitized in addition to those already configured. Keys can be + * simple strings that the property ends with or regular expressions. + */ + private String[] additionalKeysToSanitize; + public String[] getKeysToSanitize() { return this.keysToSanitize; } @@ -42,4 +48,12 @@ public void setKeysToSanitize(String[] keysToSanitize) { this.keysToSanitize = keysToSanitize; } + public String[] getAdditionalKeysToSanitize() { + return this.additionalKeysToSanitize; + } + + public void setAdditionalKeysToSanitize(String[] additionalKeysToSanitize) { + this.additionalKeysToSanitize = additionalKeysToSanitize; + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/couchbase/CouchbaseHealthContributorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/couchbase/CouchbaseHealthContributorAutoConfiguration.java index ede9c1353c0d..dd0f5b4d9008 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/couchbase/CouchbaseHealthContributorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/couchbase/CouchbaseHealthContributorAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.actuate.autoconfigure.couchbase; import java.util.Map; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/couchbase/CouchbaseReactiveHealthContributorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/couchbase/CouchbaseReactiveHealthContributorAutoConfiguration.java index 9343848a5386..7429505cdcfd 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/couchbase/CouchbaseReactiveHealthContributorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/couchbase/CouchbaseReactiveHealthContributorAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.actuate.autoconfigure.couchbase; import java.util.Map; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticSearchClientHealthContributorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticSearchClientHealthContributorAutoConfiguration.java deleted file mode 100644 index 8778925eab3c..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticSearchClientHealthContributorAutoConfiguration.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.autoconfigure.elasticsearch; - -import java.time.Duration; -import java.util.Map; - -import org.elasticsearch.client.Client; - -import org.springframework.boot.actuate.autoconfigure.health.CompositeHealthContributorConfiguration; -import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; -import org.springframework.boot.actuate.elasticsearch.ElasticsearchHealthIndicator; -import org.springframework.boot.actuate.health.HealthContributor; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -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.data.elasticsearch.ElasticsearchAutoConfiguration; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * {@link EnableAutoConfiguration Auto-configuration} for - * {@link ElasticsearchHealthIndicator} using the Elasticsearch {@link Client}. - * - * @author Stephane Nicoll - * @since 2.1.0 - * @deprecated since 2.2.0 as {@literal org.elasticsearch.client:transport} has been - * deprecated upstream - */ -@Configuration(proxyBeanMethods = false) -@ConditionalOnClass(Client.class) -@ConditionalOnBean(Client.class) -@ConditionalOnEnabledHealthIndicator("elasticsearch") -@AutoConfigureAfter(ElasticsearchAutoConfiguration.class) -@EnableConfigurationProperties(ElasticsearchHealthIndicatorProperties.class) -@Deprecated -public class ElasticSearchClientHealthContributorAutoConfiguration - extends CompositeHealthContributorConfiguration { - - private final ElasticsearchHealthIndicatorProperties properties; - - public ElasticSearchClientHealthContributorAutoConfiguration(ElasticsearchHealthIndicatorProperties properties) { - this.properties = properties; - } - - @Bean - @ConditionalOnMissingBean(name = { "elasticsearchHealthIndicator", "elasticsearchHealthContributor" }) - public HealthContributor elasticsearchHealthContributor(Map clients) { - return createContributor(clients); - } - - @Override - protected ElasticsearchHealthIndicator createIndicator(Client client) { - Duration responseTimeout = this.properties.getResponseTimeout(); - return new ElasticsearchHealthIndicator(client, (responseTimeout != null) ? responseTimeout.toMillis() : 100, - this.properties.getIndices()); - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticSearchJestHealthContributorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticSearchJestHealthContributorAutoConfiguration.java deleted file mode 100644 index 62aa0a9e9f39..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticSearchJestHealthContributorAutoConfiguration.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.autoconfigure.elasticsearch; - -import java.util.Map; - -import io.searchbox.client.JestClient; - -import org.springframework.boot.actuate.autoconfigure.health.CompositeHealthContributorConfiguration; -import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; -import org.springframework.boot.actuate.elasticsearch.ElasticsearchHealthIndicator; -import org.springframework.boot.actuate.elasticsearch.ElasticsearchJestHealthIndicator; -import org.springframework.boot.actuate.health.HealthContributor; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -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.elasticsearch.jest.JestAutoConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * {@link EnableAutoConfiguration Auto-configuration} for - * {@link ElasticsearchHealthIndicator} using the {@link JestClient}. - * - * @author Stephane Nicoll - * @since 2.1.0 - */ -@Configuration(proxyBeanMethods = false) -@ConditionalOnClass(JestClient.class) -@ConditionalOnBean(JestClient.class) -@ConditionalOnEnabledHealthIndicator("elasticsearch") -@AutoConfigureAfter({ JestAutoConfiguration.class, ElasticSearchClientHealthContributorAutoConfiguration.class }) -@Deprecated -public class ElasticSearchJestHealthContributorAutoConfiguration - extends CompositeHealthContributorConfiguration { - - @Bean - @ConditionalOnMissingBean(name = { "elasticsearchHealthIndicator", "elasticsearchHealthContributor" }) - public HealthContributor elasticsearchHealthContributor(Map clients) { - return createContributor(clients); - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticSearchReactiveHealthContributorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticSearchReactiveHealthContributorAutoConfiguration.java new file mode 100644 index 000000000000..91ebf36ef3c6 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticSearchReactiveHealthContributorAutoConfiguration.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.elasticsearch; + +import java.util.Map; + +import reactor.core.publisher.Flux; + +import org.springframework.boot.actuate.autoconfigure.health.CompositeReactiveHealthContributorConfiguration; +import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; +import org.springframework.boot.actuate.elasticsearch.ElasticsearchReactiveHealthIndicator; +import org.springframework.boot.actuate.health.ReactiveHealthContributor; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +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.data.elasticsearch.ReactiveElasticsearchRestClientAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for + * {@link ElasticsearchReactiveHealthIndicator} using the + * {@link ReactiveElasticsearchClient}. + * + * @author Aleksander Lech + * @since 2.3.2 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass({ ReactiveElasticsearchClient.class, Flux.class }) +@ConditionalOnBean(ReactiveElasticsearchClient.class) +@ConditionalOnEnabledHealthIndicator("elasticsearch") +@AutoConfigureAfter(ReactiveElasticsearchRestClientAutoConfiguration.class) +public class ElasticSearchReactiveHealthContributorAutoConfiguration extends + CompositeReactiveHealthContributorConfiguration { + + @Bean + @ConditionalOnMissingBean(name = { "elasticsearchHealthIndicator", "elasticsearchHealthContributor" }) + public ReactiveHealthContributor elasticsearchHealthContributor(Map clients) { + return createContributor(clients); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticSearchRestHealthContributorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticSearchRestHealthContributorAutoConfiguration.java index 2ce57ad27a7a..0fe95723dc24 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticSearchRestHealthContributorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticSearchRestHealthContributorAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.util.Map; import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestHighLevelClient; import org.springframework.boot.actuate.autoconfigure.health.CompositeHealthContributorConfiguration; import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; @@ -29,7 +30,7 @@ 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.elasticsearch.rest.RestClientAutoConfiguration; +import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -41,17 +42,16 @@ * @since 2.1.1 */ @Configuration(proxyBeanMethods = false) -@ConditionalOnClass(RestClient.class) -@ConditionalOnBean(RestClient.class) +@ConditionalOnClass(RestHighLevelClient.class) +@ConditionalOnBean(RestHighLevelClient.class) @ConditionalOnEnabledHealthIndicator("elasticsearch") -@AutoConfigureAfter({ RestClientAutoConfiguration.class, ElasticSearchClientHealthContributorAutoConfiguration.class }) -@SuppressWarnings("deprecation") +@AutoConfigureAfter(ElasticsearchRestClientAutoConfiguration.class) public class ElasticSearchRestHealthContributorAutoConfiguration - extends CompositeHealthContributorConfiguration { + extends CompositeHealthContributorConfiguration { @Bean - @ConditionalOnMissingBean(name = { "elasticsearchRestHealthIndicator", "elasticsearchRestHealthContributor" }) - public HealthContributor elasticsearchRestHealthContributor(Map clients) { + @ConditionalOnMissingBean(name = { "elasticsearchHealthIndicator", "elasticsearchHealthContributor" }) + public HealthContributor elasticsearchHealthContributor(Map clients) { return createContributor(clients); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticsearchHealthIndicatorProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticsearchHealthIndicatorProperties.java deleted file mode 100644 index 93863da4abc2..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticsearchHealthIndicatorProperties.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.autoconfigure.elasticsearch; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; - -import org.springframework.boot.actuate.elasticsearch.ElasticsearchHealthIndicator; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; - -/** - * External configuration properties for {@link ElasticsearchHealthIndicator}. - * - * @author Binwei Yang - * @author Andy Wilkinson - * @since 2.0.0 - * @deprecated since 2.2.0 as {@literal org.elasticsearch.client:transport} has been - * deprecated upstream - */ -@ConfigurationProperties(prefix = "management.health.elasticsearch", ignoreUnknownFields = false) -@Deprecated -public class ElasticsearchHealthIndicatorProperties { - - /** - * Comma-separated index names. - */ - private List indices = new ArrayList<>(); - - /** - * Time to wait for a response from the cluster. - */ - private Duration responseTimeout = Duration.ofMillis(100); - - @DeprecatedConfigurationProperty(reason = "Upstream elasticsearch transport is deprected") - public List getIndices() { - return this.indices; - } - - public void setIndices(List indices) { - this.indices = indices; - } - - @DeprecatedConfigurationProperty(reason = "Upstream elasticsearch transport is deprected") - public Duration getResponseTimeout() { - return this.responseTimeout; - } - - public void setResponseTimeout(Duration responseTimeout) { - this.responseTimeout = responseTimeout; - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.java index d32af3609f98..36bd3bc6d18e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/ExposeExcludePropertyEndpointFilter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/ExposeExcludePropertyEndpointFilter.java deleted file mode 100644 index e9786a232394..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/ExposeExcludePropertyEndpointFilter.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.autoconfigure.endpoint; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import java.util.Set; -import java.util.stream.Collectors; - -import org.springframework.boot.actuate.endpoint.EndpointFilter; -import org.springframework.boot.actuate.endpoint.EndpointId; -import org.springframework.boot.actuate.endpoint.ExposableEndpoint; -import org.springframework.boot.context.properties.bind.Bindable; -import org.springframework.boot.context.properties.bind.Binder; -import org.springframework.core.env.Environment; -import org.springframework.util.Assert; - -/** - * {@link EndpointFilter} that will filter endpoints based on {@code include} and - * {@code exclude} properties. - * - * @param the endpoint type - * @author Phillip Webb - * @since 2.0.0 - */ -public class ExposeExcludePropertyEndpointFilter> implements EndpointFilter { - - private final Class endpointType; - - private final Set include; - - private final Set exclude; - - private final Set exposeDefaults; - - public ExposeExcludePropertyEndpointFilter(Class endpointType, Environment environment, String prefix, - String... exposeDefaults) { - Assert.notNull(endpointType, "EndpointType must not be null"); - Assert.notNull(environment, "Environment must not be null"); - Assert.hasText(prefix, "Prefix must not be empty"); - Binder binder = Binder.get(environment); - this.endpointType = endpointType; - this.include = bind(binder, prefix + ".include"); - this.exclude = bind(binder, prefix + ".exclude"); - this.exposeDefaults = asSet(Arrays.asList(exposeDefaults)); - } - - public ExposeExcludePropertyEndpointFilter(Class endpointType, Collection include, - Collection exclude, String... exposeDefaults) { - Assert.notNull(endpointType, "EndpointType Type must not be null"); - this.endpointType = endpointType; - this.include = asSet(include); - this.exclude = asSet(exclude); - this.exposeDefaults = asSet(Arrays.asList(exposeDefaults)); - } - - private Set bind(Binder binder, String name) { - return asSet(binder.bind(name, Bindable.listOf(String.class)).map(this::cleanup).orElseGet(ArrayList::new)); - } - - private List cleanup(List values) { - return values.stream().map(this::cleanup).collect(Collectors.toList()); - } - - private String cleanup(String value) { - return "*".equals(value) ? "*" : EndpointId.fromPropertyValue(value).toLowerCaseString(); - } - - private Set asSet(Collection items) { - if (items == null) { - return Collections.emptySet(); - } - return items.stream().map((item) -> item.toLowerCase(Locale.ENGLISH)).collect(Collectors.toSet()); - } - - @Override - public boolean match(E endpoint) { - if (this.endpointType.isInstance(endpoint)) { - return isExposed(endpoint) && !isExcluded(endpoint); - } - return true; - } - - private boolean isExposed(ExposableEndpoint endpoint) { - if (this.include.isEmpty()) { - return this.exposeDefaults.contains("*") || contains(this.exposeDefaults, endpoint); - } - return this.include.contains("*") || contains(this.include, endpoint); - } - - private boolean isExcluded(ExposableEndpoint endpoint) { - if (this.exclude.isEmpty()) { - return false; - } - return this.exclude.contains("*") || contains(this.exclude, endpoint); - } - - private boolean contains(Set items, ExposableEndpoint endpoint) { - return items.contains(endpoint.getEndpointId().toLowerCaseString()); - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnEnabledEndpoint.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnEnabledEndpoint.java deleted file mode 100644 index 8dc4bdb5611a..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnEnabledEndpoint.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.autoconfigure.endpoint.condition; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import org.springframework.boot.actuate.endpoint.annotation.Endpoint; -import org.springframework.boot.actuate.endpoint.annotation.EndpointExtension; -import org.springframework.context.annotation.Conditional; -import org.springframework.core.env.Environment; - -/** - * {@link Conditional @Conditional} that checks whether an endpoint is enabled or not. - * Matches according to the endpoints specific {@link Environment} property, falling back - * to {@code management.endpoints.enabled-by-default} or failing that - * {@link Endpoint#enableByDefault()}. - *

- * When placed on a {@code @Bean} method, the endpoint defaults to the return type of the - * factory method: - * - *

- * @Configuration
- * public class MyConfiguration {
- *
- *     @ConditionalOnEnableEndpoint
- *     @Bean
- *     public MyEndpoint myEndpoint() {
- *         ...
- *     }
- *
- * }
- *

- * It is also possible to use the same mechanism for extensions: - * - *

- * @Configuration
- * public class MyConfiguration {
- *
- *     @ConditionalOnEnableEndpoint
- *     @Bean
- *     public MyEndpointWebExtension myEndpointWebExtension() {
- *         ...
- *     }
- *
- * }
- *

- * In the sample above, {@code MyEndpointWebExtension} will be created if the endpoint is - * enabled as defined by the rules above. {@code MyEndpointWebExtension} must be a regular - * extension that refers to an endpoint, something like: - * - *

- * @EndpointWebExtension(endpoint = MyEndpoint.class)
- * public class MyEndpointWebExtension {
- *
- * }
- *

- * Alternatively, the target endpoint can be manually specified for components that should - * only be created when a given endpoint is enabled: - * - *

- * @Configuration
- * public class MyConfiguration {
- *
- *     @ConditionalOnEnableEndpoint(endpoint = MyEndpoint.class)
- *     @Bean
- *     public MyComponent myComponent() {
- *         ...
- *     }
- *
- * }
- * - * @author Stephane Nicoll - * @since 2.0.0 - * @see Endpoint - * @deprecated as of 2.2.0 in favor of - * {@link ConditionalOnAvailableEndpoint @ConditionalOnAvailableEndpoint} - */ -@Retention(RetentionPolicy.RUNTIME) -@Target({ ElementType.METHOD, ElementType.TYPE }) -@Documented -@Conditional(OnEnabledEndpointCondition.class) -@Deprecated -public @interface ConditionalOnEnabledEndpoint { - - /** - * The endpoint type that should be checked. Inferred when the return type of the - * {@code @Bean} method is either an {@link Endpoint @Endpoint} or an - * {@link EndpointExtension @EndpointExtension}. - * @return the endpoint type to check - * @since 2.0.6 - */ - Class endpoint() default Void.class; - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/OnAvailableEndpointCondition.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/OnAvailableEndpointCondition.java index b13faff3d783..adc73887f089 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/OnAvailableEndpointCondition.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/OnAvailableEndpointCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,20 +16,18 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.condition; -import java.util.Arrays; -import java.util.Collections; import java.util.HashSet; -import java.util.List; +import java.util.Map; import java.util.Set; +import org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExcludeEndpointFilter; +import org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExcludeEndpointFilter.DefaultIncludes; import org.springframework.boot.actuate.endpoint.EndpointId; +import org.springframework.boot.actuate.endpoint.ExposableEndpoint; import org.springframework.boot.autoconfigure.condition.ConditionMessage; import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.cloud.CloudPlatform; -import org.springframework.boot.context.properties.bind.Bindable; -import org.springframework.boot.context.properties.bind.Binder; import org.springframework.context.annotation.ConditionContext; -import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.env.Environment; import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.util.ConcurrentReferenceHashMap; @@ -39,13 +37,14 @@ * * @author Brian Clozel * @author Stephane Nicoll + * @author Phillip Webb * @see ConditionalOnAvailableEndpoint */ class OnAvailableEndpointCondition extends AbstractEndpointCondition { private static final String JMX_ENABLED_KEY = "spring.jmx.enabled"; - private static final ConcurrentReferenceHashMap> endpointExposureCache = new ConcurrentReferenceHashMap<>(); + private static final Map> exposuresCache = new ConcurrentReferenceHashMap<>(); @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { @@ -60,79 +59,51 @@ public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeM return new ConditionOutcome(true, message.andCondition(ConditionalOnAvailableEndpoint.class) .because("application is running on Cloud Foundry")); } - AnnotationAttributes attributes = getEndpointAttributes(ConditionalOnAvailableEndpoint.class, context, - metadata); - EndpointId id = EndpointId.of(environment, attributes.getString("id")); - Set exposureInformations = getExposureInformation(environment); - for (ExposureInformation exposureInformation : exposureInformations) { - if (exposureInformation.isExposed(id)) { + EndpointId id = EndpointId.of(environment, + getEndpointAttributes(ConditionalOnAvailableEndpoint.class, context, metadata).getString("id")); + Set exposures = getExposures(environment); + for (Exposure exposure : exposures) { + if (exposure.isExposed(id)) { return new ConditionOutcome(true, message.andCondition(ConditionalOnAvailableEndpoint.class) - .because("marked as exposed by a 'management.endpoints." - + exposureInformation.getPrefix() + ".exposure' property")); + .because("marked as exposed by a 'management.endpoints." + exposure.getPrefix() + + ".exposure' property")); } } return new ConditionOutcome(false, message.andCondition(ConditionalOnAvailableEndpoint.class) .because("no 'management.endpoints' property marked it as exposed")); } - private Set getExposureInformation(Environment environment) { - Set exposureInformations = endpointExposureCache.get(environment); - if (exposureInformations == null) { - exposureInformations = new HashSet<>(2); - Binder binder = Binder.get(environment); + private Set getExposures(Environment environment) { + Set exposures = exposuresCache.get(environment); + if (exposures == null) { + exposures = new HashSet<>(2); if (environment.getProperty(JMX_ENABLED_KEY, Boolean.class, false)) { - exposureInformations.add(new ExposureInformation(binder, "jmx", "*")); + exposures.add(new Exposure(environment, "jmx", DefaultIncludes.JMX)); } - exposureInformations.add(new ExposureInformation(binder, "web", "info", "health")); - endpointExposureCache.put(environment, exposureInformations); + exposures.add(new Exposure(environment, "web", DefaultIncludes.WEB)); + exposuresCache.put(environment, exposures); } - return exposureInformations; + return exposures; } - static class ExposureInformation { + static class Exposure extends IncludeExcludeEndpointFilter> { private final String prefix; - private final Set include; - - private final Set exclude; - - private final Set exposeDefaults; - - ExposureInformation(Binder binder, String prefix, String... exposeDefaults) { + @SuppressWarnings({ "rawtypes", "unchecked" }) + Exposure(Environment environment, String prefix, DefaultIncludes defaultIncludes) { + super((Class) ExposableEndpoint.class, environment, "management.endpoints." + prefix + ".exposure", + defaultIncludes); this.prefix = prefix; - this.include = bind(binder, "management.endpoints." + prefix + ".exposure.include"); - this.exclude = bind(binder, "management.endpoints." + prefix + ".exposure.exclude"); - this.exposeDefaults = new HashSet<>(Arrays.asList(exposeDefaults)); - } - - private Set bind(Binder binder, String name) { - List values = binder.bind(name, Bindable.listOf(String.class)).orElse(Collections.emptyList()); - Set result = new HashSet<>(values.size()); - for (String value : values) { - result.add("*".equals(value) ? "*" : EndpointId.fromPropertyValue(value).toLowerCaseString()); - } - return result; } String getPrefix() { return this.prefix; } - boolean isExposed(EndpointId endpointId) { - String id = endpointId.toLowerCaseString(); - if (!this.exclude.isEmpty()) { - if (this.exclude.contains("*") || this.exclude.contains(id)) { - return false; - } - } - if (this.include.isEmpty()) { - if (this.exposeDefaults.contains("*") || this.exposeDefaults.contains(id)) { - return true; - } - } - return this.include.contains("*") || this.include.contains(id); + boolean isExposed(EndpointId id) { + return super.match(id); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/OnEnabledEndpointCondition.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/OnEnabledEndpointCondition.java deleted file mode 100644 index 2babd17f148f..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/OnEnabledEndpointCondition.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.autoconfigure.endpoint.condition; - -import org.springframework.boot.autoconfigure.condition.ConditionOutcome; -import org.springframework.context.annotation.ConditionContext; -import org.springframework.core.type.AnnotatedTypeMetadata; - -/** - * A condition that checks if an endpoint is enabled. - * - * @author Stephane Nicoll - * @author Andy Wilkinson - * @author Phillip Webb - * @see ConditionalOnEnabledEndpoint - * @deprecated as of 2.2.0 in favor of {@link OnAvailableEndpointCondition} - */ -@Deprecated -class OnEnabledEndpointCondition extends AbstractEndpointCondition { - - @Override - public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { - return getEnablementOutcome(context, metadata, ConditionalOnEnabledEndpoint.class); - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/expose/IncludeExcludeEndpointFilter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/expose/IncludeExcludeEndpointFilter.java new file mode 100644 index 000000000000..3b71edbf70c9 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/expose/IncludeExcludeEndpointFilter.java @@ -0,0 +1,237 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.endpoint.expose; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.boot.actuate.endpoint.EndpointFilter; +import org.springframework.boot.actuate.endpoint.EndpointId; +import org.springframework.boot.actuate.endpoint.ExposableEndpoint; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.core.env.Environment; +import org.springframework.util.Assert; + +/** + * {@link EndpointFilter} that will filter endpoints based on {@code include} and + * {@code exclude} patterns. + * + * @param the endpoint type + * @author Phillip Webb + * @since 2.2.7 + */ +public class IncludeExcludeEndpointFilter> implements EndpointFilter { + + private final Class endpointType; + + private final EndpointPatterns include; + + private final EndpointPatterns defaultIncludes; + + private final EndpointPatterns exclude; + + /** + * Create a new {@link IncludeExcludeEndpointFilter} with include/exclude rules bound + * from the {@link Environment}. + * @param endpointType the endpoint type that should be considered (other types always + * match) + * @param environment the environment containing the properties + * @param prefix the property prefix to bind + * @param defaultIncludes the default {@code includes} to use when none are specified. + */ + public IncludeExcludeEndpointFilter(Class endpointType, Environment environment, String prefix, + String... defaultIncludes) { + this(endpointType, environment, prefix, new EndpointPatterns(defaultIncludes)); + } + + /** + * Create a new {@link IncludeExcludeEndpointFilter} with include/exclude rules bound + * from the {@link Environment}. + * @param endpointType the endpoint type that should be considered (other types always + * match) + * @param environment the environment containing the properties + * @param prefix the property prefix to bind + * @param defaultIncludes the default {@code includes} to use when none are specified. + */ + public IncludeExcludeEndpointFilter(Class endpointType, Environment environment, String prefix, + DefaultIncludes defaultIncludes) { + this(endpointType, environment, prefix, DefaultIncludes.patterns(defaultIncludes)); + } + + private IncludeExcludeEndpointFilter(Class endpointType, Environment environment, String prefix, + EndpointPatterns defaultIncludes) { + Assert.notNull(endpointType, "EndpointType must not be null"); + Assert.notNull(environment, "Environment must not be null"); + Assert.hasText(prefix, "Prefix must not be empty"); + Assert.notNull(defaultIncludes, "DefaultIncludes must not be null"); + Binder binder = Binder.get(environment); + this.endpointType = endpointType; + this.include = new EndpointPatterns(bind(binder, prefix + ".include")); + this.defaultIncludes = defaultIncludes; + this.exclude = new EndpointPatterns(bind(binder, prefix + ".exclude")); + } + + /** + * Create a new {@link IncludeExcludeEndpointFilter} with specific include/exclude + * rules. + * @param endpointType the endpoint type that should be considered (other types always + * match) + * @param include the include patterns + * @param exclude the exclude patterns + * @param defaultIncludes the default {@code includes} to use when none are specified. + */ + public IncludeExcludeEndpointFilter(Class endpointType, Collection include, Collection exclude, + String... defaultIncludes) { + this(endpointType, include, exclude, new EndpointPatterns(defaultIncludes)); + } + + /** + * Create a new {@link IncludeExcludeEndpointFilter} with specific include/exclude + * rules. + * @param endpointType the endpoint type that should be considered (other types always + * match) + * @param include the include patterns + * @param exclude the exclude patterns + * @param defaultIncludes the default {@code includes} to use when none are specified. + */ + public IncludeExcludeEndpointFilter(Class endpointType, Collection include, Collection exclude, + DefaultIncludes defaultIncludes) { + this(endpointType, include, exclude, DefaultIncludes.patterns(defaultIncludes)); + } + + private IncludeExcludeEndpointFilter(Class endpointType, Collection include, Collection exclude, + EndpointPatterns defaultIncludes) { + Assert.notNull(endpointType, "EndpointType Type must not be null"); + Assert.notNull(defaultIncludes, "DefaultIncludes must not be null"); + this.endpointType = endpointType; + this.include = new EndpointPatterns(include); + this.defaultIncludes = defaultIncludes; + this.exclude = new EndpointPatterns(exclude); + } + + private List bind(Binder binder, String name) { + return binder.bind(name, Bindable.listOf(String.class)).orElseGet(ArrayList::new); + } + + @Override + public boolean match(E endpoint) { + if (!this.endpointType.isInstance(endpoint)) { + // Leave non-matching types for other filters + return true; + } + return match(endpoint.getEndpointId()); + } + + /** + * Return {@code true} if the filter matches. + * @param endpointId the endpoint ID to check + * @return {@code true} if the filter matches + */ + protected final boolean match(EndpointId endpointId) { + return isIncluded(endpointId) && !isExcluded(endpointId); + } + + private boolean isIncluded(EndpointId endpointId) { + if (this.include.isEmpty()) { + return this.defaultIncludes.matches(endpointId); + } + return this.include.matches(endpointId); + } + + private boolean isExcluded(EndpointId endpointId) { + if (this.exclude.isEmpty()) { + return false; + } + return this.exclude.matches(endpointId); + } + + /** + * Default include patterns that can be used. + */ + public enum DefaultIncludes { + + /** + * The default set of include patterns used for JMX. + */ + JMX("*"), + + /** + * The default set of include patterns used for web. + */ + WEB("health"); + + private final EndpointPatterns patterns; + + DefaultIncludes(String... patterns) { + this.patterns = new EndpointPatterns(patterns); + } + + static EndpointPatterns patterns(DefaultIncludes defaultIncludes) { + return (defaultIncludes != null) ? defaultIncludes.patterns : (EndpointPatterns) null; + } + + } + + /** + * A set of endpoint patterns used to match IDs. + */ + private static class EndpointPatterns { + + private final boolean empty; + + private final boolean matchesAll; + + private final Set endpointIds; + + EndpointPatterns(String[] patterns) { + this((patterns != null) ? Arrays.asList(patterns) : (Collection) null); + } + + EndpointPatterns(Collection patterns) { + patterns = (patterns != null) ? patterns : Collections.emptySet(); + boolean matchesAll = false; + Set endpointIds = new LinkedHashSet<>(); + for (String pattern : patterns) { + if ("*".equals(pattern)) { + matchesAll = true; + } + else { + endpointIds.add(EndpointId.fromPropertyValue(pattern)); + } + } + this.empty = patterns.isEmpty(); + this.matchesAll = matchesAll; + this.endpointIds = endpointIds; + } + + boolean isEmpty() { + return this.empty; + } + + boolean matches(EndpointId endpointId) { + return this.matchesAll || this.endpointIds.contains(endpointId); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/expose/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/expose/package-info.java new file mode 100644 index 000000000000..023f6306ba87 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/expose/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Endpoint exposure logic used for auto-configuration and conditions. + */ +package org.springframework.boot.actuate.autoconfigure.endpoint.expose; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointAutoConfiguration.java index e2af732276b0..4615c69d85cd 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.actuate.autoconfigure.endpoint.ExposeExcludePropertyEndpointFilter; +import org.springframework.boot.LazyInitializationExcludeFilter; +import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExcludeEndpointFilter; import org.springframework.boot.actuate.endpoint.EndpointFilter; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.invoke.OperationInvokerAdvisor; @@ -58,7 +60,7 @@ * @since 2.0.0 */ @Configuration(proxyBeanMethods = false) -@AutoConfigureAfter(JmxAutoConfiguration.class) +@AutoConfigureAfter({ JmxAutoConfiguration.class, EndpointAutoConfiguration.class }) @EnableConfigurationProperties(JmxEndpointProperties.class) @ConditionalOnProperty(prefix = "spring.jmx", name = "enabled", havingValue = "true") public class JmxEndpointAutoConfiguration { @@ -83,24 +85,35 @@ public JmxEndpointDiscoverer jmxAnnotationEndpointDiscoverer(ParameterValueMappe } @Bean - @ConditionalOnSingleCandidate(MBeanServer.class) - public JmxEndpointExporter jmxMBeanExporter(MBeanServer mBeanServer, Environment environment, - ObjectProvider objectMapper, JmxEndpointsSupplier jmxEndpointsSupplier) { + @ConditionalOnMissingBean(EndpointObjectNameFactory.class) + public DefaultEndpointObjectNameFactory endpointObjectNameFactory(MBeanServer mBeanServer, + Environment environment) { String contextId = ObjectUtils.getIdentityHexString(this.applicationContext); - EndpointObjectNameFactory objectNameFactory = new DefaultEndpointObjectNameFactory(this.properties, environment, - mBeanServer, contextId); + return new DefaultEndpointObjectNameFactory(this.properties, environment, mBeanServer, contextId); + } + + @Bean + @ConditionalOnSingleCandidate(MBeanServer.class) + public JmxEndpointExporter jmxMBeanExporter(MBeanServer mBeanServer, + EndpointObjectNameFactory endpointObjectNameFactory, ObjectProvider objectMapper, + JmxEndpointsSupplier jmxEndpointsSupplier) { JmxOperationResponseMapper responseMapper = new JacksonJmxOperationResponseMapper( objectMapper.getIfAvailable()); - return new JmxEndpointExporter(mBeanServer, objectNameFactory, responseMapper, + return new JmxEndpointExporter(mBeanServer, endpointObjectNameFactory, responseMapper, jmxEndpointsSupplier.getEndpoints()); } @Bean - public ExposeExcludePropertyEndpointFilter jmxIncludeExcludePropertyEndpointFilter() { + public IncludeExcludeEndpointFilter jmxIncludeExcludePropertyEndpointFilter() { JmxEndpointProperties.Exposure exposure = this.properties.getExposure(); - return new ExposeExcludePropertyEndpointFilter<>(ExposableJmxEndpoint.class, exposure.getInclude(), + return new IncludeExcludeEndpointFilter<>(ExposableJmxEndpoint.class, exposure.getInclude(), exposure.getExclude(), "*"); } + @Bean + static LazyInitializationExcludeFilter eagerlyInitializeJmxEndpointExporter() { + return LazyInitializationExcludeFilter.forBeanTypes(JmxEndpointExporter.class); + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/CorsEndpointProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/CorsEndpointProperties.java index 979309da2528..d5dde9984dc4 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/CorsEndpointProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/CorsEndpointProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,11 +37,21 @@ public class CorsEndpointProperties { /** - * Comma-separated list of origins to allow. '*' allows all origins. When not set, - * CORS support is disabled. + * Comma-separated list of origins to allow. '*' allows all origins. When credentials + * are allowed, '*' cannot be used and origin patterns should be configured instead. + * When no allowed origins or allowed origin patterns are set, CORS support is + * disabled. */ private List allowedOrigins = new ArrayList<>(); + /** + * Comma-separated list of origin patterns to allow. Unlike allowed origins which only + * supports '*', origin patterns are more flexible (for example + * 'https://*.example.com') and can be used when credentials are allowed. When no + * allowed origin patterns or allowed origins are set, CORS support is disabled. + */ + private List allowedOriginPatterns = new ArrayList<>(); + /** * Comma-separated list of methods to allow. '*' allows all methods. When not set, * defaults to GET. @@ -78,6 +88,14 @@ public void setAllowedOrigins(List allowedOrigins) { this.allowedOrigins = allowedOrigins; } + public List getAllowedOriginPatterns() { + return this.allowedOriginPatterns; + } + + public void setAllowedOriginPatterns(List allowedOriginPatterns) { + this.allowedOriginPatterns = allowedOriginPatterns; + } + public List getAllowedMethods() { return this.allowedMethods; } @@ -119,12 +137,13 @@ public void setMaxAge(Duration maxAge) { } public CorsConfiguration toCorsConfiguration() { - if (CollectionUtils.isEmpty(this.allowedOrigins)) { + if (CollectionUtils.isEmpty(this.allowedOrigins) && CollectionUtils.isEmpty(this.allowedOriginPatterns)) { return null; } PropertyMapper map = PropertyMapper.get(); CorsConfiguration configuration = new CorsConfiguration(); map.from(this::getAllowedOrigins).to(configuration::setAllowedOrigins); + map.from(this::getAllowedOriginPatterns).to(configuration::setAllowedOriginPatterns); map.from(this::getAllowedHeaders).whenNot(CollectionUtils::isEmpty).to(configuration::setAllowedHeaders); map.from(this::getAllowedMethods).whenNot(CollectionUtils::isEmpty).to(configuration::setAllowedMethods); map.from(this::getExposedHeaders).whenNot(CollectionUtils::isEmpty).to(configuration::setExposedHeaders); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/ServletEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/ServletEndpointManagementContextConfiguration.java index 2255a262c26f..21d84757a258 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/ServletEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/ServletEndpointManagementContextConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ import org.glassfish.jersey.server.ResourceConfig; -import org.springframework.boot.actuate.autoconfigure.endpoint.ExposeExcludePropertyEndpointFilter; +import org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExcludeEndpointFilter; import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration; import org.springframework.boot.actuate.endpoint.web.ExposableServletEndpoint; import org.springframework.boot.actuate.endpoint.web.ServletEndpointRegistrar; @@ -47,10 +47,10 @@ public class ServletEndpointManagementContextConfiguration { @Bean - public ExposeExcludePropertyEndpointFilter servletExposeExcludePropertyEndpointFilter( + public IncludeExcludeEndpointFilter servletExposeExcludePropertyEndpointFilter( WebEndpointProperties properties) { WebEndpointProperties.Exposure exposure = properties.getExposure(); - return new ExposeExcludePropertyEndpointFilter<>(ExposableServletEndpoint.class, exposure.getInclude(), + return new IncludeExcludeEndpointFilter<>(ExposableServletEndpoint.class, exposure.getInclude(), exposure.getExclude()); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfiguration.java index ea5eac25d2b8..387e82f3d132 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,8 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; -import org.springframework.boot.actuate.autoconfigure.endpoint.ExposeExcludePropertyEndpointFilter; +import org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExcludeEndpointFilter; +import org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExcludeEndpointFilter.DefaultIncludes; import org.springframework.boot.actuate.endpoint.EndpointFilter; import org.springframework.boot.actuate.endpoint.EndpointsSupplier; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; @@ -112,16 +113,16 @@ public PathMappedEndpoints pathMappedEndpoints(Collection> } @Bean - public ExposeExcludePropertyEndpointFilter webExposeExcludePropertyEndpointFilter() { + public IncludeExcludeEndpointFilter webExposeExcludePropertyEndpointFilter() { WebEndpointProperties.Exposure exposure = this.properties.getExposure(); - return new ExposeExcludePropertyEndpointFilter<>(ExposableWebEndpoint.class, exposure.getInclude(), - exposure.getExclude(), "info", "health"); + return new IncludeExcludeEndpointFilter<>(ExposableWebEndpoint.class, exposure.getInclude(), + exposure.getExclude(), DefaultIncludes.WEB); } @Bean - public ExposeExcludePropertyEndpointFilter controllerExposeExcludePropertyEndpointFilter() { + public IncludeExcludeEndpointFilter controllerExposeExcludePropertyEndpointFilter() { WebEndpointProperties.Exposure exposure = this.properties.getExposure(); - return new ExposeExcludePropertyEndpointFilter<>(ExposableControllerEndpoint.class, exposure.getInclude(), + return new IncludeExcludeEndpointFilter<>(ExposableControllerEndpoint.class, exposure.getInclude(), exposure.getExclude()); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointProperties.java index aded969536d4..06a9223eaf62 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,8 +38,11 @@ public class WebEndpointProperties { private final Exposure exposure = new Exposure(); /** - * Base path for Web endpoints. Relative to server.servlet.context-path or - * management.server.servlet.context-path if management.server.port is configured. + * Base path for Web endpoints. Relative to the servlet context path + * (server.servlet.context-path) or WebFlux base path (spring.webflux.base-path) when + * the management server is sharing the main server port. Relative to the management + * server base path (management.server.base-path) when a separate management server + * port (management.server.port) is configured. */ private String basePath = "/actuator"; @@ -48,6 +51,8 @@ public class WebEndpointProperties { */ private final Map pathMapping = new LinkedHashMap<>(); + private final Discovery discovery = new Discovery(); + public Exposure getExposure() { return this.exposure; } @@ -72,6 +77,10 @@ public Map getPathMapping() { return this.pathMapping; } + public Discovery getDiscovery() { + return this.discovery; + } + public static class Exposure { /** @@ -102,4 +111,21 @@ public void setExclude(Set exclude) { } + public static class Discovery { + + /** + * Whether the discovery page is enabled. + */ + private boolean enabled = true; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java index d22961a5f62f..c5b55c1e5140 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,14 +21,12 @@ import java.util.HashSet; import java.util.List; -import javax.annotation.PostConstruct; - import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.server.model.Resource; -import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration; +import org.springframework.boot.actuate.autoconfigure.web.jersey.ManagementContextResourceConfigCustomizer; import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType; import org.springframework.boot.actuate.endpoint.ExposableEndpoint; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; @@ -45,7 +43,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; -import org.springframework.boot.autoconfigure.jersey.ResourceConfigCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.core.env.Environment; import org.springframework.util.StringUtils; @@ -69,28 +66,24 @@ class JerseyWebEndpointManagementContextConfiguration { @Bean JerseyWebEndpointsResourcesRegistrar jerseyWebEndpointsResourcesRegistrar(Environment environment, - ObjectProvider resourceConfig, WebEndpointsSupplier webEndpointsSupplier, - ServletEndpointsSupplier servletEndpointsSupplier, EndpointMediaTypes endpointMediaTypes, - WebEndpointProperties webEndpointProperties) { + WebEndpointsSupplier webEndpointsSupplier, ServletEndpointsSupplier servletEndpointsSupplier, + EndpointMediaTypes endpointMediaTypes, WebEndpointProperties webEndpointProperties) { String basePath = webEndpointProperties.getBasePath(); - boolean shouldRegisterLinks = shouldRegisterLinksMapping(environment, basePath); - shouldRegisterLinksMapping(environment, basePath); - return new JerseyWebEndpointsResourcesRegistrar(resourceConfig.getIfAvailable(), webEndpointsSupplier, - servletEndpointsSupplier, endpointMediaTypes, basePath, shouldRegisterLinks); + boolean shouldRegisterLinks = shouldRegisterLinksMapping(webEndpointProperties, environment, basePath); + return new JerseyWebEndpointsResourcesRegistrar(webEndpointsSupplier, servletEndpointsSupplier, + endpointMediaTypes, basePath, shouldRegisterLinks); } - private boolean shouldRegisterLinksMapping(Environment environment, String basePath) { - return StringUtils.hasText(basePath) - || ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT); + private boolean shouldRegisterLinksMapping(WebEndpointProperties properties, Environment environment, + String basePath) { + return properties.getDiscovery().isEnabled() && (StringUtils.hasText(basePath) + || ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT)); } /** - * Register endpoints with the {@link ResourceConfig}. The - * {@link ResourceConfigCustomizer} cannot be used because we don't want to apply + * Register endpoints with the {@link ResourceConfig} for the management context. */ - static class JerseyWebEndpointsResourcesRegistrar { - - private final ResourceConfig resourceConfig; + static class JerseyWebEndpointsResourcesRegistrar implements ManagementContextResourceConfigCustomizer { private final WebEndpointsSupplier webEndpointsSupplier; @@ -102,11 +95,9 @@ static class JerseyWebEndpointsResourcesRegistrar { private final boolean shouldRegisterLinks; - JerseyWebEndpointsResourcesRegistrar(ResourceConfig resourceConfig, WebEndpointsSupplier webEndpointsSupplier, + JerseyWebEndpointsResourcesRegistrar(WebEndpointsSupplier webEndpointsSupplier, ServletEndpointsSupplier servletEndpointsSupplier, EndpointMediaTypes endpointMediaTypes, String basePath, boolean shouldRegisterLinks) { - super(); - this.resourceConfig = resourceConfig; this.webEndpointsSupplier = webEndpointsSupplier; this.servletEndpointsSupplier = servletEndpointsSupplier; this.mediaTypes = endpointMediaTypes; @@ -114,21 +105,19 @@ static class JerseyWebEndpointsResourcesRegistrar { this.shouldRegisterLinks = shouldRegisterLinks; } - @PostConstruct - void register() { - // We can't easily use @ConditionalOnBean because @AutoConfigureBefore is - // not an option for management contexts. Instead we manually check if - // the resource config bean exists - if (this.resourceConfig == null) { - return; - } + @Override + public void customize(ResourceConfig config) { + register(config); + } + + private void register(ResourceConfig config) { Collection webEndpoints = this.webEndpointsSupplier.getEndpoints(); Collection servletEndpoints = this.servletEndpointsSupplier.getEndpoints(); EndpointLinksResolver linksResolver = getLinksResolver(webEndpoints, servletEndpoints); EndpointMapping mapping = new EndpointMapping(this.basePath); - JerseyEndpointResourceFactory resourceFactory = new JerseyEndpointResourceFactory(); - register(resourceFactory.createEndpointResources(mapping, webEndpoints, this.mediaTypes, linksResolver, - this.shouldRegisterLinks)); + Collection endpointResources = new JerseyEndpointResourceFactory().createEndpointResources( + mapping, webEndpoints, this.mediaTypes, linksResolver, this.shouldRegisterLinks); + register(endpointResources, config); } private EndpointLinksResolver getLinksResolver(Collection webEndpoints, @@ -139,8 +128,8 @@ private EndpointLinksResolver getLinksResolver(Collection return new EndpointLinksResolver(endpoints, this.basePath); } - private void register(Collection resources) { - this.resourceConfig.registerResources(new HashSet<>(resources)); + private void register(Collection resources, ResourceConfig config) { + config.registerResources(new HashSet<>(resources)); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java index aa758ae72d7d..e4794e10c8fd 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -75,12 +75,13 @@ public WebFluxEndpointHandlerMapping webEndpointReactiveHandlerMapping(WebEndpoi allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints()); return new WebFluxEndpointHandlerMapping(endpointMapping, endpoints, endpointMediaTypes, corsProperties.toCorsConfiguration(), new EndpointLinksResolver(allEndpoints, basePath), - shouldRegisterLinksMapping(environment, basePath)); + shouldRegisterLinksMapping(webEndpointProperties, environment, basePath)); } - private boolean shouldRegisterLinksMapping(Environment environment, String basePath) { - return StringUtils.hasText(basePath) - || ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT); + private boolean shouldRegisterLinksMapping(WebEndpointProperties properties, Environment environment, + String basePath) { + return properties.getDiscovery().isEnabled() && (StringUtils.hasText(basePath) + || ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT)); } @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java index 91064d4d5aec..75fab2727432 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -74,13 +74,18 @@ public WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping(WebEndpoint allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints()); String basePath = webEndpointProperties.getBasePath(); EndpointMapping endpointMapping = new EndpointMapping(basePath); - boolean shouldRegisterLinksMapping = StringUtils.hasText(basePath) - || ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT); + boolean shouldRegisterLinksMapping = shouldRegisterLinksMapping(webEndpointProperties, environment, basePath); return new WebMvcEndpointHandlerMapping(endpointMapping, webEndpoints, endpointMediaTypes, corsProperties.toCorsConfiguration(), new EndpointLinksResolver(allEndpoints, basePath), shouldRegisterLinksMapping); } + private boolean shouldRegisterLinksMapping(WebEndpointProperties webEndpointProperties, Environment environment, + String basePath) { + return webEndpointProperties.getDiscovery().isEnabled() && (StringUtils.hasText(basePath) + || ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT)); + } + @Bean @ConditionalOnMissingBean public ControllerEndpointHandlerMapping controllerEndpointHandlerMapping( diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointAutoConfiguration.java index c8af552deabb..30547155eb46 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,6 +47,10 @@ public EnvironmentEndpoint environmentEndpoint(Environment environment, Environm if (keysToSanitize != null) { endpoint.setKeysToSanitize(keysToSanitize); } + String[] additionalKeysToSanitize = properties.getAdditionalKeysToSanitize(); + if (additionalKeysToSanitize != null) { + endpoint.keysToSanitize(additionalKeysToSanitize); + } return endpoint; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointProperties.java index fe753edcff45..cd1bfddf133a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,12 @@ public class EnvironmentEndpointProperties { */ private String[] keysToSanitize; + /** + * Keys that should be sanitized in addition to those already configured. Keys can be + * simple strings that the property ends with or regular expressions. + */ + private String[] additionalKeysToSanitize; + public String[] getKeysToSanitize() { return this.keysToSanitize; } @@ -42,4 +48,12 @@ public void setKeysToSanitize(String[] keysToSanitize) { this.keysToSanitize = keysToSanitize; } + public String[] getAdditionalKeysToSanitize() { + return this.additionalKeysToSanitize; + } + + public void setAdditionalKeysToSanitize(String[] additionalKeysToSanitize) { + this.additionalKeysToSanitize = additionalKeysToSanitize; + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroup.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroup.java index 52c44bd28260..efabc7152933 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroup.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroup.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.boot.actuate.autoconfigure.health; +import java.security.Principal; import java.util.Collection; import java.util.function.Predicate; @@ -24,6 +25,9 @@ import org.springframework.boot.actuate.health.HealthEndpointGroup; import org.springframework.boot.actuate.health.HttpCodeStatusMapper; import org.springframework.boot.actuate.health.StatusAggregator; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; /** @@ -97,10 +101,34 @@ private boolean getShowResult(SecurityContext securityContext, Show show) { } private boolean isAuthorized(SecurityContext securityContext) { - if (securityContext.getPrincipal() == null) { + Principal principal = securityContext.getPrincipal(); + if (principal == null) { return false; } - return CollectionUtils.isEmpty(this.roles) || this.roles.stream().anyMatch(securityContext::isUserInRole); + if (CollectionUtils.isEmpty(this.roles)) { + return true; + } + boolean checkAuthorities = isSpringSecurityAuthentication(principal); + for (String role : this.roles) { + if (securityContext.isUserInRole(role)) { + return true; + } + if (checkAuthorities) { + Authentication authentication = (Authentication) principal; + for (GrantedAuthority authority : authentication.getAuthorities()) { + String name = authority.getAuthority(); + if (role.equals(name)) { + return true; + } + } + } + } + return false; + } + + private boolean isSpringSecurityAuthentication(Principal principal) { + return ClassUtils.isPresent("org.springframework.security.core.Authentication", null) + && (principal instanceof Authentication); } @Override diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroups.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroups.java index b0001dafaaf8..31346fecb6e2 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroups.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroups.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,7 +51,7 @@ */ class AutoConfiguredHealthEndpointGroups implements HealthEndpointGroups { - private static Predicate ALL = (name) -> true; + private static final Predicate ALL = (name) -> true; private final HealthEndpointGroup primaryGroup; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/CompositeHealthIndicatorConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/CompositeHealthIndicatorConfiguration.java deleted file mode 100644 index c159c78dac1d..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/CompositeHealthIndicatorConfiguration.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.autoconfigure.health; - -import java.util.Map; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.actuate.health.CompositeHealthIndicator; -import org.springframework.boot.actuate.health.DefaultHealthIndicatorRegistry; -import org.springframework.boot.actuate.health.HealthAggregator; -import org.springframework.boot.actuate.health.HealthIndicator; -import org.springframework.boot.actuate.health.HealthIndicatorRegistry; -import org.springframework.core.ResolvableType; - -/** - * Base class for configurations that can combine source beans using a - * {@link CompositeHealthIndicator}. - * - * @param the health indicator type - * @param the bean source type - * @author Stephane Nicoll - * @since 2.0.0 - * @deprecated since 2.0.0 in favor of {@link CompositeHealthContributorConfiguration} - */ -@Deprecated -public abstract class CompositeHealthIndicatorConfiguration { - - @Autowired - private HealthAggregator healthAggregator; - - protected HealthIndicator createHealthIndicator(Map beans) { - if (beans.size() == 1) { - return createHealthIndicator(beans.values().iterator().next()); - } - HealthIndicatorRegistry registry = new DefaultHealthIndicatorRegistry(); - beans.forEach((name, source) -> registry.register(name, createHealthIndicator(source))); - return new CompositeHealthIndicator(this.healthAggregator, registry); - } - - @SuppressWarnings("unchecked") - protected H createHealthIndicator(S source) { - Class[] generics = ResolvableType.forClass(CompositeHealthIndicatorConfiguration.class, getClass()) - .resolveGenerics(); - Class indicatorClass = (Class) generics[0]; - Class sourceClass = (Class) generics[1]; - try { - return indicatorClass.getConstructor(sourceClass).newInstance(source); - } - catch (Exception ex) { - throw new IllegalStateException( - "Unable to create indicator " + indicatorClass + " for source " + sourceClass, ex); - } - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/CompositeReactiveHealthIndicatorConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/CompositeReactiveHealthIndicatorConfiguration.java deleted file mode 100644 index d3ca609aeb90..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/CompositeReactiveHealthIndicatorConfiguration.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.autoconfigure.health; - -import java.util.Map; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.actuate.health.CompositeReactiveHealthIndicator; -import org.springframework.boot.actuate.health.DefaultReactiveHealthIndicatorRegistry; -import org.springframework.boot.actuate.health.HealthAggregator; -import org.springframework.boot.actuate.health.ReactiveHealthIndicator; -import org.springframework.boot.actuate.health.ReactiveHealthIndicatorRegistry; -import org.springframework.core.ResolvableType; - -/** - * Reactive variant of {@link CompositeHealthIndicatorConfiguration}. - * - * @param the health indicator type - * @param the bean source type - * @author Stephane Nicoll - * @since 2.0.0 - * @deprecated since 2.2.0 in favor of - * {@link CompositeReactiveHealthContributorConfiguration} - */ -@Deprecated -public abstract class CompositeReactiveHealthIndicatorConfiguration { - - @Autowired - private HealthAggregator healthAggregator; - - protected ReactiveHealthIndicator createHealthIndicator(Map beans) { - if (beans.size() == 1) { - return createHealthIndicator(beans.values().iterator().next()); - } - ReactiveHealthIndicatorRegistry registry = new DefaultReactiveHealthIndicatorRegistry(); - beans.forEach((name, source) -> registry.register(name, createHealthIndicator(source))); - return new CompositeReactiveHealthIndicator(this.healthAggregator, registry); - } - - @SuppressWarnings("unchecked") - protected H createHealthIndicator(S source) { - Class[] generics = ResolvableType.forClass(CompositeReactiveHealthIndicatorConfiguration.class, getClass()) - .resolveGenerics(); - Class indicatorClass = (Class) generics[0]; - Class sourceClass = (Class) generics[1]; - try { - return indicatorClass.getConstructor(sourceClass).newInstance(source); - } - catch (Exception ex) { - throw new IllegalStateException( - "Unable to create indicator " + indicatorClass + " for source " + sourceClass, ex); - } - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthAggregatorStatusAggregatorAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthAggregatorStatusAggregatorAdapter.java deleted file mode 100644 index 19c002de4abe..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthAggregatorStatusAggregatorAdapter.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.autoconfigure.health; - -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Set; - -import org.springframework.boot.actuate.health.Health; -import org.springframework.boot.actuate.health.HealthAggregator; -import org.springframework.boot.actuate.health.Status; -import org.springframework.boot.actuate.health.StatusAggregator; - -/** - * Adapter class to convert a legacy {@link HealthAggregator} to a - * {@link StatusAggregator}. - * - * @author Phillip Webb - */ -@SuppressWarnings("deprecation") -class HealthAggregatorStatusAggregatorAdapter implements StatusAggregator { - - private HealthAggregator healthAggregator; - - HealthAggregatorStatusAggregatorAdapter(HealthAggregator healthAggregator) { - this.healthAggregator = healthAggregator; - } - - @Override - public Status getAggregateStatus(Set statuses) { - int index = 0; - Map healths = new LinkedHashMap<>(); - for (Status status : statuses) { - index++; - healths.put("health" + index, asHealth(status)); - } - Health aggregate = this.healthAggregator.aggregate(healths); - return aggregate.getStatus(); - } - - private Health asHealth(Status status) { - return Health.status(status).build(); - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthContributorRegistryHealthIndicatorRegistryAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthContributorRegistryHealthIndicatorRegistryAdapter.java deleted file mode 100644 index e99f170b6f9f..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthContributorRegistryHealthIndicatorRegistryAdapter.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.autoconfigure.health; - -import java.util.LinkedHashMap; -import java.util.Map; - -import org.springframework.boot.actuate.health.HealthContributor; -import org.springframework.boot.actuate.health.HealthContributorRegistry; -import org.springframework.boot.actuate.health.HealthIndicator; -import org.springframework.boot.actuate.health.HealthIndicatorRegistry; -import org.springframework.boot.actuate.health.NamedContributor; -import org.springframework.util.Assert; - -/** - * Adapter class to convert a {@link HealthContributorRegistry} to a legacy - * {@link HealthIndicatorRegistry}. - * - * @author Phillip Webb - */ -@SuppressWarnings("deprecation") -class HealthContributorRegistryHealthIndicatorRegistryAdapter implements HealthIndicatorRegistry { - - private final HealthContributorRegistry contributorRegistry; - - HealthContributorRegistryHealthIndicatorRegistryAdapter(HealthContributorRegistry contributorRegistry) { - Assert.notNull(contributorRegistry, "ContributorRegistry must not be null"); - this.contributorRegistry = contributorRegistry; - } - - @Override - public void register(String name, HealthIndicator healthIndicator) { - this.contributorRegistry.registerContributor(name, healthIndicator); - } - - @Override - public HealthIndicator unregister(String name) { - HealthContributor contributor = this.contributorRegistry.unregisterContributor(name); - if (contributor instanceof HealthIndicator) { - return (HealthIndicator) contributor; - } - return null; - } - - @Override - public HealthIndicator get(String name) { - HealthContributor contributor = this.contributorRegistry.getContributor(name); - if (contributor instanceof HealthIndicator) { - return (HealthIndicator) contributor; - } - return null; - } - - @Override - public Map getAll() { - Map all = new LinkedHashMap<>(); - for (NamedContributor namedContributor : this.contributorRegistry) { - if (namedContributor.getContributor() instanceof HealthIndicator) { - all.put(namedContributor.getName(), (HealthIndicator) namedContributor.getContributor()); - } - } - return all; - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointAutoConfiguration.java index 972ee5d3a616..096a212ecfaf 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import org.springframework.boot.actuate.health.HealthEndpoint; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -30,23 +29,14 @@ * @author Andy Wilkinson * @author Stephane Nicoll * @author Phillip Webb + * @author Scott Frederick * @since 2.0.0 */ @Configuration(proxyBeanMethods = false) @ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class) -@EnableConfigurationProperties -@Import({ LegacyHealthEndpointAdaptersConfiguration.class, LegacyHealthEndpointCompatibilityConfiguration.class, - HealthEndpointConfiguration.class, ReactiveHealthEndpointConfiguration.class, +@EnableConfigurationProperties(HealthEndpointProperties.class) +@Import({ HealthEndpointConfiguration.class, ReactiveHealthEndpointConfiguration.class, HealthEndpointWebExtensionConfiguration.class, HealthEndpointReactiveWebExtensionConfiguration.class }) public class HealthEndpointAutoConfiguration { - @Bean - @SuppressWarnings("deprecation") - HealthEndpointProperties healthEndpointProperties(HealthIndicatorProperties healthIndicatorProperties) { - HealthEndpointProperties healthEndpointProperties = new HealthEndpointProperties(); - healthEndpointProperties.getStatus().getOrder().addAll(healthIndicatorProperties.getOrder()); - healthEndpointProperties.getStatus().getHttpMapping().putAll(healthIndicatorProperties.getHttpMapping()); - return healthEndpointProperties; - } - } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.java index 54b074dce32a..0190380e57fd 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,9 @@ import java.util.LinkedHashMap; import java.util.Map; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.actuate.health.CompositeHealthContributor; import org.springframework.boot.actuate.health.CompositeReactiveHealthContributor; import org.springframework.boot.actuate.health.Health; @@ -28,6 +31,7 @@ import org.springframework.boot.actuate.health.HealthContributorRegistry; import org.springframework.boot.actuate.health.HealthEndpoint; import org.springframework.boot.actuate.health.HealthEndpointGroups; +import org.springframework.boot.actuate.health.HealthEndpointGroupsPostProcessor; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.actuate.health.HttpCodeStatusMapper; import org.springframework.boot.actuate.health.NamedContributor; @@ -88,6 +92,42 @@ HealthEndpoint healthEndpoint(HealthContributorRegistry registry, HealthEndpoint return new HealthEndpoint(registry, groups); } + @Bean + static HealthEndpointGroupsBeanPostProcessor healthEndpointGroupsBeanPostProcessor( + ObjectProvider healthEndpointGroupsPostProcessors) { + return new HealthEndpointGroupsBeanPostProcessor(healthEndpointGroupsPostProcessors); + } + + /** + * {@link BeanPostProcessor} to invoke {@link HealthEndpointGroupsPostProcessor} + * beans. + */ + static class HealthEndpointGroupsBeanPostProcessor implements BeanPostProcessor { + + private final ObjectProvider postProcessors; + + HealthEndpointGroupsBeanPostProcessor(ObjectProvider postProcessors) { + this.postProcessors = postProcessors; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof HealthEndpointGroups) { + return applyPostProcessors((HealthEndpointGroups) bean); + } + return bean; + } + + private Object applyPostProcessors(HealthEndpointGroups bean) { + for (HealthEndpointGroupsPostProcessor postProcessor : this.postProcessors.orderedStream() + .toArray(HealthEndpointGroupsPostProcessor[]::new)) { + bean = postProcessor.postProcessHealthEndpointGroups(bean); + } + return bean; + } + + } + /** * Adapter to expose {@link ReactiveHealthContributor} beans as * {@link HealthContributor} instances. diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointProperties.java index d2b6d9c46734..6ccf2ae5a133 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,16 +27,31 @@ * Configuration properties for {@link HealthEndpoint}. * * @author Phillip Webb + * @author Leo Li * @since 2.0.0 */ @ConfigurationProperties("management.endpoint.health") public class HealthEndpointProperties extends HealthProperties { + /** + * When to show full health details. + */ + private Show showDetails = Show.NEVER; + /** * Health endpoint groups. */ private Map group = new LinkedHashMap<>(); + @Override + public Show getShowDetails() { + return this.showDetails; + } + + public void setShowDetails(Show showDetails) { + this.showDetails = showDetails; + } + public Map getGroup() { return this.group; } @@ -56,6 +71,12 @@ public static class Group extends HealthProperties { */ private Set exclude; + /** + * When to show full health details. Defaults to the value of + * 'management.endpoint.health.show-details'. + */ + private Show showDetails; + public Set getInclude() { return this.include; } @@ -72,6 +93,15 @@ public void setExclude(Set exclude) { this.exclude = exclude; } + @Override + public Show getShowDetails() { + return this.showDetails; + } + + public void setShowDetails(Show showDetails) { + this.showDetails = showDetails; + } + } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorAutoConfiguration.java deleted file mode 100644 index d6ef2959aae1..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorAutoConfiguration.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.autoconfigure.health; - -import org.springframework.boot.actuate.health.HealthContributor; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.context.annotation.Configuration; - -/** - * {@link EnableAutoConfiguration Auto-configuration} for {@link HealthContributor health - * contributors}. - * - * @author Andy Wilkinson - * @author Stephane Nicoll - * @author Phillip Webb - * @author Vedran Pavic - * @since 2.0.0 - * @deprecated since 2.2.0 in favor of {@link HealthContributorAutoConfiguration} - */ -@Deprecated -@Configuration(proxyBeanMethods = false) -public class HealthIndicatorAutoConfiguration { - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorProperties.java deleted file mode 100644 index 930707a7a4d7..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorProperties.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.autoconfigure.health; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; - -/** - * Configuration properties for some health properties. - * - * @author Christian Dupuis - * @since 2.0.0 - * @deprecated since 2.2.0 in favor of {@link HealthEndpointProperties} - */ -@Deprecated -@ConfigurationProperties(prefix = "management.health.status") -public class HealthIndicatorProperties { - - private List order = new ArrayList<>(); - - private final Map httpMapping = new LinkedHashMap<>(); - - @DeprecatedConfigurationProperty(replacement = "management.endpoint.health.status.order") - public List getOrder() { - return this.order; - } - - public void setOrder(List order) { - this.order = order; - } - - @DeprecatedConfigurationProperty(replacement = "management.endpoint.health.status.http-mapping") - public Map getHttpMapping() { - return this.httpMapping; - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthProperties.java index 901c4031d53e..8603f79aa55e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,11 +43,6 @@ public abstract class HealthProperties { */ private Show showComponents; - /** - * When to show full health details. - */ - private Show showDetails = Show.NEVER; - /** * Roles used to determine whether or not a user is authorized to be shown details. * When empty, all authenticated users are authorized. @@ -66,13 +61,7 @@ public void setShowComponents(Show showComponents) { this.showComponents = showComponents; } - public Show getShowDetails() { - return this.showDetails; - } - - public void setShowDetails(Show showDetails) { - this.showDetails = showDetails; - } + public abstract Show getShowDetails(); public Set getRoles() { return this.roles; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthStatusHttpMapperHttpCodeStatusMapperAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthStatusHttpMapperHttpCodeStatusMapperAdapter.java deleted file mode 100644 index fcade05e3c52..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthStatusHttpMapperHttpCodeStatusMapperAdapter.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.autoconfigure.health; - -import org.springframework.boot.actuate.health.HealthStatusHttpMapper; -import org.springframework.boot.actuate.health.HttpCodeStatusMapper; -import org.springframework.boot.actuate.health.Status; - -/** - * Adapter class to convert a legacy {@link HealthStatusHttpMapper} to a - * {@link HttpCodeStatusMapper}. - * - * @author Phillip Webb - */ -@SuppressWarnings("deprecation") -class HealthStatusHttpMapperHttpCodeStatusMapperAdapter implements HttpCodeStatusMapper { - - private final HealthStatusHttpMapper healthStatusHttpMapper; - - HealthStatusHttpMapperHttpCodeStatusMapperAdapter(HealthStatusHttpMapper healthStatusHttpMapper) { - this.healthStatusHttpMapper = healthStatusHttpMapper; - } - - @Override - public int getStatusCode(Status status) { - return this.healthStatusHttpMapper.mapStatus(status); - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/IncludeExcludeGroupMemberPredicate.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/IncludeExcludeGroupMemberPredicate.java index 585fe7a21991..463fbea66bcf 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/IncludeExcludeGroupMemberPredicate.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/IncludeExcludeGroupMemberPredicate.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,7 +44,7 @@ public boolean test(String name) { } private boolean isIncluded(String name) { - return this.include.contains("*") || this.include.contains(clean(name)); + return this.include.isEmpty() || this.include.contains("*") || this.include.contains(clean(name)); } private boolean isExcluded(String name) { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/LegacyHealthEndpointAdaptersConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/LegacyHealthEndpointAdaptersConfiguration.java deleted file mode 100644 index e04f0a2c5afa..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/LegacyHealthEndpointAdaptersConfiguration.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.autoconfigure.health; - -import org.springframework.boot.actuate.health.HttpCodeStatusMapper; -import org.springframework.boot.actuate.health.StatusAggregator; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * Configuration to adapt legacy deprecated health endpoint classes and interfaces. - * - * @author Phillip Webb - * @see HealthEndpointAutoConfiguration - */ -@Configuration(proxyBeanMethods = false) -@SuppressWarnings("deprecation") -class LegacyHealthEndpointAdaptersConfiguration { - - @Bean - @ConditionalOnBean(org.springframework.boot.actuate.health.HealthAggregator.class) - StatusAggregator healthAggregatorStatusAggregatorAdapter( - org.springframework.boot.actuate.health.HealthAggregator healthAggregator) { - return new HealthAggregatorStatusAggregatorAdapter(healthAggregator); - } - - @Bean - @ConditionalOnBean(org.springframework.boot.actuate.health.HealthStatusHttpMapper.class) - HttpCodeStatusMapper healthStatusHttpMapperHttpCodeStatusMapperAdapter( - org.springframework.boot.actuate.health.HealthStatusHttpMapper healthStatusHttpMapper) { - return new HealthStatusHttpMapperHttpCodeStatusMapperAdapter(healthStatusHttpMapper); - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/LegacyHealthEndpointCompatibilityConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/LegacyHealthEndpointCompatibilityConfiguration.java deleted file mode 100644 index 4c1b1add27d7..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/LegacyHealthEndpointCompatibilityConfiguration.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.autoconfigure.health; - -import reactor.core.publisher.Mono; - -import org.springframework.boot.actuate.health.HealthAggregator; -import org.springframework.boot.actuate.health.HealthContributorRegistry; -import org.springframework.boot.actuate.health.HealthIndicatorRegistry; -import org.springframework.boot.actuate.health.HealthStatusHttpMapper; -import org.springframework.boot.actuate.health.OrderedHealthAggregator; -import org.springframework.boot.actuate.health.ReactiveHealthContributorRegistry; -import org.springframework.boot.actuate.health.ReactiveHealthIndicatorRegistry; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.util.CollectionUtils; - -/** - * Configuration to adapt legacy deprecated health endpoint classes and interfaces. - * - * @author Phillip Webb - * @see HealthEndpointAutoConfiguration - */ -@Configuration(proxyBeanMethods = false) -@SuppressWarnings("deprecation") -@EnableConfigurationProperties(HealthIndicatorProperties.class) -class LegacyHealthEndpointCompatibilityConfiguration { - - @Bean - @ConditionalOnMissingBean - HealthAggregator healthAggregator(HealthIndicatorProperties healthIndicatorProperties) { - OrderedHealthAggregator aggregator = new OrderedHealthAggregator(); - if (!CollectionUtils.isEmpty(healthIndicatorProperties.getOrder())) { - aggregator.setStatusOrder(healthIndicatorProperties.getOrder()); - } - return aggregator; - } - - @Bean - @ConditionalOnMissingBean - HealthStatusHttpMapper healthStatusHttpMapper(HealthIndicatorProperties healthIndicatorProperties) { - HealthStatusHttpMapper mapper = new HealthStatusHttpMapper(); - if (!CollectionUtils.isEmpty(healthIndicatorProperties.getHttpMapping())) { - mapper.setStatusMapping(healthIndicatorProperties.getHttpMapping()); - } - return mapper; - } - - @Bean - @ConditionalOnMissingBean(HealthIndicatorRegistry.class) - HealthContributorRegistryHealthIndicatorRegistryAdapter healthIndicatorRegistry( - HealthContributorRegistry healthContributorRegistry) { - return new HealthContributorRegistryHealthIndicatorRegistryAdapter(healthContributorRegistry); - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(Mono.class) - static class LegacyReactiveHealthEndpointCompatibilityConfiguration { - - @Bean - @ConditionalOnMissingBean(ReactiveHealthIndicatorRegistry.class) - ReactiveHealthContributorRegistryReactiveHealthIndicatorRegistryAdapter reactiveHealthIndicatorRegistry( - ReactiveHealthContributorRegistry reactiveHealthContributorRegistry) { - return new ReactiveHealthContributorRegistryReactiveHealthIndicatorRegistryAdapter( - reactiveHealthContributorRegistry); - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/ReactiveHealthContributorRegistryReactiveHealthIndicatorRegistryAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/ReactiveHealthContributorRegistryReactiveHealthIndicatorRegistryAdapter.java deleted file mode 100644 index 863edd6aea52..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/ReactiveHealthContributorRegistryReactiveHealthIndicatorRegistryAdapter.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.autoconfigure.health; - -import java.util.LinkedHashMap; -import java.util.Map; - -import org.springframework.boot.actuate.health.NamedContributor; -import org.springframework.boot.actuate.health.ReactiveHealthContributor; -import org.springframework.boot.actuate.health.ReactiveHealthContributorRegistry; -import org.springframework.boot.actuate.health.ReactiveHealthIndicator; -import org.springframework.boot.actuate.health.ReactiveHealthIndicatorRegistry; -import org.springframework.util.Assert; - -/** - * Adapter class to convert a {@link ReactiveHealthContributorRegistry} to a legacy - * {@link ReactiveHealthIndicatorRegistry}. - * - * @author Phillip Webb - */ -@SuppressWarnings("deprecation") -class ReactiveHealthContributorRegistryReactiveHealthIndicatorRegistryAdapter - implements ReactiveHealthIndicatorRegistry { - - private final ReactiveHealthContributorRegistry contributorRegistry; - - ReactiveHealthContributorRegistryReactiveHealthIndicatorRegistryAdapter( - ReactiveHealthContributorRegistry contributorRegistry) { - Assert.notNull(contributorRegistry, "ContributorRegistry must not be null"); - this.contributorRegistry = contributorRegistry; - } - - @Override - public void register(String name, ReactiveHealthIndicator healthIndicator) { - this.contributorRegistry.registerContributor(name, healthIndicator); - } - - @Override - public ReactiveHealthIndicator unregister(String name) { - ReactiveHealthContributor contributor = this.contributorRegistry.unregisterContributor(name); - if (contributor instanceof ReactiveHealthIndicator) { - return (ReactiveHealthIndicator) contributor; - } - return null; - } - - @Override - public ReactiveHealthIndicator get(String name) { - ReactiveHealthContributor contributor = this.contributorRegistry.getContributor(name); - if (contributor instanceof ReactiveHealthIndicator) { - return (ReactiveHealthIndicator) contributor; - } - return null; - } - - @Override - public Map getAll() { - Map all = new LinkedHashMap<>(); - for (NamedContributor namedContributor : this.contributorRegistry) { - if (namedContributor.getContributor() instanceof ReactiveHealthIndicator) { - all.put(namedContributor.getName(), (ReactiveHealthIndicator) namedContributor.getContributor()); - } - } - return all; - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/jdbc/DataSourceHealthContributorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/jdbc/DataSourceHealthContributorAutoConfiguration.java index 885c4e57de5e..26d2ba468166 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/jdbc/DataSourceHealthContributorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/jdbc/DataSourceHealthContributorAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,19 +17,20 @@ package org.springframework.boot.actuate.autoconfigure.jdbc; import java.util.Collection; +import java.util.Iterator; import java.util.Map; +import java.util.Objects; +import java.util.function.Function; import java.util.stream.Collectors; import javax.sql.DataSource; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.actuate.autoconfigure.health.CompositeHealthContributorConfiguration; import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; -import org.springframework.boot.actuate.health.AbstractHealthIndicator; -import org.springframework.boot.actuate.health.Health.Builder; +import org.springframework.boot.actuate.health.CompositeHealthContributor; import org.springframework.boot.actuate.health.HealthContributor; -import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.actuate.health.NamedContributor; import org.springframework.boot.actuate.jdbc.DataSourceHealthIndicator; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -37,6 +38,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.jdbc.metadata.CompositeDataSourcePoolMetadataProvider; import org.springframework.boot.jdbc.metadata.DataSourcePoolMetadata; import org.springframework.boot.jdbc.metadata.DataSourcePoolMetadataProvider; @@ -44,6 +46,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; +import org.springframework.util.Assert; /** * {@link EnableAutoConfiguration Auto-configuration} for @@ -54,6 +57,8 @@ * @author Andy Wilkinson * @author Stephane Nicoll * @author Arthur Kalimullin + * @author Julio Gomez + * @author Safeer Ansari * @since 2.0.0 */ @Configuration(proxyBeanMethods = false) @@ -61,33 +66,48 @@ @ConditionalOnBean(DataSource.class) @ConditionalOnEnabledHealthIndicator("db") @AutoConfigureAfter(DataSourceAutoConfiguration.class) -public class DataSourceHealthContributorAutoConfiguration extends - CompositeHealthContributorConfiguration implements InitializingBean { +@EnableConfigurationProperties(DataSourceHealthIndicatorProperties.class) +public class DataSourceHealthContributorAutoConfiguration implements InitializingBean { private final Collection metadataProviders; private DataSourcePoolMetadataProvider poolMetadataProvider; - public DataSourceHealthContributorAutoConfiguration(Map dataSources, + public DataSourceHealthContributorAutoConfiguration( ObjectProvider metadataProviders) { this.metadataProviders = metadataProviders.orderedStream().collect(Collectors.toList()); } @Override - public void afterPropertiesSet() throws Exception { + public void afterPropertiesSet() { this.poolMetadataProvider = new CompositeDataSourcePoolMetadataProvider(this.metadataProviders); } @Bean @ConditionalOnMissingBean(name = { "dbHealthIndicator", "dbHealthContributor" }) - public HealthContributor dbHealthContributor(Map dataSources) { + public HealthContributor dbHealthContributor(Map dataSources, + DataSourceHealthIndicatorProperties dataSourceHealthIndicatorProperties) { + if (dataSourceHealthIndicatorProperties.isIgnoreRoutingDataSources()) { + Map filteredDatasources = dataSources.entrySet().stream() + .filter((e) -> !(e.getValue() instanceof AbstractRoutingDataSource)) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + return createContributor(filteredDatasources); + } return createContributor(dataSources); } - @Override - protected AbstractHealthIndicator createIndicator(DataSource source) { + private HealthContributor createContributor(Map beans) { + Assert.notEmpty(beans, "Beans must not be empty"); + if (beans.size() == 1) { + return createContributor(beans.values().iterator().next()); + } + return CompositeHealthContributor.fromMap(beans, this::createContributor); + } + + private HealthContributor createContributor(DataSource source) { if (source instanceof AbstractRoutingDataSource) { - return new RoutingDataSourceHealthIndicator(); + AbstractRoutingDataSource routingDataSource = (AbstractRoutingDataSource) source; + return new RoutingDataSourceHealthContributor(routingDataSource, this::createContributor); } return new DataSourceHealthIndicator(source, getValidationQuery(source)); } @@ -98,14 +118,32 @@ private String getValidationQuery(DataSource source) { } /** - * {@link HealthIndicator} used for {@link AbstractRoutingDataSource} beans where we - * can't actually query for the status. + * {@link CompositeHealthContributor} used for {@link AbstractRoutingDataSource} beans + * where the overall health is composed of a {@link DataSourceHealthIndicator} for + * each routed datasource. */ - static class RoutingDataSourceHealthIndicator extends AbstractHealthIndicator { + static class RoutingDataSourceHealthContributor implements CompositeHealthContributor { + + private final CompositeHealthContributor delegate; + + private static final String UNNAMED_DATASOURCE_KEY = "unnamed"; + + RoutingDataSourceHealthContributor(AbstractRoutingDataSource routingDataSource, + Function contributorFunction) { + Map routedDataSources = routingDataSource.getResolvedDataSources().entrySet().stream() + .collect(Collectors.toMap((e) -> Objects.toString(e.getKey(), UNNAMED_DATASOURCE_KEY), + Map.Entry::getValue)); + this.delegate = CompositeHealthContributor.fromMap(routedDataSources, contributorFunction); + } + + @Override + public HealthContributor getContributor(String name) { + return this.delegate.getContributor(name); + } @Override - protected void doHealthCheck(Builder builder) throws Exception { - builder.unknown().withDetail("routing", true); + public Iterator> iterator() { + return this.delegate.iterator(); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/jdbc/DataSourceHealthIndicatorProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/jdbc/DataSourceHealthIndicatorProperties.java new file mode 100644 index 000000000000..5efe2583e7f0 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/jdbc/DataSourceHealthIndicatorProperties.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.jdbc; + +import org.springframework.boot.actuate.jdbc.DataSourceHealthIndicator; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * External configuration properties for {@link DataSourceHealthIndicator}. + * + * @author Julio Gomez + * @since 2.4.0 + */ +@ConfigurationProperties(prefix = "management.health.db") +public class DataSourceHealthIndicatorProperties { + + /** + * Whether to ignore AbstractRoutingDataSources when creating database health + * indicators. + */ + private boolean ignoreRoutingDataSources = false; + + public boolean isIgnoreRoutingDataSources() { + return this.ignoreRoutingDataSources; + } + + public void setIgnoreRoutingDataSources(boolean ignoreRoutingDataSources) { + this.ignoreRoutingDataSources = ignoreRoutingDataSources; + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/LogFileWebEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/LogFileWebEndpointAutoConfiguration.java index a86bea91e234..8de4be5b8170 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/LogFileWebEndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/LogFileWebEndpointAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -56,16 +56,15 @@ public LogFileWebEndpoint logFileWebEndpoint(ObjectProvider logFile, private static class LogFileCondition extends SpringBootCondition { - @SuppressWarnings("deprecation") @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { Environment environment = context.getEnvironment(); - String config = getLogFileConfig(environment, LogFile.FILE_NAME_PROPERTY, LogFile.FILE_PROPERTY); + String config = getLogFileConfig(environment, LogFile.FILE_NAME_PROPERTY); ConditionMessage.Builder message = ConditionMessage.forCondition("Log File"); if (StringUtils.hasText(config)) { return ConditionOutcome.match(message.found(LogFile.FILE_NAME_PROPERTY).items(config)); } - config = getLogFileConfig(environment, LogFile.FILE_PATH_PROPERTY, LogFile.PATH_PROPERTY); + config = getLogFileConfig(environment, LogFile.FILE_PATH_PROPERTY); if (StringUtils.hasText(config)) { return ConditionOutcome.match(message.found(LogFile.FILE_PATH_PROPERTY).items(config)); } @@ -76,12 +75,8 @@ public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeM return ConditionOutcome.noMatch(message.didNotFind("logging file").atAll()); } - private String getLogFileConfig(Environment environment, String configName, String deprecatedConfigName) { - String config = environment.resolvePlaceholders("${" + configName + ":}"); - if (StringUtils.hasText(config)) { - return config; - } - return environment.resolvePlaceholders("${" + deprecatedConfigName + ":}"); + private String getLogFileConfig(Environment environment, String configName) { + return environment.resolvePlaceholders("${" + configName + ":}"); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/AutoConfiguredCompositeMeterRegistry.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/AutoConfiguredCompositeMeterRegistry.java new file mode 100644 index 000000000000..d38b43c271b2 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/AutoConfiguredCompositeMeterRegistry.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.metrics; + +import java.util.List; + +import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.composite.CompositeMeterRegistry; + +/** + * Specialization of {@link CompositeMeterRegistry} used to identify the auto-configured + * composite. + * + * @author Andy Wilkinson + */ +class AutoConfiguredCompositeMeterRegistry extends CompositeMeterRegistry { + + AutoConfiguredCompositeMeterRegistry(Clock clock, List registries) { + super(clock, registries); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/CompositeMeterRegistryAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/CompositeMeterRegistryAutoConfiguration.java index f9452356de92..a020c76eaf46 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/CompositeMeterRegistryAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/CompositeMeterRegistryAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/CompositeMeterRegistryConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/CompositeMeterRegistryConfiguration.java index cd15fac2caf8..4e7522156526 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/CompositeMeterRegistryConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/CompositeMeterRegistryConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,8 +42,8 @@ class CompositeMeterRegistryConfiguration { @Bean @Primary - CompositeMeterRegistry compositeMeterRegistry(Clock clock, List registries) { - return new CompositeMeterRegistry(clock, registries); + AutoConfiguredCompositeMeterRegistry compositeMeterRegistry(Clock clock, List registries) { + return new AutoConfiguredCompositeMeterRegistry(clock, registries); } static class MultipleNonPrimaryMeterRegistriesCondition extends NoneNestedConditions { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/JvmMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/JvmMetricsAutoConfiguration.java index 98f50edf1db7..629e3dc8f32c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/JvmMetricsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/JvmMetricsAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,7 +37,7 @@ * @since 2.1.0 */ @Configuration(proxyBeanMethods = false) -@AutoConfigureAfter(MetricsAutoConfiguration.class) +@AutoConfigureAfter({ MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class }) @ConditionalOnClass(MeterRegistry.class) @ConditionalOnBean(MeterRegistry.class) public class JvmMetricsAutoConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/KafkaMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/KafkaMetricsAutoConfiguration.java index f2bbcfe6e196..3b43999a77b4 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/KafkaMetricsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/KafkaMetricsAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,39 +16,70 @@ package org.springframework.boot.actuate.autoconfigure.metrics; -import java.util.Collections; - -import javax.management.MBeanServer; - import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.binder.kafka.KafkaConsumerMetrics; -import org.apache.kafka.clients.consumer.KafkaConsumer; +import io.micrometer.core.instrument.binder.kafka.KafkaClientMetrics; +import io.micrometer.core.instrument.binder.kafka.KafkaStreamsMetrics; import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; 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.jmx.JmxAutoConfiguration; +import org.springframework.boot.autoconfigure.kafka.DefaultKafkaConsumerFactoryCustomizer; +import org.springframework.boot.autoconfigure.kafka.DefaultKafkaProducerFactoryCustomizer; +import org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration; +import org.springframework.boot.autoconfigure.kafka.StreamsBuilderFactoryBeanCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.config.StreamsBuilderFactoryBean; +import org.springframework.kafka.core.DefaultKafkaConsumerFactory; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.MicrometerConsumerListener; +import org.springframework.kafka.core.MicrometerProducerListener; +import org.springframework.kafka.core.ProducerFactory; +import org.springframework.kafka.streams.KafkaStreamsMicrometerListener; /** * Auto-configuration for Kafka metrics. * * @author Andy Wilkinson + * @author Stephane Nicoll + * @author Eddú Meléndez * @since 2.1.0 */ @Configuration(proxyBeanMethods = false) -@AutoConfigureAfter({ MetricsAutoConfiguration.class, JmxAutoConfiguration.class }) -@ConditionalOnClass({ KafkaConsumerMetrics.class, KafkaConsumer.class }) +@AutoConfigureBefore(KafkaAutoConfiguration.class) +@AutoConfigureAfter({ MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class }) +@ConditionalOnClass({ KafkaClientMetrics.class, ProducerFactory.class }) @ConditionalOnBean(MeterRegistry.class) public class KafkaMetricsAutoConfiguration { @Bean - @ConditionalOnMissingBean - @ConditionalOnBean(MBeanServer.class) - public KafkaConsumerMetrics kafkaConsumerMetrics(MBeanServer mbeanServer) { - return new KafkaConsumerMetrics(mbeanServer, Collections.emptyList()); + public DefaultKafkaProducerFactoryCustomizer kafkaProducerMetrics(MeterRegistry meterRegistry) { + return (producerFactory) -> addListener(producerFactory, meterRegistry); + } + + @Bean + public DefaultKafkaConsumerFactoryCustomizer kafkaConsumerMetrics(MeterRegistry meterRegistry) { + return (consumerFactory) -> addListener(consumerFactory, meterRegistry); + } + + private void addListener(DefaultKafkaConsumerFactory factory, MeterRegistry meterRegistry) { + factory.addListener(new MicrometerConsumerListener<>(meterRegistry)); + } + + private void addListener(DefaultKafkaProducerFactory factory, MeterRegistry meterRegistry) { + factory.addListener(new MicrometerProducerListener<>(meterRegistry)); + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass({ KafkaStreamsMetrics.class, StreamsBuilderFactoryBean.class }) + static class KafkaStreamsMetricsConfiguration { + + @Bean + StreamsBuilderFactoryBeanCustomizer kafkaStreamsMetrics(MeterRegistry meterRegistry) { + return (factoryBean) -> factoryBean.addListener(new KafkaStreamsMicrometerListener(meterRegistry)); + } + } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/Log4J2MetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/Log4J2MetricsAutoConfiguration.java index b70fb3deee4e..d2e860171055 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/Log4J2MetricsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/Log4J2MetricsAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ * @since 2.1.0 */ @Configuration(proxyBeanMethods = false) -@AutoConfigureAfter(MetricsAutoConfiguration.class) +@AutoConfigureAfter({ MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class }) @ConditionalOnClass(value = { Log4j2Metrics.class, LogManager.class }, name = "org.apache.logging.log4j.core.LoggerContext") @ConditionalOnBean(MeterRegistry.class) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/LogbackMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/LogbackMetricsAutoConfiguration.java index 29402da029a6..54eee525b094 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/LogbackMetricsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/LogbackMetricsAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,7 +44,7 @@ * @since 2.1.0 */ @Configuration(proxyBeanMethods = false) -@AutoConfigureAfter(MetricsAutoConfiguration.class) +@AutoConfigureAfter({ MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class }) @ConditionalOnClass({ MeterRegistry.class, LoggerContext.class, LoggerFactory.class }) @ConditionalOnBean(MeterRegistry.class) @Conditional(LogbackLoggingCondition.class) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryConfigurer.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryConfigurer.java index 3cbe4a451db7..fd062849a0c8 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryConfigurer.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,7 +61,9 @@ void configure(MeterRegistry registry) { // Customizers must be applied before binders, as they may add custom // tags or alter timer or summary configuration. customize(registry); - addFilters(registry); + if (!(registry instanceof AutoConfiguredCompositeMeterRegistry)) { + addFilters(registry); + } if (!this.hasCompositeMeterRegistry || registry instanceof CompositeMeterRegistry) { addBinders(registry); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryCustomizer.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryCustomizer.java index 2610ef44d9ee..0d06dadf3493 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryCustomizer.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,8 +26,8 @@ * Customizers are guaranteed to be applied before any {@link Meter} is registered with * the registry. * - * @author Jon Schneider * @param the registry type to customize + * @author Jon Schneider * @since 2.0.0 */ @FunctionalInterface diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterValue.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterValue.java index c6d5fbcd2542..d01b188cded7 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterValue.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterValue.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,17 +25,18 @@ /** * A meter value that is used when configuring micrometer. Can be a String representation - * of either a {@link Long} (applicable to timers and distribution summaries) or a + * of either a {@link Double} (applicable to timers and distribution summaries) or a * {@link Duration} (applicable to only timers). * * @author Phillip Webb + * @author Stephane Nicoll * @since 2.2.0 */ public final class MeterValue { private final Object value; - MeterValue(long value) { + MeterValue(double value) { this.value = value; } @@ -48,26 +49,29 @@ public final class MeterValue { * @param meterType the meter type * @return the value or {@code null} if the value cannot be applied */ - public Long getValue(Type meterType) { + public Double getValue(Type meterType) { if (meterType == Type.DISTRIBUTION_SUMMARY) { return getDistributionSummaryValue(); } if (meterType == Type.TIMER) { - return getTimerValue(); + Long timerValue = getTimerValue(); + if (timerValue != null) { + return timerValue.doubleValue(); + } } return null; } - private Long getDistributionSummaryValue() { - if (this.value instanceof Long) { - return (Long) this.value; + private Double getDistributionSummaryValue() { + if (this.value instanceof Double) { + return (Double) this.value; } return null; } private Long getTimerValue() { - if (this.value instanceof Long) { - return TimeUnit.MILLISECONDS.toNanos((long) this.value); + if (this.value instanceof Double) { + return TimeUnit.MILLISECONDS.toNanos(((Double) this.value).longValue()); } if (this.value instanceof Duration) { return ((Duration) this.value).toNanos(); @@ -82,23 +86,30 @@ private Long getTimerValue() { * @return a {@link MeterValue} instance */ public static MeterValue valueOf(String value) { - if (isNumber(value)) { - return new MeterValue(Long.parseLong(value)); + Duration duration = safeParseDuration(value); + if (duration != null) { + return new MeterValue(duration); } - return new MeterValue(DurationStyle.detectAndParse(value)); + return new MeterValue(Double.valueOf(value)); } /** - * Return a new {@link MeterValue} instance for the given long value. + * Return a new {@link MeterValue} instance for the given double value. * @param value the source value * @return a {@link MeterValue} instance + * @since 2.3.0 */ - public static MeterValue valueOf(long value) { + public static MeterValue valueOf(double value) { return new MeterValue(value); } - private static boolean isNumber(String value) { - return value.chars().allMatch(Character::isDigit); + private static Duration safeParseDuration(String value) { + try { + return DurationStyle.detectAndParse(value); + } + catch (IllegalArgumentException ex) { + return null; + } } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfiguration.java index 476b74e13557..038658936eba 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsEndpointAutoConfiguration.java index 3c55e1844fd7..71125f54706c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsEndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsEndpointAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java index de701cd6bdf0..7184104bf4f4 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import java.util.Map; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; import org.springframework.boot.context.properties.NestedConfigurationProperty; /** @@ -43,8 +42,8 @@ public class MetricsProperties { private boolean useGlobalRegistry = true; /** - * Whether meter IDs starting-with the specified name should be enabled. The longest - * match wins, the key `all` can also be used to configure all meters. + * Whether meter IDs starting with the specified name should be enabled. The longest + * match wins, the key 'all' can also be used to configure all meters. */ private final Map enable = new LinkedHashMap<>(); @@ -55,6 +54,8 @@ public class MetricsProperties { private final Web web = new Web(); + private final Data data = new Data(); + private final Distribution distribution = new Distribution(); public boolean isUseGlobalRegistry() { @@ -77,6 +78,10 @@ public Web getWeb() { return this.web; } + public Data getData() { + return this.data; + } + public Distribution getDistribution() { return this.distribution; } @@ -110,28 +115,6 @@ public ClientRequest getRequest() { return this.request; } - /** - * Return the name of the metric for client requests. - * @return request metric name - * @deprecated since 2.2.0 in favor of {@link ClientRequest#getMetricName()} - */ - @Deprecated - @DeprecatedConfigurationProperty(replacement = "management.metrics.web.client.request.metric-name") - public String getRequestsMetricName() { - return this.request.getMetricName(); - } - - /** - * Set the name of the metric for client requests. - * @param requestsMetricName request metric name - * @deprecated since 2.2.0 in favor of - * {@link ClientRequest#setMetricName(String)} - */ - @Deprecated - public void setRequestsMetricName(String requestsMetricName) { - this.request.setMetricName(requestsMetricName); - } - public int getMaxUriTags() { return this.maxUriTags; } @@ -184,50 +167,6 @@ public ServerRequest getRequest() { return this.request; } - /** - * Return whether server requests should be automatically timed. - * @return {@code true} if server request should be automatically timed - * @deprecated since 2.2.0 in favor of {@link AutoTimeProperties#isEnabled()} - */ - @DeprecatedConfigurationProperty(replacement = "management.metrics.web.server.request.autotime.enabled") - @Deprecated - public boolean isAutoTimeRequests() { - return this.request.getAutotime().isEnabled(); - } - - /** - * Set whether server requests should be automatically timed. - * @param autoTimeRequests whether server requests should be automatically - * timed - * @deprecated since 2.2.0 in favor of {@link AutoTimeProperties#isEnabled()} - */ - @Deprecated - public void setAutoTimeRequests(boolean autoTimeRequests) { - this.request.getAutotime().setEnabled(autoTimeRequests); - } - - /** - * Return name of the metric for server requests. - * @return request metric name - * @deprecated since 2.2.0 in favor of {@link ServerRequest#getMetricName()} - */ - @DeprecatedConfigurationProperty(replacement = "management.metrics.web.server.request.metric-name") - @Deprecated - public String getRequestsMetricName() { - return this.request.getMetricName(); - } - - /** - * Set the name of the metric for server requests. - * @param requestsMetricName request metric name - * @deprecated since 2.2.0 in favor of - * {@link ServerRequest#setMetricName(String)} - */ - @Deprecated - public void setRequestsMetricName(String requestsMetricName) { - this.request.setMetricName(requestsMetricName); - } - public int getMaxUriTags() { return this.maxUriTags; } @@ -243,6 +182,11 @@ public static class ServerRequest { */ private String metricName = "http.server.requests"; + /** + * Whether the trailing slash should be ignored when recording metrics. + */ + private boolean ignoreTrailingSlash = true; + /** * Auto-timed request settings. */ @@ -261,6 +205,51 @@ public void setMetricName(String metricName) { this.metricName = metricName; } + public boolean isIgnoreTrailingSlash() { + return this.ignoreTrailingSlash; + } + + public void setIgnoreTrailingSlash(boolean ignoreTrailingSlash) { + this.ignoreTrailingSlash = ignoreTrailingSlash; + } + + } + + } + + } + + public static class Data { + + private final Repository repository = new Repository(); + + public Repository getRepository() { + return this.repository; + } + + public static class Repository { + + /** + * Name of the metric for sent requests. + */ + private String metricName = "spring.data.repository.invocations"; + + /** + * Auto-timed request settings. + */ + @NestedConfigurationProperty + private final AutoTimeProperties autotime = new AutoTimeProperties(); + + public String getMetricName() { + return this.metricName; + } + + public void setMetricName(String metricName) { + this.metricName = metricName; + } + + public AutoTimeProperties getAutotime() { + return this.autotime; } } @@ -273,36 +262,36 @@ public static class Distribution { * Whether meter IDs starting with the specified name should publish percentile * histograms. For monitoring systems that support aggregable percentile * calculation based on a histogram, this can be set to true. For other systems, - * this has no effect. The longest match wins, the key `all` can also be used to + * this has no effect. The longest match wins, the key 'all' can also be used to * configure all meters. */ private final Map percentilesHistogram = new LinkedHashMap<>(); /** * Specific computed non-aggregable percentiles to ship to the backend for meter - * IDs starting-with the specified name. The longest match wins, the key `all` can + * IDs starting-with the specified name. The longest match wins, the key 'all' can * also be used to configure all meters. */ private final Map percentiles = new LinkedHashMap<>(); /** - * Specific SLA boundaries for meter IDs starting-with the specified name. The - * longest match wins. Counters will be published for each specified boundary. - * Values can be specified as a long or as a Duration value (for timer meters, - * defaulting to ms if no unit specified). + * Specific service-level objective boundaries for meter IDs starting with the + * specified name. The longest match wins. Counters will be published for each + * specified boundary. Values can be specified as a double or as a Duration value + * (for timer meters, defaulting to ms if no unit specified). */ - private final Map sla = new LinkedHashMap<>(); + private final Map slo = new LinkedHashMap<>(); /** - * Minimum value that meter IDs starting-with the specified name are expected to - * observe. The longest match wins. Values can be specified as a long or as a + * Minimum value that meter IDs starting with the specified name are expected to + * observe. The longest match wins. Values can be specified as a double or as a * Duration value (for timer meters, defaulting to ms if no unit specified). */ private final Map minimumExpectedValue = new LinkedHashMap<>(); /** - * Maximum value that meter IDs starting-with the specified name are expected to - * observe. The longest match wins. Values can be specified as a long or as a + * Maximum value that meter IDs starting with the specified name are expected to + * observe. The longest match wins. Values can be specified as a double or as a * Duration value (for timer meters, defaulting to ms if no unit specified). */ private final Map maximumExpectedValue = new LinkedHashMap<>(); @@ -315,8 +304,8 @@ public Map getPercentiles() { return this.percentiles; } - public Map getSla() { - return this.sla; + public Map getSlo() { + return this.slo; } public Map getMinimumExpectedValue() { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MissingRequiredConfigurationFailureAnalyzer.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MissingRequiredConfigurationFailureAnalyzer.java deleted file mode 100644 index 597461dcd450..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MissingRequiredConfigurationFailureAnalyzer.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.autoconfigure.metrics; - -import io.micrometer.core.instrument.config.MissingRequiredConfigurationException; - -import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; -import org.springframework.boot.diagnostics.FailureAnalysis; - -/** - * An {@link AbstractFailureAnalyzer} that performs analysis of failures caused by a - * {@link MissingRequiredConfigurationException}. - * - * @author Andy Wilkinson - */ -class MissingRequiredConfigurationFailureAnalyzer - extends AbstractFailureAnalyzer { - - @Override - protected FailureAnalysis analyze(Throwable rootFailure, MissingRequiredConfigurationException cause) { - StringBuilder description = new StringBuilder(); - description.append(cause.getMessage()); - if (!cause.getMessage().endsWith(".")) { - description.append("."); - } - return new FailureAnalysis(description.toString(), - "Update your application to provide the missing configuration.", cause); - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/OnlyOnceLoggingDenyMeterFilter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/OnlyOnceLoggingDenyMeterFilter.java index 92b4625aba71..b257f66c4fb4 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/OnlyOnceLoggingDenyMeterFilter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/OnlyOnceLoggingDenyMeterFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,7 @@ public final class OnlyOnceLoggingDenyMeterFilter implements MeterFilter { private static final Log logger = LogFactory.getLog(OnlyOnceLoggingDenyMeterFilter.class); - private final AtomicBoolean alreadyWarned = new AtomicBoolean(false); + private final AtomicBoolean alreadyWarned = new AtomicBoolean(); private final Supplier message; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/PropertiesMeterFilter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/PropertiesMeterFilter.java index bf50bb63cc43..7689480c7ea7 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/PropertiesMeterFilter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/PropertiesMeterFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -83,7 +83,8 @@ public DistributionStatisticConfig configure(Meter.Id id, DistributionStatisticC return DistributionStatisticConfig.builder() .percentilesHistogram(lookupWithFallbackToAll(distribution.getPercentilesHistogram(), id, null)) .percentiles(lookupWithFallbackToAll(distribution.getPercentiles(), id, null)) - .sla(convertSla(id.getType(), lookup(distribution.getSla(), id, null))) + .serviceLevelObjectives( + convertServiceLevelObjectives(id.getType(), lookup(distribution.getSlo(), id, null))) .minimumExpectedValue( convertMeterValue(id.getType(), lookup(distribution.getMinimumExpectedValue(), id, null))) .maximumExpectedValue( @@ -91,16 +92,16 @@ public DistributionStatisticConfig configure(Meter.Id id, DistributionStatisticC .build().merge(config); } - private long[] convertSla(Meter.Type meterType, ServiceLevelAgreementBoundary[] sla) { - if (sla == null) { + private double[] convertServiceLevelObjectives(Meter.Type meterType, ServiceLevelObjectiveBoundary[] slo) { + if (slo == null) { return null; } - long[] converted = Arrays.stream(sla).map((candidate) -> candidate.getValue(meterType)).filter(Objects::nonNull) - .mapToLong(Long::longValue).toArray(); + double[] converted = Arrays.stream(slo).map((candidate) -> candidate.getValue(meterType)) + .filter(Objects::nonNull).mapToDouble(Double::doubleValue).toArray(); return (converted.length != 0) ? converted : null; } - private Long convertMeterValue(Meter.Type meterType, String value) { + private Double convertMeterValue(Meter.Type meterType, String value) { return (value != null) ? MeterValue.valueOf(value).getValue(meterType) : null; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/ServiceLevelAgreementBoundary.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/ServiceLevelAgreementBoundary.java deleted file mode 100644 index 9fa875fc188e..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/ServiceLevelAgreementBoundary.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.autoconfigure.metrics; - -import java.time.Duration; - -import io.micrometer.core.instrument.Meter; - -/** - * A service level agreement boundary for use when configuring Micrometer. Can be - * specified as either a {@link Long} (applicable to timers and distribution summaries) or - * a {@link Duration} (applicable to only timers). - * - * @author Phillip Webb - * @since 2.0.0 - */ -public final class ServiceLevelAgreementBoundary { - - private final MeterValue value; - - ServiceLevelAgreementBoundary(MeterValue value) { - this.value = value; - } - - /** - * Return the underlying value of the SLA in form suitable to apply to the given meter - * type. - * @param meterType the meter type - * @return the value or {@code null} if the value cannot be applied - */ - public Long getValue(Meter.Type meterType) { - return this.value.getValue(meterType); - } - - /** - * Return a new {@link ServiceLevelAgreementBoundary} instance for the given long - * value. - * @param value the source value - * @return a {@link ServiceLevelAgreementBoundary} instance - */ - public static ServiceLevelAgreementBoundary valueOf(long value) { - return new ServiceLevelAgreementBoundary(MeterValue.valueOf(value)); - } - - /** - * Return a new {@link ServiceLevelAgreementBoundary} instance for the given String - * value. - * @param value the source value - * @return a {@link ServiceLevelAgreementBoundary} instance - */ - public static ServiceLevelAgreementBoundary valueOf(String value) { - return new ServiceLevelAgreementBoundary(MeterValue.valueOf(value)); - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/ServiceLevelObjectiveBoundary.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/ServiceLevelObjectiveBoundary.java new file mode 100644 index 000000000000..8bf37f09efd4 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/ServiceLevelObjectiveBoundary.java @@ -0,0 +1,70 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.metrics; + +import java.time.Duration; + +import io.micrometer.core.instrument.Meter; + +/** + * A boundary for a service-level objective (SLO) for use when configuring Micrometer. Can + * be specified as either a {@link Double} (applicable to timers and distribution + * summaries) or a {@link Duration} (applicable to only timers). + * + * @author Phillip Webb + * @author Stephane Nicoll + * @since 2.3.0 + */ +public final class ServiceLevelObjectiveBoundary { + + private final MeterValue value; + + ServiceLevelObjectiveBoundary(MeterValue value) { + this.value = value; + } + + /** + * Return the underlying value of the SLO in form suitable to apply to the given meter + * type. + * @param meterType the meter type + * @return the value or {@code null} if the value cannot be applied + */ + public Double getValue(Meter.Type meterType) { + return this.value.getValue(meterType); + } + + /** + * Return a new {@link ServiceLevelObjectiveBoundary} instance for the given double + * value. + * @param value the source value + * @return a {@link ServiceLevelObjectiveBoundary} instance + */ + public static ServiceLevelObjectiveBoundary valueOf(double value) { + return new ServiceLevelObjectiveBoundary(MeterValue.valueOf(value)); + } + + /** + * Return a new {@link ServiceLevelObjectiveBoundary} instance for the given String + * value. + * @param value the source value + * @return a {@link ServiceLevelObjectiveBoundary} instance + */ + public static ServiceLevelObjectiveBoundary valueOf(String value) { + return new ServiceLevelObjectiveBoundary(MeterValue.valueOf(value)); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/SystemMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/SystemMetricsAutoConfiguration.java index 2723d0fcc13f..58f8a357c572 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/SystemMetricsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/SystemMetricsAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,7 @@ * @since 2.1.0 */ @Configuration(proxyBeanMethods = false) -@AutoConfigureAfter(MetricsAutoConfiguration.class) +@AutoConfigureAfter({ MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class }) @ConditionalOnClass(MeterRegistry.class) @ConditionalOnBean(MeterRegistry.class) public class SystemMetricsAutoConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/ValidationFailureAnalyzer.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/ValidationFailureAnalyzer.java new file mode 100644 index 000000000000..eea4f412b3db --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/ValidationFailureAnalyzer.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.metrics; + +import io.micrometer.core.instrument.config.validate.Validated.Invalid; +import io.micrometer.core.instrument.config.validate.ValidationException; + +import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; +import org.springframework.boot.diagnostics.FailureAnalysis; + +/** + * An {@link AbstractFailureAnalyzer} that performs analysis of failures caused by a + * {@link ValidationException}. + * + * @author Andy Wilkinson + */ +class ValidationFailureAnalyzer extends AbstractFailureAnalyzer { + + @Override + protected FailureAnalysis analyze(Throwable rootFailure, ValidationException cause) { + StringBuilder description = new StringBuilder(String.format("Invalid Micrometer configuration detected:%n")); + for (Invalid failure : cause.getValidation().failures()) { + description.append(String.format("%n - %s was '%s' but it %s", failure.getProperty(), failure.getValue(), + failure.getMessage())); + } + return new FailureAnalysis(description.toString(), + "Update your application to correct the invalid configuration.", cause); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/amqp/RabbitMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/amqp/RabbitMetricsAutoConfiguration.java index 949db76c1602..4be11e03e6bd 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/amqp/RabbitMetricsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/amqp/RabbitMetricsAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,7 +42,7 @@ @AutoConfigureAfter({ MetricsAutoConfiguration.class, RabbitAutoConfiguration.class, SimpleMetricsExportAutoConfiguration.class }) @ConditionalOnClass({ ConnectionFactory.class, AbstractConnectionFactory.class }) -@ConditionalOnBean({ AbstractConnectionFactory.class, MeterRegistry.class }) +@ConditionalOnBean({ org.springframework.amqp.rabbit.connection.ConnectionFactory.class, MeterRegistry.class }) public class RabbitMetricsAutoConfiguration { @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/cache/CacheMeterBinderProvidersConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/cache/CacheMeterBinderProvidersConfiguration.java index 76796747c7f1..e756fc201cb1 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/cache/CacheMeterBinderProvidersConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/cache/CacheMeterBinderProvidersConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,12 +26,14 @@ import org.springframework.boot.actuate.metrics.cache.EhCache2CacheMeterBinderProvider; import org.springframework.boot.actuate.metrics.cache.HazelcastCacheMeterBinderProvider; import org.springframework.boot.actuate.metrics.cache.JCacheCacheMeterBinderProvider; +import org.springframework.boot.actuate.metrics.cache.RedisCacheMeterBinderProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.cache.caffeine.CaffeineCache; import org.springframework.cache.ehcache.EhCacheCache; import org.springframework.cache.jcache.JCacheCache; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCache; /** * Configure {@link CacheMeterBinderProvider} beans. @@ -86,4 +88,15 @@ JCacheCacheMeterBinderProvider jCacheCacheMeterBinderProvider() { } + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(RedisCache.class) + static class RedisCacheMeterBinderProviderConfiguration { + + @Bean + RedisCacheMeterBinderProvider redisCacheMeterBinderProvider() { + return new RedisCacheMeterBinderProvider(); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/cache/CacheMetricsRegistrarConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/cache/CacheMetricsRegistrarConfiguration.java index 8c34f201cc95..d1b402ec36fb 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/cache/CacheMetricsRegistrarConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/cache/CacheMetricsRegistrarConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,8 +19,6 @@ import java.util.Collection; import java.util.Map; -import javax.annotation.PostConstruct; - import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Tag; @@ -56,6 +54,7 @@ class CacheMetricsRegistrarConfiguration { this.registry = registry; this.cacheManagers = cacheManagers; this.cacheMetricsRegistrar = new CacheMetricsRegistrar(this.registry, binderProviders); + bindCachesToRegistry(); } @Bean @@ -63,8 +62,7 @@ CacheMetricsRegistrar cacheMetricsRegistrar() { return this.cacheMetricsRegistrar; } - @PostConstruct - void bindCachesToRegistry() { + private void bindCachesToRegistry() { this.cacheManagers.forEach(this::bindCacheManagerToRegistry); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/data/MetricsRepositoryMethodInvocationListenerBeanPostProcessor.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/data/MetricsRepositoryMethodInvocationListenerBeanPostProcessor.java new file mode 100644 index 000000000000..ae3e7f49546d --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/data/MetricsRepositoryMethodInvocationListenerBeanPostProcessor.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.metrics.data; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.boot.actuate.metrics.data.MetricsRepositoryMethodInvocationListener; +import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; +import org.springframework.data.repository.core.support.RepositoryFactoryCustomizer; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.util.function.SingletonSupplier; + +/** + * {@link BeanPostProcessor} to apply a {@link MetricsRepositoryMethodInvocationListener} + * to all {@link RepositoryFactorySupport repository factories}. + * + * @author Phillip Webb + */ +class MetricsRepositoryMethodInvocationListenerBeanPostProcessor implements BeanPostProcessor { + + private final RepositoryFactoryCustomizer customizer; + + MetricsRepositoryMethodInvocationListenerBeanPostProcessor( + SingletonSupplier listener) { + this.customizer = new MetricsRepositoryFactoryCustomizer(listener); + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof RepositoryFactoryBeanSupport) { + ((RepositoryFactoryBeanSupport) bean).addRepositoryFactoryCustomizer(this.customizer); + } + return bean; + } + + private static final class MetricsRepositoryFactoryCustomizer implements RepositoryFactoryCustomizer { + + private final SingletonSupplier listenerSupplier; + + private MetricsRepositoryFactoryCustomizer( + SingletonSupplier listenerSupplier) { + this.listenerSupplier = listenerSupplier; + } + + @Override + public void customize(RepositoryFactorySupport repositoryFactory) { + repositoryFactory.addInvocationListener(this.listenerSupplier.get()); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/data/RepositoryMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/data/RepositoryMetricsAutoConfiguration.java new file mode 100644 index 000000000000..62b1e004c809 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/data/RepositoryMetricsAutoConfiguration.java @@ -0,0 +1,82 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.metrics.data; + +import io.micrometer.core.instrument.MeterRegistry; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties; +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties.Data.Repository; +import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; +import org.springframework.boot.actuate.metrics.data.DefaultRepositoryTagsProvider; +import org.springframework.boot.actuate.metrics.data.MetricsRepositoryMethodInvocationListener; +import org.springframework.boot.actuate.metrics.data.RepositoryTagsProvider; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +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.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.function.SingletonSupplier; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Spring Data Repository metrics. + * + * @author Phillip Webb + * @since 2.5.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(org.springframework.data.repository.Repository.class) +@AutoConfigureAfter({ MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class, + SimpleMetricsExportAutoConfiguration.class }) +@ConditionalOnBean(MeterRegistry.class) +@EnableConfigurationProperties(MetricsProperties.class) +public class RepositoryMetricsAutoConfiguration { + + private final MetricsProperties properties; + + public RepositoryMetricsAutoConfiguration(MetricsProperties properties) { + this.properties = properties; + } + + @Bean + @ConditionalOnMissingBean(RepositoryTagsProvider.class) + public DefaultRepositoryTagsProvider repositoryTagsProvider() { + return new DefaultRepositoryTagsProvider(); + } + + @Bean + @ConditionalOnMissingBean + public MetricsRepositoryMethodInvocationListener metricsRepositoryMethodInvocationListener( + ObjectProvider registry, RepositoryTagsProvider tagsProvider) { + Repository properties = this.properties.getData().getRepository(); + return new MetricsRepositoryMethodInvocationListener(registry::getObject, tagsProvider, + properties.getMetricName(), properties.getAutotime()); + } + + @Bean + public static MetricsRepositoryMethodInvocationListenerBeanPostProcessor metricsRepositoryMethodInvocationListenerBeanPostProcessor( + ObjectProvider metricsRepositoryMethodInvocationListener) { + return new MetricsRepositoryMethodInvocationListenerBeanPostProcessor( + SingletonSupplier.of(metricsRepositoryMethodInvocationListener::getObject)); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/data/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/data/package-info.java new file mode 100644 index 000000000000..6d724f122c1e --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/data/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for Spring Data actuator metrics. + */ +package org.springframework.boot.actuate.autoconfigure.metrics.data; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/ConditionalOnEnabledMetricsExport.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/ConditionalOnEnabledMetricsExport.java new file mode 100644 index 000000000000..041f3db2fcfb --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/ConditionalOnEnabledMetricsExport.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.metrics.export; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Conditional; + +/** + * {@link Conditional @Conditional} that checks whether or not a metrics exporter is + * enabled. If the {@code management.metrics.export..enabled} property is configured + * then its value is used to determine if it matches. Otherwise, matches if the value of + * the {@code management.metrics.export.defaults.enabled} property is {@code true} or if + * it is not configured. + * + * @author Chris Bono + * @since 2.4.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Documented +@Conditional(OnMetricsExportEnabledCondition.class) +public @interface ConditionalOnEnabledMetricsExport { + + /** + * The name of the metrics exporter. + * @return the name of the metrics exporter + */ + String value(); + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/OnMetricsExportEnabledCondition.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/OnMetricsExportEnabledCondition.java new file mode 100644 index 000000000000..52dcf351302f --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/OnMetricsExportEnabledCondition.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.metrics.export; + +import org.springframework.boot.actuate.autoconfigure.OnEndpointElementCondition; +import org.springframework.context.annotation.Condition; + +/** + * {@link Condition} that checks if a metrics exporter is enabled. + * + * @author Chris Bono + */ +class OnMetricsExportEnabledCondition extends OnEndpointElementCondition { + + protected OnMetricsExportEnabledCondition() { + super("management.metrics.export.", ConditionalOnEnabledMetricsExport.class); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/appoptics/AppOpticsMetricsExportAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/appoptics/AppOpticsMetricsExportAutoConfiguration.java index c89429d32d60..21d7078375a6 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/appoptics/AppOpticsMetricsExportAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/appoptics/AppOpticsMetricsExportAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.export.ConditionalOnEnabledMetricsExport; import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; @@ -30,7 +31,6 @@ 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.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -47,8 +47,7 @@ @AutoConfigureAfter(MetricsAutoConfiguration.class) @ConditionalOnBean(Clock.class) @ConditionalOnClass(AppOpticsMeterRegistry.class) -@ConditionalOnProperty(prefix = "management.metrics.export.appoptics", name = "enabled", havingValue = "true", - matchIfMissing = true) +@ConditionalOnEnabledMetricsExport("appoptics") @EnableConfigurationProperties(AppOpticsProperties.class) public class AppOpticsMetricsExportAutoConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/appoptics/AppOpticsProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/appoptics/AppOpticsProperties.java index b50a50c42cff..991e733d9ce7 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/appoptics/AppOpticsProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/appoptics/AppOpticsProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,6 +46,12 @@ public class AppOpticsProperties extends StepRegistryProperties { */ private String hostTag = "instance"; + /** + * Whether to ship a floored time, useful when sending measurements from multiple + * hosts to align them on a given time boundary. + */ + private boolean floorTimes; + /** * Number of measurements per request to use for this backend. If more measurements * are found, then multiple requests will be made. @@ -81,6 +87,14 @@ public void setHostTag(String hostTag) { this.hostTag = hostTag; } + public boolean isFloorTimes() { + return this.floorTimes; + } + + public void setFloorTimes(boolean floorTimes) { + this.floorTimes = floorTimes; + } + @Override public Integer getBatchSize() { return this.batchSize; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/appoptics/AppOpticsPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/appoptics/AppOpticsPropertiesConfigAdapter.java index 88ebb38e8f74..5f7077e58d7a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/appoptics/AppOpticsPropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/appoptics/AppOpticsPropertiesConfigAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,6 +32,11 @@ class AppOpticsPropertiesConfigAdapter extends StepRegistryPropertiesConfigAdapt super(properties); } + @Override + public String prefix() { + return "management.metrics.export.appoptics"; + } + @Override public String uri() { return get(AppOpticsProperties::getUri, AppOpticsConfig.super::uri); @@ -47,4 +52,9 @@ public String hostTag() { return get(AppOpticsProperties::getHostTag, AppOpticsConfig.super::hostTag); } + @Override + public boolean floorTimes() { + return get(AppOpticsProperties::isFloorTimes, AppOpticsConfig.super::floorTimes); + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/atlas/AtlasMetricsExportAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/atlas/AtlasMetricsExportAutoConfiguration.java index 42ad1b01c95e..bcd942d9a0b7 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/atlas/AtlasMetricsExportAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/atlas/AtlasMetricsExportAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.export.ConditionalOnEnabledMetricsExport; import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; @@ -29,7 +30,6 @@ 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.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -46,8 +46,7 @@ @AutoConfigureAfter(MetricsAutoConfiguration.class) @ConditionalOnBean(Clock.class) @ConditionalOnClass(AtlasMeterRegistry.class) -@ConditionalOnProperty(prefix = "management.metrics.export.atlas", name = "enabled", havingValue = "true", - matchIfMissing = true) +@ConditionalOnEnabledMetricsExport("atlas") @EnableConfigurationProperties(AtlasProperties.class) public class AtlasMetricsExportAutoConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/atlas/AtlasProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/atlas/AtlasProperties.java index 33da891230a0..55ca22095758 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/atlas/AtlasProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/atlas/AtlasProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ import java.time.Duration; -import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.StepRegistryProperties; import org.springframework.boot.context.properties.ConfigurationProperties; /** @@ -30,13 +29,39 @@ * @since 2.0.0 */ @ConfigurationProperties(prefix = "management.metrics.export.atlas") -public class AtlasProperties extends StepRegistryProperties { +public class AtlasProperties { + + /** + * Step size (i.e. reporting frequency) to use. + */ + private Duration step = Duration.ofMinutes(1); + + /** + * Whether exporting of metrics to this backend is enabled. + */ + private boolean enabled = true; + + /** + * Connection timeout for requests to this backend. + */ + private Duration connectTimeout = Duration.ofSeconds(1); + + /** + * Read timeout for requests to this backend. + */ + private Duration readTimeout = Duration.ofSeconds(10); /** * Number of threads to use with the metrics publishing scheduler. */ private Integer numThreads = 4; + /** + * Number of measurements per request to use for this backend. If more measurements + * are found, then multiple requests will be made. + */ + private Integer batchSize = 10000; + /** * URI of the Atlas server. */ @@ -73,16 +98,54 @@ public class AtlasProperties extends StepRegistryProperties { */ private String evalUri = "http://localhost:7101/lwc/api/v1/evaluate"; - @Override + public Duration getStep() { + return this.step; + } + + public void setStep(Duration step) { + this.step = step; + } + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public Duration getConnectTimeout() { + return this.connectTimeout; + } + + public void setConnectTimeout(Duration connectTimeout) { + this.connectTimeout = connectTimeout; + } + + public Duration getReadTimeout() { + return this.readTimeout; + } + + public void setReadTimeout(Duration readTimeout) { + this.readTimeout = readTimeout; + } + public Integer getNumThreads() { return this.numThreads; } - @Override public void setNumThreads(Integer numThreads) { this.numThreads = numThreads; } + public Integer getBatchSize() { + return this.batchSize; + } + + public void setBatchSize(Integer batchSize) { + this.batchSize = batchSize; + } + public String getUri() { return this.uri; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/datadog/DatadogMetricsExportAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/datadog/DatadogMetricsExportAutoConfiguration.java index 881b4cc038a3..1a86463e876b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/datadog/DatadogMetricsExportAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/datadog/DatadogMetricsExportAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.export.ConditionalOnEnabledMetricsExport; import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; @@ -30,7 +31,6 @@ 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.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -47,8 +47,7 @@ @AutoConfigureAfter(MetricsAutoConfiguration.class) @ConditionalOnBean(Clock.class) @ConditionalOnClass(DatadogMeterRegistry.class) -@ConditionalOnProperty(prefix = "management.metrics.export.datadog", name = "enabled", havingValue = "true", - matchIfMissing = true) +@ConditionalOnEnabledMetricsExport("datadog") @EnableConfigurationProperties(DatadogProperties.class) public class DatadogMetricsExportAutoConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/datadog/DatadogProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/datadog/DatadogProperties.java index f042358d641f..139c16fd983a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/datadog/DatadogProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/datadog/DatadogProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -56,7 +56,7 @@ public class DatadogProperties extends StepRegistryProperties { * URI to ship metrics to. If you need to publish metrics to an internal proxy * en-route to Datadog, you can define the location of the proxy with this. */ - private String uri = "https://app.datadoghq.com"; + private String uri = "https://api.datadoghq.com"; public String getApiKey() { return this.apiKey; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/datadog/DatadogPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/datadog/DatadogPropertiesConfigAdapter.java index db417992cb07..d84d562f7d22 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/datadog/DatadogPropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/datadog/DatadogPropertiesConfigAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,11 @@ class DatadogPropertiesConfigAdapter extends StepRegistryPropertiesConfigAdapter super(properties); } + @Override + public String prefix() { + return "management.metrics.export.datadog"; + } + @Override public String apiKey() { return get(DatadogProperties::getApiKey, DatadogConfig.super::apiKey); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/dynatrace/DynatraceMetricsExportAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/dynatrace/DynatraceMetricsExportAutoConfiguration.java index d8ba18d21c6f..739798d7719e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/dynatrace/DynatraceMetricsExportAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/dynatrace/DynatraceMetricsExportAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.export.ConditionalOnEnabledMetricsExport; import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; @@ -30,7 +31,6 @@ 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.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -47,8 +47,7 @@ @AutoConfigureAfter(MetricsAutoConfiguration.class) @ConditionalOnBean(Clock.class) @ConditionalOnClass(DynatraceMeterRegistry.class) -@ConditionalOnProperty(prefix = "management.metrics.export.dynatrace", name = "enabled", havingValue = "true", - matchIfMissing = true) +@ConditionalOnEnabledMetricsExport("dynatrace") @EnableConfigurationProperties(DynatraceProperties.class) public class DynatraceMetricsExportAutoConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/dynatrace/DynatraceProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/dynatrace/DynatraceProperties.java index c72ab1a2e9cf..58d6d13ad400 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/dynatrace/DynatraceProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/dynatrace/DynatraceProperties.java @@ -51,6 +51,12 @@ public class DynatraceProperties extends StepRegistryProperties { */ private String uri; + /** + * Group for exported metrics. Used to specify custom device group name in the + * Dynatrace UI. + */ + private String group; + public String getApiToken() { return this.apiToken; } @@ -83,4 +89,12 @@ public void setUri(String uri) { this.uri = uri; } + public String getGroup() { + return this.group; + } + + public void setGroup(String group) { + this.group = group; + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/dynatrace/DynatracePropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/dynatrace/DynatracePropertiesConfigAdapter.java index f097e6ddad24..6d7444ce82b8 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/dynatrace/DynatracePropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/dynatrace/DynatracePropertiesConfigAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,6 +32,11 @@ class DynatracePropertiesConfigAdapter extends StepRegistryPropertiesConfigAdapt super(properties); } + @Override + public String prefix() { + return "management.metrics.export.dynatrace"; + } + @Override public String apiToken() { return get(DynatraceProperties::getApiToken, DynatraceConfig.super::apiToken); @@ -52,4 +57,9 @@ public String uri() { return get(DynatraceProperties::getUri, DynatraceConfig.super::uri); } + @Override + public String group() { + return get(DynatraceProperties::getGroup, DynatraceConfig.super::group); + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/elastic/ElasticMetricsExportAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/elastic/ElasticMetricsExportAutoConfiguration.java index 1c90122ece46..d781285619f1 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/elastic/ElasticMetricsExportAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/elastic/ElasticMetricsExportAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.export.ConditionalOnEnabledMetricsExport; import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; @@ -30,7 +31,6 @@ 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.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -47,8 +47,7 @@ @AutoConfigureAfter(MetricsAutoConfiguration.class) @ConditionalOnBean(Clock.class) @ConditionalOnClass(ElasticMeterRegistry.class) -@ConditionalOnProperty(prefix = "management.metrics.export.elastic", name = "enabled", havingValue = "true", - matchIfMissing = true) +@ConditionalOnEnabledMetricsExport("elastic") @EnableConfigurationProperties(ElasticProperties.class) public class ElasticMetricsExportAutoConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/elastic/ElasticProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/elastic/ElasticProperties.java index 5af408d5d99c..be2eea798bcb 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/elastic/ElasticProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/elastic/ElasticProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,14 +37,18 @@ public class ElasticProperties extends StepRegistryProperties { /** * Index to export metrics to. */ - private String index = "metrics"; + private String index = "micrometer-metrics"; /** - * Index date format used for rolling indices. Appended to the index name, preceded by - * a '-'. + * Index date format used for rolling indices. Appended to the index name. */ private String indexDateFormat = "yyyy-MM"; + /** + * Prefix to separate the index name from the date format used for rolling indices. + */ + private String indexDateSeparator = "-"; + /** * Name of the timestamp field. */ @@ -58,12 +62,17 @@ public class ElasticProperties extends StepRegistryProperties { /** * Login user of the Elastic server. */ - private String userName = ""; + private String userName; /** * Login password of the Elastic server. */ - private String password = ""; + private String password; + + /** + * Ingest pipeline name. By default, events are not pre-processed. + */ + private String pipeline; public String getHost() { return this.host; @@ -89,6 +98,14 @@ public void setIndexDateFormat(String indexDateFormat) { this.indexDateFormat = indexDateFormat; } + public String getIndexDateSeparator() { + return this.indexDateSeparator; + } + + public void setIndexDateSeparator(String indexDateSeparator) { + this.indexDateSeparator = indexDateSeparator; + } + public String getTimestampFieldName() { return this.timestampFieldName; } @@ -121,4 +138,12 @@ public void setPassword(String password) { this.password = password; } + public String getPipeline() { + return this.pipeline; + } + + public void setPipeline(String pipeline) { + this.pipeline = pipeline; + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/elastic/ElasticPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/elastic/ElasticPropertiesConfigAdapter.java index 8d41ebe25237..ba5a7f3a5ef5 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/elastic/ElasticPropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/elastic/ElasticPropertiesConfigAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,6 +32,11 @@ class ElasticPropertiesConfigAdapter extends StepRegistryPropertiesConfigAdapter super(properties); } + @Override + public String prefix() { + return "management.metrics.export.elastic"; + } + @Override public String host() { return get(ElasticProperties::getHost, ElasticConfig.super::host); @@ -47,6 +52,11 @@ public String indexDateFormat() { return get(ElasticProperties::getIndexDateFormat, ElasticConfig.super::indexDateFormat); } + @Override + public String indexDateSeparator() { + return get(ElasticProperties::getIndexDateSeparator, ElasticConfig.super::indexDateSeparator); + } + @Override public String timestampFieldName() { return get(ElasticProperties::getTimestampFieldName, ElasticConfig.super::timestampFieldName); @@ -67,4 +77,9 @@ public String password() { return get(ElasticProperties::getPassword, ElasticConfig.super::password); } + @Override + public String pipeline() { + return get(ElasticProperties::getPipeline, ElasticConfig.super::pipeline); + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/ganglia/GangliaMetricsExportAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/ganglia/GangliaMetricsExportAutoConfiguration.java index 1dca5c5705f2..4c3c8bae7730 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/ganglia/GangliaMetricsExportAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/ganglia/GangliaMetricsExportAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.export.ConditionalOnEnabledMetricsExport; import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; @@ -29,7 +30,6 @@ 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.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -45,8 +45,7 @@ @AutoConfigureAfter(MetricsAutoConfiguration.class) @ConditionalOnBean(Clock.class) @ConditionalOnClass(GangliaMeterRegistry.class) -@ConditionalOnProperty(prefix = "management.metrics.export.ganglia", name = "enabled", havingValue = "true", - matchIfMissing = true) +@ConditionalOnEnabledMetricsExport("ganglia") @EnableConfigurationProperties(GangliaProperties.class) public class GangliaMetricsExportAutoConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/ganglia/GangliaProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/ganglia/GangliaProperties.java index f00dad931c00..224d581182e9 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/ganglia/GangliaProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/ganglia/GangliaProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import info.ganglia.gmetric4j.gmetric.GMetric; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; /** * {@link ConfigurationProperties @ConfigurationProperties} for configuring Ganglia @@ -47,7 +48,7 @@ public class GangliaProperties { /** * Base time unit used to report rates. */ - private TimeUnit rateUnits = TimeUnit.SECONDS; + private TimeUnit rateUnits; /** * Base time unit used to report durations. @@ -57,7 +58,7 @@ public class GangliaProperties { /** * Ganglia protocol version. Must be either 3.1 or 3.0. */ - private String protocolVersion = "3.1"; + private String protocolVersion; /** * UDP addressing mode, either unicast or multicast. @@ -96,10 +97,13 @@ public void setStep(Duration step) { this.step = step; } + @Deprecated + @DeprecatedConfigurationProperty(reason = "No longer used by Micrometer.") public TimeUnit getRateUnits() { return this.rateUnits; } + @Deprecated public void setRateUnits(TimeUnit rateUnits) { this.rateUnits = rateUnits; } @@ -112,10 +116,13 @@ public void setDurationUnits(TimeUnit durationUnits) { this.durationUnits = durationUnits; } + @Deprecated + @DeprecatedConfigurationProperty(reason = "No longer used by Micrometer.") public String getProtocolVersion() { return this.protocolVersion; } + @Deprecated public void setProtocolVersion(String protocolVersion) { this.protocolVersion = protocolVersion; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/ganglia/GangliaPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/ganglia/GangliaPropertiesConfigAdapter.java index 4ce050bd9127..7cdc1dbaa890 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/ganglia/GangliaPropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/ganglia/GangliaPropertiesConfigAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,11 @@ class GangliaPropertiesConfigAdapter extends PropertiesConfigAdapter getTags() { return this.tags; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/humio/HumioPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/humio/HumioPropertiesConfigAdapter.java index 830c3ae9dde8..d74db03d40ba 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/humio/HumioPropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/humio/HumioPropertiesConfigAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,11 @@ class HumioPropertiesConfigAdapter extends StepRegistryPropertiesConfigAdapter tags() { return get(HumioProperties::getTags, HumioConfig.super::tags); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/influx/InfluxMetricsExportAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/influx/InfluxMetricsExportAutoConfiguration.java index 21d911890748..ac33e09c8301 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/influx/InfluxMetricsExportAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/influx/InfluxMetricsExportAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.export.ConditionalOnEnabledMetricsExport; import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; @@ -30,7 +31,6 @@ 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.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -47,8 +47,7 @@ @AutoConfigureAfter(MetricsAutoConfiguration.class) @ConditionalOnBean(Clock.class) @ConditionalOnClass(InfluxMeterRegistry.class) -@ConditionalOnProperty(prefix = "management.metrics.export.influx", name = "enabled", havingValue = "true", - matchIfMissing = true) +@ConditionalOnEnabledMetricsExport("influx") @EnableConfigurationProperties(InfluxProperties.class) public class InfluxMetricsExportAutoConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/influx/InfluxProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/influx/InfluxProperties.java index 88fa2951cdb2..c6841159a2dd 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/influx/InfluxProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/influx/InfluxProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.boot.actuate.autoconfigure.metrics.export.influx; +import io.micrometer.influx.InfluxApiVersion; import io.micrometer.influx.InfluxConsistency; import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.StepRegistryProperties; @@ -33,7 +34,7 @@ public class InfluxProperties extends StepRegistryProperties { /** - * Tag that will be mapped to "host" when shipping metrics to Influx. + * Database to send metrics to. InfluxDB v1 only. */ private String db = "mydb"; @@ -43,37 +44,37 @@ public class InfluxProperties extends StepRegistryProperties { private InfluxConsistency consistency = InfluxConsistency.ONE; /** - * Login user of the Influx server. + * Login user of the Influx server. InfluxDB v1 only. */ private String userName; /** - * Login password of the Influx server. + * Login password of the Influx server. InfluxDB v1 only. */ private String password; /** * Retention policy to use (Influx writes to the DEFAULT retention policy if one is - * not specified). + * not specified). InfluxDB v1 only. */ private String retentionPolicy; /** * Time period for which Influx should retain data in the current database. For * instance 7d, check the influx documentation for more details on the duration - * format. + * format. InfluxDB v1 only. */ private String retentionDuration; /** * How many copies of the data are stored in the cluster. Must be 1 for a single node - * instance. + * instance. InfluxDB v1 only. */ private Integer retentionReplicationFactor; /** * Time range covered by a shard group. For instance 2w, check the influx - * documentation for more details on the duration format. + * documentation for more details on the duration format. InfluxDB v1 only. */ private String retentionShardDuration; @@ -89,10 +90,33 @@ public class InfluxProperties extends StepRegistryProperties { /** * Whether to create the Influx database if it does not exist before attempting to - * publish metrics to it. + * publish metrics to it. InfluxDB v1 only. */ private boolean autoCreateDb = true; + /** + * API version of InfluxDB to use. Defaults to 'v1' unless an org is configured. If an + * org is configured, defaults to 'v2'. + */ + private InfluxApiVersion apiVersion; + + /** + * Org to write metrics to. InfluxDB v2 only. + */ + private String org; + + /** + * Bucket for metrics. Use either the bucket name or ID. Defaults to the value of the + * db property if not set. InfluxDB v2 only. + */ + private String bucket; + + /** + * Authentication token to use with calls to the InfluxDB backend. For InfluxDB v1, + * the Bearer scheme is used. For v2, the Token scheme is used. + */ + private String token; + public String getDb() { return this.db; } @@ -181,4 +205,36 @@ public void setAutoCreateDb(boolean autoCreateDb) { this.autoCreateDb = autoCreateDb; } + public InfluxApiVersion getApiVersion() { + return this.apiVersion; + } + + public void setApiVersion(InfluxApiVersion apiVersion) { + this.apiVersion = apiVersion; + } + + public String getOrg() { + return this.org; + } + + public void setOrg(String org) { + this.org = org; + } + + public String getBucket() { + return this.bucket; + } + + public void setBucket(String bucket) { + this.bucket = bucket; + } + + public String getToken() { + return this.token; + } + + public void setToken(String token) { + this.token = token; + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/influx/InfluxPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/influx/InfluxPropertiesConfigAdapter.java index ecff99f822c9..7ae479478c14 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/influx/InfluxPropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/influx/InfluxPropertiesConfigAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.boot.actuate.autoconfigure.metrics.export.influx; +import io.micrometer.influx.InfluxApiVersion; import io.micrometer.influx.InfluxConfig; import io.micrometer.influx.InfluxConsistency; @@ -34,6 +35,11 @@ class InfluxPropertiesConfigAdapter extends StepRegistryPropertiesConfigAdapter< super(properties); } + @Override + public String prefix() { + return "management.metrics.export.influx"; + } + @Override public String db() { return get(InfluxProperties::getDb, InfluxConfig.super::db); @@ -89,4 +95,24 @@ public boolean autoCreateDb() { return get(InfluxProperties::isAutoCreateDb, InfluxConfig.super::autoCreateDb); } + @Override + public InfluxApiVersion apiVersion() { + return get(InfluxProperties::getApiVersion, InfluxConfig.super::apiVersion); + } + + @Override + public String org() { + return get(InfluxProperties::getOrg, InfluxConfig.super::org); + } + + @Override + public String bucket() { + return get(InfluxProperties::getBucket, InfluxConfig.super::bucket); + } + + @Override + public String token() { + return get(InfluxProperties::getToken, InfluxConfig.super::token); + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/jmx/JmxMetricsExportAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/jmx/JmxMetricsExportAutoConfiguration.java index eef339e00f1d..592fc75a03d5 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/jmx/JmxMetricsExportAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/jmx/JmxMetricsExportAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.export.ConditionalOnEnabledMetricsExport; import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; @@ -29,7 +30,6 @@ 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.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -45,8 +45,7 @@ @AutoConfigureAfter(MetricsAutoConfiguration.class) @ConditionalOnBean(Clock.class) @ConditionalOnClass(JmxMeterRegistry.class) -@ConditionalOnProperty(prefix = "management.metrics.export.jmx", name = "enabled", havingValue = "true", - matchIfMissing = true) +@ConditionalOnEnabledMetricsExport("jmx") @EnableConfigurationProperties(JmxProperties.class) public class JmxMetricsExportAutoConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/jmx/JmxProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/jmx/JmxProperties.java index 2d927d34fe06..ea4adb908203 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/jmx/JmxProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/jmx/JmxProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,11 @@ @ConfigurationProperties(prefix = "management.metrics.export.jmx") public class JmxProperties { + /** + * Whether exporting of metrics to this backend is enabled. + */ + private boolean enabled = true; + /** * Metrics JMX domain name. */ @@ -57,4 +62,12 @@ public void setStep(Duration step) { this.step = step; } + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/jmx/JmxPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/jmx/JmxPropertiesConfigAdapter.java index a38d98135f9d..8c48673d1d61 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/jmx/JmxPropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/jmx/JmxPropertiesConfigAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,11 @@ class JmxPropertiesConfigAdapter extends PropertiesConfigAdapter super(properties); } + @Override + public String prefix() { + return "management.metrics.export.jmx"; + } + @Override public String get(String key) { return null; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/kairos/KairosMetricsExportAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/kairos/KairosMetricsExportAutoConfiguration.java index 3e9430ae6ca0..1213e87df9fe 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/kairos/KairosMetricsExportAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/kairos/KairosMetricsExportAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.export.ConditionalOnEnabledMetricsExport; import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; @@ -30,7 +31,6 @@ 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.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -47,8 +47,7 @@ @AutoConfigureAfter(MetricsAutoConfiguration.class) @ConditionalOnBean(Clock.class) @ConditionalOnClass(KairosMeterRegistry.class) -@ConditionalOnProperty(prefix = "management.metrics.export.kairos", name = "enabled", havingValue = "true", - matchIfMissing = true) +@ConditionalOnEnabledMetricsExport("kairos") @EnableConfigurationProperties(KairosProperties.class) public class KairosMetricsExportAutoConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/kairos/KairosPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/kairos/KairosPropertiesConfigAdapter.java index 0a946f13c7fa..13f63c58f754 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/kairos/KairosPropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/kairos/KairosPropertiesConfigAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,6 +32,11 @@ class KairosPropertiesConfigAdapter extends StepRegistryPropertiesConfigAdapter< super(properties); } + @Override + public String prefix() { + return "management.metrics.export.kairos"; + } + @Override public String uri() { return get(KairosProperties::getUri, KairosConfig.super::uri); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/newrelic/NewRelicMetricsExportAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/newrelic/NewRelicMetricsExportAutoConfiguration.java index a666810ef221..365548599937 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/newrelic/NewRelicMetricsExportAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/newrelic/NewRelicMetricsExportAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,11 +18,16 @@ import io.micrometer.core.instrument.Clock; import io.micrometer.core.ipc.http.HttpUrlConnectionSender; +import io.micrometer.newrelic.ClientProviderType; +import io.micrometer.newrelic.NewRelicClientProvider; import io.micrometer.newrelic.NewRelicConfig; +import io.micrometer.newrelic.NewRelicInsightsAgentClientProvider; +import io.micrometer.newrelic.NewRelicInsightsApiClientProvider; import io.micrometer.newrelic.NewRelicMeterRegistry; import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.export.ConditionalOnEnabledMetricsExport; import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; @@ -30,7 +35,6 @@ 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.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -48,8 +52,7 @@ @AutoConfigureAfter(MetricsAutoConfiguration.class) @ConditionalOnBean(Clock.class) @ConditionalOnClass(NewRelicMeterRegistry.class) -@ConditionalOnProperty(prefix = "management.metrics.export.newrelic", name = "enabled", havingValue = "true", - matchIfMissing = true) +@ConditionalOnEnabledMetricsExport("newrelic") @EnableConfigurationProperties(NewRelicProperties.class) public class NewRelicMetricsExportAutoConfiguration { @@ -67,11 +70,21 @@ public NewRelicConfig newRelicConfig() { @Bean @ConditionalOnMissingBean - public NewRelicMeterRegistry newRelicMeterRegistry(NewRelicConfig newRelicConfig, Clock clock) { - return NewRelicMeterRegistry.builder(newRelicConfig).clock(clock).httpClient( - new HttpUrlConnectionSender(this.properties.getConnectTimeout(), this.properties.getReadTimeout())) - .build(); + public NewRelicClientProvider newRelicClientProvider(NewRelicConfig newRelicConfig) { + if (newRelicConfig.clientProviderType() == ClientProviderType.INSIGHTS_AGENT) { + return new NewRelicInsightsAgentClientProvider(newRelicConfig); + } + return new NewRelicInsightsApiClientProvider(newRelicConfig, + new HttpUrlConnectionSender(this.properties.getConnectTimeout(), this.properties.getReadTimeout())); + + } + @Bean + @ConditionalOnMissingBean + public NewRelicMeterRegistry newRelicMeterRegistry(NewRelicConfig newRelicConfig, Clock clock, + NewRelicClientProvider newRelicClientProvider) { + return NewRelicMeterRegistry.builder(newRelicConfig).clock(clock).clientProvider(newRelicClientProvider) + .build(); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/newrelic/NewRelicProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/newrelic/NewRelicProperties.java index de7370e6c8bb..62d0075e725b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/newrelic/NewRelicProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/newrelic/NewRelicProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.boot.actuate.autoconfigure.metrics.export.newrelic; +import io.micrometer.newrelic.ClientProviderType; + import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.StepRegistryProperties; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -46,6 +48,11 @@ public class NewRelicProperties extends StepRegistryProperties { */ private String eventType = "SpringBootSample"; + /** + * Client provider type to use. + */ + private ClientProviderType clientProviderType = ClientProviderType.INSIGHTS_API; + /** * New Relic API key. */ @@ -77,6 +84,14 @@ public void setEventType(String eventType) { this.eventType = eventType; } + public ClientProviderType getClientProviderType() { + return this.clientProviderType; + } + + public void setClientProviderType(ClientProviderType clientProviderType) { + this.clientProviderType = clientProviderType; + } + public String getApiKey() { return this.apiKey; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/newrelic/NewRelicPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/newrelic/NewRelicPropertiesConfigAdapter.java index d684df089f13..60e0a336259e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/newrelic/NewRelicPropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/newrelic/NewRelicPropertiesConfigAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.boot.actuate.autoconfigure.metrics.export.newrelic; +import io.micrometer.newrelic.ClientProviderType; import io.micrometer.newrelic.NewRelicConfig; import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.StepRegistryPropertiesConfigAdapter; @@ -34,6 +35,11 @@ public NewRelicPropertiesConfigAdapter(NewRelicProperties properties) { super(properties); } + @Override + public String prefix() { + return "management.metrics.export.newrelic"; + } + @Override public boolean meterNameEventTypeEnabled() { return get(NewRelicProperties::isMeterNameEventTypeEnabled, NewRelicConfig.super::meterNameEventTypeEnabled); @@ -44,6 +50,11 @@ public String eventType() { return get(NewRelicProperties::getEventType, NewRelicConfig.super::eventType); } + @Override + public ClientProviderType clientProviderType() { + return get(NewRelicProperties::getClientProviderType, NewRelicConfig.super::clientProviderType); + } + @Override public String apiKey() { return get(NewRelicProperties::getApiKey, NewRelicConfig.super::apiKey); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/package-info.java new file mode 100644 index 000000000000..8631a0f1dd65 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for metrics exporter. + */ +package org.springframework.boot.actuate.autoconfigure.metrics.export; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusMetricsExportAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusMetricsExportAutoConfiguration.java index b6d18db0dcf1..fa027bb45de9 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusMetricsExportAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusMetricsExportAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import io.micrometer.prometheus.PrometheusConfig; import io.micrometer.prometheus.PrometheusMeterRegistry; import io.prometheus.client.CollectorRegistry; +import io.prometheus.client.exporter.BasicAuthHttpConnectionFactory; import io.prometheus.client.exporter.PushGateway; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -32,6 +33,7 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.export.ConditionalOnEnabledMetricsExport; import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusPushGatewayManager; import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusPushGatewayManager.ShutdownOperation; @@ -48,21 +50,21 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import org.springframework.core.log.LogMessage; +import org.springframework.util.StringUtils; /** * {@link EnableAutoConfiguration Auto-configuration} for exporting metrics to Prometheus. * - * @since 2.0.0 * @author Jon Schneider * @author David J. M. Karlsen + * @since 2.0.0 */ @Configuration(proxyBeanMethods = false) @AutoConfigureBefore({ CompositeMeterRegistryAutoConfiguration.class, SimpleMetricsExportAutoConfiguration.class }) @AutoConfigureAfter(MetricsAutoConfiguration.class) @ConditionalOnBean(Clock.class) @ConditionalOnClass(PrometheusMeterRegistry.class) -@ConditionalOnProperty(prefix = "management.metrics.export.prometheus", name = "enabled", havingValue = "true", - matchIfMissing = true) +@ConditionalOnEnabledMetricsExport("prometheus") @EnableConfigurationProperties(PrometheusProperties.class) public class PrometheusMetricsExportAutoConfiguration { @@ -124,11 +126,16 @@ public PrometheusPushGatewayManager prometheusPushGatewayManager(CollectorRegist String job = getJob(properties, environment); Map groupingKey = properties.getGroupingKey(); ShutdownOperation shutdownOperation = properties.getShutdownOperation(); - return new PrometheusPushGatewayManager(getPushGateway(properties.getBaseUrl()), collectorRegistry, - pushRate, job, groupingKey, shutdownOperation); + PushGateway pushGateway = initializePushGateway(properties.getBaseUrl()); + if (StringUtils.hasText(properties.getUsername())) { + pushGateway.setConnectionFactory( + new BasicAuthHttpConnectionFactory(properties.getUsername(), properties.getPassword())); + } + return new PrometheusPushGatewayManager(pushGateway, collectorRegistry, pushRate, job, groupingKey, + shutdownOperation); } - private PushGateway getPushGateway(String url) { + private PushGateway initializePushGateway(String url) { try { return new PushGateway(new URL(url)); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusProperties.java index e530ea900235..41b7845f62e4 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,8 @@ import java.util.HashMap; import java.util.Map; +import io.micrometer.prometheus.HistogramFlavor; + import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusPushGatewayManager.ShutdownOperation; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -34,6 +36,11 @@ @ConfigurationProperties(prefix = "management.metrics.export.prometheus") public class PrometheusProperties { + /** + * Whether exporting of metrics to this backend is enabled. + */ + private boolean enabled = true; + /** * Whether to enable publishing descriptions as part of the scrape payload to * Prometheus. Turn this off to minimize the amount of data sent on each scrape. @@ -46,6 +53,11 @@ public class PrometheusProperties { */ private final Pushgateway pushgateway = new Pushgateway(); + /** + * Histogram type for backing DistributionSummary and Timer. + */ + private HistogramFlavor histogramFlavor = HistogramFlavor.Prometheus; + /** * Step size (i.e. reporting frequency) to use. */ @@ -59,6 +71,14 @@ public void setDescriptions(boolean descriptions) { this.descriptions = descriptions; } + public HistogramFlavor getHistogramFlavor() { + return this.histogramFlavor; + } + + public void setHistogramFlavor(HistogramFlavor histogramFlavor) { + this.histogramFlavor = histogramFlavor; + } + public Duration getStep() { return this.step; } @@ -67,6 +87,14 @@ public void setStep(Duration step) { this.step = step; } + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + public Pushgateway getPushgateway() { return this.pushgateway; } @@ -86,6 +114,16 @@ public static class Pushgateway { */ private String baseUrl = "http://localhost:9091"; + /** + * Login user of the Prometheus Pushgateway. + */ + private String username; + + /** + * Login password of the Prometheus Pushgateway. + */ + private String password; + /** * Frequency with which to push metrics. */ @@ -122,6 +160,22 @@ public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; } + public String getUsername() { + return this.username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return this.password; + } + + public void setPassword(String password) { + this.password = password; + } + public Duration getPushRate() { return this.pushRate; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesConfigAdapter.java index 0a4ff1204e80..f44f7668919a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesConfigAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.time.Duration; +import io.micrometer.prometheus.HistogramFlavor; import io.micrometer.prometheus.PrometheusConfig; import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.PropertiesConfigAdapter; @@ -35,6 +36,11 @@ class PrometheusPropertiesConfigAdapter extends PropertiesConfigAdapter + implements StackdriverConfig { + + public StackdriverPropertiesConfigAdapter(StackdriverProperties properties) { + super(properties); + } + + @Override + public String prefix() { + return "management.metrics.export.stackdriver"; + } + + @Override + public String projectId() { + return get(StackdriverProperties::getProjectId, StackdriverConfig.super::projectId); + } + + @Override + public String resourceType() { + return get(StackdriverProperties::getResourceType, StackdriverConfig.super::resourceType); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/stackdriver/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/stackdriver/package-info.java new file mode 100644 index 000000000000..df40a7adb9dc --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/stackdriver/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Support for exporting actuator metrics to Stackdriver. + */ +package org.springframework.boot.actuate.autoconfigure.metrics.export.stackdriver; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/statsd/StatsdMetricsExportAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/statsd/StatsdMetricsExportAutoConfiguration.java index b692e032f681..3838da8dcf6c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/statsd/StatsdMetricsExportAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/statsd/StatsdMetricsExportAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,10 +19,10 @@ import io.micrometer.core.instrument.Clock; import io.micrometer.statsd.StatsdConfig; import io.micrometer.statsd.StatsdMeterRegistry; -import io.micrometer.statsd.StatsdMetrics; import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.export.ConditionalOnEnabledMetricsExport; import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; @@ -30,7 +30,6 @@ 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.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -46,8 +45,7 @@ @AutoConfigureAfter(MetricsAutoConfiguration.class) @ConditionalOnBean(Clock.class) @ConditionalOnClass(StatsdMeterRegistry.class) -@ConditionalOnProperty(prefix = "management.metrics.export.statsd", name = "enabled", havingValue = "true", - matchIfMissing = true) +@ConditionalOnEnabledMetricsExport("statsd") @EnableConfigurationProperties(StatsdProperties.class) public class StatsdMetricsExportAutoConfiguration { @@ -63,9 +61,4 @@ public StatsdMeterRegistry statsdMeterRegistry(StatsdConfig statsdConfig, Clock return new StatsdMeterRegistry(statsdConfig, clock); } - @Bean - public StatsdMetrics statsdMetrics() { - return new StatsdMetrics(); - } - } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/statsd/StatsdProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/statsd/StatsdProperties.java index dcaf939101a1..7764813d87b6 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/statsd/StatsdProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/statsd/StatsdProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.time.Duration; import io.micrometer.statsd.StatsdFlavor; +import io.micrometer.statsd.StatsdProtocol; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -53,6 +54,11 @@ public class StatsdProperties { */ private Integer port = 8125; + /** + * Protocol of the StatsD server to receive exported metrics. + */ + private StatsdProtocol protocol = StatsdProtocol.UDP; + /** * Total length of a single payload should be kept within your network's MTU. */ @@ -102,6 +108,14 @@ public void setPort(Integer port) { this.port = port; } + public StatsdProtocol getProtocol() { + return this.protocol; + } + + public void setProtocol(StatsdProtocol protocol) { + this.protocol = protocol; + } + public Integer getMaxPacketLength() { return this.maxPacketLength; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/statsd/StatsdPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/statsd/StatsdPropertiesConfigAdapter.java index aa764b3a93c8..9913faa156e6 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/statsd/StatsdPropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/statsd/StatsdPropertiesConfigAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import io.micrometer.statsd.StatsdConfig; import io.micrometer.statsd.StatsdFlavor; +import io.micrometer.statsd.StatsdProtocol; import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.PropertiesConfigAdapter; @@ -40,6 +41,11 @@ public String get(String s) { return null; } + @Override + public String prefix() { + return "management.metrics.export.statsd"; + } + @Override public StatsdFlavor flavor() { return get(StatsdProperties::getFlavor, StatsdConfig.super::flavor); @@ -60,6 +66,11 @@ public int port() { return get(StatsdProperties::getPort, StatsdConfig.super::port); } + @Override + public StatsdProtocol protocol() { + return get(StatsdProperties::getProtocol, StatsdConfig.super::protocol); + } + @Override public int maxPacketLength() { return get(StatsdProperties::getMaxPacketLength, StatsdConfig.super::maxPacketLength); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontMetricsExportAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontMetricsExportAutoConfiguration.java index 3137dc2ed6a7..9ad48ca40abd 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontMetricsExportAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontMetricsExportAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,39 +16,45 @@ package org.springframework.boot.actuate.autoconfigure.metrics.export.wavefront; +import java.time.Duration; + +import com.wavefront.sdk.common.WavefrontSender; +import com.wavefront.sdk.common.clients.WavefrontClient.Builder; import io.micrometer.core.instrument.Clock; -import io.micrometer.core.ipc.http.HttpUrlConnectionSender; import io.micrometer.wavefront.WavefrontConfig; import io.micrometer.wavefront.WavefrontMeterRegistry; import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.export.ConditionalOnEnabledMetricsExport; import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.export.wavefront.WavefrontProperties.Sender; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 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.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.util.unit.DataSize; /** * {@link EnableAutoConfiguration Auto-configuration} for exporting metrics to Wavefront. * * @author Jon Schneider * @author Artsiom Yudovin + * @author Stephane Nicoll * @since 2.0.0 */ @Configuration(proxyBeanMethods = false) @AutoConfigureBefore({ CompositeMeterRegistryAutoConfiguration.class, SimpleMetricsExportAutoConfiguration.class }) @AutoConfigureAfter(MetricsAutoConfiguration.class) @ConditionalOnBean(Clock.class) -@ConditionalOnClass(WavefrontMeterRegistry.class) -@ConditionalOnProperty(prefix = "management.metrics.export.wavefront", name = "enabled", havingValue = "true", - matchIfMissing = true) +@ConditionalOnClass({ WavefrontMeterRegistry.class, WavefrontSender.class }) +@ConditionalOnEnabledMetricsExport("wavefront") @EnableConfigurationProperties(WavefrontProperties.class) public class WavefrontMetricsExportAutoConfiguration { @@ -66,10 +72,25 @@ public WavefrontConfig wavefrontConfig() { @Bean @ConditionalOnMissingBean - public WavefrontMeterRegistry wavefrontMeterRegistry(WavefrontConfig wavefrontConfig, Clock clock) { - return WavefrontMeterRegistry.builder(wavefrontConfig).clock(clock).httpClient( - new HttpUrlConnectionSender(this.properties.getConnectTimeout(), this.properties.getReadTimeout())) - .build(); + public WavefrontSender wavefrontSender(WavefrontConfig wavefrontConfig) { + return createWavefrontSender(wavefrontConfig); + } + + @Bean + @ConditionalOnMissingBean + public WavefrontMeterRegistry wavefrontMeterRegistry(WavefrontConfig wavefrontConfig, Clock clock, + WavefrontSender wavefrontSender) { + return WavefrontMeterRegistry.builder(wavefrontConfig).clock(clock).wavefrontSender(wavefrontSender).build(); + } + + private WavefrontSender createWavefrontSender(WavefrontConfig wavefrontConfig) { + Builder builder = WavefrontMeterRegistry.getDefaultSenderBuilder(wavefrontConfig); + PropertyMapper mapper = PropertyMapper.get().alwaysApplyingWhenNonNull(); + Sender sender = this.properties.getSender(); + mapper.from(sender.getMaxQueueSize()).to(builder::maxQueueSize); + mapper.from(sender.getFlushInterval()).asInt(Duration::getSeconds).to(builder::flushIntervalSeconds); + mapper.from(sender.getMessageSize()).asInt(DataSize::toBytes).to(builder::messageSizeBytes); + return builder.build(); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontProperties.java index 3546d20ccf41..29cbdb4f04b5 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,22 +21,19 @@ import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.PushRegistryProperties; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.util.unit.DataSize; /** * {@link ConfigurationProperties @ConfigurationProperties} for configuring Wavefront * metrics export. * * @author Jon Schneider + * @author Stephane Nicoll * @since 2.0.0 */ @ConfigurationProperties("management.metrics.export.wavefront") public class WavefrontProperties extends PushRegistryProperties { - /** - * Step size (i.e. reporting frequency) to use. - */ - private Duration step = Duration.ofSeconds(10); - /** * URI to ship metrics to. */ @@ -60,6 +57,8 @@ public class WavefrontProperties extends PushRegistryProperties { */ private String globalPrefix; + private final Sender sender = new Sender(); + public URI getUri() { return this.uri; } @@ -68,16 +67,6 @@ public void setUri(URI uri) { this.uri = uri; } - @Override - public Duration getStep() { - return this.step; - } - - @Override - public void setStep(Duration step) { - this.step = step; - } - public String getSource() { return this.source; } @@ -102,4 +91,42 @@ public void setGlobalPrefix(String globalPrefix) { this.globalPrefix = globalPrefix; } + public Sender getSender() { + return this.sender; + } + + public static class Sender { + + private int maxQueueSize = 50000; + + private Duration flushInterval = Duration.ofSeconds(1); + + private DataSize messageSize = DataSize.ofBytes(Integer.MAX_VALUE); + + public int getMaxQueueSize() { + return this.maxQueueSize; + } + + public void setMaxQueueSize(int maxQueueSize) { + this.maxQueueSize = maxQueueSize; + } + + public Duration getFlushInterval() { + return this.flushInterval; + } + + public void setFlushInterval(Duration flushInterval) { + this.flushInterval = flushInterval; + } + + public DataSize getMessageSize() { + return this.messageSize; + } + + public void setMessageSize(DataSize messageSize) { + this.messageSize = messageSize; + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontPropertiesConfigAdapter.java index 88a6897305ae..22b0716479c1 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontPropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontPropertiesConfigAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,11 @@ public WavefrontPropertiesConfigAdapter(WavefrontProperties properties) { super(properties); } + @Override + public String prefix() { + return "management.metrics.export.wavefront"; + } + @Override public String get(String k) { return null; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/integration/IntegrationMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/integration/IntegrationMetricsAutoConfiguration.java new file mode 100644 index 000000000000..9bd2a75a45fd --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/integration/IntegrationMetricsAutoConfiguration.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.metrics.integration; + +import io.micrometer.core.instrument.MeterRegistry; + +import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration; +import org.springframework.context.annotation.Configuration; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Spring Integration's metrics. + * Orders auto-configuration classes to ensure that the {@link MeterRegistry} bean has + * been defined before Spring Integration's Micrometer support queries the bean factory + * for it. + * + * @author Andy Wilkinson + */ +@AutoConfigureAfter({ MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class }) +@AutoConfigureBefore(IntegrationAutoConfiguration.class) +@Configuration(proxyBeanMethods = false) +class IntegrationMetricsAutoConfiguration { + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/integration/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/integration/package-info.java new file mode 100644 index 000000000000..6172eeb26a64 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/integration/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for Spring Integration metrics. + */ +package org.springframework.boot.actuate.autoconfigure.metrics.integration; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/jdbc/DataSourcePoolMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/jdbc/DataSourcePoolMetricsAutoConfiguration.java index 0a3dc751056c..60a4e46436bb 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/jdbc/DataSourcePoolMetricsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/jdbc/DataSourcePoolMetricsAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import javax.sql.DataSource; +import com.zaxxer.hikari.HikariConfigMXBean; import com.zaxxer.hikari.HikariDataSource; import com.zaxxer.hikari.metrics.micrometer.MicrometerMetricsTrackerFactory; import io.micrometer.core.instrument.MeterRegistry; @@ -112,7 +113,8 @@ static class HikariDataSourceMetricsConfiguration { @Autowired void bindMetricsRegistryToHikariDataSources(Collection dataSources) { for (DataSource dataSource : dataSources) { - HikariDataSource hikariDataSource = DataSourceUnwrapper.unwrap(dataSource, HikariDataSource.class); + HikariDataSource hikariDataSource = DataSourceUnwrapper.unwrap(dataSource, HikariConfigMXBean.class, + HikariDataSource.class); if (hikariDataSource != null) { bindMetricsRegistryToHikariDataSource(hikariDataSource); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/mongo/MongoMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/mongo/MongoMetricsAutoConfiguration.java new file mode 100644 index 000000000000..a78f8b77005f --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/mongo/MongoMetricsAutoConfiguration.java @@ -0,0 +1,110 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.metrics.mongo; + +import com.mongodb.MongoClientSettings; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.binder.mongodb.DefaultMongoCommandTagsProvider; +import io.micrometer.core.instrument.binder.mongodb.DefaultMongoConnectionPoolTagsProvider; +import io.micrometer.core.instrument.binder.mongodb.MongoCommandTagsProvider; +import io.micrometer.core.instrument.binder.mongodb.MongoConnectionPoolTagsProvider; +import io.micrometer.core.instrument.binder.mongodb.MongoMetricsCommandListener; +import io.micrometer.core.instrument.binder.mongodb.MongoMetricsConnectionPoolListener; + +import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +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.mongo.MongoAutoConfiguration; +import org.springframework.boot.autoconfigure.mongo.MongoClientSettingsBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Mongo metrics. + * + * @author Chris Bono + * @author Jonatan Ivanov + * @since 2.5.0 + */ +@Configuration(proxyBeanMethods = false) +@AutoConfigureBefore(MongoAutoConfiguration.class) +@AutoConfigureAfter({ MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class }) +@ConditionalOnClass(MongoClientSettings.class) +@ConditionalOnBean(MeterRegistry.class) +public class MongoMetricsAutoConfiguration { + + @ConditionalOnClass(MongoMetricsCommandListener.class) + @ConditionalOnProperty(name = "management.metrics.mongo.command.enabled", havingValue = "true", + matchIfMissing = true) + static class MongoCommandMetricsConfiguration { + + @Bean + @ConditionalOnMissingBean + MongoMetricsCommandListener mongoMetricsCommandListener(MeterRegistry meterRegistry, + MongoCommandTagsProvider mongoCommandTagsProvider) { + return new MongoMetricsCommandListener(meterRegistry, mongoCommandTagsProvider); + } + + @Bean + @ConditionalOnMissingBean + MongoCommandTagsProvider mongoCommandTagsProvider() { + return new DefaultMongoCommandTagsProvider(); + } + + @Bean + MongoClientSettingsBuilderCustomizer mongoMetricsCommandListenerClientSettingsBuilderCustomizer( + MongoMetricsCommandListener mongoMetricsCommandListener) { + return (clientSettingsBuilder) -> clientSettingsBuilder.addCommandListener(mongoMetricsCommandListener); + } + + } + + @ConditionalOnClass(MongoMetricsConnectionPoolListener.class) + @ConditionalOnProperty(name = "management.metrics.mongo.connectionpool.enabled", havingValue = "true", + matchIfMissing = true) + static class MongoConnectionPoolMetricsConfiguration { + + @Bean + @ConditionalOnMissingBean + MongoMetricsConnectionPoolListener mongoMetricsConnectionPoolListener(MeterRegistry meterRegistry, + MongoConnectionPoolTagsProvider mongoConnectionPoolTagsProvider) { + return new MongoMetricsConnectionPoolListener(meterRegistry, mongoConnectionPoolTagsProvider); + } + + @Bean + @ConditionalOnMissingBean + MongoConnectionPoolTagsProvider mongoConnectionPoolTagsProvider() { + return new DefaultMongoConnectionPoolTagsProvider(); + } + + @Bean + MongoClientSettingsBuilderCustomizer mongoMetricsConnectionPoolListenerClientSettingsBuilderCustomizer( + MongoMetricsConnectionPoolListener mongoMetricsConnectionPoolListener) { + return (clientSettingsBuilder) -> clientSettingsBuilder + .applyToConnectionPoolSettings((connectionPoolSettingsBuilder) -> connectionPoolSettingsBuilder + .addConnectionPoolListener(mongoMetricsConnectionPoolListener)); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/mongo/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/mongo/package-info.java new file mode 100644 index 000000000000..ac78ff948a42 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/mongo/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for Mongo metrics. + */ +package org.springframework.boot.actuate.autoconfigure.metrics.mongo; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/orm/jpa/HibernateMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/orm/jpa/HibernateMetricsAutoConfiguration.java index fa111f2b21cb..baf55db1fb04 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/orm/jpa/HibernateMetricsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/orm/jpa/HibernateMetricsAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,10 +23,10 @@ import javax.persistence.PersistenceException; import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.binder.jpa.HibernateMetrics; import org.hibernate.SessionFactory; +import org.hibernate.stat.HibernateMetrics; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureAfter; @@ -48,13 +48,27 @@ @Configuration(proxyBeanMethods = false) @AutoConfigureAfter({ MetricsAutoConfiguration.class, HibernateJpaAutoConfiguration.class, SimpleMetricsExportAutoConfiguration.class }) -@ConditionalOnClass({ EntityManagerFactory.class, SessionFactory.class, MeterRegistry.class }) +@ConditionalOnClass({ EntityManagerFactory.class, SessionFactory.class, HibernateMetrics.class, MeterRegistry.class }) @ConditionalOnBean({ EntityManagerFactory.class, MeterRegistry.class }) -public class HibernateMetricsAutoConfiguration { +public class HibernateMetricsAutoConfiguration implements SmartInitializingSingleton { private static final String ENTITY_MANAGER_FACTORY_SUFFIX = "entityManagerFactory"; - @Autowired + private final Map entityManagerFactories; + + private final MeterRegistry meterRegistry; + + public HibernateMetricsAutoConfiguration(Map entityManagerFactories, + MeterRegistry meterRegistry) { + this.entityManagerFactories = entityManagerFactories; + this.meterRegistry = meterRegistry; + } + + @Override + public void afterSingletonsInstantiated() { + bindEntityManagerFactoriesToRegistry(this.entityManagerFactories, this.meterRegistry); + } + public void bindEntityManagerFactoriesToRegistry(Map entityManagerFactories, MeterRegistry registry) { entityManagerFactories.forEach((name, factory) -> bindEntityManagerFactoryToRegistry(name, factory, registry)); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/r2dbc/ConnectionPoolMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/r2dbc/ConnectionPoolMetricsAutoConfiguration.java new file mode 100644 index 000000000000..8843b0f1bdb7 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/r2dbc/ConnectionPoolMetricsAutoConfiguration.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.metrics.r2dbc; + +import java.util.Map; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.r2dbc.pool.ConnectionPool; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.Wrapped; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; +import org.springframework.boot.actuate.metrics.r2dbc.ConnectionPoolMetrics; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; +import org.springframework.context.annotation.Configuration; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for metrics on all available + * {@link ConnectionFactory R2DBC connection factories}. + * + * @author Tadaya Tsuyukubo + * @author Stephane Nicoll + * @since 2.3.0 + */ +@Configuration(proxyBeanMethods = false) +@AutoConfigureAfter({ MetricsAutoConfiguration.class, SimpleMetricsExportAutoConfiguration.class, + R2dbcAutoConfiguration.class }) +@ConditionalOnClass({ ConnectionPool.class, MeterRegistry.class }) +@ConditionalOnBean({ ConnectionFactory.class, MeterRegistry.class }) +public class ConnectionPoolMetricsAutoConfiguration { + + @Autowired + public void bindConnectionPoolsToRegistry(Map connectionFactories, + MeterRegistry registry) { + connectionFactories.forEach((beanName, connectionFactory) -> { + ConnectionPool pool = extractPool(connectionFactory); + if (pool != null) { + new ConnectionPoolMetrics(pool, beanName, Tags.empty()).bindTo(registry); + } + }); + } + + private ConnectionPool extractPool(Object candidate) { + if (candidate instanceof ConnectionPool) { + return (ConnectionPool) candidate; + } + if (candidate instanceof Wrapped) { + return extractPool(((Wrapped) candidate).unwrap()); + } + return null; + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/r2dbc/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/r2dbc/package-info.java new file mode 100644 index 000000000000..14f5c1fe0b17 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/r2dbc/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for R2DBC metrics. + */ +package org.springframework.boot.actuate.autoconfigure.metrics.r2dbc; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/HttpClientMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/HttpClientMetricsAutoConfiguration.java index a5f04c8a022e..206b0d76dc44 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/HttpClientMetricsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/HttpClientMetricsAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.config.MeterFilter; +import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties; import org.springframework.boot.actuate.autoconfigure.metrics.OnlyOnceLoggingDenyMeterFilter; @@ -43,8 +44,8 @@ * @since 2.1.0 */ @Configuration(proxyBeanMethods = false) -@AutoConfigureAfter({ MetricsAutoConfiguration.class, SimpleMetricsExportAutoConfiguration.class, - RestTemplateAutoConfiguration.class }) +@AutoConfigureAfter({ MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class, + SimpleMetricsExportAutoConfiguration.class, RestTemplateAutoConfiguration.class }) @ConditionalOnClass(MeterRegistry.class) @ConditionalOnBean(MeterRegistry.class) @Import({ RestTemplateMetricsConfiguration.class, WebClientMetricsConfiguration.class }) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientMetricsConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientMetricsConfiguration.java index cb9609c2322b..994f7b25db78 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientMetricsConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientMetricsConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/jetty/JettyMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/jetty/JettyMetricsAutoConfiguration.java index 474636736e15..bd74a82dbaa0 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/jetty/JettyMetricsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/jetty/JettyMetricsAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,9 @@ import io.micrometer.core.instrument.binder.jetty.JettyServerThreadPoolMetrics; import org.eclipse.jetty.server.Server; +import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; import org.springframework.boot.actuate.metrics.web.jetty.JettyServerThreadPoolMetricsBinder; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -38,6 +40,7 @@ @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication @ConditionalOnClass({ JettyServerThreadPoolMetrics.class, Server.class }) +@AutoConfigureAfter(CompositeMeterRegistryAutoConfiguration.class) public class JettyMetricsAutoConfiguration { @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/WebFluxMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/WebFluxMetricsAutoConfiguration.java index 670e896d1625..2975e76f710f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/WebFluxMetricsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/WebFluxMetricsAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,13 @@ package org.springframework.boot.actuate.autoconfigure.metrics.web.reactive; +import java.util.stream.Collectors; + import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.config.MeterFilter; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties.Web.Server.ServerRequest; @@ -26,6 +30,7 @@ import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; import org.springframework.boot.actuate.metrics.web.reactive.server.DefaultWebFluxTagsProvider; import org.springframework.boot.actuate.metrics.web.reactive.server.MetricsWebFilter; +import org.springframework.boot.actuate.metrics.web.reactive.server.WebFluxTagsContributor; import org.springframework.boot.actuate.metrics.web.reactive.server.WebFluxTagsProvider; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -45,7 +50,8 @@ * @since 2.0.0 */ @Configuration(proxyBeanMethods = false) -@AutoConfigureAfter({ MetricsAutoConfiguration.class, SimpleMetricsExportAutoConfiguration.class }) +@AutoConfigureAfter({ MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class, + SimpleMetricsExportAutoConfiguration.class }) @ConditionalOnBean(MeterRegistry.class) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) public class WebFluxMetricsAutoConfiguration { @@ -58,8 +64,9 @@ public WebFluxMetricsAutoConfiguration(MetricsProperties properties) { @Bean @ConditionalOnMissingBean(WebFluxTagsProvider.class) - public DefaultWebFluxTagsProvider webfluxTagConfigurer() { - return new DefaultWebFluxTagsProvider(); + public DefaultWebFluxTagsProvider webFluxTagsProvider(ObjectProvider contributors) { + return new DefaultWebFluxTagsProvider(this.properties.getWeb().getServer().getRequest().isIgnoreTrailingSlash(), + contributors.orderedStream().collect(Collectors.toList())); } @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/servlet/WebMvcMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/servlet/WebMvcMetricsAutoConfiguration.java index 7b81bf897290..5647036689e2 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/servlet/WebMvcMetricsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/servlet/WebMvcMetricsAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,15 @@ package org.springframework.boot.actuate.autoconfigure.metrics.web.servlet; +import java.util.stream.Collectors; + import javax.servlet.DispatcherType; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.config.MeterFilter; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties.Web.Server.ServerRequest; @@ -29,6 +33,7 @@ import org.springframework.boot.actuate.metrics.web.servlet.DefaultWebMvcTagsProvider; import org.springframework.boot.actuate.metrics.web.servlet.LongTaskTimingHandlerInterceptor; import org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter; +import org.springframework.boot.actuate.metrics.web.servlet.WebMvcTagsContributor; import org.springframework.boot.actuate.metrics.web.servlet.WebMvcTagsProvider; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -55,7 +60,8 @@ * @since 2.0.0 */ @Configuration(proxyBeanMethods = false) -@AutoConfigureAfter({ MetricsAutoConfiguration.class, SimpleMetricsExportAutoConfiguration.class }) +@AutoConfigureAfter({ MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class, + SimpleMetricsExportAutoConfiguration.class }) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) @ConditionalOnClass(DispatcherServlet.class) @ConditionalOnBean(MeterRegistry.class) @@ -70,8 +76,9 @@ public WebMvcMetricsAutoConfiguration(MetricsProperties properties) { @Bean @ConditionalOnMissingBean(WebMvcTagsProvider.class) - public DefaultWebMvcTagsProvider webMvcTagsProvider() { - return new DefaultWebMvcTagsProvider(); + public DefaultWebMvcTagsProvider webMvcTagsProvider(ObjectProvider contributors) { + return new DefaultWebMvcTagsProvider(this.properties.getWeb().getServer().getRequest().isIgnoreTrailingSlash(), + contributors.orderedStream().collect(Collectors.toList())); } @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/tomcat/TomcatMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/tomcat/TomcatMetricsAutoConfiguration.java index b17589773186..e3d17f6ee792 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/tomcat/TomcatMetricsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/tomcat/TomcatMetricsAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,9 @@ import io.micrometer.core.instrument.binder.tomcat.TomcatMetrics; import org.apache.catalina.Manager; +import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; import org.springframework.boot.actuate.metrics.web.tomcat.TomcatMetricsBinder; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -38,6 +40,7 @@ @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication @ConditionalOnClass({ TomcatMetrics.class, Manager.class }) +@AutoConfigureAfter(CompositeMeterRegistryAutoConfiguration.class) public class TomcatMetricsAutoConfiguration { @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorAutoConfiguration.java index 9f24c18323b9..7730ddf6293e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,42 +16,36 @@ package org.springframework.boot.actuate.autoconfigure.neo4j; -import java.util.Map; +import org.neo4j.driver.Driver; -import org.neo4j.ogm.session.SessionFactory; - -import org.springframework.boot.actuate.autoconfigure.health.CompositeHealthContributorConfiguration; import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; -import org.springframework.boot.actuate.health.HealthContributor; +import org.springframework.boot.actuate.autoconfigure.neo4j.Neo4jHealthContributorConfigurations.Neo4jConfiguration; +import org.springframework.boot.actuate.autoconfigure.neo4j.Neo4jHealthContributorConfigurations.Neo4jReactiveConfiguration; import org.springframework.boot.actuate.neo4j.Neo4jHealthIndicator; +import org.springframework.boot.actuate.neo4j.Neo4jReactiveHealthIndicator; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 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.data.neo4j.Neo4jDataAutoConfiguration; -import org.springframework.context.annotation.Bean; +import org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; /** - * {@link EnableAutoConfiguration Auto-configuration} for {@link Neo4jHealthIndicator}. + * {@link EnableAutoConfiguration Auto-configuration} for + * {@link Neo4jReactiveHealthIndicator} and {@link Neo4jHealthIndicator}. * * @author Eric Spiegelberg * @author Stephane Nicoll + * @author Michael J. Simons * @since 2.0.0 */ @Configuration(proxyBeanMethods = false) -@ConditionalOnClass(SessionFactory.class) -@ConditionalOnBean(SessionFactory.class) +@ConditionalOnClass(Driver.class) +@ConditionalOnBean(Driver.class) @ConditionalOnEnabledHealthIndicator("neo4j") -@AutoConfigureAfter(Neo4jDataAutoConfiguration.class) -public class Neo4jHealthContributorAutoConfiguration - extends CompositeHealthContributorConfiguration { - - @Bean - @ConditionalOnMissingBean(name = { "neo4jHealthIndicator", "neo4jHealthContributor" }) - public HealthContributor neo4jHealthContributor(Map sessionFactories) { - return createContributor(sessionFactories); - } +@AutoConfigureAfter(Neo4jAutoConfiguration.class) +@Import({ Neo4jReactiveConfiguration.class, Neo4jConfiguration.class }) +public class Neo4jHealthContributorAutoConfiguration { } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorConfigurations.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorConfigurations.java new file mode 100644 index 000000000000..e53e14d27cb1 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorConfigurations.java @@ -0,0 +1,67 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.neo4j; + +import java.util.Map; + +import org.neo4j.driver.Driver; +import reactor.core.publisher.Flux; + +import org.springframework.boot.actuate.autoconfigure.health.CompositeHealthContributorConfiguration; +import org.springframework.boot.actuate.autoconfigure.health.CompositeReactiveHealthContributorConfiguration; +import org.springframework.boot.actuate.health.HealthContributor; +import org.springframework.boot.actuate.health.ReactiveHealthContributor; +import org.springframework.boot.actuate.neo4j.Neo4jHealthIndicator; +import org.springframework.boot.actuate.neo4j.Neo4jReactiveHealthIndicator; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Health contributor options for Neo4j. + * + * @author Michael J. Simons + * @author Stephane Nicoll + */ +class Neo4jHealthContributorConfigurations { + + @Configuration(proxyBeanMethods = false) + static class Neo4jConfiguration extends CompositeHealthContributorConfiguration { + + @Bean + @ConditionalOnMissingBean(name = { "neo4jHealthIndicator", "neo4jHealthContributor" }) + HealthContributor neo4jHealthContributor(Map drivers) { + return createContributor(drivers); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(Flux.class) + static class Neo4jReactiveConfiguration + extends CompositeReactiveHealthContributorConfiguration { + + @Bean + @ConditionalOnMissingBean(name = { "neo4jHealthIndicator", "neo4jHealthContributor" }) + ReactiveHealthContributor neo4jHealthContributor(Map drivers) { + return createContributor(drivers); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/package-info.java index 6e11d71c1f9c..71917019348d 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/package-info.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/neo4j/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/quartz/QuartzEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/quartz/QuartzEndpointAutoConfiguration.java new file mode 100644 index 000000000000..3fedb6413b7f --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/quartz/QuartzEndpointAutoConfiguration.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.quartz; + +import org.quartz.Scheduler; + +import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; +import org.springframework.boot.actuate.quartz.QuartzEndpoint; +import org.springframework.boot.actuate.quartz.QuartzEndpointWebExtension; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +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.quartz.QuartzAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for {@link QuartzEndpoint}. + * + * @author Vedran Pavic + * @author Stephane Nicoll + * @since 2.5.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(Scheduler.class) +@AutoConfigureAfter(QuartzAutoConfiguration.class) +@ConditionalOnAvailableEndpoint(endpoint = QuartzEndpoint.class) +public class QuartzEndpointAutoConfiguration { + + @Bean + @ConditionalOnBean(Scheduler.class) + @ConditionalOnMissingBean + public QuartzEndpoint quartzEndpoint(Scheduler scheduler) { + return new QuartzEndpoint(scheduler); + } + + @Bean + @ConditionalOnBean(QuartzEndpoint.class) + @ConditionalOnMissingBean + public QuartzEndpointWebExtension quartzEndpointWebExtension(QuartzEndpoint endpoint) { + return new QuartzEndpointWebExtension(endpoint); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/quartz/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/quartz/package-info.java new file mode 100644 index 000000000000..723fd16fd775 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/quartz/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for actuator Quartz Scheduler concerns. + */ +package org.springframework.boot.actuate.autoconfigure.quartz; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/r2dbc/ConnectionFactoryHealthContributorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/r2dbc/ConnectionFactoryHealthContributorAutoConfiguration.java new file mode 100644 index 000000000000..167800f00859 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/r2dbc/ConnectionFactoryHealthContributorAutoConfiguration.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.r2dbc; + +import java.util.Map; + +import io.r2dbc.spi.ConnectionFactory; + +import org.springframework.boot.actuate.autoconfigure.health.CompositeReactiveHealthContributorConfiguration; +import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; +import org.springframework.boot.actuate.health.ReactiveHealthContributor; +import org.springframework.boot.actuate.r2dbc.ConnectionFactoryHealthIndicator; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +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.r2dbc.R2dbcAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for + * {@link ConnectionFactoryHealthIndicator}. + * + * @author Mark Paluch + * @since 2.3.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(ConnectionFactory.class) +@ConditionalOnBean(ConnectionFactory.class) +@ConditionalOnEnabledHealthIndicator("r2dbc") +@AutoConfigureAfter(R2dbcAutoConfiguration.class) +public class ConnectionFactoryHealthContributorAutoConfiguration + extends CompositeReactiveHealthContributorConfiguration { + + private final Map connectionFactory; + + ConnectionFactoryHealthContributorAutoConfiguration(Map connectionFactory) { + this.connectionFactory = connectionFactory; + } + + @Bean + @ConditionalOnMissingBean(name = { "r2dbcHealthIndicator", "r2dbcHealthContributor" }) + public ReactiveHealthContributor r2dbcHealthContributor() { + return createContributor(this.connectionFactory); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/r2dbc/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/r2dbc/package-info.java new file mode 100644 index 000000000000..98cdee5f4112 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/r2dbc/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for actuator R2DBC. + */ +package org.springframework.boot.actuate.autoconfigure.r2dbc; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/EndpointRequest.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/EndpointRequest.java index 05ced303ebf9..0097d5299a54 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/EndpointRequest.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/EndpointRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -115,12 +115,41 @@ public static LinksServerWebExchangeMatcher toLinks() { return new LinksServerWebExchangeMatcher(); } + /** + * Base class for supported request matchers. + */ + private abstract static class AbstractWebExchangeMatcher extends ApplicationContextServerWebExchangeMatcher { + + private ManagementPortType managementPortType; + + AbstractWebExchangeMatcher(Class contextClass) { + super(contextClass); + } + + @Override + protected boolean ignoreApplicationContext(ApplicationContext applicationContext) { + if (this.managementPortType == null) { + this.managementPortType = ManagementPortType.get(applicationContext.getEnvironment()); + } + if (this.managementPortType == ManagementPortType.DIFFERENT) { + if (applicationContext.getParent() == null) { + return true; + } + String managementContextId = applicationContext.getParent().getId() + ":management"; + if (!managementContextId.equals(applicationContext.getId())) { + return true; + } + } + return false; + } + + } + /** * The {@link ServerWebExchangeMatcher} used to match against {@link Endpoint actuator * endpoints}. */ - public static final class EndpointServerWebExchangeMatcher - extends ApplicationContextServerWebExchangeMatcher { + public static final class EndpointServerWebExchangeMatcher extends AbstractWebExchangeMatcher { private final List includes; @@ -223,33 +252,15 @@ private List getDelegateMatchers(Set paths) { @Override protected Mono matches(ServerWebExchange exchange, Supplier context) { - if (!isManagementContext(exchange)) { - return MatchResult.notMatch(); - } return this.delegate.matches(exchange); } - static boolean isManagementContext(ServerWebExchange exchange) { - ApplicationContext applicationContext = exchange.getApplicationContext(); - if (ManagementPortType.get(applicationContext.getEnvironment()) == ManagementPortType.DIFFERENT) { - if (applicationContext.getParent() == null) { - return false; - } - String managementContextId = applicationContext.getParent().getId() + ":management"; - if (!managementContextId.equals(applicationContext.getId())) { - return false; - } - } - return true; - } - } /** * The {@link ServerWebExchangeMatcher} used to match against the links endpoint. */ - public static final class LinksServerWebExchangeMatcher - extends ApplicationContextServerWebExchangeMatcher { + public static final class LinksServerWebExchangeMatcher extends AbstractWebExchangeMatcher { private volatile ServerWebExchangeMatcher delegate; @@ -271,9 +282,6 @@ private ServerWebExchangeMatcher createDelegate(WebEndpointProperties properties @Override protected Mono matches(ServerWebExchange exchange, Supplier context) { - if (!EndpointServerWebExchangeMatcher.isManagementContext(exchange)) { - return MatchResult.notMatch(); - } return this.delegate.matches(exchange); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/ReactiveManagementWebSecurityAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/ReactiveManagementWebSecurityAutoConfiguration.java index bec5da10c623..7b96c7745b93 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/ReactiveManagementWebSecurityAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/ReactiveManagementWebSecurityAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration; import org.springframework.boot.actuate.health.HealthEndpoint; -import org.springframework.boot.actuate.info.InfoEndpoint; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -34,14 +33,17 @@ import org.springframework.context.annotation.Configuration; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.SecurityWebFiltersOrder; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.WebFilterChainProxy; +import org.springframework.web.cors.reactive.PreFlightRequestHandler; +import org.springframework.web.cors.reactive.PreFlightRequestWebFilter; /** * {@link EnableAutoConfiguration Auto-configuration} for Reactive Spring Security when - * actuator is on the classpath. Specifically, it permits access to the health and info - * endpoints while securing everything else. + * actuator is on the classpath. Specifically, it permits access to the health endpoint + * while securing everything else. * * @author Madhura Bhave * @since 2.1.0 @@ -57,11 +59,13 @@ public class ReactiveManagementWebSecurityAutoConfiguration { @Bean - public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception { + public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, PreFlightRequestHandler handler) { http.authorizeExchange((exchanges) -> { - exchanges.matchers(EndpointRequest.to(HealthEndpoint.class, InfoEndpoint.class)).permitAll(); + exchanges.matchers(EndpointRequest.to(HealthEndpoint.class)).permitAll(); exchanges.anyExchange().authenticated(); }); + PreFlightRequestWebFilter filter = new PreFlightRequestWebFilter(handler); + http.addFilterAt(filter, SecurityWebFiltersOrder.CORS); http.httpBasic(Customizer.withDefaults()); http.formLogin(Customizer.withDefaults()); return http.build(); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequest.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequest.java index f8f8e4729cf6..3c5299f3bfdb 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequest.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/EndpointRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -124,14 +124,18 @@ private abstract static class AbstractRequestMatcher private volatile RequestMatcher delegate; + private ManagementPortType managementPortType; + AbstractRequestMatcher() { super(WebApplicationContext.class); } @Override protected boolean ignoreApplicationContext(WebApplicationContext applicationContext) { - ManagementPortType type = ManagementPortType.get(applicationContext.getEnvironment()); - return type == ManagementPortType.DIFFERENT + if (this.managementPortType == null) { + this.managementPortType = ManagementPortType.get(applicationContext.getEnvironment()); + } + return this.managementPortType == ManagementPortType.DIFFERENT && !WebServerApplicationContext.hasServerNamespace(applicationContext, "management"); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/ManagementWebSecurityAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/ManagementWebSecurityAutoConfiguration.java index ed95e05b3b18..e3d68aca5a3c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/ManagementWebSecurityAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/ManagementWebSecurityAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,37 +19,58 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration; +import org.springframework.boot.actuate.health.HealthEndpoint; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.security.ConditionalOnDefaultWebSecurity; +import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration; import org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration; +import org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyAutoConfiguration; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; -import org.springframework.boot.autoconfigure.security.servlet.WebSecurityEnablerConfiguration; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.core.annotation.Order; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.util.ClassUtils; /** * {@link EnableAutoConfiguration Auto-configuration} for Spring Security when actuator is - * on the classpath. Specifically, it permits access to the health and info endpoints - * while securing everything else. + * on the classpath. It allows unauthenticated access to the {@link HealthEndpoint}. If + * the user specifies their own{@link SecurityFilterChain} bean, this will back-off + * completely and the user should specify all the bits that they want to configure as part + * of the custom security configuration. * * @author Madhura Bhave + * @author Hatef Palizgar * @since 2.1.0 */ @Configuration(proxyBeanMethods = false) -@ConditionalOnClass(WebSecurityConfigurerAdapter.class) -@ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) +@ConditionalOnDefaultWebSecurity @AutoConfigureBefore(SecurityAutoConfiguration.class) @AutoConfigureAfter({ HealthEndpointAutoConfiguration.class, InfoEndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, OAuth2ClientAutoConfiguration.class, - OAuth2ResourceServerAutoConfiguration.class }) -@Import({ ManagementWebSecurityConfigurerAdapter.class, WebSecurityEnablerConfiguration.class }) + OAuth2ResourceServerAutoConfiguration.class, Saml2RelyingPartyAutoConfiguration.class }) public class ManagementWebSecurityAutoConfiguration { + @Bean + @Order(SecurityProperties.BASIC_AUTH_ORDER) + SecurityFilterChain managementSecurityFilterChain(HttpSecurity http) throws Exception { + http.authorizeRequests((requests) -> { + requests.requestMatchers(EndpointRequest.to(HealthEndpoint.class)).permitAll(); + requests.anyRequest().authenticated(); + }); + if (ClassUtils.isPresent("org.springframework.web.servlet.DispatcherServlet", null)) { + http.cors(); + } + http.formLogin(Customizer.withDefaults()); + http.httpBasic(Customizer.withDefaults()); + return http.build(); + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/ManagementWebSecurityConfigurerAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/ManagementWebSecurityConfigurerAdapter.java deleted file mode 100644 index 707d86695221..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/ManagementWebSecurityConfigurerAdapter.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.autoconfigure.security.servlet; - -import org.springframework.boot.actuate.health.HealthEndpoint; -import org.springframework.boot.actuate.info.InfoEndpoint; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.Customizer; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; - -/** - * The default configuration for web security when the actuator dependency is on the - * classpath. It is different from - * {@link org.springframework.boot.autoconfigure.security.servlet.SpringBootWebSecurityConfiguration} - * in that it allows unauthenticated access to the {@link HealthEndpoint} and - * {@link InfoEndpoint}. If the user specifies their own - * {@link WebSecurityConfigurerAdapter}, this will back-off completely and the user should - * specify all the bits that they want to configure as part of the custom security - * configuration. - * - * @author Madhura Bhave - */ -@Configuration(proxyBeanMethods = false) -class ManagementWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { - - @Override - protected void configure(HttpSecurity http) throws Exception { - http.authorizeRequests((requests) -> { - requests.requestMatchers(EndpointRequest.to(HealthEndpoint.class, InfoEndpoint.class)).permitAll(); - requests.anyRequest().authenticated(); - }); - http.formLogin(Customizer.withDefaults()); - http.httpBasic(Customizer.withDefaults()); - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/SecurityRequestMatchersManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/SecurityRequestMatchersManagementContextConfiguration.java index 661d17806114..f900ec234cd1 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/SecurityRequestMatchersManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/servlet/SecurityRequestMatchersManagementContextConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,7 +40,7 @@ * @author Madhura Bhave * @since 2.1.8 */ -@ManagementContextConfiguration +@ManagementContextConfiguration(proxyBeanMethods = false) @ConditionalOnClass({ RequestMatcher.class }) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) public class SecurityRequestMatchersManagementContextConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/startup/StartupEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/startup/StartupEndpointAutoConfiguration.java new file mode 100644 index 000000000000..21e94364e0fd --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/startup/StartupEndpointAutoConfiguration.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.startup; + +import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; +import org.springframework.boot.actuate.startup.StartupEndpoint; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionMessage; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.metrics.ApplicationStartup; +import org.springframework.core.type.AnnotatedTypeMetadata; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for the {@link StartupEndpoint}. + * + * @author Brian Clozel + * @since 2.4.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnAvailableEndpoint(endpoint = StartupEndpoint.class) +@Conditional(StartupEndpointAutoConfiguration.ApplicationStartupCondition.class) +public class StartupEndpointAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public StartupEndpoint startupEndpoint(BufferingApplicationStartup applicationStartup) { + return new StartupEndpoint(applicationStartup); + } + + /** + * {@link SpringBootCondition} checking the configured + * {@link org.springframework.core.metrics.ApplicationStartup}. + *

+ * Endpoint is enabled only if the configured implementation is + * {@link BufferingApplicationStartup}. + */ + static class ApplicationStartupCondition extends SpringBootCondition { + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { + ConditionMessage.Builder message = ConditionMessage.forCondition("ApplicationStartup"); + ApplicationStartup applicationStartup = context.getBeanFactory().getApplicationStartup(); + if (applicationStartup instanceof BufferingApplicationStartup) { + return ConditionOutcome.match( + message.because("configured applicationStartup is of type BufferingApplicationStartup.")); + } + return ConditionOutcome.noMatch(message.because("configured applicationStartup is of type " + + applicationStartup.getClass() + ", expected BufferingApplicationStartup.")); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/startup/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/startup/package-info.java new file mode 100644 index 000000000000..d2a84b9bdb97 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/startup/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for actuator ApplicationStartup concerns. + */ +package org.springframework.boot.actuate.autoconfigure.startup; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/system/DiskSpaceHealthIndicatorProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/system/DiskSpaceHealthIndicatorProperties.java index 75a79304f39f..f2b625463ad2 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/system/DiskSpaceHealthIndicatorProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/system/DiskSpaceHealthIndicatorProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,8 +48,6 @@ public File getPath() { } public void setPath(File path) { - Assert.isTrue(path.exists(), () -> "Path '" + path + "' does not exist"); - Assert.isTrue(path.canRead(), () -> "Path '" + path + "' cannot be read"); this.path = path; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/http/HttpTraceProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/http/HttpTraceProperties.java index 92179e22f230..4a3f13912748 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/http/HttpTraceProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/trace/http/HttpTraceProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,8 +37,7 @@ public class HttpTraceProperties { /** * Items to be included in the trace. Defaults to request headers (excluding - * Authorization but including Cookie), response headers (including Set-Cookie), and - * time taken. + * Authorization and Cookie), response headers (excluding Set-Cookie), and time taken. */ private Set include = new HashSet<>(Include.defaultIncludes()); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/ManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/ManagementContextConfiguration.java index abcb862d6c00..6970e2195607 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/ManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/ManagementContextConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,7 +46,7 @@ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented -@Configuration(proxyBeanMethods = false) +@Configuration public @interface ManagementContextConfiguration { /** diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseyChildManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseyChildManagementContextConfiguration.java index a17afc43569c..360e93577170 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseyChildManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseyChildManagementContextConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.actuate.autoconfigure.web.jersey; import org.glassfish.jersey.server.ResourceConfig; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration; import org.springframework.boot.actuate.autoconfigure.web.ManagementContextType; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -34,7 +36,7 @@ * @author Madhura Bhave * @since 2.1.0 */ -@ManagementContextConfiguration(ManagementContextType.CHILD) +@ManagementContextConfiguration(value = ManagementContextType.CHILD, proxyBeanMethods = false) @Import(JerseyManagementContextConfiguration.class) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) @ConditionalOnClass(ResourceConfig.class) @@ -46,4 +48,11 @@ public JerseyApplicationPath jerseyApplicationPath() { return () -> "/"; } + @Bean + ResourceConfig resourceConfig(ObjectProvider customizers) { + ResourceConfig resourceConfig = new ResourceConfig(); + customizers.orderedStream().forEach((customizer) -> customizer.customize(resourceConfig)); + return resourceConfig; + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseyManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseyManagementContextConfiguration.java index ed9117dda685..57e7bf9835d7 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseyManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseyManagementContextConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.actuate.autoconfigure.web.jersey; import org.glassfish.jersey.server.ResourceConfig; @@ -38,9 +39,4 @@ ServletRegistrationBean jerseyServletRegistration(JerseyApplic jerseyApplicationPath.getUrlMapping()); } - @Bean - ResourceConfig resourceConfig() { - return new ResourceConfig(); - } - } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseySameManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseySameManagementContextConfiguration.java index 5a2f1edfb3f9..4971e4303b87 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseySameManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseySameManagementContextConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.actuate.autoconfigure.web.jersey; import org.glassfish.jersey.server.ResourceConfig; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration; import org.springframework.boot.actuate.autoconfigure.web.ManagementContextType; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -24,10 +26,12 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.jersey.JerseyProperties; +import org.springframework.boot.autoconfigure.jersey.ResourceConfigCustomizer; import org.springframework.boot.autoconfigure.web.servlet.DefaultJerseyApplicationPath; import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; /** @@ -37,19 +41,37 @@ * @author Madhura Bhave * @since 2.1.0 */ -@ManagementContextConfiguration(ManagementContextType.SAME) -@Import(JerseyManagementContextConfiguration.class) +@ManagementContextConfiguration(value = ManagementContextType.SAME, proxyBeanMethods = false) @EnableConfigurationProperties(JerseyProperties.class) -@ConditionalOnMissingBean(ResourceConfig.class) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) @ConditionalOnClass(ResourceConfig.class) @ConditionalOnMissingClass("org.springframework.web.servlet.DispatcherServlet") public class JerseySameManagementContextConfiguration { @Bean - @ConditionalOnMissingBean(JerseyApplicationPath.class) - public JerseyApplicationPath jerseyApplicationPath(JerseyProperties properties, ResourceConfig config) { - return new DefaultJerseyApplicationPath(properties.getApplicationPath(), config); + ResourceConfigCustomizer managementResourceConfigCustomizerAdapter( + ObjectProvider customizers) { + return (config) -> customizers.orderedStream().forEach((customizer) -> customizer.customize(config)); + } + + @Configuration(proxyBeanMethods = false) + @Import(JerseyManagementContextConfiguration.class) + @ConditionalOnMissingBean(ResourceConfig.class) + static class JerseyInfrastructureConfiguration { + + @Bean + @ConditionalOnMissingBean(JerseyApplicationPath.class) + JerseyApplicationPath jerseyApplicationPath(JerseyProperties properties, ResourceConfig config) { + return new DefaultJerseyApplicationPath(properties.getApplicationPath(), config); + } + + @Bean + ResourceConfig resourceConfig(ObjectProvider resourceConfigCustomizers) { + ResourceConfig resourceConfig = new ResourceConfig(); + resourceConfigCustomizers.orderedStream().forEach((customizer) -> customizer.customize(resourceConfig)); + return resourceConfig; + } + } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/ManagementContextResourceConfigCustomizer.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/ManagementContextResourceConfigCustomizer.java new file mode 100644 index 000000000000..028815b13569 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/jersey/ManagementContextResourceConfigCustomizer.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.web.jersey; + +import org.glassfish.jersey.server.ResourceConfig; + +/** + * Callback interface that can be implemented by beans wishing to customize Jersey's + * {@link ResourceConfig} in the management context before it is used. + * + * @author Andy Wilkinson + * @since 2.3.10 + */ +public interface ManagementContextResourceConfigCustomizer { + + /** + * Customize the resource config. + * @param config the {@link ResourceConfig} to customize + */ + void customize(ResourceConfig config); + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/mappings/MappingsEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/mappings/MappingsEndpointAutoConfiguration.java index 4f1c30561f35..72e3aab572cb 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/mappings/MappingsEndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/mappings/MappingsEndpointAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,10 +44,10 @@ * @since 2.0.0 */ @Configuration(proxyBeanMethods = false) +@ConditionalOnAvailableEndpoint(endpoint = MappingsEndpoint.class) public class MappingsEndpointAutoConfiguration { @Bean - @ConditionalOnAvailableEndpoint public MappingsEndpoint mappingsEndpoint(ApplicationContext applicationContext, ObjectProvider descriptionProviders) { return new MappingsEndpoint(descriptionProviders.orderedStream().collect(Collectors.toList()), diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/reactive/ReactiveManagementChildContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/reactive/ReactiveManagementChildContextConfiguration.java index c9e95420d221..657f9dbda20d 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/reactive/ReactiveManagementChildContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/reactive/ReactiveManagementChildContextConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,13 @@ package org.springframework.boot.actuate.autoconfigure.web.reactive; +import java.util.Collections; +import java.util.Map; + import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration; import org.springframework.boot.actuate.autoconfigure.web.ManagementContextType; +import org.springframework.boot.actuate.autoconfigure.web.server.ManagementServerProperties; import org.springframework.boot.actuate.autoconfigure.web.server.ManagementWebServerFactoryCustomizer; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; @@ -31,7 +35,9 @@ import org.springframework.boot.web.reactive.server.ConfigurableReactiveWebServerFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; +import org.springframework.http.server.reactive.ContextPathCompositeHandler; import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.util.StringUtils; import org.springframework.web.reactive.config.EnableWebFlux; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; @@ -45,7 +51,7 @@ * @since 2.0.0 */ @EnableWebFlux -@ManagementContextConfiguration(ManagementContextType.CHILD) +@ManagementContextConfiguration(value = ManagementContextType.CHILD, proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.REACTIVE) public class ReactiveManagementChildContextConfiguration { @@ -56,11 +62,16 @@ public ReactiveManagementWebServerFactoryCustomizer reactiveManagementWebServerF } @Bean - public HttpHandler httpHandler(ApplicationContext applicationContext) { - return WebHttpHandlerBuilder.applicationContext(applicationContext).build(); + public HttpHandler httpHandler(ApplicationContext applicationContext, ManagementServerProperties properties) { + HttpHandler httpHandler = WebHttpHandlerBuilder.applicationContext(applicationContext).build(); + if (StringUtils.hasText(properties.getBasePath())) { + Map handlersMap = Collections.singletonMap(properties.getBasePath(), httpHandler); + return new ContextPathCompositeHandler(handlersMap); + } + return httpHandler; } - class ReactiveManagementWebServerFactoryCustomizer + static class ReactiveManagementWebServerFactoryCustomizer extends ManagementWebServerFactoryCustomizer { ReactiveManagementWebServerFactoryCustomizer(ListableBeanFactory beanFactory) { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementContextAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementContextAutoConfiguration.java index e563fc9367d2..300c8a120d6b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementContextAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementContextAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -73,6 +73,7 @@ static class SameManagementContextConfiguration implements SmartInitializingSing @Override public void afterSingletonsInstantiated() { verifySslConfiguration(); + verifyAddressConfiguration(); if (this.environment instanceof ConfigurableEnvironment) { addLocalManagementPortPropertyAlias((ConfigurableEnvironment) this.environment); } @@ -84,6 +85,12 @@ private void verifySslConfiguration() { + "server is not listening on a separate port"); } + private void verifyAddressConfiguration() { + Object address = this.environment.getProperty("management.server.address"); + Assert.state(address == null, "Management-specific server address cannot be configured as the management " + + "server is not listening on a separate port"); + } + /** * Add an alias for 'local.management.port' that actually resolves using * 'local.server.port'. diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementServerProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementServerProperties.java index 8f8319b10a97..2c0b0e46bfc2 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementServerProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementServerProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; import org.springframework.boot.context.properties.NestedConfigurationProperty; import org.springframework.boot.web.server.Ssl; import org.springframework.util.Assert; @@ -49,6 +50,12 @@ public class ManagementServerProperties { */ private InetAddress address; + /** + * Management endpoint base path (for instance, '/management'). Requires a custom + * management.server.port. + */ + private String basePath = ""; + private final Servlet servlet = new Servlet(); @NestedConfigurationProperty @@ -82,6 +89,14 @@ public void setAddress(InetAddress address) { this.address = address; } + public String getBasePath() { + return this.basePath; + } + + public void setBasePath(String basePath) { + this.basePath = cleanBasePath(basePath); + } + public Ssl getSsl() { return this.ssl; } @@ -94,13 +109,26 @@ public Servlet getServlet() { return this.servlet; } + private String cleanBasePath(String basePath) { + String candidate = StringUtils.trimWhitespace(basePath); + if (StringUtils.hasText(candidate)) { + if (!candidate.startsWith("/")) { + candidate = "/" + candidate; + } + if (candidate.endsWith("/")) { + candidate = candidate.substring(0, candidate.length() - 1); + } + } + return candidate; + } + /** * Servlet properties. */ public static class Servlet { /** - * Management endpoint context-path (for instance, `/management`). Requires a + * Management endpoint context-path (for instance, '/management'). Requires a * custom management.server.port. */ private String contextPath = ""; @@ -109,11 +137,22 @@ public static class Servlet { * Return the context path with no trailing slash (i.e. the '/' root context is * represented as the empty string). * @return the context path (no trailing slash) + * @deprecated since 2.4.0 for removal in 2.6.0 in favor of + * {@link ManagementServerProperties#getBasePath()} */ + @Deprecated + @DeprecatedConfigurationProperty(replacement = "management.server.base-path") public String getContextPath() { return this.contextPath; } + /** + * Set the context path. + * @param contextPath the context path + * @deprecated since 2.4.0 for removal in 2.6.0 in favor of + * {@link ManagementServerProperties#setBasePath(String)} + */ + @Deprecated public void setContextPath(String contextPath) { Assert.notNull(contextPath, "ContextPath must not be null"); this.contextPath = cleanContextPath(contextPath); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementWebServerFactoryCustomizer.java index 09d30a9a309d..4b0e3be41568 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementWebServerFactoryCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,6 +49,7 @@ public abstract class ManagementWebServerFactoryCustomizer>[] customizerClasses; @SafeVarargs + @SuppressWarnings("varargs") protected ManagementWebServerFactoryCustomizer(ListableBeanFactory beanFactory, Class>... customizerClasses) { this.beanFactory = beanFactory; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/CompositeHandlerAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/CompositeHandlerAdapter.java index a116a10a92a9..019fdbc38795 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/CompositeHandlerAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/CompositeHandlerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,6 +61,7 @@ public ModelAndView handle(HttpServletRequest request, HttpServletResponse respo } @Override + @Deprecated public long getLastModified(HttpServletRequest request, Object handler) { Optional adapter = getAdapter(handler); return adapter.map((handlerAdapter) -> handlerAdapter.getLastModified(request, handler)).orElse(0L); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/CompositeHandlerExceptionResolver.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/CompositeHandlerExceptionResolver.java index 6d7b7121e356..345617df912b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/CompositeHandlerExceptionResolver.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/CompositeHandlerExceptionResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.web.servlet.error.DefaultErrorAttributes; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.ModelAndView; @@ -36,6 +37,7 @@ * @author Andy Wilkinson * @author Stephane Nicoll * @author Phillip Webb + * @author Scott Frederick */ class CompositeHandlerExceptionResolver implements HandlerExceptionResolver { @@ -60,6 +62,7 @@ private List extractResolvers() { list.remove(this); AnnotationAwareOrderComparator.sort(list); if (list.isEmpty()) { + list.add(new DefaultErrorAttributes()); list.add(new DefaultHandlerExceptionResolver()); } return list; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/CompositeHandlerMapping.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/CompositeHandlerMapping.java index cacc8674f1bb..f5240d46142b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/CompositeHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/CompositeHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,10 +43,7 @@ class CompositeHandlerMapping implements HandlerMapping { @Override public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { - if (this.mappings == null) { - this.mappings = extractMappings(); - } - for (HandlerMapping mapping : this.mappings) { + for (HandlerMapping mapping : getMappings()) { HandlerExecutionChain handler = mapping.getHandler(request); if (handler != null) { return handler; @@ -55,6 +52,23 @@ public HandlerExecutionChain getHandler(HttpServletRequest request) throws Excep return null; } + @Override + public boolean usesPathPatterns() { + for (HandlerMapping mapping : getMappings()) { + if (mapping.usesPathPatterns()) { + return true; + } + } + return false; + } + + private List getMappings() { + if (this.mappings == null) { + this.mappings = extractMappings(); + } + return this.mappings; + } + private List extractMappings() { List list = new ArrayList<>(this.beanFactory.getBeansOfType(HandlerMapping.class).values()); list.remove(this); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/ManagementErrorEndpoint.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/ManagementErrorEndpoint.java index e5e0a7f21b89..2050b1b52a8a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/ManagementErrorEndpoint.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/ManagementErrorEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,9 @@ import java.util.Map; +import org.springframework.boot.autoconfigure.web.ErrorProperties; +import org.springframework.boot.web.error.ErrorAttributeOptions; +import org.springframework.boot.web.error.ErrorAttributeOptions.Include; import org.springframework.boot.web.servlet.error.ErrorAttributes; import org.springframework.boot.web.servlet.error.ErrorController; import org.springframework.stereotype.Controller; @@ -32,6 +35,7 @@ * but because of the way the handler mappings are set up it will not be detected. * * @author Dave Syer + * @author Scott Frederick * @since 2.0.0 */ @Controller @@ -39,15 +43,77 @@ public class ManagementErrorEndpoint { private final ErrorAttributes errorAttributes; - public ManagementErrorEndpoint(ErrorAttributes errorAttributes) { + private final ErrorProperties errorProperties; + + public ManagementErrorEndpoint(ErrorAttributes errorAttributes, ErrorProperties errorProperties) { Assert.notNull(errorAttributes, "ErrorAttributes must not be null"); + Assert.notNull(errorProperties, "ErrorProperties must not be null"); this.errorAttributes = errorAttributes; + this.errorProperties = errorProperties; } @RequestMapping("${server.error.path:${error.path:/error}}") @ResponseBody public Map invoke(ServletWebRequest request) { - return this.errorAttributes.getErrorAttributes(request, false); + return this.errorAttributes.getErrorAttributes(request, getErrorAttributeOptions(request)); + } + + private ErrorAttributeOptions getErrorAttributeOptions(ServletWebRequest request) { + ErrorAttributeOptions options = ErrorAttributeOptions.defaults(); + if (this.errorProperties.isIncludeException()) { + options = options.including(Include.EXCEPTION); + } + if (includeStackTrace(request)) { + options = options.including(Include.STACK_TRACE); + } + if (includeMessage(request)) { + options = options.including(Include.MESSAGE); + } + if (includeBindingErrors(request)) { + options = options.including(Include.BINDING_ERRORS); + } + return options; + } + + private boolean includeStackTrace(ServletWebRequest request) { + switch (this.errorProperties.getIncludeStacktrace()) { + case ALWAYS: + return true; + case ON_PARAM: + return getBooleanParameter(request, "trace"); + default: + return false; + } + } + + private boolean includeMessage(ServletWebRequest request) { + switch (this.errorProperties.getIncludeMessage()) { + case ALWAYS: + return true; + case ON_PARAM: + return getBooleanParameter(request, "message"); + default: + return false; + } + } + + private boolean includeBindingErrors(ServletWebRequest request) { + switch (this.errorProperties.getIncludeBindingErrors()) { + case ALWAYS: + return true; + case ON_PARAM: + return getBooleanParameter(request, "errors"); + default: + return false; + } + } + + protected boolean getBooleanParameter(ServletWebRequest request, String parameterName) { + String parameter = request.getParameter(parameterName); + if (parameter == null) { + return false; + } + return !"false".equalsIgnoreCase(parameter); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/ServletManagementChildContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/ServletManagementChildContextConfiguration.java index a222bb2be65d..159bcf49a9b9 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/ServletManagementChildContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/ServletManagementChildContextConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,6 +50,7 @@ import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory; import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean; import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -69,7 +70,7 @@ * @author Eddú Meléndez * @author Phillip Webb */ -@ManagementContextConfiguration(ManagementContextType.CHILD) +@ManagementContextConfiguration(value = ManagementContextType.CHILD, proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) class ServletManagementChildContextConfiguration { @@ -108,6 +109,13 @@ Filter springSecurityFilterChain(HierarchicalBeanFactory beanFactory) { return parent.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN, Filter.class); } + @Bean + @ConditionalOnBean(name = "securityFilterChainRegistration", search = SearchStrategy.ANCESTORS) + DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(HierarchicalBeanFactory beanFactory) { + return beanFactory.getParentBeanFactory().getBean("securityFilterChainRegistration", + DelegatingFilterProxyRegistrationBean.class); + } + } static class ServletManagementWebServerFactoryCustomizer @@ -123,7 +131,13 @@ static class ServletManagementWebServerFactoryCustomizer protected void customize(ConfigurableServletWebServerFactory webServerFactory, ManagementServerProperties managementServerProperties, ServerProperties serverProperties) { super.customize(webServerFactory, managementServerProperties, serverProperties); - webServerFactory.setContextPath(managementServerProperties.getServlet().getContextPath()); + webServerFactory.setContextPath(getContextPath(managementServerProperties)); + } + + @SuppressWarnings("deprecation") + private String getContextPath(ManagementServerProperties managementServerProperties) { + String basePath = managementServerProperties.getBasePath(); + return StringUtils.hasText(basePath) ? basePath : managementServerProperties.getServlet().getContextPath(); } } @@ -190,7 +204,7 @@ public void customize(JettyServletWebServerFactory factory) { private void customizeServer(Server server) { RequestLog requestLog = server.getRequestLog(); - if (requestLog != null && requestLog instanceof CustomRequestLog) { + if (requestLog instanceof CustomRequestLog) { customizeRequestLog((CustomRequestLog) requestLog); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfiguration.java index 953c292fdb6a..6a3bddfe7b15 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,7 +48,7 @@ * @author Andy Wilkinson * @author Phillip Webb */ -@ManagementContextConfiguration(ManagementContextType.CHILD) +@ManagementContextConfiguration(value = ManagementContextType.CHILD, proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) @ConditionalOnClass(DispatcherServlet.class) @EnableWebMvc @@ -61,8 +61,8 @@ class WebMvcEndpointChildContextConfiguration { */ @Bean @ConditionalOnBean(ErrorAttributes.class) - ManagementErrorEndpoint errorEndpoint(ErrorAttributes errorAttributes) { - return new ManagementErrorEndpoint(errorAttributes); + ManagementErrorEndpoint errorEndpoint(ErrorAttributes errorAttributes, ServerProperties serverProperties) { + return new ManagementErrorEndpoint(errorAttributes, serverProperties.getError()); } @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 1be8a99d862d..f84df137a23d 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -1,5 +1,29 @@ { + "groups": [], "properties": [ + { + "name": "info", + "type": "java.util.Map", + "description": "Arbitrary properties to add to the info endpoint." + }, + { + "name": "management.auditevents.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable storage of audit events.", + "defaultValue": true + }, + { + "name": "management.cloudfoundry.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable extended Cloud Foundry actuator endpoints.", + "defaultValue": true + }, + { + "name": "management.cloudfoundry.skip-ssl-validation", + "type": "java.lang.Boolean", + "description": "Whether to skip SSL verification for Cloud Foundry actuator endpoint security calls.", + "defaultValue": false + }, { "name": "management.endpoint.configprops.keys-to-sanitize", "defaultValue": [ @@ -24,6 +48,12 @@ "sun.java.command" ] }, + { + "name": "management.endpoint.health.probes.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable liveness and readiness probes.", + "defaultValue": false + }, { "name": "management.endpoint.health.show-details", "defaultValue": "never" @@ -51,34 +81,19 @@ "defaultValue": "*" }, { - "name": "management.endpoints.web.exposure.include", - "defaultValue": [ - "health", - "info" - ] - }, - { - "name": "info", - "type": "java.util.Map", - "description": "Arbitrary properties to add to the info endpoint." - }, - { - "name": "management.cloudfoundry.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable extended Cloud Foundry actuator endpoints.", - "defaultValue": true - }, - { - "name": "management.cloudfoundry.skip-ssl-validation", + "name": "management.endpoints.jmx.unique-names", "type": "java.lang.Boolean", - "description": "Whether to skip SSL verification for Cloud Foundry actuator endpoint security calls.", - "defaultValue": false + "description": "Whether unique runtime object names should be ensured.", + "deprecation": { + "replacement": "spring.jmx.unique-names", + "level": "error" + } }, { - "name": "management.auditevents.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable storage of audit events.", - "defaultValue": true + "name": "management.endpoints.web.exposure.include", + "defaultValue": [ + "health" + ] }, { "name": "management.health.cassandra.enabled", @@ -92,6 +107,15 @@ "description": "Whether to enable Couchbase health check.", "defaultValue": true }, + { + "name": "management.health.couchbase.timeout", + "type": "java.time.Duration", + "description": "Timeout for getting the Bucket information from the server.", + "defaultValue": "1000ms", + "deprecation": { + "level": "error" + } + }, { "name": "management.health.db.enabled", "type": "java.lang.Boolean", @@ -116,6 +140,22 @@ "description": "Whether to enable Elasticsearch health check.", "defaultValue": true }, + { + "name": "management.health.elasticsearch.indices", + "type": "java.util.List", + "description": "Comma-separated index names.", + "deprecation": { + "level": "error" + } + }, + { + "name": "management.health.elasticsearch.response-timeout", + "type": "java.time.Duration", + "description": "Time to wait for a response from the cluster.", + "deprecation": { + "level": "error" + } + }, { "name": "management.health.influxdb.enabled", "type": "java.lang.Boolean", @@ -134,24 +174,57 @@ "description": "Whether to enable LDAP health check.", "defaultValue": true }, + { + "name": "management.health.livenessstate.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable liveness state health check.", + "defaultValue": false + }, + { + "name": "management.health.mail.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable Mail health check.", + "defaultValue": true + }, { "name": "management.health.mongo.enabled", "type": "java.lang.Boolean", "description": "Whether to enable MongoDB health check.", "defaultValue": true }, + { + "name": "management.health.neo4j.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable Neo4j health check.", + "defaultValue": true + }, { "name": "management.health.ping.enabled", "type": "java.lang.Boolean", "description": "Whether to enable ping health check.", "defaultValue": true }, + { + "name": "management.health.probes.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable liveness and readiness probes.", + "defaultValue": false, + "deprecation": { + "replacement": "management.endpoint.health.probes.enabled" + } + }, { "name": "management.health.rabbit.enabled", "type": "java.lang.Boolean", "description": "Whether to enable RabbitMQ health check.", "defaultValue": true }, + { + "name": "management.health.readinessstate.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable readiness state health check.", + "defaultValue": false + }, { "name": "management.health.redis.enabled", "type": "java.lang.Boolean", @@ -173,18 +246,6 @@ "UNKNOWN" ] }, - { - "name": "management.health.mail.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable Mail health check.", - "defaultValue": true - }, - { - "name": "management.health.neo4j.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable Neo4j health check.", - "defaultValue": true - }, { "name": "management.info.build.enabled", "type": "java.lang.Boolean", @@ -214,11 +275,100 @@ "defaultValue": "simple" }, { - "name": "management.metrics.export.jmx.enabled", + "name": "management.metrics.binders.files.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable files metrics.", + "defaultValue": true, + "deprecation": { + "level": "error", + "replacement": "management.metrics.enable.process.files", + "reason": "Instead, filter 'process.files' metrics." + } + }, + { + "name": "management.metrics.binders.jvm.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable JVM metrics.", + "defaultValue": true, + "deprecation": { + "level": "error", + "replacement": "management.metrics.enable.jvm", + "reason": "Instead, disable JvmMetricsAutoConfiguration or filter 'jvm' metrics." + } + }, + { + "name": "management.metrics.binders.logback.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable Logback metrics.", + "defaultValue": true, + "deprecation": { + "level": "error", + "replacement": "management.metrics.enable.logback", + "reason": "Instead, disable LogbackMetricsAutoConfiguration or filter 'logback' metrics." + } + }, + { + "name": "management.metrics.binders.processor.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable processor metrics.", + "defaultValue": true, + "deprecation": { + "level": "error", + "reason": "Instead, filter 'system.cpu' and 'process.cpu' metrics." + } + }, + { + "name": "management.metrics.binders.uptime.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable uptime metrics.", + "defaultValue": true, + "deprecation": { + "level": "error", + "reason": "Instead, filter 'process.uptime' and 'process.start.time' metrics." + } + }, + { + "name": "management.metrics.export.appoptics.num-threads", + "type": "java.lang.Integer", + "description": "Number of threads to use with the metrics publishing scheduler.", + "defaultValue": 2, + "deprecation": { + "level": "error" + } + }, + { + "name": "management.metrics.export.datadog.num-threads", + "type": "java.lang.Integer", + "description": "Number of threads to use with the metrics publishing scheduler.", + "defaultValue": 2, + "deprecation": { + "level": "error" + } + }, + { + "name": "management.metrics.export.defaults.enabled", "type": "java.lang.Boolean", - "description": "Whether exporting of metrics to JMX is enabled.", + "description": "Whether to enable default metrics exporters.", "defaultValue": true }, + { + "name": "management.metrics.export.dynatrace.num-threads", + "type": "java.lang.Integer", + "description": "Number of threads to use with the metrics publishing scheduler.", + "defaultValue": 2, + "deprecation": { + "level": "error" + } + }, + { + "name": "management.metrics.export.elastic.num-threads", + "type": "java.lang.Integer", + "description": "Number of threads to use with the metrics publishing scheduler.", + "defaultValue": 2, + "deprecation": { + "level": "error" + } + }, { "name": "management.metrics.export.ganglia.addressing-mode", "defaultValue": "multicast" @@ -244,1573 +394,264 @@ "defaultValue": "seconds" }, { - "name": "management.metrics.export.influx.consistency", - "defaultValue": "one" - }, - { - "name": "management.metrics.export.prometheus.enabled", - "type": "java.lang.Boolean", - "description": "Whether exporting of metrics to Prometheus is enabled.", - "defaultValue": true + "name": "management.metrics.export.humio.num-threads", + "type": "java.lang.Integer", + "description": "Number of threads to use with the metrics publishing scheduler.", + "defaultValue": 2, + "deprecation": { + "level": "error" + } }, { - "name": "management.metrics.export.simple.enabled", - "type": "java.lang.Boolean", - "description": "Whether, in the absence of any other exporter, exporting of metrics to an in-memory backend is enabled.", - "defaultValue": true + "name": "management.metrics.export.humio.repository", + "deprecation": { + "level": "error" + } }, { - "name": "management.metrics.export.simple.mode", - "defaultValue": "cumulative" + "name": "management.metrics.export.influx.consistency", + "defaultValue": "one" }, { - "name": "management.metrics.export.statsd.flavor", - "defaultValue": "datadog" + "name": "management.metrics.export.influx.num-threads", + "type": "java.lang.Integer", + "description": "Number of threads to use with the metrics publishing scheduler.", + "defaultValue": 2, + "deprecation": { + "level": "error" + } }, { - "name": "management.metrics.export.statsd.queue-size", - "defaultValue": 2147483647, + "name": "management.metrics.export.kairos.num-threads", + "type": "java.lang.Integer", + "description": "Number of threads to use with the metrics publishing scheduler.", "deprecation": { "level": "error" } }, { - "name": "management.metrics.web.client.request.autotime.enabled", - "description": "Whether to automatically time web client requests.", - "defaultValue": true + "name": "management.metrics.export.newrelic.num-threads", + "type": "java.lang.Integer", + "description": "Number of threads to use with the metrics publishing scheduler.", + "defaultValue": 2, + "deprecation": { + "level": "error" + } }, { - "name": "management.metrics.web.client.request.autotime.percentiles", - "description": "Computed non-aggregable percentiles to publish." + "name": "management.metrics.export.prometheus.histogram-flavor", + "defaultValue": "prometheus" }, { - "name": "management.metrics.web.client.request.autotime.percentiles-histogram", - "description": "Whether percentile histograms should be published.", - "defaultValue": false + "name": "management.metrics.export.prometheus.pushgateway.shutdown-operation", + "defaultValue": "none" }, { - "name": "management.metrics.web.server.request.autotime.enabled", - "description": "Whether to automatically time web server requests.", - "defaultValue": true + "name": "management.metrics.export.signalfx.num-threads", + "type": "java.lang.Integer", + "description": "Number of threads to use with the metrics publishing scheduler.", + "defaultValue": 2, + "deprecation": { + "level": "error" + } }, { - "name": "management.metrics.web.server.request.autotime.percentiles", - "description": "Computed non-aggregable percentiles to publish." + "name": "management.metrics.export.simple.mode", + "defaultValue": "cumulative" }, { - "name": "management.metrics.web.server.request.autotime.percentiles-histogram", - "description": "Whether percentile histograms should be published.", - "defaultValue": false + "name": "management.metrics.export.stackdriver.num-threads", + "type": "java.lang.Integer", + "description": "Number of threads to use with the metrics publishing scheduler.", + "defaultValue": 2, + "deprecation": { + "level": "error" + } }, { - "name": "management.server.ssl.ciphers", - "description": "Supported SSL ciphers." + "name": "management.metrics.export.statsd.flavor", + "defaultValue": "datadog" }, { - "name": "management.server.ssl.client-auth", - "description": "Client authentication mode. Requires a trust store." + "name": "management.metrics.export.statsd.protocol", + "defaultValue": "udp" }, { - "name": "management.server.ssl.enabled", - "description": "Whether to enable SSL support.", - "defaultValue": true - }, - { - "name": "management.server.ssl.enabled-protocols", - "description": "Enabled SSL protocols." - }, - { - "name": "management.server.ssl.key-alias", - "description": "Alias that identifies the key in the key store." - }, - { - "name": "management.server.ssl.key-password", - "description": "Password used to access the key in the key store." - }, - { - "name": "management.server.ssl.key-store", - "description": "Path to the key store that holds the SSL certificate (typically a jks file)." - }, - { - "name": "management.server.ssl.key-store-password", - "description": "Password used to access the key store." - }, - { - "name": "management.server.ssl.key-store-provider", - "description": "Provider for the key store." - }, - { - "name": "management.server.ssl.key-store-type", - "description": "Type of the key store." - }, - { - "name": "management.server.ssl.protocol", - "description": "SSL protocol to use.", - "defaultValue": "TLS" - }, - { - "name": "management.server.ssl.trust-store", - "description": "Trust store that holds SSL certificates." - }, - { - "name": "management.server.ssl.trust-store-password", - "description": "Password used to access the trust store." - }, - { - "name": "management.server.ssl.trust-store-provider", - "description": "Provider for the trust store." - }, - { - "name": "management.server.ssl.trust-store-type", - "description": "Type of the trust store." - }, - { - "name": "management.trace.http.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable HTTP request-response tracing.", - "defaultValue": true - }, - { - "name": "management.trace.http.include", - "defaultValue": [ - "request-headers", - "response-headers", - "cookies", - "errors" - ] - }, - { - "name": "endpoints.actuator.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable the endpoint.", - "deprecation": { - "reason": "The \"actuator\" endpoint is no longer available.", - "level": "error" - } - }, - { - "name": "endpoints.actuator.path", - "type": "java.lang.String", - "description": "Endpoint URL path.", - "deprecation": { - "reason": "The \"actuator\" endpoint is no longer available.", - "level": "error" - } - }, - { - "name": "endpoints.actuator.sensitive", - "type": "java.lang.Boolean", - "description": "Mark if the endpoint exposes sensitive information.", - "deprecation": { - "reason": "The \"actuator\" endpoint is no longer available.","level": "error" - } - }, - { - "name": "endpoints.auditevents.enabled", - "type": "java.lang.Boolean", - "description": "Enable the endpoint.", - "deprecation": { - "replacement": "management.endpoint.auditevents.enabled", - "level": "error" - } - }, - { - "name": "endpoints.auditevents.path", - "type": "java.lang.String", - "description": "Endpoint URL path.", - "deprecation": { - "replacement": "management.endpoints.web.path-mapping.auditevents", - "level": "error" - } - }, - { - "name": "endpoints.auditevents.sensitive", - "type": "java.lang.Boolean", - "description": "Mark if the endpoint exposes sensitive information.", - "deprecation": { - "reason": "Endpoint sensitive flag is no longer customizable as Spring Boot no longer provides a customizable security auto-configuration\n. Create or adapt your security configuration accordingly.", - "level": "error" - } - }, - { - "name": "endpoints.autoconfig.id", - "type": "java.lang.String", - "description": "Endpoint identifier. With HTTP monitoring the identifier of the endpoint is mapped\n to a URL (e.g. 'foo' is mapped to '/foo').", - "deprecation": { - "reason": "Endpoint identifier is no longer customizable.","level": "error" - } - }, - { - "name": "endpoints.autoconfig.enabled", - "type": "java.lang.Boolean", - "description": "Enable the endpoint.", - "deprecation": { - "replacement": "management.endpoint.conditions.enabled", - "level": "error" - } - }, - { - "name": "endpoints.autoconfig.path", - "type": "java.lang.String", - "description": "Endpoint URL path.", - "deprecation": { - "replacement": "management.endpoints.web.path-mapping.conditions", - "level": "error" - } - }, - { - "name": "endpoints.autoconfig.sensitive", - "type": "java.lang.Boolean", - "description": "Mark if the endpoint exposes sensitive information.", - "deprecation": { - "reason": "Endpoint sensitive flag is no longer customizable as Spring Boot no longer provides a customizable security auto-configuration\n. Create or adapt your security configuration accordingly.", - "level": "error" - } - }, - { - "name": "endpoints.beans.enabled", - "type": "java.lang.Boolean", - "description": "Enable the endpoint.", - "deprecation": { - "replacement": "management.endpoint.beans.enabled", - "level": "error" - } - }, - { - "name": "endpoints.beans.id", - "type": "java.lang.String", - "description": "Endpoint identifier. With HTTP monitoring the identifier of the endpoint is mapped\n to a URL (e.g. 'foo' is mapped to '/foo').", - "deprecation": { - "reason": "Endpoint identifier is no longer customizable.", - "level": "error" - } - }, - { - "name": "endpoints.beans.path", - "type": "java.lang.String", - "description": "Endpoint URL path.", - "deprecation": { - "replacement": "management.endpoints.web.path-mapping.beans", - "level": "error" - } - }, - { - "name": "endpoints.beans.sensitive", - "type": "java.lang.Boolean", - "description": "Mark if the endpoint exposes sensitive information.", - "deprecation": { - "reason": "Endpoint sensitive flag is no longer customizable as Spring Boot no longer provides a customizable security auto-configuration\n. Create or adapt your security configuration accordingly.", - "level": "error" - } - }, - { - "name": "endpoints.configprops.enabled", - "type": "java.lang.Boolean", - "description": "Enable the endpoint.", - "deprecation": { - "replacement": "management.endpoint.configprops.enabled", - "level": "error" - } - }, - { - "name": "endpoints.configprops.id", - "type": "java.lang.String", - "description": "Endpoint identifier. With HTTP monitoring the identifier of the endpoint is mapped\n to a URL (e.g. 'foo' is mapped to '/foo').", - "deprecation": { - "reason": "Endpoint identifier is no longer customizable.", - "level": "error" - } - }, - { - "name": "endpoints.configprops.keys-to-sanitize", - "type": "java.lang.String[]", - "description": "Keys that should be sanitized. Keys can be simple strings that the property ends with or regex expressions.", - "deprecation": { - "replacement": "management.endpoint.configprops.keys-to-sanitize", - "level": "error" - } - }, - { - "name": "endpoints.configprops.path", - "type": "java.lang.String", - "description": "Endpoint URL path.", - "deprecation": { - "replacement": "management.endpoints.web.path-mapping.configprops", - "level": "error" - } - }, - { - "name": "endpoints.configprops.sensitive", - "type": "java.lang.Boolean", - "description": "Mark if the endpoint exposes sensitive information.", - "deprecation": { - "reason": "Endpoint sensitive flag is no longer customizable as Spring Boot no longer provides a customizable security auto-configuration\n. Create or adapt your security configuration accordingly.", - "level": "error" - } - }, - { - "name": "endpoints.cors.allow-credentials", - "type": "java.lang.Boolean", - "description": "Set whether credentials are supported. When not set, credentials are not supported.", - "deprecation": { - "replacement": "management.endpoints.web.cors.allow-credentials", - "level": "error" - } - }, - { - "name": "endpoints.cors.allowed-headers", - "type": "java.util.List", - "description": "Comma-separated list of headers to allow in a request. '*' allows all headers.", - "deprecation": { - "replacement": "management.endpoints.web.cors.allowed-headers", - "level": "error" - } - }, - { - "name": "endpoints.cors.allowed-methods", - "type": "java.util.List", - "description": "Comma-separated list of methods to allow. '*' allows all methods. When not set,\n defaults to GET.", - "deprecation": { - "replacement": "management.endpoints.web.cors.allowed-methods", - "level": "error" - } - }, - { - "name": "endpoints.cors.allowed-origins", - "type": "java.util.List", - "description": "Comma-separated list of origins to allow. '*' allows all origins. When not set,\n CORS support is disabled.", - "deprecation": { - "replacement": "management.endpoints.web.cors.allowed-origins", - "level": "error" - } - }, - { - "name": "endpoints.cors.exposed-headers", - "type": "java.util.List", - "description": "Comma-separated list of headers to include in a response.", - "deprecation": { - "replacement": "management.endpoints.web.cors.exposed-headers", - "level": "error" - } - }, - { - "name": "endpoints.cors.max-age", - "type": "java.lang.Long", - "description": "How long, in seconds, the response from a pre-flight request can be cached by\n clients.", - "defaultValue": 1800, - "deprecation": { - "replacement": "management.endpoints.web.cors.max-age", - "level": "error" - } - }, - { - "name": "endpoints.docs.curies.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable the curie generation.", - "defaultValue": false, - "deprecation": { - "reason": "The \"docs\" endpoint is no longer available.", - "level": "error" - } - }, - { - "name": "endpoints.docs.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable the endpoint.", - "deprecation": { - "reason": "The \"docs\" endpoint is no longer available.", - "level": "error" - } - }, - { - "name": "endpoints.docs.path", - "type": "java.lang.String", - "description": "Endpoint URL path.", - "deprecation": { - "reason": "The \"docs\" endpoint is no longer available.", - "level": "error" - } - }, - { - "name": "endpoints.docs.sensitive", - "type": "java.lang.Boolean", - "description": "Mark if the endpoint exposes sensitive information.", - "deprecation": { - "reason": "The \"docs\" endpoint is no longer available.", - "level": "error" - } - }, - { - "name": "endpoints.dump.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable the endpoint.", - "deprecation": { - "replacement": "management.endpoint.threaddump.enabled", - "level": "error" - } - }, - { - "name": "endpoints.dump.id", - "type": "java.lang.String", - "description": "Endpoint identifier. With HTTP monitoring the identifier of the endpoint is mapped\n to a URL (e.g. 'foo' is mapped to '/foo').", - "deprecation": { - "reason": "Endpoint identifier is no longer customizable.", - "level": "error" - } - }, - { - "name": "endpoints.dump.path", - "type": "java.lang.String", - "description": "Endpoint URL path.", - "deprecation": { - "replacement": "management.endpoints.web.path-mapping.dump", - "level": "error" - } - }, - { - "name": "endpoints.dump.sensitive", - "type": "java.lang.Boolean", - "description": "Mark if the endpoint exposes sensitive information.", - "deprecation": { - "reason": "Endpoint sensitive flag is no longer customizable as Spring Boot no longer provides a customizable security auto-configuration\n. Create or adapt your security configuration accordingly.", - "level": "error" - } - }, - { - "name": "endpoints.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable endpoints.", - "defaultValue": true, - "deprecation": { - "replacement": "management.endpoints.enabled-by-default", - "level": "error" - } - }, - { - "name": "endpoints.env.enabled", - "type": "java.lang.Boolean", - "description": "Enable the endpoint.", - "deprecation": { - "replacement": "management.endpoint.env.enabled", - "level": "error" - } - }, - { - "name": "endpoints.env.id", - "type": "java.lang.String", - "description": "Endpoint identifier. With HTTP monitoring the identifier of the endpoint is mapped\n to a URL (e.g. 'foo' is mapped to '/foo').", - "deprecation": { - "reason": "Endpoint identifier is no longer customizable.", - "level": "error" - } - }, - { - "name": "endpoints.env.keys-to-sanitize", - "type": "java.lang.String[]", - "description": "Keys that should be sanitized. Keys can be simple strings that the property ends with or regex expressions.", - "deprecation": { - "replacement": "management.endpoint.env.keys-to-sanitize", - "level": "error" - } - }, - { - "name": "endpoints.env.path", - "type": "java.lang.String", - "description": "Endpoint URL path.", - "deprecation": { - "replacement": "management.endpoints.web.path-mapping.env", - "level": "error" - } - }, - { - "name": "endpoints.env.sensitive", - "type": "java.lang.Boolean", - "description": "Mark if the endpoint exposes sensitive information.", - "deprecation": { - "reason": "Endpoint sensitive flag is no longer customizable as Spring Boot no longer provides a customizable security auto-configuration\n. Create or adapt your security configuration accordingly.", - "level": "error" - } - }, - { - "name": "endpoints.flyway.enabled", - "type": "java.lang.Boolean", - "description": "Enable the endpoint.", - "deprecation": { - "replacement": "management.endpoint.flyway.enabled", - "level": "error" - } - }, - { - "name": "endpoints.flyway.id", - "type": "java.lang.String", - "description": "Endpoint identifier. With HTTP monitoring the identifier of the endpoint is mapped\n to a URL (e.g. 'foo' is mapped to '/foo').", - "deprecation": { - "reason": "Endpoint identifier is no longer customizable.","level": "error" - } - }, - { - "name": "endpoints.flyway.sensitive", - "type": "java.lang.Boolean", - "description": "Mark if the endpoint exposes sensitive information.", - "deprecation": { - "reason": "Endpoint sensitive flag is no longer customizable as Spring Boot no longer provides a customizable security auto-configuration\n. Create or adapt your security configuration accordingly.", - "level": "error" - } - }, - { - "name": "endpoints.health.enabled", - "type": "java.lang.Boolean", - "description": "Enable the endpoint.", - "deprecation": { - "replacement": "management.endpoint.health.enabled", - "level": "error" - } - }, - { - "name": "endpoints.health.id", - "type": "java.lang.String", - "description": "Endpoint identifier. With HTTP monitoring the identifier of the endpoint is mapped\n to a URL (e.g. 'foo' is mapped to '/foo').", - "deprecation": { - "reason": "Endpoint identifier is no longer customizable.", - "level": "error" - } - }, - { - "name": "endpoints.health.mapping", - "type": "java.util.Map", - "description": "Mapping of health statuses to HTTP status codes. By default, registered health\n statuses map to sensible defaults (i.e. UP maps to 200).", - "deprecation": { - "replacement": "management.health.status.http-mapping", - "level": "error" - } - }, - { - "name": "endpoints.health.path", - "type": "java.lang.String", - "description": "Endpoint URL path.", - "deprecation": { - "replacement": "management.endpoints.web.path-mapping.health", - "level": "error" - } - }, - { - "name": "endpoints.health.sensitive", - "type": "java.lang.Boolean", - "description": "Mark if the endpoint exposes sensitive information.", - "deprecation": { - "reason": "Endpoint sensitive flag is no longer customizable as Spring Boot no longer provides a customizable security auto-configuration\n. Create or adapt your security configuration accordingly.", - "level": "error" - } - }, - { - "name": "endpoints.health.time-to-live", - "type": "java.lang.Long", - "description": "Time to live for cached result, in milliseconds.", - "defaultValue": 1000, - "deprecation": { - "replacement": "management.endpoint.health.cache.time-to-live","level": "error" - } - }, - { - "name": "endpoints.heapdump.enabled", - "type": "java.lang.Boolean", - "description": "Enable the endpoint.", - "deprecation": { - "replacement": "management.endpoint.heapdump.enabled", - "level": "error" - } - }, - { - "name": "endpoints.heapdump.path", - "type": "java.lang.String", - "description": "Endpoint URL path.", - "deprecation": { - "replacement": "management.endpoints.web.path-mapping.heapdump", - "level": "error" - } - }, - { - "name": "endpoints.heapdump.sensitive", - "type": "java.lang.Boolean", - "description": "Mark if the endpoint exposes sensitive information.", - "deprecation": { - "reason": "Endpoint sensitive flag is no longer customizable as Spring Boot no longer provides a customizable security auto-configuration\n. Create or adapt your security configuration accordingly.", - "level": "error" - } - }, - { - "name": "endpoints.hypermedia.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable hypermedia support for endpoints.", - "defaultValue": false, - "deprecation": { - "reason": "Hypermedia support in the Actuator is no longer available.","level": "error" - } - }, - { - "name": "endpoints.info.enabled", - "type": "java.lang.Boolean", - "description": "Enable the endpoint.", - "deprecation": { - "replacement": "management.endpoint.info.enabled", - "level": "error" - } - }, - { - "name": "endpoints.info.id", - "type": "java.lang.String", - "description": "Endpoint identifier. With HTTP monitoring the identifier of the endpoint is mapped\n to a URL (e.g. 'foo' is mapped to '/foo').", - "deprecation": { - "reason": "Endpoint identifier is no longer customizable.", - "level": "error" - } - }, - { - "name": "endpoints.info.path", - "type": "java.lang.String", - "description": "Endpoint URL path.", - "deprecation": { - "replacement": "management.endpoints.web.path-mapping.info", - "level": "error" - } - }, - { - "name": "endpoints.info.sensitive", - "type": "java.lang.Boolean", - "description": "Mark if the endpoint exposes sensitive information.", - "deprecation": { - "reason": "Endpoint sensitive flag is no longer customizable as Spring Boot no longer provides a customizable security auto-configuration\n. Create or adapt your security configuration accordingly.", - "level": "error" - } - }, - { - "name": "endpoints.jmx.domain", - "type": "java.lang.String", - "description": "JMX domain name. Initialized with the value of 'spring.jmx.default-domain' if set.", - "deprecation": { - "replacement": "management.endpoints.jmx.domain", - "level": "error" - } - }, - { - "name": "endpoints.jmx.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable JMX export of all endpoints.", - "defaultValue": true, - "deprecation": { - "replacement": "management.endpoints.jmx.exposure.exclude", - "level": "error" - } - }, - { - "name": "endpoints.jmx.static-names", - "type": "java.util.Properties", - "description": "Additional static properties to append to all ObjectNames of MBeans representing\n Endpoints.", - "deprecation": { - "replacement": "management.endpoints.jmx.static-names", - "level": "error" - } - }, - { - "name": "endpoints.jmx.unique-names", - "type": "java.lang.Boolean", - "description": "Whether to ensure that ObjectNames are modified in case of conflict.", - "defaultValue": false, - "deprecation": { - "replacement": "management.endpoints.jmx.unique-names", - "level": "error" - } - }, - { - "name": "endpoints.jolokia.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable the endpoint.", - "deprecation": { - "replacement": "management.endpoint.jolokia.enabled", - "level": "error" - } - }, - { - "name": "endpoints.jolokia.path", - "type": "java.lang.String", - "description": "Endpoint URL path.", - "deprecation": { - "replacement": "management.endpoints.web.path-mapping.jolokia", - "level": "error" - } - }, - { - "name": "endpoints.jolokia.sensitive", - "type": "java.lang.Boolean", - "description": "Mark if the endpoint exposes sensitive information.", - "deprecation": { - "reason": "Endpoint sensitive flag is no longer customizable as Spring Boot no longer provides a customizable security auto-configuration\n. Create or adapt your security configuration accordingly.", - "level": "error" - } - }, - { - "name": "endpoints.liquibase.enabled", - "type": "java.lang.Boolean", - "description": "Enable the endpoint.", - "deprecation": { - "replacement": "management.endpoint.liquibase.enabled", - "level": "error" - } - }, - { - "name": "endpoints.liquibase.id", - "type": "java.lang.String", - "description": "Endpoint identifier. With HTTP monitoring the identifier of the endpoint is mapped\n to a URL (e.g. 'foo' is mapped to '/foo').", - "deprecation": { - "reason": "Endpoint identifier is no longer customizable.", - "level": "error" - } - }, - { - "name": "endpoints.liquibase.sensitive", - "type": "java.lang.Boolean", - "description": "Mark if the endpoint exposes sensitive information.", - "deprecation": { - "reason": "Endpoint sensitive flag is no longer customizable as Spring Boot no longer provides a customizable security auto-configuration\n. Create or adapt your security configuration accordingly.", - "level": "error" - } - }, - { - "name": "endpoints.logfile.enabled", - "type": "java.lang.Boolean", - "description": "Enable the endpoint.", - "deprecation": { - "replacement": "management.endpoint.logfile.enabled", - "level": "error" - } - }, - { - "name": "endpoints.logfile.external-file", - "type": "java.io.File", - "description": "External Logfile to be accessed. Can be used if the logfile is written by output\n redirect and not by the logging-system itself.", - "deprecation": { - "replacement": "management.endpoint.logfile.external-file", - "level": "error" - } - }, - { - "name": "endpoints.logfile.path", - "type": "java.lang.String", - "description": "Endpoint URL path.", - "deprecation": { - "replacement": "management.endpoints.web.path-mapping.logfile", - "level": "error" - } - }, - { - "name": "endpoints.logfile.sensitive", - "type": "java.lang.Boolean", - "description": "Mark if the endpoint exposes sensitive information.", - "deprecation": { - "reason": "Endpoint sensitive flag is no longer customizable as Spring Boot no longer provides a customizable security auto-configuration\n. Create or adapt your security configuration accordingly.", - "level": "error" - } - }, - { - "name": "endpoints.loggers.enabled", - "type": "java.lang.Boolean", - "description": "Enable the endpoint.", - "deprecation": { - "replacement": "management.endpoint.loggers.enabled", - "level": "error" - } - }, - { - "name": "endpoints.loggers.id", - "type": "java.lang.String", - "description": "Endpoint identifier. With HTTP monitoring the identifier of the endpoint is mapped\n to a URL (e.g. 'foo' is mapped to '/foo').", - "deprecation": { - "reason": "Endpoint identifier is no longer customizable.", - "level": "error" - } - }, - { - "name": "endpoints.loggers.path", - "type": "java.lang.String", - "description": "Endpoint URL path.", - "deprecation": { - "replacement": "management.endpoints.web.path-mapping.loggers", - "level": "error" - } - }, - { - "name": "endpoints.loggers.sensitive", - "type": "java.lang.Boolean", - "description": "Mark if the endpoint exposes sensitive information.", - "deprecation": { - "reason": "Endpoint sensitive flag is no longer customizable as Spring Boot no longer provides a customizable security auto-configuration\n. Create or adapt your security configuration accordingly.", - "level": "error" - } - }, - { - "name": "endpoints.mappings.enabled", - "type": "java.lang.Boolean", - "description": "Enable the endpoint.", - "deprecation": { - "replacement": "management.endpoint.mappings.enabled", - "level": "error" - } - }, - { - "name": "endpoints.mappings.id", - "type": "java.lang.String", - "description": "Endpoint identifier. With HTTP monitoring the identifier of the endpoint is mapped\n to a URL (e.g. 'foo' is mapped to '/foo').", - "deprecation": { - "reason": "Endpoint identifier is no longer customizable.", - "level": "error" - } - }, - { - "name": "endpoints.mappings.path", - "type": "java.lang.String", - "description": "Endpoint URL path.", - "deprecation": { - "replacement": "management.endpoints.web.path-mapping.mappings", - "level": "error" - } - }, - { - "name": "endpoints.mappings.sensitive", - "type": "java.lang.Boolean", - "description": "Mark if the endpoint exposes sensitive information.", - "deprecation": { - "reason": "Endpoint sensitive flag is no longer customizable as Spring Boot no longer provides a customizable security auto-configuration\n. Create or adapt your security configuration accordingly.", - "level": "error" - } - }, - { - "name": "endpoints.metrics.filter.counter-submissions", - "description": "Submissions that should be made to the counter.", - "deprecation": { - "reason": "Metrics support is now using Micrometer.", - "level": "error" - } - }, - { - "name": "endpoints.metrics.enabled", - "type": "java.lang.Boolean", - "description": "Enable the endpoint.", - "deprecation": { - "replacement": "management.endpoint.metrics.enabled", - "level": "error" - } - }, - { - "name": "endpoints.metrics.filter.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable the metrics servlet filter.", - "defaultValue": true, - "deprecation": { - "reason": "Metrics support is now using Micrometer.", - "level": "error" - } - }, - { - "name": "endpoints.metrics.filter.gauge-submissions", - "description": "Submissions that should be made to the gauge.", - "deprecation": { - "reason": "Metrics support is now using Micrometer.", - "level": "error" - } - }, - { - "name": "endpoints.metrics.id", - "type": "java.lang.String", - "description": "Endpoint identifier. With HTTP monitoring the identifier of the endpoint is mapped\n to a URL (e.g. 'foo' is mapped to '/foo').", - "deprecation": { - "reason": "Endpoint identifier is no longer customizable.", - "level": "error" - } - }, - { - "name": "endpoints.metrics.path", - "type": "java.lang.String", - "description": "Endpoint URL path.", - "deprecation": { - "replacement": "management.endpoints.web.path-mapping.metrics", - "level": "error" - } - }, - { - "name": "endpoints.metrics.sensitive", - "type": "java.lang.Boolean", - "description": "Mark if the endpoint exposes sensitive information.", - "deprecation": { - "reason": "Endpoint sensitive flag is no longer customizable as Spring Boot no longer provides a customizable security auto-configuration\n. Create or adapt your security configuration accordingly.", - "level": "error" - } - }, - { - "name": "endpoints.sensitive", - "type": "java.lang.Boolean", - "description": "Default endpoint sensitive setting.", - "deprecation": { - "reason": "Endpoint sensitive flag is no longer customizable as Spring Boot no longer provides a customizable security auto-configuration\n. Create or adapt your security configuration accordingly.", - "level": "error" - } - }, - { - "name": "endpoints.shutdown.enabled", - "type": "java.lang.Boolean", - "description": "Enable the endpoint.", - "deprecation": { - "replacement": "management.endpoint.shutdown.enabled", - "level": "error" - } - }, - { - "name": "endpoints.shutdown.id", - "type": "java.lang.String", - "description": "Endpoint identifier. With HTTP monitoring the identifier of the endpoint is mapped\n to a URL (e.g. 'foo' is mapped to '/foo').", - "deprecation": { - "reason": "Endpoint identifier is no longer customizable.", - "level": "error" - } - }, - { - "name": "endpoints.shutdown.path", - "type": "java.lang.String", - "description": "Endpoint URL path.", - "deprecation": { - "replacement": "management.endpoints.web.path-mapping.shutdown", - "level": "error" - } - }, - { - "name": "endpoints.shutdown.sensitive", - "type": "java.lang.Boolean", - "description": "Mark if the endpoint exposes sensitive information.", - "deprecation": { - "reason": "Endpoint sensitive flag is no longer customizable as Spring Boot no longer provides a customizable security auto-configuration\n. Create or adapt your security configuration accordingly.", - "level": "error" - } - }, - { - "name": "endpoints.trace.filter.enabled", - "type": "java.lang.Boolean", - "description": "Enable the trace servlet filter.", - "defaultValue": true, - "deprecation": { - "replacement": "management.trace.http.enabled", - "level": "error" - } - }, - { - "name": "endpoints.trace.enabled", - "type": "java.lang.Boolean", - "description": "Enable the endpoint.", - "deprecation": { - "replacement": "management.endpoint.httptrace.enabled", - "level": "error" - } - }, - { - "name": "endpoints.trace.id", - "type": "java.lang.String", - "description": "Endpoint identifier. With HTTP monitoring the identifier of the endpoint is mapped\n to a URL (e.g. 'foo' is mapped to '/foo').", - "deprecation": { - "reason": "Endpoint identifier is no longer customizable.", - "level": "error" - } - }, - { - "name": "endpoints.trace.path", - "type": "java.lang.String", - "description": "Endpoint URL path.", - "deprecation": { - "replacement": "management.endpoints.web.path-mapping.httptrace", - "level": "error" - } - }, - { - "name": "endpoints.trace.sensitive", - "type": "java.lang.Boolean", - "description": "Mark if the endpoint exposes sensitive information.", - "deprecation": { - "reason": "Endpoint sensitive flag is no longer customizable as Spring Boot no longer provides a customizable security auto-configuration\n. Create or adapt your security configuration accordingly.", - "level": "error" - } - }, - { - "name": "jolokia.config", - "type": "java.util.Map", - "description": "Jolokia settings. These are traditionally set using servlet parameters. Refer to\n the documentation of Jolokia for more details.", - "deprecation": { - "replacement": "management.endpoint.jolokia.config", - "level": "error" - } - }, - { - "name": "management.add-application-context-header", - "type": "java.lang.Boolean", - "description": "Add the \"X-Application-Context\" HTTP header in each response.", - "defaultValue": true, - "deprecation": { - "replacement": "management.server.add-application-context-header", - "level": "error" - } - }, - { - "name": "management.address", - "type": "java.net.InetAddress", - "description": "Network address that the management endpoints should bind to.", - "deprecation": { - "replacement": "management.server.address", - "level": "error" - } - }, - { - "name": "management.context-path", - "type": "java.lang.String", - "description": "Management endpoint context-path.", - "defaultValue": "", - "deprecation": { - "replacement": "management.server.servlet.context-path", - "level": "error" - } - }, - { - "name": "management.health.couchbase.timeout", - "type": "java.time.Duration", - "description": "Timeout for getting the Bucket information from the server.", - "defaultValue": "1000ms", - "deprecation": { - "level": "error" - } - }, - { - "name": "management.metrics.binders.files.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable files metrics.", - "defaultValue": true, - "deprecation": { - "level": "error", - "replacement": "management.metrics.enable.process.files", - "reason": "Instead, filter 'process.files' metrics." - } - }, - { - "name": "management.metrics.binders.jvm.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable JVM metrics.", - "defaultValue": true, - "deprecation": { - "level": "error", - "replacement": "management.metrics.enable.jvm", - "reason": "Instead, disable JvmMetricsAutoConfiguration or filter 'jvm' metrics." - } - }, - { - "name": "management.metrics.binders.logback.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable Logback metrics.", - "defaultValue": true, - "deprecation": { - "level": "error", - "replacement": "management.metrics.enable.logback", - "reason": "Instead, disable LogbackMetricsAutoConfiguration or filter 'logback' metrics." - } - }, - { - "name": "management.metrics.binders.processor.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable processor metrics.", - "defaultValue": true, - "deprecation": { - "level": "error", - "reason": "Instead, filter 'system.cpu' and 'process.cpu' metrics." - } - }, - { - "name": "management.metrics.binders.uptime.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable uptime metrics.", - "defaultValue": true, - "deprecation": { - "level": "error", - "reason": "Instead, filter 'process.uptime' and 'process.start.time' metrics." - } - }, - { - "name": "management.port", - "type": "java.lang.Integer", - "description": "Management endpoint HTTP port. Use the same port as the application by default.", - "deprecation": { - "replacement": "management.server.port", - "level": "error" - } - }, - { - "name": "management.security.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable security.", - "defaultValue": true, - "deprecation": { - "reason": "A global security auto-configuration is now provided. Provide your own WebSecurityConfigurer bean instead.", - "level": "error" - } - }, - { - "name": "management.security.roles", - "type": "java.util.List", - "description": "Comma-separated list of roles that can access the management endpoint.", - "deprecation": { - "reason": "The security auto-configuration is no longer customizable. Provide your own WebSecurityConfigurer bean instead.", - "level": "error" - } - }, - { - "name": "management.security.sessions", - "description": "Session creating policy for security use (always, never, if_required,\n stateless).", - "defaultValue": "stateless", - "deprecation": { - "reason": "The security auto-configuration is no longer customizable. Provide your own WebSecurityConfigurer bean instead.", - "level": "error" - } - }, - { - "name": "management.server.add-application-context-header", - "type": "java.lang.Boolean", - "description": "Add the \"X-Application-Context\" HTTP header in each response.", - "defaultValue": false - }, - { - "name": "management.shell.auth.jaas.domain", - "type": "java.lang.String", - "description": "JAAS domain.", - "defaultValue": "my-domain", - "deprecation": { - "reason": "CRaSH support is no longer available.", - "level": "error" - } - }, - { - "name": "management.shell.auth.key.path", - "type": "java.lang.String", - "description": "Path to the authentication key. This should point to a valid \".pem\" file.", - "deprecation": { - "reason": "CRaSH support is no longer available.", - "level": "error" - } - }, - { - "name": "management.shell.auth.simple.user.name", - "type": "java.lang.String", - "description": "Login user.", - "defaultValue": "user", - "deprecation": { - "reason": "CRaSH support is no longer available.", - "level": "error" - } - }, - { - "name": "management.shell.auth.simple.user.password", - "type": "java.lang.String", - "description": "Login password.", - "deprecation": { - "reason": "CRaSH support is no longer available.", - "level": "error" - } - }, - { - "name": "management.shell.auth.spring.roles", - "type": "java.lang.String[]", - "description": "Comma-separated list of required roles to login to the CRaSH console.", - "defaultValue": [ - "ACTUATOR" - ], - "deprecation": { - "reason": "CRaSH support is no longer available.", - "level": "error" - } - }, - { - "name": "management.shell.auth.type", - "type": "java.lang.String", - "description": "Authentication type. Auto-detected according to the environment (i.e. if Spring\n Security is available, \"spring\" is used by default).", - "defaultValue": "simple", + "name": "management.metrics.export.statsd.queue-size", + "defaultValue": 2147483647, "deprecation": { - "reason": "CRaSH support is no longer available.", "level": "error" } }, { - "name": "management.shell.command-path-patterns", - "type": "java.lang.String[]", - "description": "Patterns to use to look for commands.", - "defaultValue": [ - "classpath*:/commands/**", - "classpath*:/crash/commands/**" - ], + "name": "management.metrics.export.wavefront.connect-timeout", "deprecation": { - "reason": "CRaSH support is no longer available.", "level": "error" } }, { - "name": "management.shell.command-refresh-interval", + "name": "management.metrics.export.wavefront.num-threads", "type": "java.lang.Integer", - "description": "Scan for changes and update the command if necessary (in seconds).", - "defaultValue": -1, + "description": "Number of threads to use with the metrics publishing scheduler.", + "defaultValue": 2, "deprecation": { - "reason": "CRaSH support is no longer available.", "level": "error" } }, { - "name": "management.shell.config-path-patterns", - "type": "java.lang.String[]", - "description": "Patterns to use to look for configurations.", - "defaultValue": [ - "classpath*:/crash/*" - ], + "name": "management.metrics.export.wavefront.read-timeout", "deprecation": { - "reason": "CRaSH support is no longer available.", "level": "error" } }, { - "name": "management.shell.disabled-commands", - "type": "java.lang.String[]", - "description": "Comma-separated list of commands to disable.", - "defaultValue": [ - "jpa*", - "jdbc*", - "jndi*" - ], - "deprecation": { - "reason": "CRaSH support is no longer available.", - "level": "error" - } + "name": "management.metrics.mongo.command.enabled", + "description": "Whether to enable Mongo client command metrics.", + "defaultValue": true }, { - "name": "management.shell.disabled-plugins", - "type": "java.lang.String[]", - "description": "Comma-separated list of plugins to disable. Certain plugins are disabled by default\n based on the environment.", - "defaultValue": [], - "deprecation": { - "reason": "CRaSH support is no longer available.", - "level": "error" - } + "name": "management.metrics.mongo.connectionpool.enabled", + "description": "Whether to enable Mongo connection pool metrics.", + "defaultValue": true }, { - "name": "management.shell.ssh.auth-timeout", - "type": "java.lang.Integer", - "description": "Number of milliseconds after user will be prompted to login again.", - "defaultValue": 600000, - "deprecation": { - "reason": "CRaSH support is no longer available.", - "level": "error" - } + "name": "management.metrics.web.client.request.autotime.enabled", + "description": "Whether to automatically time web client requests.", + "defaultValue": true }, { - "name": "management.shell.ssh.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable CRaSH SSH support.", - "defaultValue": true, - "deprecation": { - "reason": "CRaSH support is no longer available.", - "level": "error" - } + "name": "management.metrics.web.client.request.autotime.percentiles", + "description": "Computed non-aggregable percentiles to publish." }, { - "name": "management.shell.ssh.idle-timeout", - "type": "java.lang.Integer", - "description": "Number of milliseconds after which unused connections are closed.", - "defaultValue": 600000, - "deprecation": { - "reason": "CRaSH support is no longer available.", - "level": "error" - } + "name": "management.metrics.web.client.request.autotime.percentiles-histogram", + "description": "Whether percentile histograms should be published.", + "defaultValue": false }, { - "name": "management.shell.ssh.key-path", + "name": "management.metrics.web.client.requests-metric-name", "type": "java.lang.String", - "description": "Path to the SSH server key.", - "deprecation": { - "reason": "CRaSH support is no longer available.", - "level": "error" - } - }, - { - "name": "management.shell.ssh.port", - "type": "java.lang.Integer", - "description": "SSH port.", - "defaultValue": 2000, - "deprecation": { - "reason": "CRaSH support is no longer available.", - "level": "error" - } - }, - { - "name": "management.shell.telnet.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable CRaSH telnet support. Enabled by default if the TelnetPlugin is available.", - "defaultValue": false, - "deprecation": { - "reason": "CRaSH support is no longer available.", - "level": "error" - } - }, - { - "name": "management.shell.telnet.port", - "type": "java.lang.Integer", - "description": "Telnet port.", - "defaultValue": 5000, - "deprecation": { - "reason": "CRaSH support is no longer available.","level": "error" - } - }, - { - "name": "management.ssl.ciphers", - "type": "java.lang.String[]", - "deprecation": { - "replacement": "management.server.ssl.ciphers", - "level": "error" - } - }, - { - "name": "management.ssl.client-auth", "deprecation": { - "replacement": "management.server.ssl.client-auth", + "replacement": "management.metrics.web.client.request.metric-name", "level": "error" } }, { - "name": "management.ssl.enabled", + "name": "management.metrics.web.server.auto-time-requests", "type": "java.lang.Boolean", "deprecation": { - "replacement": "management.server.ssl.enabled", - "level": "error" - } - }, - { - "name": "management.ssl.enabled-protocols", - "type": "java.lang.String[]", - "deprecation": { - "replacement": "management.server.ssl.enabled-protocols", + "replacement": "management.metrics.web.server.request.autotime.enabled", "level": "error" } }, { - "name": "management.ssl.key-alias", - "type": "java.lang.String", - "deprecation": { - "replacement": "management.server.ssl.key-alias", - "level": "error" - } + "name": "management.metrics.web.server.request.autotime.enabled", + "description": "Whether to automatically time web server requests.", + "defaultValue": true }, { - "name": "management.ssl.key-password", - "type": "java.lang.String", - "deprecation": { - "replacement": "management.server.ssl.key-password", - "level": "error" - } + "name": "management.metrics.web.server.request.autotime.percentiles", + "description": "Computed non-aggregable percentiles to publish." }, { - "name": "management.ssl.key-store", - "type": "java.lang.String", - "deprecation": { - "replacement": "management.server.ssl.key-store", - "level": "error" - } + "name": "management.metrics.web.server.request.autotime.percentiles-histogram", + "description": "Whether percentile histograms should be published.", + "defaultValue": false }, { - "name": "management.ssl.key-store-password", + "name": "management.metrics.web.server.requests-metric-name", "type": "java.lang.String", "deprecation": { - "replacement": "management.server.ssl.key-store-password", + "replacement": "management.metrics.web.server.request.metric-name", "level": "error" } }, { - "name": "management.ssl.key-store-provider", - "type": "java.lang.String", - "deprecation": { - "replacement": "management.server.ssl.key-store-provider", - "level": "error" - } + "name": "management.server.add-application-context-header", + "type": "java.lang.Boolean", + "description": "Add the \"X-Application-Context\" HTTP header in each response.", + "defaultValue": false }, { - "name": "management.ssl.key-store-type", - "type": "java.lang.String", - "deprecation": { - "replacement": "management.server.ssl.key-store-type", - "level": "error" - } + "name": "management.server.ssl.ciphers", + "description": "Supported SSL ciphers." }, { - "name": "management.ssl.protocol", - "type": "java.lang.String", - "deprecation": { - "replacement": "management.server.ssl.protocol", - "level": "error" - } + "name": "management.server.ssl.client-auth", + "description": "Client authentication mode. Requires a trust store." }, { - "name": "management.ssl.trust-store", - "type": "java.lang.String", - "deprecation": { - "replacement": "management.server.ssl.trust-store", - "level": "error" - } + "name": "management.server.ssl.enabled", + "description": "Whether to enable SSL support.", + "defaultValue": true }, { - "name": "management.ssl.trust-store-password", - "type": "java.lang.String", - "deprecation": { - "replacement": "management.server.ssl.trust-store-password", - "level": "error" - } + "name": "management.server.ssl.enabled-protocols", + "description": "Enabled SSL protocols." }, { - "name": "management.ssl.trust-store-provider", - "type": "java.lang.String", - "deprecation": { - "replacement": "management.server.ssl.trust-store-provider", - "level": "error" - } + "name": "management.server.ssl.key-alias", + "description": "Alias that identifies the key in the key store." }, { - "name": "management.ssl.trust-store-type", - "type": "java.lang.String", - "deprecation": { - "replacement": "management.server.ssl.trust-store-type", - "level": "error" - } + "name": "management.server.ssl.key-password", + "description": "Password used to access the key in the key store." }, { - "name": "management.trace.include", - "deprecation": { - "replacement": "management.trace.http.include", - "level": "error" - } + "name": "management.server.ssl.key-store", + "description": "Path to the key store that holds the SSL certificate (typically a jks file)." }, { - "name": "spring.metrics.export.aggregate.key-pattern", - "type": "java.lang.String", - "description": "Pattern that tells the aggregator what to do with the keys from the source\n repository. The keys in the source repository are assumed to be period\n separated, and the pattern is in the same format, e.g. \"d.d.k.d\". Here \"d\"\n means \"discard\" and \"k\" means \"keep\" the key segment in the corresponding\n position in the source.", - "defaultValue": "", - "deprecation": { - "reason": "Metrics support is now using Micrometer.", - "level": "error" - } + "name": "management.server.ssl.key-store-password", + "description": "Password used to access the key store." }, { - "name": "spring.metrics.export.aggregate.prefix", - "type": "java.lang.String", - "description": "Prefix for global repository if active. Should be unique for this JVM, but most\n useful if it also has the form \"a.b\" where \"a\" is unique to this logical\n process (this application) and \"b\" is unique to this physical process. If you\n set spring.application.name elsewhere, then the default will be in the right\n form.", - "defaultValue": "", - "deprecation": { - "reason": "Metrics support is now using Micrometer.", - "level": "error" - } + "name": "management.server.ssl.key-store-provider", + "description": "Provider for the key store." }, { - "name": "spring.metrics.export.delay-millis", - "type": "java.lang.Long", - "description": "Delay in milliseconds between export ticks. Metrics are exported to external\n sources on a schedule with this delay.", - "deprecation": { - "reason": "Metrics support is now using Micrometer.", - "level": "error" - } + "name": "management.server.ssl.key-store-type", + "description": "Type of the key store." }, { - "name": "spring.metrics.export.enabled", - "type": "java.lang.Boolean", - "description": "Flag to enable metric export (assuming a MetricWriter is available).", - "defaultValue": true, - "deprecation": { - "reason": "Metrics support is now using Micrometer.", - "level": "error" - } + "name": "management.server.ssl.protocol", + "description": "SSL protocol to use.", + "defaultValue": "TLS" }, { - "name": "spring.metrics.export.excludes", - "type": "java.lang.String[]", - "description": "List of patterns for metric names to exclude. Applied after the includes.", - "deprecation": { - "reason": "Metrics support is now using Micrometer.", - "level": "error" - } + "name": "management.server.ssl.trust-store", + "description": "Trust store that holds SSL certificates." }, { - "name": "spring.metrics.export.includes", - "type": "java.lang.String[]", - "description": "List of patterns for metric names to include.", - "deprecation": { - "reason": "Metrics support is now using Micrometer.", - "level": "error" - } + "name": "management.server.ssl.trust-store-password", + "description": "Password used to access the trust store." }, { - "name": "spring.metrics.export.redis.key", - "type": "java.lang.String", - "description": "Key for redis repository export (if active). Should be globally unique for a\n system sharing a redis repository across multiple processes.", - "defaultValue": "keys.spring.metrics", - "deprecation": { - "reason": "Metrics support is now using Micrometer.", - "level": "error" - } + "name": "management.server.ssl.trust-store-provider", + "description": "Provider for the trust store." }, { - "name": "spring.metrics.export.redis.prefix", - "type": "java.lang.String", - "description": "Prefix for redis repository if active. Should be globally unique across all\n processes sharing the same repository.", - "defaultValue": "spring.metrics", - "deprecation": { - "reason": "Metrics support is now using Micrometer.", - "level": "error" - } + "name": "management.server.ssl.trust-store-type", + "description": "Type of the trust store." }, { - "name": "spring.metrics.export.send-latest", + "name": "management.trace.http.enabled", "type": "java.lang.Boolean", - "description": "Flag to switch off any available optimizations based on not exporting unchanged\n metric values.", - "deprecation": { - "reason": "Metrics support is now using Micrometer.", - "level": "error" - } - }, - { - "name": "spring.metrics.export.statsd.host", - "type": "java.lang.String", - "description": "Host of a statsd server to receive exported metrics.", - "deprecation": { - "replacement": "management.metrics.export.statsd.host", - "level": "error" - } - }, - { - "name": "spring.metrics.export.statsd.port", - "type": "java.lang.Integer", - "description": "Port of a statsd server to receive exported metrics.", - "defaultValue": 8125, - "deprecation": { - "replacement": "management.metrics.export.statsd.port", - "level": "error" - } + "description": "Whether to enable HTTP request-response tracing.", + "defaultValue": true }, { - "name": "spring.metrics.export.statsd.prefix", - "type": "java.lang.String", - "description": "Prefix for statsd exported metrics.", - "deprecation": { - "reason": "Metrics support is now using Micrometer.", - "level": "error" - } + "name": "management.trace.http.include", + "defaultValue": [ + "request-headers", + "response-headers", + "errors" + ] }, { - "name": "spring.metrics.export.triggers", - "description": "Specific trigger properties per MetricWriter bean name.", + "name": "management.trace.include", "deprecation": { - "reason": "Metrics support is now using Micrometer.", + "replacement": "management.trace.http.include", "level": "error" } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories index 43cbe4ceb5ed..ed0cca8ecc6e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories @@ -2,6 +2,8 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.boot.actuate.autoconfigure.amqp.RabbitHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.audit.AuditAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.audit.AuditEventsEndpointAutoConfiguration,\ +org.springframework.boot.actuate.autoconfigure.availability.AvailabilityHealthContributorAutoConfiguration,\ +org.springframework.boot.actuate.autoconfigure.availability.AvailabilityProbesAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.beans.BeansEndpointAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.cache.CachesEndpointAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.cassandra.CassandraHealthContributorAutoConfiguration,\ @@ -13,8 +15,7 @@ org.springframework.boot.actuate.autoconfigure.context.properties.ConfigurationP org.springframework.boot.actuate.autoconfigure.context.ShutdownEndpointAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.couchbase.CouchbaseHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.couchbase.CouchbaseReactiveHealthContributorAutoConfiguration,\ -org.springframework.boot.actuate.autoconfigure.elasticsearch.ElasticSearchClientHealthContributorAutoConfiguration,\ -org.springframework.boot.actuate.autoconfigure.elasticsearch.ElasticSearchJestHealthContributorAutoConfiguration,\ +org.springframework.boot.actuate.autoconfigure.elasticsearch.ElasticSearchReactiveHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.elasticsearch.ElasticSearchRestHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.endpoint.jmx.JmxEndpointAutoConfiguration,\ @@ -24,7 +25,6 @@ org.springframework.boot.actuate.autoconfigure.flyway.FlywayEndpointAutoConfigur org.springframework.boot.actuate.autoconfigure.hazelcast.HazelcastHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration,\ -org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.influx.InfluxDbHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.info.InfoContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration,\ @@ -49,6 +49,7 @@ org.springframework.boot.actuate.autoconfigure.metrics.MetricsEndpointAutoConfig org.springframework.boot.actuate.autoconfigure.metrics.SystemMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.amqp.RabbitMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.cache.CacheMetricsAutoConfiguration,\ +org.springframework.boot.actuate.autoconfigure.metrics.data.RepositoryMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.export.appoptics.AppOpticsMetricsExportAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.export.atlas.AtlasMetricsExportAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.export.datadog.DatadogMetricsExportAutoConfiguration,\ @@ -64,11 +65,15 @@ org.springframework.boot.actuate.autoconfigure.metrics.export.newrelic.NewRelicM org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusMetricsExportAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.export.signalfx.SignalFxMetricsExportAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration,\ +org.springframework.boot.actuate.autoconfigure.metrics.export.stackdriver.StackdriverMetricsExportAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.export.statsd.StatsdMetricsExportAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.export.wavefront.WavefrontMetricsExportAutoConfiguration,\ +org.springframework.boot.actuate.autoconfigure.metrics.integration.IntegrationMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.jdbc.DataSourcePoolMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.jersey.JerseyServerMetricsAutoConfiguration,\ +org.springframework.boot.actuate.autoconfigure.metrics.mongo.MongoMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.orm.jpa.HibernateMetricsAutoConfiguration,\ +org.springframework.boot.actuate.autoconfigure.metrics.r2dbc.ConnectionPoolMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.web.client.HttpClientMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.web.jetty.JettyMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.web.reactive.WebFluxMetricsAutoConfiguration,\ @@ -77,6 +82,8 @@ org.springframework.boot.actuate.autoconfigure.metrics.web.tomcat.TomcatMetricsA org.springframework.boot.actuate.autoconfigure.mongo.MongoHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.mongo.MongoReactiveHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.neo4j.Neo4jHealthContributorAutoConfiguration,\ +org.springframework.boot.actuate.autoconfigure.quartz.QuartzEndpointAutoConfiguration,\ +org.springframework.boot.actuate.autoconfigure.r2dbc.ConnectionFactoryHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.redis.RedisHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.redis.RedisReactiveHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.scheduling.ScheduledTasksEndpointAutoConfiguration,\ @@ -84,6 +91,7 @@ org.springframework.boot.actuate.autoconfigure.security.reactive.ReactiveManagem org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.session.SessionsEndpointAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.solr.SolrHealthContributorAutoConfiguration,\ +org.springframework.boot.actuate.autoconfigure.startup.StartupEndpointAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.trace.http.HttpTraceAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.trace.http.HttpTraceEndpointAutoConfiguration,\ @@ -104,4 +112,4 @@ org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementChil org.springframework.boot.actuate.autoconfigure.web.servlet.WebMvcEndpointChildContextConfiguration org.springframework.boot.diagnostics.FailureAnalyzer=\ -org.springframework.boot.actuate.autoconfigure.metrics.MissingRequiredConfigurationFailureAnalyzer +org.springframework.boot.actuate.autoconfigure.metrics.ValidationFailureAnalyzer diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/SpringApplicationHierarchyTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/SpringApplicationHierarchyTests.java index cd2fc368e368..5de0389b3143 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/SpringApplicationHierarchyTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/SpringApplicationHierarchyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,6 +32,7 @@ import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration; +import org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.test.util.ApplicationContextTestUtils; import org.springframework.context.ConfigurableApplicationContext; @@ -66,29 +67,23 @@ void testChild() { } @Configuration - @EnableAutoConfiguration( - exclude = { ElasticsearchDataAutoConfiguration.class, ElasticsearchRepositoriesAutoConfiguration.class, - CassandraAutoConfiguration.class, CassandraDataAutoConfiguration.class, - MongoDataAutoConfiguration.class, MongoReactiveDataAutoConfiguration.class, - Neo4jDataAutoConfiguration.class, Neo4jRepositoriesAutoConfiguration.class, - RedisAutoConfiguration.class, RedisRepositoriesAutoConfiguration.class, - FlywayAutoConfiguration.class, MetricsAutoConfiguration.class }, - excludeName = { - "org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchAutoConfiguration" }) + @EnableAutoConfiguration(exclude = { ElasticsearchDataAutoConfiguration.class, + ElasticsearchRepositoriesAutoConfiguration.class, CassandraAutoConfiguration.class, + CassandraDataAutoConfiguration.class, MongoDataAutoConfiguration.class, + MongoReactiveDataAutoConfiguration.class, Neo4jAutoConfiguration.class, Neo4jDataAutoConfiguration.class, + Neo4jRepositoriesAutoConfiguration.class, RedisAutoConfiguration.class, + RedisRepositoriesAutoConfiguration.class, FlywayAutoConfiguration.class, MetricsAutoConfiguration.class }) static class Parent { } @Configuration - @EnableAutoConfiguration( - exclude = { ElasticsearchDataAutoConfiguration.class, ElasticsearchRepositoriesAutoConfiguration.class, - CassandraAutoConfiguration.class, CassandraDataAutoConfiguration.class, - MongoDataAutoConfiguration.class, MongoReactiveDataAutoConfiguration.class, - Neo4jDataAutoConfiguration.class, Neo4jRepositoriesAutoConfiguration.class, - RedisAutoConfiguration.class, RedisRepositoriesAutoConfiguration.class, - FlywayAutoConfiguration.class, MetricsAutoConfiguration.class }, - excludeName = { - "org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchAutoConfiguration" }) + @EnableAutoConfiguration(exclude = { ElasticsearchDataAutoConfiguration.class, + ElasticsearchRepositoriesAutoConfiguration.class, CassandraAutoConfiguration.class, + CassandraDataAutoConfiguration.class, MongoDataAutoConfiguration.class, + MongoReactiveDataAutoConfiguration.class, Neo4jAutoConfiguration.class, Neo4jDataAutoConfiguration.class, + Neo4jRepositoriesAutoConfiguration.class, RedisAutoConfiguration.class, + RedisRepositoriesAutoConfiguration.class, FlywayAutoConfiguration.class, MetricsAutoConfiguration.class }) static class Child { } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/amqp/RabbitHealthContributorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/amqp/RabbitHealthContributorAutoConfigurationTests.java index 25ddb2d9eeaa..e642c1015f0e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/amqp/RabbitHealthContributorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/amqp/RabbitHealthContributorAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ */ class RabbitHealthContributorAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(RabbitAutoConfiguration.class, RabbitHealthContributorAutoConfiguration.class, HealthContributorAutoConfiguration.class)); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/audit/AuditAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/audit/AuditAutoConfigurationTests.java index b8fea5652928..471e2dd0bb96 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/audit/AuditAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/audit/AuditAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,7 +46,7 @@ */ class AuditAutoConfigurationTests { - private WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(AuditAutoConfiguration.class)); @Test diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityHealthContributorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityHealthContributorAutoConfigurationTests.java new file mode 100644 index 000000000000..dce124c76598 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityHealthContributorAutoConfigurationTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.availability; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.availability.LivenessStateHealthIndicator; +import org.springframework.boot.actuate.availability.ReadinessStateHealthIndicator; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration; +import org.springframework.boot.availability.ApplicationAvailability; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AvailabilityHealthContributorAutoConfiguration} + * + * @author Brian Clozel + */ +class AvailabilityHealthContributorAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ApplicationAvailabilityAutoConfiguration.class, + AvailabilityHealthContributorAutoConfiguration.class)); + + @Test + void probesWhenNotKubernetesAddsNoBeans() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ApplicationAvailability.class) + .doesNotHaveBean(LivenessStateHealthIndicator.class) + .doesNotHaveBean(ReadinessStateHealthIndicator.class)); + } + + @Test + void livenessIndicatorWhenPropertyEnabledAddsBeans() { + this.contextRunner.withPropertyValues("management.health.livenessState.enabled=true") + .run((context) -> assertThat(context).hasSingleBean(ApplicationAvailability.class) + .hasSingleBean(LivenessStateHealthIndicator.class) + .doesNotHaveBean(ReadinessStateHealthIndicator.class)); + } + + @Test + void readinessIndicatorWhenPropertyEnabledAddsBeans() { + this.contextRunner.withPropertyValues("management.health.readinessState.enabled=true") + .run((context) -> assertThat(context).hasSingleBean(ApplicationAvailability.class) + .hasSingleBean(ReadinessStateHealthIndicator.class) + .doesNotHaveBean(LivenessStateHealthIndicator.class)); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesAutoConfigurationTests.java new file mode 100644 index 000000000000..7e422cc57959 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesAutoConfigurationTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.availability; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.availability.LivenessStateHealthIndicator; +import org.springframework.boot.actuate.availability.ReadinessStateHealthIndicator; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration; +import org.springframework.boot.availability.ApplicationAvailability; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AvailabilityProbesAutoConfiguration}. + * + * @author Brian Clozel + */ +class AvailabilityProbesAutoConfigurationTests { + + private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ApplicationAvailabilityAutoConfiguration.class, + AvailabilityHealthContributorAutoConfiguration.class, AvailabilityProbesAutoConfiguration.class)); + + @Test + void probesWhenNotKubernetesAddsNoBeans() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ApplicationAvailability.class) + .doesNotHaveBean(LivenessStateHealthIndicator.class) + .doesNotHaveBean(ReadinessStateHealthIndicator.class) + .doesNotHaveBean(AvailabilityProbesHealthEndpointGroupsPostProcessor.class)); + } + + @Test + void probesWhenKubernetesAddsBeans() { + this.contextRunner.withPropertyValues("spring.main.cloud-platform=kubernetes") + .run((context) -> assertThat(context).hasSingleBean(ApplicationAvailability.class) + .hasSingleBean(LivenessStateHealthIndicator.class).hasBean("livenessStateHealthIndicator") + .hasSingleBean(ReadinessStateHealthIndicator.class).hasBean("readinessStateHealthIndicator") + .hasSingleBean(AvailabilityProbesHealthEndpointGroupsPostProcessor.class)); + } + + @Test + void probesWhenPropertyEnabledAddsBeans() { + this.contextRunner.withPropertyValues("management.endpoint.health.probes.enabled=true") + .run((context) -> assertThat(context).hasSingleBean(ApplicationAvailability.class) + .hasSingleBean(LivenessStateHealthIndicator.class).hasBean("livenessStateHealthIndicator") + .hasSingleBean(ReadinessStateHealthIndicator.class).hasBean("readinessStateHealthIndicator") + .hasSingleBean(AvailabilityProbesHealthEndpointGroupsPostProcessor.class)); + } + + @Test + void probesWhenKubernetesAndPropertyDisabledAddsNotBeans() { + this.contextRunner + .withPropertyValues("spring.main.cloud-platform=kubernetes", + "management.endpoint.health.probes.enabled=false") + .run((context) -> assertThat(context).hasSingleBean(ApplicationAvailability.class) + .doesNotHaveBean(LivenessStateHealthIndicator.class) + .doesNotHaveBean(ReadinessStateHealthIndicator.class) + .doesNotHaveBean(AvailabilityProbesHealthEndpointGroupsPostProcessor.class)); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroupTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroupTests.java new file mode 100644 index 000000000000..1a80c04dc124 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroupTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.availability; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.endpoint.SecurityContext; +import org.springframework.boot.actuate.health.HttpCodeStatusMapper; +import org.springframework.boot.actuate.health.StatusAggregator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link AvailabilityProbesHealthEndpointGroup}. + * + * @author Phillip Webb + */ +class AvailabilityProbesHealthEndpointGroupTests { + + private AvailabilityProbesHealthEndpointGroup group = new AvailabilityProbesHealthEndpointGroup("a", "b"); + + @Test + void isMemberWhenMemberReturnsTrue() { + assertThat(this.group.isMember("a")).isTrue(); + assertThat(this.group.isMember("b")).isTrue(); + } + + @Test + void isMemberWhenNotMemberReturnsFalse() { + assertThat(this.group.isMember("c")).isFalse(); + } + + @Test + void showComponentsReturnsFalse() { + assertThat(this.group.showComponents(mock(SecurityContext.class))).isFalse(); + } + + @Test + void showDetailsReturnsFalse() { + assertThat(this.group.showDetails(mock(SecurityContext.class))).isFalse(); + } + + @Test + void getStatusAggregatorReturnsDefaultStatusAggregator() { + assertThat(this.group.getStatusAggregator()).isEqualTo(StatusAggregator.getDefault()); + } + + @Test + void getHttpCodeStatusMapperReturnsDefaultHttpCodeStatusMapper() { + assertThat(this.group.getHttpCodeStatusMapper()).isEqualTo(HttpCodeStatusMapper.DEFAULT); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroupsPostProcessorTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroupsPostProcessorTests.java new file mode 100644 index 000000000000..d7994b896a00 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroupsPostProcessorTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.availability; + +import java.util.LinkedHashSet; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.health.HealthEndpointGroups; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link AvailabilityProbesHealthEndpointGroupsPostProcessor}. + * + * @author Phillip Webb + */ +class AvailabilityProbesHealthEndpointGroupsPostProcessorTests { + + private AvailabilityProbesHealthEndpointGroupsPostProcessor postProcessor = new AvailabilityProbesHealthEndpointGroupsPostProcessor(); + + @Test + void postProcessHealthEndpointGroupsWhenGroupsAlreadyContainedReturnsOriginal() { + HealthEndpointGroups groups = mock(HealthEndpointGroups.class); + Set names = new LinkedHashSet<>(); + names.add("test"); + names.add("readiness"); + names.add("liveness"); + given(groups.getNames()).willReturn(names); + assertThat(this.postProcessor.postProcessHealthEndpointGroups(groups)).isSameAs(groups); + } + + @Test + void postProcessHealthEndpointGroupsWhenGroupContainsOneReturnsPostProcessed() { + HealthEndpointGroups groups = mock(HealthEndpointGroups.class); + Set names = new LinkedHashSet<>(); + names.add("test"); + names.add("readiness"); + given(groups.getNames()).willReturn(names); + assertThat(this.postProcessor.postProcessHealthEndpointGroups(groups)) + .isInstanceOf(AvailabilityProbesHealthEndpointGroups.class); + } + + @Test + void postProcessHealthEndpointGroupsWhenGroupsContainsNoneReturnsProcessed() { + HealthEndpointGroups groups = mock(HealthEndpointGroups.class); + Set names = new LinkedHashSet<>(); + names.add("test"); + names.add("spring"); + names.add("boot"); + given(groups.getNames()).willReturn(names); + assertThat(this.postProcessor.postProcessHealthEndpointGroups(groups)) + .isInstanceOf(AvailabilityProbesHealthEndpointGroups.class); + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroupsTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroupsTests.java new file mode 100644 index 000000000000..1ab7a565e8a4 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/availability/AvailabilityProbesHealthEndpointGroupsTests.java @@ -0,0 +1,124 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.availability; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.health.HealthEndpointGroup; +import org.springframework.boot.actuate.health.HealthEndpointGroups; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link AvailabilityProbesHealthEndpointGroups}. + * + * @author Phillip Webb + */ +class AvailabilityProbesHealthEndpointGroupsTests { + + private HealthEndpointGroups delegate; + + private HealthEndpointGroup group; + + @BeforeEach + void setup() { + this.delegate = mock(HealthEndpointGroups.class); + this.group = mock(HealthEndpointGroup.class); + } + + @Test + void createWhenGroupsIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new AvailabilityProbesHealthEndpointGroups(null)) + .withMessage("Groups must not be null"); + } + + @Test + void getPrimaryDelegatesToGroups() { + given(this.delegate.getPrimary()).willReturn(this.group); + HealthEndpointGroups availabilityProbes = new AvailabilityProbesHealthEndpointGroups(this.delegate); + assertThat(availabilityProbes.getPrimary()).isEqualTo(this.group); + } + + @Test + void getNamesIncludesAvailabilityProbeGroups() { + given(this.delegate.getNames()).willReturn(Collections.singleton("test")); + HealthEndpointGroups availabilityProbes = new AvailabilityProbesHealthEndpointGroups(this.delegate); + assertThat(availabilityProbes.getNames()).containsExactly("test", "liveness", "readiness"); + } + + @Test + void getWhenProbeInDelegateReturnsGroupFromDelegate() { + given(this.delegate.get("liveness")).willReturn(this.group); + HealthEndpointGroups availabilityProbes = new AvailabilityProbesHealthEndpointGroups(this.delegate); + assertThat(availabilityProbes.get("liveness")).isEqualTo(this.group); + } + + @Test + void getWhenProbeNotInDelegateReturnsProbeGroup() { + HealthEndpointGroups availabilityProbes = new AvailabilityProbesHealthEndpointGroups(this.delegate); + assertThat(availabilityProbes.get("liveness")).isInstanceOf(AvailabilityProbesHealthEndpointGroup.class); + } + + @Test + void getWhenNotProbeAndNotInDelegateReturnsNull() { + HealthEndpointGroups availabilityProbes = new AvailabilityProbesHealthEndpointGroups(this.delegate); + assertThat(availabilityProbes.get("mygroup")).isNull(); + } + + @Test + void getLivenessProbeHasOnlyLivenessStateAsMember() { + HealthEndpointGroups availabilityProbes = new AvailabilityProbesHealthEndpointGroups(this.delegate); + HealthEndpointGroup probeGroup = availabilityProbes.get("liveness"); + assertThat(probeGroup.isMember("livenessState")).isTrue(); + assertThat(probeGroup.isMember("readinessState")).isFalse(); + } + + @Test + void getReadinessProbeHasOnlyReadinessStateAsMember() { + HealthEndpointGroups availabilityProbes = new AvailabilityProbesHealthEndpointGroups(this.delegate); + HealthEndpointGroup probeGroup = availabilityProbes.get("readiness"); + assertThat(probeGroup.isMember("livenessState")).isFalse(); + assertThat(probeGroup.isMember("readinessState")).isTrue(); + } + + @Test + void containsAllWhenContainsAllReturnTrue() { + given(this.delegate.getNames()).willReturn(new LinkedHashSet<>(Arrays.asList("test", "liveness", "readiness"))); + assertThat(AvailabilityProbesHealthEndpointGroups.containsAllProbeGroups(this.delegate)).isTrue(); + } + + @Test + void containsAllWhenContainsOneReturnFalse() { + given(this.delegate.getNames()).willReturn(new LinkedHashSet<>(Arrays.asList("test", "liveness"))); + assertThat(AvailabilityProbesHealthEndpointGroups.containsAllProbeGroups(this.delegate)).isFalse(); + } + + @Test + void containsAllWhenContainsNoneReturnFalse() { + given(this.delegate.getNames()).willReturn(new LinkedHashSet<>(Arrays.asList("test", "spring"))); + assertThat(AvailabilityProbesHealthEndpointGroups.containsAllProbeGroups(this.delegate)).isFalse(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cassandra/CassandraHealthContributorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cassandra/CassandraHealthContributorAutoConfigurationTests.java index 65baa9377b66..3e6e09d25ec3 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cassandra/CassandraHealthContributorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cassandra/CassandraHealthContributorAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,15 +16,14 @@ package org.springframework.boot.actuate.autoconfigure.cassandra; +import com.datastax.oss.driver.api.core.CqlSession; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration; -import org.springframework.boot.actuate.cassandra.CassandraHealthIndicator; +import org.springframework.boot.actuate.cassandra.CassandraDriverHealthIndicator; import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; import org.springframework.data.cassandra.core.CassandraOperations; import static org.assertj.core.api.Assertions.assertThat; @@ -34,33 +33,61 @@ * Tests for {@link CassandraHealthContributorAutoConfiguration}. * * @author Phillip Webb + * @author Stephane Nicoll */ +@SuppressWarnings("deprecation") class CassandraHealthContributorAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(CassandraConfiguration.class, - CassandraHealthContributorAutoConfiguration.class, HealthContributorAutoConfiguration.class)); + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(CassandraHealthContributorAutoConfiguration.class, + HealthContributorAutoConfiguration.class)); @Test - void runShouldCreateIndicator() { - this.contextRunner.run((context) -> assertThat(context).hasSingleBean(CassandraHealthIndicator.class)); + void runWithoutCqlSessionOrCassandraOperationsShouldNotCreateIndicator() { + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean("cassandraHealthContributor") + .doesNotHaveBean(org.springframework.boot.actuate.cassandra.CassandraHealthIndicator.class) + .doesNotHaveBean(CassandraDriverHealthIndicator.class)); } @Test - void runWhenDisabledShouldNotCreateIndicator() { - this.contextRunner.withPropertyValues("management.health.cassandra.enabled:false") - .run((context) -> assertThat(context).doesNotHaveBean(CassandraHealthIndicator.class)); + void runWithCqlSessionOnlyShouldCreateDriverIndicator() { + this.contextRunner.withBean(CqlSession.class, () -> mock(CqlSession.class)) + .run((context) -> assertThat(context).hasSingleBean(CassandraDriverHealthIndicator.class) + .doesNotHaveBean(org.springframework.boot.actuate.cassandra.CassandraHealthIndicator.class)); } - @Configuration(proxyBeanMethods = false) - @AutoConfigureBefore(CassandraHealthContributorAutoConfiguration.class) - static class CassandraConfiguration { + @Test + void runWithCassandraOperationsOnlyShouldCreateRegularIndicator() { + this.contextRunner.withBean(CassandraOperations.class, () -> mock(CassandraOperations.class)) + .run((context) -> assertThat(context) + .hasSingleBean(org.springframework.boot.actuate.cassandra.CassandraHealthIndicator.class) + .doesNotHaveBean(CassandraDriverHealthIndicator.class)); + } + + @Test + void runWithCqlSessionAndCassandraOperationsShouldCreateDriverIndicator() { + this.contextRunner.withBean(CqlSession.class, () -> mock(CqlSession.class)) + .withBean(CassandraOperations.class, () -> mock(CassandraOperations.class)) + .run((context) -> assertThat(context).hasSingleBean(CassandraDriverHealthIndicator.class) + .doesNotHaveBean(org.springframework.boot.actuate.cassandra.CassandraHealthIndicator.class)); + } - @Bean - CassandraOperations cassandraOperations() { - return mock(CassandraOperations.class); - } + @Test + void runWithCqlSessionAndSpringDataAbsentShouldCreateDriverIndicator() { + this.contextRunner.withBean(CqlSession.class, () -> mock(CqlSession.class)) + .withClassLoader(new FilteredClassLoader("org.springframework.data")) + .run((context) -> assertThat(context).hasSingleBean(CassandraDriverHealthIndicator.class) + .doesNotHaveBean(org.springframework.boot.actuate.cassandra.CassandraHealthIndicator.class)); + } + @Test + void runWhenDisabledShouldNotCreateIndicator() { + this.contextRunner.withBean(CqlSession.class, () -> mock(CqlSession.class)) + .withBean(CassandraOperations.class, () -> mock(CassandraOperations.class)) + .withPropertyValues("management.health.cassandra.enabled:false") + .run((context) -> assertThat(context).doesNotHaveBean("cassandraHealthContributor") + .doesNotHaveBean(org.springframework.boot.actuate.cassandra.CassandraHealthIndicator.class) + .doesNotHaveBean(CassandraDriverHealthIndicator.class)); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cassandra/CassandraReactiveHealthContributorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cassandra/CassandraReactiveHealthContributorAutoConfigurationTests.java index 94d5495135f6..21bb7d94d9d3 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cassandra/CassandraReactiveHealthContributorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cassandra/CassandraReactiveHealthContributorAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,16 @@ package org.springframework.boot.actuate.autoconfigure.cassandra; +import com.datastax.oss.driver.api.core.CqlSession; import org.junit.jupiter.api.Test; -import org.springframework.boot.actuate.autoconfigure.cassandra.CassandraHealthContributorAutoConfigurationTests.CassandraConfiguration; import org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration; -import org.springframework.boot.actuate.cassandra.CassandraHealthIndicator; -import org.springframework.boot.actuate.cassandra.CassandraReactiveHealthIndicator; +import org.springframework.boot.actuate.cassandra.CassandraDriverHealthIndicator; +import org.springframework.boot.actuate.cassandra.CassandraDriverReactiveHealthIndicator; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.data.cassandra.core.CassandraOperations; import org.springframework.data.cassandra.core.ReactiveCassandraOperations; import static org.assertj.core.api.Assertions.assertThat; @@ -35,33 +37,69 @@ * @author Artsiom Yudovin * @author Stephane Nicoll */ +@SuppressWarnings("deprecation") class CassandraReactiveHealthContributorAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withBean(ReactiveCassandraOperations.class, () -> mock(ReactiveCassandraOperations.class)) + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(CassandraReactiveHealthContributorAutoConfiguration.class, - HealthContributorAutoConfiguration.class)); + CassandraHealthContributorAutoConfiguration.class, HealthContributorAutoConfiguration.class)); @Test - void runShouldCreateIndicator() { - this.contextRunner.run((context) -> assertThat(context).hasSingleBean(CassandraReactiveHealthIndicator.class) - .hasBean("cassandraHealthContributor")); + void runWithoutCqlSessionOrReactiveCassandraOperationsShouldNotCreateIndicator() { + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean("cassandraHealthContributor") + .doesNotHaveBean(org.springframework.boot.actuate.cassandra.CassandraReactiveHealthIndicator.class) + .doesNotHaveBean(CassandraDriverReactiveHealthIndicator.class)); } @Test - void runWithRegularIndicatorShouldOnlyCreateReactiveIndicator() { - this.contextRunner - .withConfiguration(AutoConfigurations.of(CassandraConfiguration.class, - CassandraHealthContributorAutoConfiguration.class)) - .run((context) -> assertThat(context).hasSingleBean(CassandraReactiveHealthIndicator.class) - .hasBean("cassandraHealthContributor").doesNotHaveBean(CassandraHealthIndicator.class)); + void runWithCqlSessionOnlyShouldCreateDriverIndicator() { + this.contextRunner.withBean(CqlSession.class, () -> mock(CqlSession.class)) + .run((context) -> assertThat(context).hasBean("cassandraHealthContributor") + .hasSingleBean(CassandraDriverReactiveHealthIndicator.class).doesNotHaveBean( + org.springframework.boot.actuate.cassandra.CassandraReactiveHealthIndicator.class)); + } + + @Test + void runWithReactiveCassandraOperationsOnlyShouldCreateReactiveIndicator() { + this.contextRunner.withBean(ReactiveCassandraOperations.class, () -> mock(ReactiveCassandraOperations.class)) + .withBean(CassandraOperations.class, () -> mock(CassandraOperations.class)) + .run((context) -> assertThat(context).hasBean("cassandraHealthContributor") + .hasSingleBean( + org.springframework.boot.actuate.cassandra.CassandraReactiveHealthIndicator.class) + .doesNotHaveBean(CassandraDriverReactiveHealthIndicator.class) + .doesNotHaveBean(org.springframework.boot.actuate.cassandra.CassandraHealthIndicator.class) + .doesNotHaveBean(CassandraDriverHealthIndicator.class)); + } + + @Test + void runWithCqlSessionAndReactiveCassandraOperationsShouldCreateDriverIndicator() { + this.contextRunner.withBean(CqlSession.class, () -> mock(CqlSession.class)) + .withBean(ReactiveCassandraOperations.class, () -> mock(ReactiveCassandraOperations.class)) + .withBean(CassandraOperations.class, () -> mock(CassandraOperations.class)) + .run((context) -> assertThat(context).hasBean("cassandraHealthContributor") + .hasSingleBean(CassandraDriverReactiveHealthIndicator.class) + .doesNotHaveBean( + org.springframework.boot.actuate.cassandra.CassandraReactiveHealthIndicator.class) + .doesNotHaveBean(org.springframework.boot.actuate.cassandra.CassandraHealthIndicator.class) + .doesNotHaveBean(CassandraDriverHealthIndicator.class)); + } + + @Test + void runWithCqlSessionAndSpringDataAbsentShouldCreateDriverIndicator() { + this.contextRunner.withBean(CqlSession.class, () -> mock(CqlSession.class)) + .withClassLoader(new FilteredClassLoader("org.springframework.data")) + .run((context) -> assertThat(context).hasBean("cassandraHealthContributor") + .hasSingleBean(CassandraDriverReactiveHealthIndicator.class).doesNotHaveBean( + org.springframework.boot.actuate.cassandra.CassandraReactiveHealthIndicator.class)); } @Test void runWhenDisabledShouldNotCreateIndicator() { - this.contextRunner.withPropertyValues("management.health.cassandra.enabled:false") - .run((context) -> assertThat(context).doesNotHaveBean(CassandraReactiveHealthIndicator.class) - .doesNotHaveBean("cassandraHealthContributor")); + this.contextRunner.withBean(CqlSession.class, () -> mock(CqlSession.class)) + .withBean(ReactiveCassandraOperations.class, () -> mock(ReactiveCassandraOperations.class)) + .withPropertyValues("management.health.cassandra.enabled:false") + .run((context) -> assertThat(context).doesNotHaveBean("cassandraHealthContributor").doesNotHaveBean( + org.springframework.boot.actuate.cassandra.CassandraReactiveHealthIndicator.class)); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscovererTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscovererTests.java index bc487df92857..84b9f4c62bf6 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscovererTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscovererTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -79,7 +79,7 @@ private WebOperation findMainReadOperation(ExposableWebEndpoint endpoint) { } private void load(Class configuration, Consumer consumer) { - this.load((id) -> null, EndpointId::toString, configuration, consumer); + load((id) -> null, EndpointId::toString, configuration, consumer); } private void load(Function timeToLive, PathMapper endpointPathMapper, Class configuration, diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryReactiveHealthEndpointWebExtensionTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryReactiveHealthEndpointWebExtensionTests.java index 1cd7882a08e1..79e977a7aa5a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryReactiveHealthEndpointWebExtensionTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryReactiveHealthEndpointWebExtensionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ import org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration; -import org.springframework.boot.actuate.endpoint.http.ApiVersion; +import org.springframework.boot.actuate.endpoint.ApiVersion; import org.springframework.boot.actuate.health.CompositeHealth; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthComponent; @@ -49,7 +49,7 @@ */ class CloudFoundryReactiveHealthEndpointWebExtensionTests { - private ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() + private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() .withPropertyValues("VCAP_APPLICATION={}") .withConfiguration(AutoConfigurations.of(ReactiveSecurityAutoConfiguration.class, ReactiveUserDetailsServiceAutoConfiguration.class, WebFluxAutoConfiguration.class, diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointIntegrationTests.java index ceb011459db8..7b1d3ad0338c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryWebFluxEndpointIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.boot.actuate.autoconfigure.cloudfoundry.reactive; +import java.time.Duration; import java.util.Arrays; import java.util.Collections; import java.util.Map; @@ -151,7 +152,8 @@ private ContextConsumer withWebTestClie return (context) -> { int port = ((AnnotationConfigReactiveWebServerApplicationContext) context.getSourceApplicationContext()) .getWebServer().getPort(); - clientConsumer.accept(WebTestClient.bindToServer().baseUrl("http://localhost:" + port).build()); + clientConsumer.accept(WebTestClient.bindToServer().baseUrl("http://localhost:" + port) + .responseTimeout(Duration.ofMinutes(5)).build()); }; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfigurationTests.java index 62fe9f9f3aa4..fadaeb8feeae 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,11 +37,10 @@ import org.springframework.boot.actuate.autoconfigure.info.InfoContributorAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration; +import org.springframework.boot.actuate.endpoint.ApiVersion; import org.springframework.boot.actuate.endpoint.EndpointId; -import org.springframework.boot.actuate.endpoint.ExposableEndpoint; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; -import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint; import org.springframework.boot.actuate.endpoint.web.WebOperation; @@ -81,6 +80,10 @@ */ class ReactiveCloudFoundryActuatorAutoConfigurationTests { + private static final String V2_JSON = ApiVersion.V2.getProducedMimeType().toString(); + + private static final String V3_JSON = ApiVersion.V3.getProducedMimeType().toString(); + private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(ReactiveSecurityAutoConfiguration.class, ReactiveUserDetailsServiceAutoConfiguration.class, WebFluxAutoConfiguration.class, @@ -121,7 +124,7 @@ void cloudfoundryapplicationProducesActuatorMediaType() { "vcap.application.cf_api:https://my-cloud-controller.com").run((context) -> { WebTestClient webTestClient = WebTestClient.bindToApplicationContext(context).build(); webTestClient.get().uri("/cloudfoundryapplication").header("Content-Type", - ActuatorMediaType.V2_JSON + ";charset=UTF-8"); + V2_JSON + ";charset=UTF-8"); }); } @@ -208,7 +211,7 @@ void allEndpointsAvailableUnderCloudFoundryWithoutEnablingWebIncludes() { .run((context) -> { CloudFoundryWebFluxEndpointHandlerMapping handlerMapping = getHandlerMapping(context); Collection endpoints = handlerMapping.getEndpoints(); - List endpointIds = endpoints.stream().map(ExposableEndpoint::getEndpointId) + List endpointIds = endpoints.stream().map(ExposableWebEndpoint::getEndpointId) .collect(Collectors.toList()); assertThat(endpointIds).contains(EndpointId.of("test")); }); @@ -240,8 +243,7 @@ void healthEndpointInvokerShouldBeCloudFoundryWebExtension() { ExposableWebEndpoint endpoint = endpoints.iterator().next(); assertThat(endpoint.getOperations()).hasSize(2); WebOperation webOperation = findOperationWithRequestPath(endpoint, "health"); - Object invoker = ReflectionTestUtils.getField(webOperation, "invoker"); - assertThat(ReflectionTestUtils.getField(invoker, "target")) + assertThat(webOperation).extracting("invoker").extracting("target") .isInstanceOf(CloudFoundryReactiveHealthEndpointWebExtension.class); }); } @@ -271,7 +273,8 @@ void skipSslValidation() { "cloudFoundrySecurityService"); WebClient webClient = (WebClient) ReflectionTestUtils.getField(interceptorSecurityService, "webClient"); - webClient.get().uri("https://self-signed.badssl.com/").exchange().block(Duration.ofSeconds(30)); + webClient.get().uri("https://self-signed.badssl.com/").retrieve().toBodilessEntity() + .block(Duration.ofSeconds(30)); }); } @@ -287,8 +290,9 @@ void sslValidationNotSkippedByDefault() { "cloudFoundrySecurityService"); WebClient webClient = (WebClient) ReflectionTestUtils.getField(interceptorSecurityService, "webClient"); - assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> webClient.get() - .uri("https://self-signed.badssl.com/").exchange().block(Duration.ofSeconds(30))) + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> webClient.get().uri("https://self-signed.badssl.com/").retrieve() + .toBodilessEntity().block(Duration.ofSeconds(30))) .withCauseInstanceOf(SSLException.class); }); } @@ -301,8 +305,7 @@ private CloudFoundryWebFluxEndpointHandlerMapping getHandlerMapping(ApplicationC private WebOperation findOperationWithRequestPath(ExposableWebEndpoint endpoint, String requestPath) { for (WebOperation operation : endpoint.getOperations()) { WebOperationRequestPredicate predicate = operation.getRequestPredicate(); - if (predicate.getPath().equals(requestPath) - && predicate.getProduces().contains(ActuatorMediaType.V3_JSON)) { + if (predicate.getPath().equals(requestPath) && predicate.getProduces().contains(V3_JSON)) { return operation; } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityInterceptorTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityInterceptorTests.java index 645b3bd287b4..5e88fd55cb6d 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityInterceptorTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityInterceptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,8 +18,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -41,6 +42,7 @@ * * @author Madhura Bhave */ +@ExtendWith(MockitoExtension.class) class ReactiveCloudFoundrySecurityInterceptorTests { @Mock @@ -53,7 +55,6 @@ class ReactiveCloudFoundrySecurityInterceptorTests { @BeforeEach void setup() { - MockitoAnnotations.initMocks(this); this.interceptor = new CloudFoundrySecurityInterceptor(this.tokenValidator, this.securityService, "my-app-id"); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityServiceTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityServiceTests.java index 0f437b4d632b..f082733ea32a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityServiceTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityServiceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -204,8 +204,6 @@ void getUaaUrlShouldCallCloudControllerInfoOnlyOnce() throws Exception { }); StepVerifier.create(this.securityService.getUaaUrl()) .consumeNextWith((uaaUrl) -> assertThat(uaaUrl).isEqualTo(UAA_URL)).expectComplete().verify(); - // this.securityService.getUaaUrl().block(); //FIXME subscribe again to check that - // it isn't called again expectRequest((request) -> assertThat(request.getPath()).isEqualTo(CLOUD_CONTROLLER + "/info")); expectRequestCount(1); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveTokenValidatorTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveTokenValidatorTests.java index c541e257583c..b2ca744728f3 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveTokenValidatorTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveTokenValidatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,8 +31,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import reactor.test.publisher.PublisherProbe; @@ -52,6 +53,7 @@ * * @author Madhura Bhave */ +@ExtendWith(MockitoExtension.class) class ReactiveTokenValidatorTests { private static final byte[] DOT = ".".getBytes(); @@ -85,7 +87,6 @@ class ReactiveTokenValidatorTests { @BeforeEach void setup() { - MockitoAnnotations.initMocks(this); VALID_KEYS.put("valid-key", VALID_KEY); INVALID_KEYS.put("invalid-key", INVALID_KEY); this.tokenValidator = new ReactiveTokenValidator(this.securityService); @@ -159,7 +160,6 @@ void validateTokenWhenCacheEmptyAndInvalidKeyShouldThrowException() throws Excep void validateTokenWhenCacheValidShouldNotFetchTokenKeys() throws Exception { PublisherProbe> fetchTokenKeys = PublisherProbe.empty(); ReflectionTestUtils.setField(this.tokenValidator, "cachedTokenKeys", VALID_KEYS); - given(this.securityService.fetchTokenKeys()).willReturn(fetchTokenKeys.mono()); given(this.securityService.getUaaUrl()).willReturn(Mono.just("http://localhost:8080/uaa")); String header = "{\"alg\": \"RS256\", \"kid\": \"valid-key\",\"typ\": \"JWT\"}"; String claims = "{\"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"actuator.read\"]}"; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfigurationTests.java index d895df495c7a..2bc35b8b0618 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,10 +27,10 @@ import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration; +import org.springframework.boot.actuate.endpoint.ApiVersion; import org.springframework.boot.actuate.endpoint.EndpointId; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; -import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint; import org.springframework.boot.actuate.endpoint.web.WebOperation; @@ -67,6 +67,8 @@ */ class CloudFoundryActuatorAutoConfigurationTests { + private static final String V3_JSON = ApiVersion.V3.getProducedMimeType().toString(); + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(SecurityAutoConfiguration.class, WebMvcAutoConfiguration.class, JacksonAutoConfiguration.class, DispatcherServletAutoConfiguration.class, @@ -94,12 +96,12 @@ void cloudFoundryPlatformActive() { } @Test - void cloudfoundryapplicationProducesActuatorMediaType() throws Exception { + void cloudfoundryapplicationProducesActuatorMediaType() { this.contextRunner.withPropertyValues("VCAP_APPLICATION:---", "vcap.application.application_id:my-app-id", "vcap.application.cf_api:https://my-cloud-controller.com").run((context) -> { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context).build(); mockMvc.perform(get("/cloudfoundryapplication")) - .andExpect(header().string("Content-Type", ActuatorMediaType.V3_JSON)); + .andExpect(header().string("Content-Type", V3_JSON)); }); } @@ -231,8 +233,7 @@ void healthEndpointInvokerShouldBeCloudFoundryWebExtension() { ExposableWebEndpoint endpoint = endpoints.iterator().next(); assertThat(endpoint.getOperations()).hasSize(2); WebOperation webOperation = findOperationWithRequestPath(endpoint, "health"); - Object invoker = ReflectionTestUtils.getField(webOperation, "invoker"); - assertThat(ReflectionTestUtils.getField(invoker, "target")) + assertThat(webOperation).extracting("invoker").extracting("target") .isInstanceOf(CloudFoundryHealthEndpointWebExtension.class); }); } @@ -245,8 +246,7 @@ private CloudFoundryWebEndpointServletHandlerMapping getHandlerMapping(Applicati private WebOperation findOperationWithRequestPath(ExposableWebEndpoint endpoint, String requestPath) { for (WebOperation operation : endpoint.getOperations()) { WebOperationRequestPredicate predicate = operation.getRequestPredicate(); - if (predicate.getPath().equals(requestPath) - && predicate.getProduces().contains(ActuatorMediaType.V3_JSON)) { + if (predicate.getPath().equals(requestPath) && predicate.getProduces().contains(V3_JSON)) { return operation; } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryHealthEndpointWebExtensionTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryHealthEndpointWebExtensionTests.java index 7a940aebfca5..024c7b107f38 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryHealthEndpointWebExtensionTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryHealthEndpointWebExtensionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration; -import org.springframework.boot.actuate.endpoint.http.ApiVersion; +import org.springframework.boot.actuate.endpoint.ApiVersion; import org.springframework.boot.actuate.health.CompositeHealth; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthComponent; @@ -48,7 +48,7 @@ */ class CloudFoundryHealthEndpointWebExtensionTests { - private WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() .withPropertyValues("VCAP_APPLICATION={}") .withConfiguration(AutoConfigurations.of(SecurityAutoConfiguration.class, WebMvcAutoConfiguration.class, JacksonAutoConfiguration.class, DispatcherServletAutoConfiguration.class, diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryInfoEndpointWebExtensionTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryInfoEndpointWebExtensionTests.java index 18d717f48726..042e7988c6b1 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryInfoEndpointWebExtensionTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryInfoEndpointWebExtensionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.actuate.autoconfigure.cloudfoundry.servlet; import java.util.Map; @@ -31,12 +32,10 @@ import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; -import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; -import org.springframework.boot.logging.LogLevel; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import static org.assertj.core.api.Assertions.assertThat; @@ -48,7 +47,7 @@ */ class CloudFoundryInfoEndpointWebExtensionTests { - private WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() .withPropertyValues("VCAP_APPLICATION={}") .withConfiguration(AutoConfigurations.of(SecurityAutoConfiguration.class, WebMvcAutoConfiguration.class, JacksonAutoConfiguration.class, DispatcherServletAutoConfiguration.class, @@ -62,14 +61,13 @@ class CloudFoundryInfoEndpointWebExtensionTests { @Test @SuppressWarnings("unchecked") void gitFullDetailsAlwaysPresent() { - this.contextRunner.withInitializer(new ConditionEvaluationReportLoggingListener(LogLevel.INFO)) - .run((context) -> { - CloudFoundryInfoEndpointWebExtension extension = context - .getBean(CloudFoundryInfoEndpointWebExtension.class); - Map git = (Map) extension.info().get("git"); - Map commit = (Map) git.get("commit"); - assertThat(commit).hasSize(4); - }); + this.contextRunner.run((context) -> { + CloudFoundryInfoEndpointWebExtension extension = context + .getBean(CloudFoundryInfoEndpointWebExtension.class); + Map git = (Map) extension.info().get("git"); + Map commit = (Map) git.get("commit"); + assertThat(commit).hasSize(4); + }); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryMvcWebEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryMvcWebEndpointIntegrationTests.java index 8af163751080..a7affad6a078 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryMvcWebEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryMvcWebEndpointIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.boot.actuate.autoconfigure.cloudfoundry.servlet; +import java.time.Duration; import java.util.Arrays; import java.util.Collections; import java.util.Map; @@ -146,8 +147,8 @@ private void load(Class configuration, Consumer clientConsumer BiConsumer consumer = (context, client) -> clientConsumer.accept(client); try (AnnotationConfigServletWebServerApplicationContext context = createApplicationContext(configuration, CloudFoundryMvcConfiguration.class)) { - consumer.accept(context, - WebTestClient.bindToServer().baseUrl("http://localhost:" + getPort(context)).build()); + consumer.accept(context, WebTestClient.bindToServer().baseUrl("http://localhost:" + getPort(context)) + .responseTimeout(Duration.ofMinutes(5)).build()); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundrySecurityInterceptorTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundrySecurityInterceptorTests.java index 8ccaf56103ba..b1b3e4e981d7 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundrySecurityInterceptorTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundrySecurityInterceptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,9 +18,10 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.boot.actuate.autoconfigure.cloudfoundry.AccessLevel; import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason; @@ -34,13 +35,14 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.verify; +import static org.mockito.BDDMockito.then; /** * Tests for {@link CloudFoundrySecurityInterceptor}. * * @author Madhura Bhave */ +@ExtendWith(MockitoExtension.class) class CloudFoundrySecurityInterceptorTests { @Mock @@ -55,7 +57,6 @@ class CloudFoundrySecurityInterceptorTests { @BeforeEach void setup() { - MockitoAnnotations.initMocks(this); this.interceptor = new CloudFoundrySecurityInterceptor(this.tokenValidator, this.securityService, "my-app-id"); this.request = new MockHttpServletRequest(); } @@ -114,7 +115,7 @@ void preHandleSuccessfulWithFullAccess() { given(this.securityService.getAccessLevel(accessToken, "my-app-id")).willReturn(AccessLevel.FULL); SecurityResponse response = this.interceptor.preHandle(this.request, EndpointId.of("test")); ArgumentCaptor tokenArgumentCaptor = ArgumentCaptor.forClass(Token.class); - verify(this.tokenValidator).validate(tokenArgumentCaptor.capture()); + then(this.tokenValidator).should().validate(tokenArgumentCaptor.capture()); Token token = tokenArgumentCaptor.getValue(); assertThat(token.toString()).isEqualTo(accessToken); assertThat(response.getStatus()).isEqualTo(HttpStatus.OK); @@ -128,7 +129,7 @@ void preHandleSuccessfulWithRestrictedAccess() { given(this.securityService.getAccessLevel(accessToken, "my-app-id")).willReturn(AccessLevel.RESTRICTED); SecurityResponse response = this.interceptor.preHandle(this.request, EndpointId.of("info")); ArgumentCaptor tokenArgumentCaptor = ArgumentCaptor.forClass(Token.class); - verify(this.tokenValidator).validate(tokenArgumentCaptor.capture()); + then(this.tokenValidator).should().validate(tokenArgumentCaptor.capture()); Token token = tokenArgumentCaptor.getValue(); assertThat(token.toString()).isEqualTo(accessToken); assertThat(response.getStatus()).isEqualTo(HttpStatus.OK); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundrySecurityServiceTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundrySecurityServiceTests.java index 70d2020ea03b..de4953e21650 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundrySecurityServiceTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundrySecurityServiceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -76,7 +76,7 @@ void skipSslValidationWhenTrue() { } @Test - void doNotskipSslValidationWhenFalse() { + void doNotSkipSslValidationWhenFalse() { RestTemplateBuilder builder = new RestTemplateBuilder(); this.securityService = new CloudFoundrySecurityService(builder, CLOUD_CONTROLLER, false); RestTemplate restTemplate = (RestTemplate) ReflectionTestUtils.getField(this.securityService, "restTemplate"); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/TokenValidatorTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/TokenValidatorTests.java index a707f2ee7a56..cf469ce8977e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/TokenValidatorTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/TokenValidatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,9 +31,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException; import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryAuthorizationException.Reason; @@ -45,13 +45,15 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.verify; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.never; /** * Tests for {@link TokenValidator}. * * @author Madhura Bhave */ +@ExtendWith(MockitoExtension.class) class TokenValidatorTests { private static final byte[] DOT = ".".getBytes(); @@ -85,12 +87,11 @@ class TokenValidatorTests { @BeforeEach void setup() { - MockitoAnnotations.initMocks(this); this.tokenValidator = new TokenValidator(this.securityService); } @Test - void validateTokenWhenKidValidationFailsTwiceShouldThrowException() throws Exception { + void validateTokenWhenKidValidationFailsTwiceShouldThrowException() { ReflectionTestUtils.setField(this.tokenValidator, "tokenKeys", INVALID_KEYS); given(this.securityService.fetchTokenKeys()).willReturn(INVALID_KEYS); String header = "{\"alg\": \"RS256\", \"kid\": \"valid-key\",\"typ\": \"JWT\"}"; @@ -108,7 +109,7 @@ void validateTokenWhenKidValidationSucceedsInTheSecondAttempt() throws Exception String header = "{ \"alg\": \"RS256\", \"kid\": \"valid-key\",\"typ\": \"JWT\"}"; String claims = "{ \"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"actuator.read\"]}"; this.tokenValidator.validate(new Token(getSignedToken(header.getBytes(), claims.getBytes()))); - verify(this.securityService).fetchTokenKeys(); + then(this.securityService).should().fetchTokenKeys(); } @Test @@ -118,7 +119,7 @@ void validateTokenShouldFetchTokenKeysIfNull() throws Exception { String header = "{ \"alg\": \"RS256\", \"kid\": \"valid-key\",\"typ\": \"JWT\"}"; String claims = "{ \"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"actuator.read\"]}"; this.tokenValidator.validate(new Token(getSignedToken(header.getBytes(), claims.getBytes()))); - verify(this.securityService).fetchTokenKeys(); + then(this.securityService).should().fetchTokenKeys(); } @Test @@ -128,14 +129,13 @@ void validateTokenWhenValidShouldNotFetchTokenKeys() throws Exception { String header = "{ \"alg\": \"RS256\", \"kid\": \"valid-key\",\"typ\": \"JWT\"}"; String claims = "{ \"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"actuator.read\"]}"; this.tokenValidator.validate(new Token(getSignedToken(header.getBytes(), claims.getBytes()))); - verify(this.securityService, Mockito.never()).fetchTokenKeys(); + then(this.securityService).should(never()).fetchTokenKeys(); } @Test - void validateTokenWhenSignatureInvalidShouldThrowException() throws Exception { + void validateTokenWhenSignatureInvalidShouldThrowException() { ReflectionTestUtils.setField(this.tokenValidator, "tokenKeys", Collections.singletonMap("valid-key", INVALID_KEY)); - given(this.securityService.getUaaUrl()).willReturn("http://localhost:8080/uaa"); String header = "{ \"alg\": \"RS256\", \"kid\": \"valid-key\",\"typ\": \"JWT\"}"; String claims = "{ \"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"actuator.read\"]}"; assertThatExceptionOfType(CloudFoundryAuthorizationException.class).isThrownBy( @@ -144,8 +144,7 @@ void validateTokenWhenSignatureInvalidShouldThrowException() throws Exception { } @Test - void validateTokenWhenTokenAlgorithmIsNotRS256ShouldThrowException() throws Exception { - given(this.securityService.fetchTokenKeys()).willReturn(VALID_KEYS); + void validateTokenWhenTokenAlgorithmIsNotRS256ShouldThrowException() { String header = "{ \"alg\": \"HS256\", \"typ\": \"JWT\"}"; String claims = "{ \"exp\": 2147483647, \"iss\": \"http://localhost:8080/uaa/oauth/token\", \"scope\": [\"actuator.read\"]}"; assertThatExceptionOfType(CloudFoundryAuthorizationException.class).isThrownBy( @@ -154,7 +153,7 @@ void validateTokenWhenTokenAlgorithmIsNotRS256ShouldThrowException() throws Exce } @Test - void validateTokenWhenExpiredShouldThrowException() throws Exception { + void validateTokenWhenExpiredShouldThrowException() { given(this.securityService.fetchTokenKeys()).willReturn(VALID_KEYS); given(this.securityService.fetchTokenKeys()).willReturn(VALID_KEYS); String header = "{ \"alg\": \"RS256\", \"kid\": \"valid-key\", \"typ\": \"JWT\"}"; @@ -165,7 +164,7 @@ void validateTokenWhenExpiredShouldThrowException() throws Exception { } @Test - void validateTokenWhenIssuerIsNotValidShouldThrowException() throws Exception { + void validateTokenWhenIssuerIsNotValidShouldThrowException() { given(this.securityService.fetchTokenKeys()).willReturn(VALID_KEYS); given(this.securityService.getUaaUrl()).willReturn("https://other-uaa.com"); String header = "{ \"alg\": \"RS256\", \"kid\": \"valid-key\", \"typ\": \"JWT\", \"scope\": [\"actuator.read\"]}"; @@ -176,7 +175,7 @@ void validateTokenWhenIssuerIsNotValidShouldThrowException() throws Exception { } @Test - void validateTokenWhenAudienceIsNotValidShouldThrowException() throws Exception { + void validateTokenWhenAudienceIsNotValidShouldThrowException() { given(this.securityService.fetchTokenKeys()).willReturn(VALID_KEYS); given(this.securityService.getUaaUrl()).willReturn("http://localhost:8080/uaa"); String header = "{ \"alg\": \"RS256\", \"kid\": \"valid-key\", \"typ\": \"JWT\"}"; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointAutoConfigurationTests.java index f9bc7bf6089c..731a6292a9d6 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -64,6 +64,14 @@ void keysToSanitizeCanBeConfiguredViaTheEnvironment() { .run(validateTestProperties("******", "******")); } + @Test + void additionalKeysToSanitizeCanBeConfiguredViaTheEnvironment() { + this.contextRunner.withUserConfiguration(Config.class) + .withPropertyValues("management.endpoint.configprops.additional-keys-to-sanitize: property") + .withPropertyValues("management.endpoints.web.exposure.include=configprops") + .run(validateTestProperties("******", "******")); + } + @Test void runWhenNotExposedShouldNotHaveEndpointBean() { this.contextRunner diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/couchbase/CouchbaseHealthContributorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/couchbase/CouchbaseHealthContributorAutoConfigurationTests.java index 41c02fabed66..4d8860877efd 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/couchbase/CouchbaseHealthContributorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/couchbase/CouchbaseHealthContributorAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.actuate.autoconfigure.couchbase; import com.couchbase.client.java.Cluster; @@ -35,7 +36,7 @@ */ class CouchbaseHealthContributorAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withBean(Cluster.class, () -> mock(Cluster.class)).withConfiguration(AutoConfigurations .of(CouchbaseHealthContributorAutoConfiguration.class, HealthContributorAutoConfiguration.class)); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/couchbase/CouchbaseReactiveHealthContributorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/couchbase/CouchbaseReactiveHealthContributorAutoConfigurationTests.java index b3cbe62b1968..d4474a9bd6f4 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/couchbase/CouchbaseReactiveHealthContributorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/couchbase/CouchbaseReactiveHealthContributorAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,7 @@ */ class CouchbaseReactiveHealthContributorAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withBean(Cluster.class, () -> mock(Cluster.class)) .withConfiguration(AutoConfigurations.of(CouchbaseReactiveHealthContributorAutoConfiguration.class, HealthContributorAutoConfiguration.class)); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticsearchHealthContributorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticsearchHealthContributorAutoConfigurationTests.java deleted file mode 100644 index d7b7176c9a13..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticsearchHealthContributorAutoConfigurationTests.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.autoconfigure.elasticsearch; - -import io.searchbox.client.JestClient; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration; -import org.springframework.boot.actuate.elasticsearch.ElasticsearchHealthIndicator; -import org.springframework.boot.actuate.elasticsearch.ElasticsearchJestHealthIndicator; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link ElasticSearchClientHealthContributorAutoConfiguration} and - * {@link ElasticSearchJestHealthContributorAutoConfiguration}. - * - * @author Phillip Webb - */ -@Deprecated -class ElasticsearchHealthContributorAutoConfigurationTests { - - private ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration(AutoConfigurations - .of(ElasticsearchAutoConfiguration.class, ElasticSearchClientHealthContributorAutoConfiguration.class, - ElasticSearchJestHealthContributorAutoConfiguration.class, - HealthContributorAutoConfiguration.class)); - - @Test - void runShouldCreateIndicator() { - this.contextRunner.withPropertyValues("spring.data.elasticsearch.cluster-nodes:localhost:0") - .withSystemProperties("es.set.netty.runtime.available.processors=false") - .run((context) -> assertThat(context).hasSingleBean(ElasticsearchHealthIndicator.class) - .doesNotHaveBean(ElasticsearchJestHealthIndicator.class)); - } - - @Test - void runWhenUsingJestClientShouldCreateIndicator() { - this.contextRunner.withBean(JestClient.class, () -> mock(JestClient.class)) - .withSystemProperties("es.set.netty.runtime.available.processors=false") - .run((context) -> assertThat(context).hasSingleBean(ElasticsearchJestHealthIndicator.class) - .doesNotHaveBean(ElasticsearchHealthIndicator.class)); - } - - @Test - void runWhenDisabledShouldNotCreateIndicator() { - this.contextRunner.withPropertyValues("management.health.elasticsearch.enabled:false") - .run((context) -> assertThat(context).doesNotHaveBean(ElasticsearchHealthIndicator.class) - .doesNotHaveBean(ElasticsearchJestHealthIndicator.class)); - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticsearchReactiveHealthContributorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticsearchReactiveHealthContributorAutoConfigurationTests.java new file mode 100644 index 000000000000..c14fc96e4f62 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/elasticsearch/ElasticsearchReactiveHealthContributorAutoConfigurationTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.elasticsearch; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration; +import org.springframework.boot.actuate.elasticsearch.ElasticsearchReactiveHealthIndicator; +import org.springframework.boot.actuate.elasticsearch.ElasticsearchRestHealthIndicator; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration; +import org.springframework.boot.autoconfigure.data.elasticsearch.ReactiveElasticsearchRestClientAutoConfiguration; +import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ElasticSearchReactiveHealthContributorAutoConfiguration}. + * + * @author Aleksander Lech + */ +class ElasticsearchReactiveHealthContributorAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ElasticsearchDataAutoConfiguration.class, + ReactiveElasticsearchRestClientAutoConfiguration.class, + ElasticsearchRestClientAutoConfiguration.class, + ElasticSearchReactiveHealthContributorAutoConfiguration.class, + HealthContributorAutoConfiguration.class)); + + @Test + void runShouldCreateIndicator() { + this.contextRunner.run((context) -> assertThat(context) + .hasSingleBean(ElasticsearchReactiveHealthIndicator.class).hasBean("elasticsearchHealthContributor")); + } + + @Test + void runWithRegularIndicatorShouldOnlyCreateReactiveIndicator() { + this.contextRunner + .withConfiguration(AutoConfigurations.of(ElasticSearchRestHealthContributorAutoConfiguration.class)) + .run((context) -> assertThat(context).hasSingleBean(ElasticsearchReactiveHealthIndicator.class) + .hasBean("elasticsearchHealthContributor") + .doesNotHaveBean(ElasticsearchRestHealthIndicator.class)); + } + + @Test + void runWhenDisabledShouldNotCreateIndicator() { + this.contextRunner.withPropertyValues("management.health.elasticsearch.enabled:false") + .run((context) -> assertThat(context).doesNotHaveBean(ElasticsearchReactiveHealthIndicator.class) + .doesNotHaveBean("elasticsearchHealthContributor")); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/ExposeExcludePropertyEndpointFilterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/ExposeExcludePropertyEndpointFilterTests.java deleted file mode 100644 index 8fd9d70b09ee..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/ExposeExcludePropertyEndpointFilterTests.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.autoconfigure.endpoint; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.MockitoAnnotations; - -import org.springframework.boot.actuate.endpoint.EndpointFilter; -import org.springframework.boot.actuate.endpoint.EndpointId; -import org.springframework.boot.actuate.endpoint.ExposableEndpoint; -import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint; -import org.springframework.mock.env.MockEnvironment; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link ExposeExcludePropertyEndpointFilter}. - * - * @author Phillip Webb - */ -class ExposeExcludePropertyEndpointFilterTests { - - private ExposeExcludePropertyEndpointFilter filter; - - @BeforeEach - void setup() { - MockitoAnnotations.initMocks(this); - } - - @Test - void createWhenEndpointTypeIsNullShouldThrowException() { - assertThatIllegalArgumentException() - .isThrownBy(() -> new ExposeExcludePropertyEndpointFilter<>(null, new MockEnvironment(), "foo")) - .withMessageContaining("EndpointType must not be null"); - } - - @Test - void createWhenEnvironmentIsNullShouldThrowException() { - assertThatIllegalArgumentException() - .isThrownBy(() -> new ExposeExcludePropertyEndpointFilter<>(ExposableEndpoint.class, null, "foo")) - .withMessageContaining("Environment must not be null"); - } - - @Test - void createWhenPrefixIsNullShouldThrowException() { - assertThatIllegalArgumentException().isThrownBy( - () -> new ExposeExcludePropertyEndpointFilter<>(ExposableEndpoint.class, new MockEnvironment(), null)) - .withMessageContaining("Prefix must not be empty"); - } - - @Test - void createWhenPrefixIsEmptyShouldThrowException() { - assertThatIllegalArgumentException().isThrownBy( - () -> new ExposeExcludePropertyEndpointFilter<>(ExposableEndpoint.class, new MockEnvironment(), "")) - .withMessageContaining("Prefix must not be empty"); - } - - @Test - void matchWhenExposeIsEmptyAndExcludeIsEmptyAndInDefaultShouldMatch() { - setupFilter("", ""); - assertThat(match(EndpointId.of("def"))).isTrue(); - } - - @Test - void matchWhenExposeIsEmptyAndExcludeIsEmptyAndNotInDefaultShouldNotMatch() { - setupFilter("", ""); - assertThat(match(EndpointId.of("bar"))).isFalse(); - } - - @Test - void matchWhenExposeMatchesAndExcludeIsEmptyShouldMatch() { - setupFilter("bar", ""); - assertThat(match(EndpointId.of("bar"))).isTrue(); - } - - @Test - void matchWhenExposeDoesNotMatchAndExcludeIsEmptyShouldNotMatch() { - setupFilter("bar", ""); - assertThat(match(EndpointId.of("baz"))).isFalse(); - } - - @Test - void matchWhenExposeMatchesAndExcludeMatchesShouldNotMatch() { - setupFilter("bar,baz", "baz"); - assertThat(match(EndpointId.of("baz"))).isFalse(); - } - - @Test - void matchWhenExposeMatchesAndExcludeDoesNotMatchShouldMatch() { - setupFilter("bar,baz", "buz"); - assertThat(match(EndpointId.of("baz"))).isTrue(); - } - - @Test - void matchWhenExposeMatchesWithDifferentCaseShouldMatch() { - setupFilter("bar", ""); - assertThat(match(EndpointId.of("bAr"))).isTrue(); - } - - @Test - void matchWhenDiscovererDoesNotMatchShouldMatch() { - MockEnvironment environment = new MockEnvironment(); - environment.setProperty("foo.include", "bar"); - environment.setProperty("foo.exclude", ""); - this.filter = new ExposeExcludePropertyEndpointFilter<>(DifferentTestExposableWebEndpoint.class, environment, - "foo"); - assertThat(match(EndpointId.of("baz"))).isTrue(); - } - - @Test - void matchWhenIncludeIsAsteriskShouldMatchAll() { - setupFilter("*", "buz"); - assertThat(match(EndpointId.of("bar"))).isTrue(); - assertThat(match(EndpointId.of("baz"))).isTrue(); - assertThat(match(EndpointId.of("buz"))).isFalse(); - } - - @Test - void matchWhenExcludeIsAsteriskShouldMatchNone() { - setupFilter("bar,baz,buz", "*"); - assertThat(match(EndpointId.of("bar"))).isFalse(); - assertThat(match(EndpointId.of("baz"))).isFalse(); - assertThat(match(EndpointId.of("buz"))).isFalse(); - } - - @Test - void matchWhenMixedCaseShouldMatch() { - setupFilter("foo-bar", ""); - assertThat(match(EndpointId.of("fooBar"))).isTrue(); - } - - private void setupFilter(String include, String exclude) { - MockEnvironment environment = new MockEnvironment(); - environment.setProperty("foo.include", include); - environment.setProperty("foo.exclude", exclude); - this.filter = new ExposeExcludePropertyEndpointFilter<>(TestExposableWebEndpoint.class, environment, "foo", - "def"); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - private boolean match(EndpointId id) { - ExposableEndpoint endpoint = mock(TestExposableWebEndpoint.class); - given(endpoint.getEndpointId()).willReturn(id); - return ((EndpointFilter) this.filter).match(endpoint); - } - - abstract static class TestExposableWebEndpoint implements ExposableWebEndpoint { - - } - - abstract static class DifferentTestExposableWebEndpoint implements ExposableWebEndpoint { - - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnAvailableEndpointTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnAvailableEndpointTests.java index 41acd56904fb..49f18df9d4db 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnAvailableEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnAvailableEndpointTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,8 +40,8 @@ class ConditionalOnAvailableEndpointTests { @Test void outcomeShouldMatchDefaults() { - this.contextRunner.run((context) -> assertThat(context).hasBean("info").hasBean("health") - .doesNotHaveBean("spring").doesNotHaveBean("test").doesNotHaveBean("shutdown")); + this.contextRunner.run((context) -> assertThat(context).hasBean("health").doesNotHaveBean("spring") + .doesNotHaveBean("test").doesNotHaveBean("shutdown")); } @Test @@ -79,7 +79,7 @@ void outcomeWhenIncludeAllWebAndEnablingEndpointDisabledByDefaultShouldMatchAll( @Test void outcomeWhenIncludeAllJmxButJmxDisabledShouldMatchDefaults() { this.contextRunner.withPropertyValues("management.endpoints.jmx.exposure.include=*") - .run((context) -> assertThat(context).hasBean("info").hasBean("health").doesNotHaveBean("spring") + .run((context) -> assertThat(context).hasBean("health").doesNotHaveBean("spring") .doesNotHaveBean("test").doesNotHaveBean("shutdown")); } @@ -95,8 +95,8 @@ void outcomeWhenIncludeAllJmxAndJmxEnabledAndEnablingEndpointDisabledByDefaultSh this.contextRunner .withPropertyValues("management.endpoints.jmx.exposure.include=*", "spring.jmx.enabled=true", "management.endpoint.shutdown.enabled=true") - .run((context) -> assertThat(context).hasBean("info").hasBean("health").hasBean("test") - .hasBean("spring").hasBean("shutdown")); + .run((context) -> assertThat(context).hasBean("health").hasBean("test").hasBean("spring") + .hasBean("shutdown")); } @Test @@ -182,6 +182,20 @@ void outcomeOnCloudFoundryShouldMatchAll() { (context) -> assertThat(context).hasBean("info").hasBean("health").hasBean("spring").hasBean("test")); } + @Test // gh-21044 + void outcomeWhenIncludeAllShouldMatchDashedEndpoint() { + this.contextRunner.withUserConfiguration(DashedEndpointConfiguration.class) + .withPropertyValues("management.endpoints.web.exposure.include=*") + .run((context) -> assertThat(context).hasSingleBean(DashedEndpoint.class)); + } + + @Test // gh-21044 + void outcomeWhenIncludeDashedShouldMatchDashedEndpoint() { + this.contextRunner.withUserConfiguration(DashedEndpointConfiguration.class) + .withPropertyValues("management.endpoints.web.exposure.include=test-dashed") + .run((context) -> assertThat(context).hasSingleBean(DashedEndpoint.class)); + } + @Endpoint(id = "health") static class HealthEndpoint { @@ -207,6 +221,11 @@ static class ShutdownEndpoint { } + @Endpoint(id = "test-dashed") + static class DashedEndpoint { + + } + @EndpointExtension(endpoint = SpringEndpoint.class, filter = TestFilter.class) static class SpringEndpointExtension { @@ -284,4 +303,15 @@ String springcomp() { } + @Configuration(proxyBeanMethods = false) + static class DashedEndpointConfiguration { + + @Bean + @ConditionalOnAvailableEndpoint + DashedEndpoint dashedEndpoint() { + return new DashedEndpoint(); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnEnabledEndpointTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnEnabledEndpointTests.java deleted file mode 100644 index 98a66fcc5a31..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/ConditionalOnEnabledEndpointTests.java +++ /dev/null @@ -1,277 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.autoconfigure.endpoint.condition; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.actuate.endpoint.EndpointFilter; -import org.springframework.boot.actuate.endpoint.ExposableEndpoint; -import org.springframework.boot.actuate.endpoint.annotation.Endpoint; -import org.springframework.boot.actuate.endpoint.annotation.EndpointExtension; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link ConditionalOnEnabledEndpoint @ConditionalOnEnabledEndpoint}. - * - * @author Stephane Nicoll - * @author Andy Wilkinson - */ -@Deprecated -@SuppressWarnings("deprecation") -class ConditionalOnEnabledEndpointTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner(); - - @Test - void outcomeWhenEndpointEnabledPropertyIsTrueShouldMatch() { - this.contextRunner.withPropertyValues("management.endpoint.foo.enabled=true") - .withUserConfiguration(FooEndpointEnabledByDefaultFalseConfiguration.class) - .run((context) -> assertThat(context).hasBean("foo")); - } - - @Test - void outcomeWhenEndpointEnabledPropertyIsFalseShouldNotMatch() { - this.contextRunner.withPropertyValues("management.endpoint.foo.enabled=false") - .withUserConfiguration(FooEndpointEnabledByDefaultTrueConfiguration.class) - .run((context) -> assertThat(context).doesNotHaveBean("foo")); - } - - @Test - void outcomeWhenNoEndpointPropertyAndUserDefinedDefaultIsTrueShouldMatch() { - this.contextRunner.withPropertyValues("management.endpoints.enabled-by-default=true") - .withUserConfiguration(FooEndpointEnabledByDefaultFalseConfiguration.class) - .run((context) -> assertThat(context).hasBean("foo")); - } - - @Test - void outcomeWhenNoEndpointPropertyAndUserDefinedDefaultIsFalseShouldNotMatch() { - this.contextRunner.withPropertyValues("management.endpoints.enabled-by-default=false") - .withUserConfiguration(FooEndpointEnabledByDefaultTrueConfiguration.class) - .run((context) -> assertThat(context).doesNotHaveBean("foo")); - } - - @Test - void outcomeWhenNoPropertiesAndAnnotationIsEnabledByDefaultShouldMatch() { - this.contextRunner.withUserConfiguration(FooEndpointEnabledByDefaultTrueConfiguration.class) - .run((context) -> assertThat(context).hasBean("foo")); - } - - @Test - void outcomeWhenNoPropertiesAndAnnotationIsNotEnabledByDefaultShouldNotMatch() { - this.contextRunner.withUserConfiguration(FooEndpointEnabledByDefaultFalseConfiguration.class) - .run((context) -> assertThat(context).doesNotHaveBean("foo")); - } - - @Test - void outcomeWhenNoPropertiesAndExtensionAnnotationIsEnabledByDefaultShouldMatch() { - this.contextRunner.withUserConfiguration(FooEndpointAndExtensionEnabledByDefaultTrueConfiguration.class) - .run((context) -> assertThat(context).hasBean("foo").hasBean("fooExt")); - } - - @Test - void outcomeWhenNoPropertiesAndExtensionAnnotationIsNotEnabledByDefaultShouldNotMatch() { - this.contextRunner.withUserConfiguration(FooEndpointAndExtensionEnabledByDefaultFalseConfiguration.class) - .run((context) -> assertThat(context).doesNotHaveBean("foo").doesNotHaveBean("fooExt")); - } - - @Test - void outcomeWithReferenceWhenNoPropertiesShouldMatch() { - this.contextRunner - .withUserConfiguration(FooEndpointEnabledByDefaultTrue.class, - ComponentEnabledIfEndpointIsEnabledConfiguration.class) - .run((context) -> assertThat(context).hasBean("fooComponent")); - } - - @Test - void outcomeWithReferenceWhenEndpointEnabledPropertyIsTrueShouldMatch() { - this.contextRunner.withPropertyValues("management.endpoint.foo.enabled=true") - .withUserConfiguration(FooEndpointEnabledByDefaultTrue.class, - ComponentEnabledIfEndpointIsEnabledConfiguration.class) - .run((context) -> assertThat(context).hasBean("fooComponent")); - } - - @Test - void outcomeWithReferenceWhenEndpointEnabledPropertyIsFalseShouldNotMatch() { - this.contextRunner.withPropertyValues("management.endpoint.foo.enabled=false") - .withUserConfiguration(FooEndpointEnabledByDefaultTrue.class, - ComponentEnabledIfEndpointIsEnabledConfiguration.class) - .run((context) -> assertThat(context).doesNotHaveBean("fooComponent")); - } - - @Test - void outcomeWithNoReferenceShouldFail() { - this.contextRunner.withUserConfiguration(ComponentWithNoEndpointReferenceConfiguration.class).run((context) -> { - assertThat(context).hasFailed(); - assertThat(context.getStartupFailure().getCause().getMessage()) - .contains("No endpoint is specified and the return type of the @Bean method " - + "is neither an @Endpoint, nor an @EndpointExtension"); - }); - } - - @Test - void outcomeWhenEndpointEnabledPropertyIsTrueAndMixedCaseShouldMatch() { - this.contextRunner.withPropertyValues("management.endpoint.foo-bar.enabled=true") - .withUserConfiguration(FooBarEndpointEnabledByDefaultFalseConfiguration.class) - .run((context) -> assertThat(context).hasBean("fooBar")); - } - - @Test - void outcomeWhenEndpointEnabledPropertyIsFalseOnClassShouldNotMatch() { - this.contextRunner.withPropertyValues("management.endpoint.foo.enabled=false") - .withUserConfiguration(FooEndpointEnabledByDefaultTrueOnConfigurationConfiguration.class) - .run((context) -> assertThat(context).doesNotHaveBean("foo")); - } - - @Endpoint(id = "foo", enableByDefault = true) - static class FooEndpointEnabledByDefaultTrue { - - } - - @Endpoint(id = "foo", enableByDefault = false) - static class FooEndpointEnabledByDefaultFalse { - - } - - @Endpoint(id = "fooBar", enableByDefault = false) - static class FooBarEndpointEnabledByDefaultFalse { - - } - - @EndpointExtension(endpoint = FooEndpointEnabledByDefaultTrue.class, filter = TestFilter.class) - static class FooEndpointExtensionEnabledByDefaultTrue { - - } - - @EndpointExtension(endpoint = FooEndpointEnabledByDefaultFalse.class, filter = TestFilter.class) - static class FooEndpointExtensionEnabledByDefaultFalse { - - } - - static class TestFilter implements EndpointFilter> { - - @Override - public boolean match(ExposableEndpoint endpoint) { - return true; - } - - } - - @Configuration(proxyBeanMethods = false) - static class FooEndpointEnabledByDefaultTrueConfiguration { - - @Bean - @ConditionalOnEnabledEndpoint - FooEndpointEnabledByDefaultTrue foo() { - return new FooEndpointEnabledByDefaultTrue(); - } - - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnEnabledEndpoint(endpoint = FooEndpointEnabledByDefaultTrue.class) - static class FooEndpointEnabledByDefaultTrueOnConfigurationConfiguration { - - @Bean - FooEndpointEnabledByDefaultTrue foo() { - return new FooEndpointEnabledByDefaultTrue(); - } - - } - - @Configuration(proxyBeanMethods = false) - static class FooEndpointEnabledByDefaultFalseConfiguration { - - @Bean - @ConditionalOnEnabledEndpoint - FooEndpointEnabledByDefaultFalse foo() { - return new FooEndpointEnabledByDefaultFalse(); - } - - } - - @Configuration(proxyBeanMethods = false) - static class FooBarEndpointEnabledByDefaultFalseConfiguration { - - @Bean - @ConditionalOnEnabledEndpoint - FooBarEndpointEnabledByDefaultFalse fooBar() { - return new FooBarEndpointEnabledByDefaultFalse(); - } - - } - - @Configuration(proxyBeanMethods = false) - static class FooEndpointAndExtensionEnabledByDefaultTrueConfiguration { - - @Bean - @ConditionalOnEnabledEndpoint - FooEndpointEnabledByDefaultTrue foo() { - return new FooEndpointEnabledByDefaultTrue(); - } - - @Bean - @ConditionalOnEnabledEndpoint - FooEndpointExtensionEnabledByDefaultTrue fooExt() { - return new FooEndpointExtensionEnabledByDefaultTrue(); - } - - } - - @Configuration(proxyBeanMethods = false) - static class FooEndpointAndExtensionEnabledByDefaultFalseConfiguration { - - @Bean - @ConditionalOnEnabledEndpoint - FooEndpointEnabledByDefaultFalse foo() { - return new FooEndpointEnabledByDefaultFalse(); - } - - @Bean - @ConditionalOnEnabledEndpoint - FooEndpointExtensionEnabledByDefaultFalse fooExt() { - return new FooEndpointExtensionEnabledByDefaultFalse(); - } - - } - - @Configuration(proxyBeanMethods = false) - static class ComponentEnabledIfEndpointIsEnabledConfiguration { - - @Bean - @ConditionalOnEnabledEndpoint(endpoint = FooEndpointEnabledByDefaultTrue.class) - String fooComponent() { - return "foo"; - } - - } - - @Configuration(proxyBeanMethods = false) - static class ComponentWithNoEndpointReferenceConfiguration { - - @Bean - @ConditionalOnEnabledEndpoint - String fooComponent() { - return "foo"; - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/expose/IncludeExcludeEndpointFilterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/expose/IncludeExcludeEndpointFilterTests.java new file mode 100644 index 000000000000..aefe8aae5a02 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/expose/IncludeExcludeEndpointFilterTests.java @@ -0,0 +1,181 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.endpoint.expose; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.boot.actuate.endpoint.EndpointFilter; +import org.springframework.boot.actuate.endpoint.EndpointId; +import org.springframework.boot.actuate.endpoint.ExposableEndpoint; +import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint; +import org.springframework.mock.env.MockEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link IncludeExcludeEndpointFilter}. + * + * @author Phillip Webb + */ +@ExtendWith(MockitoExtension.class) +class IncludeExcludeEndpointFilterTests { + + private IncludeExcludeEndpointFilter filter; + + @Test + void createWhenEndpointTypeIsNullShouldThrowException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new IncludeExcludeEndpointFilter<>(null, new MockEnvironment(), "foo")) + .withMessageContaining("EndpointType must not be null"); + } + + @Test + void createWhenEnvironmentIsNullShouldThrowException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new IncludeExcludeEndpointFilter<>(ExposableEndpoint.class, null, "foo")) + .withMessageContaining("Environment must not be null"); + } + + @Test + void createWhenPrefixIsNullShouldThrowException() { + assertThatIllegalArgumentException() + .isThrownBy( + () -> new IncludeExcludeEndpointFilter<>(ExposableEndpoint.class, new MockEnvironment(), null)) + .withMessageContaining("Prefix must not be empty"); + } + + @Test + void createWhenPrefixIsEmptyShouldThrowException() { + assertThatIllegalArgumentException() + .isThrownBy( + () -> new IncludeExcludeEndpointFilter<>(ExposableEndpoint.class, new MockEnvironment(), "")) + .withMessageContaining("Prefix must not be empty"); + } + + @Test + void matchWhenExposeIsEmptyAndExcludeIsEmptyAndInDefaultShouldMatch() { + setupFilter("", ""); + assertThat(match(EndpointId.of("def"))).isTrue(); + } + + @Test + void matchWhenExposeIsEmptyAndExcludeIsEmptyAndNotInDefaultShouldNotMatch() { + setupFilter("", ""); + assertThat(match(EndpointId.of("bar"))).isFalse(); + } + + @Test + void matchWhenExposeMatchesAndExcludeIsEmptyShouldMatch() { + setupFilter("bar", ""); + assertThat(match(EndpointId.of("bar"))).isTrue(); + } + + @Test + void matchWhenExposeDoesNotMatchAndExcludeIsEmptyShouldNotMatch() { + setupFilter("bar", ""); + assertThat(match(EndpointId.of("baz"))).isFalse(); + } + + @Test + void matchWhenExposeMatchesAndExcludeMatchesShouldNotMatch() { + setupFilter("bar,baz", "baz"); + assertThat(match(EndpointId.of("baz"))).isFalse(); + } + + @Test + void matchWhenExposeMatchesAndExcludeDoesNotMatchShouldMatch() { + setupFilter("bar,baz", "buz"); + assertThat(match(EndpointId.of("baz"))).isTrue(); + } + + @Test + void matchWhenExposeMatchesWithDifferentCaseShouldMatch() { + setupFilter("bar", ""); + assertThat(match(EndpointId.of("bAr"))).isTrue(); + } + + @Test + void matchWhenDiscovererDoesNotMatchShouldMatch() { + MockEnvironment environment = new MockEnvironment(); + environment.setProperty("foo.include", "bar"); + environment.setProperty("foo.exclude", ""); + this.filter = new IncludeExcludeEndpointFilter<>(DifferentTestExposableWebEndpoint.class, environment, "foo"); + assertThat(match()).isTrue(); + } + + @Test + void matchWhenIncludeIsAsteriskShouldMatchAll() { + setupFilter("*", "buz"); + assertThat(match(EndpointId.of("bar"))).isTrue(); + assertThat(match(EndpointId.of("baz"))).isTrue(); + assertThat(match(EndpointId.of("buz"))).isFalse(); + } + + @Test + void matchWhenExcludeIsAsteriskShouldMatchNone() { + setupFilter("bar,baz,buz", "*"); + assertThat(match(EndpointId.of("bar"))).isFalse(); + assertThat(match(EndpointId.of("baz"))).isFalse(); + assertThat(match(EndpointId.of("buz"))).isFalse(); + } + + @Test + void matchWhenMixedCaseShouldMatch() { + setupFilter("foo-bar", ""); + assertThat(match(EndpointId.of("fooBar"))).isTrue(); + } + + @Test // gh-20997 + void matchWhenDashInName() { + setupFilter("bus-refresh", ""); + assertThat(match(EndpointId.of("bus-refresh"))).isTrue(); + } + + private void setupFilter(String include, String exclude) { + MockEnvironment environment = new MockEnvironment(); + environment.setProperty("foo.include", include); + environment.setProperty("foo.exclude", exclude); + this.filter = new IncludeExcludeEndpointFilter<>(TestExposableWebEndpoint.class, environment, "foo", "def"); + } + + private boolean match() { + return match(null); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private boolean match(EndpointId id) { + ExposableEndpoint endpoint = mock(TestExposableWebEndpoint.class); + if (id != null) { + given(endpoint.getEndpointId()).willReturn(id); + } + return ((EndpointFilter) this.filter).match(endpoint); + } + + abstract static class TestExposableWebEndpoint implements ExposableWebEndpoint { + + } + + abstract static class DifferentTestExposableWebEndpoint implements ExposableWebEndpoint { + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointAutoConfigurationTests.java new file mode 100644 index 000000000000..eedf2db39887 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointAutoConfigurationTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.endpoint.jmx; + +import java.util.function.Function; + +import javax.management.MBeanServer; + +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.jmx.EndpointObjectNameFactory; +import org.springframework.boot.actuate.endpoint.jmx.ExposableJmxEndpoint; +import org.springframework.boot.actuate.endpoint.jmx.JmxEndpointExporter; +import org.springframework.boot.actuate.endpoint.jmx.annotation.JmxEndpointDiscoverer; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link JmxEndpointAutoConfiguration}. + * + * @author Stephane Nicoll + */ +class JmxEndpointAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(EndpointAutoConfiguration.class, JmxAutoConfiguration.class, + JmxEndpointAutoConfiguration.class)) + .withUserConfiguration(TestEndpoint.class); + + private final MBeanServer mBeanServer = mock(MBeanServer.class); + + @Test + void jmxEndpointWithoutJmxSupportNotAutoConfigured() { + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(MBeanServer.class) + .doesNotHaveBean(JmxEndpointDiscoverer.class).doesNotHaveBean(JmxEndpointExporter.class)); + } + + @Test + void jmxEndpointWithJmxSupportAutoConfigured() { + this.contextRunner.withPropertyValues("spring.jmx.enabled=true").with(mockMBeanServer()) + .run((context) -> assertThat(context).hasSingleBean(JmxEndpointDiscoverer.class) + .hasSingleBean(JmxEndpointExporter.class)); + } + + @Test + void jmxEndpointWithCustomEndpointObjectNameFactory() { + EndpointObjectNameFactory factory = mock(EndpointObjectNameFactory.class); + this.contextRunner.withPropertyValues("spring.jmx.enabled=true").with(mockMBeanServer()) + .withBean(EndpointObjectNameFactory.class, () -> factory).run((context) -> { + ArgumentCaptor argumentCaptor = ArgumentCaptor + .forClass(ExposableJmxEndpoint.class); + then(factory).should().getObjectName(argumentCaptor.capture()); + ExposableJmxEndpoint jmxEndpoint = argumentCaptor.getValue(); + assertThat(jmxEndpoint.getEndpointId().toLowerCaseString()).isEqualTo("test"); + }); + } + + private Function mockMBeanServer() { + return (ctxRunner) -> ctxRunner.withBean("mbeanServer", MBeanServer.class, () -> this.mBeanServer); + } + + @Endpoint(id = "test") + static class TestEndpoint { + + @ReadOperation + String hello() { + return "hello world"; + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/ServletEndpointManagementContextConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/ServletEndpointManagementContextConfigurationTests.java index fd0183dde951..8c914672c06f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/ServletEndpointManagementContextConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/ServletEndpointManagementContextConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,7 +44,7 @@ */ class ServletEndpointManagementContextConfigurationTests { - private WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() .withUserConfiguration(TestConfig.class); @Test diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfigurationTests.java index 89fc5ae40c7d..38a704205406 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/WebEndpointAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,10 +24,10 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; -import org.springframework.boot.actuate.autoconfigure.endpoint.ExposeExcludePropertyEndpointFilter; +import org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExcludeEndpointFilter; +import org.springframework.boot.actuate.endpoint.ApiVersion; import org.springframework.boot.actuate.endpoint.EndpointId; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; -import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint; import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoint; @@ -51,18 +51,21 @@ */ class WebEndpointAutoConfigurationTests { + private static final String V2_JSON = ApiVersion.V2.getProducedMimeType().toString(); + + private static final String V3_JSON = ApiVersion.V3.getProducedMimeType().toString(); + private static final AutoConfigurations CONFIGURATIONS = AutoConfigurations.of(EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class); - private WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() .withConfiguration(CONFIGURATIONS); @Test void webApplicationConfiguresEndpointMediaTypes() { this.contextRunner.run((context) -> { EndpointMediaTypes endpointMediaTypes = context.getBean(EndpointMediaTypes.class); - assertThat(endpointMediaTypes.getConsumed()).containsExactly(ActuatorMediaType.V3_JSON, - ActuatorMediaType.V2_JSON, "application/json"); + assertThat(endpointMediaTypes.getConsumed()).containsExactly(V3_JSON, V2_JSON, "application/json"); }); } @@ -104,7 +107,7 @@ void webApplicationConfiguresEndpointDiscoverer() { @Test void webApplicationConfiguresExposeExcludePropertyEndpointFilter() { this.contextRunner - .run((context) -> assertThat(context).getBeans(ExposeExcludePropertyEndpointFilter.class).containsKeys( + .run((context) -> assertThat(context).getBeans(IncludeExcludeEndpointFilter.class).containsKeys( "webExposeExcludePropertyEndpointFilter", "controllerExposeExcludePropertyEndpointFilter")); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/AbstractEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/AbstractEndpointDocumentationTests.java index 72029805e7ca..8ab7f40e92ab 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/AbstractEndpointDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/AbstractEndpointDocumentationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -59,7 +59,7 @@ "management.endpoints.web.exposure.include=*", "spring.jackson.default-property-inclusion=non_null" }) public abstract class AbstractEndpointDocumentationTests { - protected String describeEnumValues(Class> enumType) { + protected static String describeEnumValues(Class> enumType) { return StringUtils.collectionToDelimitedString(Stream.of(enumType.getEnumConstants()) .map((constant) -> "`" + constant.name() + "`").collect(Collectors.toList()), ", "); } @@ -77,13 +77,11 @@ protected OperationPreprocessor limit(Predicate filter, String... keys) { Object target = payload; Map parent = null; for (String key : keys) { - if (target instanceof Map) { - parent = (Map) target; - target = parent.get(key); - } - else { + if (!(target instanceof Map)) { throw new IllegalStateException(); } + parent = (Map) target; + target = parent.get(key); } if (target instanceof Map) { parent.put(keys[keys.length - 1], select((Map) target, filter)); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/AuditEventsEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/AuditEventsEndpointDocumentationTests.java index 5b267f0430f8..d5c18d173596 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/AuditEventsEndpointDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/AuditEventsEndpointDocumentationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.verify; +import static org.mockito.BDDMockito.then; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; @@ -83,7 +83,7 @@ void filteredAuditEvents() throws Exception { "Restricts the events to those with the given principal. Optional."), parameterWithName("type") .description("Restricts the events to those with the given type. Optional.")))); - verify(this.repository).find("alice", now.toInstant(), "logout"); + then(this.repository).should().find("alice", now.toInstant(), "logout"); } @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/BeansEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/BeansEndpointDocumentationTests.java index e96b6ac54fc8..41a4ae8563c0 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/BeansEndpointDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/BeansEndpointDocumentationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.restdocs.payload.FieldDescriptor; +import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.restdocs.payload.ResponseFieldsSnippet; import org.springframework.util.CollectionUtils; @@ -52,7 +53,8 @@ void beans() throws Exception { List beanFields = Arrays.asList(fieldWithPath("aliases").description("Names of any aliases."), fieldWithPath("scope").description("Scope of the bean."), fieldWithPath("type").description("Fully qualified type of the bean."), - fieldWithPath("resource").description("Resource in which the bean was defined, if any.").optional(), + fieldWithPath("resource").description("Resource in which the bean was defined, if any.").optional() + .type(JsonFieldType.STRING), fieldWithPath("dependencies").description("Names of any dependencies.")); ResponseFieldsSnippet responseFields = responseFields( fieldWithPath("contexts").description("Application contexts keyed by id."), parentIdField(), diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/ConfigurationPropertiesReportEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/ConfigurationPropertiesReportEndpointDocumentationTests.java index 7ba82095666f..fa7dc514e90a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/ConfigurationPropertiesReportEndpointDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/ConfigurationPropertiesReportEndpointDocumentationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,13 +36,14 @@ * {@link ConfigurationPropertiesReportEndpoint}. * * @author Andy Wilkinson + * @author Chris Bono */ class ConfigurationPropertiesReportEndpointDocumentationTests extends MockMvcEndpointDocumentationTests { @Test void configProps() throws Exception { this.mockMvc.perform(get("/actuator/configprops")).andExpect(status().isOk()) - .andDo(MockMvcRestDocumentation.document("configprops", + .andDo(MockMvcRestDocumentation.document("configprops/all", preprocessResponse(limit("contexts", getApplicationContext().getId(), "beans")), responseFields(fieldWithPath("contexts").description("Application contexts keyed by id."), fieldWithPath("contexts.*.beans.*") @@ -51,6 +52,25 @@ void configProps() throws Exception { .description("Prefix applied to the names of the bean's properties."), subsectionWithPath("contexts.*.beans.*.properties") .description("Properties of the bean as name-value pairs."), + subsectionWithPath("contexts.*.beans.*.inputs").description( + "Origin and value of the configuration property used when binding to this bean."), + parentIdField()))); + } + + @Test + void configPropsFilterByPrefix() throws Exception { + this.mockMvc.perform(get("/actuator/configprops/spring.resources")).andExpect(status().isOk()) + .andDo(MockMvcRestDocumentation.document("configprops/prefixed", + preprocessResponse(limit("contexts", getApplicationContext().getId(), "beans")), + responseFields(fieldWithPath("contexts").description("Application contexts keyed by id."), + fieldWithPath("contexts.*.beans.*") + .description("`@ConfigurationProperties` beans keyed by bean name."), + fieldWithPath("contexts.*.beans.*.prefix") + .description("Prefix applied to the names of the bean's properties."), + subsectionWithPath("contexts.*.beans.*.properties") + .description("Properties of the bean as name-value pairs."), + subsectionWithPath("contexts.*.beans.*.inputs").description( + "Origin and value of the configuration property used when binding to this bean."), parentIdField()))); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/HealthEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/HealthEndpointDocumentationTests.java index 588bbe881ace..4a7925ee2712 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/HealthEndpointDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/HealthEndpointDocumentationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -81,8 +81,7 @@ void health() throws Exception { .description("The nested components that make up the health.").optional(); FieldDescriptor componentDetails = subsectionWithPath("components.*.details") .description("Details of the health of a specific part of the application. " - + "Presence is controlled by `management.endpoint.health.show-details`. May contain nested " - + "components that make up the health.") + + "Presence is controlled by `management.endpoint.health.show-details`.") .optional(); this.mockMvc.perform(get("/actuator/health").accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) .andDo(document("health", diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/HeapDumpWebEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/HeapDumpWebEndpointDocumentationTests.java index a811e9533d8d..6e84c73fe358 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/HeapDumpWebEndpointDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/HeapDumpWebEndpointDocumentationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -65,7 +65,7 @@ HeapDumpWebEndpoint endpoint() { return new HeapDumpWebEndpoint() { @Override - protected HeapDumper createHeapDumper() throws HeapDumperUnavailableException { + protected HeapDumper createHeapDumper() { return (file, live) -> FileCopyUtils.copy("<>", new FileWriter(file)); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/InfoEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/InfoEndpointDocumentationTests.java index 6d934bc91e93..c7c88a301192 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/InfoEndpointDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/InfoEndpointDocumentationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -80,7 +80,7 @@ InfoEndpoint endpoint(List infoContributors) { @Bean GitInfoContributor gitInfoContributor() { Properties properties = new Properties(); - properties.put("branch", "master"); + properties.put("branch", "main"); properties.put("commit.id", "df027cf1ec5aeba2d4fedd7b8c42b88dc5ce38e5"); properties.put("commit.id.abbrev", "df027cf"); properties.put("commit.time", Long.toString(System.currentTimeMillis())); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/LogFileWebEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/LogFileWebEndpointDocumentationTests.java index 5e68cde6ca34..668c702e849f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/LogFileWebEndpointDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/LogFileWebEndpointDocumentationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,9 +23,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import org.springframework.core.env.Environment; +import org.springframework.mock.env.MockEnvironment; import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; -import org.springframework.test.context.TestPropertySource; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -35,8 +34,6 @@ * * @author Andy Wilkinson */ -@TestPropertySource( - properties = "logging.file.name=src/test/resources/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/sample.log") class LogFileWebEndpointDocumentationTests extends MockMvcEndpointDocumentationTests { @Test @@ -56,7 +53,10 @@ void logFileRange() throws Exception { static class TestConfiguration { @Bean - LogFileWebEndpoint endpoint(Environment environment) { + LogFileWebEndpoint endpoint() { + MockEnvironment environment = new MockEnvironment(); + environment.setProperty("logging.file.name", + "src/test/resources/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/sample.log"); return new LogFileWebEndpoint(LogFile.get(environment), null); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/LoggersEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/LoggersEndpointDocumentationTests.java index 81ad2c352a8c..ff01109eb15b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/LoggersEndpointDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/LoggersEndpointDocumentationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,7 +40,7 @@ import org.springframework.restdocs.payload.JsonFieldType; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.verify; +import static org.mockito.BDDMockito.then; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; @@ -114,7 +114,7 @@ void setLogLevel() throws Exception { .andExpect(status().isNoContent()) .andDo(MockMvcRestDocumentation.document("loggers/set", requestFields(fieldWithPath("configuredLevel") .description("Level for the logger. May be omitted to clear the level.").optional()))); - verify(this.loggingSystem).setLogLevel("com.example", LogLevel.DEBUG); + then(this.loggingSystem).should().setLogLevel("com.example", LogLevel.DEBUG); } @Test @@ -127,8 +127,8 @@ void setLogLevelOfLoggerGroup() throws Exception { requestFields(fieldWithPath("configuredLevel").description( "Level for the logger group. May be omitted to clear the level of the loggers.") .optional()))); - verify(this.loggingSystem).setLogLevel("test.member1", LogLevel.DEBUG); - verify(this.loggingSystem).setLogLevel("test.member2", LogLevel.DEBUG); + then(this.loggingSystem).should().setLogLevel("test.member1", LogLevel.DEBUG); + then(this.loggingSystem).should().setLogLevel("test.member2", LogLevel.DEBUG); resetLogger(); } @@ -142,7 +142,7 @@ void clearLogLevel() throws Exception { this.mockMvc .perform(post("/actuator/loggers/com.example").content("{}").contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isNoContent()).andDo(MockMvcRestDocumentation.document("loggers/clear")); - verify(this.loggingSystem).setLogLevel("com.example", null); + then(this.loggingSystem).should().setLogLevel("com.example", null); } @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/MappingsEndpointReactiveDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/MappingsEndpointReactiveDocumentationTests.java index 9be633f7dba1..7ae63277f911 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/MappingsEndpointReactiveDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/MappingsEndpointReactiveDocumentationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.web.documentation; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -73,11 +74,11 @@ class MappingsEndpointReactiveDocumentationTests extends AbstractEndpointDocumen void webTestClient(RestDocumentationContextProvider restDocumentation) { this.client = WebTestClient.bindToServer() .filter(documentationConfiguration(restDocumentation).snippets().withDefaults()) - .baseUrl("http://localhost:" + this.port).build(); + .baseUrl("http://localhost:" + this.port).responseTimeout(Duration.ofMinutes(5)).build(); } @Test - void mappings() throws Exception { + void mappings() { List requestMappingConditions = Arrays.asList( requestMappingConditionField("").description("Details of the request mapping conditions.").optional(), requestMappingConditionField(".consumes").description("Details of the consumes condition"), diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/MappingsEndpointServletDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/MappingsEndpointServletDocumentationTests.java index b4c5c6b4e2d0..4055c9752ce8 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/MappingsEndpointServletDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/MappingsEndpointServletDocumentationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.boot.actuate.autoconfigure.endpoint.web.documentation; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -72,11 +73,11 @@ class MappingsEndpointServletDocumentationTests extends AbstractEndpointDocument @BeforeEach void webTestClient(RestDocumentationContextProvider restDocumentation) { this.client = WebTestClient.bindToServer().filter(documentationConfiguration(restDocumentation)) - .baseUrl("http://localhost:" + this.port).build(); + .baseUrl("http://localhost:" + this.port).responseTimeout(Duration.ofMinutes(5)).build(); } @Test - void mappings() throws Exception { + void mappings() { ResponseFieldsSnippet commonResponseFields = responseFields( fieldWithPath("contexts").description("Application contexts keyed by id."), fieldWithPath("contexts.*.mappings").description("Mappings in the context, keyed by mapping type."), diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/MetricsEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/MetricsEndpointDocumentationTests.java index aa01ae3d639f..4b93645e72fe 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/MetricsEndpointDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/MetricsEndpointDocumentationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/PrometheusScrapeEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/PrometheusScrapeEndpointDocumentationTests.java index c72db1e99e0a..94da4bf4b543 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/PrometheusScrapeEndpointDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/PrometheusScrapeEndpointDocumentationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; import io.micrometer.prometheus.PrometheusMeterRegistry; import io.prometheus.client.CollectorRegistry; +import io.prometheus.client.exporter.common.TextFormat; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusScrapeEndpoint; @@ -28,19 +29,41 @@ import org.springframework.context.annotation.Import; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** * Tests for generating documentation describing the {@link PrometheusScrapeEndpoint}. * * @author Andy Wilkinson + * @author Johnny Lim */ class PrometheusScrapeEndpointDocumentationTests extends MockMvcEndpointDocumentationTests { @Test void prometheus() throws Exception { - this.mockMvc.perform(get("/actuator/prometheus")).andExpect(status().isOk()).andDo(document("prometheus")); + this.mockMvc.perform(get("/actuator/prometheus")).andExpect(status().isOk()).andDo(document("prometheus/all")); + } + + @Test + void prometheusOpenmetrics() throws Exception { + this.mockMvc.perform(get("/actuator/prometheus").accept(TextFormat.CONTENT_TYPE_OPENMETRICS_100)) + .andExpect(status().isOk()) + .andExpect(header().string("Content-Type", "application/openmetrics-text;version=1.0.0;charset=utf-8")) + .andDo(document("prometheus/openmetrics")); + } + + @Test + void filteredPrometheus() throws Exception { + this.mockMvc + .perform(get("/actuator/prometheus").param("includedNames", + "jvm_memory_used_bytes,jvm_memory_committed_bytes")) + .andExpect(status().isOk()) + .andDo(document("prometheus/names", requestParameters(parameterWithName("includedNames") + .description("Restricts the samples to those that match the names. Optional.").optional()))); } @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/QuartzEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/QuartzEndpointDocumentationTests.java new file mode 100644 index 000000000000..87cb2b9f1b0f --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/QuartzEndpointDocumentationTests.java @@ -0,0 +1,469 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.endpoint.web.documentation; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map.Entry; +import java.util.TimeZone; + +import org.junit.jupiter.api.Test; +import org.quartz.CalendarIntervalScheduleBuilder; +import org.quartz.CalendarIntervalTrigger; +import org.quartz.CronScheduleBuilder; +import org.quartz.CronTrigger; +import org.quartz.DailyTimeIntervalScheduleBuilder; +import org.quartz.DailyTimeIntervalTrigger; +import org.quartz.DateBuilder.IntervalUnit; +import org.quartz.Job; +import org.quartz.JobBuilder; +import org.quartz.JobDetail; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.SimpleScheduleBuilder; +import org.quartz.SimpleTrigger; +import org.quartz.TimeOfDay; +import org.quartz.Trigger; +import org.quartz.Trigger.TriggerState; +import org.quartz.TriggerBuilder; +import org.quartz.TriggerKey; +import org.quartz.impl.matchers.GroupMatcher; +import org.quartz.spi.OperableTrigger; + +import org.springframework.boot.actuate.quartz.QuartzEndpoint; +import org.springframework.boot.actuate.quartz.QuartzEndpointWebExtension; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.restdocs.payload.FieldDescriptor; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.scheduling.quartz.DelegatingJob; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.relaxedResponseFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Tests for generating documentation describing the {@link QuartzEndpoint}. + * + * @author Vedran Pavic + * @author Stephane Nicoll + */ +class QuartzEndpointDocumentationTests extends MockMvcEndpointDocumentationTests { + + private static final TimeZone timeZone = TimeZone.getTimeZone("Europe/Paris"); + + private static final JobDetail jobOne = JobBuilder.newJob(DelegatingJob.class).withIdentity("jobOne", "samples") + .withDescription("A sample job").usingJobData("user", "admin").usingJobData("password", "secret").build(); + + private static final JobDetail jobTwo = JobBuilder.newJob(Job.class).withIdentity("jobTwo", "samples").build(); + + private static final JobDetail jobThree = JobBuilder.newJob(Job.class).withIdentity("jobThree", "tests").build(); + + private static final CronTrigger cronTrigger = TriggerBuilder.newTrigger().forJob(jobOne).withPriority(3) + .withDescription("3AM on weekdays").withIdentity("3am-weekdays", "samples") + .withSchedule( + CronScheduleBuilder.atHourAndMinuteOnGivenDaysOfWeek(3, 0, 1, 2, 3, 4, 5).inTimeZone(timeZone)) + .build(); + + private static final SimpleTrigger simpleTrigger = TriggerBuilder.newTrigger().forJob(jobOne).withPriority(7) + .withDescription("Once a day").withIdentity("every-day", "samples") + .withSchedule(SimpleScheduleBuilder.repeatHourlyForever(24)).build(); + + private static final CalendarIntervalTrigger calendarIntervalTrigger = TriggerBuilder.newTrigger().forJob(jobTwo) + .withDescription("Once a week").withIdentity("once-a-week", "samples") + .withSchedule(CalendarIntervalScheduleBuilder.calendarIntervalSchedule().withIntervalInWeeks(1) + .inTimeZone(timeZone)) + .build(); + + private static final DailyTimeIntervalTrigger dailyTimeIntervalTrigger = TriggerBuilder.newTrigger() + .forJob(jobThree).withDescription("Every hour between 9AM and 6PM on Tuesday and Thursday") + .withIdentity("every-hour-tue-thu") + .withSchedule(DailyTimeIntervalScheduleBuilder.dailyTimeIntervalSchedule() + .onDaysOfTheWeek(Calendar.TUESDAY, Calendar.THURSDAY) + .startingDailyAt(TimeOfDay.hourAndMinuteOfDay(9, 0)) + .endingDailyAt(TimeOfDay.hourAndMinuteOfDay(18, 0)).withInterval(1, IntervalUnit.HOUR)) + .build(); + + private static final List triggerSummary = Arrays.asList(previousFireTime(""), nextFireTime(""), + priority("")); + + private static final List cronTriggerSummary = Arrays.asList( + fieldWithPath("expression").description("Cron expression to use."), + fieldWithPath("timeZone").type(JsonFieldType.STRING).optional() + .description("Time zone for which the expression will be resolved, if any.")); + + private static final List simpleTriggerSummary = Collections + .singletonList(fieldWithPath("interval").description("Interval, in milliseconds, between two executions.")); + + private static final List dailyTimeIntervalTriggerSummary = Arrays.asList( + fieldWithPath("interval").description( + "Interval, in milliseconds, added to the fire time in order to calculate the time of the next trigger repeat."), + fieldWithPath("daysOfWeek").type(JsonFieldType.ARRAY) + .description("An array of days of the week upon which to fire."), + fieldWithPath("startTimeOfDay").type(JsonFieldType.STRING) + .description("Time of day to start firing at the given interval, if any."), + fieldWithPath("endTimeOfDay").type(JsonFieldType.STRING) + .description("Time of day to complete firing at the given interval, if any.")); + + private static final List calendarIntervalTriggerSummary = Arrays.asList( + fieldWithPath("interval").description( + "Interval, in milliseconds, added to the fire time in order to calculate the time of the next trigger repeat."), + fieldWithPath("timeZone").type(JsonFieldType.STRING) + .description("Time zone within which time calculations will be performed, if any.")); + + private static final List customTriggerSummary = Collections.singletonList( + fieldWithPath("trigger").description("A toString representation of the custom trigger instance.")); + + private static final FieldDescriptor[] commonCronDetails = new FieldDescriptor[] { + fieldWithPath("group").description("Name of the group."), + fieldWithPath("name").description("Name of the trigger."), + fieldWithPath("description").description("Description of the trigger, if any."), + fieldWithPath("state") + .description("State of the trigger (" + describeEnumValues(TriggerState.class) + ")."), + fieldWithPath("type").description( + "Type of the trigger (`calendarInterval`, `cron`, `custom`, `dailyTimeInterval`, `simple`). " + + "Determines the key of the object containing type-specific details."), + fieldWithPath("calendarName").description("Name of the Calendar associated with this Trigger, if any."), + startTime(""), endTime(""), previousFireTime(""), nextFireTime(""), priority(""), + fieldWithPath("finalFireTime").optional().type(JsonFieldType.STRING) + .description("Last time at which the Trigger will fire, if any."), + fieldWithPath("data").optional().type(JsonFieldType.OBJECT) + .description("Job data map keyed by name, if any.") }; + + @MockBean + private Scheduler scheduler; + + @Test + void quartzReport() throws Exception { + mockJobs(jobOne, jobTwo, jobThree); + mockTriggers(cronTrigger, simpleTrigger, calendarIntervalTrigger, dailyTimeIntervalTrigger); + this.mockMvc.perform(get("/actuator/quartz")).andExpect(status().isOk()) + .andDo(document("quartz/report", + responseFields(fieldWithPath("jobs.groups").description("An array of job group names."), + fieldWithPath("triggers.groups").description("An array of trigger group names.")))); + } + + @Test + void quartzJobs() throws Exception { + mockJobs(jobOne, jobTwo, jobThree); + this.mockMvc.perform(get("/actuator/quartz/jobs")).andExpect(status().isOk()).andDo( + document("quartz/jobs", responseFields(fieldWithPath("groups").description("Job groups keyed by name."), + fieldWithPath("groups.*.jobs").description("An array of job names.")))); + } + + @Test + void quartzTriggers() throws Exception { + mockTriggers(cronTrigger, simpleTrigger, calendarIntervalTrigger, dailyTimeIntervalTrigger); + this.mockMvc.perform(get("/actuator/quartz/triggers")).andExpect(status().isOk()) + .andDo(document("quartz/triggers", + responseFields(fieldWithPath("groups").description("Trigger groups keyed by name."), + fieldWithPath("groups.*.paused").description("Whether this trigger group is paused."), + fieldWithPath("groups.*.triggers").description("An array of trigger names.")))); + } + + @Test + void quartzJobGroup() throws Exception { + mockJobs(jobOne, jobTwo, jobThree); + this.mockMvc.perform(get("/actuator/quartz/jobs/samples")).andExpect(status().isOk()) + .andDo(document("quartz/job-group", + responseFields(fieldWithPath("group").description("Name of the group."), + fieldWithPath("jobs").description("Job details keyed by name."), + fieldWithPath("jobs.*.className") + .description("Fully qualified name of the job implementation.")))); + } + + @Test + void quartzTriggerGroup() throws Exception { + CronTrigger cron = cronTrigger.getTriggerBuilder().startAt(fromUtc("2020-11-30T17:00:00Z")) + .endAt(fromUtc("2020-12-30T03:00:00Z")).withIdentity("3am-week", "tests").build(); + setPreviousNextFireTime(cron, "2020-12-04T03:00:00Z", "2020-12-07T03:00:00Z"); + SimpleTrigger simple = simpleTrigger.getTriggerBuilder().withIdentity("every-day", "tests").build(); + setPreviousNextFireTime(simple, null, "2020-12-04T12:00:00Z"); + CalendarIntervalTrigger calendarInterval = calendarIntervalTrigger.getTriggerBuilder() + .withIdentity("once-a-week", "tests").startAt(fromUtc("2019-07-10T14:00:00Z")) + .endAt(fromUtc("2023-01-01T12:00:00Z")).build(); + setPreviousNextFireTime(calendarInterval, "2020-12-02T14:00:00Z", "2020-12-08T14:00:00Z"); + DailyTimeIntervalTrigger tueThuTrigger = dailyTimeIntervalTrigger.getTriggerBuilder() + .withIdentity("tue-thu", "tests").build(); + Trigger customTrigger = mock(Trigger.class); + given(customTrigger.getKey()).willReturn(TriggerKey.triggerKey("once-a-year-custom", "tests")); + given(customTrigger.toString()).willReturn("com.example.CustomTrigger@fdsfsd"); + given(customTrigger.getPriority()).willReturn(10); + given(customTrigger.getPreviousFireTime()).willReturn(fromUtc("2020-07-14T16:00:00Z")); + given(customTrigger.getNextFireTime()).willReturn(fromUtc("2021-07-14T16:00:00Z")); + mockTriggers(cron, simple, calendarInterval, tueThuTrigger, customTrigger); + this.mockMvc.perform(get("/actuator/quartz/triggers/tests")).andExpect(status().isOk()).andDo(document( + "quartz/trigger-group", + responseFields(fieldWithPath("group").description("Name of the group."), + fieldWithPath("paused").description("Whether the group is paused."), + fieldWithPath("triggers.cron").description("Cron triggers keyed by name, if any."), + fieldWithPath("triggers.simple").description("Simple triggers keyed by name, if any."), + fieldWithPath("triggers.dailyTimeInterval") + .description("Daily time interval triggers keyed by name, if any."), + fieldWithPath("triggers.calendarInterval") + .description("Calendar interval triggers keyed by name, if any."), + fieldWithPath("triggers.custom").description("Any other triggers keyed by name, if any.")) + .andWithPrefix("triggers.cron.*.", concat(triggerSummary, cronTriggerSummary)) + .andWithPrefix("triggers.simple.*.", concat(triggerSummary, simpleTriggerSummary)) + .andWithPrefix("triggers.dailyTimeInterval.*.", + concat(triggerSummary, dailyTimeIntervalTriggerSummary)) + .andWithPrefix("triggers.calendarInterval.*.", + concat(triggerSummary, calendarIntervalTriggerSummary)) + .andWithPrefix("triggers.custom.*.", concat(triggerSummary, customTriggerSummary)))); + } + + @Test + void quartzJob() throws Exception { + mockJobs(jobOne); + CronTrigger firstTrigger = cronTrigger.getTriggerBuilder().build(); + setPreviousNextFireTime(firstTrigger, null, "2020-12-07T03:00:00Z"); + SimpleTrigger secondTrigger = simpleTrigger.getTriggerBuilder().build(); + setPreviousNextFireTime(secondTrigger, "2020-12-04T03:00:00Z", "2020-12-04T12:00:00Z"); + mockTriggers(firstTrigger, secondTrigger); + given(this.scheduler.getTriggersOfJob(jobOne.getKey())) + .willAnswer((invocation) -> Arrays.asList(firstTrigger, secondTrigger)); + this.mockMvc.perform(get("/actuator/quartz/jobs/samples/jobOne")).andExpect(status().isOk()).andDo(document( + "quartz/job-details", + responseFields(fieldWithPath("group").description("Name of the group."), + fieldWithPath("name").description("Name of the job."), + fieldWithPath("description").description("Description of the job, if any."), + fieldWithPath("className").description("Fully qualified name of the job implementation."), + fieldWithPath("durable") + .description("Whether the job should remain stored after it is orphaned."), + fieldWithPath("requestRecovery").description( + "Whether the job should be re-executed if a 'recovery' or 'fail-over' situation is encountered."), + fieldWithPath("data.*").description("Job data map as key/value pairs, if any."), + fieldWithPath("triggers").description("An array of triggers associated to the job, if any."), + fieldWithPath("triggers.[].group").description("Name of the the trigger group."), + fieldWithPath("triggers.[].name").description("Name of the the trigger."), + previousFireTime("triggers.[]."), nextFireTime("triggers.[]."), priority("triggers.[].")))); + } + + @Test + void quartzTriggerCommon() throws Exception { + setupTriggerDetails(cronTrigger.getTriggerBuilder(), TriggerState.NORMAL); + this.mockMvc.perform(get("/actuator/quartz/triggers/samples/example")).andExpect(status().isOk()) + .andDo(document("quartz/trigger-details-common", responseFields(commonCronDetails).and( + subsectionWithPath("calendarInterval").description( + "Calendar time interval trigger details, if any. Present when `type` is `calendarInterval`.") + .optional().type(JsonFieldType.OBJECT), + subsectionWithPath("custom") + .description("Custom trigger details, if any. Present when `type` is `custom`.") + .optional().type(JsonFieldType.OBJECT), + subsectionWithPath("cron") + .description("Cron trigger details, if any. Present when `type` is `cron`.").optional() + .type(JsonFieldType.OBJECT), + subsectionWithPath("dailyTimeInterval").description( + "Daily time interval trigger details, if any. Present when `type` is `dailyTimeInterval`.") + .optional().type(JsonFieldType.OBJECT), + subsectionWithPath("simple") + .description("Simple trigger details, if any. Present when `type` is `simple`.") + .optional().type(JsonFieldType.OBJECT)))); + } + + @Test + void quartzTriggerCron() throws Exception { + setupTriggerDetails(cronTrigger.getTriggerBuilder(), TriggerState.NORMAL); + this.mockMvc.perform(get("/actuator/quartz/triggers/samples/example")).andExpect(status().isOk()) + .andDo(document("quartz/trigger-details-cron", + relaxedResponseFields(fieldWithPath("cron").description("Cron trigger specific details.")) + .andWithPrefix("cron.", cronTriggerSummary))); + } + + @Test + void quartzTriggerSimple() throws Exception { + setupTriggerDetails(simpleTrigger.getTriggerBuilder(), TriggerState.NORMAL); + this.mockMvc.perform(get("/actuator/quartz/triggers/samples/example")).andExpect(status().isOk()) + .andDo(document("quartz/trigger-details-simple", + relaxedResponseFields(fieldWithPath("simple").description("Simple trigger specific details.")) + .andWithPrefix("simple.", simpleTriggerSummary) + .and(repeatCount("simple."), timesTriggered("simple.")))); + } + + @Test + void quartzTriggerCalendarInterval() throws Exception { + setupTriggerDetails(calendarIntervalTrigger.getTriggerBuilder(), TriggerState.NORMAL); + this.mockMvc.perform(get("/actuator/quartz/triggers/samples/example")).andExpect(status().isOk()) + .andDo(document("quartz/trigger-details-calendar-interval", relaxedResponseFields( + fieldWithPath("calendarInterval").description("Calendar interval trigger specific details.")) + .andWithPrefix("calendarInterval.", calendarIntervalTriggerSummary) + .and(timesTriggered("calendarInterval."), fieldWithPath( + "calendarInterval.preserveHourOfDayAcrossDaylightSavings").description( + "Whether to fire the trigger at the same time of day, regardless of daylight " + + "saving time transitions."), + fieldWithPath("calendarInterval.skipDayIfHourDoesNotExist").description( + "Whether to skip if the hour of the day does not exist on a given day.")))); + } + + @Test + void quartzTriggerDailyTimeInterval() throws Exception { + setupTriggerDetails(dailyTimeIntervalTrigger.getTriggerBuilder(), TriggerState.PAUSED); + this.mockMvc.perform(get("/actuator/quartz/triggers/samples/example")).andExpect(status().isOk()) + .andDo(document("quartz/trigger-details-daily-time-interval", + relaxedResponseFields(fieldWithPath("dailyTimeInterval") + .description("Daily time interval trigger specific details.")) + .andWithPrefix("dailyTimeInterval.", dailyTimeIntervalTriggerSummary) + .and(repeatCount("dailyTimeInterval."), timesTriggered("dailyTimeInterval.")))); + } + + @Test + void quartzTriggerCustom() throws Exception { + Trigger trigger = mock(Trigger.class); + given(trigger.getKey()).willReturn(TriggerKey.triggerKey("example", "samples")); + given(trigger.getDescription()).willReturn("Example trigger."); + given(trigger.toString()).willReturn("com.example.CustomTrigger@fdsfsd"); + given(trigger.getPriority()).willReturn(10); + given(trigger.getStartTime()).willReturn(fromUtc("2020-11-30T17:00:00Z")); + given(trigger.getEndTime()).willReturn(fromUtc("2020-12-30T03:00:00Z")); + given(trigger.getCalendarName()).willReturn("bankHolidays"); + given(trigger.getPreviousFireTime()).willReturn(fromUtc("2020-12-04T03:00:00Z")); + given(trigger.getNextFireTime()).willReturn(fromUtc("2020-12-07T03:00:00Z")); + given(this.scheduler.getTriggerState(trigger.getKey())).willReturn(TriggerState.NORMAL); + mockTriggers(trigger); + this.mockMvc.perform(get("/actuator/quartz/triggers/samples/example")).andExpect(status().isOk()) + .andDo(document("quartz/trigger-details-custom", + relaxedResponseFields(fieldWithPath("custom").description("Custom trigger specific details.")) + .andWithPrefix("custom.", customTriggerSummary))); + } + + private T setupTriggerDetails(TriggerBuilder builder, TriggerState state) + throws SchedulerException { + T trigger = builder.withIdentity("example", "samples").withDescription("Example trigger") + .startAt(fromUtc("2020-11-30T17:00:00Z")).modifiedByCalendar("bankHolidays") + .endAt(fromUtc("2020-12-30T03:00:00Z")).build(); + setPreviousNextFireTime(trigger, "2020-12-04T03:00:00Z", "2020-12-07T03:00:00Z"); + given(this.scheduler.getTriggerState(trigger.getKey())).willReturn(state); + mockTriggers(trigger); + return trigger; + } + + private static FieldDescriptor startTime(String prefix) { + return fieldWithPath(prefix + "startTime").description("Time at which the Trigger should take effect, if any."); + } + + private static FieldDescriptor endTime(String prefix) { + return fieldWithPath(prefix + "endTime").description( + "Time at which the Trigger should quit repeating, regardless of any remaining repeats, if any."); + } + + private static FieldDescriptor previousFireTime(String prefix) { + return fieldWithPath(prefix + "previousFireTime").optional().type(JsonFieldType.STRING) + .description("Last time the trigger fired, if any."); + } + + private static FieldDescriptor nextFireTime(String prefix) { + return fieldWithPath(prefix + "nextFireTime").optional().type(JsonFieldType.STRING) + .description("Next time at which the Trigger is scheduled to fire, if any."); + } + + private static FieldDescriptor priority(String prefix) { + return fieldWithPath(prefix + "priority") + .description("Priority to use if two triggers have the same scheduled fire time."); + } + + private static FieldDescriptor repeatCount(String prefix) { + return fieldWithPath(prefix + "repeatCount") + .description("Number of times the trigger should repeat, or -1 to repeat indefinitely."); + } + + private static FieldDescriptor timesTriggered(String prefix) { + return fieldWithPath(prefix + "timesTriggered").description("Number of times the trigger has already fired."); + } + + private static List concat(List initial, List additionalFields) { + List result = new ArrayList<>(initial); + result.addAll(additionalFields); + return result; + } + + private void mockJobs(JobDetail... jobs) throws SchedulerException { + MultiValueMap jobKeys = new LinkedMultiValueMap<>(); + for (JobDetail jobDetail : jobs) { + JobKey key = jobDetail.getKey(); + given(this.scheduler.getJobDetail(key)).willReturn(jobDetail); + jobKeys.add(key.getGroup(), key); + } + given(this.scheduler.getJobGroupNames()).willReturn(new ArrayList<>(jobKeys.keySet())); + for (Entry> entry : jobKeys.entrySet()) { + given(this.scheduler.getJobKeys(GroupMatcher.jobGroupEquals(entry.getKey()))) + .willReturn(new LinkedHashSet<>(entry.getValue())); + } + } + + private void mockTriggers(Trigger... triggers) throws SchedulerException { + MultiValueMap triggerKeys = new LinkedMultiValueMap<>(); + for (Trigger trigger : triggers) { + TriggerKey key = trigger.getKey(); + given(this.scheduler.getTrigger(key)).willReturn(trigger); + triggerKeys.add(key.getGroup(), key); + } + given(this.scheduler.getTriggerGroupNames()).willReturn(new ArrayList<>(triggerKeys.keySet())); + for (Entry> entry : triggerKeys.entrySet()) { + given(this.scheduler.getTriggerKeys(GroupMatcher.triggerGroupEquals(entry.getKey()))) + .willReturn(new LinkedHashSet<>(entry.getValue())); + } + } + + private T setPreviousNextFireTime(T trigger, String previousFireTime, String nextFireTime) { + OperableTrigger operableTrigger = (OperableTrigger) trigger; + if (previousFireTime != null) { + operableTrigger.setPreviousFireTime(fromUtc(previousFireTime)); + } + if (nextFireTime != null) { + operableTrigger.setNextFireTime(fromUtc(nextFireTime)); + } + return trigger; + } + + private static Date fromUtc(String utcTime) { + return Date.from(Instant.parse(utcTime)); + } + + @Configuration(proxyBeanMethods = false) + @Import(BaseDocumentationConfiguration.class) + static class TestConfiguration { + + @Bean + QuartzEndpoint endpoint(Scheduler scheduler) { + return new QuartzEndpoint(scheduler); + } + + @Bean + QuartzEndpointWebExtension endpointWebExtension(QuartzEndpoint endpoint) { + return new QuartzEndpointWebExtension(endpoint); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/ScheduledTasksEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/ScheduledTasksEndpointDocumentationTests.java index 03d946837aec..a056ca55d3a1 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/ScheduledTasksEndpointDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/ScheduledTasksEndpointDocumentationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/SessionsEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/SessionsEndpointDocumentationTests.java index 97205d9b7b5c..d2282f986d46 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/SessionsEndpointDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/SessionsEndpointDocumentationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,7 @@ import org.springframework.test.context.TestPropertySource; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.verify; +import static org.mockito.BDDMockito.then; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; @@ -93,10 +93,6 @@ void sessionsForUsername() throws Exception { @Test void sessionWithId() throws Exception { - Map sessions = new HashMap<>(); - sessions.put(sessionOne.getId(), sessionOne); - sessions.put(sessionTwo.getId(), sessionTwo); - sessions.put(sessionThree.getId(), sessionThree); given(this.sessionRepository.findById(sessionTwo.getId())).willReturn(sessionTwo); this.mockMvc.perform(get("/actuator/sessions/{id}", sessionTwo.getId())).andExpect(status().isOk()) .andDo(document("sessions/id", responseFields(sessionFields))); @@ -106,7 +102,7 @@ void sessionWithId() throws Exception { void deleteASession() throws Exception { this.mockMvc.perform(delete("/actuator/sessions/{id}", sessionTwo.getId())).andExpect(status().isNoContent()) .andDo(document("sessions/delete")); - verify(this.sessionRepository).deleteById(sessionTwo.getId()); + then(this.sessionRepository).should().deleteById(sessionTwo.getId()); } private static MapSession createSession(Instant creationTime, Instant lastAccessedTime) { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/StartupEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/StartupEndpointDocumentationTests.java new file mode 100644 index 000000000000..e2651b884f40 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/StartupEndpointDocumentationTests.java @@ -0,0 +1,107 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.endpoint.web.documentation; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.startup.StartupEndpoint; +import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.metrics.StartupStep; +import org.springframework.restdocs.payload.FieldDescriptor; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.restdocs.payload.PayloadDocumentation; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Tests for generating documentation describing {@link StartupEndpoint}. + * + * @author Brian Clozel + * @author Stephane Nicoll + */ +class StartupEndpointDocumentationTests extends MockMvcEndpointDocumentationTests { + + @BeforeEach + void appendSampleStartupSteps(@Autowired BufferingApplicationStartup applicationStartup) { + StartupStep starting = applicationStartup.start("spring.boot.application.starting"); + starting.tag("mainApplicationClass", "com.example.startup.StartupApplication"); + StartupStep instantiate = applicationStartup.start("spring.beans.instantiate"); + instantiate.tag("beanName", "homeController"); + instantiate.end(); + starting.end(); + } + + @Test + void startupSnapshot() throws Exception { + this.mockMvc.perform(get("/actuator/startup")).andExpect(status().isOk()) + .andDo(document("startup-snapshot", PayloadDocumentation.responseFields(responseFields()))); + } + + @Test + void startup() throws Exception { + this.mockMvc.perform(post("/actuator/startup")).andExpect(status().isOk()) + .andDo(document("startup", PayloadDocumentation.responseFields(responseFields()))); + } + + private FieldDescriptor[] responseFields() { + return new FieldDescriptor[] { + fieldWithPath("springBootVersion").type(JsonFieldType.STRING) + .description("Spring Boot version for this application.").optional(), + fieldWithPath("timeline.startTime").description("Start time of the application."), + fieldWithPath("timeline.events") + .description("An array of steps collected during application startup so far."), + fieldWithPath("timeline.events.[].startTime").description("The timestamp of the start of this event."), + fieldWithPath("timeline.events.[].endTime").description("The timestamp of the end of this event."), + fieldWithPath("timeline.events.[].duration").description("The precise duration of this event."), + fieldWithPath("timeline.events.[].startupStep.name").description("The name of the StartupStep."), + fieldWithPath("timeline.events.[].startupStep.id").description("The id of this StartupStep."), + fieldWithPath("timeline.events.[].startupStep.parentId") + .description("The parent id for this StartupStep.").optional(), + fieldWithPath("timeline.events.[].startupStep.tags") + .description("An array of key/value pairs with additional step info."), + fieldWithPath("timeline.events.[].startupStep.tags[].key") + .description("The key of the StartupStep Tag."), + fieldWithPath("timeline.events.[].startupStep.tags[].value") + .description("The value of the StartupStep Tag.") }; + } + + @Configuration(proxyBeanMethods = false) + @Import(BaseDocumentationConfiguration.class) + static class TestConfiguration { + + @Bean + StartupEndpoint startupEndpoint(BufferingApplicationStartup startup) { + return new StartupEndpoint(startup); + } + + @Bean + BufferingApplicationStartup bufferingApplicationStartup() { + return new BufferingApplicationStartup(16); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/ThreadDumpEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/ThreadDumpEndpointDocumentationTests.java index a95b7d1dc65a..a64a99acbaa4 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/ThreadDumpEndpointDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/ThreadDumpEndpointDocumentationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointIntegrationTests.java new file mode 100644 index 000000000000..03d8f497ae2c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointIntegrationTests.java @@ -0,0 +1,75 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.endpoint.web.jersey; + +import java.util.Set; + +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.model.Resource; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.web.jersey.JerseySameManagementContextConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.DispatcherServlet; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for web endpoints running on Jersey. + * + * @author Andy Wilkinson + */ +class JerseyWebEndpointIntegrationTests { + + @Test + void whenJerseyIsConfiguredToUseAFilterThenResourceRegistrationSucceeds() { + new WebApplicationContextRunner(AnnotationConfigServletWebServerApplicationContext::new) + .withConfiguration(AutoConfigurations.of(JerseySameManagementContextConfiguration.class, + JerseyAutoConfiguration.class, ServletWebServerFactoryAutoConfiguration.class, + EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, + JerseyWebEndpointManagementContextConfiguration.class)) + .withUserConfiguration(ResourceConfigConfiguration.class) + .withClassLoader(new FilteredClassLoader(DispatcherServlet.class)) + .withPropertyValues("spring.jersey.type=filter", "server.port=0").run((context) -> { + assertThat(context).hasNotFailed(); + Set resources = context.getBean(ResourceConfig.class).getResources(); + assertThat(resources).hasSize(1); + Resource resource = resources.iterator().next(); + assertThat(resource.getPath()).isEqualTo("/actuator"); + }); + } + + @Configuration(proxyBeanMethods = false) + static class ResourceConfigConfiguration { + + @Bean + ResourceConfig resourceConfig() { + return new ResourceConfig(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfigurationTests.java index 5e161080ed5e..475cbd073a61 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointAutoConfigurationTests.java index 6965b59bb88f..0115bbccd6bd 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/env/EnvironmentEndpointAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -67,6 +67,14 @@ void keysToSanitizeCanBeConfiguredViaTheEnvironment() { .run(validateSystemProperties("******", "123456")); } + @Test + void additionalKeysToSanitizeCanBeConfiguredViaTheEnvironment() { + this.contextRunner.withPropertyValues("management.endpoints.web.exposure.include=env") + .withSystemProperties("dbPassword=123456", "apiKey=123456") + .withPropertyValues("management.endpoint.env.additional-keys-to-sanitize=key") + .run(validateSystemProperties("******", "******")); + } + private ContextConsumer validateSystemProperties(String dbPassword, String apiKey) { return (context) -> { assertThat(context).hasSingleBean(EnvironmentEndpoint.class); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/hazelcast/HazelcastHealthContributorAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/hazelcast/HazelcastHealthContributorAutoConfigurationIntegrationTests.java index 7c2cee8380d7..51b6f0128bb3 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/hazelcast/HazelcastHealthContributorAutoConfigurationIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/hazelcast/HazelcastHealthContributorAutoConfigurationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,7 @@ */ class HazelcastHealthContributorAutoConfigurationIntegrationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(HazelcastHealthContributorAutoConfiguration.class, HazelcastAutoConfiguration.class, HealthContributorAutoConfiguration.class)); @@ -48,7 +48,7 @@ void hazelcastUp() { Health health = context.getBean(HazelcastHealthIndicator.class).health(); assertThat(health.getStatus()).isEqualTo(Status.UP); assertThat(health.getDetails()).containsOnlyKeys("name", "uuid").containsEntry("name", hazelcast.getName()) - .containsEntry("uuid", hazelcast.getLocalEndpoint().getUuid()); + .containsEntry("uuid", hazelcast.getLocalEndpoint().getUuid().toString()); }); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/hazelcast/HazelcastHealthContributorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/hazelcast/HazelcastHealthContributorAutoConfigurationTests.java index 6f388a830b98..0d0ea309bd06 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/hazelcast/HazelcastHealthContributorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/hazelcast/HazelcastHealthContributorAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ */ class HazelcastHealthContributorAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(HazelcastAutoConfiguration.class, HazelcastHealthContributorAutoConfiguration.class, HealthContributorAutoConfiguration.class)); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupTests.java index 8a31c44361d8..3c22e80dae0d 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,24 +20,28 @@ import java.util.Arrays; import java.util.Collections; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.boot.actuate.autoconfigure.health.HealthProperties.Show; import org.springframework.boot.actuate.endpoint.SecurityContext; import org.springframework.boot.actuate.health.HttpCodeStatusMapper; import org.springframework.boot.actuate.health.StatusAggregator; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; /** * Tests for {@link AutoConfiguredHealthEndpointGroup}. * * @author Phillip Webb */ +@ExtendWith(MockitoExtension.class) class AutoConfiguredHealthEndpointGroupTests { @Mock @@ -52,11 +56,6 @@ class AutoConfiguredHealthEndpointGroupTests { @Mock private Principal principal; - @BeforeEach - void setup() { - MockitoAnnotations.initMocks(this); - } - @Test void isMemberWhenMemberPredicateMatchesAcceptsTrue() { AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> name.startsWith("a"), @@ -109,17 +108,41 @@ void showDetailsWhenShowDetailsIsWhenAuthorizedAndUseIsInRoleReturnsTrue() { this.statusAggregator, this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED, Arrays.asList("admin", "root", "bossmode")); given(this.securityContext.getPrincipal()).willReturn(this.principal); + given(this.securityContext.isUserInRole("admin")).willReturn(false); given(this.securityContext.isUserInRole("root")).willReturn(true); assertThat(group.showDetails(this.securityContext)).isTrue(); } @Test - void showDetailsWhenShowDetailsIsWhenAuthorizedAndUseIsNotInRoleReturnsFalse() { + void showDetailsWhenShowDetailsIsWhenAuthorizedAndUserIsNotInRoleReturnsFalse() { AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true, this.statusAggregator, this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED, - Arrays.asList("admin", "rot", "bossmode")); + Arrays.asList("admin", "root", "bossmode")); given(this.securityContext.getPrincipal()).willReturn(this.principal); - given(this.securityContext.isUserInRole("root")).willReturn(true); + assertThat(group.showDetails(this.securityContext)).isFalse(); + } + + @Test + void showDetailsWhenShowDetailsIsWhenAuthorizedAndUserHasRightAuthorityReturnsTrue() { + AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true, + this.statusAggregator, this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED, + Arrays.asList("admin", "root", "bossmode")); + Authentication principal = mock(Authentication.class); + given(principal.getAuthorities()) + .willAnswer((invocation) -> Collections.singleton(new SimpleGrantedAuthority("admin"))); + given(this.securityContext.getPrincipal()).willReturn(principal); + assertThat(group.showDetails(this.securityContext)).isTrue(); + } + + @Test + void showDetailsWhenShowDetailsIsWhenAuthorizedAndUserDoesNotHaveRightAuthoritiesReturnsFalse() { + AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true, + this.statusAggregator, this.httpCodeStatusMapper, null, Show.WHEN_AUTHORIZED, + Arrays.asList("admin", "rot", "bossmode")); + Authentication principal = mock(Authentication.class); + given(principal.getAuthorities()) + .willAnswer((invocation) -> Collections.singleton(new SimpleGrantedAuthority("other"))); + given(this.securityContext.getPrincipal()).willReturn(principal); assertThat(group.showDetails(this.securityContext)).isFalse(); } @@ -134,21 +157,21 @@ void showComponentsWhenShowComponentsIsNullDelegatesToShowDetails() { } @Test - void showComponentsWhenShowDetailsIsNeverReturnsFalse() { + void showComponentsWhenShowComponentsIsNeverReturnsFalse() { AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true, this.statusAggregator, this.httpCodeStatusMapper, Show.NEVER, Show.ALWAYS, Collections.emptySet()); assertThat(group.showComponents(SecurityContext.NONE)).isFalse(); } @Test - void showComponentsWhenShowDetailsIsAlwaysReturnsTrue() { + void showComponentsWhenShowComponentsIsAlwaysReturnsTrue() { AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true, this.statusAggregator, this.httpCodeStatusMapper, Show.ALWAYS, Show.NEVER, Collections.emptySet()); assertThat(group.showComponents(SecurityContext.NONE)).isTrue(); } @Test - void showComponentsWhenShowDetailsIsWhenAuthorizedAndPrincipalIsNullReturnsFalse() { + void showComponentsWhenShowComponentsIsWhenAuthorizedAndPrincipalIsNullReturnsFalse() { AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true, this.statusAggregator, this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER, Collections.emptySet()); @@ -157,7 +180,7 @@ void showComponentsWhenShowDetailsIsWhenAuthorizedAndPrincipalIsNullReturnsFalse } @Test - void showComponentsWhenShowDetailsIsWhenAuthorizedAndRolesAreEmptyReturnsTrue() { + void showComponentsWhenShowComponentsIsWhenAuthorizedAndRolesAreEmptyReturnsTrue() { AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true, this.statusAggregator, this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER, Collections.emptySet()); @@ -166,22 +189,46 @@ void showComponentsWhenShowDetailsIsWhenAuthorizedAndRolesAreEmptyReturnsTrue() } @Test - void showComponentsWhenShowDetailsIsWhenAuthorizedAndUseIsInRoleReturnsTrue() { + void showComponentsWhenShowComponentsIsWhenAuthorizedAndUseIsInRoleReturnsTrue() { AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true, this.statusAggregator, this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER, Arrays.asList("admin", "root", "bossmode")); given(this.securityContext.getPrincipal()).willReturn(this.principal); + given(this.securityContext.isUserInRole("admin")).willReturn(false); given(this.securityContext.isUserInRole("root")).willReturn(true); assertThat(group.showComponents(this.securityContext)).isTrue(); } @Test - void showComponentsWhenShowDetailsIsWhenAuthorizedAndUseIsNotInRoleReturnsFalse() { + void showComponentsWhenShowComponentsIsWhenAuthorizedAndUserIsNotInRoleReturnsFalse() { AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true, this.statusAggregator, this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER, Arrays.asList("admin", "rot", "bossmode")); given(this.securityContext.getPrincipal()).willReturn(this.principal); - given(this.securityContext.isUserInRole("root")).willReturn(true); + assertThat(group.showComponents(this.securityContext)).isFalse(); + } + + @Test + void showComponentsWhenShowComponentsIsWhenAuthorizedAndUserHasRightAuthoritiesReturnsTrue() { + AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true, + this.statusAggregator, this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER, + Arrays.asList("admin", "root", "bossmode")); + Authentication principal = mock(Authentication.class); + given(principal.getAuthorities()) + .willAnswer((invocation) -> Collections.singleton(new SimpleGrantedAuthority("admin"))); + given(this.securityContext.getPrincipal()).willReturn(principal); + assertThat(group.showComponents(this.securityContext)).isTrue(); + } + + @Test + void showComponentsWhenShowComponentsIsWhenAuthorizedAndUserDoesNotHaveRightAuthoritiesReturnsFalse() { + AutoConfiguredHealthEndpointGroup group = new AutoConfiguredHealthEndpointGroup((name) -> true, + this.statusAggregator, this.httpCodeStatusMapper, Show.WHEN_AUTHORIZED, Show.NEVER, + Arrays.asList("admin", "rot", "bossmode")); + Authentication principal = mock(Authentication.class); + given(principal.getAuthorities()) + .willAnswer((invocation) -> Collections.singleton(new SimpleGrantedAuthority("other"))); + given(this.securityContext.getPrincipal()).willReturn(principal); assertThat(group.showComponents(this.securityContext)).isFalse(); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupsTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupsTests.java index b63154031a4a..36ac40b30839 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupsTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/AutoConfiguredHealthEndpointGroupsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,6 +43,7 @@ * Tests for {@link AutoConfiguredHealthEndpointGroups}. * * @author Phillip Webb + * @author Leo Li */ class AutoConfiguredHealthEndpointGroupsTests { @@ -103,7 +104,7 @@ void createWhenNoDefinedBeansAdaptsProperties() { } @Test - void createWhenHasStatusAggregatorBeanReturnsInstanceWithAgregatorUsedForAllGroups() { + void createWhenHasStatusAggregatorBeanReturnsInstanceWithAggregatorUsedForAllGroups() { this.contextRunner.withUserConfiguration(CustomStatusAggregatorConfiguration.class) .withPropertyValues("management.endpoint.health.status.order=up,down", "management.endpoint.health.group.a.include=*") @@ -308,6 +309,16 @@ void createWhenHasGroupSpecificHttpCodeStatusMapperPropertyAndGroupQualifiedBean }); } + @Test + void createWhenGroupWithNoShowDetailsOverrideInheritsShowDetails() { + this.contextRunner.withPropertyValues("management.endpoint.health.show-details=always", + "management.endpoint.health.group.a.include=*").run((context) -> { + HealthEndpointGroups groups = context.getBean(HealthEndpointGroups.class); + HealthEndpointGroup groupA = groups.get("a"); + assertThat(groupA.showDetails(SecurityContext.NONE)).isTrue(); + }); + } + @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(HealthEndpointProperties.class) static class AutoConfiguredHealthEndpointGroupsTestConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthAggregatorStatusAggregatorAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthAggregatorStatusAggregatorAdapterTests.java deleted file mode 100644 index 6aa7be19c912..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthAggregatorStatusAggregatorAdapterTests.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.autoconfigure.health; - -import java.util.List; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.actuate.health.AbstractHealthAggregator; -import org.springframework.boot.actuate.health.Status; -import org.springframework.boot.actuate.health.StatusAggregator; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link HealthAggregatorStatusAggregatorAdapter}. - * - * @author Phillip Webb - */ -@SuppressWarnings("deprecation") -class HealthAggregatorStatusAggregatorAdapterTests { - - @Test - void getAggregateStatusDelegateToHealthAggregator() { - StatusAggregator adapter = new HealthAggregatorStatusAggregatorAdapter(new TestHealthAggregator()); - Status status = adapter.getAggregateStatus(Status.UP, Status.DOWN); - assertThat(status.getCode()).isEqualTo("called2"); - } - - private static class TestHealthAggregator extends AbstractHealthAggregator { - - @Override - protected Status aggregateStatus(List candidates) { - return new Status("called" + candidates.size()); - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthContributorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthContributorAutoConfigurationTests.java index 88d4f391480b..78e7fa9b03aa 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthContributorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthContributorAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,7 @@ */ class HealthContributorAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(HealthContributorAutoConfiguration.class)); @Test diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthContributorRegistryHealthIndicatorRegistryAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthContributorRegistryHealthIndicatorRegistryAdapterTests.java deleted file mode 100644 index 9f93c2311593..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthContributorRegistryHealthIndicatorRegistryAdapterTests.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.autoconfigure.health; - -import java.util.Map; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.actuate.health.CompositeHealthContributor; -import org.springframework.boot.actuate.health.DefaultHealthContributorRegistry; -import org.springframework.boot.actuate.health.HealthContributorRegistry; -import org.springframework.boot.actuate.health.HealthIndicator; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.entry; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link HealthContributorRegistryHealthIndicatorRegistryAdapter}. - * - * @author Phillip Webb - */ -class HealthContributorRegistryHealthIndicatorRegistryAdapterTests { - - private HealthContributorRegistry contributorRegistry = new DefaultHealthContributorRegistry(); - - private HealthContributorRegistryHealthIndicatorRegistryAdapter adapter = new HealthContributorRegistryHealthIndicatorRegistryAdapter( - this.contributorRegistry); - - @Test - void createWhenContributorRegistryIsNullThrowsException() { - assertThatIllegalArgumentException() - .isThrownBy(() -> new HealthContributorRegistryHealthIndicatorRegistryAdapter(null)) - .withMessage("ContributorRegistry must not be null"); - } - - @Test - void registerDelegatesToContributorRegistry() { - HealthIndicator healthIndicator = mock(HealthIndicator.class); - this.adapter.register("test", healthIndicator); - assertThat(this.contributorRegistry.getContributor("test")).isSameAs(healthIndicator); - } - - @Test - void unregisterDelegatesToContributorRegistry() { - HealthIndicator healthIndicator = mock(HealthIndicator.class); - this.contributorRegistry.registerContributor("test", healthIndicator); - HealthIndicator unregistered = this.adapter.unregister("test"); - assertThat(unregistered).isSameAs(healthIndicator); - assertThat(this.contributorRegistry.getContributor("test")).isNull(); - } - - @Test - void unregisterWhenContributorRegistryResultIsNotHealthIndicatorReturnsNull() { - CompositeHealthContributor healthContributor = mock(CompositeHealthContributor.class); - this.contributorRegistry.registerContributor("test", healthContributor); - HealthIndicator unregistered = this.adapter.unregister("test"); - assertThat(unregistered).isNull(); - assertThat(this.contributorRegistry.getContributor("test")).isNull(); - } - - @Test - void getDelegatesToContributorRegistry() { - HealthIndicator healthIndicator = mock(HealthIndicator.class); - this.contributorRegistry.registerContributor("test", healthIndicator); - assertThat(this.adapter.get("test")).isSameAs(healthIndicator); - } - - @Test - void getWhenContributorRegistryResultIsNotHealthIndicatorReturnsNull() { - CompositeHealthContributor healthContributor = mock(CompositeHealthContributor.class); - this.contributorRegistry.registerContributor("test", healthContributor); - assertThat(this.adapter.get("test")).isNull(); - } - - @Test - void getAllDelegatesToContributorRegistry() { - HealthIndicator healthIndicator = mock(HealthIndicator.class); - this.contributorRegistry.registerContributor("test", healthIndicator); - Map all = this.adapter.getAll(); - assertThat(all).containsOnly(entry("test", healthIndicator)); - } - - @Test - void getAllWhenContributorRegistryContainsNonHealthIndicatorInstancesReturnsFilteredMap() { - CompositeHealthContributor healthContributor = mock(CompositeHealthContributor.class); - this.contributorRegistry.registerContributor("test1", healthContributor); - HealthIndicator healthIndicator = mock(HealthIndicator.class); - this.contributorRegistry.registerContributor("test2", healthIndicator); - Map all = this.adapter.getAll(); - assertThat(all).containsOnly(entry("test2", healthIndicator)); - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointAutoConfigurationTests.java index 092d6cdee098..d3f134ea6ad5 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,35 +17,29 @@ package org.springframework.boot.actuate.autoconfigure.health; import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.boot.actuate.endpoint.ApiVersion; import org.springframework.boot.actuate.endpoint.SecurityContext; -import org.springframework.boot.actuate.endpoint.http.ApiVersion; import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; -import org.springframework.boot.actuate.health.AbstractHealthAggregator; import org.springframework.boot.actuate.health.DefaultHealthContributorRegistry; import org.springframework.boot.actuate.health.DefaultReactiveHealthContributorRegistry; import org.springframework.boot.actuate.health.Health; -import org.springframework.boot.actuate.health.HealthAggregator; import org.springframework.boot.actuate.health.HealthComponent; import org.springframework.boot.actuate.health.HealthContributorRegistry; import org.springframework.boot.actuate.health.HealthEndpoint; import org.springframework.boot.actuate.health.HealthEndpointGroups; +import org.springframework.boot.actuate.health.HealthEndpointGroupsPostProcessor; import org.springframework.boot.actuate.health.HealthEndpointWebExtension; import org.springframework.boot.actuate.health.HealthIndicator; -import org.springframework.boot.actuate.health.HealthStatusHttpMapper; import org.springframework.boot.actuate.health.HttpCodeStatusMapper; import org.springframework.boot.actuate.health.NamedContributor; import org.springframework.boot.actuate.health.ReactiveHealthContributorRegistry; import org.springframework.boot.actuate.health.ReactiveHealthEndpointWebExtension; import org.springframework.boot.actuate.health.ReactiveHealthIndicator; -import org.springframework.boot.actuate.health.ReactiveHealthIndicatorRegistry; import org.springframework.boot.actuate.health.Status; import org.springframework.boot.actuate.health.StatusAggregator; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -56,6 +50,7 @@ import org.springframework.context.annotation.Configuration; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -65,15 +60,15 @@ * @author Phillip Webb * @author Andy Wilkinson * @author Stephane Nicoll + * @author Scott Frederick */ -@SuppressWarnings("deprecation") class HealthEndpointAutoConfigurationTests { - private WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() .withUserConfiguration(HealthIndicatorsConfiguration.class).withConfiguration(AutoConfigurations .of(HealthContributorAutoConfiguration.class, HealthEndpointAutoConfiguration.class)); - private ReactiveWebApplicationContextRunner reactiveContextRunner = new ReactiveWebApplicationContextRunner() + private final ReactiveWebApplicationContextRunner reactiveContextRunner = new ReactiveWebApplicationContextRunner() .withUserConfiguration(HealthIndicatorsConfiguration.class).withConfiguration(AutoConfigurations .of(HealthContributorAutoConfiguration.class, HealthEndpointAutoConfiguration.class)); @@ -91,22 +86,6 @@ void runWhenHealthEndpointIsDisabledDoesNotCreateBeans() { }); } - @Test - void runWhenHasHealthAggregatorAdaptsToStatusAggregator() { - this.contextRunner.withUserConfiguration(HealthAggregatorConfiguration.class).run((context) -> { - StatusAggregator aggregator = context.getBean(StatusAggregator.class); - assertThat(aggregator.getAggregateStatus(Status.UP, Status.DOWN)).isEqualTo(Status.UNKNOWN); - }); - } - - @Test - void runWhenHasHealthStatusHttpMapperAdaptsToHttpCodeStatusMapper() { - this.contextRunner.withUserConfiguration(HealthStatusHttpMapperConfiguration.class).run((context) -> { - HttpCodeStatusMapper mapper = context.getBean(HttpCodeStatusMapper.class); - assertThat(mapper.getStatusCode(Status.UP)).isEqualTo(123); - }); - } - @Test void runCreatesStatusAggregatorFromProperties() { this.contextRunner.withPropertyValues("management.endpoint.health.status.order=up,down").run((context) -> { @@ -115,18 +94,10 @@ void runCreatesStatusAggregatorFromProperties() { }); } - @Test - void runWhenUsingDeprecatedPropertyCreatesStatusAggregatorFromProperties() { - this.contextRunner.withPropertyValues("management.health.status.order=up,down").run((context) -> { - StatusAggregator aggregator = context.getBean(StatusAggregator.class); - assertThat(aggregator.getAggregateStatus(Status.UP, Status.DOWN)).isEqualTo(Status.UP); - }); - } - @Test void runWhenHasStatusAggregatorBeanIgnoresProperties() { this.contextRunner.withUserConfiguration(StatusAggregatorConfiguration.class) - .withPropertyValues("management.health.status.order=up,down").run((context) -> { + .withPropertyValues("management.endpoint.health.status.order=up,down").run((context) -> { StatusAggregator aggregator = context.getBean(StatusAggregator.class); assertThat(aggregator.getAggregateStatus(Status.UP, Status.DOWN)).isEqualTo(Status.UNKNOWN); }); @@ -141,14 +112,6 @@ void runCreatesHttpCodeStatusMapperFromProperties() { }); } - @Test - void runUsingDeprecatedPropertyCreatesHttpCodeStatusMapperFromProperties() { - this.contextRunner.withPropertyValues("management.health.status.http-mapping.up=123").run((context) -> { - HttpCodeStatusMapper mapper = context.getBean(HttpCodeStatusMapper.class); - assertThat(mapper.getStatusCode(Status.UP)).isEqualTo(123); - }); - } - @Test void runWhenHasHttpCodeStatusMapperBeanIgnoresProperties() { this.contextRunner.withUserConfiguration(HttpCodeStatusMapperConfiguration.class) @@ -286,35 +249,14 @@ void runWhenHasReactiveHealthEndpointWebExtensionBeanDoesNotCreateExtraReactiveH }); } - @Test // gh-18354 - void runCreatesLegacyHealthAggregator() { - this.contextRunner.run((context) -> { - HealthAggregator aggregator = context.getBean(HealthAggregator.class); - Map healths = new LinkedHashMap<>(); - healths.put("one", Health.up().build()); - healths.put("two", Health.down().build()); - Health result = aggregator.aggregate(healths); - assertThat(result.getStatus()).isEqualTo(Status.DOWN); - }); - } - - @Test // gh-18354 - void runCreatesLegacyHealthStatusHttpMapper() { - this.contextRunner.run((context) -> { - HealthStatusHttpMapper mapper = context.getBean(HealthStatusHttpMapper.class); - assertThat(mapper.mapStatus(Status.DOWN)).isEqualTo(503); - }); - } - @Test - void runWhenReactorAvailableCreatesReactiveHealthIndicatorRegistryBean() { - this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ReactiveHealthIndicatorRegistry.class)); - } - - @Test // gh-18570 - void runWhenReactorUnavailableDoesNotCreateReactiveHealthIndicatorRegistryBean() { - this.contextRunner.withClassLoader(new FilteredClassLoader(Mono.class.getPackage().getName())) - .run((context) -> assertThat(context).doesNotHaveBean(ReactiveHealthIndicatorRegistry.class)); + void runWhenHasHealthEndpointGroupsPostProcessorPerformsProcessing() { + this.contextRunner.withPropertyValues("management.endpoint.health.group.ready.include=*").withUserConfiguration( + HealthEndpointGroupsConfiguration.class, TestHealthEndpointGroupsPostProcessor.class).run((context) -> { + HealthEndpointGroups groups = context.getBean(HealthEndpointGroups.class); + assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> groups.get("test")) + .withMessage("postprocessed"); + }); } @Configuration(proxyBeanMethods = false) @@ -337,40 +279,6 @@ ReactiveHealthIndicator reactiveHealthIndicator() { } - @Configuration(proxyBeanMethods = false) - static class HealthAggregatorConfiguration { - - @Bean - HealthAggregator healthAggregator() { - return new AbstractHealthAggregator() { - - @Override - protected Status aggregateStatus(List candidates) { - return Status.UNKNOWN; - } - - }; - } - - } - - @Configuration(proxyBeanMethods = false) - static class HealthStatusHttpMapperConfiguration { - - @Bean - HealthStatusHttpMapper healthStatusHttpMapper() { - return new HealthStatusHttpMapper() { - - @Override - public int mapStatus(Status status) { - return 123; - } - - }; - } - - } - @Configuration(proxyBeanMethods = false) static class StatusAggregatorConfiguration { @@ -453,4 +361,14 @@ ReactiveHealthEndpointWebExtension reactiveHealthEndpointWebExtension() { } + static class TestHealthEndpointGroupsPostProcessor implements HealthEndpointGroupsPostProcessor { + + @Override + public HealthEndpointGroups postProcessHealthEndpointGroups(HealthEndpointGroups groups) { + given(groups.get("test")).willThrow(new RuntimeException("postprocessed")); + return groups; + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorRegistryInjectionIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorRegistryInjectionIntegrationTests.java deleted file mode 100644 index 1b8d8f77c85d..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorRegistryInjectionIntegrationTests.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.autoconfigure.health; - -import io.micrometer.core.instrument.Gauge; -import io.micrometer.core.instrument.MeterRegistry; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; -import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; -import org.springframework.boot.actuate.health.CompositeHealthIndicator; -import org.springframework.boot.actuate.health.HealthAggregator; -import org.springframework.boot.actuate.health.HealthIndicatorRegistry; -import org.springframework.boot.actuate.health.Status; -import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.context.annotation.Configuration; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Integration test to ensure that the legacy {@link HealthIndicatorRegistry} can still be - * injected. - * - * @author Phillip Webb - */ -@SuppressWarnings("deprecation") -@SpringBootTest(webEnvironment = WebEnvironment.NONE) -public class HealthIndicatorRegistryInjectionIntegrationTests { - - // gh-18194 - - @Test - void meterRegistryBeanHasBeenConfigured(@Autowired MeterRegistry meterRegistry) { - assertThat(meterRegistry).isNotNull(); - assertThat(meterRegistry.get("health").gauge()).isNotNull(); - } - - @Configuration - @ImportAutoConfiguration({ HealthEndpointAutoConfiguration.class, HealthContributorAutoConfiguration.class, - CompositeMeterRegistryAutoConfiguration.class, MetricsAutoConfiguration.class }) - static class Config { - - Config(HealthAggregator healthAggregator, HealthIndicatorRegistry healthIndicatorRegistry, - MeterRegistry registry) { - CompositeHealthIndicator healthIndicator = new CompositeHealthIndicator(healthAggregator, - healthIndicatorRegistry); - Gauge.builder("health", healthIndicator, this::getGuageValue) - .description("Spring boot health indicator. 3=UP, 2=OUT_OF_SERVICE, 1=DOWN, 0=UNKNOWN") - .strongReference(true).register(registry); - } - - private double getGuageValue(CompositeHealthIndicator health) { - Status status = health.health().getStatus(); - switch (status.getCode()) { - case "UP": - return 3; - case "OUT_OF_SERVICE": - return 2; - case "DOWN": - return 1; - case "UNKNOWN": - default: - return 0; - } - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthStatusHttpMapperHttpCodeStatusMapperAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthStatusHttpMapperHttpCodeStatusMapperAdapterTests.java deleted file mode 100644 index 32bdeb8863a2..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/HealthStatusHttpMapperHttpCodeStatusMapperAdapterTests.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.autoconfigure.health; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.actuate.health.HealthStatusHttpMapper; -import org.springframework.boot.actuate.health.HttpCodeStatusMapper; -import org.springframework.boot.actuate.health.Status; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link HealthStatusHttpMapperHttpCodeStatusMapperAdapter}. - * - * @author Phillip Webb - */ -@SuppressWarnings("deprecation") -class HealthStatusHttpMapperHttpCodeStatusMapperAdapterTests { - - @Test - void getStatusCodeDelegatesToHealthStatusHttpMapper() { - HttpCodeStatusMapper adapter = new HealthStatusHttpMapperHttpCodeStatusMapperAdapter( - new TestHealthStatusHttpMapper()); - assertThat(adapter.getStatusCode(Status.UP)).isEqualTo(123); - } - - static class TestHealthStatusHttpMapper extends HealthStatusHttpMapper { - - @Override - public int mapStatus(Status status) { - return 123; - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/IncludeExcludeGroupMemberPredicateTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/IncludeExcludeGroupMemberPredicateTests.java index 7944ac6251da..b61c3fa55644 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/IncludeExcludeGroupMemberPredicateTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/IncludeExcludeGroupMemberPredicateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.boot.actuate.autoconfigure.health; import java.util.Arrays; +import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; import java.util.function.Predicate; @@ -29,13 +30,14 @@ * Tests for {@link IncludeExcludeGroupMemberPredicate}. * * @author Phillip Webb + * @author Madhura Bhave */ class IncludeExcludeGroupMemberPredicateTests { @Test - void testWhenEmptyIncludeAndExcludeRejectsAll() { + void testWhenEmptyIncludeAndExcludeAcceptsAll() { Predicate predicate = new IncludeExcludeGroupMemberPredicate(null, null); - assertThat(predicate).rejects("a", "b", "c"); + assertThat(predicate).accepts("a", "b", "c"); } @Test @@ -44,6 +46,12 @@ void testWhenStarIncludeAndEmptyExcludeAcceptsAll() { assertThat(predicate).accepts("a", "b", "c"); } + @Test + void testWhenEmptyIncludeAndNonEmptyExcludeAcceptsAllButExclude() { + Predicate predicate = new IncludeExcludeGroupMemberPredicate(null, Collections.singleton("c")); + assertThat(predicate).accepts("a", "b"); + } + @Test void testWhenStarIncludeAndSpecificExcludeDoesNotAcceptExclude() { Predicate predicate = include("*").exclude("c"); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/ReactiveHealthContributorRegistryReactiveHealthIndicatorRegistryAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/ReactiveHealthContributorRegistryReactiveHealthIndicatorRegistryAdapterTests.java deleted file mode 100644 index ef89e9b24a0a..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/ReactiveHealthContributorRegistryReactiveHealthIndicatorRegistryAdapterTests.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.autoconfigure.health; - -import java.util.Map; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.actuate.health.CompositeReactiveHealthContributor; -import org.springframework.boot.actuate.health.DefaultReactiveHealthContributorRegistry; -import org.springframework.boot.actuate.health.ReactiveHealthContributorRegistry; -import org.springframework.boot.actuate.health.ReactiveHealthIndicator; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.entry; -import static org.mockito.Mockito.mock; - -/** - * Test for - * {@link ReactiveHealthContributorRegistryReactiveHealthIndicatorRegistryAdapter}. - * - * @author Phillip Webb - */ -class ReactiveHealthContributorRegistryReactiveHealthIndicatorRegistryAdapterTests { - - private ReactiveHealthContributorRegistry contributorRegistry = new DefaultReactiveHealthContributorRegistry(); - - private ReactiveHealthContributorRegistryReactiveHealthIndicatorRegistryAdapter adapter = new ReactiveHealthContributorRegistryReactiveHealthIndicatorRegistryAdapter( - this.contributorRegistry); - - @Test - void createWhenContributorRegistryIsNullThrowsException() { - assertThatIllegalArgumentException() - .isThrownBy(() -> new ReactiveHealthContributorRegistryReactiveHealthIndicatorRegistryAdapter(null)) - .withMessage("ContributorRegistry must not be null"); - } - - @Test - void registerDelegatesToContributorRegistry() { - ReactiveHealthIndicator healthIndicator = mock(ReactiveHealthIndicator.class); - this.adapter.register("test", healthIndicator); - assertThat(this.contributorRegistry.getContributor("test")).isSameAs(healthIndicator); - } - - @Test - void unregisterDelegatesToContributorRegistry() { - ReactiveHealthIndicator healthIndicator = mock(ReactiveHealthIndicator.class); - this.contributorRegistry.registerContributor("test", healthIndicator); - ReactiveHealthIndicator unregistered = this.adapter.unregister("test"); - assertThat(unregistered).isSameAs(healthIndicator); - assertThat(this.contributorRegistry.getContributor("test")).isNull(); - } - - @Test - void unregisterWhenContributorRegistryResultIsNotHealthIndicatorReturnsNull() { - CompositeReactiveHealthContributor healthContributor = mock(CompositeReactiveHealthContributor.class); - this.contributorRegistry.registerContributor("test", healthContributor); - ReactiveHealthIndicator unregistered = this.adapter.unregister("test"); - assertThat(unregistered).isNull(); - assertThat(this.contributorRegistry.getContributor("test")).isNull(); - } - - @Test - void getDelegatesToContributorRegistry() { - ReactiveHealthIndicator healthIndicator = mock(ReactiveHealthIndicator.class); - this.contributorRegistry.registerContributor("test", healthIndicator); - assertThat(this.adapter.get("test")).isSameAs(healthIndicator); - } - - @Test - void getWhenContributorRegistryResultIsNotHealthIndicatorReturnsNull() { - CompositeReactiveHealthContributor healthContributor = mock(CompositeReactiveHealthContributor.class); - this.contributorRegistry.registerContributor("test", healthContributor); - assertThat(this.adapter.get("test")).isNull(); - } - - @Test - void getAllDelegatesToContributorRegistry() { - ReactiveHealthIndicator healthIndicator = mock(ReactiveHealthIndicator.class); - this.contributorRegistry.registerContributor("test", healthIndicator); - Map all = this.adapter.getAll(); - assertThat(all).containsOnly(entry("test", healthIndicator)); - } - - @Test - void getAllWhenContributorRegistryContainsNonHealthIndicatorInstancesReturnsFilteredMap() { - CompositeReactiveHealthContributor healthContributor = mock(CompositeReactiveHealthContributor.class); - this.contributorRegistry.registerContributor("test1", healthContributor); - ReactiveHealthIndicator healthIndicator = mock(ReactiveHealthIndicator.class); - this.contributorRegistry.registerContributor("test2", healthIndicator); - Map all = this.adapter.getAll(); - assertThat(all).containsOnly(entry("test2", healthIndicator)); - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/influx/InfluxDbHealthContributorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/influx/InfluxDbHealthContributorAutoConfigurationTests.java index 755fd75322e3..dacea343e26b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/influx/InfluxDbHealthContributorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/influx/InfluxDbHealthContributorAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,7 @@ */ class InfluxDbHealthContributorAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withBean(InfluxDB.class, () -> mock(InfluxDB.class)).withConfiguration(AutoConfigurations .of(InfluxDbHealthContributorAutoConfiguration.class, HealthContributorAutoConfiguration.class)); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/info/InfoEndpointAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/info/InfoEndpointAutoConfigurationTests.java index e670aef99c1e..79a10f45f2d2 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/info/InfoEndpointAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/info/InfoEndpointAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,15 +36,13 @@ class InfoEndpointAutoConfigurationTests { @Test void runShouldHaveEndpointBean() { - this.contextRunner.withPropertyValues("management.endpoint.shutdown.enabled:true") + this.contextRunner.withPropertyValues("management.endpoints.web.exposure.include=info") .run((context) -> assertThat(context).hasSingleBean(InfoEndpoint.class)); } @Test - void runShouldHaveEndpointBeanEvenIfDefaultIsDisabled() { - // FIXME - this.contextRunner.withPropertyValues("management.endpoint.default.enabled:false") - .run((context) -> assertThat(context).hasSingleBean(InfoEndpoint.class)); + void runWhenNotExposedShouldNotHaveEndpointBean() { + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(InfoEndpoint.class)); } @Test diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/ControllerEndpointWebFluxIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/ControllerEndpointWebFluxIntegrationTests.java index 518f0a9e599f..442ad7dff815 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/ControllerEndpointWebFluxIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/ControllerEndpointWebFluxIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -56,7 +56,7 @@ void close() { } @Test - void endpointsCanBeAccessed() throws Exception { + void endpointsCanBeAccessed() { TestSecurityContextHolder.getContext() .setAuthentication(new TestingAuthenticationToken("user", "N/A", "ROLE_ACTUATOR")); this.context = new AnnotationConfigReactiveWebApplicationContext(); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JerseyEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JerseyEndpointIntegrationTests.java index d04bb5a28345..c60196cebfde 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JerseyEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JerseyEndpointIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,18 +16,25 @@ package org.springframework.boot.actuate.autoconfigure.integrationtest; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + import org.glassfish.jersey.server.ResourceConfig; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.autoconfigure.beans.BeansEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration; import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint; import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; @@ -47,32 +54,71 @@ class JerseyEndpointIntegrationTests { @Test void linksAreProvidedToAllEndpointTypes() { - testJerseyEndpoints(new Class[] { EndpointsConfiguration.class, ResourceConfigConfiguration.class }); + testJerseyEndpoints(new Class[] { EndpointsConfiguration.class, ResourceConfigConfiguration.class }); + } + + @Test + void linksPageIsNotAvailableWhenDisabled() { + getContextRunner(new Class[] { EndpointsConfiguration.class, ResourceConfigConfiguration.class }) + .withPropertyValues("management.endpoints.web.discovery.enabled:false").run((context) -> { + int port = context + .getSourceApplicationContext(AnnotationConfigServletWebServerApplicationContext.class) + .getWebServer().getPort(); + WebTestClient client = WebTestClient.bindToServer().baseUrl("http://localhost:" + port) + .responseTimeout(Duration.ofMinutes(5)).build(); + client.get().uri("/actuator").exchange().expectStatus().isNotFound(); + }); } @Test void actuatorEndpointsWhenUserProvidedResourceConfigBeanNotAvailable() { - testJerseyEndpoints(new Class[] { EndpointsConfiguration.class }); + testJerseyEndpoints(new Class[] { EndpointsConfiguration.class }); + } + + @Test + void actuatorEndpointsWhenSecurityAvailable() { + WebApplicationContextRunner contextRunner = getContextRunner( + new Class[] { EndpointsConfiguration.class, ResourceConfigConfiguration.class }, + getAutoconfigurations(SecurityAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class)); + contextRunner.run((context) -> { + int port = context.getSourceApplicationContext(AnnotationConfigServletWebServerApplicationContext.class) + .getWebServer().getPort(); + WebTestClient client = WebTestClient.bindToServer().baseUrl("http://localhost:" + port) + .responseTimeout(Duration.ofMinutes(5)).build(); + client.get().uri("/actuator").exchange().expectStatus().isUnauthorized(); + }); + } protected void testJerseyEndpoints(Class[] userConfigurations) { + getContextRunner(userConfigurations).run((context) -> { + int port = context.getSourceApplicationContext(AnnotationConfigServletWebServerApplicationContext.class) + .getWebServer().getPort(); + WebTestClient client = WebTestClient.bindToServer().baseUrl("http://localhost:" + port) + .responseTimeout(Duration.ofMinutes(5)).build(); + client.get().uri("/actuator").exchange().expectStatus().isOk().expectBody().jsonPath("_links.beans") + .isNotEmpty().jsonPath("_links.restcontroller").doesNotExist().jsonPath("_links.controller") + .doesNotExist(); + }); + } + + WebApplicationContextRunner getContextRunner(Class[] userConfigurations, + Class... additionalAutoConfigurations) { FilteredClassLoader classLoader = new FilteredClassLoader(DispatcherServlet.class); - new WebApplicationContextRunner(AnnotationConfigServletWebServerApplicationContext::new) + return new WebApplicationContextRunner(AnnotationConfigServletWebServerApplicationContext::new) .withClassLoader(classLoader) - .withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class, JerseyAutoConfiguration.class, - EndpointAutoConfiguration.class, ServletWebServerFactoryAutoConfiguration.class, - WebEndpointAutoConfiguration.class, ManagementContextAutoConfiguration.class, - BeansEndpointAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(getAutoconfigurations(additionalAutoConfigurations))) .withUserConfiguration(userConfigurations) - .withPropertyValues("management.endpoints.web.exposure.include:*", "server.port:0").run((context) -> { - int port = context - .getSourceApplicationContext(AnnotationConfigServletWebServerApplicationContext.class) - .getWebServer().getPort(); - WebTestClient client = WebTestClient.bindToServer().baseUrl("http://localhost:" + port).build(); - client.get().uri("/actuator").exchange().expectStatus().isOk().expectBody().jsonPath("_links.beans") - .isNotEmpty().jsonPath("_links.restcontroller").doesNotExist().jsonPath("_links.controller") - .doesNotExist(); - }); + .withPropertyValues("management.endpoints.web.exposure.include:*", "server.port:0"); + } + + private Class[] getAutoconfigurations(Class... additional) { + List> autoconfigurations = new ArrayList<>(Arrays.asList(JacksonAutoConfiguration.class, + JerseyAutoConfiguration.class, EndpointAutoConfiguration.class, + ServletWebServerFactoryAutoConfiguration.class, WebEndpointAutoConfiguration.class, + ManagementContextAutoConfiguration.class, BeansEndpointAutoConfiguration.class)); + autoconfigurations.addAll(Arrays.asList(additional)); + return autoconfigurations.toArray(new Class[0]); } @ControllerEndpoint(id = "controller") diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JmxEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JmxEndpointIntegrationTests.java index f63c65df7801..1db121791832 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JmxEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/JmxEndpointIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import org.junit.jupiter.api.Test; +import org.springframework.boot.LazyInitializationBeanFactoryPostProcessor; import org.springframework.boot.actuate.audit.InMemoryAuditEventRepository; import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.endpoint.jmx.JmxEndpointAutoConfiguration; @@ -66,6 +67,16 @@ void jmxEndpointsAreExposed() { }); } + @Test + void jmxEndpointsAreExposedWhenLazyInitializationIsEnabled() { + this.contextRunner.withBean(LazyInitializationBeanFactoryPostProcessor.class, + LazyInitializationBeanFactoryPostProcessor::new).run((context) -> { + MBeanServer mBeanServer = context.getBean(MBeanServer.class); + checkEndpointMBeans(mBeanServer, new String[] { "beans", "conditions", "configprops", "env", + "health", "info", "mappings", "threaddump", "httptrace" }, new String[] { "shutdown" }); + }); + } + @Test void jmxEndpointsCanBeExcluded() { this.contextRunner.withPropertyValues("management.endpoints.jmx.exposure.exclude:*").run((context) -> { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebEndpointsAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebEndpointsAutoConfigurationIntegrationTests.java index 418c9fc95222..c25193341f45 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebEndpointsAutoConfigurationIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebEndpointsAutoConfigurationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,6 @@ import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration; import org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration; -import org.springframework.boot.autoconfigure.data.solr.SolrRepositoriesAutoConfiguration; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration; import org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration; import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration; @@ -80,13 +79,9 @@ private ReactiveWebApplicationContextRunner reactiveWebRunner() { Neo4jRepositoriesAutoConfiguration.class, MongoAutoConfiguration.class, MongoDataAutoConfiguration.class, MongoReactiveAutoConfiguration.class, MongoReactiveDataAutoConfiguration.class, RepositoryRestMvcAutoConfiguration.class, HazelcastAutoConfiguration.class, - org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchAutoConfiguration.class, - ElasticsearchDataAutoConfiguration.class, - org.springframework.boot.autoconfigure.elasticsearch.jest.JestAutoConfiguration.class, - SolrRepositoriesAutoConfiguration.class, SolrAutoConfiguration.class, RedisAutoConfiguration.class, + ElasticsearchDataAutoConfiguration.class, SolrAutoConfiguration.class, RedisAutoConfiguration.class, RedisRepositoriesAutoConfiguration.class, MetricsAutoConfiguration.class }) @SpringBootConfiguration - @SuppressWarnings("deprecation") static class WebEndpointTestApplication { } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebFluxEndpointCorsIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebFluxEndpointCorsIntegrationTests.java index 4072d8674c28..2cb180b832f6 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebFluxEndpointCorsIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebFluxEndpointCorsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,7 +46,7 @@ */ class WebFluxEndpointCorsIntegrationTests { - private ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() + private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class, CodecsAutoConfiguration.class, WebFluxAutoConfiguration.class, HttpHandlerAutoConfiguration.class, EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, ManagementContextAutoConfiguration.class, @@ -71,6 +71,19 @@ void settingAllowedOriginsEnablesCors() { })); } + @Test + void settingAllowedOriginPatternsEnablesCors() { + this.contextRunner + .withPropertyValues("management.endpoints.web.cors.allowed-origin-patterns:*.example.org", + "management.endpoints.web.cors.allow-credentials:true") + .run(withWebTestClient((webTestClient) -> { + webTestClient.options().uri("/actuator/beans").header("Origin", "spring.example.com") + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET").exchange().expectStatus() + .isForbidden(); + performAcceptedCorsRequest(webTestClient, "/actuator/beans"); + })); + } + @Test void maxAgeDefaultsTo30Minutes() { this.contextRunner.withPropertyValues("management.endpoints.web.cors.allowed-origins:spring.example.org") diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebFluxEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebFluxEndpointIntegrationTests.java index 321e9dd8decd..65bf929442e0 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebFluxEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebFluxEndpointIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,21 +43,29 @@ */ class WebFluxEndpointIntegrationTests { + private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class, CodecsAutoConfiguration.class, + WebFluxAutoConfiguration.class, HttpHandlerAutoConfiguration.class, EndpointAutoConfiguration.class, + WebEndpointAutoConfiguration.class, ManagementContextAutoConfiguration.class, + ReactiveManagementContextAutoConfiguration.class, BeansEndpointAutoConfiguration.class)) + .withUserConfiguration(EndpointsConfiguration.class); + + @Test + void linksAreProvidedToAllEndpointTypes() { + this.contextRunner.withPropertyValues("management.endpoints.web.exposure.include:*").run((context) -> { + WebTestClient client = createWebTestClient(context); + client.get().uri("/actuator").exchange().expectStatus().isOk().expectBody().jsonPath("_links.beans") + .isNotEmpty().jsonPath("_links.restcontroller").isNotEmpty().jsonPath("_links.controller") + .isNotEmpty(); + }); + } + @Test - void linksAreProvidedToAllEndpointTypes() throws Exception { - new ReactiveWebApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class, CodecsAutoConfiguration.class, - WebFluxAutoConfiguration.class, HttpHandlerAutoConfiguration.class, - EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, - ManagementContextAutoConfiguration.class, ReactiveManagementContextAutoConfiguration.class, - BeansEndpointAutoConfiguration.class)) - .withUserConfiguration(EndpointsConfiguration.class) - .withPropertyValues("management.endpoints.web.exposure.include:*").run((context) -> { - WebTestClient client = createWebTestClient(context); - client.get().uri("/actuator").exchange().expectStatus().isOk().expectBody().jsonPath("_links.beans") - .isNotEmpty().jsonPath("_links.restcontroller").isNotEmpty().jsonPath("_links.controller") - .isNotEmpty(); - }); + void linksPageIsNotAvailableWhenDisabled() { + this.contextRunner.withPropertyValues("management.endpoints.web.discovery.enabled=false").run((context) -> { + WebTestClient client = createWebTestClient(context); + client.get().uri("/actuator").exchange().expectStatus().isNotFound(); + }); } private WebTestClient createWebTestClient(ApplicationContext context) { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointCorsIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointCorsIntegrationTests.java index 518567586f21..0d063be2714b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointCorsIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointCorsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -77,6 +77,17 @@ void settingAllowedOriginsEnablesCors() { })); } + @Test + void settingAllowedOriginPatternsEnablesCors() { + this.contextRunner.withPropertyValues("management.endpoints.web.cors.allowed-origin-patterns:*.example.com", + "management.endpoints.web.cors.allow-credentials:true").run(withMockMvc((mockMvc) -> { + mockMvc.perform(options("/actuator/beans").header("Origin", "bar.example.org") + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET")) + .andExpect(status().isForbidden()); + performAcceptedCorsRequest(mockMvc); + })); + } + @Test void maxAgeDefaultsTo30Minutes() { this.contextRunner.withPropertyValues("management.endpoints.web.cors.allowed-origins:foo.example.com") diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointExposureIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointExposureIntegrationTests.java index 1e70a0aec75d..fd8346d92057 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointExposureIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointExposureIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.boot.actuate.autoconfigure.integrationtest; import java.io.IOException; +import java.time.Duration; import java.util.function.Supplier; import javax.servlet.ServletException; @@ -54,6 +55,7 @@ import org.springframework.test.web.reactive.server.EntityExchangeResult; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.reactive.function.client.ExchangeStrategies; import static org.assertj.core.api.Assertions.assertThat; @@ -90,7 +92,7 @@ void webEndpointsAreDisabledByDefault() { assertThat(isExposed(client, HttpMethod.GET, "customservlet")).isFalse(); assertThat(isExposed(client, HttpMethod.GET, "env")).isFalse(); assertThat(isExposed(client, HttpMethod.GET, "health")).isTrue(); - assertThat(isExposed(client, HttpMethod.GET, "info")).isTrue(); + assertThat(isExposed(client, HttpMethod.GET, "info")).isFalse(); assertThat(isExposed(client, HttpMethod.GET, "mappings")).isFalse(); assertThat(isExposed(client, HttpMethod.POST, "shutdown")).isFalse(); assertThat(isExposed(client, HttpMethod.GET, "threaddump")).isFalse(); @@ -164,10 +166,13 @@ void singleWebEndpointCanBeExcluded() { private WebTestClient createClient(AssertableWebApplicationContext context) { int port = context.getSourceApplicationContext(ServletWebServerApplicationContext.class).getWebServer() .getPort(); - return WebTestClient.bindToServer().baseUrl("http://localhost:" + port).build(); + ExchangeStrategies exchangeStrategies = ExchangeStrategies.builder() + .codecs((configurer) -> configurer.defaultCodecs().maxInMemorySize(-1)).build(); + return WebTestClient.bindToServer().baseUrl("http://localhost:" + port).exchangeStrategies(exchangeStrategies) + .responseTimeout(Duration.ofMinutes(5)).build(); } - private boolean isExposed(WebTestClient client, HttpMethod method, String path) throws Exception { + private boolean isExposed(WebTestClient client, HttpMethod method, String path) { path = "/actuator/" + path; EntityExchangeResult result = client.method(method).uri(path).exchange().expectBody().returnResult(); if (result.getStatus() == HttpStatus.OK) { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointIntegrationTests.java index 36021b89f9bd..384d4ef27163 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -119,6 +119,15 @@ void linksAreProvidedToAllEndpointTypes() throws Exception { both(hasKey("beans")).and(hasKey("servlet")).and(hasKey("restcontroller")).and(hasKey("controller")))); } + @Test + void linksPageIsNotAvailableWhenDisabled() throws Exception { + this.context = new AnnotationConfigServletWebApplicationContext(); + this.context.register(DefaultConfiguration.class, EndpointsConfiguration.class); + TestPropertyValues.of("management.endpoints.web.discovery.enabled=false").applyTo(this.context); + MockMvc mockMvc = doCreateMockMvc(); + mockMvc.perform(get("/actuator").accept("*/*")).andExpect(status().isNotFound()); + } + private MockMvc createSecureMockMvc() { return doCreateMockMvc(springSecurity()); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/jdbc/DataSourceHealthContributorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/jdbc/DataSourceHealthContributorAutoConfigurationTests.java index 080d03cfa4a1..8316f0acd913 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/jdbc/DataSourceHealthContributorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/jdbc/DataSourceHealthContributorAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,15 @@ package org.springframework.boot.actuate.autoconfigure.jdbc; +import java.util.HashMap; +import java.util.Map; + import javax.sql.DataSource; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration; -import org.springframework.boot.actuate.autoconfigure.jdbc.DataSourceHealthContributorAutoConfiguration.RoutingDataSourceHealthIndicator; +import org.springframework.boot.actuate.autoconfigure.jdbc.DataSourceHealthContributorAutoConfiguration.RoutingDataSourceHealthContributor; import org.springframework.boot.actuate.health.CompositeHealthContributor; import org.springframework.boot.actuate.health.NamedContributor; import org.springframework.boot.actuate.jdbc.DataSourceHealthIndicator; @@ -38,19 +41,21 @@ import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; /** * Tests for {@link DataSourceHealthContributorAutoConfiguration}. * * @author Phillip Webb + * @author Julio Gomez + * @author Safeer Ansari */ class DataSourceHealthContributorAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, - HealthContributorAutoConfiguration.class, DataSourceHealthContributorAutoConfiguration.class)) - .withPropertyValues("spring.datasource.initialization-mode=never"); + HealthContributorAutoConfiguration.class, DataSourceHealthContributorAutoConfiguration.class)); @Test void runShouldCreateIndicator() { @@ -72,20 +77,45 @@ void runWhenMultipleDataSourceBeansShouldCreateCompositeIndicator() { } @Test - void runWithRoutingAndEmbeddedDataSourceShouldFilterRoutingDataSource() { - this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class, RoutingDatasourceConfig.class) + void runWithRoutingAndEmbeddedDataSourceShouldIncludeRoutingDataSource() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class, RoutingDataSourceConfig.class) .run((context) -> { CompositeHealthContributor composite = context.getBean(CompositeHealthContributor.class); assertThat(composite.getContributor("dataSource")).isInstanceOf(DataSourceHealthIndicator.class); assertThat(composite.getContributor("routingDataSource")) - .isInstanceOf(RoutingDataSourceHealthIndicator.class); + .isInstanceOf(RoutingDataSourceHealthContributor.class); }); } @Test - void runWithOnlyRoutingDataSourceShouldFilterRoutingDataSource() { - this.contextRunner.withUserConfiguration(RoutingDatasourceConfig.class) - .run((context) -> assertThat(context).hasSingleBean(RoutingDataSourceHealthIndicator.class)); + void runWithRoutingAndEmbeddedDataSourceShouldNotIncludeRoutingDataSourceWhenIgnored() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class, RoutingDataSourceConfig.class) + .withPropertyValues("management.health.db.ignore-routing-datasources:true").run((context) -> { + assertThat(context).doesNotHaveBean(CompositeHealthContributor.class); + assertThat(context).hasSingleBean(DataSourceHealthIndicator.class); + assertThat(context).doesNotHaveBean(RoutingDataSourceHealthContributor.class); + }); + } + + @Test + void runWithOnlyRoutingDataSourceShouldIncludeRoutingDataSourceWithComposedIndicators() { + this.contextRunner.withUserConfiguration(RoutingDataSourceConfig.class).run((context) -> { + assertThat(context).hasSingleBean(RoutingDataSourceHealthContributor.class); + RoutingDataSourceHealthContributor routingHealthContributor = context + .getBean(RoutingDataSourceHealthContributor.class); + assertThat(routingHealthContributor.getContributor("one")).isInstanceOf(DataSourceHealthIndicator.class); + assertThat(routingHealthContributor.getContributor("two")).isInstanceOf(DataSourceHealthIndicator.class); + assertThat(routingHealthContributor.iterator()).toIterable().extracting("name") + .containsExactlyInAnyOrder("one", "two"); + }); + } + + @Test + void runWithOnlyRoutingDataSourceShouldCrashWhenIgnored() { + this.contextRunner.withUserConfiguration(RoutingDataSourceConfig.class) + .withPropertyValues("management.health.db.ignore-routing-datasources:true") + .run((context) -> assertThat(context).hasFailed().getFailure() + .hasRootCauseInstanceOf(IllegalArgumentException.class)); } @Test @@ -107,6 +137,20 @@ void runWhenDisabledShouldNotCreateIndicator() { .doesNotHaveBean(CompositeHealthContributor.class)); } + @Test + void runWhenDataSourceHasNullRoutingKeyShouldProduceUnnamedComposedIndicator() { + this.contextRunner.withUserConfiguration(NullKeyRoutingDataSourceConfig.class).run((context) -> { + assertThat(context).hasSingleBean(RoutingDataSourceHealthContributor.class); + RoutingDataSourceHealthContributor routingHealthContributor = context + .getBean(RoutingDataSourceHealthContributor.class); + assertThat(routingHealthContributor.getContributor("unnamed")) + .isInstanceOf(DataSourceHealthIndicator.class); + assertThat(routingHealthContributor.getContributor("one")).isInstanceOf(DataSourceHealthIndicator.class); + assertThat(routingHealthContributor.iterator()).toIterable().extracting("name") + .containsExactlyInAnyOrder("unnamed", "one"); + }); + } + @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties static class DataSourceConfig { @@ -121,11 +165,31 @@ DataSource testDataSource() { } @Configuration(proxyBeanMethods = false) - static class RoutingDatasourceConfig { + static class RoutingDataSourceConfig { + + @Bean + AbstractRoutingDataSource routingDataSource() { + Map dataSources = new HashMap<>(); + dataSources.put("one", mock(DataSource.class)); + dataSources.put("two", mock(DataSource.class)); + AbstractRoutingDataSource routingDataSource = mock(AbstractRoutingDataSource.class); + given(routingDataSource.getResolvedDataSources()).willReturn(dataSources); + return routingDataSource; + } + + } + + @Configuration(proxyBeanMethods = false) + static class NullKeyRoutingDataSourceConfig { @Bean AbstractRoutingDataSource routingDataSource() { - return mock(AbstractRoutingDataSource.class); + Map dataSources = new HashMap<>(); + dataSources.put(null, mock(DataSource.class)); + dataSources.put("one", mock(DataSource.class)); + AbstractRoutingDataSource routingDataSource = mock(AbstractRoutingDataSource.class); + given(routingDataSource.getResolvedDataSources()).willReturn(dataSources); + return routingDataSource; } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfigurationTests.java index 80cf99671d59..102f1839503f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,7 @@ */ class JmsHealthContributorAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(ActiveMQAutoConfiguration.class, JmsHealthContributorAutoConfiguration.class, HealthContributorAutoConfiguration.class)); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/ldap/LdapHealthContributorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/ldap/LdapHealthContributorAutoConfigurationTests.java index 1166ac79dda4..38f897ab946b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/ldap/LdapHealthContributorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/ldap/LdapHealthContributorAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,7 @@ */ class LdapHealthContributorAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withBean(LdapOperations.class, () -> mock(LdapOperations.class)).withConfiguration(AutoConfigurations .of(LdapHealthContributorAutoConfiguration.class, HealthContributorAutoConfiguration.class)); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logging/LogFileWebEndpointAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logging/LogFileWebEndpointAutoConfigurationTests.java index acda4f0055c4..6492a90207e7 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logging/LogFileWebEndpointAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logging/LogFileWebEndpointAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,7 +42,7 @@ */ class LogFileWebEndpointAutoConfigurationTests { - private WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(LogFileWebEndpointAutoConfiguration.class)); @Test @@ -64,14 +64,6 @@ void runWhenLoggingFileIsSetAndExposedShouldHaveEndpointBean() { .run((context) -> assertThat(context).hasSingleBean(LogFileWebEndpoint.class)); } - @Test - @Deprecated - void runWhenLoggingFileIsSetWithDeprecatedPropertyAndExposedShouldHaveEndpointBean() { - this.contextRunner - .withPropertyValues("logging.file:test.log", "management.endpoints.web.exposure.include=logfile") - .run((context) -> assertThat(context).hasSingleBean(LogFileWebEndpoint.class)); - } - @Test void runWhenLoggingPathIsSetAndNotExposedShouldNotHaveEndpointBean() { this.contextRunner.withPropertyValues("logging.file.path:test/logs") @@ -85,14 +77,6 @@ void runWhenLoggingPathIsSetAndExposedShouldHaveEndpointBean() { .run((context) -> assertThat(context).hasSingleBean(LogFileWebEndpoint.class)); } - @Test - @Deprecated - void runWhenLoggingPathIsSetWithDeprecatedPropertyAndExposedShouldHaveEndpointBean() { - this.contextRunner - .withPropertyValues("logging.path:test/logs", "management.endpoints.web.exposure.include=logfile") - .run((context) -> assertThat(context).hasSingleBean(LogFileWebEndpoint.class)); - } - @Test void logFileWebEndpointIsAutoConfiguredWhenExternalFileIsSet() { this.contextRunner diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/mail/MailHealthContributorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/mail/MailHealthContributorAutoConfigurationTests.java index 44e9f5b8bc37..b51a8fed005c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/mail/MailHealthContributorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/mail/MailHealthContributorAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ */ class MailHealthContributorAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(MailSenderAutoConfiguration.class, MailHealthContributorAutoConfiguration.class, HealthContributorAutoConfiguration.class)) .withPropertyValues("spring.mail.host:smtp.example.com"); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/CompositeMeterRegistryAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/CompositeMeterRegistryAutoConfigurationTests.java index 3baba57538fa..4b6f8610be46 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/CompositeMeterRegistryAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/CompositeMeterRegistryAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ class CompositeMeterRegistryAutoConfigurationTests { private static final String COMPOSITE_NAME = "compositeMeterRegistry"; - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withUserConfiguration(BaseConfig.class) .withConfiguration(AutoConfigurations.of(CompositeMeterRegistryAutoConfiguration.class)); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/KafkaMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/KafkaMetricsAutoConfigurationTests.java index 1b4450ec4730..8a2dcf27c159 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/KafkaMetricsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/KafkaMetricsAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,15 +16,27 @@ package org.springframework.boot.actuate.autoconfigure.metrics; -import io.micrometer.core.instrument.binder.kafka.KafkaConsumerMetrics; +import java.util.regex.Pattern; + +import org.apache.kafka.streams.StreamsBuilder; +import org.apache.kafka.streams.kstream.KStream; +import org.apache.kafka.streams.kstream.KTable; +import org.apache.kafka.streams.kstream.Materialized; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration; +import org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.annotation.EnableKafkaStreams; +import org.springframework.kafka.config.StreamsBuilderFactoryBean; +import org.springframework.kafka.core.DefaultKafkaConsumerFactory; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.MicrometerConsumerListener; +import org.springframework.kafka.core.MicrometerProducerListener; +import org.springframework.kafka.streams.KafkaStreamsMicrometerListener; import static org.assertj.core.api.Assertions.assertThat; @@ -32,38 +44,66 @@ * Tests for {@link KafkaMetricsAutoConfiguration}. * * @author Andy Wilkinson + * @author Stephane Nicoll + * @author Eddú Meléndez */ class KafkaMetricsAutoConfigurationTests { - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().with(MetricsRun.simple()) - .withPropertyValues("spring.jmx.enabled=true") + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(KafkaMetricsAutoConfiguration.class)); @Test - void whenThereIsNoMBeanServerAutoConfigurationBacksOff() { - this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(KafkaConsumerMetrics.class)); + void whenThereIsAMeterRegistryThenMetricsListenersAreAdded() { + this.contextRunner.with(MetricsRun.simple()) + .withConfiguration(AutoConfigurations.of(KafkaAutoConfiguration.class)).run((context) -> { + assertThat(((DefaultKafkaProducerFactory) context.getBean(DefaultKafkaProducerFactory.class)) + .getListeners()).hasSize(1).hasOnlyElementsOfTypes(MicrometerProducerListener.class); + assertThat(((DefaultKafkaConsumerFactory) context.getBean(DefaultKafkaConsumerFactory.class)) + .getListeners()).hasSize(1).hasOnlyElementsOfTypes(MicrometerConsumerListener.class); + }); + } + + @Test + void whenThereIsNoMeterRegistryThenListenerCustomizationBacksOff() { + this.contextRunner.withConfiguration(AutoConfigurations.of(KafkaAutoConfiguration.class)).run((context) -> { + assertThat(((DefaultKafkaProducerFactory) context.getBean(DefaultKafkaProducerFactory.class)) + .getListeners()).isEmpty(); + assertThat(((DefaultKafkaConsumerFactory) context.getBean(DefaultKafkaConsumerFactory.class)) + .getListeners()).isEmpty(); + }); } @Test - void whenThereIsAnMBeanServerKafkaConsumerMetricsIsConfigured() { - this.contextRunner.withConfiguration(AutoConfigurations.of(JmxAutoConfiguration.class)) - .run((context) -> assertThat(context).hasSingleBean(KafkaConsumerMetrics.class)); + void whenKafkaStreamsIsEnabledAndThereIsAMeterRegistryThenMetricsListenersAreAdded() { + this.contextRunner.withConfiguration(AutoConfigurations.of(KafkaAutoConfiguration.class)) + .withUserConfiguration(EnableKafkaStreamsConfiguration.class) + .withPropertyValues("spring.application.name=my-test-app").with(MetricsRun.simple()).run((context) -> { + StreamsBuilderFactoryBean streamsBuilderFactoryBean = context + .getBean(StreamsBuilderFactoryBean.class); + assertThat(streamsBuilderFactoryBean.getListeners()).hasSize(1) + .hasOnlyElementsOfTypes(KafkaStreamsMicrometerListener.class); + }); } @Test - void allowsCustomKafkaConsumerMetricsToBeUsed() { - this.contextRunner.withConfiguration(AutoConfigurations.of(JmxAutoConfiguration.class)) - .withUserConfiguration(CustomKafkaConsumerMetricsConfiguration.class) - .run((context) -> assertThat(context).hasSingleBean(KafkaConsumerMetrics.class) - .hasBean("customKafkaConsumerMetrics")); + void whenKafkaStreamsIsEnabledAndThereIsNoMeterRegistryThenListenerCustomizationBacksOff() { + this.contextRunner.withConfiguration(AutoConfigurations.of(KafkaAutoConfiguration.class)) + .withUserConfiguration(EnableKafkaStreamsConfiguration.class) + .withPropertyValues("spring.application.name=my-test-app").run((context) -> { + StreamsBuilderFactoryBean streamsBuilderFactoryBean = context + .getBean(StreamsBuilderFactoryBean.class); + assertThat(streamsBuilderFactoryBean.getListeners()).isEmpty(); + }); } @Configuration(proxyBeanMethods = false) - static class CustomKafkaConsumerMetricsConfiguration { + @EnableKafkaStreams + static class EnableKafkaStreamsConfiguration { @Bean - KafkaConsumerMetrics customKafkaConsumerMetrics() { - return new KafkaConsumerMetrics(); + KTable table(StreamsBuilder builder) { + KStream stream = builder.stream(Pattern.compile("test")); + return stream.groupByKey().count(Materialized.as("store")); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryConfigurerIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryConfigurerIntegrationTests.java index 5f54204fd128..29e82b079294 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryConfigurerIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryConfigurerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,6 @@ import org.junit.jupiter.api.Test; import org.slf4j.impl.StaticLoggerBinder; -import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.actuate.autoconfigure.metrics.export.atlas.AtlasMetricsExportAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.export.jmx.JmxMetricsExportAutoConfiguration; @@ -48,7 +47,7 @@ */ class MeterRegistryConfigurerIntegrationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .with(MetricsRun.limitedTo(AtlasMetricsExportAutoConfiguration.class, PrometheusMetricsExportAutoConfiguration.class)) .withConfiguration(AutoConfigurations.of(JvmMetricsAutoConfiguration.class)); @@ -134,7 +133,7 @@ static BeanPostProcessor testPostProcessor(ApplicationContext context) { return new BeanPostProcessor() { @Override - public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + public Object postProcessAfterInitialization(Object bean, String beanName) { if (bean instanceof Bravo) { MeterRegistry meterRegistry = context.getBean(MeterRegistry.class); meterRegistry.gauge("test", 1); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryConfigurerTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryConfigurerTests.java index 0ad0f7af5ece..b7fbc5eacaac 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryConfigurerTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,20 +25,19 @@ import io.micrometer.core.instrument.binder.MeterBinder; import io.micrometer.core.instrument.composite.CompositeMeterRegistry; import io.micrometer.core.instrument.config.MeterFilter; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InOrder; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.beans.factory.ObjectProvider; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; /** * Tests for {@link MeterRegistryConfigurer}. @@ -46,6 +45,7 @@ * @author Phillip Webb * @author Andy Wilkinson */ +@ExtendWith(MockitoExtension.class) class MeterRegistryConfigurerTests { private List binders = new ArrayList<>(); @@ -69,12 +69,6 @@ class MeterRegistryConfigurerTests { @Mock private Config mockConfig; - @BeforeEach - void setup() { - MockitoAnnotations.initMocks(this); - given(this.mockRegistry.config()).willReturn(this.mockConfig); - } - @Test void configureWhenCompositeShouldApplyCustomizer() { this.customizers.add(this.mockCustomizer); @@ -82,34 +76,37 @@ void configureWhenCompositeShouldApplyCustomizer() { createObjectProvider(this.filters), createObjectProvider(this.binders), false, false); CompositeMeterRegistry composite = new CompositeMeterRegistry(); configurer.configure(composite); - verify(this.mockCustomizer).customize(composite); + then(this.mockCustomizer).should().customize(composite); } @Test void configureShouldApplyCustomizer() { + given(this.mockRegistry.config()).willReturn(this.mockConfig); this.customizers.add(this.mockCustomizer); MeterRegistryConfigurer configurer = new MeterRegistryConfigurer(createObjectProvider(this.customizers), createObjectProvider(this.filters), createObjectProvider(this.binders), false, false); configurer.configure(this.mockRegistry); - verify(this.mockCustomizer).customize(this.mockRegistry); + then(this.mockCustomizer).should().customize(this.mockRegistry); } @Test void configureShouldApplyFilter() { + given(this.mockRegistry.config()).willReturn(this.mockConfig); this.filters.add(this.mockFilter); MeterRegistryConfigurer configurer = new MeterRegistryConfigurer(createObjectProvider(this.customizers), createObjectProvider(this.filters), createObjectProvider(this.binders), false, false); configurer.configure(this.mockRegistry); - verify(this.mockConfig).meterFilter(this.mockFilter); + then(this.mockConfig).should().meterFilter(this.mockFilter); } @Test void configureShouldApplyBinder() { + given(this.mockRegistry.config()).willReturn(this.mockConfig); this.binders.add(this.mockBinder); MeterRegistryConfigurer configurer = new MeterRegistryConfigurer(createObjectProvider(this.customizers), createObjectProvider(this.filters), createObjectProvider(this.binders), false, false); configurer.configure(this.mockRegistry); - verify(this.mockBinder).bindTo(this.mockRegistry); + then(this.mockBinder).should().bindTo(this.mockRegistry); } @Test @@ -119,20 +116,21 @@ void configureShouldApplyBinderToComposite() { createObjectProvider(this.filters), createObjectProvider(this.binders), false, true); CompositeMeterRegistry composite = new CompositeMeterRegistry(); configurer.configure(composite); - verify(this.mockBinder).bindTo(composite); + then(this.mockBinder).should().bindTo(composite); } @Test void configureShouldNotApplyBinderWhenCompositeExists() { - this.binders.add(this.mockBinder); + given(this.mockRegistry.config()).willReturn(this.mockConfig); MeterRegistryConfigurer configurer = new MeterRegistryConfigurer(createObjectProvider(this.customizers), - createObjectProvider(this.filters), createObjectProvider(this.binders), false, true); + createObjectProvider(this.filters), null, false, true); configurer.configure(this.mockRegistry); - verifyNoInteractions(this.mockBinder); + then(this.mockBinder).shouldHaveNoInteractions(); } @Test void configureShouldBeCalledInOrderCustomizerFilterBinder() { + given(this.mockRegistry.config()).willReturn(this.mockConfig); this.customizers.add(this.mockCustomizer); this.filters.add(this.mockFilter); this.binders.add(this.mockBinder); @@ -140,13 +138,14 @@ void configureShouldBeCalledInOrderCustomizerFilterBinder() { createObjectProvider(this.filters), createObjectProvider(this.binders), false, false); configurer.configure(this.mockRegistry); InOrder ordered = inOrder(this.mockBinder, this.mockConfig, this.mockCustomizer); - ordered.verify(this.mockCustomizer).customize(this.mockRegistry); - ordered.verify(this.mockConfig).meterFilter(this.mockFilter); - ordered.verify(this.mockBinder).bindTo(this.mockRegistry); + then(this.mockCustomizer).should(ordered).customize(this.mockRegistry); + then(this.mockConfig).should(ordered).meterFilter(this.mockFilter); + then(this.mockBinder).should(ordered).bindTo(this.mockRegistry); } @Test void configureWhenAddToGlobalRegistryShouldAddToGlobalRegistry() { + given(this.mockRegistry.config()).willReturn(this.mockConfig); MeterRegistryConfigurer configurer = new MeterRegistryConfigurer(createObjectProvider(this.customizers), createObjectProvider(this.filters), createObjectProvider(this.binders), true, false); try { @@ -160,6 +159,7 @@ void configureWhenAddToGlobalRegistryShouldAddToGlobalRegistry() { @Test void configureWhenNotAddToGlobalRegistryShouldAddToGlobalRegistry() { + given(this.mockRegistry.config()).willReturn(this.mockConfig); MeterRegistryConfigurer configurer = new MeterRegistryConfigurer(createObjectProvider(this.customizers), createObjectProvider(this.filters), createObjectProvider(this.binders), false, false); configurer.configure(this.mockRegistry); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryCustomizerTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryCustomizerTests.java index deaebff86b60..d1d48fdf9b65 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryCustomizerTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterRegistryCustomizerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,7 +39,7 @@ */ class MeterRegistryCustomizerTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .with(MetricsRun.limitedTo(AtlasMetricsExportAutoConfiguration.class, PrometheusMetricsExportAutoConfiguration.class)) .withConfiguration(AutoConfigurations.of(JvmMetricsAutoConfiguration.class)); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterValueTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterValueTests.java index f63da5faaef2..316eea078b79 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterValueTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterValueTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,19 +30,20 @@ * Tests for {@link MeterValue}. * * @author Phillip Webb + * @author Stephane Nicoll */ class MeterValueTests { @Test - void getValueForDistributionSummaryWhenFromLongShouldReturnLongValue() { - MeterValue meterValue = MeterValue.valueOf(123L); - assertThat(meterValue.getValue(Type.DISTRIBUTION_SUMMARY)).isEqualTo(123); + void getValueForDistributionSummaryWhenFromNumberShouldReturnDoubleValue() { + MeterValue meterValue = MeterValue.valueOf(123.42); + assertThat(meterValue.getValue(Type.DISTRIBUTION_SUMMARY)).isEqualTo(123.42); } @Test - void getValueForDistributionSummaryWhenFromNumberStringShouldReturnLongValue() { - MeterValue meterValue = MeterValue.valueOf("123"); - assertThat(meterValue.getValue(Type.DISTRIBUTION_SUMMARY)).isEqualTo(123); + void getValueForDistributionSummaryWhenFromNumberStringShouldReturnDoubleValue() { + MeterValue meterValue = MeterValue.valueOf("123.42"); + assertThat(meterValue.getValue(Type.DISTRIBUTION_SUMMARY)).isEqualTo(123.42); } @Test @@ -52,8 +53,8 @@ void getValueForDistributionSummaryWhenFromDurationStringShouldReturnNull() { } @Test - void getValueForTimerWhenFromLongShouldReturnMsToNanosValue() { - MeterValue meterValue = MeterValue.valueOf(123L); + void getValueForTimerWhenFromNumberShouldReturnMsToNanosValue() { + MeterValue meterValue = MeterValue.valueOf(123d); assertThat(meterValue.getValue(Type.TIMER)).isEqualTo(123000000); } @@ -81,11 +82,11 @@ void getValueForOthersShouldReturnNull() { @Test void valueOfShouldWorkInBinder() { MockEnvironment environment = new MockEnvironment(); - TestPropertyValues.of("duration=10ms", "long=20").applyTo(environment); + TestPropertyValues.of("duration=10ms", "number=20.42").applyTo(environment); assertThat(Binder.get(environment).bind("duration", Bindable.of(MeterValue.class)).get().getValue(Type.TIMER)) .isEqualTo(10000000); - assertThat(Binder.get(environment).bind("long", Bindable.of(MeterValue.class)).get().getValue(Type.TIMER)) - .isEqualTo(20000000); + assertThat(Binder.get(environment).bind("number", Bindable.of(MeterValue.class)).get() + .getValue(Type.DISTRIBUTION_SUMMARY)).isEqualTo(20.42); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfigurationIntegrationTests.java index a5bc05a903ce..70f28e4f163c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfigurationIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfigurationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,9 @@ package org.springframework.boot.actuate.autoconfigure.metrics; +import java.util.Arrays; +import java.util.Set; + import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.MockClock; import io.micrometer.core.instrument.composite.CompositeMeterRegistry; @@ -23,6 +26,7 @@ import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import io.micrometer.graphite.GraphiteMeterRegistry; import io.micrometer.jmx.JmxMeterRegistry; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.autoconfigure.metrics.export.graphite.GraphiteMetricsExportAutoConfiguration; @@ -42,7 +46,7 @@ */ class MetricsAutoConfigurationIntegrationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner().with(MetricsRun.simple()); + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().with(MetricsRun.simple()); @Test void propertyBasedMeterFilteringIsAutoConfigured() { @@ -109,6 +113,35 @@ void compositeCreatedWithMultipleRegistries() { }); } + @Test + void autoConfiguredCompositeDoesNotHaveMeterFiltersApplied() { + new ApplicationContextRunner().with(MetricsRun.limitedTo(GraphiteMetricsExportAutoConfiguration.class, + JmxMetricsExportAutoConfiguration.class)).run((context) -> { + MeterRegistry composite = context.getBean(MeterRegistry.class); + assertThat(composite).extracting("filters", InstanceOfAssertFactories.ARRAY).hasSize(0); + assertThat(composite).isInstanceOf(CompositeMeterRegistry.class); + Set registries = ((CompositeMeterRegistry) composite).getRegistries(); + assertThat(registries).hasSize(2); + assertThat(registries).hasAtLeastOneElementOfType(GraphiteMeterRegistry.class) + .hasAtLeastOneElementOfType(JmxMeterRegistry.class); + assertThat(registries).allSatisfy((registry) -> assertThat(registry) + .extracting("filters", InstanceOfAssertFactories.ARRAY).hasSize(1)); + }); + } + + @Test + void userConfiguredCompositeHasMeterFiltersApplied() { + new ApplicationContextRunner().with(MetricsRun.limitedTo()) + .withUserConfiguration(CompositeMeterRegistryConfiguration.class).run((context) -> { + MeterRegistry composite = context.getBean(MeterRegistry.class); + assertThat(composite).extracting("filters", InstanceOfAssertFactories.ARRAY).hasSize(1); + assertThat(composite).isInstanceOf(CompositeMeterRegistry.class); + Set registries = ((CompositeMeterRegistry) composite).getRegistries(); + assertThat(registries).hasSize(2); + assertThat(registries).hasOnlyElementsOfTypes(SimpleMeterRegistry.class); + }); + } + @Configuration(proxyBeanMethods = false) static class PrimaryMeterRegistryConfiguration { @@ -120,4 +153,15 @@ MeterRegistry simpleMeterRegistry() { } + @Configuration(proxyBeanMethods = false) + static class CompositeMeterRegistryConfiguration { + + @Bean + CompositeMeterRegistry compositeMeterRegistry() { + return new CompositeMeterRegistry(new MockClock(), + Arrays.asList(new SimpleMeterRegistry(), new SimpleMeterRegistry())); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfigurationTests.java index 6737aba194ae..fd8f773084d3 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,8 +33,8 @@ import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link MetricsAutoConfiguration}. @@ -67,8 +67,8 @@ void configuresMeterRegistries() { assertThat(filters[0].accept((Meter.Id) null)).isEqualTo(MeterFilterReply.DENY); assertThat(filters[1]).isInstanceOf(PropertiesMeterFilter.class); assertThat(filters[2].accept((Meter.Id) null)).isEqualTo(MeterFilterReply.ACCEPT); - verify((MeterBinder) context.getBean("meterBinder")).bindTo(meterRegistry); - verify(context.getBean(MeterRegistryCustomizer.class)).customize(meterRegistry); + then((MeterBinder) context.getBean("meterBinder")).should().bindTo(meterRegistry); + then(context.getBean(MeterRegistryCustomizer.class)).should().customize(meterRegistry); }); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MissingRequiredConfigurationFailureAnalyzerTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MissingRequiredConfigurationFailureAnalyzerTests.java deleted file mode 100644 index e8ed900191d1..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MissingRequiredConfigurationFailureAnalyzerTests.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.autoconfigure.metrics; - -import io.micrometer.core.instrument.Clock; -import io.micrometer.newrelic.NewRelicMeterRegistry; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.diagnostics.FailureAnalysis; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.fail; - -/** - * Tests for {@link MissingRequiredConfigurationFailureAnalyzer}. - * - * @author Andy Wilkinson - */ -class MissingRequiredConfigurationFailureAnalyzerTests { - - @Test - void analyzesMissingRequiredConfiguration() { - FailureAnalysis analysis = new MissingRequiredConfigurationFailureAnalyzer() - .analyze(createFailure(MissingAccountIdConfiguration.class)); - assertThat(analysis).isNotNull(); - assertThat(analysis.getDescription()).isEqualTo("accountId must be set to report metrics to New Relic."); - assertThat(analysis.getAction()).isEqualTo("Update your application to provide the missing configuration."); - } - - private Exception createFailure(Class configuration) { - try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(configuration)) { - fail("Expected failure did not occur"); - return null; - } - catch (Exception ex) { - return ex; - } - } - - @Configuration(proxyBeanMethods = false) - static class MissingAccountIdConfiguration { - - @Bean - NewRelicMeterRegistry meterRegistry() { - return new NewRelicMeterRegistry((key) -> null, Clock.SYSTEM); - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/PropertiesMeterFilterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/PropertiesMeterFilterTests.java index d865e2e07d7f..7d6611df10a5 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/PropertiesMeterFilterTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/PropertiesMeterFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -204,29 +204,26 @@ void configureWhenAllPercentilesSetShouldSetPercentilesToValue() { } @Test - void configureWhenHasSlaShouldSetSlaToValue() { + void configureWhenHasSloShouldSetSloToValue() { PropertiesMeterFilter filter = new PropertiesMeterFilter( - createProperties("distribution.sla.spring.boot=1,2,3")); - assertThat( - filter.configure(createMeterId("spring.boot"), DistributionStatisticConfig.DEFAULT).getSlaBoundaries()) - .containsExactly(1000000, 2000000, 3000000); + createProperties("distribution.slo.spring.boot=1,2,3")); + assertThat(filter.configure(createMeterId("spring.boot"), DistributionStatisticConfig.DEFAULT) + .getServiceLevelObjectiveBoundaries()).containsExactly(1000000, 2000000, 3000000); } @Test - void configureWhenHasHigherSlaShouldSetPercentilesToValue() { - PropertiesMeterFilter filter = new PropertiesMeterFilter(createProperties("distribution.sla.spring=1,2,3")); - assertThat( - filter.configure(createMeterId("spring.boot"), DistributionStatisticConfig.DEFAULT).getSlaBoundaries()) - .containsExactly(1000000, 2000000, 3000000); + void configureWhenHasHigherSloShouldSetPercentilesToValue() { + PropertiesMeterFilter filter = new PropertiesMeterFilter(createProperties("distribution.slo.spring=1,2,3")); + assertThat(filter.configure(createMeterId("spring.boot"), DistributionStatisticConfig.DEFAULT) + .getServiceLevelObjectiveBoundaries()).containsExactly(1000000, 2000000, 3000000); } @Test - void configureWhenHasHigherSlaAndLowerShouldSetSlaToHigher() { + void configureWhenHasHigherSloAndLowerShouldSetSloToHigher() { PropertiesMeterFilter filter = new PropertiesMeterFilter( - createProperties("distribution.sla.spring=1,2,3", "distribution.sla.spring.boot=4,5,6")); - assertThat( - filter.configure(createMeterId("spring.boot"), DistributionStatisticConfig.DEFAULT).getSlaBoundaries()) - .containsExactly(4000000, 5000000, 6000000); + createProperties("distribution.slo.spring=1,2,3", "distribution.slo.spring.boot=4,5,6")); + assertThat(filter.configure(createMeterId("spring.boot"), DistributionStatisticConfig.DEFAULT) + .getServiceLevelObjectiveBoundaries()).containsExactly(4000000, 5000000, 6000000); } @Test @@ -234,7 +231,7 @@ void configureWhenHasMinimumExpectedValueShouldSetMinimumExpectedToValue() { PropertiesMeterFilter filter = new PropertiesMeterFilter( createProperties("distribution.minimum-expected-value.spring.boot=10")); assertThat(filter.configure(createMeterId("spring.boot"), DistributionStatisticConfig.DEFAULT) - .getMinimumExpectedValue()).isEqualTo(Duration.ofMillis(10).toNanos()); + .getMinimumExpectedValueAsDouble()).isEqualTo(Duration.ofMillis(10).toNanos()); } @Test @@ -242,7 +239,7 @@ void configureWhenHasHigherMinimumExpectedValueShouldSetMinimumExpectedValueToVa PropertiesMeterFilter filter = new PropertiesMeterFilter( createProperties("distribution.minimum-expected-value.spring=10")); assertThat(filter.configure(createMeterId("spring.boot"), DistributionStatisticConfig.DEFAULT) - .getMinimumExpectedValue()).isEqualTo(Duration.ofMillis(10).toNanos()); + .getMinimumExpectedValueAsDouble()).isEqualTo(Duration.ofMillis(10).toNanos()); } @Test @@ -250,7 +247,7 @@ void configureWhenHasHigherMinimumExpectedValueAndLowerShouldSetMinimumExpectedV PropertiesMeterFilter filter = new PropertiesMeterFilter(createProperties( "distribution.minimum-expected-value.spring=10", "distribution.minimum-expected-value.spring.boot=50")); assertThat(filter.configure(createMeterId("spring.boot"), DistributionStatisticConfig.DEFAULT) - .getMinimumExpectedValue()).isEqualTo(Duration.ofMillis(50).toNanos()); + .getMinimumExpectedValueAsDouble()).isEqualTo(Duration.ofMillis(50).toNanos()); } @Test @@ -258,7 +255,7 @@ void configureWhenHasMaximumExpectedValueShouldSetMaximumExpectedToValue() { PropertiesMeterFilter filter = new PropertiesMeterFilter( createProperties("distribution.maximum-expected-value.spring.boot=5000")); assertThat(filter.configure(createMeterId("spring.boot"), DistributionStatisticConfig.DEFAULT) - .getMaximumExpectedValue()).isEqualTo(Duration.ofMillis(5000).toNanos()); + .getMaximumExpectedValueAsDouble()).isEqualTo(Duration.ofMillis(5000).toNanos()); } @Test @@ -266,7 +263,7 @@ void configureWhenHasHigherMaximumExpectedValueShouldSetMaximumExpectedValueToVa PropertiesMeterFilter filter = new PropertiesMeterFilter( createProperties("distribution.maximum-expected-value.spring=5000")); assertThat(filter.configure(createMeterId("spring.boot"), DistributionStatisticConfig.DEFAULT) - .getMaximumExpectedValue()).isEqualTo(Duration.ofMillis(5000).toNanos()); + .getMaximumExpectedValueAsDouble()).isEqualTo(Duration.ofMillis(5000).toNanos()); } @Test @@ -275,7 +272,7 @@ void configureWhenHasHigherMaximumExpectedValueAndLowerShouldSetMaximumExpectedV createProperties("distribution.maximum-expected-value.spring=5000", "distribution.maximum-expected-value.spring.boot=10000")); assertThat(filter.configure(createMeterId("spring.boot"), DistributionStatisticConfig.DEFAULT) - .getMaximumExpectedValue()).isEqualTo(Duration.ofMillis(10000).toNanos()); + .getMaximumExpectedValueAsDouble()).isEqualTo(Duration.ofMillis(10000).toNanos()); } private Id createMeterId(String name) { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/ServiceLevelAgreementBoundaryTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/ServiceLevelAgreementBoundaryTests.java deleted file mode 100644 index d9cb37e7f1d1..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/ServiceLevelAgreementBoundaryTests.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.autoconfigure.metrics; - -import io.micrometer.core.instrument.Meter.Type; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link ServiceLevelAgreementBoundary}. - * - * @author Phillip Webb - */ -class ServiceLevelAgreementBoundaryTests { - - @Test - void getValueForTimerWhenFromLongShouldReturnMsToNanosValue() { - ServiceLevelAgreementBoundary sla = ServiceLevelAgreementBoundary.valueOf(123L); - assertThat(sla.getValue(Type.TIMER)).isEqualTo(123000000); - } - - @Test - void getValueForTimerWhenFromNumberStringShouldMsToNanosValue() { - ServiceLevelAgreementBoundary sla = ServiceLevelAgreementBoundary.valueOf("123"); - assertThat(sla.getValue(Type.TIMER)).isEqualTo(123000000); - } - - @Test - void getValueForTimerWhenFromDurationStringShouldReturnDurationNanos() { - ServiceLevelAgreementBoundary sla = ServiceLevelAgreementBoundary.valueOf("123ms"); - assertThat(sla.getValue(Type.TIMER)).isEqualTo(123000000); - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/ServiceLevelObjectiveBoundaryTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/ServiceLevelObjectiveBoundaryTests.java new file mode 100644 index 000000000000..8fd5eb246115 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/ServiceLevelObjectiveBoundaryTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.metrics; + +import java.time.Duration; + +import io.micrometer.core.instrument.Meter.Type; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ServiceLevelObjectiveBoundary}. + * + * @author Phillip Webb + * @author Stephane Nicoll + */ +class ServiceLevelObjectiveBoundaryTests { + + @Test + void getValueForTimerWhenFromLongShouldReturnMsToNanosValue() { + ServiceLevelObjectiveBoundary slo = ServiceLevelObjectiveBoundary.valueOf(123L); + assertThat(slo.getValue(Type.TIMER)).isEqualTo(123000000); + } + + @Test + void getValueForTimerWhenFromNumberStringShouldMsToNanosValue() { + ServiceLevelObjectiveBoundary slo = ServiceLevelObjectiveBoundary.valueOf("123"); + assertThat(slo.getValue(Type.TIMER)).isEqualTo(123000000); + } + + @Test + void getValueForTimerWhenFromMillisecondDurationStringShouldReturnDurationNanos() { + ServiceLevelObjectiveBoundary slo = ServiceLevelObjectiveBoundary.valueOf("123ms"); + assertThat(slo.getValue(Type.TIMER)).isEqualTo(123000000); + } + + @Test + void getValueForTimerWhenFromDaysDurationStringShouldReturnDurationNanos() { + ServiceLevelObjectiveBoundary slo = ServiceLevelObjectiveBoundary.valueOf("1d"); + assertThat(slo.getValue(Type.TIMER)).isEqualTo(Duration.ofDays(1).toNanos()); + } + + @Test + void getValueForDistributionSummaryWhenFromDoubleShouldReturnDoubleValue() { + ServiceLevelObjectiveBoundary slo = ServiceLevelObjectiveBoundary.valueOf(123.42); + assertThat(slo.getValue(Type.DISTRIBUTION_SUMMARY)).isEqualTo(123.42); + } + + @Test + void getValueForDistributionSummaryWhenFromStringShouldReturnDoubleValue() { + ServiceLevelObjectiveBoundary slo = ServiceLevelObjectiveBoundary.valueOf("123.42"); + assertThat(slo.getValue(Type.DISTRIBUTION_SUMMARY)).isEqualTo(123.42); + } + + @Test + void getValueForDistributionSummaryWhenFromDurationShouldReturnNull() { + ServiceLevelObjectiveBoundary slo = ServiceLevelObjectiveBoundary.valueOf("123ms"); + assertThat(slo.getValue(Type.DISTRIBUTION_SUMMARY)).isNull(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/ValidationFailureAnalyzerTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/ValidationFailureAnalyzerTests.java new file mode 100644 index 000000000000..804748c5fc4b --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/ValidationFailureAnalyzerTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.metrics; + +import io.micrometer.core.instrument.Clock; +import io.micrometer.newrelic.NewRelicMeterRegistry; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.metrics.export.newrelic.NewRelicProperties; +import org.springframework.boot.actuate.autoconfigure.metrics.export.newrelic.NewRelicPropertiesConfigAdapter; +import org.springframework.boot.diagnostics.FailureAnalysis; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Tests for {@link ValidationFailureAnalyzer}. + * + * @author Andy Wilkinson + */ +class ValidationFailureAnalyzerTests { + + @Test + void analyzesMissingRequiredConfiguration() { + FailureAnalysis analysis = new ValidationFailureAnalyzer() + .analyze(createFailure(MissingAccountIdAndApiKeyConfiguration.class)); + assertThat(analysis).isNotNull(); + assertThat(analysis.getCause().getMessage()).contains("management.metrics.export.newrelic.apiKey was 'null'"); + assertThat(analysis.getDescription()).isEqualTo(String.format("Invalid Micrometer configuration detected:%n%n" + + " - management.metrics.export.newrelic.apiKey was 'null' but it is required when publishing to Insights API%n" + + " - management.metrics.export.newrelic.accountId was 'null' but it is required when publishing to Insights API")); + } + + private Exception createFailure(Class configuration) { + try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(configuration)) { + fail("Expected failure did not occur"); + return null; + } + catch (Exception ex) { + return ex; + } + } + + @Configuration(proxyBeanMethods = false) + @Import(NewRelicProperties.class) + static class MissingAccountIdAndApiKeyConfiguration { + + @Bean + NewRelicMeterRegistry meterRegistry(NewRelicProperties newRelicProperties) { + return new NewRelicMeterRegistry(new NewRelicPropertiesConfigAdapter(newRelicProperties), Clock.SYSTEM); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/amqp/RabbitMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/amqp/RabbitMetricsAutoConfigurationTests.java index 48fc19121aac..dd2c9931a319 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/amqp/RabbitMetricsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/amqp/RabbitMetricsAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,10 +19,14 @@ import io.micrometer.core.instrument.MeterRegistry; import org.junit.jupiter.api.Test; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import static org.assertj.core.api.Assertions.assertThat; @@ -33,7 +37,7 @@ */ class RabbitMetricsAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner().with(MetricsRun.simple()) + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().with(MetricsRun.simple()) .withConfiguration( AutoConfigurations.of(RabbitAutoConfiguration.class, RabbitMetricsAutoConfiguration.class)); @@ -45,6 +49,15 @@ void autoConfiguredConnectionFactoryIsInstrumented() { }); } + @Test + void abstractConnectionFactoryDefinedAsAConnectionFactoryIsInstrumented() { + this.contextRunner.withUserConfiguration(ConnectionFactoryConfiguration.class).run((context) -> { + assertThat(context).hasBean("customConnectionFactory"); + MeterRegistry registry = context.getBean(MeterRegistry.class); + registry.get("rabbitmq.connections").meter(); + }); + } + @Test void rabbitmqNativeConnectionFactoryInstrumentationCanBeDisabled() { this.contextRunner.withPropertyValues("management.metrics.enable.rabbitmq=false").run((context) -> { @@ -53,4 +66,14 @@ void rabbitmqNativeConnectionFactoryInstrumentationCanBeDisabled() { }); } + @Configuration + static class ConnectionFactoryConfiguration { + + @Bean + ConnectionFactory customConnectionFactory() { + return new CachingConnectionFactory(); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/cache/CacheMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/cache/CacheMetricsAutoConfigurationTests.java index fb40f572065d..8850c2979990 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/cache/CacheMetricsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/cache/CacheMetricsAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,7 @@ */ class CacheMetricsAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner().with(MetricsRun.simple()) + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().with(MetricsRun.simple()) .withUserConfiguration(CachingConfiguration.class).withConfiguration( AutoConfigurations.of(CacheAutoConfiguration.class, CacheMetricsAutoConfiguration.class)); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/data/MetricsRepositoryMethodInvocationListenerBeanPostProcessorTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/data/MetricsRepositoryMethodInvocationListenerBeanPostProcessorTests.java new file mode 100644 index 000000000000..2077bfeefac7 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/data/MetricsRepositoryMethodInvocationListenerBeanPostProcessorTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.metrics.data; + +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import org.springframework.boot.actuate.metrics.data.MetricsRepositoryMethodInvocationListener; +import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; +import org.springframework.data.repository.core.support.RepositoryFactoryCustomizer; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.util.function.SingletonSupplier; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link MetricsRepositoryMethodInvocationListenerBeanPostProcessor} . + * + * @author Phillip Webb + */ +class MetricsRepositoryMethodInvocationListenerBeanPostProcessorTests { + + private MetricsRepositoryMethodInvocationListener listener = mock(MetricsRepositoryMethodInvocationListener.class); + + private MetricsRepositoryMethodInvocationListenerBeanPostProcessor postProcessor = new MetricsRepositoryMethodInvocationListenerBeanPostProcessor( + SingletonSupplier.of(this.listener)); + + @Test + @SuppressWarnings("rawtypes") + void postProcessBeforeInitializationWhenRepositoryFactoryBeanSupportAddsListener() { + RepositoryFactoryBeanSupport bean = mock(RepositoryFactoryBeanSupport.class); + Object result = this.postProcessor.postProcessBeforeInitialization(bean, "name"); + assertThat(result).isSameAs(bean); + ArgumentCaptor customizer = ArgumentCaptor + .forClass(RepositoryFactoryCustomizer.class); + then(bean).should().addRepositoryFactoryCustomizer(customizer.capture()); + RepositoryFactorySupport repositoryFactory = mock(RepositoryFactorySupport.class); + customizer.getValue().customize(repositoryFactory); + then(repositoryFactory).should().addInvocationListener(this.listener); + } + + @Test + void postProcessBeforeInitializationWhenOtherBeanDoesNothing() { + Object bean = new Object(); + Object result = this.postProcessor.postProcessBeforeInitialization(bean, "name"); + assertThat(result).isSameAs(bean); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/data/RepositoryMetricsAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/data/RepositoryMetricsAutoConfigurationIntegrationTests.java new file mode 100644 index 000000000000..53c110fdeec1 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/data/RepositoryMetricsAutoConfigurationIntegrationTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.metrics.data; + +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.binder.MeterBinder; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.metrics.data.city.CityRepository; +import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; +import org.springframework.boot.autoconfigure.AutoConfigurationPackage; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link RepositoryMetricsAutoConfiguration}. + * + * @author Phillip Webb + */ +class RepositoryMetricsAutoConfigurationIntegrationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().with(MetricsRun.simple()) + .withConfiguration( + AutoConfigurations.of(HibernateJpaAutoConfiguration.class, JpaRepositoriesAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class, RepositoryMetricsAutoConfiguration.class)) + .withUserConfiguration(EmbeddedDataSourceConfiguration.class, TestConfig.class); + + @Test + void repositoryMethodCallRecordsMetrics() { + this.contextRunner.run((context) -> { + context.getBean(CityRepository.class).count(); + MeterRegistry registry = context.getBean(MeterRegistry.class); + assertThat(registry.get("spring.data.repository.invocations").tag("repository", "CityRepository").timer() + .count()).isEqualTo(1); + }); + } + + @Test + void doesNotPreventMeterBindersFromDependingUponSpringDataRepositories() { + this.contextRunner.withUserConfiguration(SpringDataRepositoryMeterBinderConfiguration.class) + .run((context) -> assertThat(context).hasNotFailed()); + } + + @Configuration(proxyBeanMethods = false) + @AutoConfigurationPackage + static class TestConfig { + + } + + @Configuration(proxyBeanMethods = false) + static class SpringDataRepositoryMeterBinderConfiguration { + + @Bean + MeterBinder meterBinder(CityRepository repository) { + return (registry) -> Gauge.builder("city.count", repository::count); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/data/RepositoryMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/data/RepositoryMetricsAutoConfigurationTests.java new file mode 100644 index 000000000000..bf461b030f1c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/data/RepositoryMetricsAutoConfigurationTests.java @@ -0,0 +1,220 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.metrics.data; + +import java.util.Collection; +import java.util.Collections; +import java.util.function.Supplier; + +import io.micrometer.core.annotation.Timed; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.binder.MeterBinder; +import io.micrometer.core.instrument.distribution.HistogramSnapshot; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; +import org.springframework.boot.actuate.metrics.AutoTimer; +import org.springframework.boot.actuate.metrics.data.DefaultRepositoryTagsProvider; +import org.springframework.boot.actuate.metrics.data.MetricsRepositoryMethodInvocationListener; +import org.springframework.boot.actuate.metrics.data.RepositoryTagsProvider; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocation; +import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocationResult; +import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocationResult.State; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link RepositoryMetricsAutoConfiguration}. + * + * @author Phillip Webb + */ +class RepositoryMetricsAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().with(MetricsRun.simple()) + .withConfiguration(AutoConfigurations.of(RepositoryMetricsAutoConfiguration.class)); + + @Test + void backsOffWhenMeterRegistryIsMissing() { + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(RepositoryMetricsAutoConfiguration.class)) + .run((context) -> assertThat(context).doesNotHaveBean(RepositoryTagsProvider.class)); + } + + @Test + void definesTagsProviderAndListenerWhenMeterRegistryIsPresent() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(DefaultRepositoryTagsProvider.class); + assertThat(context).hasSingleBean(MetricsRepositoryMethodInvocationListener.class); + assertThat(context).hasSingleBean(MetricsRepositoryMethodInvocationListenerBeanPostProcessor.class); + }); + } + + @Test + void tagsProviderBacksOff() { + this.contextRunner.withUserConfiguration(TagsProviderConfiguration.class).run((context) -> { + assertThat(context).doesNotHaveBean(DefaultRepositoryTagsProvider.class); + assertThat(context).hasSingleBean(TestRepositoryTagsProvider.class); + }); + } + + @Test + void metricsRepositoryMethodInvocationListenerBacksOff() { + this.contextRunner.withUserConfiguration(MetricsRepositoryMethodInvocationListenerConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(MetricsRepositoryMethodInvocationListener.class); + assertThat(context).hasSingleBean(TestMetricsRepositoryMethodInvocationListener.class); + }); + } + + @Test + void metricNameCanBeConfigured() { + this.contextRunner.withPropertyValues("management.metrics.data.repository.metric-name=datarepo") + .run((context) -> { + MeterRegistry registry = getInitializedMeterRegistry(context, ExampleRepository.class); + Timer timer = registry.get("datarepo").timer(); + assertThat(timer).isNotNull(); + }); + } + + @Test + void autoTimeRequestsCanBeConfigured() { + this.contextRunner.withPropertyValues("management.metrics.data.repository.autotime.enabled=true", + "management.metrics.data.repository.autotime.percentiles=0.5,0.7").run((context) -> { + MeterRegistry registry = getInitializedMeterRegistry(context, ExampleRepository.class); + Timer timer = registry.get("spring.data.repository.invocations").timer(); + HistogramSnapshot snapshot = timer.takeSnapshot(); + assertThat(snapshot.percentileValues()).hasSize(2); + assertThat(snapshot.percentileValues()[0].percentile()).isEqualTo(0.5); + assertThat(snapshot.percentileValues()[1].percentile()).isEqualTo(0.7); + }); + } + + @Test + void timerWorksWithTimedAnnotationsWhenAutoTimeRequestsIsFalse() { + this.contextRunner.withPropertyValues("management.metrics.data.repository.autotime.enabled=false") + .run((context) -> { + MeterRegistry registry = getInitializedMeterRegistry(context, ExampleAnnotatedRepository.class); + Collection meters = registry.get("spring.data.repository.invocations").meters(); + assertThat(meters).hasSize(1); + Meter meter = meters.iterator().next(); + assertThat(meter.getId().getTag("method")).isEqualTo("count"); + }); + } + + @Test + void doesNotTriggerEarlyInitializationThatPreventsMeterBindersFromBindingMeters() { + this.contextRunner.withUserConfiguration(MeterBinderConfiguration.class) + .run((context) -> assertThat(context.getBean(MeterRegistry.class).find("binder.test").counter()) + .isNotNull()); + } + + private MeterRegistry getInitializedMeterRegistry(AssertableApplicationContext context, + Class repositoryInterface) { + MetricsRepositoryMethodInvocationListener listener = context + .getBean(MetricsRepositoryMethodInvocationListener.class); + ReflectionUtils.doWithLocalMethods(repositoryInterface, (method) -> { + RepositoryMethodInvocationResult result = mock(RepositoryMethodInvocationResult.class); + given(result.getState()).willReturn(State.SUCCESS); + RepositoryMethodInvocation invocation = new RepositoryMethodInvocation(repositoryInterface, method, result, + 10); + listener.afterInvocation(invocation); + }); + return context.getBean(MeterRegistry.class); + } + + @Configuration(proxyBeanMethods = false) + static class TagsProviderConfiguration { + + @Bean + TestRepositoryTagsProvider tagsProvider() { + return new TestRepositoryTagsProvider(); + } + + } + + private static final class TestRepositoryTagsProvider implements RepositoryTagsProvider { + + @Override + public Iterable repositoryTags(RepositoryMethodInvocation invocation) { + return Collections.emptyList(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class MeterBinderConfiguration { + + @Bean + MeterBinder meterBinder() { + return (registry) -> registry.counter("binder.test"); + } + + } + + @Configuration(proxyBeanMethods = false) + static class MetricsRepositoryMethodInvocationListenerConfiguration { + + @Bean + MetricsRepositoryMethodInvocationListener metricsRepositoryMethodInvocationListener( + ObjectFactory registry, RepositoryTagsProvider tagsProvider) { + return new TestMetricsRepositoryMethodInvocationListener(registry::getObject, tagsProvider); + } + + } + + static class TestMetricsRepositoryMethodInvocationListener extends MetricsRepositoryMethodInvocationListener { + + TestMetricsRepositoryMethodInvocationListener(Supplier registrySupplier, + RepositoryTagsProvider tagsProvider) { + super(registrySupplier, tagsProvider, "test", AutoTimer.DISABLED); + } + + } + + interface ExampleRepository extends Repository { + + long count(); + + } + + interface ExampleAnnotatedRepository extends Repository { + + @Timed + long count(); + + long delete(); + + } + + static class Example { + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/data/city/City.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/data/city/City.java new file mode 100644 index 000000000000..9b64dcbc10cc --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/data/city/City.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.metrics.data.city; + +import java.io.Serializable; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; + +@Entity +public class City implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue + private Long id; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String state; + + @Column(nullable = false) + private String country; + + @Column(nullable = false) + private String map; + + protected City() { + } + + public City(String name, String country) { + this.name = name; + this.country = country; + } + + public String getName() { + return this.name; + } + + public String getState() { + return this.state; + } + + public String getCountry() { + return this.country; + } + + public String getMap() { + return this.map; + } + + @Override + public String toString() { + return getName() + "," + getState() + "," + getCountry(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/data/city/CityRepository.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/data/city/CityRepository.java new file mode 100644 index 000000000000..1fd0a63cf582 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/data/city/CityRepository.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.metrics.data.city; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CityRepository extends JpaRepository { + + @Override + Page findAll(Pageable pageable); + + Page findByNameLikeAndCountryLikeAllIgnoringCase(String name, String country, Pageable pageable); + + City findByNameAndCountryAllIgnoringCase(String name, String country); + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/ConditionalOnEnabledMetricsExportAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/ConditionalOnEnabledMetricsExportAutoConfigurationTests.java new file mode 100644 index 000000000000..f00365ab089c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/ConditionalOnEnabledMetricsExportAutoConfigurationTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.metrics.export; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConditionalOnEnabledMetricsExport}. + * + * @author Chris Bono + */ +class ConditionalOnEnabledMetricsExportAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().with(MetricsRun.simple()); + + @Test + void exporterIsEnabledByDefault() { + this.contextRunner.run((context) -> assertThat(context).hasBean("simpleMeterRegistry")); + } + + @Test + void exporterCanBeSpecificallyDisabled() { + this.contextRunner.withPropertyValues("management.metrics.export.simple.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean("simpleMeterRegistry")); + } + + @Test + void exporterCanBeGloballyDisabled() { + this.contextRunner.withPropertyValues("management.metrics.export.defaults.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean("simpleMeterRegistry")); + } + + @Test + void exporterCanBeGloballyDisabledWithSpecificOverride() { + this.contextRunner + .withPropertyValues("management.metrics.export.defaults.enabled=false", + "management.metrics.export.simple.enabled=true") + .run((context) -> assertThat(context).hasBean("simpleMeterRegistry")); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/appoptics/AppOpticsMetricsExportAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/appoptics/AppOpticsMetricsExportAutoConfigurationTests.java index cfc8b3697c1f..c40f21373315 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/appoptics/AppOpticsMetricsExportAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/appoptics/AppOpticsMetricsExportAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,7 +52,15 @@ void autoConfiguresItsConfigAndMeterRegistry() { } @Test - void autoConfigurationCanBeDisabled() { + void autoConfigurationCanBeDisabledWithDefaultsEnabledProperty() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.metrics.export.defaults.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(AppOpticsMeterRegistry.class) + .doesNotHaveBean(AppOpticsConfig.class)); + } + + @Test + void autoConfigurationCanBeDisabledWithSpecificEnabledProperty() { this.contextRunner.withUserConfiguration(BaseConfiguration.class) .withPropertyValues("management.metrics.export.appoptics.enabled=false") .run((context) -> assertThat(context).doesNotHaveBean(AppOpticsMeterRegistry.class) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/appoptics/AppOpticsPropertiesConfigAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/appoptics/AppOpticsPropertiesConfigAdapterTests.java index ff619497c5b2..6b5de57c7fae 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/appoptics/AppOpticsPropertiesConfigAdapterTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/appoptics/AppOpticsPropertiesConfigAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,4 +61,11 @@ void whenPropertiesHostTagIsSetAdapterHostTagReturnsIt() { assertThat(createConfigAdapter(properties).hostTag()).isEqualTo("node"); } + @Test + void whenPropertiesFloorTimesIsSetAdapterFloorTimesReturnsIt() { + AppOpticsProperties properties = createProperties(); + properties.setFloorTimes(true); + assertThat(createConfigAdapter(properties).floorTimes()).isTrue(); + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/appoptics/AppOpticsPropertiesTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/appoptics/AppOpticsPropertiesTests.java index 2ca99cf87abc..5e3f4b6cec26 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/appoptics/AppOpticsPropertiesTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/appoptics/AppOpticsPropertiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +37,7 @@ void defaultValuesAreConsistent() { assertStepRegistryDefaultValues(properties, config); assertThat(properties.getUri()).isEqualToIgnoringWhitespace(config.uri()); assertThat(properties.getHostTag()).isEqualToIgnoringWhitespace(config.hostTag()); + assertThat(properties.isFloorTimes()).isEqualTo(config.floorTimes()); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/atlas/AtlasMetricsExportAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/atlas/AtlasMetricsExportAutoConfigurationTests.java index 4ffcc953bcc0..f0fdf313f1ed 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/atlas/AtlasMetricsExportAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/atlas/AtlasMetricsExportAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,7 +51,15 @@ void autoConfiguresItsConfigAndMeterRegistry() { } @Test - void autoConfigurationCanBeDisabled() { + void autoConfigurationCanBeDisabledWithDefaultsEnabledProperty() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.metrics.export.defaults.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(AtlasMeterRegistry.class) + .doesNotHaveBean(AtlasConfig.class)); + } + + @Test + void autoConfigurationCanBeDisabledWithSpecificEnabledProperty() { this.contextRunner.withUserConfiguration(BaseConfiguration.class) .withPropertyValues("management.metrics.export.atlas.enabled=false") .run((context) -> assertThat(context).doesNotHaveBean(AtlasMeterRegistry.class) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/datadog/DatadogMetricsExportAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/datadog/DatadogMetricsExportAutoConfigurationTests.java index 24a4d989a8f6..abb6192b0637 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/datadog/DatadogMetricsExportAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/datadog/DatadogMetricsExportAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -59,7 +59,15 @@ void autoConfiguresConfigAndMeterRegistry() { } @Test - void autoConfigurationCanBeDisabled() { + void autoConfigurationCanBeDisabledWithDefaultsEnabledProperty() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.metrics.export.defaults.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(DatadogMeterRegistry.class) + .doesNotHaveBean(DatadogConfig.class)); + } + + @Test + void autoConfigurationCanBeDisabledWithSpecificEnabledProperty() { this.contextRunner.withUserConfiguration(BaseConfiguration.class) .withPropertyValues("management.metrics.export.datadog.enabled=false") .run((context) -> assertThat(context).doesNotHaveBean(DatadogMeterRegistry.class) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/dynatrace/DynatraceMetricsExportAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/dynatrace/DynatraceMetricsExportAutoConfigurationTests.java index a08be99a48f9..6525488881f9 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/dynatrace/DynatraceMetricsExportAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/dynatrace/DynatraceMetricsExportAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,7 +61,15 @@ void autoConfiguresConfigAndMeterRegistry() { } @Test - void autoConfigurationCanBeDisabled() { + void autoConfigurationCanBeDisabledWithDefaultsEnabledProperty() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.metrics.export.defaults.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(DynatraceMeterRegistry.class) + .doesNotHaveBean(DynatraceConfig.class)); + } + + @Test + void autoConfigurationCanBeDisabledWithSpecificEnabledProperty() { this.contextRunner.withUserConfiguration(BaseConfiguration.class) .withPropertyValues("management.metrics.export.dynatrace.enabled=false") .run((context) -> assertThat(context).doesNotHaveBean(DynatraceMeterRegistry.class) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/dynatrace/DynatracePropertiesConfigAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/dynatrace/DynatracePropertiesConfigAdapterTests.java index 688873bf6327..8a4b25b07902 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/dynatrace/DynatracePropertiesConfigAdapterTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/dynatrace/DynatracePropertiesConfigAdapterTests.java @@ -55,4 +55,11 @@ void whenPropertiesTechnologyTypeIsSetAdapterTechnologyTypeReturnsIt() { assertThat(new DynatracePropertiesConfigAdapter(properties).technologyType()).isEqualTo("tech-1"); } + @Test + void whenPropertiesGroupIsSetAdapterGroupReturnsIt() { + DynatraceProperties properties = new DynatraceProperties(); + properties.setGroup("group-1"); + assertThat(new DynatracePropertiesConfigAdapter(properties).group()).isEqualTo("group-1"); + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/elastic/ElasticMetricsExportAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/elastic/ElasticMetricsExportAutoConfigurationTests.java index 7220e0bbef5a..d27979266224 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/elastic/ElasticMetricsExportAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/elastic/ElasticMetricsExportAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,7 +51,15 @@ void autoConfiguresConfigAndMeterRegistry() { } @Test - void autoConfigurationCanBeDisabled() { + void autoConfigurationCanBeDisabledWithDefaultsEnabledProperty() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.metrics.export.defaults.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(ElasticMeterRegistry.class) + .doesNotHaveBean(ElasticConfig.class)); + } + + @Test + void autoConfigurationCanBeDisabledWithSpecificEnabledProperty() { this.contextRunner.withUserConfiguration(BaseConfiguration.class) .withPropertyValues("management.metrics.export.elastic.enabled=false") .run((context) -> assertThat(context).doesNotHaveBean(ElasticMeterRegistry.class) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/elastic/ElasticPropertiesConfigAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/elastic/ElasticPropertiesConfigAdapterTests.java index f3a27b724a3e..46e4371d56bb 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/elastic/ElasticPropertiesConfigAdapterTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/elastic/ElasticPropertiesConfigAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,6 +48,13 @@ void whenPropertiesIndexDateFormatIsSetAdapterIndexDateFormatReturnsIt() { assertThat(new ElasticPropertiesConfigAdapter(properties).indexDateFormat()).isEqualTo("yyyy"); } + @Test + void whenPropertiesIndexDateSeparatorIsSetAdapterIndexDateSeparatorReturnsIt() { + ElasticProperties properties = new ElasticProperties(); + properties.setIndexDateSeparator("*"); + assertThat(new ElasticPropertiesConfigAdapter(properties).indexDateSeparator()).isEqualTo("*"); + } + @Test void whenPropertiesTimestampFieldNameIsSetAdapterTimestampFieldNameReturnsIt() { ElasticProperties properties = new ElasticProperties(); @@ -76,4 +83,11 @@ void whenPropertiesPasswordIsSetAdapterPasswordReturnsIt() { assertThat(new ElasticPropertiesConfigAdapter(properties).password()).isEqualTo("secret"); } + @Test + void whenPropertiesPipelineIsSetAdapterPipelineReturnsIt() { + ElasticProperties properties = new ElasticProperties(); + properties.setPipeline("testPipeline"); + assertThat(new ElasticPropertiesConfigAdapter(properties).pipeline()).isEqualTo("testPipeline"); + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/elastic/ElasticPropertiesTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/elastic/ElasticPropertiesTests.java index 407c968c14f0..7e692f0a7f04 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/elastic/ElasticPropertiesTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/elastic/ElasticPropertiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,10 +38,12 @@ void defaultValuesAreConsistent() { assertThat(properties.getHost()).isEqualTo(config.host()); assertThat(properties.getIndex()).isEqualTo(config.index()); assertThat(properties.getIndexDateFormat()).isEqualTo(config.indexDateFormat()); + assertThat(properties.getIndexDateSeparator()).isEqualTo(config.indexDateSeparator()); assertThat(properties.getPassword()).isEqualTo(config.password()); assertThat(properties.getTimestampFieldName()).isEqualTo(config.timestampFieldName()); assertThat(properties.getUserName()).isEqualTo(config.userName()); assertThat(properties.isAutoCreateIndex()).isEqualTo(config.autoCreateIndex()); + assertThat(properties.getPipeline()).isEqualTo(config.pipeline()); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/ganglia/GangliaMetricsExportAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/ganglia/GangliaMetricsExportAutoConfigurationTests.java index 517790e399ac..8dad0fbfac2a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/ganglia/GangliaMetricsExportAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/ganglia/GangliaMetricsExportAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,7 +51,15 @@ void autoConfiguresItsConfigAndMeterRegistry() { } @Test - void autoConfigurationCanBeDisabled() { + void autoConfigurationCanBeDisabledWithDefaultsEnabledProperty() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.metrics.export.defaults.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(GangliaMeterRegistry.class) + .doesNotHaveBean(GangliaConfig.class)); + } + + @Test + void autoConfigurationCanBeDisabledWithSpecificEnabledProperty() { this.contextRunner.withUserConfiguration(BaseConfiguration.class) .withPropertyValues("management.metrics.export.ganglia.enabled=false") .run((context) -> assertThat(context).doesNotHaveBean(GangliaMeterRegistry.class) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/ganglia/GangliaPropertiesTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/ganglia/GangliaPropertiesTests.java index a840f380788b..6d4fd0d0f5f0 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/ganglia/GangliaPropertiesTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/ganglia/GangliaPropertiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ class GangliaPropertiesTests { @Test + @SuppressWarnings("deprecation") void defaultValuesAreConsistent() { GangliaProperties properties = new GangliaProperties(); GangliaConfig config = GangliaConfig.DEFAULT; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/graphite/GraphiteMetricsExportAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/graphite/GraphiteMetricsExportAutoConfigurationTests.java index 7be60e9b6c2a..d2c0bdb55b84 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/graphite/GraphiteMetricsExportAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/graphite/GraphiteMetricsExportAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,6 +57,19 @@ void autoConfiguresUseTagsAsPrefix() { }); } + @Test + void autoConfiguresWithTagsAsPrefixCanBeDisabled() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.metrics.export.graphite.tags-as-prefix=app", + "management.metrics.export.graphite.graphite-tags-enabled=true") + .run((context) -> { + assertThat(context).hasSingleBean(GraphiteMeterRegistry.class); + GraphiteMeterRegistry registry = context.getBean(GraphiteMeterRegistry.class); + registry.counter("test.count", Tags.of("app", "myapp")); + assertThat(registry.getDropwizardRegistry().getMeters()).containsOnlyKeys("test.count;app=myapp"); + }); + } + @Test void autoConfiguresItsConfigAndMeterRegistry() { this.contextRunner.withUserConfiguration(BaseConfiguration.class).run((context) -> assertThat(context) @@ -64,7 +77,15 @@ void autoConfiguresItsConfigAndMeterRegistry() { } @Test - void autoConfigurationCanBeDisabled() { + void autoConfigurationCanBeDisabledWithDefaultsEnabledProperty() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.metrics.export.defaults.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(GraphiteMeterRegistry.class) + .doesNotHaveBean(GraphiteConfig.class)); + } + + @Test + void autoConfigurationCanBeDisabledWithSpecificEnabledProperty() { this.contextRunner.withUserConfiguration(BaseConfiguration.class) .withPropertyValues("management.metrics.export.graphite.enabled=false") .run((context) -> assertThat(context).doesNotHaveBean(GraphiteMeterRegistry.class) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/graphite/GraphitePropertiesTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/graphite/GraphitePropertiesTests.java index 54aef944a34f..ea3cf5358cfa 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/graphite/GraphitePropertiesTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/graphite/GraphitePropertiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,7 +39,23 @@ void defaultValuesAreConsistent() { assertThat(properties.getHost()).isEqualTo(config.host()); assertThat(properties.getPort()).isEqualTo(config.port()); assertThat(properties.getProtocol()).isEqualTo(config.protocol()); + assertThat(properties.getGraphiteTagsEnabled()).isEqualTo(config.graphiteTagsEnabled()); assertThat(properties.getTagsAsPrefix()).isEqualTo(config.tagsAsPrefix()); } + @Test + void graphiteTagsAreDisabledIfTagsAsPrefixIsSet() { + GraphiteProperties properties = new GraphiteProperties(); + properties.setTagsAsPrefix(new String[] { "app" }); + assertThat(properties.getGraphiteTagsEnabled()).isFalse(); + } + + @Test + void graphiteTagsCanBeEnabledEvenIfTagsAsPrefixIsSet() { + GraphiteProperties properties = new GraphiteProperties(); + properties.setGraphiteTagsEnabled(true); + properties.setTagsAsPrefix(new String[] { "app" }); + assertThat(properties.getGraphiteTagsEnabled()).isTrue(); + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/humio/HumioMetricsExportAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/humio/HumioMetricsExportAutoConfigurationTests.java index 0e53b0270356..7efe69360b29 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/humio/HumioMetricsExportAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/humio/HumioMetricsExportAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,7 +52,15 @@ void autoConfiguresConfigAndMeterRegistry() { } @Test - void autoConfigurationCanBeDisabled() { + void autoConfigurationCanBeDisabledWithDefaultsEnabledProperty() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.metrics.export.defaults.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(HumioMeterRegistry.class) + .doesNotHaveBean(HumioConfig.class)); + } + + @Test + void autoConfigurationCanBeDisabledWithSpecificEnabledProperty() { this.contextRunner.withUserConfiguration(BaseConfiguration.class) .withPropertyValues("management.metrics.export.humio.enabled=false") .run((context) -> assertThat(context).doesNotHaveBean(HumioMeterRegistry.class) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/humio/HumioPropertiesConfigAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/humio/HumioPropertiesConfigAdapterTests.java index 5f8fdcc176d3..dffce7e8eb3e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/humio/HumioPropertiesConfigAdapterTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/humio/HumioPropertiesConfigAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,14 +36,6 @@ void whenApiTokenIsSetAdapterApiTokenReturnsIt() { assertThat(new HumioPropertiesConfigAdapter(properties).apiToken()).isEqualTo("ABC123"); } - @Test - @Deprecated - void whenPropertiesRepositoryIsSetAdapterRepositoryReturnsIt() { - HumioProperties properties = new HumioProperties(); - properties.setRepository("test"); - assertThat(new HumioPropertiesConfigAdapter(properties).repository()).isEqualTo("test"); - } - @Test void whenPropertiesTagsIsSetAdapterTagsReturnsIt() { HumioProperties properties = new HumioProperties(); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/humio/HumioPropertiesTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/humio/HumioPropertiesTests.java index cc141cdc1a76..d5c44e1a5d04 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/humio/HumioPropertiesTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/humio/HumioPropertiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,13 +31,11 @@ class HumioPropertiesTests extends StepRegistryPropertiesTests { @Test - @SuppressWarnings("deprecation") void defaultValuesAreConsistent() { HumioProperties properties = new HumioProperties(); HumioConfig config = (key) -> null; assertStepRegistryDefaultValues(properties, config); assertThat(properties.getApiToken()).isEqualTo(config.apiToken()); - assertThat(properties.getRepository()).isEqualTo(config.repository()); assertThat(properties.getTags()).isEmpty(); assertThat(config.tags()).isNull(); assertThat(properties.getUri()).isEqualTo(config.uri()); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/influx/InfluxMetricsExportAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/influx/InfluxMetricsExportAutoConfigurationTests.java index 5397b2c175fd..5b306b7f61ed 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/influx/InfluxMetricsExportAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/influx/InfluxMetricsExportAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,7 +51,15 @@ void autoConfiguresItsConfigAndMeterRegistry() { } @Test - void autoConfigurationCanBeDisabled() { + void autoConfigurationCanBeDisabledWithDefaultsEnabledProperty() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.metrics.export.defaults.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(InfluxMeterRegistry.class) + .doesNotHaveBean(InfluxConfig.class)); + } + + @Test + void autoConfigurationCanBeDisabledWithSpecificEnabledProperty() { this.contextRunner.withUserConfiguration(BaseConfiguration.class) .withPropertyValues("management.metrics.export.influx.enabled=false") .run((context) -> assertThat(context).doesNotHaveBean(InfluxMeterRegistry.class) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/influx/InfluxPropertiesConfigAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/influx/InfluxPropertiesConfigAdapterTests.java new file mode 100644 index 000000000000..2d86262c668a --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/influx/InfluxPropertiesConfigAdapterTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.metrics.export.influx; + +import io.micrometer.influx.InfluxApiVersion; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link InfluxPropertiesConfigAdapter}. + * + * @author Stephane Nicoll + */ +class InfluxPropertiesConfigAdapterTests { + + @Test + void adaptInfluxV1BasicConfig() { + InfluxProperties properties = new InfluxProperties(); + properties.setDb("test-db"); + properties.setUri("https://influx.example.com:8086"); + properties.setUserName("user"); + properties.setPassword("secret"); + InfluxPropertiesConfigAdapter adapter = new InfluxPropertiesConfigAdapter(properties); + assertThat(adapter.apiVersion()).isEqualTo(InfluxApiVersion.V1); + assertThat(adapter.db()).isEqualTo("test-db"); + assertThat(adapter.uri()).isEqualTo("https://influx.example.com:8086"); + assertThat(adapter.userName()).isEqualTo("user"); + assertThat(adapter.password()).isEqualTo("secret"); + } + + @Test + void adaptInfluxV2BasicConfig() { + InfluxProperties properties = new InfluxProperties(); + properties.setOrg("test-org"); + properties.setBucket("test-bucket"); + properties.setUri("https://influx.example.com:8086"); + properties.setToken("token"); + InfluxPropertiesConfigAdapter adapter = new InfluxPropertiesConfigAdapter(properties); + assertThat(adapter.apiVersion()).isEqualTo(InfluxApiVersion.V2); + assertThat(adapter.org()).isEqualTo("test-org"); + assertThat(adapter.bucket()).isEqualTo("test-bucket"); + assertThat(adapter.uri()).isEqualTo("https://influx.example.com:8086"); + assertThat(adapter.token()).isEqualTo("token"); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/influx/InfluxPropertiesTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/influx/InfluxPropertiesTests.java index c45eb100277a..3511c268bc56 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/influx/InfluxPropertiesTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/influx/InfluxPropertiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,6 +46,8 @@ void defaultValuesAreConsistent() { assertThat(properties.getUri()).isEqualTo(config.uri()); assertThat(properties.isCompressed()).isEqualTo(config.compressed()); assertThat(properties.isAutoCreateDb()).isEqualTo(config.autoCreateDb()); + assertThat(properties.getOrg()).isEqualTo(config.org()); + assertThat(properties.getToken()).isEqualTo(config.token()); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/jmx/JmxMetricsExportAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/jmx/JmxMetricsExportAutoConfigurationTests.java index eea0f5ac7101..fc303d940b04 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/jmx/JmxMetricsExportAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/jmx/JmxMetricsExportAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,7 +51,15 @@ void autoConfiguresItsConfigAndMeterRegistry() { } @Test - void autoConfigurationCanBeDisabled() { + void autoConfigurationCanBeDisabledWithDefaultsEnabledProperty() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.metrics.export.defaults.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(JmxMeterRegistry.class) + .doesNotHaveBean(JmxConfig.class)); + } + + @Test + void autoConfigurationCanBeDisabledWithSpecificEnabledProperty() { this.contextRunner.withUserConfiguration(BaseConfiguration.class) .withPropertyValues("management.metrics.export.jmx.enabled=false").run((context) -> assertThat(context) .doesNotHaveBean(JmxMeterRegistry.class).doesNotHaveBean(JmxConfig.class)); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/kairos/KairosMetricsExportAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/kairos/KairosMetricsExportAutoConfigurationTests.java index 3e260c87d186..44d73ac432ac 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/kairos/KairosMetricsExportAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/kairos/KairosMetricsExportAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,7 +51,15 @@ void autoConfiguresItsConfigAndMeterRegistry() { } @Test - void autoConfigurationCanBeDisabled() { + void autoConfigurationCanBeDisabledWithDefaultsEnabledProperty() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.metrics.export.defaults.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(KairosMeterRegistry.class) + .doesNotHaveBean(KairosConfig.class)); + } + + @Test + void autoConfigurationCanBeDisabledWithSpecificEnabledProperty() { this.contextRunner.withUserConfiguration(BaseConfiguration.class) .withPropertyValues("management.metrics.export.kairos.enabled=false") .run((context) -> assertThat(context).doesNotHaveBean(KairosMeterRegistry.class) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/newrelic/NewRelicMetricsExportAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/newrelic/NewRelicMetricsExportAutoConfigurationTests.java index 8df7196c21e5..e0f063cc63ba 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/newrelic/NewRelicMetricsExportAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/newrelic/NewRelicMetricsExportAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.boot.actuate.autoconfigure.metrics.export.newrelic; import io.micrometer.core.instrument.Clock; +import io.micrometer.newrelic.NewRelicClientProvider; import io.micrometer.newrelic.NewRelicConfig; import io.micrometer.newrelic.NewRelicMeterRegistry; import org.junit.jupiter.api.Test; @@ -28,12 +29,14 @@ import org.springframework.context.annotation.Import; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * * Tests for {@link NewRelicMetricsExportAutoConfiguration}. * * @author Andy Wilkinson + * @author Stephane Nicoll */ class NewRelicMetricsExportAutoConfigurationTests { @@ -69,7 +72,7 @@ void failsToAutoConfigureWithoutEventType() { } @Test - void autoConfiguresWithEventTypeOverriden() { + void autoConfiguresWithEventTypeOverridden() { this.contextRunner.withUserConfiguration(BaseConfiguration.class) .withPropertyValues("management.metrics.export.newrelic.api-key=abcde", "management.metrics.export.newrelic.account-id=12345", @@ -99,7 +102,15 @@ void autoConfiguresWithAccountIdAndApiKey() { } @Test - void autoConfigurationCanBeDisabled() { + void autoConfigurationCanBeDisabledWithDefaultsEnabledProperty() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.metrics.export.defaults.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(NewRelicMeterRegistry.class) + .doesNotHaveBean(NewRelicConfig.class)); + } + + @Test + void autoConfigurationCanBeDisabledWithSpecificEnabledProperty() { this.contextRunner.withUserConfiguration(BaseConfiguration.class) .withPropertyValues("management.metrics.export.newrelic.enabled=false") .run((context) -> assertThat(context).doesNotHaveBean(NewRelicMeterRegistry.class) @@ -123,6 +134,18 @@ void allowsRegistryToBeCustomized() { .hasBean("customRegistry")); } + @Test + void allowsClientProviderToBeCustomized() { + this.contextRunner.withUserConfiguration(CustomClientProviderConfiguration.class) + .withPropertyValues("management.metrics.export.newrelic.api-key=abcde", + "management.metrics.export.newrelic.account-id=12345") + .run((context) -> { + assertThat(context).hasSingleBean(NewRelicMeterRegistry.class); + assertThat(context.getBean(NewRelicMeterRegistry.class)) + .hasFieldOrPropertyWithValue("clientProvider", context.getBean("customClientProvider")); + }); + } + @Test void stopsMeterRegistryWhenContextIsClosed() { this.contextRunner @@ -176,4 +199,15 @@ NewRelicMeterRegistry customRegistry(NewRelicConfig config, Clock clock) { } + @Configuration(proxyBeanMethods = false) + @Import(BaseConfiguration.class) + static class CustomClientProviderConfiguration { + + @Bean + NewRelicClientProvider customClientProvider() { + return mock(NewRelicClientProvider.class); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/newrelic/NewRelicPropertiesTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/newrelic/NewRelicPropertiesTests.java index b2c416f9869b..1175a46dfb05 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/newrelic/NewRelicPropertiesTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/newrelic/NewRelicPropertiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,13 +35,14 @@ void defaultValuesAreConsistent() { NewRelicProperties properties = new NewRelicProperties(); NewRelicConfig config = (key) -> null; assertStepRegistryDefaultValues(properties, config); + assertThat(properties.getClientProviderType()).isEqualTo(config.clientProviderType()); // apiKey and account are mandatory assertThat(properties.getUri()).isEqualTo(config.uri()); assertThat(properties.isMeterNameEventTypeEnabled()).isEqualTo(config.meterNameEventTypeEnabled()); } @Test - void eventTypeDefaultValueIsOverriden() { + void eventTypeDefaultValueIsOverridden() { NewRelicProperties properties = new NewRelicProperties(); NewRelicConfig config = (key) -> null; assertThat(properties.getEventType()).isNotEqualTo(config.eventType()); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusMetricsExportAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusMetricsExportAutoConfigurationTests.java index d1b947dba601..b19f63c3bccc 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusMetricsExportAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusMetricsExportAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,16 @@ package org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus; +import java.util.function.Consumer; + import io.micrometer.core.instrument.Clock; import io.micrometer.prometheus.PrometheusConfig; import io.micrometer.prometheus.PrometheusMeterRegistry; import io.prometheus.client.CollectorRegistry; +import io.prometheus.client.exporter.BasicAuthHttpConnectionFactory; +import io.prometheus.client.exporter.DefaultHttpConnectionFactory; +import io.prometheus.client.exporter.HttpConnectionFactory; +import io.prometheus.client.exporter.PushGateway; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -29,6 +35,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.context.annotation.Bean; @@ -42,6 +49,7 @@ * Tests for {@link PrometheusMetricsExportAutoConfiguration}. * * @author Andy Wilkinson + * @author Stephane Nicoll */ @ExtendWith(OutputCaptureExtension.class) class PrometheusMetricsExportAutoConfigurationTests { @@ -62,8 +70,17 @@ void autoConfiguresItsConfigCollectorRegistryAndMeterRegistry() { } @Test - void autoConfigurationCanBeDisabled() { - this.contextRunner.withPropertyValues("management.metrics.export.prometheus.enabled=false") + void autoConfigurationCanBeDisabledWithDefaultsEnabledProperty() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.metrics.export.defaults.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(PrometheusMeterRegistry.class) + .doesNotHaveBean(CollectorRegistry.class).doesNotHaveBean(PrometheusConfig.class)); + } + + @Test + void autoConfigurationCanBeDisabledWithSpecificEnabledProperty() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.metrics.export.prometheus.enabled=false") .run((context) -> assertThat(context).doesNotHaveBean(PrometheusMeterRegistry.class) .doesNotHaveBean(CollectorRegistry.class).doesNotHaveBean(PrometheusConfig.class)); } @@ -123,6 +140,12 @@ void allowsCustomScrapeEndpointToBeUsed() { .hasBean("customEndpoint").hasSingleBean(PrometheusScrapeEndpoint.class)); } + @Test + void pushGatewayIsNotConfiguredWhenEnabledFlagIsNotSet() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(PrometheusPushGatewayManager.class)); + } + @Test void withPushGatewayEnabled(CapturedOutput output) { this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) @@ -133,6 +156,15 @@ void withPushGatewayEnabled(CapturedOutput output) { }); } + @Test + void withPushGatewayNoBasicAuth() { + this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) + .withPropertyValues("management.metrics.export.prometheus.pushgateway.enabled=true") + .withUserConfiguration(BaseConfiguration.class) + .run(hasHttpConnectionFactory((httpConnectionFactory) -> assertThat(httpConnectionFactory) + .isInstanceOf(DefaultHttpConnectionFactory.class))); + } + @Test @Deprecated void withCustomLegacyPushGatewayURL(CapturedOutput output) { @@ -154,11 +186,34 @@ void withCustomPushGatewayURL() { .run((context) -> hasGatewayURL(context, "https://example.com:8080/metrics/")); } + @Test + void withPushGatewayBasicAuth() { + this.contextRunner.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class)) + .withPropertyValues("management.metrics.export.prometheus.pushgateway.enabled=true", + "management.metrics.export.prometheus.pushgateway.username=admin", + "management.metrics.export.prometheus.pushgateway.password=secret") + .withUserConfiguration(BaseConfiguration.class) + .run(hasHttpConnectionFactory((httpConnectionFactory) -> assertThat(httpConnectionFactory) + .isInstanceOf(BasicAuthHttpConnectionFactory.class))); + } + private void hasGatewayURL(AssertableApplicationContext context, String url) { + assertThat(getPushGateway(context)).hasFieldOrPropertyWithValue("gatewayBaseURL", url); + } + + private ContextConsumer hasHttpConnectionFactory( + Consumer httpConnectionFactory) { + return (context) -> { + PushGateway pushGateway = getPushGateway(context); + httpConnectionFactory + .accept((HttpConnectionFactory) ReflectionTestUtils.getField(pushGateway, "connectionFactory")); + }; + } + + private PushGateway getPushGateway(AssertableApplicationContext context) { assertThat(context).hasSingleBean(PrometheusPushGatewayManager.class); PrometheusPushGatewayManager gatewayManager = context.getBean(PrometheusPushGatewayManager.class); - Object pushGateway = ReflectionTestUtils.getField(gatewayManager, "pushGateway"); - assertThat(pushGateway).hasFieldOrPropertyWithValue("gatewayBaseURL", url); + return (PushGateway) ReflectionTestUtils.getField(gatewayManager, "pushGateway"); } @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesTests.java index 993986a80b5e..3cfc3c5ac9c5 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/prometheus/PrometheusPropertiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,7 @@ void defaultValuesAreConsistent() { PrometheusProperties properties = new PrometheusProperties(); PrometheusConfig config = PrometheusConfig.DEFAULT; assertThat(properties.isDescriptions()).isEqualTo(config.descriptions()); + assertThat(properties.getHistogramFlavor()).isEqualTo(config.histogramFlavor()); assertThat(properties.getStep()).isEqualTo(config.step()); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/properties/PushRegistryPropertiesConfigAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/properties/PushRegistryPropertiesConfigAdapterTests.java index cfb47e3a6b8a..29b2a1ec5ca3 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/properties/PushRegistryPropertiesConfigAdapterTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/properties/PushRegistryPropertiesConfigAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,7 +40,7 @@ public abstract class PushRegistryPropertiesConfigAdapterTests

assertThat(context).doesNotHaveBean(SignalFxMeterRegistry.class) + .doesNotHaveBean(SignalFxConfig.class)); + } + + @Test + void autoConfigurationCanBeDisabledWithSpecificEnabledProperty() { this.contextRunner.withUserConfiguration(BaseConfiguration.class) .withPropertyValues("management.metrics.export.signalfx.enabled=false") .run((context) -> assertThat(context).doesNotHaveBean(SignalFxMeterRegistry.class) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/simple/SimpleMetricsExportAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/simple/SimpleMetricsExportAutoConfigurationTests.java index b6415d401aa7..45f37037d028 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/simple/SimpleMetricsExportAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/simple/SimpleMetricsExportAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,7 +49,15 @@ void autoConfiguresConfigAndMeterRegistry() { } @Test - void backsOffWhenSpecificallyDisabled() { + void autoConfigurationCanBeDisabledWithDefaultsEnabledProperty() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.metrics.export.defaults.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(SimpleMeterRegistry.class) + .doesNotHaveBean(SimpleConfig.class)); + } + + @Test + void autoConfigurationCanBeDisabledWithSpecificEnabledProperty() { this.contextRunner.withUserConfiguration(BaseConfiguration.class) .withPropertyValues("management.metrics.export.simple.enabled=false") .run((context) -> assertThat(context).doesNotHaveBean(SimpleMeterRegistry.class) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/stackdriver/StackdriverMetricsExportAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/stackdriver/StackdriverMetricsExportAutoConfigurationTests.java new file mode 100644 index 000000000000..fceeebab81f1 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/stackdriver/StackdriverMetricsExportAutoConfigurationTests.java @@ -0,0 +1,140 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.metrics.export.stackdriver; + +import io.micrometer.core.instrument.Clock; +import io.micrometer.stackdriver.StackdriverConfig; +import io.micrometer.stackdriver.StackdriverMeterRegistry; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link StackdriverMetricsExportAutoConfiguration}. + * + * @author Johannes Graf + */ +class StackdriverMetricsExportAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(StackdriverMetricsExportAutoConfiguration.class)); + + @Test + void backsOffWithoutAClock() { + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(StackdriverMeterRegistry.class)); + } + + @Test + void failsWithoutAProjectId() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .run((context) -> assertThat(context).hasFailed()); + } + + @Test + void autoConfiguresConfigAndMeterRegistry() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.metrics.export.stackdriver.project-id=test-project") + .run((context) -> assertThat(context).hasSingleBean(StackdriverMeterRegistry.class) + .hasSingleBean(StackdriverConfig.class)); + } + + @Test + void autoConfigurationCanBeDisabledWithDefaultsEnabledProperty() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.metrics.export.defaults.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(StackdriverMeterRegistry.class) + .doesNotHaveBean(StackdriverConfig.class)); + } + + @Test + void autoConfigurationCanBeDisabledWithSpecificEnabledProperty() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.metrics.export.stackdriver.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(StackdriverMeterRegistry.class) + .doesNotHaveBean(StackdriverConfig.class)); + } + + @Test + void allowsCustomConfigToBeUsed() { + this.contextRunner.withUserConfiguration(CustomConfigConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(StackdriverMeterRegistry.class) + .hasSingleBean(StackdriverConfig.class).hasBean("customConfig")); + } + + @Test + void allowsCustomRegistryToBeUsed() { + this.contextRunner.withUserConfiguration(CustomRegistryConfiguration.class) + .withPropertyValues("management.metrics.export.stackdriver.project-id=test-project") + .run((context) -> assertThat(context).hasSingleBean(StackdriverMeterRegistry.class) + .hasBean("customRegistry").hasSingleBean(StackdriverConfig.class)); + } + + @Test + void stopsMeterRegistryWhenContextIsClosed() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.metrics.export.stackdriver.project-id=test-project").run((context) -> { + StackdriverMeterRegistry registry = context.getBean(StackdriverMeterRegistry.class); + assertThat(registry.isClosed()).isFalse(); + context.close(); + assertThat(registry.isClosed()).isTrue(); + }); + } + + @Configuration(proxyBeanMethods = false) + static class BaseConfiguration { + + @Bean + Clock clock() { + return Clock.SYSTEM; + } + + } + + @Configuration(proxyBeanMethods = false) + @Import(BaseConfiguration.class) + static class CustomConfigConfiguration { + + @Bean + StackdriverConfig customConfig() { + return (key) -> { + if ("stackdriver.projectId".equals(key)) { + return "test-project"; + } + return null; + }; + } + + } + + @Configuration(proxyBeanMethods = false) + @Import(BaseConfiguration.class) + static class CustomRegistryConfiguration { + + @Bean + StackdriverMeterRegistry customRegistry(StackdriverConfig config, Clock clock) { + return new StackdriverMeterRegistry(config, clock); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/stackdriver/StackdriverPropertiesConfigAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/stackdriver/StackdriverPropertiesConfigAdapterTests.java new file mode 100644 index 000000000000..74b282f64528 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/stackdriver/StackdriverPropertiesConfigAdapterTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.metrics.export.stackdriver; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link StackdriverPropertiesConfigAdapter}. + * + * @author Johannes Graf + */ +class StackdriverPropertiesConfigAdapterTests { + + @Test + void whenPropertiesProjectIdIsSetAdapterProjectIdReturnsIt() { + StackdriverProperties properties = new StackdriverProperties(); + properties.setProjectId("my-gcp-project-id"); + assertThat(new StackdriverPropertiesConfigAdapter(properties).projectId()).isEqualTo("my-gcp-project-id"); + } + + @Test + void whenPropertiesResourceTypeIsSetAdapterResourceTypeReturnsIt() { + StackdriverProperties properties = new StackdriverProperties(); + properties.setResourceType("my-resource-type"); + assertThat(new StackdriverPropertiesConfigAdapter(properties).resourceType()).isEqualTo("my-resource-type"); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/stackdriver/StackdriverPropertiesTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/stackdriver/StackdriverPropertiesTests.java new file mode 100644 index 000000000000..e612fa2a7c3f --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/stackdriver/StackdriverPropertiesTests.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.metrics.export.stackdriver; + +import io.micrometer.stackdriver.StackdriverConfig; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.StepRegistryPropertiesTests; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link StackdriverProperties}. + * + * @author Johannes Graf + */ +class StackdriverPropertiesTests extends StepRegistryPropertiesTests { + + @Test + void defaultValuesAreConsistent() { + StackdriverProperties properties = new StackdriverProperties(); + StackdriverConfig config = (key) -> null; + assertStepRegistryDefaultValues(properties, config); + assertThat(properties.getResourceType()).isEqualTo(config.resourceType()); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/statsd/StatsdMetricsExportAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/statsd/StatsdMetricsExportAutoConfigurationTests.java index 30dcdc04ec4e..59d249549181 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/statsd/StatsdMetricsExportAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/statsd/StatsdMetricsExportAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ import io.micrometer.core.instrument.Clock; import io.micrometer.statsd.StatsdConfig; import io.micrometer.statsd.StatsdMeterRegistry; -import io.micrometer.statsd.StatsdMetrics; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -47,13 +46,19 @@ void backsOffWithoutAClock() { @Test void autoConfiguresItsConfigMeterRegistryAndMetrics() { - this.contextRunner.withUserConfiguration(BaseConfiguration.class) - .run((context) -> assertThat(context).hasSingleBean(StatsdMeterRegistry.class) - .hasSingleBean(StatsdConfig.class).hasSingleBean(StatsdMetrics.class)); + this.contextRunner.withUserConfiguration(BaseConfiguration.class).run((context) -> assertThat(context) + .hasSingleBean(StatsdMeterRegistry.class).hasSingleBean(StatsdConfig.class)); } @Test - void autoConfigurationCanBeDisabled() { + void autoConfigurationCanBeDisabledWithDefaultsEnabledProperty() { + this.contextRunner.withPropertyValues("management.metrics.export.defaults.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(StatsdMeterRegistry.class) + .doesNotHaveBean(StatsdConfig.class)); + } + + @Test + void autoConfigurationCanBeDisabledWithSpecificEnabledProperty() { this.contextRunner.withPropertyValues("management.metrics.export.statsd.enabled=false") .run((context) -> assertThat(context).doesNotHaveBean(StatsdMeterRegistry.class) .doesNotHaveBean(StatsdConfig.class)); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/statsd/StatsdPropertiesTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/statsd/StatsdPropertiesTests.java index 006e4e5e2dcc..ae1f6fc60894 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/statsd/StatsdPropertiesTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/statsd/StatsdPropertiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,7 @@ void defaultValuesAreConsistent() { assertThat(properties.getFlavor()).isEqualTo(config.flavor()); assertThat(properties.getHost()).isEqualTo(config.host()); assertThat(properties.getPort()).isEqualTo(config.port()); + assertThat(properties.getProtocol()).isEqualTo(config.protocol()); assertThat(properties.getMaxPacketLength()).isEqualTo(config.maxPacketLength()); assertThat(properties.getPollingFrequency()).isEqualTo(config.pollingFrequency()); assertThat(properties.isPublishUnchangedMeters()).isEqualTo(config.publishUnchangedMeters()); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontMetricsExportAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontMetricsExportAutoConfigurationTests.java index 50c04b9c4a24..da870c84464f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontMetricsExportAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontMetricsExportAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,13 @@ package org.springframework.boot.actuate.autoconfigure.metrics.export.wavefront; +import java.util.concurrent.LinkedBlockingQueue; + +import com.wavefront.sdk.common.WavefrontSender; import io.micrometer.core.instrument.Clock; import io.micrometer.wavefront.WavefrontConfig; import io.micrometer.wavefront.WavefrontMeterRegistry; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -27,12 +31,15 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import static org.assertj.core.api.Assertions.as; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * Tests for {@link WavefrontMetricsExportAutoConfiguration}. * * @author Jon Schneider + * @author Stephane Nicoll */ class WavefrontMetricsExportAutoConfigurationTests { @@ -51,11 +58,21 @@ void failsWithoutAnApiTokenWhenPublishingDirectly() { } @Test - void autoConfigurationCanBeDisabled() { + void autoConfigurationCanBeDisabledWithDefaultsEnabledProperty() { this.contextRunner.withUserConfiguration(BaseConfiguration.class) - .withPropertyValues("management.metrics.export.wavefront.enabled=false") + .withPropertyValues("management.metrics.export.wavefront.api-token=abcde", + "management.metrics.export.defaults.enabled=false") .run((context) -> assertThat(context).doesNotHaveBean(WavefrontMeterRegistry.class) - .doesNotHaveBean(WavefrontConfig.class)); + .doesNotHaveBean(WavefrontConfig.class).doesNotHaveBean(WavefrontSender.class)); + } + + @Test + void autoConfigurationCanBeDisabledWithSpecificEnabledProperty() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.metrics.export.wavefront.api-token=abcde", + "management.metrics.export.wavefront.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(WavefrontMeterRegistry.class) + .doesNotHaveBean(WavefrontConfig.class).doesNotHaveBean(WavefrontSender.class)); } @Test @@ -63,7 +80,48 @@ void allowsConfigToBeCustomized() { this.contextRunner.withUserConfiguration(CustomConfigConfiguration.class) .run((context) -> assertThat(context).hasSingleBean(Clock.class) .hasSingleBean(WavefrontMeterRegistry.class).hasSingleBean(WavefrontConfig.class) - .hasBean("customConfig")); + .hasSingleBean(WavefrontSender.class).hasBean("customConfig")); + } + + @Test + void defaultWavefrontSenderSettingsAreConsistent() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.metrics.export.wavefront.api-token=abcde").run((context) -> { + WavefrontProperties properties = new WavefrontProperties(); + WavefrontSender sender = context.getBean(WavefrontSender.class); + assertThat(sender) + .extracting("metricsBuffer", as(InstanceOfAssertFactories.type(LinkedBlockingQueue.class))) + .satisfies((queue) -> assertThat(queue.remainingCapacity() + queue.size()) + .isEqualTo(properties.getSender().getMaxQueueSize())); + assertThat(sender).hasFieldOrPropertyWithValue("batchSize", properties.getBatchSize()); + assertThat(sender).hasFieldOrPropertyWithValue("messageSizeBytes", + (int) properties.getSender().getMessageSize().toBytes()); + }); + } + + @Test + void configureWavefrontSender() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .withPropertyValues("management.metrics.export.wavefront.api-token=abcde", + "management.metrics.export.wavefront.batch-size=50", + "management.metrics.export.wavefront.sender.max-queue-size=100", + "management.metrics.export.wavefront.sender.message-size=1KB") + .run((context) -> { + WavefrontSender sender = context.getBean(WavefrontSender.class); + assertThat(sender).hasFieldOrPropertyWithValue("batchSize", 50); + assertThat(sender) + .extracting("metricsBuffer", as(InstanceOfAssertFactories.type(LinkedBlockingQueue.class))) + .satisfies((queue) -> assertThat(queue.remainingCapacity() + queue.size()).isEqualTo(100)); + assertThat(sender).hasFieldOrPropertyWithValue("messageSizeBytes", 1024); + }); + } + + @Test + void allowsWavefrontSenderToBeCustomized() { + this.contextRunner.withUserConfiguration(CustomSenderConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(Clock.class) + .hasSingleBean(WavefrontMeterRegistry.class).hasSingleBean(WavefrontConfig.class) + .hasSingleBean(WavefrontSender.class).hasBean("customSender")); } @Test @@ -109,13 +167,29 @@ public String get(String key) { @Override public String uri() { - return WavefrontConfig.DEFAULT_PROXY.uri(); + return WavefrontConfig.DEFAULT_DIRECT.uri(); + } + + @Override + public String apiToken() { + return "abc-def"; } }; } } + @Configuration(proxyBeanMethods = false) + @Import(BaseConfiguration.class) + static class CustomSenderConfiguration { + + @Bean + WavefrontSender customSender() { + return mock(WavefrontSender.class); + } + + } + @Configuration(proxyBeanMethods = false) @Import(BaseConfiguration.class) static class CustomRegistryConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontPropertiesTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontPropertiesTests.java index 1be810c36e93..4ec6050d468c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontPropertiesTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontPropertiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,8 +36,6 @@ void defaultValuesAreConsistent() { WavefrontConfig config = WavefrontConfig.DEFAULT_DIRECT; assertStepRegistryDefaultValues(properties, config); assertThat(properties.getUri().toString()).isEqualTo(config.uri()); - // source has no static default value - assertThat(properties.getApiToken()).isEqualTo(config.apiToken()); assertThat(properties.getGlobalPrefix()).isEqualTo(config.globalPrefix()); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/integration/IntegrationMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/integration/IntegrationMetricsAutoConfigurationTests.java new file mode 100644 index 000000000000..8d44c8f13359 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/integration/IntegrationMetricsAutoConfigurationTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.metrics.integration; + +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.integration.IntegrationGraphEndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link IntegrationMetricsAutoConfiguration}. + * + * @author Artem Bilan + */ +class IntegrationMetricsAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(IntegrationAutoConfiguration.class, + IntegrationGraphEndpointAutoConfiguration.class, IntegrationMetricsAutoConfiguration.class)) + .with(MetricsRun.simple()).withPropertyValues("management.metrics.tags.someTag=someValue"); + + @Test + void integrationMetersAreInstrumented() { + this.contextRunner.run((context) -> { + MeterRegistry registry = context.getBean(MeterRegistry.class); + Gauge gauge = registry.get("spring.integration.channels").tag("someTag", "someValue").gauge(); + assertThat(gauge).isNotNull().extracting(Gauge::value).isEqualTo(2.0); + }); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/jdbc/DataSourcePoolMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/jdbc/DataSourcePoolMetricsAutoConfigurationTests.java index 74c929fe3e47..77d37405d325 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/jdbc/DataSourcePoolMetricsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/jdbc/DataSourcePoolMetricsAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,11 +28,11 @@ import org.junit.jupiter.api.Test; import org.springframework.aop.framework.ProxyFactory; -import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.boot.jdbc.metadata.DataSourcePoolMetadataProvider; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -53,7 +53,7 @@ */ class DataSourcePoolMetricsAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("spring.datasource.generate-unique-name=true").with(MetricsRun.simple()) .withConfiguration(AutoConfigurations.of(DataSourcePoolMetricsAutoConfiguration.class)) .withUserConfiguration(BaseConfiguration.class); @@ -101,7 +101,8 @@ void autoConfiguredHikariDataSourceIsInstrumented() { } @Test - void autoConfiguredHikariDataSourceIsInstrumentedWhenUsingDataSourceInitialization() { + @Deprecated + void autoConfiguredHikariDataSourceIsInstrumentedWhenUsingDeprecatedDataSourceInitialization() { this.contextRunner.withPropertyValues("spring.datasource.schema:db/create-custom-schema.sql") .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)).run((context) -> { context.getBean(DataSource.class).getConnection(); @@ -110,6 +111,17 @@ void autoConfiguredHikariDataSourceIsInstrumentedWhenUsingDataSourceInitializati }); } + @Test + void autoConfiguredHikariDataSourceIsInstrumentedWhenUsingDataSourceInitialization() { + this.contextRunner.withPropertyValues("spring.sql.init.schema:db/create-custom-schema.sql").withConfiguration( + AutoConfigurations.of(DataSourceAutoConfiguration.class, SqlInitializationAutoConfiguration.class)) + .run((context) -> { + context.getBean(DataSource.class).getConnection(); + MeterRegistry registry = context.getBean(MeterRegistry.class); + registry.get("hikaricp.connections").meter(); + }); + } + @Test void hikariCanBeInstrumentedAfterThePoolHasBeenSealed() { this.contextRunner.withUserConfiguration(HikariSealingConfiguration.class) @@ -299,7 +311,7 @@ public int getOrder() { } @Override - public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + public Object postProcessAfterInitialization(Object bean, String beanName) { if (bean instanceof HikariDataSource) { try { ((HikariDataSource) bean).getConnection().close(); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/mongo/MongoMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/mongo/MongoMetricsAutoConfigurationTests.java new file mode 100644 index 000000000000..2e92c944cd97 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/mongo/MongoMetricsAutoConfigurationTests.java @@ -0,0 +1,203 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.metrics.mongo; + +import java.util.List; + +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.internal.MongoClientImpl; +import com.mongodb.connection.ConnectionPoolSettings; +import com.mongodb.event.ConnectionPoolListener; +import io.micrometer.core.instrument.binder.mongodb.DefaultMongoCommandTagsProvider; +import io.micrometer.core.instrument.binder.mongodb.DefaultMongoConnectionPoolTagsProvider; +import io.micrometer.core.instrument.binder.mongodb.MongoCommandTagsProvider; +import io.micrometer.core.instrument.binder.mongodb.MongoConnectionPoolTagsProvider; +import io.micrometer.core.instrument.binder.mongodb.MongoMetricsCommandListener; +import io.micrometer.core.instrument.binder.mongodb.MongoMetricsConnectionPoolListener; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.ContextConsumer; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link MongoMetricsAutoConfiguration}. + * + * @author Chris Bono + * @author Johnny Lim + */ +class MongoMetricsAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(MongoMetricsAutoConfiguration.class)); + + @Test + void whenThereIsAMeterRegistryThenMetricsCommandListenerIsAdded() { + this.contextRunner.with(MetricsRun.simple()) + .withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class)).run((context) -> { + assertThat(context).hasSingleBean(MongoMetricsCommandListener.class); + assertThat(getActualMongoClientSettingsUsedToConstructClient(context)).isNotNull() + .extracting(MongoClientSettings::getCommandListeners).asList() + .containsExactly(context.getBean(MongoMetricsCommandListener.class)); + assertThat(getMongoCommandTagsProviderUsedToConstructListener(context)) + .isInstanceOf(DefaultMongoCommandTagsProvider.class); + }); + } + + @Test + void whenThereIsAMeterRegistryThenMetricsConnectionPoolListenerIsAdded() { + this.contextRunner.with(MetricsRun.simple()) + .withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class)).run((context) -> { + assertThat(context).hasSingleBean(MongoMetricsConnectionPoolListener.class); + assertThat(getConnectionPoolListenersFromClient(context)) + .containsExactly(context.getBean(MongoMetricsConnectionPoolListener.class)); + assertThat(getMongoConnectionPoolTagsProviderUsedToConstructListener(context)) + .isInstanceOf(DefaultMongoConnectionPoolTagsProvider.class); + }); + } + + @Test + void whenThereIsNoMeterRegistryThenNoMetricsCommandListenerIsAdded() { + this.contextRunner.withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class)) + .run(assertThatMetricsCommandListenerNotAdded()); + } + + @Test + void whenThereIsNoMeterRegistryThenNoMetricsConnectionPoolListenerIsAdded() { + this.contextRunner.withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class)) + .run(assertThatMetricsConnectionPoolListenerNotAdded()); + } + + @Test + void whenThereIsACustomMetricsCommandTagsProviderItIsUsed() { + final MongoCommandTagsProvider customTagsProvider = mock(MongoCommandTagsProvider.class); + this.contextRunner.with(MetricsRun.simple()) + .withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class)) + .withBean("customMongoCommandTagsProvider", MongoCommandTagsProvider.class, () -> customTagsProvider) + .run((context) -> assertThat(getMongoCommandTagsProviderUsedToConstructListener(context)) + .isSameAs(customTagsProvider)); + } + + @Test + void whenThereIsACustomMetricsConnectionPoolTagsProviderItIsUsed() { + final MongoConnectionPoolTagsProvider customTagsProvider = mock(MongoConnectionPoolTagsProvider.class); + this.contextRunner.with(MetricsRun.simple()) + .withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class)) + .withBean("customMongoConnectionPoolTagsProvider", MongoConnectionPoolTagsProvider.class, + () -> customTagsProvider) + .run((context) -> assertThat(getMongoConnectionPoolTagsProviderUsedToConstructListener(context)) + .isSameAs(customTagsProvider)); + } + + @Test + void whenThereIsNoMongoClientSettingsOnClasspathThenNoMetricsCommandListenerIsAdded() { + this.contextRunner.with(MetricsRun.simple()) + .withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class)) + .withClassLoader(new FilteredClassLoader(MongoClientSettings.class)) + .run(assertThatMetricsCommandListenerNotAdded()); + } + + @Test + void whenThereIsNoMongoClientSettingsOnClasspathThenNoMetricsConnectionPoolListenerIsAdded() { + this.contextRunner.with(MetricsRun.simple()) + .withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class)) + .withClassLoader(new FilteredClassLoader(MongoClientSettings.class)) + .run(assertThatMetricsConnectionPoolListenerNotAdded()); + } + + @Test + void whenThereIsNoMongoMetricsCommandListenerOnClasspathThenNoMetricsCommandListenerIsAdded() { + this.contextRunner.with(MetricsRun.simple()) + .withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class)) + .withClassLoader(new FilteredClassLoader(MongoMetricsCommandListener.class)) + .run(assertThatMetricsCommandListenerNotAdded()); + } + + @Test + void whenThereIsNoMongoMetricsConnectionPoolListenerOnClasspathThenNoMetricsConnectionPoolListenerIsAdded() { + this.contextRunner.with(MetricsRun.simple()) + .withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class)) + .withClassLoader(new FilteredClassLoader(MongoMetricsConnectionPoolListener.class)) + .run(assertThatMetricsConnectionPoolListenerNotAdded()); + } + + @Test + void whenMetricsCommandListenerEnabledPropertyFalseThenNoMetricsCommandListenerIsAdded() { + this.contextRunner.with(MetricsRun.simple()) + .withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class)) + .withPropertyValues("management.metrics.mongo.command.enabled:false") + .run(assertThatMetricsCommandListenerNotAdded()); + } + + @Test + void whenMetricsConnectionPoolListenerEnabledPropertyFalseThenNoMetricsConnectionPoolListenerIsAdded() { + this.contextRunner.with(MetricsRun.simple()) + .withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class)) + .withPropertyValues("management.metrics.mongo.connectionpool.enabled:false") + .run(assertThatMetricsConnectionPoolListenerNotAdded()); + } + + private ContextConsumer assertThatMetricsCommandListenerNotAdded() { + return (context) -> { + assertThat(context).doesNotHaveBean(MongoMetricsCommandListener.class); + assertThat(getActualMongoClientSettingsUsedToConstructClient(context)).isNotNull() + .extracting(MongoClientSettings::getCommandListeners).asList().isEmpty(); + }; + } + + private ContextConsumer assertThatMetricsConnectionPoolListenerNotAdded() { + return (context) -> { + assertThat(context).doesNotHaveBean(MongoMetricsConnectionPoolListener.class); + assertThat(getConnectionPoolListenersFromClient(context)).isEmpty(); + }; + } + + private MongoClientSettings getActualMongoClientSettingsUsedToConstructClient( + final AssertableApplicationContext context) { + final MongoClientImpl mongoClient = (MongoClientImpl) context.getBean(MongoClient.class); + return mongoClient.getSettings(); + } + + private List getConnectionPoolListenersFromClient( + final AssertableApplicationContext context) { + MongoClientSettings mongoClientSettings = getActualMongoClientSettingsUsedToConstructClient(context); + ConnectionPoolSettings connectionPoolSettings = mongoClientSettings.getConnectionPoolSettings(); + return connectionPoolSettings.getConnectionPoolListeners(); + } + + private MongoCommandTagsProvider getMongoCommandTagsProviderUsedToConstructListener( + final AssertableApplicationContext context) { + MongoMetricsCommandListener listener = context.getBean(MongoMetricsCommandListener.class); + return (MongoCommandTagsProvider) ReflectionTestUtils.getField(listener, "tagsProvider"); + } + + private MongoConnectionPoolTagsProvider getMongoConnectionPoolTagsProviderUsedToConstructListener( + final AssertableApplicationContext context) { + MongoMetricsConnectionPoolListener listener = context.getBean(MongoMetricsConnectionPoolListener.class); + return (MongoConnectionPoolTagsProvider) ReflectionTestUtils.getField(listener, "tagsProvider"); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/orm/jpa/HibernateMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/orm/jpa/HibernateMetricsAutoConfigurationTests.java index 12bbb0e57d30..2f372877b969 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/orm/jpa/HibernateMetricsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/orm/jpa/HibernateMetricsAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,13 +35,17 @@ import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.orm.jpa.EntityManagerFactoryBuilderCustomizer; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration; import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; @@ -58,7 +62,7 @@ */ class HibernateMetricsAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner().with(MetricsRun.simple()) + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().with(MetricsRun.simple()) .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class, HibernateMetricsAutoConfiguration.class)) .withUserConfiguration(BaseConfiguration.class); @@ -127,6 +131,21 @@ void entityManagerFactoryInstrumentationIsDisabledIfHibernateIsNotAvailable() { }); } + @Test + void entityManagerFactoryInstrumentationDoesNotDeadlockWithDeferredInitialization() { + this.contextRunner.withPropertyValues("spring.jpa.properties.hibernate.generate_statistics:true", + "spring.sql.init.schema-locations:city-schema.sql", "spring.sql.init.data-locations=city-data.sql") + .withConfiguration(AutoConfigurations.of(SqlInitializationAutoConfiguration.class)) + .withBean(EntityManagerFactoryBuilderCustomizer.class, + () -> (builder) -> builder.setBootstrapExecutor(new SimpleAsyncTaskExecutor())) + .run((context) -> { + JdbcTemplate jdbcTemplate = new JdbcTemplate(context.getBean(DataSource.class)); + assertThat(jdbcTemplate.queryForObject("SELECT COUNT(*) from CITY", Integer.class)).isEqualTo(1); + MeterRegistry registry = context.getBean(MeterRegistry.class); + registry.get("hibernate.statements").tags("entityManagerFactory", "entityManagerFactory").meter(); + }); + } + @Configuration(proxyBeanMethods = false) static class BaseConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/r2dbc/ConnectionPoolMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/r2dbc/ConnectionPoolMetricsAutoConfigurationTests.java new file mode 100644 index 000000000000..41137e5b98f5 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/r2dbc/ConnectionPoolMetricsAutoConfigurationTests.java @@ -0,0 +1,197 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.metrics.r2dbc; + +import java.util.Collections; +import java.util.UUID; + +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.r2dbc.h2.CloseableConnectionFactory; +import io.r2dbc.h2.H2ConnectionFactory; +import io.r2dbc.h2.H2ConnectionOption; +import io.r2dbc.pool.ConnectionPool; +import io.r2dbc.pool.ConnectionPoolConfiguration; +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.ConnectionFactoryMetadata; +import io.r2dbc.spi.Wrapped; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; + +import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConnectionPoolMetricsAutoConfiguration}. + * + * @author Tadaya Tsuyukubo + * @author Stephane Nicoll + */ +class ConnectionPoolMetricsAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("spring.r2dbc.generate-unique-name=true").with(MetricsRun.simple()) + .withConfiguration(AutoConfigurations.of(ConnectionPoolMetricsAutoConfiguration.class)) + .withUserConfiguration(BaseConfiguration.class); + + @Test + void autoConfiguredDataSourceIsInstrumented() { + this.contextRunner.withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class)).run((context) -> { + MeterRegistry registry = context.getBean(MeterRegistry.class); + assertThat(registry.find("r2dbc.pool.acquired").gauges()).hasSize(1); + }); + } + + @Test + void autoConfiguredDataSourceExposedAsConnectionFactoryTypeIsInstrumented() { + this.contextRunner + .withPropertyValues( + "spring.r2dbc.url:r2dbc:pool:h2:mem:///name?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE") + .withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class)).run((context) -> { + MeterRegistry registry = context.getBean(MeterRegistry.class); + assertThat(registry.find("r2dbc.pool.acquired").gauges()).hasSize(1); + }); + } + + @Test + void connectionPoolInstrumentationCanBeDisabled() { + this.contextRunner.withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class)) + .withPropertyValues("management.metrics.enable.r2dbc=false").run((context) -> { + MeterRegistry registry = context.getBean(MeterRegistry.class); + assertThat(registry.find("r2dbc.pool.acquired").gauge()).isNull(); + }); + } + + @Test + void connectionPoolExposedAsConnectionFactoryTypeIsInstrumented() { + this.contextRunner.withUserConfiguration(ConnectionFactoryConfiguration.class).run((context) -> { + MeterRegistry registry = context.getBean(MeterRegistry.class); + assertThat(registry.find("r2dbc.pool.acquired").gauges()).extracting(Meter::getId) + .extracting((id) -> id.getTag("name")).containsExactly("testConnectionPool"); + }); + } + + @Test + void wrappedConnectionPoolExposedAsConnectionFactoryTypeIsInstrumented() { + this.contextRunner.withUserConfiguration(WrappedConnectionPoolConfiguration.class).run((context) -> { + MeterRegistry registry = context.getBean(MeterRegistry.class); + assertThat(registry.find("r2dbc.pool.acquired").gauges()).extracting(Meter::getId) + .extracting((id) -> id.getTag("name")).containsExactly("wrappedConnectionPool"); + }); + } + + @Test + void allConnectionPoolsCanBeInstrumented() { + this.contextRunner.withUserConfiguration(TwoConnectionPoolsConfiguration.class).run((context) -> { + MeterRegistry registry = context.getBean(MeterRegistry.class); + assertThat(registry.find("r2dbc.pool.acquired").gauges()).extracting(Meter::getId) + .extracting((id) -> id.getTag("name")).containsExactlyInAnyOrder("firstPool", "secondPool"); + }); + } + + @Configuration(proxyBeanMethods = false) + static class BaseConfiguration { + + @Bean + SimpleMeterRegistry registry() { + return new SimpleMeterRegistry(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class ConnectionFactoryConfiguration { + + @Bean + ConnectionFactory testConnectionPool() { + return new ConnectionPool( + ConnectionPoolConfiguration.builder(H2ConnectionFactory.inMemory("db-" + UUID.randomUUID(), "sa", + "", Collections.singletonMap(H2ConnectionOption.DB_CLOSE_DELAY, "-1"))).build()); + } + + } + + @Configuration(proxyBeanMethods = false) + static class WrappedConnectionPoolConfiguration { + + @Bean + ConnectionFactory wrappedConnectionPool() { + return new Wrapper( + new ConnectionPool( + ConnectionPoolConfiguration + .builder(H2ConnectionFactory.inMemory("db-" + UUID.randomUUID(), "sa", "", + Collections.singletonMap(H2ConnectionOption.DB_CLOSE_DELAY, "-1"))) + .build())); + } + + static class Wrapper implements ConnectionFactory, Wrapped { + + private final ConnectionFactory delegate; + + Wrapper(ConnectionFactory delegate) { + this.delegate = delegate; + } + + @Override + public ConnectionFactory unwrap() { + return this.delegate; + } + + @Override + public Publisher create() { + return this.delegate.create(); + } + + @Override + public ConnectionFactoryMetadata getMetadata() { + return this.delegate.getMetadata(); + } + + } + + } + + @Configuration(proxyBeanMethods = false) + static class TwoConnectionPoolsConfiguration { + + @Bean + CloseableConnectionFactory connectionFactory() { + return H2ConnectionFactory.inMemory("db-" + UUID.randomUUID(), "sa", "", + Collections.singletonMap(H2ConnectionOption.DB_CLOSE_DELAY, "-1")); + } + + @Bean + ConnectionPool firstPool(ConnectionFactory connectionFactory) { + return new ConnectionPool(ConnectionPoolConfiguration.builder(connectionFactory).build()); + } + + @Bean + ConnectionPool secondPool(ConnectionFactory connectionFactory) { + return new ConnectionPool(ConnectionPoolConfiguration.builder(connectionFactory).build()); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/test/MetricsIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/test/MetricsIntegrationTests.java index 921ee3215c61..842c7a0f99f8 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/test/MetricsIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/test/MetricsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.boot.actuate.autoconfigure.metrics.test; +import java.time.Duration; import java.util.Collections; import java.util.Map; import java.util.Set; @@ -71,6 +72,7 @@ import org.springframework.web.client.RestTemplate; import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.waitAtMost; import static org.springframework.test.web.client.ExpectedCount.once; import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; @@ -110,7 +112,9 @@ void restTemplateIsInstrumented() { @Test void requestMappingIsInstrumented() { this.loopback.getForObject("/api/people", Set.class); - assertThat(this.registry.get("http.server.requests").timer().count()).isEqualTo(1); + waitAtMost(Duration.ofSeconds(5)).untilAsserted( + () -> assertThat(this.registry.get("http.server.requests").timer().count()).isEqualTo(1)); + } @Test diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/test/MetricsRun.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/test/MetricsRun.java index a2a50519e671..8df509ddb34c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/test/MetricsRun.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/test/MetricsRun.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/TestController.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/TestController.java index efbae5216488..42d81a70dd13 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/TestController.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/TestController.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.boot.actuate.autoconfigure.metrics.web; +import io.micrometer.core.annotation.Timed; + import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @@ -24,6 +26,7 @@ * * @author Dmytro Nosan * @author Stephane Nicoll + * @author Chanhyeong LEE */ @RestController public class TestController { @@ -43,4 +46,10 @@ public String test2() { return "test2"; } + @Timed + @GetMapping("test3") + public String test3() { + return "test3"; + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/RestTemplateMetricsConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/RestTemplateMetricsConfigurationTests.java index 197c3e0681ea..69232dafe12e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/RestTemplateMetricsConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/RestTemplateMetricsConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -63,6 +63,16 @@ void restTemplateCreatedWithBuilderIsInstrumented() { }); } + @Test + void restTemplateWithRootUriIsInstrumented() { + this.contextRunner.run((context) -> { + MeterRegistry registry = context.getBean(MeterRegistry.class); + RestTemplateBuilder builder = context.getBean(RestTemplateBuilder.class); + builder = builder.rootUri("/root"); + validateRestTemplate(builder, registry, "/root"); + }); + } + @Test void restTemplateCanBeCustomizedManually() { this.contextRunner.run((context) -> { @@ -130,17 +140,22 @@ private MeterRegistry getInitializedMeterRegistry(AssertableApplicationContext c } private void validateRestTemplate(RestTemplateBuilder builder, MeterRegistry registry) { - RestTemplate restTemplate = mockRestTemplate(builder); + this.validateRestTemplate(builder, registry, ""); + } + + private void validateRestTemplate(RestTemplateBuilder builder, MeterRegistry registry, String rootUri) { + RestTemplate restTemplate = mockRestTemplate(builder, rootUri); assertThat(registry.find("http.client.requests").meter()).isNull(); assertThat(restTemplate.getForEntity("/projects/{project}", Void.class, "spring-boot").getStatusCode()) .isEqualTo(HttpStatus.OK); - assertThat(registry.get("http.client.requests").tags("uri", "/projects/{project}").meter()).isNotNull(); + assertThat(registry.get("http.client.requests").tags("uri", rootUri + "/projects/{project}").meter()) + .isNotNull(); } - private RestTemplate mockRestTemplate(RestTemplateBuilder builder) { + private RestTemplate mockRestTemplate(RestTemplateBuilder builder, String rootUri) { RestTemplate restTemplate = builder.build(); MockRestServiceServer server = MockRestServiceServer.createServer(restTemplate); - server.expect(requestTo("/projects/spring-boot")).andRespond(withStatus(HttpStatus.OK)); + server.expect(requestTo(rootUri + "/projects/spring-boot")).andRespond(withStatus(HttpStatus.OK)); return restTemplate; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientMetricsConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientMetricsConfigurationTests.java index 7eea946366ed..6e91ddaa16eb 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientMetricsConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientMetricsConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -111,7 +111,8 @@ private MeterRegistry getInitializedMeterRegistry(AssertableApplicationContext c WebClient webClient = mockWebClient(context.getBean(WebClient.Builder.class)); MeterRegistry registry = context.getBean(MeterRegistry.class); for (int i = 0; i < 3; i++) { - webClient.get().uri("https://example.org/projects/" + i).exchange().block(Duration.ofSeconds(30)); + webClient.get().uri("https://example.org/projects/" + i).retrieve().toBodilessEntity() + .block(Duration.ofSeconds(30)); } return registry; } @@ -119,7 +120,7 @@ private MeterRegistry getInitializedMeterRegistry(AssertableApplicationContext c private void validateWebClient(WebClient.Builder builder, MeterRegistry registry) { WebClient webClient = mockWebClient(builder); assertThat(registry.find("http.client.requests").meter()).isNull(); - webClient.get().uri("https://example.org/projects/{project}", "spring-boot").exchange() + webClient.get().uri("https://example.org/projects/{project}", "spring-boot").retrieve().toBodilessEntity() .block(Duration.ofSeconds(30)); assertThat(registry.find("http.client.requests").tags("uri", "/projects/{project}").meter()).isNotNull(); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/WebFluxMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/WebFluxMetricsAutoConfigurationTests.java index c5bfe12c5ece..45352944a9f2 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/WebFluxMetricsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/WebFluxMetricsAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import org.springframework.boot.actuate.autoconfigure.metrics.web.TestController; import org.springframework.boot.actuate.metrics.web.reactive.server.DefaultWebFluxTagsProvider; import org.springframework.boot.actuate.metrics.web.reactive.server.MetricsWebFilter; +import org.springframework.boot.actuate.metrics.web.reactive.server.WebFluxTagsContributor; import org.springframework.boot.actuate.metrics.web.reactive.server.WebFluxTagsProvider; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; @@ -43,11 +44,12 @@ * * @author Brian Clozel * @author Dmytro Nosan + * @author Madhura Bhave */ @ExtendWith(OutputCaptureExtension.class) class WebFluxMetricsAutoConfigurationTests { - private ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() + private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() .with(MetricsRun.simple()).withConfiguration(AutoConfigurations.of(WebFluxMetricsAutoConfiguration.class)); @Test @@ -55,9 +57,21 @@ void shouldProvideWebFluxMetricsBeans() { this.contextRunner.run((context) -> { assertThat(context).getBeans(MetricsWebFilter.class).hasSize(1); assertThat(context).getBeans(DefaultWebFluxTagsProvider.class).hasSize(1); + assertThat(context.getBean(DefaultWebFluxTagsProvider.class)).extracting("ignoreTrailingSlash") + .isEqualTo(true); }); } + @Test + void tagsProviderWhenIgnoreTrailingSlashIsFalse() { + this.contextRunner.withPropertyValues("management.metrics.web.server.request.ignore-trailing-slash=false") + .run((context) -> { + assertThat(context).hasSingleBean(DefaultWebFluxTagsProvider.class); + assertThat(context.getBean(DefaultWebFluxTagsProvider.class)).extracting("ignoreTrailingSlash") + .isEqualTo(false); + }); + } + @Test void shouldNotOverrideCustomTagsProvider() { this.contextRunner.withUserConfiguration(CustomWebFluxTagsProviderConfig.class) @@ -99,14 +113,12 @@ void metricsAreNotRecordedIfAutoTimeRequestsIsDisabled() { } @Test - @Deprecated - void metricsAreNotRecordedIfAutoTimeRequestsIsDisabledWithDeprecatedProperty() { - this.contextRunner.withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class)) - .withUserConfiguration(TestController.class) - .withPropertyValues("management.metrics.web.server.auto-time-requests=false").run((context) -> { - MeterRegistry registry = getInitializedMeterRegistry(context); - assertThat(registry.find("http.server.requests").meter()).isNull(); - }); + void whenTagContributorsAreDefinedThenTagsProviderUsesThem() { + this.contextRunner.withUserConfiguration(TagsContributorsConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(DefaultWebFluxTagsProvider.class); + assertThat(context.getBean(DefaultWebFluxTagsProvider.class)).extracting("contributors").asList() + .hasSize(2); + }); } private MeterRegistry getInitializedMeterRegistry(AssertableReactiveWebApplicationContext context) { @@ -127,4 +139,19 @@ WebFluxTagsProvider customWebFluxTagsProvider() { } + @Configuration(proxyBeanMethods = false) + static class TagsContributorsConfiguration { + + @Bean + WebFluxTagsContributor tagContributorOne() { + return mock(WebFluxTagsContributor.class); + } + + @Bean + WebFluxTagsContributor tagContributorTwo() { + return mock(WebFluxTagsContributor.class); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/servlet/WebMvcMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/servlet/WebMvcMetricsAutoConfigurationTests.java index 5b570ee30074..0c07ec22fc83 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/servlet/WebMvcMetricsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/servlet/WebMvcMetricsAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.boot.actuate.autoconfigure.metrics.web.servlet; +import java.util.Collection; import java.util.Collections; import java.util.EnumSet; @@ -24,6 +25,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import io.micrometer.core.instrument.Meter; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Tag; import io.micrometer.core.instrument.Timer; @@ -37,6 +39,7 @@ import org.springframework.boot.actuate.metrics.web.servlet.DefaultWebMvcTagsProvider; import org.springframework.boot.actuate.metrics.web.servlet.LongTaskTimingHandlerInterceptor; import org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter; +import org.springframework.boot.actuate.metrics.web.servlet.WebMvcTagsContributor; import org.springframework.boot.actuate.metrics.web.servlet.WebMvcTagsProvider; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; @@ -54,6 +57,7 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** @@ -62,12 +66,14 @@ * @author Andy Wilkinson * @author Dmytro Nosan * @author Tadaya Tsuyukubo + * @author Madhura Bhave + * @author Chanhyeong LEE */ @ExtendWith(OutputCaptureExtension.class) class WebMvcMetricsAutoConfigurationTests { - private WebApplicationContextRunner contextRunner = new WebApplicationContextRunner().with(MetricsRun.simple()) - .withConfiguration(AutoConfigurations.of(WebMvcMetricsAutoConfiguration.class)); + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .with(MetricsRun.simple()).withConfiguration(AutoConfigurations.of(WebMvcMetricsAutoConfiguration.class)); @Test void backsOffWhenMeterRegistryIsMissing() { @@ -79,12 +85,24 @@ void backsOffWhenMeterRegistryIsMissing() { void definesTagsProviderAndFilterWhenMeterRegistryIsPresent() { this.contextRunner.run((context) -> { assertThat(context).hasSingleBean(DefaultWebMvcTagsProvider.class); + assertThat(context.getBean(DefaultWebMvcTagsProvider.class)).extracting("ignoreTrailingSlash") + .isEqualTo(true); assertThat(context).hasSingleBean(FilterRegistrationBean.class); assertThat(context.getBean(FilterRegistrationBean.class).getFilter()) .isInstanceOf(WebMvcMetricsFilter.class); }); } + @Test + void tagsProviderWhenIgnoreTrailingSlashIsFalse() { + this.contextRunner.withPropertyValues("management.metrics.web.server.request.ignore-trailing-slash=false") + .run((context) -> { + assertThat(context).hasSingleBean(DefaultWebMvcTagsProvider.class); + assertThat(context.getBean(DefaultWebMvcTagsProvider.class)).extracting("ignoreTrailingSlash") + .isEqualTo(false); + }); + } + @Test void tagsProviderBacksOff() { this.contextRunner.withUserConfiguration(TagsProviderConfiguration.class).run((context) -> { @@ -143,6 +161,19 @@ void autoTimeRequestsCanBeConfigured() { }); } + @Test + void timerWorksWithTimedAnnotationsWhenAutoTimeRequestsIsFalse() { + this.contextRunner.withUserConfiguration(TestController.class) + .withConfiguration(AutoConfigurations.of(MetricsAutoConfiguration.class, WebMvcAutoConfiguration.class)) + .withPropertyValues("management.metrics.web.server.request.autotime.enabled=false").run((context) -> { + MeterRegistry registry = getInitializedMeterRegistry(context, "/test3"); + Collection meters = registry.get("http.server.requests").meters(); + assertThat(meters).hasSize(1); + Meter meter = meters.iterator().next(); + assertThat(meter.getId().getTag("uri")).isEqualTo("/test3"); + }); + } + @Test @SuppressWarnings("rawtypes") void longTaskTimingInterceptorIsRegistered() { @@ -153,13 +184,26 @@ void longTaskTimingInterceptorIsRegistered() { .contains(LongTaskTimingHandlerInterceptor.class)); } + @Test + void whenTagContributorsAreDefinedThenTagsProviderUsesThem() { + this.contextRunner.withUserConfiguration(TagsContributorsConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(DefaultWebMvcTagsProvider.class); + assertThat(context.getBean(DefaultWebMvcTagsProvider.class)).extracting("contributors").asList().hasSize(2); + }); + } + private MeterRegistry getInitializedMeterRegistry(AssertableWebApplicationContext context) throws Exception { + return getInitializedMeterRegistry(context, "/test0", "/test1", "/test2"); + } + + private MeterRegistry getInitializedMeterRegistry(AssertableWebApplicationContext context, String... urls) + throws Exception { assertThat(context).hasSingleBean(FilterRegistrationBean.class); Filter filter = context.getBean(FilterRegistrationBean.class).getFilter(); assertThat(filter).isInstanceOf(WebMvcMetricsFilter.class); MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context).addFilters(filter).build(); - for (int i = 0; i < 3; i++) { - mockMvc.perform(MockMvcRequestBuilders.get("/test" + i)).andExpect(status().isOk()); + for (String url : urls) { + mockMvc.perform(MockMvcRequestBuilders.get(url)).andExpect(status().isOk()); } return context.getBean(MeterRegistry.class); } @@ -174,6 +218,21 @@ TestWebMvcTagsProvider tagsProvider() { } + @Configuration(proxyBeanMethods = false) + static class TagsContributorsConfiguration { + + @Bean + WebMvcTagsContributor tagContributorOne() { + return mock(WebMvcTagsContributor.class); + } + + @Bean + WebMvcTagsContributor tagContributorTwo() { + return mock(WebMvcTagsContributor.class); + } + + } + private static final class TestWebMvcTagsProvider implements WebMvcTagsProvider { @Override diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/mongo/MongoHealthContributorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/mongo/MongoHealthContributorAutoConfigurationTests.java index 06f627d16e54..02a763d73437 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/mongo/MongoHealthContributorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/mongo/MongoHealthContributorAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,7 @@ */ class MongoHealthContributorAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class, MongoDataAutoConfiguration.class, MongoHealthContributorAutoConfiguration.class, HealthContributorAutoConfiguration.class)); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/mongo/MongoReactiveHealthContributorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/mongo/MongoReactiveHealthContributorAutoConfigurationTests.java index 7268d9c53ef3..7b37b2271117 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/mongo/MongoReactiveHealthContributorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/mongo/MongoReactiveHealthContributorAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,7 +37,7 @@ */ class MongoReactiveHealthContributorAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class, MongoDataAutoConfiguration.class, MongoReactiveAutoConfiguration.class, MongoReactiveDataAutoConfiguration.class, MongoReactiveHealthContributorAutoConfiguration.class, HealthContributorAutoConfiguration.class)); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorAutoConfigurationTests.java index b768bc2faf4d..c47a0d0e39c2 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/neo4j/Neo4jHealthContributorAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,13 +17,17 @@ package org.springframework.boot.actuate.autoconfigure.neo4j; import org.junit.jupiter.api.Test; -import org.neo4j.ogm.session.Session; -import org.neo4j.ogm.session.SessionFactory; +import org.neo4j.driver.Driver; +import reactor.core.publisher.Flux; import org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration; +import org.springframework.boot.actuate.health.AbstractHealthIndicator; import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.actuate.neo4j.Neo4jHealthIndicator; +import org.springframework.boot.actuate.neo4j.Neo4jReactiveHealthIndicator; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -37,39 +41,57 @@ * * @author Phillip Webb * @author Stephane Nicoll + * @author Michael J. Simons */ class Neo4jHealthContributorAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withUserConfiguration(Neo4jConfiguration.class).withConfiguration(AutoConfigurations - .of(Neo4jHealthContributorAutoConfiguration.class, HealthContributorAutoConfiguration.class)); + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(HealthContributorAutoConfiguration.class, + Neo4jHealthContributorAutoConfiguration.class)); @Test - void runShouldCreateIndicator() { - this.contextRunner.run((context) -> assertThat(context).hasSingleBean(Neo4jHealthIndicator.class)); + void runShouldCreateHealthIndicator() { + this.contextRunner.withUserConfiguration(Neo4jConfiguration.class).run((context) -> assertThat(context) + .hasSingleBean(Neo4jReactiveHealthIndicator.class).doesNotHaveBean(Neo4jHealthIndicator.class)); + } + + @Test + void runWithoutReactorShouldCreateHealthIndicator() { + this.contextRunner.withUserConfiguration(Neo4jConfiguration.class) + .withClassLoader(new FilteredClassLoader(Flux.class)).run((context) -> assertThat(context) + .hasSingleBean(Neo4jHealthIndicator.class).doesNotHaveBean(Neo4jReactiveHealthIndicator.class)); } @Test void runWhenDisabledShouldNotCreateIndicator() { - this.contextRunner.withPropertyValues("management.health.neo4j.enabled:false") - .run((context) -> assertThat(context).doesNotHaveBean(Neo4jHealthIndicator.class)); + this.contextRunner.withUserConfiguration(Neo4jConfiguration.class) + .withPropertyValues("management.health.neo4j.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(Neo4jHealthIndicator.class) + .doesNotHaveBean(Neo4jReactiveHealthIndicator.class)); } @Test void defaultIndicatorCanBeReplaced() { - this.contextRunner.withUserConfiguration(CustomIndicatorConfiguration.class).run((context) -> { - assertThat(context).hasSingleBean(Neo4jHealthIndicator.class); - Health health = context.getBean(Neo4jHealthIndicator.class).health(); - assertThat(health.getDetails()).containsOnly(entry("test", true)); - }); + this.contextRunner.withUserConfiguration(Neo4jConfiguration.class, CustomIndicatorConfiguration.class) + .run((context) -> { + assertThat(context).hasBean("neo4jHealthIndicator"); + Health health = context.getBean("neo4jHealthIndicator", HealthIndicator.class).health(); + assertThat(health.getDetails()).containsOnly(entry("test", true)); + }); + } + + @Test + void shouldRequireDriverBean() { + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(Neo4jHealthIndicator.class) + .doesNotHaveBean(Neo4jReactiveHealthIndicator.class)); } @Configuration(proxyBeanMethods = false) static class Neo4jConfiguration { @Bean - SessionFactory sessionFactory() { - return mock(SessionFactory.class); + Driver driver() { + return mock(Driver.class); } } @@ -78,14 +100,13 @@ SessionFactory sessionFactory() { static class CustomIndicatorConfiguration { @Bean - Neo4jHealthIndicator neo4jHealthIndicator(SessionFactory sessionFactory) { - return new Neo4jHealthIndicator(sessionFactory) { + HealthIndicator neo4jHealthIndicator() { + return new AbstractHealthIndicator() { @Override - protected void extractResult(Session session, Health.Builder builder) { + protected void doHealthCheck(Health.Builder builder) { builder.up().withDetail("test", true); } - }; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/quartz/QuartzEndpointAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/quartz/QuartzEndpointAutoConfigurationTests.java new file mode 100644 index 000000000000..5343b9b0de59 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/quartz/QuartzEndpointAutoConfigurationTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.quartz; + +import org.junit.jupiter.api.Test; +import org.quartz.Scheduler; + +import org.springframework.boot.actuate.quartz.QuartzEndpoint; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link QuartzEndpointAutoConfiguration}. + * + * @author Vedran Pavic + * @author Stephane Nicoll + */ +class QuartzEndpointAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(QuartzEndpointAutoConfiguration.class)); + + @Test + void endpointIsAutoConfigured() { + this.contextRunner.withBean(Scheduler.class, () -> mock(Scheduler.class)) + .withPropertyValues("management.endpoints.web.exposure.include=quartz") + .run((context) -> assertThat(context).hasSingleBean(QuartzEndpoint.class)); + } + + @Test + void endpointIsNotAutoConfiguredIfSchedulerIsNotAvailable() { + this.contextRunner.withPropertyValues("management.endpoints.web.exposure.include=quartz") + .run((context) -> assertThat(context).doesNotHaveBean(QuartzEndpoint.class)); + } + + @Test + void endpointNotAutoConfiguredWhenNotExposed() { + this.contextRunner.withBean(Scheduler.class, () -> mock(Scheduler.class)) + .run((context) -> assertThat(context).doesNotHaveBean(QuartzEndpoint.class)); + } + + @Test + void endpointCanBeDisabled() { + this.contextRunner.withBean(Scheduler.class, () -> mock(Scheduler.class)) + .withPropertyValues("management.endpoint.quartz.enabled:false") + .run((context) -> assertThat(context).doesNotHaveBean(QuartzEndpoint.class)); + } + + @Test + void endpointBacksOffWhenUserProvidedEndpointIsPresent() { + this.contextRunner.withUserConfiguration(CustomEndpointConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(QuartzEndpoint.class).hasBean("customEndpoint")); + } + + @Configuration(proxyBeanMethods = false) + static class CustomEndpointConfiguration { + + @Bean + CustomEndpoint customEndpoint() { + return new CustomEndpoint(); + } + + } + + private static final class CustomEndpoint extends QuartzEndpoint { + + private CustomEndpoint() { + super(mock(Scheduler.class)); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/r2dbc/ConnectionFactoryHealthContributorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/r2dbc/ConnectionFactoryHealthContributorAutoConfigurationTests.java new file mode 100644 index 000000000000..d10344b5e203 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/r2dbc/ConnectionFactoryHealthContributorAutoConfigurationTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.r2dbc; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration; +import org.springframework.boot.actuate.r2dbc.ConnectionFactoryHealthIndicator; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for {@link ConnectionFactoryHealthContributorAutoConfiguration}. + * + * @author Stephane Nicoll + */ +class ConnectionFactoryHealthContributorAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ConnectionFactoryHealthContributorAutoConfiguration.class, + HealthContributorAutoConfiguration.class)); + + @Test + void runShouldCreateIndicator() { + this.contextRunner.withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class)) + .run((context) -> assertThat(context).hasSingleBean(ConnectionFactoryHealthIndicator.class)); + } + + @Test + void runWithNoConnectionFactoryShouldNotCreateIndicator() { + this.contextRunner + .run((context) -> assertThat(context).doesNotHaveBean(ConnectionFactoryHealthIndicator.class)); + } + + @Test + void runWhenDisabledShouldNotCreateIndicator() { + this.contextRunner.withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class)) + .withPropertyValues("management.health.r2dbc.enabled:false") + .run((context) -> assertThat(context).doesNotHaveBean(ConnectionFactoryHealthIndicator.class)); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/redis/RedisHealthContributorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/redis/RedisHealthContributorAutoConfigurationTests.java index 295633162662..ff100f4359e9 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/redis/RedisHealthContributorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/redis/RedisHealthContributorAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,7 @@ @ClassPathExclusions({ "reactor-core*.jar", "lettuce-core*.jar" }) class RedisHealthContributorAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class, RedisHealthContributorAutoConfiguration.class, HealthContributorAutoConfiguration.class)); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/redis/RedisReactiveHealthContributorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/redis/RedisReactiveHealthContributorAutoConfigurationTests.java index d5a6e0f0cfeb..846fff6dce2a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/redis/RedisReactiveHealthContributorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/redis/RedisReactiveHealthContributorAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,7 @@ */ class RedisReactiveHealthContributorAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class, RedisReactiveHealthContributorAutoConfiguration.class, HealthContributorAutoConfiguration.class)); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/reactive/ReactiveManagementWebSecurityAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/reactive/ReactiveManagementWebSecurityAutoConfigurationTests.java index c714bf980fe4..9355cabce0c4 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/reactive/ReactiveManagementWebSecurityAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/reactive/ReactiveManagementWebSecurityAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; -import org.springframework.beans.BeansException; import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.env.EnvironmentEndpointAutoConfiguration; @@ -35,6 +34,7 @@ import org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration; import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration; +import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.context.ApplicationContext; @@ -65,12 +65,12 @@ */ class ReactiveManagementWebSecurityAutoConfigurationTests { - private ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() + private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(HealthContributorAutoConfiguration.class, HealthEndpointAutoConfiguration.class, InfoEndpointAutoConfiguration.class, - EnvironmentEndpointAutoConfiguration.class, EndpointAutoConfiguration.class, - WebEndpointAutoConfiguration.class, ReactiveSecurityAutoConfiguration.class, - ReactiveUserDetailsServiceAutoConfiguration.class, + WebFluxAutoConfiguration.class, EnvironmentEndpointAutoConfiguration.class, + EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, + ReactiveSecurityAutoConfiguration.class, ReactiveUserDetailsServiceAutoConfiguration.class, ReactiveManagementWebSecurityAutoConfiguration.class)); @Test @@ -78,11 +78,6 @@ void permitAllForHealth() { this.contextRunner.run((context) -> assertThat(getAuthenticateHeader(context, "/actuator/health")).isNull()); } - @Test - void permitAllForInfo() { - this.contextRunner.run((context) -> assertThat(getAuthenticateHeader(context, "/actuator/info")).isNull()); - } - @Test void securesEverythingElse() { this.contextRunner.run((context) -> { @@ -164,7 +159,7 @@ protected ServerWebExchange createExchange(ServerHttpRequest request, ServerHttp static class CustomSecurityConfiguration { @Bean - SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception { + SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { http.authorizeExchange((exchanges) -> { exchanges.pathMatchers("/foo").permitAll(); exchanges.anyExchange().authenticated(); @@ -184,7 +179,7 @@ ReactiveAuthenticationManager authenticationManager() { } @Bean - WebFilterChainProxy webFilterChainProxy(ServerHttpSecurity http) throws Exception { + WebFilterChainProxy webFilterChainProxy(ServerHttpSecurity http) { return new WebFilterChainProxy(getFilterChains(http)); } @@ -195,7 +190,7 @@ TestServerHttpSecurity http(ReactiveAuthenticationManager authenticationManager) return httpSecurity; } - private List getFilterChains(ServerHttpSecurity http) throws Exception { + private List getFilterChains(ServerHttpSecurity http) { http.authorizeExchange((exchanges) -> exchanges.anyExchange().authenticated()); http.formLogin(Customizer.withDefaults()); return Collections.singletonList(http.build()); @@ -204,7 +199,7 @@ private List getFilterChains(ServerHttpSecurity http) th static class TestServerHttpSecurity extends ServerHttpSecurity implements ApplicationContextAware { @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + public void setApplicationContext(ApplicationContext applicationContext) { super.setApplicationContext(applicationContext); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/AbstractEndpointRequestIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/AbstractEndpointRequestIntegrationTests.java index f048926e1dfc..67dcf86f57af 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/AbstractEndpointRequestIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/AbstractEndpointRequestIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,8 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.actuate.autoconfigure.security.servlet; +import java.time.Duration; import java.util.Base64; import java.util.function.Supplier; @@ -30,10 +32,8 @@ import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpoint; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; -import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration; -import org.springframework.boot.logging.LogLevel; import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext; @@ -60,13 +60,12 @@ void toEndpointShouldMatch() { @Test void toAllEndpointsShouldMatch() { - getContextRunner().withInitializer(new ConditionEvaluationReportLoggingListener(LogLevel.INFO)) - .withPropertyValues("spring.security.user.password=password").run((context) -> { - WebTestClient webTestClient = getWebTestClient(context); - webTestClient.get().uri("/actuator/e2").exchange().expectStatus().isUnauthorized(); - webTestClient.get().uri("/actuator/e2").header("Authorization", getBasicAuth()).exchange() - .expectStatus().isOk(); - }); + getContextRunner().withPropertyValues("spring.security.user.password=password").run((context) -> { + WebTestClient webTestClient = getWebTestClient(context); + webTestClient.get().uri("/actuator/e2").exchange().expectStatus().isUnauthorized(); + webTestClient.get().uri("/actuator/e2").header("Authorization", getBasicAuth()).exchange().expectStatus() + .isOk(); + }); } @Test @@ -92,7 +91,8 @@ protected final WebApplicationContextRunner getContextRunner() { protected WebTestClient getWebTestClient(AssertableWebApplicationContext context) { int port = context.getSourceApplicationContext(AnnotationConfigServletWebServerApplicationContext.class) .getWebServer().getPort(); - return WebTestClient.bindToServer().baseUrl("http://localhost:" + port).build(); + return WebTestClient.bindToServer().baseUrl("http://localhost:" + port).responseTimeout(Duration.ofMinutes(5)) + .build(); } String getBasicAuth() { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/JerseyEndpointRequestIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/JerseyEndpointRequestIntegrationTests.java index c8eee1e27005..1e06f70cb582 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/JerseyEndpointRequestIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/JerseyEndpointRequestIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.actuate.autoconfigure.security.servlet; import org.glassfish.jersey.server.ResourceConfig; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/ManagementWebSecurityAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/ManagementWebSecurityAutoConfigurationTests.java index 4d680bf46cee..1b99ffeacfde 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/ManagementWebSecurityAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/ManagementWebSecurityAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,8 @@ package org.springframework.boot.actuate.autoconfigure.security.servlet; import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; import org.junit.jupiter.api.Test; @@ -27,11 +29,17 @@ import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration; +import org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyAutoConfiguration; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.mock.web.MockFilterChain; import org.springframework.mock.web.MockHttpServletRequest; @@ -41,6 +49,8 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.FilterChainProxy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.web.context.WebApplicationContext; import static org.assertj.core.api.Assertions.assertThat; @@ -49,31 +59,27 @@ * Tests for {@link ManagementWebSecurityAutoConfiguration}. * * @author Madhura Bhave + * @author Hatef Palizgar */ class ManagementWebSecurityAutoConfigurationTests { - private WebApplicationContextRunner contextRunner = new WebApplicationContextRunner().withConfiguration( + private static final String MANAGEMENT_SECURITY_FILTER_CHAIN_BEAN = "managementSecurityFilterChain"; + + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner().withConfiguration( AutoConfigurations.of(HealthContributorAutoConfiguration.class, HealthEndpointAutoConfiguration.class, InfoEndpointAutoConfiguration.class, EnvironmentEndpointAutoConfiguration.class, - EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, + EndpointAutoConfiguration.class, WebMvcAutoConfiguration.class, WebEndpointAutoConfiguration.class, SecurityAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class)); @Test void permitAllForHealth() { this.contextRunner.run((context) -> { + assertThat(context).hasBean(MANAGEMENT_SECURITY_FILTER_CHAIN_BEAN); HttpStatus status = getResponseStatus(context, "/actuator/health"); assertThat(status).isEqualTo(HttpStatus.OK); }); } - @Test - void permitAllForInfo() { - this.contextRunner.run((context) -> { - HttpStatus status = getResponseStatus(context, "/actuator/info"); - assertThat(status).isEqualTo(HttpStatus.OK); - }); - } - @Test void securesEverythingElse() { this.contextRunner.run((context) -> { @@ -84,6 +90,15 @@ void securesEverythingElse() { }); } + @Test + void autoConfigIsConditionalOnSecurityFilterChainClass() { + this.contextRunner.withClassLoader(new FilteredClassLoader(SecurityFilterChain.class)).run((context) -> { + assertThat(context).doesNotHaveBean(ManagementWebSecurityAutoConfiguration.class); + HttpStatus status = getResponseStatus(context, "/actuator/health"); + assertThat(status).isEqualTo(HttpStatus.UNAUTHORIZED); + }); + } + @Test void usesMatchersBasedOffConfiguredActuatorBasePath() { this.contextRunner.withPropertyValues("management.endpoints.web.base-path=/").run((context) -> { @@ -102,11 +117,47 @@ void backOffIfCustomSecurityIsAdded() { }); } + @Test + void backsOffIfSecurityFilterChainBeanIsPresent() { + this.contextRunner.withUserConfiguration(TestSecurityFilterChainConfig.class).run((context) -> { + assertThat(context.getBeansOfType(SecurityFilterChain.class)).hasSize(1); + assertThat(context.containsBean("testSecurityFilterChain")).isTrue(); + }); + } + @Test void backOffIfOAuth2ResourceServerAutoConfigurationPresent() { this.contextRunner.withConfiguration(AutoConfigurations.of(OAuth2ResourceServerAutoConfiguration.class)) .withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://authserver") - .run((context) -> assertThat(context).doesNotHaveBean(ManagementWebSecurityConfigurerAdapter.class)); + .run((context) -> assertThat(context).doesNotHaveBean(ManagementWebSecurityAutoConfiguration.class) + .doesNotHaveBean(MANAGEMENT_SECURITY_FILTER_CHAIN_BEAN)); + } + + @Test + void backOffIfSaml2RelyingPartyAutoConfigurationPresent() { + this.contextRunner.withConfiguration(AutoConfigurations.of(Saml2RelyingPartyAutoConfiguration.class)) + .withPropertyValues( + "spring.security.saml2.relyingparty.registration.simplesamlphp.identity-provider.single-sign-on.url=https://simplesaml-for-spring-saml/SSOService.php", + "spring.security.saml2.relyingparty.registration.simplesamlphp.identity-provider.single-sign-on.sign-request=false", + "spring.security.saml2.relyingparty.registration.simplesamlphp.identityprovider.entity-id=https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php", + "spring.security.saml2.relyingparty.registration.simplesamlphp.identityprovider.verification.credentials[0].certificate-location=classpath:saml/certificate-location") + .run((context) -> assertThat(context).doesNotHaveBean(ManagementWebSecurityAutoConfiguration.class) + .doesNotHaveBean(MANAGEMENT_SECURITY_FILTER_CHAIN_BEAN)); + } + + @Test + void backOffIfRemoteDevToolsSecurityFilterChainIsPresent() { + this.contextRunner.withUserConfiguration(TestRemoteDevToolsSecurityFilterChainConfig.class).run((context) -> { + SecurityFilterChain testSecurityFilterChain = context.getBean("testSecurityFilterChain", + SecurityFilterChain.class); + SecurityFilterChain testRemoteDevToolsSecurityFilterChain = context + .getBean("testRemoteDevToolsSecurityFilterChain", SecurityFilterChain.class); + List orderedSecurityFilterChains = context.getBeanProvider(SecurityFilterChain.class) + .orderedStream().collect(Collectors.toList()); + assertThat(orderedSecurityFilterChains).containsExactly(testRemoteDevToolsSecurityFilterChain, + testSecurityFilterChain); + assertThat(context).doesNotHaveBean(ManagementWebSecurityAutoConfiguration.class); + }); } private HttpStatus getResponseStatus(AssertableWebApplicationContext context, String path) @@ -137,4 +188,27 @@ protected void configure(HttpSecurity http) throws Exception { } + @Configuration(proxyBeanMethods = false) + static class TestSecurityFilterChainConfig { + + @Bean + SecurityFilterChain testSecurityFilterChain(HttpSecurity http) throws Exception { + return http.antMatcher("/**").authorizeRequests((authorize) -> authorize.anyRequest().authenticated()) + .build(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class TestRemoteDevToolsSecurityFilterChainConfig extends TestSecurityFilterChainConfig { + + @Bean + @Order(SecurityProperties.BASIC_AUTH_ORDER - 1) + SecurityFilterChain testRemoteDevToolsSecurityFilterChain(HttpSecurity http) throws Exception { + return http.requestMatcher(new AntPathRequestMatcher("/**")).authorizeRequests().anyRequest().anonymous() + .and().csrf().disable().build(); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/MvcEndpointRequestIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/MvcEndpointRequestIntegrationTests.java index ce93ec195e06..86722ba93d09 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/MvcEndpointRequestIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/MvcEndpointRequestIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.actuate.autoconfigure.security.servlet; import org.junit.jupiter.api.Test; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/SecurityRequestMatchersManagementContextConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/SecurityRequestMatchersManagementContextConfigurationTests.java index 1c08e3239ca0..951d4b5ba927 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/SecurityRequestMatchersManagementContextConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/SecurityRequestMatchersManagementContextConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.web.util.matcher.RequestMatcher; -import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -40,7 +39,7 @@ */ class SecurityRequestMatchersManagementContextConfigurationTests { - private WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(SecurityRequestMatchersManagementContextConfiguration.class)); @Test @@ -64,7 +63,7 @@ void registersRequestMatcherProviderIfMvcPresent() { this.contextRunner.withUserConfiguration(TestMvcConfiguration.class).run((context) -> { AntPathRequestMatcherProvider matcherProvider = context.getBean(AntPathRequestMatcherProvider.class); RequestMatcher requestMatcher = matcherProvider.getRequestMatcher("/example"); - assertThat(ReflectionTestUtils.getField(requestMatcher, "pattern")).isEqualTo("/custom/example"); + assertThat(requestMatcher).extracting("pattern").isEqualTo("/custom/example"); }); } @@ -75,7 +74,7 @@ void registersRequestMatcherForJerseyProviderIfJerseyPresentAndMvcAbsent() { AntPathRequestMatcherProvider matcherProvider = context .getBean(AntPathRequestMatcherProvider.class); RequestMatcher requestMatcher = matcherProvider.getRequestMatcher("/example"); - assertThat(ReflectionTestUtils.getField(requestMatcher, "pattern")).isEqualTo("/admin/example"); + assertThat(requestMatcher).extracting("pattern").isEqualTo("/admin/example"); }); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/solr/SolrHealthContributorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/solr/SolrHealthContributorAutoConfigurationTests.java index 3250b2d2424a..562568ea592f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/solr/SolrHealthContributorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/solr/SolrHealthContributorAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ */ class SolrHealthContributorAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(SolrAutoConfiguration.class, SolrHealthContributorAutoConfiguration.class, HealthContributorAutoConfiguration.class)); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/startup/StartupEndpointAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/startup/StartupEndpointAutoConfigurationTests.java new file mode 100644 index 000000000000..163731311d09 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/startup/StartupEndpointAutoConfigurationTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.startup; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.startup.StartupEndpoint; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link StartupEndpointAutoConfiguration} + * + * @author Brian Clozel + */ +class StartupEndpointAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(StartupEndpointAutoConfiguration.class)); + + @Test + void runShouldNotHaveStartupEndpoint() { + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(StartupEndpoint.class)); + } + + @Test + void runWhenMissingAppStartupShouldNotHaveStartupEndpoint() { + this.contextRunner.withPropertyValues("management.endpoints.web.exposure.include=startup") + .run((context) -> assertThat(context).doesNotHaveBean(StartupEndpoint.class)); + } + + @Test + void runShouldHaveStartupEndpoint() { + new ApplicationContextRunner(() -> { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.setApplicationStartup(new BufferingApplicationStartup(1)); + return context; + }).withConfiguration(AutoConfigurations.of(StartupEndpointAutoConfiguration.class)) + .withPropertyValues("management.endpoints.web.exposure.include=startup") + .run((context) -> assertThat(context).hasSingleBean(StartupEndpoint.class)); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/system/DiskSpaceHealthContributorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/system/DiskSpaceHealthContributorAutoConfigurationTests.java index 88cda1c89b91..1e5ac554b84f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/system/DiskSpaceHealthContributorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/system/DiskSpaceHealthContributorAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,8 +34,9 @@ */ class DiskSpaceHealthContributorAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration(AutoConfigurations - .of(DiskSpaceHealthContributorAutoConfiguration.class, HealthContributorAutoConfiguration.class)); + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DiskSpaceHealthContributorAutoConfiguration.class, + HealthContributorAutoConfiguration.class)); @Test void runShouldCreateIndicator() { @@ -58,6 +59,12 @@ void thresholdCanBeCustomized() { }); } + @Test + void runWhenPathDoesNotExistShouldCreateIndicator() { + this.contextRunner.withPropertyValues("management.health.diskspace.path=does/not/exist") + .run((context) -> assertThat(context).hasSingleBean(DiskSpaceHealthIndicator.class)); + } + @Test void runWhenDisabledShouldNotCreateIndicator() { this.contextRunner.withPropertyValues("management.health.diskspace.enabled:false") diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseyChildManagementContextConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseyChildManagementContextConfigurationTests.java index 9d57fa5f6ed7..cca272741d1e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseyChildManagementContextConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseyChildManagementContextConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,8 +27,12 @@ import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.testsupport.classpath.ClassPathExclusions; import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; /** * Tests for {@link JerseyChildManagementContextConfiguration}. @@ -78,4 +82,25 @@ void resourceConfigCustomizerBeanIsNotRequired() { this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ResourceConfig.class)); } + @Test + void resourceConfigIsCustomizedWithResourceConfigCustomizerBean() { + this.contextRunner.withUserConfiguration(CustomizerConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(ResourceConfig.class); + ResourceConfig config = context.getBean(ResourceConfig.class); + ManagementContextResourceConfigCustomizer customizer = context + .getBean(ManagementContextResourceConfigCustomizer.class); + then(customizer).should().customize(config); + }); + } + + @Configuration(proxyBeanMethods = false) + static class CustomizerConfiguration { + + @Bean + ManagementContextResourceConfigCustomizer resourceConfigCustomizer() { + return mock(ManagementContextResourceConfigCustomizer.class); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseySameManagementContextConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseySameManagementContextConfigurationTests.java index d29c5e8d7390..e8c8120d3542 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseySameManagementContextConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/jersey/JerseySameManagementContextConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.actuate.autoconfigure.web.jersey; import org.glassfish.jersey.server.ResourceConfig; @@ -31,6 +32,7 @@ import org.springframework.context.annotation.Configuration; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; /** @@ -90,6 +92,17 @@ void servletRegistrationBeanIsAutoConfiguredWhenNeeded() { }); } + @Test + void resourceConfigIsCustomizedWithResourceConfigCustomizerBean() { + this.contextRunner.withUserConfiguration(CustomizerConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(ResourceConfig.class); + ResourceConfig config = context.getBean(ResourceConfig.class); + ManagementContextResourceConfigCustomizer customizer = context + .getBean(ManagementContextResourceConfigCustomizer.class); + then(customizer).should().customize(config); + }); + } + @Configuration(proxyBeanMethods = false) static class ConfigWithJerseyApplicationPath { @@ -110,4 +123,14 @@ ResourceConfig customResourceConfig() { } + @Configuration(proxyBeanMethods = false) + static class CustomizerConfiguration { + + @Bean + ManagementContextResourceConfigCustomizer resourceConfigCustomizer() { + return mock(ManagementContextResourceConfigCustomizer.class); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/mappings/MappingsEndpointAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/mappings/MappingsEndpointAutoConfigurationTests.java new file mode 100644 index 000000000000..36f70d28a68c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/mappings/MappingsEndpointAutoConfigurationTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.web.mappings; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.servlet.WebMvcEndpointManagementContextConfiguration; +import org.springframework.boot.actuate.web.mappings.MappingDescriptionProvider; +import org.springframework.boot.actuate.web.mappings.MappingsEndpoint; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MappingsEndpointAutoConfiguration}. + * + * @author Andy Wilkinson + */ +class MappingsEndpointAutoConfigurationTests { + + @Test + void whenEndpointIsUnavailableThenEndpointAndDescriptionProvidersAreNotCreated() { + new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(MappingsEndpointAutoConfiguration.class, + JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, + WebMvcAutoConfiguration.class, DispatcherServletAutoConfiguration.class, + EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, + WebMvcEndpointManagementContextConfiguration.class, PropertyPlaceholderAutoConfiguration.class)) + .run((context) -> { + assertThat(context).doesNotHaveBean(MappingsEndpoint.class); + assertThat(context).doesNotHaveBean(MappingDescriptionProvider.class); + }); + + } + + @Test + void whenEndpointIsAvailableThenEndpointAndDescriptionProvidersAreCreated() { + new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(MappingsEndpointAutoConfiguration.class, + JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, + WebMvcAutoConfiguration.class, DispatcherServletAutoConfiguration.class, + EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, + WebMvcEndpointManagementContextConfiguration.class, PropertyPlaceholderAutoConfiguration.class)) + .withPropertyValues("management.endpoints.web.exposure.include=mappings").run((context) -> { + assertThat(context).hasSingleBean(MappingsEndpoint.class); + assertThat(context.getBeansOfType(MappingDescriptionProvider.class)).hasSize(3); + }); + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/reactive/MockReactiveWebServerFactory.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/reactive/MockReactiveWebServerFactory.java new file mode 100644 index 000000000000..954e5a6ce13e --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/reactive/MockReactiveWebServerFactory.java @@ -0,0 +1,90 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.web.reactive; + +import java.util.Map; + +import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory; +import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory; +import org.springframework.boot.web.server.WebServer; +import org.springframework.http.server.reactive.HttpHandler; + +import static org.mockito.Mockito.spy; + +/** + * Mock {@link ReactiveWebServerFactory}. + * + * @author Brian Clozel + */ +class MockReactiveWebServerFactory extends AbstractReactiveWebServerFactory { + + private MockReactiveWebServer webServer; + + @Override + public WebServer getWebServer(HttpHandler httpHandler) { + this.webServer = spy(new MockReactiveWebServer(httpHandler, getPort())); + return this.webServer; + } + + MockReactiveWebServer getWebServer() { + return this.webServer; + } + + static class MockReactiveWebServer implements WebServer { + + private final int port; + + private HttpHandler httpHandler; + + private Map httpHandlerMap; + + MockReactiveWebServer(HttpHandler httpHandler, int port) { + this.httpHandler = httpHandler; + this.port = port; + } + + MockReactiveWebServer(Map httpHandlerMap, int port) { + this.httpHandlerMap = httpHandlerMap; + this.port = port; + } + + HttpHandler getHttpHandler() { + return this.httpHandler; + } + + Map getHttpHandlerMap() { + return this.httpHandlerMap; + } + + @Override + public void start() { + + } + + @Override + public void stop() { + + } + + @Override + public int getPort() { + return this.port; + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/reactive/ReactiveManagementChildContextConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/reactive/ReactiveManagementChildContextConfigurationIntegrationTests.java new file mode 100644 index 000000000000..c9cfda2b0a8d --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/reactive/ReactiveManagementChildContextConfigurationIntegrationTests.java @@ -0,0 +1,97 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.web.reactive; + +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration; +import org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration; +import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; +import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext; +import org.springframework.boot.test.context.runner.ContextConsumer; +import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; +import org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer; +import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link ReactiveManagementChildContextConfiguration}. + * + * @author Andy Wilkinson + */ +class ReactiveManagementChildContextConfigurationIntegrationTests { + + private final ReactiveWebApplicationContextRunner runner = new ReactiveWebApplicationContextRunner( + AnnotationConfigReactiveWebServerApplicationContext::new) + .withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class, + ReactiveWebServerFactoryAutoConfiguration.class, + ReactiveManagementContextAutoConfiguration.class, WebEndpointAutoConfiguration.class, + EndpointAutoConfiguration.class, HttpHandlerAutoConfiguration.class, + WebFluxAutoConfiguration.class)) + .withUserConfiguration(SucceedingEndpoint.class) + .withInitializer(new ServerPortInfoApplicationContextInitializer()).withPropertyValues( + "server.port=0", "management.server.port=0", "management.endpoints.web.exposure.include=*"); + + @Test + void endpointsAreBeneathActuatorByDefault() { + this.runner.withPropertyValues("management.server.port:0").run(withWebTestClient((client) -> { + String body = client.get().uri("actuator/success").accept(MediaType.APPLICATION_JSON) + .exchangeToMono((response) -> response.bodyToMono(String.class)).block(); + assertThat(body).isEqualTo("Success"); + })); + } + + @Test + void whenManagementServerBasePathIsConfiguredThenEndpointsAreBeneathThatPath() { + this.runner.withPropertyValues("management.server.port:0", "management.server.base-path:/manage") + .run(withWebTestClient((client) -> { + String body = client.get().uri("manage/actuator/success").accept(MediaType.APPLICATION_JSON) + .exchangeToMono((response) -> response.bodyToMono(String.class)).block(); + assertThat(body).isEqualTo("Success"); + })); + } + + private ContextConsumer withWebTestClient(Consumer webClient) { + return (context) -> { + String port = context.getEnvironment().getProperty("local.management.port"); + WebClient client = WebClient.create("http://localhost:" + port); + webClient.accept(client); + }; + } + + @Endpoint(id = "success") + static class SucceedingEndpoint { + + @ReadOperation + String fail() { + return "Success"; + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/reactive/ReactiveManagementContextFactoryTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/reactive/ReactiveManagementContextFactoryTests.java index a046568ba852..2d5aeee54146 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/reactive/ReactiveManagementContextFactoryTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/reactive/ReactiveManagementContextFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -60,7 +60,7 @@ static class ParentConfiguration { @Bean ReactiveWebServerFactory reactiveWebServerFactory() { - return mock(ReactiveWebServerFactory.class); + return new MockReactiveWebServerFactory(); } @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementContextAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementContextAutoConfigurationTests.java index 681216d1ca46..6f920fe9ffb2 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementContextAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementContextAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.actuate.autoconfigure.web.server; import java.util.function.Consumer; @@ -24,6 +25,7 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.test.system.CapturedOutput; @@ -54,6 +56,19 @@ void childManagementContextShouldStartForEmbeddedServer(CapturedOutput output) { .run((context) -> assertThat(output).satisfies(numberOfOccurrences("Tomcat started on port", 2))); } + @Test + void givenSamePortManagementServerWhenManagementServerAddressIsConfiguredThenContextRefreshFails() { + WebApplicationContextRunner contextRunner = new WebApplicationContextRunner( + AnnotationConfigServletWebServerApplicationContext::new) + .withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class, + ServletWebServerFactoryAutoConfiguration.class, + ServletManagementContextAutoConfiguration.class, WebEndpointAutoConfiguration.class, + EndpointAutoConfiguration.class, DispatcherServletAutoConfiguration.class)); + contextRunner.withPropertyValues("server.port=0", "management.server.address=127.0.0.1") + .run((context) -> assertThat(context).getFailure() + .hasMessageStartingWith("Management-specific server address cannot be configured")); + } + private Consumer numberOfOccurrences(String substring, int expectedCount) { return (charSequence) -> { int count = StringUtils.countOccurrencesOf(charSequence.toString(), substring); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementServerPropertiesTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementServerPropertiesTests.java index bea755292c2b..a4c24495c7bc 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementServerPropertiesTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementServerPropertiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,22 +29,48 @@ class ManagementServerPropertiesTests { @Test - void defaultManagementServerProperties() { + void defaultPortIsNull() { ManagementServerProperties properties = new ManagementServerProperties(); assertThat(properties.getPort()).isNull(); - assertThat(properties.getServlet().getContextPath()).isEqualTo(""); } @Test - void definedManagementServerProperties() { + void definedPort() { ManagementServerProperties properties = new ManagementServerProperties(); properties.setPort(123); - properties.getServlet().setContextPath("/foo"); assertThat(properties.getPort()).isEqualTo(123); + } + + @Test + @Deprecated + void defaultContextPathIsEmptyString() { + ManagementServerProperties properties = new ManagementServerProperties(); + assertThat(properties.getServlet().getContextPath()).isEqualTo(""); + } + + @Test + @Deprecated + void definedContextPath() { + ManagementServerProperties properties = new ManagementServerProperties(); + properties.getServlet().setContextPath("/foo"); assertThat(properties.getServlet().getContextPath()).isEqualTo("/foo"); } @Test + void defaultBasePathIsEmptyString() { + ManagementServerProperties properties = new ManagementServerProperties(); + assertThat(properties.getBasePath()).isEqualTo(""); + } + + @Test + void definedBasePath() { + ManagementServerProperties properties = new ManagementServerProperties(); + properties.setBasePath("/foo"); + assertThat(properties.getBasePath()).isEqualTo("/foo"); + } + + @Test + @Deprecated void trailingSlashOfContextPathIsRemoved() { ManagementServerProperties properties = new ManagementServerProperties(); properties.getServlet().setContextPath("/foo/"); @@ -52,10 +78,25 @@ void trailingSlashOfContextPathIsRemoved() { } @Test + void trailingSlashOfBasePathIsRemoved() { + ManagementServerProperties properties = new ManagementServerProperties(); + properties.setBasePath("/foo/"); + assertThat(properties.getBasePath()).isEqualTo("/foo"); + } + + @Test + @Deprecated void slashOfContextPathIsDefaultValue() { ManagementServerProperties properties = new ManagementServerProperties(); properties.getServlet().setContextPath("/"); assertThat(properties.getServlet().getContextPath()).isEqualTo(""); } + @Test + void slashOfBasePathIsDefaultValue() { + ManagementServerProperties properties = new ManagementServerProperties(); + properties.setBasePath("/"); + assertThat(properties.getBasePath()).isEqualTo(""); + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/CompositeHandlerExceptionResolverTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/CompositeHandlerExceptionResolverTests.java index 4fdc3f0e8a48..f78843a96ee7 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/CompositeHandlerExceptionResolverTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/CompositeHandlerExceptionResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,6 +38,7 @@ * Tests for {@link CompositeHandlerExceptionResolver}. * * @author Madhura Bhave + * @author Scott Frederick */ class CompositeHandlerExceptionResolverTests { @@ -62,9 +63,10 @@ void resolverShouldAddDefaultResolverIfNonePresent() { load(BaseConfiguration.class); CompositeHandlerExceptionResolver resolver = (CompositeHandlerExceptionResolver) this.context .getBean(DispatcherServlet.HANDLER_EXCEPTION_RESOLVER_BEAN_NAME); - ModelAndView resolved = resolver.resolveException(this.request, this.response, null, - new HttpRequestMethodNotSupportedException("POST")); + HttpRequestMethodNotSupportedException exception = new HttpRequestMethodNotSupportedException("POST"); + ModelAndView resolved = resolver.resolveException(this.request, this.response, null, exception); assertThat(resolved).isNotNull(); + assertThat(resolved.isEmpty()).isTrue(); } private void load(Class... configs) { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/ManagementErrorEndpointTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/ManagementErrorEndpointTests.java new file mode 100644 index 000000000000..a11e090e4ff7 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/ManagementErrorEndpointTests.java @@ -0,0 +1,167 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.web.servlet; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.web.ErrorProperties; +import org.springframework.boot.web.error.ErrorAttributeOptions; +import org.springframework.boot.web.servlet.error.DefaultErrorAttributes; +import org.springframework.boot.web.servlet.error.ErrorAttributes; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.context.request.WebRequest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +/** + * Tests for {@link ManagementErrorEndpoint}. + * + * @author Scott Frederick + */ +class ManagementErrorEndpointTests { + + private final ErrorAttributes errorAttributes = new DefaultErrorAttributes(); + + private final ErrorProperties errorProperties = new ErrorProperties(); + + private final MockHttpServletRequest request = new MockHttpServletRequest(); + + @BeforeEach + void setUp() { + this.request.setAttribute("javax.servlet.error.exception", new RuntimeException("test exception")); + } + + @Test + void errorResponseNeverDetails() { + ManagementErrorEndpoint endpoint = new ManagementErrorEndpoint(this.errorAttributes, this.errorProperties); + Map response = endpoint.invoke(new ServletWebRequest(new MockHttpServletRequest())); + assertThat(response).doesNotContainKey("message"); + assertThat(response).doesNotContainKey("trace"); + } + + @Test + void errorResponseAlwaysDetails() { + this.errorProperties.setIncludeStacktrace(ErrorProperties.IncludeAttribute.ALWAYS); + this.errorProperties.setIncludeMessage(ErrorProperties.IncludeAttribute.ALWAYS); + this.request.addParameter("trace", "false"); + this.request.addParameter("message", "false"); + ManagementErrorEndpoint endpoint = new ManagementErrorEndpoint(this.errorAttributes, this.errorProperties); + Map response = endpoint.invoke(new ServletWebRequest(this.request)); + assertThat(response).containsEntry("message", "test exception"); + assertThat(response).hasEntrySatisfying("trace", + (value) -> assertThat(value).asString().startsWith("java.lang.RuntimeException: test exception")); + } + + @Test + void errorResponseParamsAbsent() { + this.errorProperties.setIncludeStacktrace(ErrorProperties.IncludeAttribute.ON_PARAM); + this.errorProperties.setIncludeMessage(ErrorProperties.IncludeAttribute.ON_PARAM); + ManagementErrorEndpoint endpoint = new ManagementErrorEndpoint(this.errorAttributes, this.errorProperties); + Map response = endpoint.invoke(new ServletWebRequest(this.request)); + assertThat(response).doesNotContainKey("message"); + assertThat(response).doesNotContainKey("trace"); + } + + @Test + void errorResponseParamsTrue() { + this.errorProperties.setIncludeStacktrace(ErrorProperties.IncludeAttribute.ON_PARAM); + this.errorProperties.setIncludeMessage(ErrorProperties.IncludeAttribute.ON_PARAM); + this.request.addParameter("trace", "true"); + this.request.addParameter("message", "true"); + ManagementErrorEndpoint endpoint = new ManagementErrorEndpoint(this.errorAttributes, this.errorProperties); + Map response = endpoint.invoke(new ServletWebRequest(this.request)); + assertThat(response).containsEntry("message", "test exception"); + assertThat(response).hasEntrySatisfying("trace", + (value) -> assertThat(value).asString().startsWith("java.lang.RuntimeException: test exception")); + } + + @Test + void errorResponseParamsFalse() { + this.errorProperties.setIncludeStacktrace(ErrorProperties.IncludeAttribute.ON_PARAM); + this.errorProperties.setIncludeMessage(ErrorProperties.IncludeAttribute.ON_PARAM); + this.request.addParameter("trace", "false"); + this.request.addParameter("message", "false"); + ManagementErrorEndpoint endpoint = new ManagementErrorEndpoint(this.errorAttributes, this.errorProperties); + Map response = endpoint.invoke(new ServletWebRequest(this.request)); + assertThat(response).doesNotContainKey("message"); + assertThat(response).doesNotContainKey("trace"); + } + + @Test + void errorResponseWithCustomErrorAttributesUsingDeprecatedApi() { + ErrorAttributes attributes = new ErrorAttributes() { + + @Override + public Map getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) { + return Collections.singletonMap("message", "An error occurred"); + } + + @Override + public Throwable getError(WebRequest webRequest) { + return null; + } + + }; + ManagementErrorEndpoint endpoint = new ManagementErrorEndpoint(attributes, this.errorProperties); + Map response = endpoint.invoke(new ServletWebRequest(new MockHttpServletRequest())); + assertThat(response).containsExactly(entry("message", "An error occurred")); + } + + @Test + void errorResponseWithDefaultErrorAttributesSubclassUsingDelegation() { + ErrorAttributes attributes = new DefaultErrorAttributes() { + + @Override + public Map getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) { + Map response = super.getErrorAttributes(webRequest, options); + response.put("error", "custom error"); + response.put("custom", "value"); + response.remove("path"); + return response; + } + + }; + ManagementErrorEndpoint endpoint = new ManagementErrorEndpoint(attributes, this.errorProperties); + Map response = endpoint.invoke(new ServletWebRequest(new MockHttpServletRequest())); + assertThat(response).containsEntry("error", "custom error"); + assertThat(response).containsEntry("custom", "value"); + assertThat(response).doesNotContainKey("path"); + assertThat(response).containsKey("timestamp"); + } + + @Test + void errorResponseWithDefaultErrorAttributesSubclassWithoutDelegation() { + ErrorAttributes attributes = new DefaultErrorAttributes() { + + @Override + public Map getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) { + return Collections.singletonMap("error", "custom error"); + } + + }; + ManagementErrorEndpoint endpoint = new ManagementErrorEndpoint(attributes, this.errorProperties); + Map response = endpoint.invoke(new ServletWebRequest(new MockHttpServletRequest())); + assertThat(response).containsExactly(entry("error", "custom error")); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/MockServletWebServerFactory.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/MockServletWebServerFactory.java index 9ef18a083050..20325894bffa 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/MockServletWebServerFactory.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/MockServletWebServerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,6 @@ import org.springframework.boot.testsupport.web.servlet.MockServletWebServer.RegisteredFilter; import org.springframework.boot.testsupport.web.servlet.MockServletWebServer.RegisteredServlet; import org.springframework.boot.web.server.WebServer; -import org.springframework.boot.web.server.WebServerException; import org.springframework.boot.web.servlet.ServletContextInitializer; import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory; import org.springframework.boot.web.servlet.server.ServletWebServerFactory; @@ -71,7 +70,7 @@ static class MockServletWebServer extends org.springframework.boot.testsupport.w } @Override - public void start() throws WebServerException { + public void start() { } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfigurationIntegrationTests.java index d82ea973b7d7..400661f60c13 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfigurationIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfigurationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,22 +16,38 @@ package org.springframework.boot.actuate.autoconfigure.web.servlet; +import java.util.Collections; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; + +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; + import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration; +import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; +import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer; import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.MediaType; -import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.WebClient; @@ -41,29 +57,117 @@ * Integration tests for {@link WebMvcEndpointChildContextConfiguration}. * * @author Phillip Webb + * @author Scott Frederick */ class WebMvcEndpointChildContextConfigurationIntegrationTests { + private final WebApplicationContextRunner runner = new WebApplicationContextRunner( + AnnotationConfigServletWebServerApplicationContext::new) + .withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class, + ServletWebServerFactoryAutoConfiguration.class, + ServletManagementContextAutoConfiguration.class, WebEndpointAutoConfiguration.class, + EndpointAutoConfiguration.class, DispatcherServletAutoConfiguration.class, + ErrorMvcAutoConfiguration.class)) + .withUserConfiguration(SucceedingEndpoint.class, FailingEndpoint.class, + FailingControllerEndpoint.class) + .withInitializer(new ServerPortInfoApplicationContextInitializer()) + .withPropertyValues("server.port=0", "management.server.port=0", + "management.endpoints.web.exposure.include=*", "server.error.include-exception=true", + "server.error.include-message=always", "server.error.include-binding-errors=always"); + @Test // gh-17938 - void errorPageAndErrorControllerAreUsed() { - new WebApplicationContextRunner(AnnotationConfigServletWebServerApplicationContext::new) - .withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class, - ServletWebServerFactoryAutoConfiguration.class, ServletManagementContextAutoConfiguration.class, - WebEndpointAutoConfiguration.class, EndpointAutoConfiguration.class, - DispatcherServletAutoConfiguration.class, ErrorMvcAutoConfiguration.class)) - .withUserConfiguration(FailingEndpoint.class) - .withInitializer(new ServerPortInfoApplicationContextInitializer()).withPropertyValues("server.port=0", - "management.server.port=0", "management.endpoints.web.exposure.include=*") - .run((context) -> { - String port = context.getEnvironment().getProperty("local.management.port"); - WebClient client = WebClient.create("http://localhost:" + port); - ClientResponse response = client.get().uri("actuator/fail").accept(MediaType.APPLICATION_JSON) - .exchange().block(); - assertThat(response.bodyToMono(String.class).block()).contains("message\":\"Epic Fail"); - }); + void errorEndpointIsUsedWithEndpoint() { + this.runner.run(withWebTestClient((client) -> { + Map body = client.get().uri("actuator/fail").accept(MediaType.APPLICATION_JSON) + .exchangeToMono(toResponseBody()).block(); + assertThat(body).hasEntrySatisfying("exception", + (value) -> assertThat(value).asString().contains("IllegalStateException")); + assertThat(body).hasEntrySatisfying("message", + (value) -> assertThat(value).asString().contains("Epic Fail")); + })); + } + + @Test + void errorPageAndErrorControllerIncludeDetails() { + this.runner.withPropertyValues("server.error.include-stacktrace=always", "server.error.include-message=always") + .run(withWebTestClient((client) -> { + Map body = client.get().uri("actuator/fail").accept(MediaType.APPLICATION_JSON) + .exchangeToMono(toResponseBody()).block(); + assertThat(body).hasEntrySatisfying("message", + (value) -> assertThat(value).asString().contains("Epic Fail")); + assertThat(body).hasEntrySatisfying("trace", (value) -> assertThat(value).asString() + .contains("java.lang.IllegalStateException: Epic Fail")); + })); + } + + @Test + void errorEndpointIsUsedWithRestControllerEndpoint() { + this.runner.run(withWebTestClient((client) -> { + Map body = client.get().uri("actuator/failController").accept(MediaType.APPLICATION_JSON) + .exchangeToMono(toResponseBody()).block(); + assertThat(body).hasEntrySatisfying("exception", + (value) -> assertThat(value).asString().contains("IllegalStateException")); + assertThat(body).hasEntrySatisfying("message", + (value) -> assertThat(value).asString().contains("Epic Fail")); + })); + } + + @Test + void errorEndpointIsUsedWithRestControllerEndpointOnBindingError() { + this.runner.run(withWebTestClient((client) -> { + Map body = client.post().uri("actuator/failController") + .bodyValue(Collections.singletonMap("content", "")).accept(MediaType.APPLICATION_JSON) + .exchangeToMono(toResponseBody()).block(); + assertThat(body).hasEntrySatisfying("exception", + (value) -> assertThat(value).asString().contains("MethodArgumentNotValidException")); + assertThat(body).hasEntrySatisfying("message", + (value) -> assertThat(value).asString().contains("Validation failed")); + assertThat(body).hasEntrySatisfying("errors", (value) -> assertThat(value).asList().isNotEmpty()); + })); + } + + @Test + void whenManagementServerBasePathIsConfiguredThenEndpointsAreBeneathThatPath() { + this.runner.withPropertyValues("management.server.base-path:/manage").run(withWebTestClient((client) -> { + String body = client.get().uri("manage/actuator/success").accept(MediaType.APPLICATION_JSON) + .exchangeToMono((response) -> response.bodyToMono(String.class)).block(); + assertThat(body).isEqualTo("Success"); + })); + } + + @Test + void whenManagementServletContextPathIsConfiguredThenEndpointsAreBeneathThatPath() { + this.runner.withPropertyValues("management.server.servlet.context-path:/manage") + .run(withWebTestClient((client) -> { + String body = client.get().uri("manage/actuator/success").accept(MediaType.APPLICATION_JSON) + .exchangeToMono((response) -> response.bodyToMono(String.class)).block(); + assertThat(body).isEqualTo("Success"); + })); + } + + @Test + void whenManagementBasePathAndServletContextPathAreConfiguredThenEndpointsAreBeneathBasePath() { + this.runner.withPropertyValues("management.server.servlet.context-path:/admin", + "management.server.base-path:/manage").run(withWebTestClient((client) -> { + String body = client.get().uri("manage/actuator/success").accept(MediaType.APPLICATION_JSON) + .exchangeToMono((response) -> response.bodyToMono(String.class)).block(); + assertThat(body).isEqualTo("Success"); + })); + } + + private ContextConsumer withWebTestClient(Consumer webClient) { + return (context) -> { + String port = context.getEnvironment().getProperty("local.management.port"); + WebClient client = WebClient.create("http://localhost:" + port); + webClient.accept(client); + }; + } + + private Function>> toResponseBody() { + return ((clientResponse) -> clientResponse.bodyToMono(new ParameterizedTypeReference>() { + })); } - @Component @Endpoint(id = "fail") static class FailingEndpoint { @@ -74,4 +178,45 @@ String fail() { } + @Endpoint(id = "success") + static class SucceedingEndpoint { + + @ReadOperation + String fail() { + return "Success"; + } + + } + + @RestControllerEndpoint(id = "failController") + static class FailingControllerEndpoint { + + @GetMapping + String fail() { + throw new IllegalStateException("Epic Fail"); + } + + @PostMapping(produces = "application/json") + @ResponseBody + String bodyValidation(@Valid @RequestBody TestBody body) { + return body.getContent(); + } + + } + + public static class TestBody { + + @NotEmpty + private String content; + + public String getContent() { + return this.content; + } + + public void setContent(String content) { + this.content = content; + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfigurationTests.java index e4aa6e719f87..a4e8a30864e8 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,8 @@ */ class WebMvcEndpointChildContextConfigurationTests { - private WebApplicationContextRunner contextRunner = new WebApplicationContextRunner(); + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withAllowBeanDefinitionOverriding(true); @Test void contextShouldConfigureRequestContextFilter() { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/trace/HttpTraceAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/trace/HttpTraceAutoConfigurationTests.java index ad28940ccf14..bef1caa34ddb 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/trace/HttpTraceAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/trace/HttpTraceAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,7 +46,7 @@ */ class HttpTraceAutoConfigurationTests { - private WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(HttpTraceAutoConfiguration.class)); @Test diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/resources/cache/test-ehcache.xml b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/resources/cache/test-ehcache.xml deleted file mode 100644 index cdee1d0503e8..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/resources/cache/test-ehcache.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/resources/cache/test-hazelcast.xml b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/resources/cache/test-hazelcast.xml deleted file mode 100644 index bc025d7316c3..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/resources/cache/test-hazelcast.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/resources/cache/test-infinispan.xml b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/resources/cache/test-infinispan.xml deleted file mode 100644 index 741fb6292b7b..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/resources/cache/test-infinispan.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/resources/city-data.sql b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/resources/city-data.sql new file mode 100644 index 000000000000..eb08623b4cf0 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/resources/city-data.sql @@ -0,0 +1 @@ +INSERT INTO CITY (ID, NAME, STATE, COUNTRY, MAP) values (2000, 'Washington', 'DC', 'US', 'Google'); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/resources/data-jdbc-schema.sql b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/resources/city-schema.sql similarity index 100% rename from spring-boot-project/spring-boot-autoconfigure/src/test/resources/data-jdbc-schema.sql rename to spring-boot-project/spring-boot-actuator-autoconfigure/src/test/resources/city-schema.sql diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/resources/hazelcast.xml b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/resources/hazelcast.xml index 1a61470e6004..cfb3b2e15b0d 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/resources/hazelcast.xml +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/resources/hazelcast.xml @@ -1,5 +1,5 @@ diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/resources/saml/certificate-location b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/resources/saml/certificate-location new file mode 100644 index 000000000000..c04a9c1602fa --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/resources/saml/certificate-location @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYD +VQQGEwJVUzELMAkGA1UECAwCQ08xFDASBgNVBAcMC0Nhc3RsZSBSb2NrMRwwGgYD +VQQKDBNTYW1sIFRlc3RpbmcgU2VydmVyMQswCQYDVQQLDAJJVDEgMB4GA1UEAwwX +c2ltcGxlc2FtbHBocC5jZmFwcHMuaW8xIDAeBgkqhkiG9w0BCQEWEWZoYW5pa0Bw +aXZvdGFsLmlvMB4XDTE1MDIyMzIyNDUwM1oXDTI1MDIyMjIyNDUwM1owgZ8xCzAJ +BgNVBAYTAlVTMQswCQYDVQQIDAJDTzEUMBIGA1UEBwwLQ2FzdGxlIFJvY2sxHDAa +BgNVBAoME1NhbWwgVGVzdGluZyBTZXJ2ZXIxCzAJBgNVBAsMAklUMSAwHgYDVQQD +DBdzaW1wbGVzYW1scGhwLmNmYXBwcy5pbzEgMB4GCSqGSIb3DQEJARYRZmhhbmlr +QHBpdm90YWwuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4cn62 +E1xLqpN34PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz +2ZivLwZXW+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWW +RDodcoHEfDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQ +nX8Ttl7hZ6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5 +cljz0X/TXy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gph +iJH3jvZ7I+J5lS8VAgMBAAGjUDBOMB0GA1UdDgQWBBTTyP6Cc5HlBJ5+ucVCwGc5 +ogKNGzAfBgNVHSMEGDAWgBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAMBgNVHRMEBTAD +AQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAvMS4EQeP/ipV4jOG5lO6/tYCb/iJeAduO +nRhkJk0DbX329lDLZhTTL/x/w/9muCVcvLrzEp6PN+VWfw5E5FWtZN0yhGtP9R+v +ZnrV+oc2zGD+no1/ySFOe3EiJCO5dehxKjYEmBRv5sU/LZFKZpozKN/BMEa6CqLu +xbzb7ykxVr7EVFXwltPxzE9TmL9OACNNyF5eJHWMRMllarUvkcXlh4pux4ks9e6z +V9DQBy2zds9f1I3qxg0eX6JnGrXi/ZiCT+lJgVe3ZFXiejiLAiKB04sXW3ti0LW3 +lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk +-----END CERTIFICATE----- \ No newline at end of file diff --git a/spring-boot-project/spring-boot-actuator/README.adoc b/spring-boot-project/spring-boot-actuator/README.adoc index d577588c7f37..3adcc5550bad 100644 --- a/spring-boot-project/spring-boot-actuator/README.adoc +++ b/spring-boot-project/spring-boot-actuator/README.adoc @@ -8,7 +8,7 @@ https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#production covers the features in more detail. == Enabling the Actuator -The simplest way to enable the features is to add a dependency to the +The recommended way to enable the features is to add a dependency to the `spring-boot-starter-actuator` '`Starter`'. To add the actuator to a Maven-based project, add the following '`Starter`' dependency: @@ -27,7 +27,7 @@ For Gradle, use the following declaration: [indent=0] ---- dependencies { - compile("org.springframework.boot:spring-boot-starter-actuator") + implementation 'org.springframework.boot:spring-boot-starter-actuator' } ---- diff --git a/spring-boot-project/spring-boot-actuator/build.gradle b/spring-boot-project/spring-boot-actuator/build.gradle new file mode 100644 index 000000000000..a9a6a5c9bd9a --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/build.gradle @@ -0,0 +1,111 @@ +plugins { + id "java-library" + id "org.springframework.boot.conventions" + id "org.springframework.boot.configuration-properties" + id "org.springframework.boot.optional-dependencies" + id "org.springframework.boot.deployed" +} + +description = "Spring Boot Actuator" + +dependencies { + api(project(":spring-boot-project:spring-boot")) + + optional("com.datastax.oss:java-driver-core") { + exclude group: "org.slf4j", module: "jcl-over-slf4j" + } + optional("com.fasterxml.jackson.core:jackson-databind") + optional("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") + optional("com.github.ben-manes.caffeine:caffeine") + optional("com.hazelcast:hazelcast") + optional("com.hazelcast:hazelcast-spring") + optional("com.sun.mail:jakarta.mail") + optional("com.zaxxer:HikariCP") + optional("io.lettuce:lettuce-core") + optional("io.micrometer:micrometer-core") + optional("io.micrometer:micrometer-registry-prometheus") + optional("io.prometheus:simpleclient_pushgateway") { + exclude(group: "javax.xml.bind", module: "jaxb-api") + } + optional("io.r2dbc:r2dbc-pool") + optional("io.r2dbc:r2dbc-spi") + optional("io.reactivex:rxjava-reactive-streams") + optional("org.elasticsearch.client:elasticsearch-rest-client") { + exclude(group: "commons-logging", module: "commons-logging") + } + optional("io.undertow:undertow-servlet") { + exclude group: "org.jboss.spec.javax.annotation", module: "jboss-annotations-api_1.3_spec" + exclude group: "org.jboss.spec.javax.servlet", module: "jboss-servlet-api_4.0_spec" + } + optional("javax.cache:cache-api") + optional("jakarta.jms:jakarta.jms-api") + optional("net.sf.ehcache:ehcache") + optional("org.apache.solr:solr-solrj") { + exclude group: "org.slf4j", module: "jcl-over-slf4j" + } + optional("org.apache.tomcat.embed:tomcat-embed-core") + optional("org.aspectj:aspectjweaver") + optional("org.eclipse.jetty:jetty-server") { + exclude(group: "javax.servlet", module: "javax.servlet-api") + } + optional("org.elasticsearch:elasticsearch") + optional("org.flywaydb:flyway-core") + optional("org.glassfish.jersey.core:jersey-server") + optional("org.glassfish.jersey.containers:jersey-container-servlet-core") + optional("org.hibernate.validator:hibernate-validator") + optional("org.influxdb:influxdb-java") + optional("org.liquibase:liquibase-core") { + exclude(group: "javax.xml.bind", module: "jaxb-api") + } + optional("org.mongodb:mongodb-driver-reactivestreams") + optional("org.mongodb:mongodb-driver-sync") + optional("org.neo4j.driver:neo4j-java-driver") + optional("org.quartz-scheduler:quartz") + optional("org.springframework:spring-jdbc") + optional("org.springframework:spring-messaging") + optional("org.springframework:spring-webflux") + optional("org.springframework:spring-web") + optional("org.springframework:spring-webmvc") + optional("org.springframework.amqp:spring-rabbit") + optional("org.springframework.data:spring-data-cassandra") { + exclude group: "org.slf4j", module: "jcl-over-slf4j" + } + optional("org.springframework.data:spring-data-couchbase") + optional("org.springframework.data:spring-data-elasticsearch") { + exclude(group: "commons-logging", module: "commons-logging") + } + optional("org.springframework.data:spring-data-ldap") + optional("org.springframework.data:spring-data-mongodb") + optional("org.springframework.data:spring-data-redis") + optional("org.springframework.data:spring-data-rest-webmvc") + optional("org.springframework.integration:spring-integration-core") + optional("org.springframework.security:spring-security-core") + optional("org.springframework.security:spring-security-web") + optional("org.springframework.session:spring-session-core") + + testImplementation(project(":spring-boot-project:spring-boot-test")) + testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) + testImplementation(project(":spring-boot-project:spring-boot-autoconfigure")) + testImplementation("org.assertj:assertj-core") + testImplementation("com.jayway.jsonpath:json-path") + testImplementation("io.projectreactor:reactor-test") + testImplementation("io.r2dbc:r2dbc-h2") + testImplementation("org.apache.logging.log4j:log4j-to-slf4j") + testImplementation("org.awaitility:awaitility") + testImplementation("org.glassfish.jersey.media:jersey-media-json-jackson") + testImplementation("org.hamcrest:hamcrest") + testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.mockito:mockito-core") + testImplementation("org.mockito:mockito-junit-jupiter") + testImplementation("org.skyscreamer:jsonassert") + testImplementation("org.springframework:spring-test") + testImplementation("com.squareup.okhttp3:mockwebserver") + testImplementation("org.testcontainers:junit-jupiter") + + testRuntimeOnly("ch.qos.logback:logback-classic") + testRuntimeOnly("io.projectreactor.netty:reactor-netty-http") + testRuntimeOnly("jakarta.xml.bind:jakarta.xml.bind-api") + testRuntimeOnly("org.apache.tomcat.embed:tomcat-embed-el") + testRuntimeOnly("org.glassfish.jersey.ext:jersey-spring5") + testRuntimeOnly("org.hsqldb:hsqldb") +} diff --git a/spring-boot-project/spring-boot-actuator/pom.xml b/spring-boot-project/spring-boot-actuator/pom.xml deleted file mode 100644 index c18b3ce871e8..000000000000 --- a/spring-boot-project/spring-boot-actuator/pom.xml +++ /dev/null @@ -1,396 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-parent - ${revision} - ../spring-boot-parent - - spring-boot-actuator - Spring Boot Actuator - Spring Boot Actuator - - ${basedir}/../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - - org.springframework.boot - spring-boot - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - - - - com.fasterxml.jackson.core - jackson-databind - true - - - com.hazelcast - hazelcast - true - - - com.hazelcast - hazelcast-spring - true - - - com.sun.mail - jakarta.mail - true - - - com.zaxxer - HikariCP - true - - - io.lettuce - lettuce-core - true - - - io.micrometer - micrometer-core - true - - - io.micrometer - micrometer-registry-prometheus - true - - - io.prometheus - simpleclient_pushgateway - true - - - io.reactivex - rxjava-reactive-streams - true - - - io.searchbox - jest - true - - - org.elasticsearch.client - elasticsearch-rest-client - true - - - io.undertow - undertow-servlet - true - - - org.jboss.spec.javax.servlet - jboss-servlet-api_3.1_spec - - - - - javax.cache - cache-api - true - - - jakarta.jms - jakarta.jms-api - true - - - jakarta.ws.rs - jakarta.ws.rs-api - true - - - net.sf.ehcache - ehcache - true - - - org.apache.tomcat.embed - tomcat-embed-core - true - - - org.aspectj - aspectjweaver - true - - - org.eclipse.jetty - jetty-server - true - - - javax.servlet - javax.servlet-api - - - - - org.elasticsearch - elasticsearch - true - - - org.flywaydb - flyway-core - true - - - org.glassfish.jersey.core - jersey-server - true - - - javax.validation - validation-api - - - - - org.glassfish.jersey.containers - jersey-container-servlet-core - true - - - org.hibernate.validator - hibernate-validator - true - - - javax.validation - validation-api - - - - - org.infinispan - infinispan-spring5-embedded - true - - - org.influxdb - influxdb-java - true - - - org.liquibase - liquibase-core - true - - - org.mongodb - mongodb-driver-async - true - - - org.mongodb - mongodb-driver-reactivestreams - true - - - org.springframework - spring-jdbc - true - - - org.springframework - spring-messaging - true - - - org.springframework - spring-webflux - true - - - org.springframework - spring-web - true - - - org.springframework - spring-webmvc - true - - - org.springframework.amqp - spring-rabbit - true - - - org.springframework.data - spring-data-cassandra - true - - - org.springframework.data - spring-data-couchbase - true - - - org.springframework.data - spring-data-ldap - true - - - org.springframework.data - spring-data-mongodb - true - - - org.springframework.data - spring-data-neo4j - true - - - org.springframework.data - spring-data-redis - true - - - org.springframework.data - spring-data-rest-webmvc - true - - - org.springframework.data - spring-data-solr - true - - - - wstx-asl - org.codehaus.woodstox - - - - - org.springframework.integration - spring-integration-core - true - - - org.springframework.security - spring-security-core - true - - - org.springframework.security - spring-security-web - true - - - org.springframework.session - spring-session-core - true - - - - org.springframework.boot - spring-boot-configuration-processor - true - - - - org.springframework.boot - spring-boot-test - test - - - org.springframework.boot - spring-boot-test-support - test - - - org.springframework.boot - spring-boot-autoconfigure - test - - - ch.qos.logback - logback-classic - test - - - jakarta.validation - jakarta.validation-api - test - - - jakarta.xml.bind - jakarta.xml.bind-api - test - - - org.apache.logging.log4j - log4j-to-slf4j - test - - - org.glassfish.jersey.media - jersey-media-json-jackson - test - - - org.awaitility - awaitility - test - - - com.jayway.jsonpath - json-path - test - - - io.projectreactor - reactor-test - test - - - io.projectreactor.netty - reactor-netty - test - - - org.hsqldb - hsqldb - test - - - org.glassfish.jersey.ext - jersey-spring5 - test - - - javax.validation - validation-api - - - - - org.skyscreamer - jsonassert - test - - - - diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/availability/AvailabilityStateHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/availability/AvailabilityStateHealthIndicator.java new file mode 100644 index 000000000000..3564d5a77e7d --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/availability/AvailabilityStateHealthIndicator.java @@ -0,0 +1,123 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.availability; + +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +import org.springframework.boot.actuate.health.AbstractHealthIndicator; +import org.springframework.boot.actuate.health.Health.Builder; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.actuate.health.Status; +import org.springframework.boot.availability.ApplicationAvailability; +import org.springframework.boot.availability.AvailabilityState; +import org.springframework.util.Assert; + +/** + * A {@link HealthIndicator} that checks a specific {@link AvailabilityState} of the + * application. + * + * @author Phillip Webb + * @author Brian Clozel + * @since 2.3.0 + */ +public class AvailabilityStateHealthIndicator extends AbstractHealthIndicator { + + private final ApplicationAvailability applicationAvailability; + + private final Class stateType; + + private final Map statusMappings = new HashMap<>(); + + /** + * Create a new {@link AvailabilityStateHealthIndicator} instance. + * @param the availability state type + * @param applicationAvailability the application availability + * @param stateType the availability state type + * @param statusMappings consumer used to setup the status mappings + */ + public AvailabilityStateHealthIndicator( + ApplicationAvailability applicationAvailability, Class stateType, + Consumer> statusMappings) { + Assert.notNull(applicationAvailability, "ApplicationAvailability must not be null"); + Assert.notNull(stateType, "StateType must not be null"); + Assert.notNull(statusMappings, "StatusMappings must not be null"); + this.applicationAvailability = applicationAvailability; + this.stateType = stateType; + statusMappings.accept(this.statusMappings::put); + assertAllEnumsMapped(stateType); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private void assertAllEnumsMapped(Class stateType) { + if (!this.statusMappings.containsKey(null) && Enum.class.isAssignableFrom(stateType)) { + EnumSet elements = EnumSet.allOf((Class) stateType); + for (Object element : elements) { + Assert.isTrue(this.statusMappings.containsKey(element), + () -> "StatusMappings does not include " + element); + } + } + } + + @Override + protected void doHealthCheck(Builder builder) throws Exception { + AvailabilityState state = getState(this.applicationAvailability); + Status status = this.statusMappings.get(state); + if (status == null) { + status = this.statusMappings.get(null); + } + Assert.state(status != null, () -> "No mapping provided for " + state); + builder.status(status); + } + + /** + * Return the current availability state. Subclasses can override this method if a + * different retrieval mechanism is needed. + * @param applicationAvailability the application availability + * @return the current availability state + */ + protected AvailabilityState getState(ApplicationAvailability applicationAvailability) { + return applicationAvailability.getState(this.stateType); + } + + /** + * Callback used to add status mappings. + * + * @param the availability state type + */ + public interface StatusMappings { + + /** + * Add the status that should be used if no explicit mapping is defined. + * @param status the default status + */ + default void addDefaultStatus(Status status) { + add(null, status); + } + + /** + * Add a new status mapping . + * @param availabilityState the availability state + * @param status the mapped status + */ + void add(S availabilityState, Status status); + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/availability/LivenessStateHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/availability/LivenessStateHealthIndicator.java new file mode 100644 index 000000000000..8b631649ae15 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/availability/LivenessStateHealthIndicator.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.availability; + +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.actuate.health.Status; +import org.springframework.boot.availability.ApplicationAvailability; +import org.springframework.boot.availability.AvailabilityState; +import org.springframework.boot.availability.LivenessState; + +/** + * A {@link HealthIndicator} that checks the {@link LivenessState} of the application. + * + * @author Brian Clozel + * @since 2.3.0 + */ +public class LivenessStateHealthIndicator extends AvailabilityStateHealthIndicator { + + public LivenessStateHealthIndicator(ApplicationAvailability availability) { + super(availability, LivenessState.class, (statusMappings) -> { + statusMappings.add(LivenessState.CORRECT, Status.UP); + statusMappings.add(LivenessState.BROKEN, Status.DOWN); + }); + } + + @Override + protected AvailabilityState getState(ApplicationAvailability applicationAvailability) { + return applicationAvailability.getLivenessState(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/availability/ReadinessStateHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/availability/ReadinessStateHealthIndicator.java new file mode 100644 index 000000000000..0837ffe78769 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/availability/ReadinessStateHealthIndicator.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.availability; + +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.actuate.health.Status; +import org.springframework.boot.availability.ApplicationAvailability; +import org.springframework.boot.availability.AvailabilityState; +import org.springframework.boot.availability.ReadinessState; + +/** + * A {@link HealthIndicator} that checks the {@link ReadinessState} of the application. + * + * @author Brian Clozel + * @author Phillip Webb + * @since 2.3.0 + */ +public class ReadinessStateHealthIndicator extends AvailabilityStateHealthIndicator { + + public ReadinessStateHealthIndicator(ApplicationAvailability availability) { + super(availability, ReadinessState.class, (statusMappings) -> { + statusMappings.add(ReadinessState.ACCEPTING_TRAFFIC, Status.UP); + statusMappings.add(ReadinessState.REFUSING_TRAFFIC, Status.OUT_OF_SERVICE); + }); + } + + @Override + protected AvailabilityState getState(ApplicationAvailability applicationAvailability) { + return applicationAvailability.getReadinessState(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/availability/package-info.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/availability/package-info.java new file mode 100644 index 000000000000..76b194da698b --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/availability/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Actuator support for application availability concerns. + */ +package org.springframework.boot.actuate.availability; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraDriverHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraDriverHealthIndicator.java new file mode 100644 index 000000000000..ed42a0bbda6a --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraDriverHealthIndicator.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.cassandra; + +import java.util.Collection; +import java.util.Optional; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeState; + +import org.springframework.boot.actuate.health.AbstractHealthIndicator; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.actuate.health.Status; +import org.springframework.util.Assert; + +/** + * Simple implementation of a {@link HealthIndicator} returning status information for + * Cassandra data stores. + * + * @author Alexandre Dutra + * @author Tomasz Lelek + * @since 2.4.0 + */ +public class CassandraDriverHealthIndicator extends AbstractHealthIndicator { + + private final CqlSession session; + + /** + * Create a new {@link CassandraDriverHealthIndicator} instance. + * @param session the {@link CqlSession}. + */ + public CassandraDriverHealthIndicator(CqlSession session) { + super("Cassandra health check failed"); + Assert.notNull(session, "session must not be null"); + this.session = session; + } + + @Override + protected void doHealthCheck(Health.Builder builder) throws Exception { + Collection nodes = this.session.getMetadata().getNodes().values(); + Optional nodeUp = nodes.stream().filter((node) -> node.getState() == NodeState.UP).findAny(); + builder.status(nodeUp.isPresent() ? Status.UP : Status.DOWN); + nodeUp.map(Node::getCassandraVersion).ifPresent((version) -> builder.withDetail("version", version)); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraDriverReactiveHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraDriverReactiveHealthIndicator.java new file mode 100644 index 000000000000..646560d0788c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraDriverReactiveHealthIndicator.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.cassandra; + +import java.util.Collection; +import java.util.Optional; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeState; +import reactor.core.publisher.Mono; + +import org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.ReactiveHealthIndicator; +import org.springframework.boot.actuate.health.Status; +import org.springframework.util.Assert; + +/** + * Simple implementation of a {@link ReactiveHealthIndicator} returning status information + * for Cassandra data stores. + * + * @author Alexandre Dutra + * @author Tomasz Lelek + * @since 2.4.0 + */ +public class CassandraDriverReactiveHealthIndicator extends AbstractReactiveHealthIndicator { + + private final CqlSession session; + + /** + * Create a new {@link CassandraDriverReactiveHealthIndicator} instance. + * @param session the {@link CqlSession}. + */ + public CassandraDriverReactiveHealthIndicator(CqlSession session) { + super("Cassandra health check failed"); + Assert.notNull(session, "session must not be null"); + this.session = session; + } + + @Override + protected Mono doHealthCheck(Health.Builder builder) { + return Mono.fromSupplier(() -> { + Collection nodes = this.session.getMetadata().getNodes().values(); + Optional nodeUp = nodes.stream().filter((node) -> node.getState() == NodeState.UP).findAny(); + builder.status(nodeUp.isPresent() ? Status.UP : Status.DOWN); + nodeUp.map(Node::getCassandraVersion).ifPresent((version) -> builder.withDetail("version", version)); + return builder.build(); + }); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicator.java index bcaaae1bd867..dffc334aa78e 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,8 @@ package org.springframework.boot.actuate.cassandra; -import com.datastax.driver.core.ResultSet; -import com.datastax.driver.core.querybuilder.QueryBuilder; -import com.datastax.driver.core.querybuilder.Select; +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; import org.springframework.boot.actuate.health.AbstractHealthIndicator; import org.springframework.boot.actuate.health.Health; @@ -31,10 +30,17 @@ * Cassandra data stores. * * @author Julien Dubois + * @author Alexandre Dutra * @since 2.0.0 + * @deprecated since 2.4.0 for removal in 2.6.0 in favor of + * {@link CassandraDriverHealthIndicator} */ +@Deprecated public class CassandraHealthIndicator extends AbstractHealthIndicator { + private static final SimpleStatement SELECT = SimpleStatement + .newInstance("SELECT release_version FROM system.local").setConsistencyLevel(ConsistencyLevel.LOCAL_ONE); + private CassandraOperations cassandraOperations; public CassandraHealthIndicator() { @@ -53,13 +59,7 @@ public CassandraHealthIndicator(CassandraOperations cassandraOperations) { @Override protected void doHealthCheck(Health.Builder builder) throws Exception { - Select select = QueryBuilder.select("release_version").from("system", "local"); - ResultSet results = this.cassandraOperations.getCqlOperations().queryForResultSet(select); - if (results.isExhausted()) { - builder.up(); - return; - } - String version = results.one().getString(0); + String version = this.cassandraOperations.getCqlOperations().queryForObject(SELECT, String.class); builder.up().withDetail("version", version); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicator.java index 845f28698660..83dd9d944406 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,10 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.actuate.cassandra; -import com.datastax.driver.core.querybuilder.QueryBuilder; -import com.datastax.driver.core.querybuilder.Select; +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; import reactor.core.publisher.Mono; import org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator; @@ -30,9 +31,15 @@ * * @author Artsiom Yudovin * @since 2.1.0 + * @deprecated since 2.4.0 for removal in 2.6.0 in favor of + * {@link CassandraDriverHealthIndicator} */ +@Deprecated public class CassandraReactiveHealthIndicator extends AbstractReactiveHealthIndicator { + private static final SimpleStatement SELECT = SimpleStatement + .newInstance("SELECT release_version FROM system.local").setConsistencyLevel(ConsistencyLevel.LOCAL_ONE); + private final ReactiveCassandraOperations reactiveCassandraOperations; /** @@ -47,8 +54,7 @@ public CassandraReactiveHealthIndicator(ReactiveCassandraOperations reactiveCass @Override protected Mono doHealthCheck(Health.Builder builder) { - Select select = QueryBuilder.select("release_version").from("system", "local"); - return this.reactiveCassandraOperations.getReactiveCqlOperations().queryForObject(select, String.class) + return this.reactiveCassandraOperations.getReactiveCqlOperations().queryForObject(SELECT, String.class) .map((version) -> builder.up().withDetail("version", version).build()).single(); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java index f09686b29957..c79c58a06033 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,13 +17,16 @@ package org.springframework.boot.actuate.context.properties; import java.lang.reflect.Constructor; +import java.lang.reflect.Parameter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.function.Predicate; import java.util.stream.Collectors; import com.fasterxml.jackson.annotation.JsonInclude.Include; @@ -53,12 +56,21 @@ import org.springframework.boot.actuate.endpoint.Sanitizer; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.annotation.Selector; +import org.springframework.boot.context.properties.BoundConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationPropertiesBean; import org.springframework.boot.context.properties.ConstructorBinding; +import org.springframework.boot.context.properties.bind.Name; +import org.springframework.boot.context.properties.source.ConfigurationProperty; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.origin.Origin; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.KotlinDetector; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; import org.springframework.util.ClassUtils; @@ -71,12 +83,16 @@ *

* To protect sensitive information from being exposed, certain property values are masked * if their names end with a set of configurable values (default "password" and "secret"). - * Configure property names by using {@code endpoints.configprops.keys_to_sanitize} in - * your Spring Boot application configuration. + * Configure property names by using + * {@code management.endpoint.configprops.keys-to-sanitize} in your Spring Boot + * application configuration. * * @author Christian Dupuis * @author Dave Syer * @author Stephane Nicoll + * @author Madhura Bhave + * @author Andy Wilkinson + * @author Chris Bono * @since 2.0.0 */ @Endpoint(id = "configprops") @@ -99,50 +115,38 @@ public void setKeysToSanitize(String... keysToSanitize) { this.sanitizer.setKeysToSanitize(keysToSanitize); } + public void keysToSanitize(String... keysToSanitize) { + this.sanitizer.keysToSanitize(keysToSanitize); + } + @ReadOperation public ApplicationConfigurationProperties configurationProperties() { - return extract(this.context); + return extract(this.context, (bean) -> true); } - private ApplicationConfigurationProperties extract(ApplicationContext context) { - Map contextProperties = new HashMap<>(); + @ReadOperation + public ApplicationConfigurationProperties configurationPropertiesWithPrefix(@Selector String prefix) { + return extract(this.context, (bean) -> bean.getAnnotation().prefix().startsWith(prefix)); + } + + private ApplicationConfigurationProperties extract(ApplicationContext context, + Predicate beanFilterPredicate) { + ObjectMapper mapper = getObjectMapper(); + Map contexts = new HashMap<>(); ApplicationContext target = context; while (target != null) { - contextProperties.put(target.getId(), describeConfigurationProperties(target, getObjectMapper())); + contexts.put(target.getId(), describeBeans(mapper, target, beanFilterPredicate)); target = target.getParent(); } - return new ApplicationConfigurationProperties(contextProperties); - } - - private ContextConfigurationProperties describeConfigurationProperties(ApplicationContext context, - ObjectMapper mapper) { - Map beans = ConfigurationPropertiesBean.getAll(context); - Map descriptors = new HashMap<>(); - beans.forEach((beanName, bean) -> { - String prefix = bean.getAnnotation().prefix(); - descriptors.put(beanName, new ConfigurationPropertiesBeanDescriptor(prefix, - sanitize(prefix, safeSerialize(mapper, bean.getInstance(), prefix)))); - }); - return new ContextConfigurationProperties(descriptors, - (context.getParent() != null) ? context.getParent().getId() : null); + return new ApplicationConfigurationProperties(contexts); } - /** - * Cautiously serialize the bean to a map (returning a map with an error message - * instead of throwing an exception if there is a problem). - * @param mapper the object mapper - * @param bean the source bean - * @param prefix the prefix - * @return the serialized instance - */ - @SuppressWarnings("unchecked") - private Map safeSerialize(ObjectMapper mapper, Object bean, String prefix) { - try { - return new HashMap<>(mapper.convertValue(bean, Map.class)); - } - catch (Exception ex) { - return new HashMap<>(Collections.singletonMap("error", "Cannot serialize '" + prefix + "'")); + private ObjectMapper getObjectMapper() { + if (this.objectMapper == null) { + this.objectMapper = new ObjectMapper(); + configureObjectMapper(this.objectMapper); } + return this.objectMapper; } /** @@ -162,12 +166,10 @@ protected void configureObjectMapper(ObjectMapper mapper) { mapper.registerModule(new JavaTimeModule()); } - private ObjectMapper getObjectMapper() { - if (this.objectMapper == null) { - this.objectMapper = new ObjectMapper(); - configureObjectMapper(this.objectMapper); - } - return this.objectMapper; + private void applyConfigurationPropertiesFilter(ObjectMapper mapper) { + mapper.setAnnotationIntrospector(new ConfigurationPropertiesAnnotationIntrospector()); + mapper.setFilterProvider( + new SimpleFilterProvider().setDefaultFilter(new ConfigurationPropertiesPropertyFilter())); } /** @@ -180,10 +182,40 @@ private void applySerializationModifier(ObjectMapper mapper) { mapper.setSerializerFactory(factory); } - private void applyConfigurationPropertiesFilter(ObjectMapper mapper) { - mapper.setAnnotationIntrospector(new ConfigurationPropertiesAnnotationIntrospector()); - mapper.setFilterProvider( - new SimpleFilterProvider().setDefaultFilter(new ConfigurationPropertiesPropertyFilter())); + private ContextConfigurationProperties describeBeans(ObjectMapper mapper, ApplicationContext context, + Predicate beanFilterPredicate) { + Map beans = ConfigurationPropertiesBean.getAll(context); + Map descriptors = beans.values().stream() + .filter(beanFilterPredicate) + .collect(Collectors.toMap(ConfigurationPropertiesBean::getName, (bean) -> describeBean(mapper, bean))); + return new ContextConfigurationProperties(descriptors, + (context.getParent() != null) ? context.getParent().getId() : null); + } + + private ConfigurationPropertiesBeanDescriptor describeBean(ObjectMapper mapper, ConfigurationPropertiesBean bean) { + String prefix = bean.getAnnotation().prefix(); + Map serialized = safeSerialize(mapper, bean.getInstance(), prefix); + Map properties = sanitize(prefix, serialized); + Map inputs = getInputs(prefix, serialized); + return new ConfigurationPropertiesBeanDescriptor(prefix, properties, inputs); + } + + /** + * Cautiously serialize the bean to a map (returning a map with an error message + * instead of throwing an exception if there is a problem). + * @param mapper the object mapper + * @param bean the source bean + * @param prefix the prefix + * @return the serialized instance + */ + @SuppressWarnings({ "unchecked" }) + private Map safeSerialize(ObjectMapper mapper, Object bean, String prefix) { + try { + return new HashMap<>(mapper.convertValue(bean, Map.class)); + } + catch (Exception ex) { + return new HashMap<>(Collections.singletonMap("error", "Cannot serialize '" + prefix + "'")); + } } /** @@ -196,7 +228,7 @@ private void applyConfigurationPropertiesFilter(ObjectMapper mapper) { @SuppressWarnings("unchecked") private Map sanitize(String prefix, Map map) { map.forEach((key, value) -> { - String qualifiedKey = (prefix.isEmpty() ? prefix : prefix + ".") + key; + String qualifiedKey = getQualifiedKey(prefix, key); if (value instanceof Map) { map.put(key, sanitize(qualifiedKey, (Map) value)); } @@ -229,11 +261,87 @@ else if (item instanceof List) { return sanitized; } + @SuppressWarnings("unchecked") + private Map getInputs(String prefix, Map map) { + Map augmented = new LinkedHashMap<>(map); + map.forEach((key, value) -> { + String qualifiedKey = getQualifiedKey(prefix, key); + if (value instanceof Map) { + augmented.put(key, getInputs(qualifiedKey, (Map) value)); + } + else if (value instanceof List) { + augmented.put(key, getInputs(qualifiedKey, (List) value)); + } + else { + augmented.put(key, applyInput(qualifiedKey)); + } + }); + return augmented; + } + + @SuppressWarnings("unchecked") + private List getInputs(String prefix, List list) { + List augmented = new ArrayList<>(); + int index = 0; + for (Object item : list) { + String name = prefix + "[" + index++ + "]"; + if (item instanceof Map) { + augmented.add(getInputs(name, (Map) item)); + } + else if (item instanceof List) { + augmented.add(getInputs(name, (List) item)); + } + else { + augmented.add(applyInput(name)); + } + } + return augmented; + } + + private Map applyInput(String qualifiedKey) { + BoundConfigurationProperties bound = BoundConfigurationProperties.get(this.context); + if (bound == null) { + return Collections.emptyMap(); + } + ConfigurationPropertyName currentName = ConfigurationPropertyName.adapt(qualifiedKey, '.'); + ConfigurationProperty candidate = bound.get(currentName); + if (candidate == null && currentName.isLastElementIndexed()) { + candidate = bound.get(currentName.chop(currentName.getNumberOfElements() - 1)); + } + return (candidate != null) ? getInput(currentName.toString(), candidate) : Collections.emptyMap(); + } + + private Map getInput(String property, ConfigurationProperty candidate) { + Map input = new LinkedHashMap<>(); + Object value = stringifyIfNecessary(candidate.getValue()); + Origin origin = Origin.from(candidate); + List originParents = Origin.parentsFrom(candidate); + input.put("value", this.sanitizer.sanitize(property, value)); + input.put("origin", (origin != null) ? origin.toString() : "none"); + if (!originParents.isEmpty()) { + input.put("originParents", originParents.stream().map(Object::toString).toArray(String[]::new)); + } + return input; + } + + private Object stringifyIfNecessary(Object value) { + if (value == null || value.getClass().isPrimitive()) { + return value; + } + if (CharSequence.class.isAssignableFrom(value.getClass())) { + return value.toString(); + } + return "Complex property value " + value.getClass().getName(); + } + + private String getQualifiedKey(String prefix, String key) { + return (prefix.isEmpty() ? prefix : prefix + ".") + key; + } + /** * Extension to {@link JacksonAnnotationIntrospector} to suppress CGLIB generated bean * properties. */ - @SuppressWarnings("serial") private static class ConfigurationPropertiesAnnotationIntrospector extends JacksonAnnotationIntrospector { @Override @@ -306,11 +414,14 @@ public void serializeAsField(Object pojo, JsonGenerator jgen, SerializerProvider */ protected static class GenericSerializerModifier extends BeanSerializerModifier { + private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer(); + @Override public List changeProperties(SerializationConfig config, BeanDescription beanDesc, List beanProperties) { List result = new ArrayList<>(); - Constructor bindConstructor = findBindConstructor(beanDesc.getType().getRawClass()); + Class beanClass = beanDesc.getType().getRawClass(); + Constructor bindConstructor = findBindConstructor(ClassUtils.getUserClass(beanClass)); for (BeanPropertyWriter writer : beanProperties) { if (isCandidate(beanDesc, writer, bindConstructor)) { result.add(writer); @@ -319,15 +430,23 @@ public List changeProperties(SerializationConfig config, Bea return result; } - private boolean isCandidate(BeanDescription beanDesc, BeanPropertyWriter writer, - Constructor bindConstructor) { - if (bindConstructor != null) { - return Arrays.stream(bindConstructor.getParameters()) - .anyMatch((parameter) -> parameter.getName().equals(writer.getName())); - } - else { - return isReadable(beanDesc, writer); + private boolean isCandidate(BeanDescription beanDesc, BeanPropertyWriter writer, Constructor constructor) { + if (constructor != null) { + Parameter[] parameters = constructor.getParameters(); + String[] names = PARAMETER_NAME_DISCOVERER.getParameterNames(constructor); + if (names == null) { + names = new String[parameters.length]; + } + for (int i = 0; i < parameters.length; i++) { + String name = MergedAnnotations.from(parameters[i]).get(Name.class) + .getValue(MergedAnnotation.VALUE, String.class) + .orElse((names[i] != null) ? names[i] : parameters[i].getName()); + if (name.equals(writer.getName())) { + return true; + } + } } + return isReadable(beanDesc, writer); } private boolean isReadable(BeanDescription beanDesc, BeanPropertyWriter writer) { @@ -456,9 +575,13 @@ public static final class ConfigurationPropertiesBeanDescriptor { private final Map properties; - private ConfigurationPropertiesBeanDescriptor(String prefix, Map properties) { + private final Map inputs; + + private ConfigurationPropertiesBeanDescriptor(String prefix, Map properties, + Map inputs) { this.prefix = prefix; this.properties = properties; + this.inputs = inputs; } public String getPrefix() { @@ -469,6 +592,10 @@ public Map getProperties() { return this.properties; } + public Map getInputs() { + return this.inputs; + } + } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointWebExtension.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointWebExtension.java new file mode 100644 index 000000000000..8f3b5db2614b --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointWebExtension.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.context.properties; + +import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ApplicationConfigurationProperties; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.annotation.Selector; +import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; +import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension; + +/** + * {@link EndpointWebExtension @EndpointWebExtension} for the + * {@link ConfigurationPropertiesReportEndpoint}. + * + * @author Chris Bono + * @since 2.5.0 + */ +@EndpointWebExtension(endpoint = ConfigurationPropertiesReportEndpoint.class) +public class ConfigurationPropertiesReportEndpointWebExtension { + + private final ConfigurationPropertiesReportEndpoint delegate; + + public ConfigurationPropertiesReportEndpointWebExtension(ConfigurationPropertiesReportEndpoint delegate) { + this.delegate = delegate; + } + + @ReadOperation + public WebEndpointResponse configurationPropertiesWithPrefix( + @Selector String prefix) { + ApplicationConfigurationProperties configurationProperties = this.delegate + .configurationPropertiesWithPrefix(prefix); + boolean foundMatchingBeans = configurationProperties.getContexts().values().stream() + .anyMatch((context) -> !context.getBeans().isEmpty()); + return (foundMatchingBeans) ? new WebEndpointResponse<>(configurationProperties, WebEndpointResponse.STATUS_OK) + : new WebEndpointResponse<>(WebEndpointResponse.STATUS_NOT_FOUND); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/couchbase/CouchbaseHealth.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/couchbase/CouchbaseHealth.java index ea33c9122763..005921fd49e5 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/couchbase/CouchbaseHealth.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/couchbase/CouchbaseHealth.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,14 @@ package org.springframework.boot.actuate.couchbase; +import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; -import com.couchbase.client.core.message.internal.DiagnosticsReport; -import com.couchbase.client.core.message.internal.EndpointHealth; -import com.couchbase.client.core.state.LifecycleState; +import com.couchbase.client.core.diagnostics.ClusterState; +import com.couchbase.client.core.diagnostics.DiagnosticsResult; +import com.couchbase.client.core.diagnostics.EndpointDiagnostics; import org.springframework.boot.actuate.health.Health.Builder; @@ -33,35 +34,29 @@ */ class CouchbaseHealth { - private final DiagnosticsReport diagnostics; + private final DiagnosticsResult diagnostics; - CouchbaseHealth(DiagnosticsReport diagnostics) { + CouchbaseHealth(DiagnosticsResult diagnostics) { this.diagnostics = diagnostics; } void applyTo(Builder builder) { builder = isCouchbaseUp(this.diagnostics) ? builder.up() : builder.down(); builder.withDetail("sdk", this.diagnostics.sdk()); - builder.withDetail("endpoints", - this.diagnostics.endpoints().stream().map(this::describe).collect(Collectors.toList())); + builder.withDetail("endpoints", this.diagnostics.endpoints().values().stream().flatMap(Collection::stream) + .map(this::describe).collect(Collectors.toList())); } - private boolean isCouchbaseUp(DiagnosticsReport diagnostics) { - for (EndpointHealth health : diagnostics.endpoints()) { - LifecycleState state = health.state(); - if (state != LifecycleState.CONNECTED && state != LifecycleState.IDLE) { - return false; - } - } - return true; + private boolean isCouchbaseUp(DiagnosticsResult diagnostics) { + return diagnostics.state() == ClusterState.ONLINE; } - private Map describe(EndpointHealth endpointHealth) { + private Map describe(EndpointDiagnostics endpointHealth) { Map map = new HashMap<>(); map.put("id", endpointHealth.id()); map.put("lastActivity", endpointHealth.lastActivity()); - map.put("local", endpointHealth.local().toString()); - map.put("remote", endpointHealth.remote().toString()); + map.put("local", endpointHealth.local()); + map.put("remote", endpointHealth.remote()); map.put("state", endpointHealth.state()); map.put("type", endpointHealth.type()); return map; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/couchbase/CouchbaseHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/couchbase/CouchbaseHealthIndicator.java index 86e0c3f4e286..cefa92cc5424 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/couchbase/CouchbaseHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/couchbase/CouchbaseHealthIndicator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ package org.springframework.boot.actuate.couchbase; -import com.couchbase.client.core.message.internal.DiagnosticsReport; +import com.couchbase.client.core.diagnostics.DiagnosticsResult; import com.couchbase.client.java.Cluster; import org.springframework.boot.actuate.health.AbstractHealthIndicator; @@ -48,7 +48,7 @@ public CouchbaseHealthIndicator(Cluster cluster) { @Override protected void doHealthCheck(Health.Builder builder) throws Exception { - DiagnosticsReport diagnostics = this.cluster.diagnostics(); + DiagnosticsResult diagnostics = this.cluster.diagnostics(); new CouchbaseHealth(diagnostics).applyTo(builder); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/couchbase/CouchbaseReactiveHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/couchbase/CouchbaseReactiveHealthIndicator.java index 62f5cd1e11bd..b368f9306e1b 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/couchbase/CouchbaseReactiveHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/couchbase/CouchbaseReactiveHealthIndicator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.actuate.couchbase; -import com.couchbase.client.core.message.internal.DiagnosticsReport; +import com.couchbase.client.core.diagnostics.DiagnosticsResult; import com.couchbase.client.java.Cluster; import reactor.core.publisher.Mono; @@ -45,7 +46,7 @@ public CouchbaseReactiveHealthIndicator(Cluster cluster) { @Override protected Mono doHealthCheck(Health.Builder builder) { - DiagnosticsReport diagnostics = this.cluster.diagnostics(); + DiagnosticsResult diagnostics = this.cluster.diagnostics(); new CouchbaseHealth(diagnostics).applyTo(builder); return Mono.just(builder.build()); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchHealthIndicator.java deleted file mode 100644 index 93dac44a3a6a..000000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchHealthIndicator.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.elasticsearch; - -import java.util.List; - -import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest; -import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse; -import org.elasticsearch.client.Client; -import org.elasticsearch.client.Requests; - -import org.springframework.boot.actuate.health.AbstractHealthIndicator; -import org.springframework.boot.actuate.health.Health; -import org.springframework.boot.actuate.health.HealthIndicator; -import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; - -/** - * {@link HealthIndicator} for an Elasticsearch cluster. - * - * @author Binwei Yang - * @author Andy Wilkinson - * @since 2.0.0 - * @deprecated since 2.2.0 as {@literal org.elasticsearch.client:transport} has been - * deprecated upstream - */ -@Deprecated -public class ElasticsearchHealthIndicator extends AbstractHealthIndicator { - - private static final String[] ALL_INDICES = { "_all" }; - - private final Client client; - - private final String[] indices; - - private final long responseTimeout; - - /** - * Create a new {@link ElasticsearchHealthIndicator} instance. - * @param client the Elasticsearch client - * @param responseTimeout the request timeout in milliseconds - * @param indices the indices to check - */ - public ElasticsearchHealthIndicator(Client client, long responseTimeout, List indices) { - this(client, responseTimeout, (indices != null) ? StringUtils.toStringArray(indices) : null); - } - - /** - * Create a new {@link ElasticsearchHealthIndicator} instance. - * @param client the Elasticsearch client - * @param responseTimeout the request timeout in milliseconds - * @param indices the indices to check - */ - public ElasticsearchHealthIndicator(Client client, long responseTimeout, String... indices) { - super("Elasticsearch health check failed"); - this.client = client; - this.responseTimeout = responseTimeout; - this.indices = indices; - } - - @Override - protected void doHealthCheck(Health.Builder builder) throws Exception { - ClusterHealthRequest request = Requests - .clusterHealthRequest(ObjectUtils.isEmpty(this.indices) ? ALL_INDICES : this.indices); - ClusterHealthResponse response = this.client.admin().cluster().health(request).actionGet(this.responseTimeout); - switch (response.getStatus()) { - case GREEN: - case YELLOW: - builder.up(); - break; - case RED: - default: - builder.down(); - break; - } - builder.withDetail("clusterName", response.getClusterName()); - builder.withDetail("numberOfNodes", response.getNumberOfNodes()); - builder.withDetail("numberOfDataNodes", response.getNumberOfDataNodes()); - builder.withDetail("activePrimaryShards", response.getActivePrimaryShards()); - builder.withDetail("activeShards", response.getActiveShards()); - builder.withDetail("relocatingShards", response.getRelocatingShards()); - builder.withDetail("initializingShards", response.getInitializingShards()); - builder.withDetail("unassignedShards", response.getUnassignedShards()); - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchJestHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchJestHealthIndicator.java deleted file mode 100644 index 47c55c6d71ac..000000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchJestHealthIndicator.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.elasticsearch; - -import java.util.Map; - -import io.searchbox.client.JestClient; -import io.searchbox.client.JestResult; - -import org.springframework.boot.actuate.health.AbstractHealthIndicator; -import org.springframework.boot.actuate.health.Health; -import org.springframework.boot.actuate.health.HealthIndicator; -import org.springframework.boot.json.JsonParser; -import org.springframework.boot.json.JsonParserFactory; - -/** - * {@link HealthIndicator} for Elasticsearch using a {@link JestClient}. - * - * @author Stephane Nicoll - * @author Julian Devia Serna - * @author Brian Clozel - * @since 2.0.0 - */ -public class ElasticsearchJestHealthIndicator extends AbstractHealthIndicator { - - private final JestClient jestClient; - - private final JsonParser jsonParser = JsonParserFactory.getJsonParser(); - - public ElasticsearchJestHealthIndicator(JestClient jestClient) { - super("Elasticsearch health check failed"); - this.jestClient = jestClient; - } - - @Override - protected void doHealthCheck(Health.Builder builder) throws Exception { - JestResult healthResult = this.jestClient.execute(new io.searchbox.cluster.Health.Builder().build()); - if (healthResult.getResponseCode() != 200 || !healthResult.isSucceeded()) { - builder.down(); - builder.withDetail("statusCode", healthResult.getResponseCode()); - } - else { - Map response = this.jsonParser.parseMap(healthResult.getJsonString()); - String status = (String) response.get("status"); - if (status.equals(io.searchbox.cluster.Health.Status.RED.getKey())) { - builder.outOfService(); - } - else { - builder.up(); - } - builder.withDetails(response); - } - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchReactiveHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchReactiveHealthIndicator.java new file mode 100644 index 000000000000..5290bfea7d49 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchReactiveHealthIndicator.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.elasticsearch; + +import java.util.Map; + +import reactor.core.publisher.Mono; + +import org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.actuate.health.Status; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * {@link HealthIndicator} for an Elasticsearch cluster using a + * {@link ReactiveElasticsearchClient}. + * + * @author Brian Clozel + * @author Aleksander Lech + * @author Scott Frederick + * @since 2.3.2 + */ +public class ElasticsearchReactiveHealthIndicator extends AbstractReactiveHealthIndicator { + + private static final ParameterizedTypeReference> STRING_OBJECT_MAP = new ParameterizedTypeReference>() { + }; + + private static final String RED_STATUS = "red"; + + private final ReactiveElasticsearchClient client; + + public ElasticsearchReactiveHealthIndicator(ReactiveElasticsearchClient client) { + super("Elasticsearch health check failed"); + this.client = client; + } + + @Override + protected Mono doHealthCheck(Health.Builder builder) { + return this.client.execute((webClient) -> getHealth(builder, webClient)); + } + + private Mono getHealth(Health.Builder builder, WebClient webClient) { + return webClient.get().uri("/_cluster/health/").exchangeToMono((response) -> doHealthCheck(builder, response)); + } + + private Mono doHealthCheck(Health.Builder builder, ClientResponse response) { + if (response.statusCode().is2xxSuccessful()) { + return response.bodyToMono(STRING_OBJECT_MAP).map((body) -> getHealth(builder, body)); + } + builder.down(); + builder.withDetail("statusCode", response.rawStatusCode()); + builder.withDetail("reasonPhrase", response.statusCode().getReasonPhrase()); + return response.releaseBody().thenReturn(builder.build()); + } + + private Health getHealth(Health.Builder builder, Map body) { + String status = (String) body.get("status"); + builder.status(RED_STATUS.equals(status) ? Status.OUT_OF_SERVICE : Status.UP); + builder.withDetails(body); + return builder.build(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchRestHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchRestHealthIndicator.java index 79626acd3200..c6b7f69332cb 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchRestHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchRestHealthIndicator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import org.elasticsearch.client.Request; import org.elasticsearch.client.Response; import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestHighLevelClient; import org.springframework.boot.actuate.health.AbstractHealthIndicator; import org.springframework.boot.actuate.health.Health; @@ -49,6 +50,10 @@ public class ElasticsearchRestHealthIndicator extends AbstractHealthIndicator { private final JsonParser jsonParser; + public ElasticsearchRestHealthIndicator(RestHighLevelClient client) { + this(client.getLowLevelClient()); + } + public ElasticsearchRestHealthIndicator(RestClient client) { super("Elasticsearch health check failed"); this.client = client; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/ApiVersion.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/ApiVersion.java new file mode 100644 index 000000000000..d1f1e50bae91 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/ApiVersion.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.endpoint; + +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; + +/** + * API versions supported for the actuator API. This enum may be injected into actuator + * endpoints in order to return a response compatible with the requested version. + * + * @author Phillip Webb + * @since 2.4.0 + */ +public enum ApiVersion implements Producible { + + /** + * Version 2 (supported by Spring Boot 2.0+). + */ + V2("application/vnd.spring-boot.actuator.v2+json"), + + /** + * Version 3 (supported by Spring Boot 2.2+). + */ + V3("application/vnd.spring-boot.actuator.v3+json"); + + /** + * The latest API version. + */ + public static final ApiVersion LATEST = ApiVersion.V3; + + private final MimeType mimeType; + + ApiVersion(String mimeType) { + this.mimeType = MimeTypeUtils.parseMimeType(mimeType); + } + + @Override + public MimeType getProducedMimeType() { + return this.mimeType; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/EndpointFilter.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/EndpointFilter.java index 37a4d18f4ab7..986b0b76d7e9 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/EndpointFilter.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/EndpointFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,8 +19,8 @@ /** * Strategy class that can be used to filter {@link ExposableEndpoint endpoints}. * - * @author Phillip Webb * @param the endpoint type + * @author Phillip Webb * @since 2.0.0 */ @FunctionalInterface diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/EndpointId.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/EndpointId.java index cda5378f11e4..269ca381aca3 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/EndpointId.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/EndpointId.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,9 +28,9 @@ import org.springframework.util.Assert; /** - * An identifier for an actuator endpoint. Endpoint IDs may contain only letters, numbers - * {@code '.'} and {@code '-'}. They must begin with a lower-case letter. Case and syntax - * characters are ignored when comparing endpoint IDs. + * An identifier for an actuator endpoint. Endpoint IDs may contain only letters and + * numbers. They must begin with a lower-case letter. Case and syntax characters are + * ignored when comparing endpoint IDs. * * @author Phillip Webb * @since 2.0.6 @@ -41,9 +41,9 @@ public final class EndpointId { private static final Set loggedWarnings = new HashSet<>(); - private static final Pattern VALID_PATTERN = Pattern.compile("[a-zA-Z0-9\\.\\-]+"); + private static final Pattern VALID_PATTERN = Pattern.compile("[a-zA-Z0-9.-]+"); - private static final Pattern WARNING_PATTERN = Pattern.compile("[\\.\\-]+"); + private static final Pattern WARNING_PATTERN = Pattern.compile("[.-]+"); private static final String MIGRATE_LEGACY_NAMES_PROPERTY = "management.endpoints.migrate-legacy-ids"; @@ -131,7 +131,7 @@ public static EndpointId of(Environment environment, String value) { private static String migrateLegacyId(Environment environment, String value) { if (environment.getProperty(MIGRATE_LEGACY_NAMES_PROPERTY, Boolean.class, false)) { - return value.replace(".", ""); + return value.replaceAll("[-.]+", ""); } return value; } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/InvocationContext.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/InvocationContext.java index 8c8022541235..eefb3e3fdb48 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/InvocationContext.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/InvocationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,12 @@ package org.springframework.boot.actuate.endpoint; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.Map; -import org.springframework.boot.actuate.endpoint.http.ApiVersion; import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; import org.springframework.util.Assert; @@ -31,11 +34,9 @@ */ public class InvocationContext { - private final SecurityContext securityContext; - private final Map arguments; - private final ApiVersion apiVersion; + private final List argumentResolvers; /** * Creates a new context for an operation being invoked by the given @@ -54,30 +55,63 @@ public InvocationContext(SecurityContext securityContext, Map ar * @param securityContext the current security context. Never {@code null} * @param arguments the arguments available to the operation. Never {@code null} * @since 2.2.0 + * @deprecated since 2.5.0 for removal in 2.7.0 in favor of + * {@link #InvocationContext(SecurityContext, Map, OperationArgumentResolver[])} + */ + @Deprecated + public InvocationContext(org.springframework.boot.actuate.endpoint.http.ApiVersion apiVersion, + SecurityContext securityContext, Map arguments) { + this(securityContext, arguments, OperationArgumentResolver.of(ApiVersion.class, + () -> (apiVersion != null) ? ApiVersion.valueOf(apiVersion.name()) : null)); + } + + /** + * Creates a new context for an operation being invoked by the given + * {@code securityContext} with the given available {@code arguments}. + * @param securityContext the current security context. Never {@code null} + * @param arguments the arguments available to the operation. Never {@code null} + * @param argumentResolvers resolvers for additional arguments should be available to + * the operation. */ - public InvocationContext(ApiVersion apiVersion, SecurityContext securityContext, Map arguments) { + public InvocationContext(SecurityContext securityContext, Map arguments, + OperationArgumentResolver... argumentResolvers) { Assert.notNull(securityContext, "SecurityContext must not be null"); Assert.notNull(arguments, "Arguments must not be null"); - this.apiVersion = (apiVersion != null) ? apiVersion : ApiVersion.LATEST; - this.securityContext = securityContext; this.arguments = arguments; + this.argumentResolvers = new ArrayList<>(); + if (argumentResolvers != null) { + this.argumentResolvers.addAll(Arrays.asList(argumentResolvers)); + } + this.argumentResolvers.add(OperationArgumentResolver.of(SecurityContext.class, () -> securityContext)); + this.argumentResolvers.add(OperationArgumentResolver.of(Principal.class, securityContext::getPrincipal)); + this.argumentResolvers.add(OperationArgumentResolver.of(ApiVersion.class, () -> ApiVersion.LATEST)); } /** * Return the API version in use. * @return the apiVersion the API version * @since 2.2.0 + * @deprecated since 2.5.0 for removal in 2.7.0 in favor of + * {@link #resolveArgument(Class)} using + * {@link org.springframework.boot.actuate.endpoint.ApiVersion} */ - public ApiVersion getApiVersion() { - return this.apiVersion; + @Deprecated + public org.springframework.boot.actuate.endpoint.http.ApiVersion getApiVersion() { + ApiVersion version = resolveArgument(ApiVersion.class); + System.out.println(version); + return (version != null) ? org.springframework.boot.actuate.endpoint.http.ApiVersion.valueOf(version.name()) + : org.springframework.boot.actuate.endpoint.http.ApiVersion.LATEST; } /** * Return the security context to use for the invocation. * @return the security context + * @deprecated since 2.5.0 for removal in 2.7.0 in favor of + * {@link #resolveArgument(Class)} */ + @Deprecated public SecurityContext getSecurityContext() { - return this.securityContext; + return resolveArgument(SecurityContext.class); } /** @@ -88,4 +122,44 @@ public Map getArguments() { return this.arguments; } + /** + * Resolves an argument with the given {@code argumentType}. + * @param type of the argument + * @param argumentType type of the argument + * @return resolved argument of the required type or {@code null} + * @since 2.5.0 + * @see #canResolve(Class) + */ + public T resolveArgument(Class argumentType) { + for (OperationArgumentResolver argumentResolver : this.argumentResolvers) { + if (argumentResolver.canResolve(argumentType)) { + T result = argumentResolver.resolve(argumentType); + if (result != null) { + return result; + } + } + } + return null; + } + + /** + * Returns whether or not the context is capable of resolving an argument of the given + * {@code type}. Note that, even when {@code true} is returned, + * {@link #resolveArgument argument resolution} will return {@code null} if no + * argument of the required type is available. + * @param type argument type + * @return {@code true} if resolution of arguments of the given type is possible, + * otherwise {@code false}. + * @since 2.5.0 + * @see #resolveArgument(Class) + */ + public boolean canResolve(Class type) { + for (OperationArgumentResolver argumentResolver : this.argumentResolvers) { + if (argumentResolver.canResolve(type)) { + return true; + } + } + return false; + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/OperationArgumentResolver.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/OperationArgumentResolver.java new file mode 100644 index 000000000000..9ade2ec523d9 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/OperationArgumentResolver.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.endpoint; + +import java.util.function.Supplier; + +import org.springframework.util.Assert; + +/** + * Resolver for an argument of an {@link Operation}. + * + * @author Andy Wilkinson + * @since 2.5.0 + */ +public interface OperationArgumentResolver { + + /** + * Return whether an argument of the given {@code type} can be resolved. + * @param type argument type + * @return {@code true} if an argument of the required type can be resolved, otherwise + * {@code false} + */ + boolean canResolve(Class type); + + /** + * Resolves an argument of the given {@code type}. + * @param required type of the argument + * @param type argument type + * @return an argument of the required type, or {@code null} + */ + T resolve(Class type); + + /** + * Factory method that creates an {@link OperationArgumentResolver} for a specific + * type using a {@link Supplier}. + * @param the resolvable type + * @param type the resolvable type + * @param supplier the value supplier + * @return an {@link OperationArgumentResolver} instance + */ + static OperationArgumentResolver of(Class type, Supplier supplier) { + Assert.notNull(type, "Type must not be null"); + Assert.notNull(supplier, "Supplier must not be null"); + return new OperationArgumentResolver() { + + @Override + public boolean canResolve(Class actualType) { + return actualType.equals(type); + } + + @Override + @SuppressWarnings("unchecked") + public R resolve(Class argumentType) { + return (R) supplier.get(); + } + + }; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Producible.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Producible.java new file mode 100644 index 000000000000..32bdf8927f39 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Producible.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.endpoint; + +import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.annotation.WriteOperation; +import org.springframework.util.MimeType; + +/** + * Interface that can be implemented by any {@link Enum} that represents a finite set of + * producible mime-types. + *

+ * Can be used with {@link ReadOperation @ReadOperation}, + * {@link WriteOperation @ReadOperation} and {@link DeleteOperation @ReadOperation} + * annotations to quickly define a list of {@code produces} values. + *

+ * {@link Producible} types can also be injected into operations when the underlying + * technology supports content negotiation. For example, with web based endpoints, the + * value of the {@code Producible} enum is resolved using the {@code Accept} header of the + * request. When multiple values are equally acceptable, the value with the highest + * {@link Enum#ordinal() ordinal} is used. + * + * @param enum type that implements this interface + * @author Andy Wilkinson + * @since 2.5.0 + */ +public interface Producible & Producible> { + + /** + * Mime type that can be produced. + * @return the producible mime type + */ + MimeType getProducedMimeType(); + + /** + * Return if this enum value should be used as the default value when an accept header + * of */* is provided, or if the accept header is missing. Only one value + * can be marked as default. If no value is marked, then the value with the highest + * {@link Enum#ordinal() ordinal} is used as the default. + * @return if this value should be used as the default value + * @since 2.5.6 + */ + default boolean isDefault() { + return false; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/ProducibleOperationArgumentResolver.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/ProducibleOperationArgumentResolver.java new file mode 100644 index 000000000000..75a76f787aff --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/ProducibleOperationArgumentResolver.java @@ -0,0 +1,109 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.endpoint; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; + +/** + * An {@link OperationArgumentResolver} for {@link Producible producible enums}. + * + * @author Andy Wilkinson + * @author Phillip Webb + * @since 2.5.0 + */ +public class ProducibleOperationArgumentResolver implements OperationArgumentResolver { + + private final Supplier> accepts; + + /** + * Create a new {@link ProducibleOperationArgumentResolver} instance. + * @param accepts supplier that returns accepted mime types + */ + public ProducibleOperationArgumentResolver(Supplier> accepts) { + this.accepts = accepts; + } + + @Override + public boolean canResolve(Class type) { + return Producible.class.isAssignableFrom(type) && Enum.class.isAssignableFrom(type); + } + + @SuppressWarnings("unchecked") + @Override + public T resolve(Class type) { + return (T) resolveProducible((Class>>) type); + } + + private Enum> resolveProducible(Class>> type) { + List accepts = this.accepts.get(); + List>> values = getValues(type); + if (CollectionUtils.isEmpty(accepts)) { + return getDefaultValue(values); + } + Enum> result = null; + for (String accept : accepts) { + for (String mimeType : MimeTypeUtils.tokenize(accept)) { + result = mostRecent(result, forMimeType(values, MimeTypeUtils.parseMimeType(mimeType))); + } + } + return result; + } + + private Enum> mostRecent(Enum> existing, + Enum> candidate) { + int existingOrdinal = (existing != null) ? existing.ordinal() : -1; + int candidateOrdinal = (candidate != null) ? candidate.ordinal() : -1; + return (candidateOrdinal > existingOrdinal) ? candidate : existing; + } + + private Enum> forMimeType(List>> values, MimeType mimeType) { + if (mimeType.isWildcardType() && mimeType.isWildcardSubtype()) { + return getDefaultValue(values); + } + for (Enum> candidate : values) { + if (mimeType.isCompatibleWith(((Producible) candidate).getProducedMimeType())) { + return candidate; + } + } + return null; + } + + private List>> getValues(Class>> type) { + List>> values = Arrays.asList(type.getEnumConstants()); + Collections.reverse(values); + Assert.state(values.stream().filter(this::isDefault).count() <= 1, + "Multiple default values declared in " + type.getName()); + return values; + } + + private Enum> getDefaultValue(List>> values) { + return values.stream().filter(this::isDefault).findFirst().orElseGet(() -> values.get(0)); + } + + private boolean isDefault(Enum> value) { + return ((Producible) value).isDefault(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Sanitizer.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Sanitizer.java index 7cb865f0523b..20d83571d970 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Sanitizer.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Sanitizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,12 @@ package org.springframework.boot.actuate.endpoint; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -32,18 +36,32 @@ * @author Nicolas Lejeune * @author Stephane Nicoll * @author HaiTao Zhang + * @author Chris Bono + * @author David Good * @since 2.0.0 */ public class Sanitizer { private static final String[] REGEX_PARTS = { "*", "$", "^", "+" }; - private static final Pattern URI_USERINFO_PATTERN = Pattern.compile("[A-Za-z]+://.+:(.*)@.+$"); + private static final Set DEFAULT_KEYS_TO_SANITIZE = new LinkedHashSet<>( + Arrays.asList("password", "secret", "key", "token", ".*credentials.*", "vcap_services", + "^vcap\\.services.*$", "sun.java.command", "^spring[\\._]application[\\\\._]json$")); + + private static final Set URI_USERINFO_KEYS = new LinkedHashSet<>( + Arrays.asList("uri", "uris", "url", "urls", "address", "addresses")); + + private static final Pattern URI_USERINFO_PATTERN = Pattern + .compile("^\\[?[A-Za-z][A-Za-z0-9\\+\\.\\-]+://.+:(.*)@.+$"); private Pattern[] keysToSanitize; + static { + DEFAULT_KEYS_TO_SANITIZE.addAll(URI_USERINFO_KEYS); + } + public Sanitizer() { - this("password", "secret", "key", "token", ".*credentials.*", "vcap_services", "sun.java.command", "uri"); + this(DEFAULT_KEYS_TO_SANITIZE.toArray(new String[0])); } public Sanitizer(String... keysToSanitize) { @@ -51,8 +69,8 @@ public Sanitizer(String... keysToSanitize) { } /** - * Keys that should be sanitized. Keys can be simple strings that the property ends - * with or regular expressions. + * Set the keys that should be sanitized, overwriting any existing configuration. Keys + * can be simple strings that the property ends with or regular expressions. * @param keysToSanitize the keys to sanitize */ public void setKeysToSanitize(String... keysToSanitize) { @@ -63,6 +81,21 @@ public void setKeysToSanitize(String... keysToSanitize) { } } + /** + * Adds keys that should be sanitized. Keys can be simple strings that the property + * ends with or regular expressions. + * @param keysToSanitize the keys to sanitize + * @since 2.5.0 + */ + public void keysToSanitize(String... keysToSanitize) { + Assert.notNull(keysToSanitize, "KeysToSanitize must not be null"); + int existingKeys = this.keysToSanitize.length; + this.keysToSanitize = Arrays.copyOf(this.keysToSanitize, this.keysToSanitize.length + keysToSanitize.length); + for (int i = 0; i < keysToSanitize.length; i++) { + this.keysToSanitize[i + existingKeys] = getPattern(keysToSanitize[i]); + } + } + private Pattern getPattern(String value) { if (isRegex(value)) { return Pattern.compile(value, Pattern.CASE_INSENSITIVE); @@ -91,8 +124,8 @@ public Object sanitize(String key, Object value) { } for (Pattern pattern : this.keysToSanitize) { if (pattern.matcher(key).matches()) { - if (pattern.matcher("uri").matches()) { - return sanitizeUri(value); + if (keyIsUriWithUserInfo(pattern)) { + return sanitizeUris(value.toString()); } return "******"; } @@ -100,12 +133,24 @@ public Object sanitize(String key, Object value) { return value; } - private Object sanitizeUri(Object value) { - String uriString = value.toString(); - Matcher matcher = URI_USERINFO_PATTERN.matcher(uriString); + private boolean keyIsUriWithUserInfo(Pattern pattern) { + for (String uriKey : URI_USERINFO_KEYS) { + if (pattern.matcher(uriKey).matches()) { + return true; + } + } + return false; + } + + private Object sanitizeUris(String value) { + return Arrays.stream(value.split(",")).map(this::sanitizeUri).collect(Collectors.joining(",")); + } + + private String sanitizeUri(String value) { + Matcher matcher = URI_USERINFO_PATTERN.matcher(value); String password = matcher.matches() ? matcher.group(1) : null; if (password != null) { - return StringUtils.replace(uriString, ":" + password + "@", ":******@"); + return StringUtils.replace(value, ":" + password + "@", ":******@"); } return value; } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DeleteOperation.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DeleteOperation.java index e30afe791296..a05250458904 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DeleteOperation.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DeleteOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,8 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.boot.actuate.endpoint.Producible; + /** * Identifies a method on an {@link Endpoint @Endpoint} as being a delete operation. * @@ -40,4 +42,11 @@ */ String[] produces() default {}; + /** + * The media types of the result of the operation. + * @return the media types + */ + @SuppressWarnings("rawtypes") + Class producesFrom() default Producible.class; + } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationMethod.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationMethod.java index 89d216035fc6..4eb94e4066d0 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationMethod.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,11 +17,13 @@ package org.springframework.boot.actuate.endpoint.annotation; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import org.springframework.boot.actuate.endpoint.OperationType; +import org.springframework.boot.actuate.endpoint.Producible; import org.springframework.boot.actuate.endpoint.invoke.reflect.OperationMethod; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.util.Assert; @@ -40,8 +42,32 @@ public DiscoveredOperationMethod(Method method, OperationType operationType, AnnotationAttributes annotationAttributes) { super(method, operationType); Assert.notNull(annotationAttributes, "AnnotationAttributes must not be null"); - String[] produces = annotationAttributes.getStringArray("produces"); - this.producesMediaTypes = Collections.unmodifiableList(Arrays.asList(produces)); + List producesMediaTypes = new ArrayList<>(); + producesMediaTypes.addAll(Arrays.asList(annotationAttributes.getStringArray("produces"))); + producesMediaTypes.addAll(getProducesFromProducable(annotationAttributes)); + this.producesMediaTypes = Collections.unmodifiableList(producesMediaTypes); + } + + private & Producible> List getProducesFromProducable( + AnnotationAttributes annotationAttributes) { + Class type = getProducesFrom(annotationAttributes); + if (type == Producible.class) { + return Collections.emptyList(); + } + List produces = new ArrayList<>(); + for (Object value : type.getEnumConstants()) { + produces.add(((Producible) value).getProducedMimeType().toString()); + } + return produces; + } + + private Class getProducesFrom(AnnotationAttributes annotationAttributes) { + try { + return annotationAttributes.getClass("producesFrom"); + } + catch (IllegalArgumentException ex) { + return Producible.class; + } } public List getProducesMediaTypes() { diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/EndpointDiscoverer.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/EndpointDiscoverer.java index bbdf544f668e..0848a449d85c 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/EndpointDiscoverer.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/EndpointDiscoverer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,6 +49,7 @@ import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; import org.springframework.core.env.Environment; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -140,8 +141,9 @@ private Collection createEndpointBeans() { } private EndpointBean createEndpointBean(String beanName) { - Object bean = this.applicationContext.getBean(beanName); - return new EndpointBean(this.applicationContext.getEnvironment(), beanName, bean); + Class beanType = ClassUtils.getUserClass(this.applicationContext.getType(beanName, false)); + Supplier beanSupplier = () -> this.applicationContext.getBean(beanName); + return new EndpointBean(this.applicationContext.getEnvironment(), beanName, beanType, beanSupplier); } private void addExtensionBeans(Collection endpointBeans) { @@ -159,8 +161,9 @@ private void addExtensionBeans(Collection endpointBeans) { } private ExtensionBean createExtensionBean(String beanName) { - Object bean = this.applicationContext.getBean(beanName); - return new ExtensionBean(this.applicationContext.getEnvironment(), beanName, bean); + Class beanType = ClassUtils.getUserClass(this.applicationContext.getType(beanName)); + Supplier beanSupplier = () -> this.applicationContext.getBean(beanName); + return new ExtensionBean(this.applicationContext.getEnvironment(), beanName, beanType, beanSupplier); } private void addExtensionBean(EndpointBean endpointBean, ExtensionBean extensionBean) { @@ -233,31 +236,32 @@ private void assertNoDuplicateOperations(EndpointBean endpointBean, MultiValueMa } private boolean isExtensionExposed(EndpointBean endpointBean, ExtensionBean extensionBean) { - return isFilterMatch(extensionBean.getFilter(), endpointBean) && isExtensionExposed(extensionBean.getBean()); + return isFilterMatch(extensionBean.getFilter(), endpointBean) + && isExtensionTypeExposed(extensionBean.getBeanType()); } /** * Determine if an extension bean should be exposed. Subclasses can override this * method to provide additional logic. - * @param extensionBean the extension bean + * @param extensionBeanType the extension bean type * @return {@code true} if the extension is exposed */ - protected boolean isExtensionExposed(Object extensionBean) { + protected boolean isExtensionTypeExposed(Class extensionBeanType) { return true; } private boolean isEndpointExposed(EndpointBean endpointBean) { return isFilterMatch(endpointBean.getFilter(), endpointBean) && !isEndpointFiltered(endpointBean) - && isEndpointExposed(endpointBean.getBean()); + && isEndpointTypeExposed(endpointBean.getBeanType()); } /** * Determine if an endpoint bean should be exposed. Subclasses can override this * method to provide additional logic. - * @param endpointBean the endpoint bean + * @param beanType the endpoint bean type * @return {@code true} if the endpoint is exposed */ - protected boolean isEndpointExposed(Object endpointBean) { + protected boolean isEndpointTypeExposed(Class beanType) { return true; } @@ -272,7 +276,7 @@ private boolean isEndpointFiltered(EndpointBean endpointBean) { @SuppressWarnings("unchecked") private boolean isFilterMatch(Class filter, EndpointBean endpointBean) { - if (!isEndpointExposed(endpointBean.getBean())) { + if (!isEndpointTypeExposed(endpointBean.getBeanType())) { return false; } if (filter == null) { @@ -392,7 +396,9 @@ private static class EndpointBean { private final String beanName; - private final Object bean; + private final Class beanType; + + private final Supplier beanSupplier; private final EndpointId id; @@ -402,17 +408,18 @@ private static class EndpointBean { private Set extensions = new LinkedHashSet<>(); - EndpointBean(Environment environment, String beanName, Object bean) { - MergedAnnotation annotation = MergedAnnotations - .from(bean.getClass(), SearchStrategy.TYPE_HIERARCHY).get(Endpoint.class); + EndpointBean(Environment environment, String beanName, Class beanType, Supplier beanSupplier) { + MergedAnnotation annotation = MergedAnnotations.from(beanType, SearchStrategy.TYPE_HIERARCHY) + .get(Endpoint.class); String id = annotation.getString("id"); Assert.state(StringUtils.hasText(id), - () -> "No @Endpoint id attribute specified for " + bean.getClass().getName()); + () -> "No @Endpoint id attribute specified for " + beanType.getName()); this.beanName = beanName; - this.bean = bean; + this.beanType = beanType; + this.beanSupplier = beanSupplier; this.id = EndpointId.of(environment, id); this.enabledByDefault = annotation.getBoolean("enableByDefault"); - this.filter = getFilter(this.bean.getClass()); + this.filter = getFilter(beanType); } void addExtension(ExtensionBean extensionBean) { @@ -432,8 +439,12 @@ String getBeanName() { return this.beanName; } + Class getBeanType() { + return this.beanType; + } + Object getBean() { - return this.bean; + return this.beanSupplier.get(); } EndpointId getId() { @@ -457,17 +468,20 @@ private static class ExtensionBean { private final String beanName; - private final Object bean; + private final Class beanType; + + private final Supplier beanSupplier; private final EndpointId endpointId; private final Class filter; - ExtensionBean(Environment environment, String beanName, Object bean) { - this.bean = bean; + ExtensionBean(Environment environment, String beanName, Class beanType, Supplier beanSupplier) { this.beanName = beanName; + this.beanType = beanType; + this.beanSupplier = beanSupplier; MergedAnnotation extensionAnnotation = MergedAnnotations - .from(bean.getClass(), SearchStrategy.TYPE_HIERARCHY).get(EndpointExtension.class); + .from(beanType, SearchStrategy.TYPE_HIERARCHY).get(EndpointExtension.class); Class endpointType = extensionAnnotation.getClass("endpoint"); MergedAnnotation endpointAnnotation = MergedAnnotations .from(endpointType, SearchStrategy.TYPE_HIERARCHY).get(Endpoint.class); @@ -481,8 +495,12 @@ String getBeanName() { return this.beanName; } + Class getBeanType() { + return this.beanType; + } + Object getBean() { - return this.bean; + return this.beanSupplier.get(); } EndpointId getEndpointId() { diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/ReadOperation.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/ReadOperation.java index 4fa2c4730191..1997393171c6 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/ReadOperation.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/ReadOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,8 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.boot.actuate.endpoint.Producible; + /** * Identifies a method on an {@link Endpoint @Endpoint} as being a read operation. * @@ -39,4 +41,11 @@ */ String[] produces() default {}; + /** + * The media types of the result of the operation. + * @return the media types + */ + @SuppressWarnings("rawtypes") + Class producesFrom() default Producible.class; + } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/WriteOperation.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/WriteOperation.java index a12e4d9cfe8e..df39b75fec34 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/WriteOperation.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/WriteOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,8 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.boot.actuate.endpoint.Producible; + /** * Identifies a method on an {@link Endpoint @Endpoint} as being a write operation. * @@ -39,4 +41,11 @@ */ String[] produces() default {}; + /** + * The media types of the result of the operation. + * @return the media types + */ + @SuppressWarnings("rawtypes") + Class producesFrom() default Producible.class; + } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/http/ActuatorMediaType.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/http/ActuatorMediaType.java index 831f4e0c4b93..e31892f2a281 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/http/ActuatorMediaType.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/http/ActuatorMediaType.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,16 +22,12 @@ * @author Andy Wilkinson * @author Madhura Bhave * @since 2.0.0 + * @deprecated since 2.5.0 for removal in 2.7.0 in favor of + * {@link org.springframework.boot.actuate.endpoint.ApiVersion#getProducedMimeType()} */ +@Deprecated public final class ActuatorMediaType { - /** - * Constant for the Actuator V1 media type. - * @deprecated since 2.2.0 as the v1 format is no longer supported - */ - @Deprecated - public static final String V1_JSON = "application/vnd.spring-boot.actuator.v1+json"; - /** * Constant for the Actuator {@link ApiVersion#V2 v2} media type. */ diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/http/ApiVersion.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/http/ApiVersion.java index 6267ef8f8515..2b4a4d36083f 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/http/ApiVersion.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/http/ApiVersion.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.util.List; import java.util.Map; +import org.springframework.boot.actuate.endpoint.ProducibleOperationArgumentResolver; import org.springframework.util.CollectionUtils; import org.springframework.util.MimeTypeUtils; @@ -28,7 +29,10 @@ * * @author Phillip Webb * @since 2.2.0 + * @deprecated since 2.5.0 for removal in 2.7.0 in favor of + * {@link org.springframework.boot.actuate.endpoint.ApiVersion} */ +@Deprecated public enum ApiVersion { /** @@ -53,7 +57,10 @@ public enum ApiVersion { * will be deduced based on the {@code Accept} header. * @param headers the HTTP headers * @return the API version to use + * @deprecated since 2.5.0 for removal in 2.7.0 in favor of direct injection with + * resolution via the {@link ProducibleOperationArgumentResolver}. */ + @Deprecated public static ApiVersion fromHttpHeaders(Map> headers) { ApiVersion version = null; List accepts = headers.get("Accept"); @@ -70,7 +77,7 @@ public static ApiVersion fromHttpHeaders(Map> headers) { private static ApiVersion forType(String type) { if (type.startsWith(MEDIA_TYPE_PREFIX)) { type = type.substring(MEDIA_TYPE_PREFIX.length()); - int suffixIndex = type.indexOf("+"); + int suffixIndex = type.indexOf('+'); type = (suffixIndex != -1) ? type.substring(0, suffixIndex) : type; try { return valueOf(type.toUpperCase()); diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameter.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameter.java index 8b6cbcc07a8e..3aeac7d7613c 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameter.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,8 +18,14 @@ import java.lang.reflect.Parameter; +import javax.annotation.Nonnull; +import javax.annotation.meta.When; + import org.springframework.boot.actuate.endpoint.invoke.OperationParameter; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; /** @@ -29,6 +35,8 @@ */ class OperationMethodParameter implements OperationParameter { + private static final boolean jsr305Present = ClassUtils.isPresent("javax.annotation.Nonnull", null); + private final String name; private final Parameter parameter; @@ -55,7 +63,10 @@ public Class getType() { @Override public boolean isMandatory() { - return ObjectUtils.isEmpty(this.parameter.getAnnotationsByType(Nullable.class)); + if (!ObjectUtils.isEmpty(this.parameter.getAnnotationsByType(Nullable.class))) { + return false; + } + return (jsr305Present) ? new Jsr305().isMandatory(this.parameter) : true; } @Override @@ -63,4 +74,13 @@ public String toString() { return this.name + " of type " + this.parameter.getType().getName(); } + private static class Jsr305 { + + boolean isMandatory(Parameter parameter) { + MergedAnnotation annotation = MergedAnnotations.from(parameter).get(Nonnull.class); + return !annotation.isPresent() || annotation.getEnum("when", When.class) == When.ALWAYS; + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/ReflectiveOperationInvoker.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/ReflectiveOperationInvoker.java index 38137d9ee13a..5a277da4b7a1 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/ReflectiveOperationInvoker.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/ReflectiveOperationInvoker.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,13 +17,10 @@ package org.springframework.boot.actuate.endpoint.invoke.reflect; import java.lang.reflect.Method; -import java.security.Principal; import java.util.Set; import java.util.stream.Collectors; import org.springframework.boot.actuate.endpoint.InvocationContext; -import org.springframework.boot.actuate.endpoint.SecurityContext; -import org.springframework.boot.actuate.endpoint.http.ApiVersion; import org.springframework.boot.actuate.endpoint.invoke.MissingParametersException; import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; import org.springframework.boot.actuate.endpoint.invoke.OperationParameter; @@ -89,13 +86,7 @@ private boolean isMissing(InvocationContext context, OperationParameter paramete if (!parameter.isMandatory()) { return false; } - if (ApiVersion.class.equals(parameter.getType())) { - return false; - } - if (Principal.class.equals(parameter.getType())) { - return context.getSecurityContext().getPrincipal() == null; - } - if (SecurityContext.class.equals(parameter.getType())) { + if (context.canResolve(parameter.getType())) { return false; } return context.getArguments().get(parameter.getName()) == null; @@ -107,14 +98,9 @@ private Object[] resolveArguments(InvocationContext context) { } private Object resolveArgument(OperationParameter parameter, InvocationContext context) { - if (ApiVersion.class.equals(parameter.getType())) { - return context.getApiVersion(); - } - if (Principal.class.equals(parameter.getType())) { - return context.getSecurityContext().getPrincipal(); - } - if (SecurityContext.class.equals(parameter.getType())) { - return context.getSecurityContext(); + Object resolvedByType = context.resolveArgument(parameter.getType()); + if (resolvedByType != null) { + return resolvedByType; } Object value = context.getArguments().get(parameter.getName()); return this.parameterValueMapper.mapParameterValue(parameter, value); diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvoker.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvoker.java index e19d68763b1c..00f8ca160555 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvoker.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvoker.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,17 +16,22 @@ package org.springframework.boot.actuate.endpoint.invoker.cache; +import java.security.Principal; import java.time.Duration; +import java.util.Iterator; import java.util.Map; +import java.util.Map.Entry; import java.util.Objects; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.boot.actuate.endpoint.ApiVersion; import org.springframework.boot.actuate.endpoint.InvocationContext; import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ObjectUtils; /** @@ -42,11 +47,13 @@ public class CachingOperationInvoker implements OperationInvoker { private static final boolean IS_REACTOR_PRESENT = ClassUtils.isPresent("reactor.core.publisher.Mono", null); + private static final int CACHE_CLEANUP_THRESHOLD = 40; + private final OperationInvoker invoker; private final long timeToLive; - private volatile CachedResponse cachedResponse; + private final Map cachedResponses; /** * Create a new instance with the target {@link OperationInvoker} to use to compute @@ -58,6 +65,7 @@ public class CachingOperationInvoker implements OperationInvoker { Assert.isTrue(timeToLive > 0, "TimeToLive must be strictly positive"); this.invoker = invoker; this.timeToLive = timeToLive; + this.cachedResponses = new ConcurrentReferenceHashMap<>(); } /** @@ -74,19 +82,36 @@ public Object invoke(InvocationContext context) { return this.invoker.invoke(context); } long accessTime = System.currentTimeMillis(); - CachedResponse cached = this.cachedResponse; + if (this.cachedResponses.size() > CACHE_CLEANUP_THRESHOLD) { + cleanExpiredCachedResponses(accessTime); + } + ApiVersion contextApiVersion = context.resolveArgument(ApiVersion.class); + Principal principal = context.resolveArgument(Principal.class); + CacheKey cacheKey = new CacheKey(contextApiVersion, principal); + CachedResponse cached = this.cachedResponses.get(cacheKey); if (cached == null || cached.isStale(accessTime, this.timeToLive)) { Object response = this.invoker.invoke(context); cached = createCachedResponse(response, accessTime); - this.cachedResponse = cached; + this.cachedResponses.put(cacheKey, cached); } return cached.getResponse(); } - private boolean hasInput(InvocationContext context) { - if (context.getSecurityContext().getPrincipal() != null) { - return true; + private void cleanExpiredCachedResponses(long accessTime) { + try { + Iterator> iterator = this.cachedResponses.entrySet().iterator(); + while (iterator.hasNext()) { + Entry entry = iterator.next(); + if (entry.getValue().isStale(accessTime, this.timeToLive)) { + iterator.remove(); + } + } } + catch (Exception ex) { + } + } + + private boolean hasInput(InvocationContext context) { Map arguments = context.getArguments(); if (!ObjectUtils.isEmpty(arguments)) { return arguments.values().stream().anyMatch(Objects::nonNull); @@ -101,20 +126,6 @@ private CachedResponse createCachedResponse(Object response, long accessTime) { return new CachedResponse(response, accessTime); } - /** - * Apply caching configuration when appropriate to the given invoker. - * @param invoker the invoker to wrap - * @param timeToLive the maximum time in milliseconds that a response can be cached - * @return a caching version of the invoker or the original instance if caching is not - * required - */ - public static OperationInvoker apply(OperationInvoker invoker, long timeToLive) { - if (timeToLive > 0) { - return new CachingOperationInvoker(invoker, timeToLive); - } - return invoker; - } - /** * A cached response that encapsulates the response itself and the time at which it * was created. @@ -161,4 +172,39 @@ private static Object applyCaching(Object response, long timeToLive) { } + private static final class CacheKey { + + private final ApiVersion apiVersion; + + private final Principal principal; + + private CacheKey(ApiVersion apiVersion, Principal principal) { + this.principal = principal; + this.apiVersion = apiVersion; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + CacheKey other = (CacheKey) obj; + return this.apiVersion.equals(other.apiVersion) + && ObjectUtils.nullSafeEquals(this.principal, other.principal); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + this.apiVersion.hashCode(); + result = prime * result + ObjectUtils.nullSafeHashCode(this.principal); + return result; + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerAdvisor.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerAdvisor.java index b62320b89333..51cab56b7667 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerAdvisor.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerAdvisor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.util.function.Function; +import org.springframework.boot.actuate.endpoint.ApiVersion; import org.springframework.boot.actuate.endpoint.EndpointId; import org.springframework.boot.actuate.endpoint.OperationType; import org.springframework.boot.actuate.endpoint.SecurityContext; @@ -54,7 +55,8 @@ public OperationInvoker apply(EndpointId endpointId, OperationType operationType private boolean hasMandatoryParameter(OperationParameters parameters) { for (OperationParameter parameter : parameters) { - if (parameter.isMandatory() && !SecurityContext.class.isAssignableFrom(parameter.getType())) { + if (parameter.isMandatory() && !ApiVersion.class.isAssignableFrom(parameter.getType()) + && !SecurityContext.class.isAssignableFrom(parameter.getType())) { return true; } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBean.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBean.java index bce34a41f9ad..5bd2031ad756 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBean.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ import javax.management.MBeanInfo; import javax.management.ReflectionException; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException; @@ -172,6 +173,9 @@ public AttributeList setAttributes(AttributeList attributes) { private static class ReactiveHandler { static Object handle(Object result) { + if (result instanceof Flux) { + result = ((Flux) result).collectList(); + } if (result instanceof Mono) { return ((Mono) result).block(); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/EndpointMediaTypes.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/EndpointMediaTypes.java index 0a7e46d3a230..b46accae7538 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/EndpointMediaTypes.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/EndpointMediaTypes.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ import java.util.Collections; import java.util.List; -import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; +import org.springframework.boot.actuate.endpoint.ApiVersion; import org.springframework.util.Assert; /** @@ -31,13 +31,12 @@ */ public class EndpointMediaTypes { - private static final String JSON_MEDIA_TYPE = "application/json"; - /** * Default {@link EndpointMediaTypes} for this version of Spring Boot. */ - public static final EndpointMediaTypes DEFAULT = new EndpointMediaTypes(ActuatorMediaType.V3_JSON, - ActuatorMediaType.V2_JSON, JSON_MEDIA_TYPE); + public static final EndpointMediaTypes DEFAULT = new EndpointMediaTypes( + ApiVersion.V3.getProducedMimeType().toString(), ApiVersion.V2.getProducedMimeType().toString(), + "application/json"); private final List produced; @@ -51,7 +50,7 @@ public class EndpointMediaTypes { * @since 2.2.0 */ public EndpointMediaTypes(String... producedAndConsumed) { - this((producedAndConsumed != null) ? Arrays.asList(producedAndConsumed) : (List) null); + this((producedAndConsumed != null) ? Arrays.asList(producedAndConsumed) : null); } /** diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/PathMappedEndpoints.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/PathMappedEndpoints.java index 6a98053496b3..7da9f1214605 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/PathMappedEndpoints.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/PathMappedEndpoints.java @@ -65,13 +65,11 @@ public PathMappedEndpoints(String basePath, Collection> sup private Map getEndpoints(Collection> suppliers) { Map endpoints = new LinkedHashMap<>(); - suppliers.forEach((supplier) -> { - supplier.getEndpoints().forEach((endpoint) -> { - if (endpoint instanceof PathMappedEndpoint) { - endpoints.put(endpoint.getEndpointId(), (PathMappedEndpoint) endpoint); - } - }); - }); + suppliers.forEach((supplier) -> supplier.getEndpoints().forEach((endpoint) -> { + if (endpoint instanceof PathMappedEndpoint) { + endpoints.put(endpoint.getEndpointId(), (PathMappedEndpoint) endpoint); + } + })); return Collections.unmodifiableMap(endpoints); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/WebEndpointResponse.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/WebEndpointResponse.java index 0f390e23da79..a37c1c826314 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/WebEndpointResponse.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/WebEndpointResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,9 @@ package org.springframework.boot.actuate.endpoint.web; +import org.springframework.boot.actuate.endpoint.Producible; import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension; +import org.springframework.util.MimeType; /** * A {@code WebEndpointResponse} can be returned by an operation on a @@ -70,6 +72,8 @@ public final class WebEndpointResponse { private final int status; + private final MimeType contentType; + /** * Creates a new {@code WebEndpointResponse} with no body and a 200 (OK) status. */ @@ -87,7 +91,7 @@ public WebEndpointResponse(int status) { } /** - * Creates a new {@code WebEndpointResponse} with then given body and a 200 (OK) + * Creates a new {@code WebEndpointResponse} with the given body and a 200 (OK) * status. * @param body the body */ @@ -96,13 +100,55 @@ public WebEndpointResponse(T body) { } /** - * Creates a new {@code WebEndpointResponse} with then given body and status. + * Creates a new {@code WebEndpointResponse} with the given body and content type and + * a 200 (OK) status. + * @param body the body + * @param producible the producible providing the content type + * @since 2.5.0 + */ + public WebEndpointResponse(T body, Producible producible) { + this(body, STATUS_OK, producible.getProducedMimeType()); + } + + /** + * Creates a new {@code WebEndpointResponse} with the given body and content type and + * a 200 (OK) status. + * @param body the body + * @param contentType the content type of the response + * @since 2.5.0 + */ + public WebEndpointResponse(T body, MimeType contentType) { + this(body, STATUS_OK, contentType); + } + + /** + * Creates a new {@code WebEndpointResponse} with the given body and status. * @param body the body * @param status the HTTP status */ public WebEndpointResponse(T body, int status) { + this(body, status, null); + } + + /** + * Creates a new {@code WebEndpointResponse} with the given body and status. + * @param body the body + * @param status the HTTP status + * @param contentType the content type of the response + * @since 2.5.0 + */ + public WebEndpointResponse(T body, int status, MimeType contentType) { this.body = body; this.status = status; + this.contentType = contentType; + } + + /** + * Returns the content type of the response. + * @return the content type; + */ + public MimeType getContentType() { + return this.contentType; } /** diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/ControllerEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/ControllerEndpoint.java index fbee3d99e081..dee548086ef9 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/ControllerEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/ControllerEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,8 +35,8 @@ /** * Identifies a type as being an endpoint that is only exposed over Spring MVC or Spring * WebFlux. Mapped methods must be annotated with {@link GetMapping @GetMapping}, - * {@link PostMapping @PostMapping}, {@link DeleteMapping @DeleteMapping}, etc annotations - * rather than {@link ReadOperation @ReadOperation}, + * {@link PostMapping @PostMapping}, {@link DeleteMapping @DeleteMapping}, etc. + * annotations rather than {@link ReadOperation @ReadOperation}, * {@link WriteOperation @WriteOperation}, {@link DeleteOperation @DeleteOperation}. *

* This annotation can be used when deeper Spring integration is required, but at the diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/ControllerEndpointDiscoverer.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/ControllerEndpointDiscoverer.java index 45c672a680b8..1b991c71ffa6 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/ControllerEndpointDiscoverer.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/ControllerEndpointDiscoverer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ import org.springframework.boot.actuate.endpoint.web.PathMapper; import org.springframework.context.ApplicationContext; import org.springframework.core.annotation.MergedAnnotations; -import org.springframework.util.ClassUtils; +import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; /** * {@link EndpointDiscoverer} for {@link ExposableControllerEndpoint controller @@ -57,9 +57,8 @@ public ControllerEndpointDiscoverer(ApplicationContext applicationContext, List< } @Override - protected boolean isEndpointExposed(Object endpointBean) { - Class type = ClassUtils.getUserClass(endpointBean.getClass()); - MergedAnnotations annotations = MergedAnnotations.from(type); + protected boolean isEndpointTypeExposed(Class beanType) { + MergedAnnotations annotations = MergedAnnotations.from(beanType, SearchStrategy.SUPERCLASS); return annotations.isPresent(ControllerEndpoint.class) || annotations.isPresent(RestControllerEndpoint.class); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/RestControllerEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/RestControllerEndpoint.java index 15f5fee332dc..0aec498c5436 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/RestControllerEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/RestControllerEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,8 +36,8 @@ /** * Identifies a type as being a REST endpoint that is only exposed over Spring MVC or * Spring WebFlux. Mapped methods must be annotated with {@link GetMapping @GetMapping}, - * {@link PostMapping @PostMapping}, {@link DeleteMapping @DeleteMapping}, etc annotations - * rather than {@link ReadOperation @ReadOperation}, + * {@link PostMapping @PostMapping}, {@link DeleteMapping @DeleteMapping}, etc. + * annotations rather than {@link ReadOperation @ReadOperation}, * {@link WriteOperation @WriteOperation}, {@link DeleteOperation @DeleteOperation}. *

* This annotation can be used when deeper Spring integration is required, but at the diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/ServletEndpointDiscoverer.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/ServletEndpointDiscoverer.java index 5c7dc7bd67a0..0349c4a3d193 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/ServletEndpointDiscoverer.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/annotation/ServletEndpointDiscoverer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,7 @@ import org.springframework.boot.actuate.endpoint.web.PathMapper; import org.springframework.context.ApplicationContext; import org.springframework.core.annotation.MergedAnnotations; -import org.springframework.util.ClassUtils; +import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; /** * {@link EndpointDiscoverer} for {@link ExposableServletEndpoint servlet endpoints}. @@ -57,9 +57,8 @@ public ServletEndpointDiscoverer(ApplicationContext applicationContext, List type = ClassUtils.getUserClass(endpointBean.getClass()); - return MergedAnnotations.from(type).isPresent(ServletEndpoint.class); + protected boolean isEndpointTypeExposed(Class beanType) { + return MergedAnnotations.from(beanType, SearchStrategy.SUPERCLASS).isPresent(ServletEndpoint.class); } @Override diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyEndpointResourceFactory.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyEndpointResourceFactory.java index 6c8690758104..feb08cffb6ea 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyEndpointResourceFactory.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/jersey/JerseyEndpointResourceFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,12 +38,13 @@ import org.glassfish.jersey.server.ContainerRequest; import org.glassfish.jersey.server.model.Resource; import org.glassfish.jersey.server.model.Resource.Builder; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException; import org.springframework.boot.actuate.endpoint.InvocationContext; +import org.springframework.boot.actuate.endpoint.ProducibleOperationArgumentResolver; import org.springframework.boot.actuate.endpoint.SecurityContext; -import org.springframework.boot.actuate.endpoint.http.ApiVersion; import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; @@ -128,6 +129,7 @@ private static final class OperationInflector implements Inflector> converters = new ArrayList<>(); converters.add(new ResourceBodyConverter()); if (ClassUtils.isPresent("reactor.core.publisher.Mono", OperationInflector.class.getClassLoader())) { + converters.add(new FluxBodyConverter()); converters.add(new MonoBodyConverter()); } BODY_CONVERTERS = Collections.unmodifiableList(converters); @@ -151,9 +153,9 @@ public Response apply(ContainerRequestContext data) { arguments.putAll(extractPathParameters(data)); arguments.putAll(extractQueryParameters(data)); try { - ApiVersion apiVersion = ApiVersion.fromHttpHeaders(data.getHeaders()); JerseySecurityContext securityContext = new JerseySecurityContext(data.getSecurityContext()); - InvocationContext invocationContext = new InvocationContext(apiVersion, securityContext, arguments); + InvocationContext invocationContext = new InvocationContext(securityContext, arguments, + new ProducibleOperationArgumentResolver(() -> data.getHeaders().get("Accept"))); Object response = this.operation.invoke(invocationContext); return convertToJaxRsResponse(response, data.getRequest().getMethod()); } @@ -164,11 +166,8 @@ public Response apply(ContainerRequestContext data) { @SuppressWarnings("unchecked") private Map extractBodyArguments(ContainerRequestContext data) { - Map entity = ((ContainerRequest) data).readEntity(Map.class); - if (entity == null) { - return Collections.emptyMap(); - } - return (Map) entity; + Map entity = ((ContainerRequest) data).readEntity(Map.class); + return (entity != null) ? entity : Collections.emptyMap(); } private Map extractPathParameters(ContainerRequestContext requestContext) { @@ -218,6 +217,7 @@ private Response convertToJaxRsResponse(Object response, String httpMethod) { } WebEndpointResponse webEndpointResponse = (WebEndpointResponse) response; return Response.status(webEndpointResponse.getStatus()) + .header("Content-Type", webEndpointResponse.getContentType()) .entity(convertIfNecessary(webEndpointResponse.getBody())).build(); } catch (IOException ex) { @@ -270,6 +270,21 @@ public Object apply(Object body) { } + /** + * Body converter from {@link Flux} to {@link Flux#collectList Mono<List>}. + */ + private static final class FluxBodyConverter implements Function { + + @Override + public Object apply(Object body) { + if (body instanceof Flux) { + return ((Flux) body).collectList(); + } + return body; + } + + } + /** * {@link Inflector} to for endpoint links. */ diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AbstractWebFluxEndpointHandlerMapping.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AbstractWebFluxEndpointHandlerMapping.java index a9aaff3664f1..3ae2ea0fb156 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AbstractWebFluxEndpointHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/AbstractWebFluxEndpointHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,14 +26,15 @@ import java.util.function.Supplier; import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException; import org.springframework.boot.actuate.endpoint.InvocationContext; import org.springframework.boot.actuate.endpoint.OperationType; +import org.springframework.boot.actuate.endpoint.ProducibleOperationArgumentResolver; import org.springframework.boot.actuate.endpoint.SecurityContext; -import org.springframework.boot.actuate.endpoint.http.ApiVersion; import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; @@ -43,6 +44,7 @@ import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.AccessDecisionVoter; import org.springframework.security.access.SecurityConfig; @@ -59,15 +61,10 @@ import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerMapping; -import org.springframework.web.reactive.result.condition.ConsumesRequestCondition; -import org.springframework.web.reactive.result.condition.PatternsRequestCondition; -import org.springframework.web.reactive.result.condition.ProducesRequestCondition; -import org.springframework.web.reactive.result.condition.RequestMethodsRequestCondition; import org.springframework.web.reactive.result.method.RequestMappingInfo; import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping; import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ServerWebExchange; -import org.springframework.web.util.pattern.PathPatternParser; /** * A custom {@link HandlerMapping} that makes web endpoints available over HTTP using @@ -81,8 +78,6 @@ */ public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappingInfoHandlerMapping { - private static final PathPatternParser pathPatternParser = new PathPatternParser(); - private final EndpointMapping endpointMapping; private final Collection endpoints; @@ -165,24 +160,18 @@ protected ReactiveWebOperation wrapReactiveWebOperation(ExposableWebEndpoint end private RequestMappingInfo createRequestMappingInfo(WebOperation operation) { WebOperationRequestPredicate predicate = operation.getRequestPredicate(); - PatternsRequestCondition patterns = new PatternsRequestCondition( - pathPatternParser.parse(this.endpointMapping.createSubPath(predicate.getPath()))); - RequestMethodsRequestCondition methods = new RequestMethodsRequestCondition( - RequestMethod.valueOf(predicate.getHttpMethod().name())); - ConsumesRequestCondition consumes = new ConsumesRequestCondition( - StringUtils.toStringArray(predicate.getConsumes())); - ProducesRequestCondition produces = new ProducesRequestCondition( - StringUtils.toStringArray(predicate.getProduces())); - return new RequestMappingInfo(null, patterns, methods, null, null, consumes, produces, null); + String path = this.endpointMapping.createSubPath(predicate.getPath()); + RequestMethod method = RequestMethod.valueOf(predicate.getHttpMethod().name()); + String[] consumes = StringUtils.toStringArray(predicate.getConsumes()); + String[] produces = StringUtils.toStringArray(predicate.getProduces()); + return RequestMappingInfo.paths(path).methods(method).consumes(consumes).produces(produces).build(); } private void registerLinksMapping() { - PatternsRequestCondition patterns = new PatternsRequestCondition( - pathPatternParser.parse(this.endpointMapping.getPath())); - RequestMethodsRequestCondition methods = new RequestMethodsRequestCondition(RequestMethod.GET); - ProducesRequestCondition produces = new ProducesRequestCondition( - StringUtils.toStringArray(this.endpointMediaTypes.getProduced())); - RequestMappingInfo mapping = new RequestMappingInfo(patterns, methods, null, null, null, produces, null); + String path = this.endpointMapping.getPath(); + String[] produces = StringUtils.toStringArray(this.endpointMediaTypes.getProduced()); + RequestMappingInfo mapping = RequestMappingInfo.paths(path).methods(RequestMethod.GET).produces(produces) + .build(); LinksHandler linksHandler = getLinksHandler(); registerMapping(mapping, linksHandler, ReflectionUtils.findMethod(linksHandler.getClass(), "links", ServerWebExchange.class)); @@ -310,7 +299,6 @@ Mono emptySecurityContext() { @Override public Mono> handle(ServerWebExchange exchange, Map body) { - ApiVersion apiVersion = ApiVersion.fromHttpHeaders(exchange.getRequest().getHeaders()); Map arguments = getArguments(exchange, body); String matchAllRemainingPathSegmentsVariable = this.operation.getRequestPredicate() .getMatchAllRemainingPathSegmentsVariable(); @@ -319,7 +307,9 @@ public Mono> handle(ServerWebExchange exchange, Map new InvocationContext(apiVersion, securityContext, arguments)) + .map((securityContext) -> new InvocationContext(securityContext, arguments, + new ProducibleOperationArgumentResolver( + () -> exchange.getRequest().getHeaders().get("Accept")))) .flatMap((invocationContext) -> handleResult((Publisher) this.invoker.invoke(invocationContext), exchange.getRequest().getMethod())); } @@ -349,6 +339,9 @@ private Map getTemplateVariables(ServerWebExchange exchange) { } private Mono> handleResult(Publisher result, HttpMethod httpMethod) { + if (result instanceof Flux) { + result = ((Flux) result).collectList(); + } return Mono.from(result).map(this::toResponseEntity) .onErrorMap(InvalidEndpointRequestException.class, (ex) -> new ResponseStatusException(HttpStatus.BAD_REQUEST, ex.getReason())) @@ -361,8 +354,10 @@ private ResponseEntity toResponseEntity(Object response) { return new ResponseEntity<>(response, HttpStatus.OK); } WebEndpointResponse webEndpointResponse = (WebEndpointResponse) response; - return new ResponseEntity<>(webEndpointResponse.getBody(), - HttpStatus.valueOf(webEndpointResponse.getStatus())); + MediaType contentType = (webEndpointResponse.getContentType() != null) + ? new MediaType(webEndpointResponse.getContentType()) : null; + return ResponseEntity.status(webEndpointResponse.getStatus()).contentType(contentType) + .body(webEndpointResponse.getBody()); } @Override @@ -375,7 +370,7 @@ public String toString() { /** * Handler for a {@link ReactiveWebOperation}. */ - private final class WriteOperationHandler { + private static final class WriteOperationHandler { private final ReactiveWebOperation operation; @@ -394,7 +389,7 @@ Publisher> handle(ServerWebExchange exchange, /** * Handler for a {@link ReactiveWebOperation}. */ - private final class ReadOperationHandler { + private static final class ReadOperationHandler { private final ReactiveWebOperation operation; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/ControllerEndpointHandlerMapping.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/ControllerEndpointHandlerMapping.java index 93d84098c6f2..ef7583bc4130 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/ControllerEndpointHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/reactive/ControllerEndpointHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,6 @@ import org.springframework.util.Assert; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.reactive.HandlerMapping; -import org.springframework.web.reactive.result.condition.PatternsRequestCondition; import org.springframework.web.reactive.result.method.RequestMappingInfo; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.util.pattern.PathPattern; @@ -92,20 +91,13 @@ private RequestMappingInfo withEndpointMappedPatterns(ExposableControllerEndpoin if (patterns.isEmpty()) { patterns = Collections.singleton(getPathPatternParser().parse("")); } - PathPattern[] endpointMappedPatterns = patterns.stream() - .map((pattern) -> getEndpointMappedPattern(endpoint, pattern)).toArray(PathPattern[]::new); - return withNewPatterns(mapping, endpointMappedPatterns); + String[] endpointMappedPatterns = patterns.stream() + .map((pattern) -> getEndpointMappedPattern(endpoint, pattern)).toArray(String[]::new); + return mapping.mutate().paths(endpointMappedPatterns).build(); } - private PathPattern getEndpointMappedPattern(ExposableControllerEndpoint endpoint, PathPattern pattern) { - return getPathPatternParser().parse(this.endpointMapping.createSubPath(endpoint.getRootPath() + pattern)); - } - - private RequestMappingInfo withNewPatterns(RequestMappingInfo mapping, PathPattern[] patterns) { - PatternsRequestCondition patternsCondition = new PatternsRequestCondition(patterns); - return new RequestMappingInfo(patternsCondition, mapping.getMethodsCondition(), mapping.getParamsCondition(), - mapping.getHeadersCondition(), mapping.getConsumesCondition(), mapping.getProducesCondition(), - mapping.getCustomCondition()); + private String getEndpointMappedPattern(ExposableControllerEndpoint endpoint, PathPattern pattern) { + return this.endpointMapping.createSubPath(endpoint.getRootPath() + pattern); } @Override diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java index 0b7f9970516e..c7482f976e3e 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/AbstractWebMvcEndpointHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,21 +19,26 @@ import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; import java.security.Principal; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Function; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import reactor.core.publisher.Flux; + import org.springframework.beans.factory.InitializingBean; import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException; import org.springframework.boot.actuate.endpoint.InvocationContext; +import org.springframework.boot.actuate.endpoint.ProducibleOperationArgumentResolver; import org.springframework.boot.actuate.endpoint.SecurityContext; -import org.springframework.boot.actuate.endpoint.http.ApiVersion; import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; @@ -44,27 +49,26 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.util.AntPathMatcher; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; -import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.method.HandlerMethod; +import org.springframework.web.server.ResponseStatusException; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.handler.MatchableHandlerMapping; import org.springframework.web.servlet.handler.RequestMatchResult; -import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition; -import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; -import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition; -import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping; +import org.springframework.web.util.UrlPathHelper; /** * A custom {@link HandlerMapping} that makes {@link ExposableWebEndpoint web endpoints} @@ -158,9 +162,9 @@ public RequestMatchResult match(HttpServletRequest request, String pattern) { return new RequestMatchResult(patterns.iterator().next(), lookupPath, getPathMatcher()); } + @SuppressWarnings("deprecation") private static RequestMappingInfo.BuilderConfiguration getBuilderConfig() { RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration(); - config.setUrlPathHelper(null); config.setPathMatcher(null); config.setSuffixPatternMatch(false); config.setTrailingSlashMatch(true); @@ -194,33 +198,21 @@ protected ServletWebOperation wrapServletWebOperation(ExposableWebEndpoint endpo } private RequestMappingInfo createRequestMappingInfo(WebOperationRequestPredicate predicate, String path) { - PatternsRequestCondition patterns = patternsRequestConditionForPattern(path); - RequestMethodsRequestCondition methods = new RequestMethodsRequestCondition( - RequestMethod.valueOf(predicate.getHttpMethod().name())); - ConsumesRequestCondition consumes = new ConsumesRequestCondition( - StringUtils.toStringArray(predicate.getConsumes())); - ProducesRequestCondition produces = new ProducesRequestCondition( - StringUtils.toStringArray(predicate.getProduces())); - return new RequestMappingInfo(null, patterns, methods, null, null, consumes, produces, null); + return RequestMappingInfo.paths(this.endpointMapping.createSubPath(path)) + .methods(RequestMethod.valueOf(predicate.getHttpMethod().name())) + .consumes(predicate.getConsumes().toArray(new String[0])) + .produces(predicate.getProduces().toArray(new String[0])).build(); } private void registerLinksMapping() { - PatternsRequestCondition patterns = patternsRequestConditionForPattern(""); - RequestMethodsRequestCondition methods = new RequestMethodsRequestCondition(RequestMethod.GET); - ProducesRequestCondition produces = new ProducesRequestCondition(this.endpointMediaTypes.getProduced() - .toArray(StringUtils.toStringArray(this.endpointMediaTypes.getProduced()))); - RequestMappingInfo mapping = new RequestMappingInfo(patterns, methods, null, null, null, produces, null); + RequestMappingInfo mapping = RequestMappingInfo.paths(this.endpointMapping.createSubPath("")) + .methods(RequestMethod.GET).produces(this.endpointMediaTypes.getProduced().toArray(new String[0])) + .options(builderConfig).build(); LinksHandler linksHandler = getLinksHandler(); registerMapping(mapping, linksHandler, ReflectionUtils.findMethod(linksHandler.getClass(), "links", HttpServletRequest.class, HttpServletResponse.class)); } - private PatternsRequestCondition patternsRequestConditionForPattern(String path) { - String[] patterns = new String[] { this.endpointMapping.createSubPath(path) }; - return new PatternsRequestCondition(patterns, builderConfig.getUrlPathHelper(), builderConfig.getPathMatcher(), - builderConfig.useSuffixPatternMatch(), builderConfig.useTrailingSlashMatch()); - } - @Override protected boolean hasCorsConfigurationSource(Object handler) { return this.corsConfiguration != null; @@ -284,10 +276,21 @@ protected interface ServletWebOperation { * Adapter class to convert an {@link OperationInvoker} into a * {@link ServletWebOperation}. */ - private class ServletWebOperationAdapter implements ServletWebOperation { + private static class ServletWebOperationAdapter implements ServletWebOperation { private static final String PATH_SEPARATOR = AntPathMatcher.DEFAULT_PATH_SEPARATOR; + private static final List> BODY_CONVERTERS; + + static { + List> converters = new ArrayList<>(); + if (ClassUtils.isPresent("reactor.core.publisher.Flux", + ServletWebOperationAdapter.class.getClassLoader())) { + converters.add(new FluxBodyConverter()); + } + BODY_CONVERTERS = Collections.unmodifiableList(converters); + } + private final WebOperation operation; ServletWebOperationAdapter(WebOperation operation) { @@ -299,13 +302,13 @@ public Object handle(HttpServletRequest request, @RequestBody(required = false) HttpHeaders headers = new ServletServerHttpRequest(request).getHeaders(); Map arguments = getArguments(request, body); try { - ApiVersion apiVersion = ApiVersion.fromHttpHeaders(headers); ServletSecurityContext securityContext = new ServletSecurityContext(request); - InvocationContext invocationContext = new InvocationContext(apiVersion, securityContext, arguments); + InvocationContext invocationContext = new InvocationContext(securityContext, arguments, + new ProducibleOperationArgumentResolver(() -> headers.get("Accept"))); return handleResult(this.operation.invoke(invocationContext), HttpMethod.resolve(request.getMethod())); } catch (InvalidEndpointRequestException ex) { - throw new BadOperationRequestException(ex.getReason()); + throw new InvalidEndpointBadRequestException(ex); } } @@ -330,7 +333,7 @@ private Map getArguments(HttpServletRequest request, Map= 0, "Unable to extract remaining path segments"); @@ -364,10 +367,32 @@ private Object handleResult(Object result, HttpMethod httpMethod) { (httpMethod != HttpMethod.GET) ? HttpStatus.NO_CONTENT : HttpStatus.NOT_FOUND); } if (!(result instanceof WebEndpointResponse)) { - return result; + return convertIfNecessary(result); } WebEndpointResponse response = (WebEndpointResponse) result; - return new ResponseEntity(response.getBody(), HttpStatus.valueOf(response.getStatus())); + MediaType contentType = (response.getContentType() != null) ? new MediaType(response.getContentType()) + : null; + return ResponseEntity.status(response.getStatus()).contentType(contentType) + .body(convertIfNecessary(response.getBody())); + } + + private Object convertIfNecessary(Object body) { + for (Function converter : BODY_CONVERTERS) { + body = converter.apply(body); + } + return body; + } + + private static class FluxBodyConverter implements Function { + + @Override + public Object apply(Object body) { + if (!(body instanceof Flux)) { + return body; + } + return ((Flux) body).collectList(); + } + } } @@ -375,7 +400,7 @@ private Object handleResult(Object result, HttpMethod httpMethod) { /** * Handler for a {@link ServletWebOperation}. */ - private final class OperationHandler { + private static final class OperationHandler { private final ServletWebOperation operation; @@ -416,11 +441,14 @@ public HandlerMethod createWithResolvedBean() { } - @ResponseStatus(code = HttpStatus.BAD_REQUEST) - private static class BadOperationRequestException extends RuntimeException { + /** + * Nested exception used to wrap an {@link InvalidEndpointRequestException} and + * provide a {@link HttpStatus#BAD_REQUEST} status. + */ + private static class InvalidEndpointBadRequestException extends ResponseStatusException { - BadOperationRequestException(String message) { - super(message); + InvalidEndpointBadRequestException(InvalidEndpointRequestException cause) { + super(HttpStatus.BAD_REQUEST, cause.getReason(), cause); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/ControllerEndpointHandlerMapping.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/ControllerEndpointHandlerMapping.java index f06cf3406b91..45b226e5b93a 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/ControllerEndpointHandlerMapping.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/ControllerEndpointHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,6 @@ import org.springframework.util.Assert; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.servlet.HandlerMapping; -import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; @@ -58,6 +57,7 @@ public class ControllerEndpointHandlerMapping extends RequestMappingHandlerMappi * @param endpoints the web endpoints * @param corsConfiguration the CORS configuration for the endpoints or {@code null} */ + @SuppressWarnings("deprecation") public ControllerEndpointHandlerMapping(EndpointMapping endpointMapping, Collection endpoints, CorsConfiguration corsConfiguration) { Assert.notNull(endpointMapping, "EndpointMapping must not be null"); @@ -95,21 +95,13 @@ private RequestMappingInfo withEndpointMappedPatterns(ExposableControllerEndpoin } String[] endpointMappedPatterns = patterns.stream() .map((pattern) -> getEndpointMappedPattern(endpoint, pattern)).toArray(String[]::new); - return withNewPatterns(mapping, endpointMappedPatterns); + return mapping.mutate().paths(endpointMappedPatterns).build(); } private String getEndpointMappedPattern(ExposableControllerEndpoint endpoint, String pattern) { return this.endpointMapping.createSubPath(endpoint.getRootPath() + pattern); } - private RequestMappingInfo withNewPatterns(RequestMappingInfo mapping, String[] patterns) { - PatternsRequestCondition patternsCondition = new PatternsRequestCondition(patterns, null, null, - useSuffixPatternMatch(), useTrailingSlashMatch(), null); - return new RequestMappingInfo(patternsCondition, mapping.getMethodsCondition(), mapping.getParamsCondition(), - mapping.getHeadersCondition(), mapping.getConsumesCondition(), mapping.getProducesCondition(), - mapping.getCustomCondition()); - } - @Override protected boolean hasCorsConfigurationSource(Object handler) { return this.corsConfiguration != null; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/SkipPathExtensionContentNegotiation.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/SkipPathExtensionContentNegotiation.java index 43fd5aaafd0f..156ee9799624 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/SkipPathExtensionContentNegotiation.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/servlet/SkipPathExtensionContentNegotiation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,18 +19,20 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.springframework.web.accept.PathExtensionContentNegotiationStrategy; -import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; +import org.springframework.web.servlet.HandlerInterceptor; /** - * {@link HandlerInterceptorAdapter} to ensure that - * {@link PathExtensionContentNegotiationStrategy} is skipped for web endpoints. + * {@link HandlerInterceptor} to ensure that + * {@link org.springframework.web.accept.PathExtensionContentNegotiationStrategy} is + * skipped for web endpoints. * * @author Phillip Webb */ -final class SkipPathExtensionContentNegotiation extends HandlerInterceptorAdapter { +final class SkipPathExtensionContentNegotiation implements HandlerInterceptor { - private static final String SKIP_ATTRIBUTE = PathExtensionContentNegotiationStrategy.class.getName() + ".SKIP"; + @SuppressWarnings("deprecation") + private static final String SKIP_ATTRIBUTE = org.springframework.web.accept.PathExtensionContentNegotiationStrategy.class + .getName() + ".SKIP"; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpoint.java index c652e590ca12..bd9118e7518e 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,6 +44,7 @@ import org.springframework.core.env.PropertySource; import org.springframework.core.env.StandardEnvironment; import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; import org.springframework.util.PropertyPlaceholderHelper; import org.springframework.util.StringUtils; import org.springframework.util.SystemPropertyUtils; @@ -57,6 +58,7 @@ * @author Christian Dupuis * @author Madhura Bhave * @author Stephane Nicoll + * @author Scott Frederick * @since 2.0.0 */ @Endpoint(id = "env") @@ -74,6 +76,10 @@ public void setKeysToSanitize(String... keysToSanitize) { this.sanitizer.setKeysToSanitize(keysToSanitize); } + public void keysToSanitize(String... keysToSanitize) { + this.sanitizer.keysToSanitize(keysToSanitize); + } + @ReadOperation public EnvironmentDescriptor environment(@Nullable String pattern) { if (StringUtils.hasText(pattern)) { @@ -142,13 +148,8 @@ private PropertySourceDescriptor describeSource(String sourceName, EnumerablePro private PropertyValueDescriptor describeValueOf(String name, PropertySource source, PlaceholdersResolver resolver) { Object resolved = resolver.resolvePlaceholders(source.getProperty(name)); - String origin = ((source instanceof OriginLookup) ? getOrigin((OriginLookup) source, name) : null); - return new PropertyValueDescriptor(sanitize(name, resolved), origin); - } - - private String getOrigin(OriginLookup lookup, String name) { - Origin origin = lookup.getOrigin(name); - return (origin != null) ? origin.toString() : null; + Origin origin = ((source instanceof OriginLookup) ? ((OriginLookup) source).getOrigin(name) : null); + return new PropertyValueDescriptor(stringifyIfNecessary(sanitize(name, resolved)), origin); } private PlaceholdersResolver getResolver() { @@ -187,6 +188,17 @@ public Object sanitize(String name, Object object) { return this.sanitizer.sanitize(name, object); } + protected Object stringifyIfNecessary(Object value) { + if (value == null || ClassUtils.isPrimitiveOrWrapper(value.getClass()) + || Number.class.isAssignableFrom(value.getClass())) { + return value; + } + if (CharSequence.class.isAssignableFrom(value.getClass())) { + return value.toString(); + } + return "Complex property type " + value.getClass().getName(); + } + /** * {@link PropertySourcesPlaceholdersResolver} that sanitizes sensitive placeholders * if present. @@ -353,9 +365,14 @@ public static final class PropertyValueDescriptor { private final String origin; - private PropertyValueDescriptor(Object value, String origin) { + private final String[] originParents; + + private PropertyValueDescriptor(Object value, Origin origin) { this.value = value; - this.origin = origin; + this.origin = (origin != null) ? origin.toString() : null; + List originParents = Origin.parentsFrom(origin); + this.originParents = originParents.isEmpty() ? null + : originParents.stream().map(Object::toString).toArray(String[]::new); } public Object getValue() { @@ -366,6 +383,10 @@ public String getOrigin() { return this.origin; } + public String[] getOriginParents() { + return this.originParents; + } + } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpointWebExtension.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpointWebExtension.java index 9615fdd4aa13..833b57a28485 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpointWebExtension.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpointWebExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ * {@link EndpointWebExtension @EndpointWebExtension} for the {@link EnvironmentEndpoint}. * * @author Stephane Nicoll + * @author Scott Frederick * @since 2.0.0 */ @EndpointWebExtension(endpoint = EnvironmentEndpoint.class) @@ -40,14 +41,8 @@ public EnvironmentEndpointWebExtension(EnvironmentEndpoint delegate) { @ReadOperation public WebEndpointResponse environmentEntry(@Selector String toMatch) { EnvironmentEntryDescriptor descriptor = this.delegate.environmentEntry(toMatch); - return new WebEndpointResponse<>(descriptor, getStatus(descriptor)); - } - - private int getStatus(EnvironmentEntryDescriptor descriptor) { - if (descriptor.getProperty() == null) { - return WebEndpointResponse.STATUS_NOT_FOUND; - } - return WebEndpointResponse.STATUS_OK; + return (descriptor.getProperty() != null) ? new WebEndpointResponse<>(descriptor, WebEndpointResponse.STATUS_OK) + : new WebEndpointResponse<>(WebEndpointResponse.STATUS_NOT_FOUND); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/hazelcast/HazelcastHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/hazelcast/HazelcastHealthIndicator.java index d2c23f3a7366..33abb53377b8 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/hazelcast/HazelcastHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/hazelcast/HazelcastHealthIndicator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,15 @@ package org.springframework.boot.actuate.hazelcast; +import java.lang.reflect.Method; + import com.hazelcast.core.HazelcastInstance; import org.springframework.boot.actuate.health.AbstractHealthIndicator; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; /** * {@link HealthIndicator} for Hazelcast. @@ -43,10 +46,22 @@ public HazelcastHealthIndicator(HazelcastInstance hazelcast) { @Override protected void doHealthCheck(Health.Builder builder) { this.hazelcast.executeTransaction((context) -> { - builder.up().withDetail("name", this.hazelcast.getName()).withDetail("uuid", - this.hazelcast.getLocalEndpoint().getUuid()); + builder.up().withDetail("name", this.hazelcast.getName()).withDetail("uuid", extractUuid()); return null; }); } + private String extractUuid() { + try { + return this.hazelcast.getLocalEndpoint().getUuid().toString(); + } + catch (NoSuchMethodError ex) { + // Hazelcast 3 + Method endpointAccessor = ReflectionUtils.findMethod(HazelcastInstance.class, "getLocalEndpoint"); + Object endpoint = ReflectionUtils.invokeMethod(endpointAccessor, this.hazelcast); + Method uuidAccessor = ReflectionUtils.findMethod(endpoint.getClass(), "getUuid"); + return (String) ReflectionUtils.invokeMethod(uuidAccessor, endpoint); + } + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/AbstractHealthAggregator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/AbstractHealthAggregator.java deleted file mode 100644 index fbe099b4b817..000000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/AbstractHealthAggregator.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.health; - -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -/** - * Base {@link HealthAggregator} implementation to allow subclasses to focus on - * aggregating the {@link Status} instances and not deal with contextual details etc. - * - * @author Christian Dupuis - * @author Vedran Pavic - * @since 1.1.0 - * @deprecated since 2.2.0 as {@link HealthAggregator} has been deprecated - */ -@Deprecated -public abstract class AbstractHealthAggregator implements HealthAggregator { - - @Override - public final Health aggregate(Map healths) { - List statusCandidates = healths.values().stream().map(Health::getStatus).collect(Collectors.toList()); - Status status = aggregateStatus(statusCandidates); - Map details = aggregateDetails(healths); - return new Health.Builder(status, details).build(); - } - - /** - * Return the single 'aggregate' status that should be used from the specified - * candidates. - * @param candidates the candidates - * @return a single status - */ - protected abstract Status aggregateStatus(List candidates); - - /** - * Return the map of 'aggregate' details that should be used from the specified - * healths. - * @param healths the health instances to aggregate - * @return a map of details - * @since 1.3.1 - */ - protected Map aggregateDetails(Map healths) { - return new LinkedHashMap<>(healths); - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ApplicationHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ApplicationHealthIndicator.java deleted file mode 100644 index 242be3cb1135..000000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ApplicationHealthIndicator.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.health; - -/** - * Default implementation of {@link HealthIndicator} that returns {@link Status#UP}. - * - * @author Dave Syer - * @author Christian Dupuis - * @since 1.2.0 - * @see Status#UP - * @deprecated since 2.2 in favor of {@link PingHealthIndicator}. - */ -@Deprecated -public class ApplicationHealthIndicator extends AbstractHealthIndicator { - - public ApplicationHealthIndicator() { - super("Application health check failed"); - } - - @Override - protected void doHealthCheck(Health.Builder builder) throws Exception { - builder.up(); - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/CompositeHealth.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/CompositeHealth.java index 8e897dd148b5..6f1a8b7c247c 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/CompositeHealth.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/CompositeHealth.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; -import org.springframework.boot.actuate.endpoint.http.ApiVersion; +import org.springframework.boot.actuate.endpoint.ApiVersion; import org.springframework.util.Assert; /** @@ -66,7 +66,7 @@ public Map getComponents() { @JsonInclude(Include.NON_EMPTY) @JsonProperty - Map getDetails() { + public Map getDetails() { return this.details; } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/CompositeHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/CompositeHealthIndicator.java deleted file mode 100644 index 6a40d818a44d..000000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/CompositeHealthIndicator.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.health; - -import java.util.LinkedHashMap; -import java.util.Map; - -/** - * {@link HealthIndicator} that returns health indications from all registered delegates. - * - * @author Tyler J. Frederick - * @author Phillip Webb - * @author Christian Dupuis - * @since 1.1.0 - * @deprecated since 2.2.0 in favor of a {@link CompositeHealthContributor} - */ -@Deprecated -public class CompositeHealthIndicator implements HealthIndicator { - - private final HealthIndicatorRegistry registry; - - private final HealthAggregator aggregator; - - /** - * Create a new {@link CompositeHealthIndicator} from the specified indicators. - * @param healthAggregator the health aggregator - * @param indicators a map of {@link HealthIndicator HealthIndicators} with the key - * being used as an indicator name. - */ - public CompositeHealthIndicator(HealthAggregator healthAggregator, Map indicators) { - this(healthAggregator, new DefaultHealthIndicatorRegistry(indicators)); - } - - /** - * Create a new {@link CompositeHealthIndicator} from the indicators in the given - * {@code registry}. - * @param healthAggregator the health aggregator - * @param registry the registry of {@link HealthIndicator HealthIndicators}. - */ - public CompositeHealthIndicator(HealthAggregator healthAggregator, HealthIndicatorRegistry registry) { - this.aggregator = healthAggregator; - this.registry = registry; - } - - /** - * Return the {@link HealthIndicatorRegistry} of this instance. - * @return the registry of nested {@link HealthIndicator health indicators} - * @since 2.1.0 - */ - public HealthIndicatorRegistry getRegistry() { - return this.registry; - } - - @Override - public Health health() { - Map healths = new LinkedHashMap<>(); - for (Map.Entry entry : this.registry.getAll().entrySet()) { - healths.put(entry.getKey(), entry.getValue().health()); - } - return this.aggregator.aggregate(healths); - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/CompositeReactiveHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/CompositeReactiveHealthIndicator.java deleted file mode 100644 index 3b3d36a9164c..000000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/CompositeReactiveHealthIndicator.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.health; - -import java.time.Duration; -import java.util.function.Function; - -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.util.function.Tuple2; - -/** - * {@link ReactiveHealthIndicator} that returns health indications from all registered - * delegates. Provides an alternative {@link Health} for a delegate that reaches a - * configurable timeout. - * - * @author Stephane Nicoll - * @since 2.0.0 - * @deprecated since 2.2.0 in favor of a {@link CompositeReactiveHealthContributor} - */ -@Deprecated -public class CompositeReactiveHealthIndicator implements ReactiveHealthIndicator { - - private final ReactiveHealthIndicatorRegistry registry; - - private final HealthAggregator healthAggregator; - - private Long timeout; - - private Health timeoutHealth; - - private final Function, Mono> timeoutCompose; - - /** - * Create a new {@link CompositeReactiveHealthIndicator} from the indicators in the - * given {@code registry}. - * @param healthAggregator the health aggregator - * @param registry the registry of {@link ReactiveHealthIndicator HealthIndicators}. - */ - public CompositeReactiveHealthIndicator(HealthAggregator healthAggregator, - ReactiveHealthIndicatorRegistry registry) { - this.registry = registry; - this.healthAggregator = healthAggregator; - this.timeoutCompose = (mono) -> (this.timeout != null) - ? mono.timeout(Duration.ofMillis(this.timeout), Mono.just(this.timeoutHealth)) : mono; - } - - /** - * Specify an alternative timeout {@link Health} if a {@link HealthIndicator} failed - * to reply after specified {@code timeout}. - * @param timeout number of milliseconds to wait before using the - * {@code timeoutHealth} - * @param timeoutHealth the {@link Health} to use if an health indicator reached the - * {@code timeout} - * @return this instance - */ - public CompositeReactiveHealthIndicator timeoutStrategy(long timeout, Health timeoutHealth) { - this.timeout = timeout; - this.timeoutHealth = (timeoutHealth != null) ? timeoutHealth : Health.unknown().build(); - return this; - } - - ReactiveHealthIndicatorRegistry getRegistry() { - return this.registry; - } - - @Override - public Mono health() { - return Flux.fromIterable(this.registry.getAll().entrySet()) - .flatMap((entry) -> Mono.zip(Mono.just(entry.getKey()), - entry.getValue().health().transformDeferred(this.timeoutCompose))) - .collectMap(Tuple2::getT1, Tuple2::getT2).map(this.healthAggregator::aggregate); - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/DefaultHealthIndicatorRegistry.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/DefaultHealthIndicatorRegistry.java deleted file mode 100644 index 612b2a7b7bdd..000000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/DefaultHealthIndicatorRegistry.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.health; - -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; - -import org.springframework.util.Assert; - -/** - * Default implementation of {@link HealthIndicatorRegistry}. - * - * @author Vedran Pavic - * @author Stephane Nicoll - * @since 2.1.0 - * @deprecated since 2.2.0 in favor of {@link DefaultContributorRegistry} - */ -@Deprecated -public class DefaultHealthIndicatorRegistry implements HealthIndicatorRegistry { - - private final Object monitor = new Object(); - - private final Map healthIndicators; - - /** - * Create a new {@link DefaultHealthIndicatorRegistry}. - */ - public DefaultHealthIndicatorRegistry() { - this(new LinkedHashMap<>()); - } - - /** - * Create a new {@link DefaultHealthIndicatorRegistry} from the specified indicators. - * @param healthIndicators a map of {@link HealthIndicator}s with the key being used - * as an indicator name. - */ - public DefaultHealthIndicatorRegistry(Map healthIndicators) { - Assert.notNull(healthIndicators, "HealthIndicators must not be null"); - this.healthIndicators = new LinkedHashMap<>(healthIndicators); - } - - @Override - public void register(String name, HealthIndicator healthIndicator) { - Assert.notNull(healthIndicator, "HealthIndicator must not be null"); - Assert.notNull(name, "Name must not be null"); - synchronized (this.monitor) { - HealthIndicator existing = this.healthIndicators.putIfAbsent(name, healthIndicator); - if (existing != null) { - throw new IllegalStateException("HealthIndicator with name '" + name + "' already registered"); - } - } - } - - @Override - public HealthIndicator unregister(String name) { - Assert.notNull(name, "Name must not be null"); - synchronized (this.monitor) { - return this.healthIndicators.remove(name); - } - } - - @Override - public HealthIndicator get(String name) { - Assert.notNull(name, "Name must not be null"); - synchronized (this.monitor) { - return this.healthIndicators.get(name); - } - } - - @Override - public Map getAll() { - synchronized (this.monitor) { - return Collections.unmodifiableMap(new LinkedHashMap<>(this.healthIndicators)); - } - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/DefaultReactiveHealthIndicatorRegistry.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/DefaultReactiveHealthIndicatorRegistry.java deleted file mode 100644 index 78c74d7c9936..000000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/DefaultReactiveHealthIndicatorRegistry.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.health; - -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; - -import org.springframework.util.Assert; - -/** - * Default implementation of {@link ReactiveHealthIndicatorRegistry}. - * - * @author Vedran Pavic - * @author Stephane Nicoll - * @since 2.1.0 - * @deprecated since 2.2.0 in favor of {@link DefaultContributorRegistry} - */ -@Deprecated -public class DefaultReactiveHealthIndicatorRegistry implements ReactiveHealthIndicatorRegistry { - - private final Object monitor = new Object(); - - private final Map healthIndicators; - - /** - * Create a new {@link DefaultReactiveHealthIndicatorRegistry}. - */ - public DefaultReactiveHealthIndicatorRegistry() { - this(new LinkedHashMap<>()); - } - - /** - * Create a new {@link DefaultReactiveHealthIndicatorRegistry} from the specified - * indicators. - * @param healthIndicators a map of {@link HealthIndicator}s with the key being used - * as an indicator name. - */ - public DefaultReactiveHealthIndicatorRegistry(Map healthIndicators) { - Assert.notNull(healthIndicators, "HealthIndicators must not be null"); - this.healthIndicators = new LinkedHashMap<>(healthIndicators); - } - - @Override - public void register(String name, ReactiveHealthIndicator healthIndicator) { - Assert.notNull(healthIndicator, "HealthIndicator must not be null"); - Assert.notNull(name, "Name must not be null"); - synchronized (this.monitor) { - ReactiveHealthIndicator existing = this.healthIndicators.putIfAbsent(name, healthIndicator); - if (existing != null) { - throw new IllegalStateException("HealthIndicator with name '" + name + "' already registered"); - } - } - } - - @Override - public ReactiveHealthIndicator unregister(String name) { - Assert.notNull(name, "Name must not be null"); - synchronized (this.monitor) { - return this.healthIndicators.remove(name); - } - } - - @Override - public ReactiveHealthIndicator get(String name) { - Assert.notNull(name, "Name must not be null"); - synchronized (this.monitor) { - return this.healthIndicators.get(name); - } - } - - @Override - public Map getAll() { - synchronized (this.monitor) { - return Collections.unmodifiableMap(new LinkedHashMap<>(this.healthIndicators)); - } - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthAggregator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthAggregator.java deleted file mode 100644 index 3049fe742ba6..000000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthAggregator.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.health; - -import java.util.Map; - -/** - * Strategy interface used to aggregate {@link Health} instances into a final one. - *

- * This is especially useful to combine subsystem states expressed through - * {@link Health#getStatus()} into one state for the entire system. The default - * implementation {@link OrderedHealthAggregator} sorts {@link Status} instances based on - * a priority list. - *

- * It is possible to add more complex {@link Status} types to the system. In that case - * either the {@link OrderedHealthAggregator} needs to be properly configured or users - * need to register a custom {@link HealthAggregator} as bean. - * - * @author Christian Dupuis - * @since 1.1.0 - * @deprecated since 2.2.0 in favor of {@link StatusAggregator} - */ -@FunctionalInterface -@Deprecated -public interface HealthAggregator { - - /** - * Aggregate several given {@link Health} instances into one. - * @param healths the health instances to aggregate - * @return the aggregated health - */ - Health aggregate(Map healths); - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpoint.java index ce02cfabd90b..da130063dd76 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,12 +19,12 @@ import java.util.Map; import java.util.Set; +import org.springframework.boot.actuate.endpoint.ApiVersion; import org.springframework.boot.actuate.endpoint.SecurityContext; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.Selector; import org.springframework.boot.actuate.endpoint.annotation.Selector.Match; -import org.springframework.boot.actuate.endpoint.http.ApiVersion; /** * {@link Endpoint @Endpoint} to expose application health information. @@ -33,6 +33,7 @@ * @author Christian Dupuis * @author Andy Wilkinson * @author Stephane Nicoll + * @author Scott Frederick * @since 2.0.0 */ @Endpoint(id = "health") @@ -40,17 +41,6 @@ public class HealthEndpoint extends HealthEndpointSupport the contributor type * @param the contributed health component type * @author Phillip Webb + * @author Scott Frederick */ abstract class HealthEndpointSupport { @@ -40,16 +41,6 @@ abstract class HealthEndpointSupport { private final HealthEndpointGroups groups; - /** - * Throw a new {@link IllegalStateException} to indicate a constructor has been - * deprecated. - * @deprecated since 2.2.0 in order to support deprecated subclass constructors - */ - @Deprecated - HealthEndpointSupport() { - throw new IllegalStateException("Unable to create " + getClass() + " using deprecated constructor"); - } - /** * Create a new {@link HealthEndpointSupport} instance. * @param registry the health contributor registry @@ -81,7 +72,7 @@ private HealthResult getHealth(ApiVersion apiVersion, HealthEndpointGroup gro } Object contributor = getContributor(path, pathOffset); T health = getContribution(apiVersion, group, contributor, showComponents, showDetails, - isSystemHealth ? this.groups.getNames() : null); + isSystemHealth ? this.groups.getNames() : null, false); return (health != null) ? new HealthResult<>(health, group) : null; } @@ -100,23 +91,24 @@ private Object getContributor(String[] path, int pathOffset) { @SuppressWarnings("unchecked") private T getContribution(ApiVersion apiVersion, HealthEndpointGroup group, Object contributor, - boolean showComponents, boolean showDetails, Set groupNames) { + boolean showComponents, boolean showDetails, Set groupNames, boolean isNested) { if (contributor instanceof NamedContributors) { return getAggregateHealth(apiVersion, group, (NamedContributors) contributor, showComponents, - showDetails, groupNames); + showDetails, groupNames, isNested); } return (contributor != null) ? getHealth((C) contributor, showDetails) : null; } private T getAggregateHealth(ApiVersion apiVersion, HealthEndpointGroup group, - NamedContributors namedContributors, boolean showComponents, boolean showDetails, - Set groupNames) { + NamedContributors namedContributors, boolean showComponents, boolean showDetails, Set groupNames, + boolean isNested) { Map contributions = new LinkedHashMap<>(); for (NamedContributor namedContributor : namedContributors) { String name = namedContributor.getName(); C contributor = namedContributor.getContributor(); - if (group.isMember(name)) { - T contribution = getContribution(apiVersion, group, contributor, showComponents, showDetails, null); + if (group.isMember(name) || isNested) { + T contribution = getContribution(apiVersion, group, contributor, showComponents, showDetails, null, + true); if (contribution != null) { contributions.put(name, contribution); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointWebExtension.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointWebExtension.java index 034faf3a89f1..11eea5fb9995 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointWebExtension.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpointWebExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,11 +20,11 @@ import java.util.Map; import java.util.Set; +import org.springframework.boot.actuate.endpoint.ApiVersion; import org.springframework.boot.actuate.endpoint.SecurityContext; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.Selector; import org.springframework.boot.actuate.endpoint.annotation.Selector.Match; -import org.springframework.boot.actuate.endpoint.http.ApiVersion; import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension; @@ -38,6 +38,7 @@ * @author Eddú Meléndez * @author Madhura Bhave * @author Stephane Nicoll + * @author Scott Frederick * @since 2.0.0 */ @EndpointWebExtension(endpoint = HealthEndpoint.class) @@ -45,17 +46,6 @@ public class HealthEndpointWebExtension extends HealthEndpointSupport - * Implementations must be thread-safe. - * - * @author Andy Wilkinson - * @author Vedran Pavic - * @author Stephane Nicoll - * @since 2.1.0 - * @see HealthEndpoint - * @deprecated since 2.2.0 in favor of a {@link HealthContributorRegistry} - */ -@Deprecated -public interface HealthIndicatorRegistry { - - /** - * Registers the given {@link HealthIndicator}, associating it with the given - * {@code name}. - * @param name the name of the indicator - * @param healthIndicator the indicator - * @throws IllegalStateException if the indicator cannot be registered with the given - * {@code name}. - */ - void register(String name, HealthIndicator healthIndicator); - - /** - * Unregisters the {@link HealthIndicator} previously registered with the given - * {@code name}. - * @param name the name of the indicator - * @return the unregistered indicator, or {@code null} if no indicator was found in - * the registry for the given {@code name}. - */ - HealthIndicator unregister(String name); - - /** - * Returns the {@link HealthIndicator} registered with the given {@code name}. - * @param name the name of the indicator - * @return the health indicator, or {@code null} if no indicator was registered with - * the given {@code name}. - */ - HealthIndicator get(String name); - - /** - * Returns a snapshot of the registered health indicators and their names. The - * contents of the map do not reflect subsequent changes to the registry. - * @return the snapshot of registered health indicators - */ - Map getAll(); - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthIndicatorRegistryFactory.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthIndicatorRegistryFactory.java deleted file mode 100644 index 5a34072e90c3..000000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthIndicatorRegistryFactory.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.health; - -import java.util.Map; -import java.util.function.Function; - -import org.springframework.util.Assert; - -/** - * Factory to create a {@link HealthIndicatorRegistry}. - * - * @author Stephane Nicoll - * @since 2.1.0 - * @deprecated since 2.2.0 in favor of {@link DefaultHealthIndicatorRegistry} - */ -@Deprecated -public class HealthIndicatorRegistryFactory { - - private final Function healthIndicatorNameFactory; - - public HealthIndicatorRegistryFactory(Function healthIndicatorNameFactory) { - this.healthIndicatorNameFactory = healthIndicatorNameFactory; - } - - public HealthIndicatorRegistryFactory() { - this(new HealthIndicatorNameFactory()); - } - - /** - * Create a {@link HealthIndicatorRegistry} based on the specified health indicators. - * @param healthIndicators the {@link HealthIndicator} instances mapped by name - * @return a {@link HealthIndicator} that delegates to the specified - * {@code healthIndicators}. - */ - public HealthIndicatorRegistry createHealthIndicatorRegistry(Map healthIndicators) { - Assert.notNull(healthIndicators, "HealthIndicators must not be null"); - return initialize(new DefaultHealthIndicatorRegistry(), healthIndicators); - } - - protected T initialize(T registry, - Map healthIndicators) { - for (Map.Entry entry : healthIndicators.entrySet()) { - String name = this.healthIndicatorNameFactory.apply(entry.getKey()); - registry.register(name, entry.getValue()); - } - return registry; - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthStatusHttpMapper.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthStatusHttpMapper.java deleted file mode 100644 index c38793ef9b4b..000000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthStatusHttpMapper.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.health; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; -import org.springframework.util.Assert; - -/** - * Map a {@link Status} to an HTTP status code. - * - * @author Stephane Nicoll - * @since 2.0.0 - * @deprecated since 2.2.0 in favor of {@link HttpCodeStatusMapper} or - * {@link SimpleHttpCodeStatusMapper} - */ -@Deprecated -public class HealthStatusHttpMapper { - - private Map statusMapping = new HashMap<>(); - - /** - * Create a new instance. - */ - public HealthStatusHttpMapper() { - setupDefaultStatusMapping(); - } - - private void setupDefaultStatusMapping() { - addStatusMapping(Status.DOWN, WebEndpointResponse.STATUS_SERVICE_UNAVAILABLE); - addStatusMapping(Status.OUT_OF_SERVICE, WebEndpointResponse.STATUS_SERVICE_UNAVAILABLE); - } - - /** - * Set specific status mappings. - * @param statusMapping a map of health status code to HTTP status code - */ - public void setStatusMapping(Map statusMapping) { - Assert.notNull(statusMapping, "StatusMapping must not be null"); - this.statusMapping = new HashMap<>(statusMapping); - } - - /** - * Add specific status mappings to the existing set. - * @param statusMapping a map of health status code to HTTP status code - */ - public void addStatusMapping(Map statusMapping) { - Assert.notNull(statusMapping, "StatusMapping must not be null"); - this.statusMapping.putAll(statusMapping); - } - - /** - * Add a status mapping to the existing set. - * @param status the status to map - * @param httpStatus the http status - */ - public void addStatusMapping(Status status, Integer httpStatus) { - Assert.notNull(status, "Status must not be null"); - Assert.notNull(httpStatus, "HttpStatus must not be null"); - addStatusMapping(status.getCode(), httpStatus); - } - - /** - * Add a status mapping to the existing set. - * @param statusCode the status code to map - * @param httpStatus the http status - */ - public void addStatusMapping(String statusCode, Integer httpStatus) { - Assert.notNull(statusCode, "StatusCode must not be null"); - Assert.notNull(httpStatus, "HttpStatus must not be null"); - this.statusMapping.put(statusCode, httpStatus); - } - - /** - * Return an immutable view of the status mapping. - * @return the http status codes mapped by status name - */ - public Map getStatusMapping() { - return Collections.unmodifiableMap(this.statusMapping); - } - - /** - * Map the specified {@link Status} to an HTTP status code. - * @param status the health {@link Status} - * @return the corresponding HTTP status code - */ - public int mapStatus(Status status) { - String code = getUniformValue(status.getCode()); - if (code != null) { - return this.statusMapping.entrySet().stream() - .filter((entry) -> code.equals(getUniformValue(entry.getKey()))).map(Map.Entry::getValue) - .findFirst().orElse(WebEndpointResponse.STATUS_OK); - } - return WebEndpointResponse.STATUS_OK; - } - - private String getUniformValue(String code) { - if (code == null) { - return null; - } - StringBuilder builder = new StringBuilder(); - for (char ch : code.toCharArray()) { - if (Character.isAlphabetic(ch) || Character.isDigit(ch)) { - builder.append(Character.toLowerCase(ch)); - } - } - return builder.toString(); - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthWebEndpointResponseMapper.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthWebEndpointResponseMapper.java deleted file mode 100644 index c26f6263ac92..000000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthWebEndpointResponseMapper.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.health; - -import java.util.Set; -import java.util.function.Supplier; - -import org.springframework.boot.actuate.endpoint.SecurityContext; -import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; -import org.springframework.util.CollectionUtils; - -/** - * Maps a {@link Health} to a {@link WebEndpointResponse}. - * - * @author Andy Wilkinson - * @since 2.0.0 - * @deprecated since 2.2.0 in favor of {@link HealthEndpointWebExtension} or - * {@link ReactiveHealthEndpointWebExtension} - */ -@Deprecated -public class HealthWebEndpointResponseMapper { - - private final HealthStatusHttpMapper statusHttpMapper; - - private final ShowDetails showDetails; - - private final Set authorizedRoles; - - public HealthWebEndpointResponseMapper(HealthStatusHttpMapper statusHttpMapper, ShowDetails showDetails, - Set authorizedRoles) { - this.statusHttpMapper = statusHttpMapper; - this.showDetails = showDetails; - this.authorizedRoles = authorizedRoles; - } - - /** - * Maps the given {@code health} details to a {@link WebEndpointResponse}, honouring - * the mapper's default {@link ShowDetails} using the given {@code securityContext}. - *

- * If the current user does not have the right to see the details, the - * {@link Supplier} is not invoked and a 404 response is returned instead. - * @param health the provider of health details, invoked if the current user has the - * right to see them - * @param securityContext the security context - * @return the mapped response - */ - public WebEndpointResponse mapDetails(Supplier health, SecurityContext securityContext) { - if (canSeeDetails(securityContext, this.showDetails)) { - Health healthDetails = health.get(); - if (healthDetails != null) { - return createWebEndpointResponse(healthDetails); - } - } - return new WebEndpointResponse<>(WebEndpointResponse.STATUS_NOT_FOUND); - } - - /** - * Maps the given {@code health} to a {@link WebEndpointResponse}, honouring the - * mapper's default {@link ShowDetails} using the given {@code securityContext}. - * @param health the health to map - * @param securityContext the security context - * @return the mapped response - */ - public WebEndpointResponse map(Health health, SecurityContext securityContext) { - return map(health, securityContext, this.showDetails); - } - - /** - * Maps the given {@code health} to a {@link WebEndpointResponse}, honouring the given - * {@code showDetails} using the given {@code securityContext}. - * @param health the health to map - * @param securityContext the security context - * @param showDetails when to show details in the response - * @return the mapped response - */ - public WebEndpointResponse map(Health health, SecurityContext securityContext, ShowDetails showDetails) { - if (!canSeeDetails(securityContext, showDetails)) { - health = Health.status(health.getStatus()).build(); - } - return createWebEndpointResponse(health); - } - - private WebEndpointResponse createWebEndpointResponse(Health health) { - Integer status = this.statusHttpMapper.mapStatus(health.getStatus()); - return new WebEndpointResponse<>(health, status); - } - - private boolean canSeeDetails(SecurityContext securityContext, ShowDetails showDetails) { - return showDetails != ShowDetails.NEVER && (showDetails != ShowDetails.WHEN_AUTHORIZED - || (securityContext.getPrincipal() != null && isUserInRole(securityContext))); - } - - private boolean isUserInRole(SecurityContext securityContext) { - if (CollectionUtils.isEmpty(this.authorizedRoles)) { - return true; - } - for (String role : this.authorizedRoles) { - if (securityContext.isUserInRole(role)) { - return true; - } - } - return false; - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HttpCodeStatusMapper.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HttpCodeStatusMapper.java index 2e809040e690..10d9d291d43b 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HttpCodeStatusMapper.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HttpCodeStatusMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,12 @@ @FunctionalInterface public interface HttpCodeStatusMapper { + /** + * A {@link HttpCodeStatusMapper} instance using default mappings. + * @since 2.3.0 + */ + HttpCodeStatusMapper DEFAULT = new SimpleHttpCodeStatusMapper(); + /** * Return the HTTP status code that corresponds to the given {@link Status health * status}. diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/NamedContributors.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/NamedContributors.java index 4cae6dca7bbb..5d4b9e1cdc8a 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/NamedContributors.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/NamedContributors.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ public interface NamedContributors extends Iterable> { /** * Return the contributor with the given name. * @param name the name of the contributor - * @return a contributor instance of {@code null} + * @return a contributor instance or {@code null} */ C getContributor(String name); diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/NamedContributorsMapAdapter.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/NamedContributorsMapAdapter.java index 4645ad047b7c..68ceb77daa7d 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/NamedContributorsMapAdapter.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/NamedContributorsMapAdapter.java @@ -43,7 +43,7 @@ abstract class NamedContributorsMapAdapter implements NamedContributors NamedContributorsMapAdapter(Map map, Function valueAdapter) { Assert.notNull(map, "Map must not be null"); Assert.notNull(valueAdapter, "ValueAdapter must not be null"); - map.keySet().stream().forEach((key) -> Assert.notNull(key, "Map must not contain null keys")); + map.keySet().forEach((key) -> Assert.notNull(key, "Map must not contain null keys")); map.values().stream().map(valueAdapter) .forEach((value) -> Assert.notNull(value, "Map must not contain null values")); this.map = Collections.unmodifiableMap(new LinkedHashMap<>(map)); diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/OrderedHealthAggregator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/OrderedHealthAggregator.java deleted file mode 100644 index 3d80f16b6537..000000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/OrderedHealthAggregator.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.health; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.List; - -import org.springframework.util.Assert; - -/** - * Default {@link HealthAggregator} implementation that aggregates {@link Health} - * instances and determines the final system state based on a simple ordered list. - *

- * If a different order is required or a new {@link Status} type will be used, the order - * can be set by calling {@link #setStatusOrder(List)}. - * - * @author Christian Dupuis - * @since 1.1.0 - * @deprecated since 2.2.0 in favor of {@link SimpleStatusAggregator} - */ -@Deprecated -public class OrderedHealthAggregator extends AbstractHealthAggregator { - - private List statusOrder; - - /** - * Create a new {@link OrderedHealthAggregator} instance. - */ - public OrderedHealthAggregator() { - setStatusOrder(Status.DOWN, Status.OUT_OF_SERVICE, Status.UP, Status.UNKNOWN); - } - - /** - * Set the ordering of the status. - * @param statusOrder an ordered list of the status - */ - public void setStatusOrder(Status... statusOrder) { - String[] order = new String[statusOrder.length]; - for (int i = 0; i < statusOrder.length; i++) { - order[i] = statusOrder[i].getCode(); - } - setStatusOrder(Arrays.asList(order)); - } - - /** - * Set the ordering of the status. - * @param statusOrder an ordered list of the status codes - */ - public void setStatusOrder(List statusOrder) { - Assert.notNull(statusOrder, "StatusOrder must not be null"); - this.statusOrder = statusOrder; - } - - @Override - protected Status aggregateStatus(List candidates) { - // Only sort those status instances that we know about - List filteredCandidates = new ArrayList<>(); - for (Status candidate : candidates) { - if (this.statusOrder.contains(candidate.getCode())) { - filteredCandidates.add(candidate); - } - } - // If no status is given return UNKNOWN - if (filteredCandidates.isEmpty()) { - return Status.UNKNOWN; - } - // Sort given Status instances by configured order - filteredCandidates.sort(new StatusComparator(this.statusOrder)); - return filteredCandidates.get(0); - } - - /** - * {@link Comparator} used to order {@link Status}. - */ - private class StatusComparator implements Comparator { - - private final List statusOrder; - - StatusComparator(List statusOrder) { - this.statusOrder = statusOrder; - } - - @Override - public int compare(Status s1, Status s2) { - int i1 = this.statusOrder.indexOf(s1.getCode()); - int i2 = this.statusOrder.indexOf(s2.getCode()); - return (i1 < i2) ? -1 : (i1 != i2) ? 1 : s1.getCode().compareTo(s2.getCode()); - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthContributor.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthContributor.java index 3bd7eb245a72..89b4c3a148e1 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthContributor.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthContributor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,6 @@ */ public interface ReactiveHealthContributor { - @SuppressWarnings("deprecation") static ReactiveHealthContributor adapt(HealthContributor healthContributor) { Assert.notNull(healthContributor, "HealthContributor must not be null"); if (healthContributor instanceof HealthIndicator) { diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtension.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtension.java index a3d16e62c17a..483e0a9e45b8 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtension.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,11 +23,11 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.boot.actuate.endpoint.ApiVersion; import org.springframework.boot.actuate.endpoint.SecurityContext; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.Selector; import org.springframework.boot.actuate.endpoint.annotation.Selector.Match; -import org.springframework.boot.actuate.endpoint.http.ApiVersion; import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension; @@ -37,6 +37,7 @@ * * @author Stephane Nicoll * @author Phillip Webb + * @author Scott Frederick * @since 2.0.0 */ @EndpointWebExtension(endpoint = HealthEndpoint.class) @@ -45,18 +46,6 @@ public class ReactiveHealthEndpointWebExtension private static final String[] NO_PATH = {}; - /** - * Create a new {@link ReactiveHealthEndpointWebExtension} instance. - * @param delegate the delegate health indicator - * @param responseMapper the response mapper - * @deprecated since 2.2.0 in favor of - * {@link #ReactiveHealthEndpointWebExtension(ReactiveHealthContributorRegistry, HealthEndpointGroups)} - */ - @Deprecated - public ReactiveHealthEndpointWebExtension(ReactiveHealthIndicator delegate, - HealthWebEndpointResponseMapper responseMapper) { - } - /** * Create a new {@link ReactiveHealthEndpointWebExtension} instance. * @param registry the health contributor registry diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthIndicatorRegistry.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthIndicatorRegistry.java deleted file mode 100644 index 7d3f68e36c06..000000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthIndicatorRegistry.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.health; - -import java.util.Map; - -/** - * A registry of {@link ReactiveHealthIndicator ReactiveHealthIndicators}. - *

- * Implementations must be thread-safe. - * - * @author Andy Wilkinson - * @author Vedran Pavic - * @author Stephane Nicoll - * @since 2.1.0 - * @see HealthIndicatorRegistry - * @deprecated since 2.2.0 in favor of a {@link ReactiveHealthContributorRegistry} - */ -@Deprecated -public interface ReactiveHealthIndicatorRegistry { - - /** - * Registers the given {@link ReactiveHealthIndicator}, associating it with the given - * {@code name}. - * @param name the name of the indicator - * @param healthIndicator the indicator - * @throws IllegalStateException if an indicator with the given {@code name} is - * already registered. - */ - void register(String name, ReactiveHealthIndicator healthIndicator); - - /** - * Unregisters the {@link ReactiveHealthIndicator} previously registered with the - * given {@code name}. - * @param name the name of the indicator - * @return the unregistered indicator, or {@code null} if no indicator was found in - * the registry for the given {@code name}. - */ - ReactiveHealthIndicator unregister(String name); - - /** - * Returns the {@link ReactiveHealthIndicator} registered with the given {@code name}. - * @param name the name of the indicator - * @return the health indicator, or {@code null} if no indicator was registered with - * the given {@code name}. - */ - ReactiveHealthIndicator get(String name); - - /** - * Returns a snapshot of the registered health indicators and their names. The - * contents of the map do not reflect subsequent changes to the registry. - * @return the snapshot of registered health indicators - */ - Map getAll(); - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthIndicatorRegistryFactory.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthIndicatorRegistryFactory.java deleted file mode 100644 index 9896104a137d..000000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ReactiveHealthIndicatorRegistryFactory.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.health; - -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.function.Function; - -import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; - -/** - * Factory to create a {@link HealthIndicatorRegistry}. - * - * @author Stephane Nicoll - * @since 2.1.0 - * @deprecated since 2.2.0 in favor of {@link DefaultReactiveHealthIndicatorRegistry} - */ -@Deprecated -public class ReactiveHealthIndicatorRegistryFactory { - - private final Function healthIndicatorNameFactory; - - public ReactiveHealthIndicatorRegistryFactory(Function healthIndicatorNameFactory) { - this.healthIndicatorNameFactory = healthIndicatorNameFactory; - } - - public ReactiveHealthIndicatorRegistryFactory() { - this(new HealthIndicatorNameFactory()); - } - - /** - * Create a {@link ReactiveHealthIndicatorRegistry} based on the specified health - * indicators. Each {@link HealthIndicator} are wrapped to a - * {@link HealthIndicatorReactiveAdapter}. If two instances share the same name, the - * reactive variant takes precedence. - * @param reactiveHealthIndicators the {@link ReactiveHealthIndicator} instances - * mapped by name - * @param healthIndicators the {@link HealthIndicator} instances mapped by name if - * any. - * @return a {@link ReactiveHealthIndicator} that delegates to the specified - * {@code reactiveHealthIndicators}. - */ - public ReactiveHealthIndicatorRegistry createReactiveHealthIndicatorRegistry( - Map reactiveHealthIndicators, - Map healthIndicators) { - Assert.notNull(reactiveHealthIndicators, "ReactiveHealthIndicators must not be null"); - return initialize(new DefaultReactiveHealthIndicatorRegistry(), reactiveHealthIndicators, healthIndicators); - } - - protected T initialize(T registry, - Map reactiveHealthIndicators, - Map healthIndicators) { - merge(reactiveHealthIndicators, healthIndicators).forEach((beanName, indicator) -> { - String name = this.healthIndicatorNameFactory.apply(beanName); - registry.register(name, indicator); - }); - return registry; - } - - private Map merge(Map reactiveHealthIndicators, - Map healthIndicators) { - if (ObjectUtils.isEmpty(healthIndicators)) { - return reactiveHealthIndicators; - } - Map allIndicators = new LinkedHashMap<>(reactiveHealthIndicators); - healthIndicators.forEach((beanName, indicator) -> { - String name = this.healthIndicatorNameFactory.apply(beanName); - allIndicators.computeIfAbsent(name, (n) -> new HealthIndicatorReactiveAdapter(indicator)); - }); - return allIndicators; - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ShowDetails.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ShowDetails.java deleted file mode 100644 index 30432741f5c5..000000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/ShowDetails.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.health; - -/** - * Options for showing details in responses from the {@link HealthEndpoint} web - * extensions. - * - * @author Andy Wilkinson - * @since 2.0.0 - * @deprecated since 2.2.0 in favor of {@code HealthEndpointProperties.ShowDetails} - */ -@Deprecated -public enum ShowDetails { - - /** - * Never show details in the response. - */ - NEVER, - - /** - * Show details in the response when accessed by an authorized user. - */ - WHEN_AUTHORIZED, - - /** - * Always show details in the response. - */ - ALWAYS - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/SimpleHttpCodeStatusMapper.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/SimpleHttpCodeStatusMapper.java index 934c36b6f616..9f4f9321e615 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/SimpleHttpCodeStatusMapper.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/SimpleHttpCodeStatusMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -81,7 +81,8 @@ private static String getUniformCode(String code) { return null; } StringBuilder builder = new StringBuilder(); - for (char ch : code.toCharArray()) { + for (int i = 0; i < code.length(); i++) { + char ch = code.charAt(i); if (Character.isAlphabetic(ch) || Character.isDigit(ch)) { builder.append(Character.toLowerCase(ch)); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/SimpleStatusAggregator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/SimpleStatusAggregator.java index 37f75fbee558..4c5b9efe79b4 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/SimpleStatusAggregator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/SimpleStatusAggregator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +37,9 @@ public class SimpleStatusAggregator implements StatusAggregator { private static final List DEFAULT_ORDER; + + static final StatusAggregator INSTANCE; + static { List defaultOrder = new ArrayList<>(); defaultOrder.add(Status.DOWN.getCode()); @@ -44,6 +47,7 @@ public class SimpleStatusAggregator implements StatusAggregator { defaultOrder.add(Status.UP.getCode()); defaultOrder.add(Status.UNKNOWN.getCode()); DEFAULT_ORDER = Collections.unmodifiableList(getUniformCodes(defaultOrder.stream())); + INSTANCE = new SimpleStatusAggregator(); } private final List order; @@ -69,7 +73,7 @@ public SimpleStatusAggregator(List order) { @Override public Status getAggregateStatus(Set statuses) { - return statuses.stream().filter(this::contains).sorted(this.comparator).findFirst().orElse(Status.UNKNOWN); + return statuses.stream().filter(this::contains).min(this.comparator).orElse(Status.UNKNOWN); } private boolean contains(Status status) { @@ -85,7 +89,8 @@ private static String getUniformCode(String code) { return null; } StringBuilder builder = new StringBuilder(); - for (char ch : code.toCharArray()) { + for (int i = 0; i < code.length(); i++) { + char ch = code.charAt(i); if (Character.isAlphabetic(ch) || Character.isDigit(ch)) { builder.append(Character.toLowerCase(ch)); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/StatusAggregator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/StatusAggregator.java index e83e18349220..82f4be77b6dc 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/StatusAggregator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/StatusAggregator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,6 +32,15 @@ @FunctionalInterface public interface StatusAggregator { + /** + * Return {@link StatusAggregator} instance using default ordering rules. + * @return a {@code StatusAggregator} with default ordering rules. + * @since 2.3.0 + */ + static StatusAggregator getDefault() { + return SimpleStatusAggregator.INSTANCE; + } + /** * Return the aggregate status for the given set of statuses. * @param statuses the statuses to aggregate diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/SystemHealth.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/SystemHealth.java index 3ea7ce787cda..d52676ce5a47 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/SystemHealth.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/SystemHealth.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; -import org.springframework.boot.actuate.endpoint.http.ApiVersion; +import org.springframework.boot.actuate.endpoint.ApiVersion; /** * A {@link HealthComponent} that represents the overall system health and the available diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/jdbc/DataSourceHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/jdbc/DataSourceHealthIndicator.java index 25cd37e82deb..b92c7398a67d 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/jdbc/DataSourceHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/jdbc/DataSourceHealthIndicator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ import org.springframework.boot.actuate.health.AbstractHealthIndicator; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; -import org.springframework.boot.jdbc.DatabaseDriver; +import org.springframework.boot.actuate.health.Status; import org.springframework.dao.support.DataAccessUtils; import org.springframework.jdbc.IncorrectResultSetColumnCountException; import org.springframework.jdbc.core.ConnectionCallback; @@ -51,8 +51,6 @@ */ public class DataSourceHealthIndicator extends AbstractHealthIndicator implements InitializingBean { - private static final String DEFAULT_QUERY = "SELECT 1"; - private DataSource dataSource; private String query; @@ -104,19 +102,19 @@ protected void doHealthCheck(Health.Builder builder) throws Exception { } private void doDataSourceHealthCheck(Health.Builder builder) throws Exception { - String product = getProduct(); - builder.up().withDetail("database", product); - String validationQuery = getValidationQuery(product); + builder.up().withDetail("database", getProduct()); + String validationQuery = this.query; if (StringUtils.hasText(validationQuery)) { - try { - // Avoid calling getObject as it breaks MySQL on Java 7 - List results = this.jdbcTemplate.query(validationQuery, new SingleColumnRowMapper()); - Object result = DataAccessUtils.requiredSingleResult(results); - builder.withDetail("result", result); - } - finally { - builder.withDetail("validationQuery", validationQuery); - } + builder.withDetail("validationQuery", validationQuery); + // Avoid calling getObject as it breaks MySQL on Java 7 and later + List results = this.jdbcTemplate.query(validationQuery, new SingleColumnRowMapper()); + Object result = DataAccessUtils.requiredSingleResult(results); + builder.withDetail("result", result); + } + else { + builder.withDetail("validationQuery", "isValid()"); + boolean valid = isConnectionValid(); + builder.status((valid) ? Status.UP : Status.DOWN); } } @@ -128,16 +126,12 @@ private String getProduct(Connection connection) throws SQLException { return connection.getMetaData().getDatabaseProductName(); } - protected String getValidationQuery(String product) { - String query = this.query; - if (!StringUtils.hasText(query)) { - DatabaseDriver specific = DatabaseDriver.fromProductName(product); - query = specific.getValidationQuery(); - } - if (!StringUtils.hasText(query)) { - query = DEFAULT_QUERY; - } - return query; + private Boolean isConnectionValid() { + return this.jdbcTemplate.execute((ConnectionCallback) this::isConnectionValid); + } + + private Boolean isConnectionValid(Connection connection) throws SQLException { + return connection.isValid(0); } /** @@ -151,8 +145,8 @@ public void setDataSource(DataSource dataSource) { /** * Set a specific validation query to use to validate a connection. If none is set, a - * default validation query is used. - * @param query the query + * validation based on {@link Connection#isValid(int)} is used. + * @param query the validation query to use */ public void setQuery(String query) { this.query = query; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/liquibase/LiquibaseEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/liquibase/LiquibaseEndpoint.java index 40a5d029aa6c..da4222bb8628 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/liquibase/LiquibaseEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/liquibase/LiquibaseEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,6 @@ import javax.sql.DataSource; -import liquibase.changelog.ChangeLogHistoryService; import liquibase.changelog.ChangeSet.ExecType; import liquibase.changelog.RanChangeSet; import liquibase.changelog.StandardChangeLogHistoryService; @@ -63,9 +62,8 @@ public ApplicationLiquibaseBeans liquibaseBeans() { while (target != null) { Map liquibaseBeans = new HashMap<>(); DatabaseFactory factory = DatabaseFactory.getInstance(); - StandardChangeLogHistoryService service = new StandardChangeLogHistoryService(); - this.context.getBeansOfType(SpringLiquibase.class) - .forEach((name, liquibase) -> liquibaseBeans.put(name, createReport(liquibase, service, factory))); + target.getBeansOfType(SpringLiquibase.class) + .forEach((name, liquibase) -> liquibaseBeans.put(name, createReport(liquibase, factory))); ApplicationContext parent = target.getParent(); contextBeans.put(target.getId(), new ContextLiquibaseBeans(liquibaseBeans, (parent != null) ? parent.getId() : null)); @@ -74,8 +72,7 @@ public ApplicationLiquibaseBeans liquibaseBeans() { return new ApplicationLiquibaseBeans(contextBeans); } - private LiquibaseBean createReport(SpringLiquibase liquibase, ChangeLogHistoryService service, - DatabaseFactory factory) { + private LiquibaseBean createReport(SpringLiquibase liquibase, DatabaseFactory factory) { try { DataSource dataSource = liquibase.getDataSource(); JdbcConnection connection = new JdbcConnection(dataSource.getConnection()); @@ -88,6 +85,7 @@ private LiquibaseBean createReport(SpringLiquibase liquibase, ChangeLogHistorySe } database.setDatabaseChangeLogTableName(liquibase.getDatabaseChangeLogTable()); database.setDatabaseChangeLogLockTableName(liquibase.getDatabaseChangeLogLockTable()); + StandardChangeLogHistoryService service = new StandardChangeLogHistoryService(); service.setDatabase(database); return new LiquibaseBean( service.getRanChangeSets().stream().map(ChangeSet::new).collect(Collectors.toList())); diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/management/HeapDumpWebEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/management/HeapDumpWebEndpoint.java index c2f9fee1fb1d..51d64b258c2b 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/management/HeapDumpWebEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/management/HeapDumpWebEndpoint.java @@ -27,8 +27,8 @@ import java.nio.ByteBuffer; import java.nio.channels.ReadableByteChannel; import java.nio.file.Files; -import java.text.SimpleDateFormat; -import java.util.Date; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -106,7 +106,7 @@ private Resource dumpHeap(boolean live) throws IOException, InterruptedException } private File createTempFile(boolean live) throws IOException { - String date = new SimpleDateFormat("yyyy-MM-dd-HH-mm").format(new Date()); + String date = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm").format(LocalDateTime.now()); File file = File.createTempFile("heapdump" + date + (live ? "-live" : ""), ".hprof"); file.delete(); return file; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/management/PlainTextThreadDumpFormatter.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/management/PlainTextThreadDumpFormatter.java index 50e801c2497c..80569a79675e 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/management/PlainTextThreadDumpFormatter.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/management/PlainTextThreadDumpFormatter.java @@ -23,8 +23,8 @@ import java.lang.management.MonitorInfo; import java.lang.management.RuntimeMXBean; import java.lang.management.ThreadInfo; -import java.text.SimpleDateFormat; -import java.util.Date; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -47,8 +47,8 @@ String format(ThreadInfo[] threads) { } private void writePreamble(PrintWriter writer) { - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - writer.println(dateFormat.format(new Date())); + DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + writer.println(dateFormat.format(LocalDateTime.now())); RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean(); writer.printf("Full thread dump %s (%s %s):%n", runtime.getVmName(), runtime.getVmVersion(), System.getProperty("java.vm.info")); diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/AutoTimer.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/AutoTimer.java index 33ed81f0708a..4484d749e596 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/AutoTimer.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/AutoTimer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,16 @@ package org.springframework.boot.actuate.metrics; +import java.util.Set; +import java.util.function.Consumer; import java.util.function.Supplier; import io.micrometer.core.annotation.Timed; import io.micrometer.core.instrument.Timer; import io.micrometer.core.instrument.Timer.Builder; +import org.springframework.util.CollectionUtils; + /** * Strategy that can be used to apply {@link Timer Timers} automatically instead of using * {@link Timed @Timed}. @@ -68,7 +72,7 @@ default boolean isEnabled() { /** * Factory method to create a new {@link Builder Timer.Builder} with auto-timer - * settings {@link #apply(Builder) applied}. + * settings {@link #apply(Timer.Builder) applied}. * @param name the name of the timer * @return a new builder instance with auto-settings applied */ @@ -78,7 +82,7 @@ default Timer.Builder builder(String name) { /** * Factory method to create a new {@link Builder Timer.Builder} with auto-timer - * settings {@link #apply(Builder) applied}. + * settings {@link #apply(Timer.Builder) applied}. * @param supplier the builder supplier * @return a new builder instance with auto-settings applied */ @@ -94,4 +98,17 @@ default Timer.Builder builder(Supplier supplier) { */ void apply(Timer.Builder builder); + static void apply(AutoTimer autoTimer, String metricName, Set annotations, Consumer action) { + if (!CollectionUtils.isEmpty(annotations)) { + for (Timed annotation : annotations) { + action.accept(Timer.builder(annotation, metricName)); + } + } + else { + if (autoTimer != null && autoTimer.isEnabled()) { + action.accept(autoTimer.builder(metricName)); + } + } + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/MetricsEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/MetricsEndpoint.java index 1295eef94982..8e88b241407b 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/MetricsEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/MetricsEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,10 +21,10 @@ import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeSet; import java.util.function.BiFunction; import java.util.stream.Collectors; @@ -58,7 +58,7 @@ public MetricsEndpoint(MeterRegistry registry) { @ReadOperation public ListNamesResponse listNames() { - Set names = new LinkedHashSet<>(); + Set names = new TreeSet<>(); collectNames(names, this.registry); return new ListNamesResponse(names); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/annotation/TimedAnnotations.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/annotation/TimedAnnotations.java new file mode 100644 index 000000000000..67e6c4beeb87 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/annotation/TimedAnnotations.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.annotation; + +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import io.micrometer.core.annotation.Timed; + +import org.springframework.core.annotation.MergedAnnotationCollectors; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.util.ConcurrentReferenceHashMap; + +/** + * Utility used to obtain {@link Timed @Timed} annotations from bean methods. + * + * @author Phillip Webb + * @since 2.5.0 + */ +public final class TimedAnnotations { + + private static final Map> cache = new ConcurrentReferenceHashMap<>(); + + private TimedAnnotations() { + } + + /** + * Return {@link Timed} annotations that should be used for the given {@code method} + * and {@code type}. + * @param method the source method + * @param type the source type + * @return the {@link Timed} annotations to use or an empty set + */ + public static Set get(Method method, Class type) { + Set methodAnnotations = findTimedAnnotations(method); + if (!methodAnnotations.isEmpty()) { + return methodAnnotations; + } + return findTimedAnnotations(type); + } + + private static Set findTimedAnnotations(AnnotatedElement element) { + if (element == null) { + return Collections.emptySet(); + } + Set result = cache.get(element); + if (result != null) { + return result; + } + MergedAnnotations annotations = MergedAnnotations.from(element); + result = (!annotations.isPresent(Timed.class)) ? Collections.emptySet() + : annotations.stream(Timed.class).collect(MergedAnnotationCollectors.toAnnotationSet()); + cache.put(element, result); + return result; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/annotation/package-info.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/annotation/package-info.java new file mode 100644 index 000000000000..2903dd44bc93 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/annotation/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Support classes for handler method metrics. + */ +package org.springframework.boot.actuate.metrics.annotation; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/cache/HazelcastCacheMeterBinderProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/cache/HazelcastCacheMeterBinderProvider.java index 8319ecabc5e1..8688fe72a7c9 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/cache/HazelcastCacheMeterBinderProvider.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/cache/HazelcastCacheMeterBinderProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,15 @@ package org.springframework.boot.actuate.metrics.cache; +import java.lang.reflect.Method; + import com.hazelcast.spring.cache.HazelcastCache; import io.micrometer.core.instrument.Tag; import io.micrometer.core.instrument.binder.MeterBinder; import io.micrometer.core.instrument.binder.cache.HazelcastCacheMetrics; +import org.springframework.util.ReflectionUtils; + /** * {@link CacheMeterBinderProvider} implementation for Hazelcast. * @@ -31,7 +35,25 @@ public class HazelcastCacheMeterBinderProvider implements CacheMeterBinderProvid @Override public MeterBinder getMeterBinder(HazelcastCache cache, Iterable tags) { - return new HazelcastCacheMetrics(cache.getNativeCache(), tags); + try { + return new HazelcastCacheMetrics(cache.getNativeCache(), tags); + } + catch (NoSuchMethodError ex) { + // Hazelcast 4 + return createHazelcast4CacheMetrics(cache, tags); + } + } + + private MeterBinder createHazelcast4CacheMetrics(HazelcastCache cache, Iterable tags) { + try { + Method nativeCacheAccessor = ReflectionUtils.findMethod(HazelcastCache.class, "getNativeCache"); + Object nativeCache = ReflectionUtils.invokeMethod(nativeCacheAccessor, cache); + return HazelcastCacheMetrics.class.getConstructor(Object.class, Iterable.class).newInstance(nativeCache, + tags); + } + catch (Exception ex) { + throw new IllegalStateException("Failed to create MeterBinder for Hazelcast", ex); + } } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/cache/RedisCacheMeterBinderProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/cache/RedisCacheMeterBinderProvider.java new file mode 100644 index 000000000000..a31aea3807ee --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/cache/RedisCacheMeterBinderProvider.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.cache; + +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.binder.MeterBinder; + +import org.springframework.data.redis.cache.RedisCache; + +/** + * {@link CacheMeterBinderProvider} implementation for Redis. + * + * @author Stephane Nicoll + * @since 2.4.0 + */ +public class RedisCacheMeterBinderProvider implements CacheMeterBinderProvider { + + @Override + public MeterBinder getMeterBinder(RedisCache cache, Iterable tags) { + return new RedisCacheMetrics(cache, tags); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/cache/RedisCacheMetrics.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/cache/RedisCacheMetrics.java new file mode 100644 index 000000000000..8ede30d9d8c9 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/cache/RedisCacheMetrics.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.cache; + +import java.util.concurrent.TimeUnit; + +import io.micrometer.core.instrument.FunctionCounter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.TimeGauge; +import io.micrometer.core.instrument.binder.cache.CacheMeterBinder; + +import org.springframework.data.redis.cache.RedisCache; + +/** + * {@link CacheMeterBinder} for {@link RedisCache}. + * + * @author Stephane Nicoll + * @since 2.4.0 + */ +public class RedisCacheMetrics extends CacheMeterBinder { + + private final RedisCache cache; + + public RedisCacheMetrics(RedisCache cache, Iterable tags) { + super(cache, cache.getName(), tags); + this.cache = cache; + } + + @Override + protected Long size() { + return null; + } + + @Override + protected long hitCount() { + return this.cache.getStatistics().getHits(); + } + + @Override + protected Long missCount() { + return this.cache.getStatistics().getMisses(); + } + + @Override + protected Long evictionCount() { + return null; + } + + @Override + protected long putCount() { + return this.cache.getStatistics().getPuts(); + } + + @Override + protected void bindImplementationSpecificMetrics(MeterRegistry registry) { + FunctionCounter.builder("cache.removals", this.cache, (cache) -> cache.getStatistics().getDeletes()) + .tags(getTagsWithCacheName()).description("Cache removals").register(registry); + FunctionCounter.builder("cache.gets", this.cache, (cache) -> cache.getStatistics().getPending()) + .tags(getTagsWithCacheName()).tag("result", "pending").description("The number of pending requests") + .register(registry); + TimeGauge + .builder("cache.lock.duration", this.cache, TimeUnit.NANOSECONDS, + (cache) -> cache.getStatistics().getLockWaitDuration(TimeUnit.NANOSECONDS)) + .tags(getTagsWithCacheName()).description("The time the cache has spent waiting on a lock") + .register(registry); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/data/DefaultRepositoryTagsProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/data/DefaultRepositoryTagsProvider.java new file mode 100644 index 000000000000..801feffa3bb7 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/data/DefaultRepositoryTagsProvider.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.data; + +import java.lang.reflect.Method; +import java.util.function.Function; + +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; + +import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocation; +import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocationResult.State; +import org.springframework.util.StringUtils; + +/** + * Default {@link RepositoryTagsProvider} implementation. + * + * @author Phillip Webb + * @since 2.5.0 + */ +public class DefaultRepositoryTagsProvider implements RepositoryTagsProvider { + + private static final Tag EXCEPTION_NONE = Tag.of("exception", "None"); + + @Override + public Iterable repositoryTags(RepositoryMethodInvocation invocation) { + Tags tags = Tags.empty(); + tags = and(tags, invocation.getRepositoryInterface(), "repository", this::getSimpleClassName); + tags = and(tags, invocation.getMethod(), "method", Method::getName); + tags = and(tags, invocation.getResult().getState(), "state", State::name); + tags = and(tags, invocation.getResult().getError(), "exception", this::getExceptionName, EXCEPTION_NONE); + return tags; + } + + private Tags and(Tags tags, T instance, String key, Function value) { + return and(tags, instance, key, value, null); + } + + private Tags and(Tags tags, T instance, String key, Function value, Tag fallback) { + if (instance != null) { + return tags.and(key, value.apply(instance)); + } + return (fallback != null) ? tags.and(fallback) : tags; + } + + private String getExceptionName(Throwable error) { + return getSimpleClassName(error.getClass()); + } + + private String getSimpleClassName(Class type) { + String simpleName = type.getSimpleName(); + return (!StringUtils.hasText(simpleName)) ? type.getName() : simpleName; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/data/MetricsRepositoryMethodInvocationListener.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/data/MetricsRepositoryMethodInvocationListener.java new file mode 100644 index 000000000000..add03dc69179 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/data/MetricsRepositoryMethodInvocationListener.java @@ -0,0 +1,90 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.data; + +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +import io.micrometer.core.annotation.Timed; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; + +import org.springframework.boot.actuate.metrics.AutoTimer; +import org.springframework.boot.actuate.metrics.annotation.TimedAnnotations; +import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener; +import org.springframework.util.function.SingletonSupplier; + +/** + * Intercepts Spring Data {@code Repository} invocations and records metrics about + * execution time and results. + * + * @author Phillip Webb + * @since 2.5.0 + */ +public class MetricsRepositoryMethodInvocationListener implements RepositoryMethodInvocationListener { + + private final SingletonSupplier registrySupplier; + + private final RepositoryTagsProvider tagsProvider; + + private final String metricName; + + private final AutoTimer autoTimer; + + /** + * Create a new {@code MetricsRepositoryMethodInvocationListener}. + * @param registry the registry to which metrics are recorded + * @param tagsProvider provider for metrics tags + * @param metricName name of the metric to record + * @param autoTimer the auto-timers to apply or {@code null} to disable auto-timing + * @deprecated since 2.5.4 for removal in 2.7.0 in favor of + * {@link #MetricsRepositoryMethodInvocationListener(Supplier, RepositoryTagsProvider, String, AutoTimer)} + */ + @Deprecated + public MetricsRepositoryMethodInvocationListener(MeterRegistry registry, RepositoryTagsProvider tagsProvider, + String metricName, AutoTimer autoTimer) { + this(SingletonSupplier.of(registry), tagsProvider, metricName, autoTimer); + } + + /** + * Create a new {@code MetricsRepositoryMethodInvocationListener}. + * @param registrySupplier a supplier for the registry to which metrics are recorded + * @param tagsProvider provider for metrics tags + * @param metricName name of the metric to record + * @param autoTimer the auto-timers to apply or {@code null} to disable auto-timing + * @since 2.5.4 + */ + public MetricsRepositoryMethodInvocationListener(Supplier registrySupplier, + RepositoryTagsProvider tagsProvider, String metricName, AutoTimer autoTimer) { + this.registrySupplier = (registrySupplier instanceof SingletonSupplier) + ? (SingletonSupplier) registrySupplier : SingletonSupplier.of(registrySupplier); + this.tagsProvider = tagsProvider; + this.metricName = metricName; + this.autoTimer = (autoTimer != null) ? autoTimer : AutoTimer.DISABLED; + } + + @Override + public void afterInvocation(RepositoryMethodInvocation invocation) { + Set annotations = TimedAnnotations.get(invocation.getMethod(), invocation.getRepositoryInterface()); + Iterable tags = this.tagsProvider.repositoryTags(invocation); + long duration = invocation.getDuration(TimeUnit.NANOSECONDS); + AutoTimer.apply(this.autoTimer, this.metricName, annotations, (builder) -> builder.tags(tags) + .register(this.registrySupplier.get()).record(duration, TimeUnit.NANOSECONDS)); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/data/RepositoryTagsProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/data/RepositoryTagsProvider.java new file mode 100644 index 000000000000..4f6b55f94357 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/data/RepositoryTagsProvider.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.data; + +import io.micrometer.core.instrument.Tag; + +import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocation; + +/** + * Provides {@link Tag Tags} for Spring Data {@link RepositoryMethodInvocation Repository + * invocations}. + * + * @author Phillip Webb + * @since 2.5.0 + */ +@FunctionalInterface +public interface RepositoryTagsProvider { + + /** + * Provides tags to be associated with metrics for the given {@code invocation}. + * @param invocation the repository invocation + * @return tags to associate with metrics for the invocation + */ + Iterable repositoryTags(RepositoryMethodInvocation invocation); + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/data/package-info.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/data/package-info.java new file mode 100644 index 000000000000..90e4f3bb2e21 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/data/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Actuator support for Spring Data Repository metrics. + */ +package org.springframework.boot.actuate.metrics.data; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusPushGatewayManager.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusPushGatewayManager.java index 1960ed6e0750..466612baf817 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusPushGatewayManager.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusPushGatewayManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.springframework.boot.actuate.metrics.export.prometheus; -import java.net.UnknownHostException; import java.time.Duration; import java.util.Map; import java.util.concurrent.Executors; @@ -31,7 +30,6 @@ import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.util.Assert; -import org.springframework.util.StringUtils; /** * Class that can be used to manage the pushing of metrics to a {@link PushGateway @@ -107,16 +105,8 @@ private void push() { try { this.pushGateway.pushAdd(this.registry, this.job, this.groupingKey); } - catch (UnknownHostException ex) { - String host = ex.getMessage(); - String message = "Unable to locate prometheus push gateway host" - + (StringUtils.hasLength(host) ? " '" + host + "'" : "") - + ". No longer attempting metrics publication to this host"; - logger.error(message, ex); - shutdown(ShutdownOperation.NONE); - } catch (Throwable ex) { - logger.error("Unable to push metrics to Prometheus Pushgateway", ex); + logger.warn("Unexpected exception thrown while pushing metrics to Prometheus Pushgateway", ex); } } @@ -125,7 +115,7 @@ private void delete() { this.pushGateway.delete(this.job, this.groupingKey); } catch (Throwable ex) { - logger.error("Unable to delete metrics from Prometheus Pushgateway", ex); + logger.warn("Unexpected exception thrown while deleting metrics from Promethues Pushgateway", ex); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpoint.java index 48afc50f4eba..89cc2822d3e0 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,41 +19,56 @@ import java.io.IOException; import java.io.StringWriter; import java.io.Writer; +import java.util.Enumeration; +import java.util.Set; +import io.prometheus.client.Collector.MetricFamilySamples; import io.prometheus.client.CollectorRegistry; -import io.prometheus.client.exporter.common.TextFormat; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint; +import org.springframework.lang.Nullable; /** * {@link Endpoint @Endpoint} that outputs metrics in a format that can be scraped by the * Prometheus server. * * @author Jon Schneider + * @author Johnny Lim * @since 2.0.0 */ @WebEndpoint(id = "prometheus") public class PrometheusScrapeEndpoint { + private static final int METRICS_SCRAPE_CHARS_EXTRA = 1024; + private final CollectorRegistry collectorRegistry; + private volatile int nextMetricsScrapeSize = 16; + public PrometheusScrapeEndpoint(CollectorRegistry collectorRegistry) { this.collectorRegistry = collectorRegistry; } - @ReadOperation(produces = TextFormat.CONTENT_TYPE_004) - public String scrape() { + @ReadOperation(producesFrom = TextOutputFormat.class) + public WebEndpointResponse scrape(TextOutputFormat format, @Nullable Set includedNames) { try { - Writer writer = new StringWriter(); - TextFormat.write004(writer, this.collectorRegistry.metricFamilySamples()); - return writer.toString(); + Writer writer = new StringWriter(this.nextMetricsScrapeSize); + Enumeration samples = (includedNames != null) + ? this.collectorRegistry.filteredMetricFamilySamples(includedNames) + : this.collectorRegistry.metricFamilySamples(); + format.write(writer, samples); + + String scrapePage = writer.toString(); + this.nextMetricsScrapeSize = scrapePage.length() + METRICS_SCRAPE_CHARS_EXTRA; + + return new WebEndpointResponse<>(scrapePage, format); } catch (IOException ex) { - // This actually never happens since StringWriter::write() doesn't throw any - // IOException - throw new RuntimeException("Writing metrics failed", ex); + // This actually never happens since StringWriter doesn't throw an IOException + throw new IllegalStateException("Writing metrics failed", ex); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/TextOutputFormat.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/TextOutputFormat.java new file mode 100644 index 000000000000..54b16b7110c9 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/TextOutputFormat.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.export.prometheus; + +import java.io.IOException; +import java.io.Writer; +import java.util.Enumeration; + +import io.prometheus.client.Collector.MetricFamilySamples; +import io.prometheus.client.exporter.common.TextFormat; + +import org.springframework.boot.actuate.endpoint.Producible; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; + +/** + * A {@link Producible} enum for supported Prometheus {@link TextFormat}. + * + * @author Andy Wilkinson + * @since 2.5.0 + */ +public enum TextOutputFormat implements Producible { + + /** + * Prometheus text version 0.0.4. + */ + CONTENT_TYPE_004(TextFormat.CONTENT_TYPE_004) { + + @Override + void write(Writer writer, Enumeration samples) throws IOException { + TextFormat.write004(writer, samples); + } + + @Override + public boolean isDefault() { + return true; + } + + }, + + /** + * OpenMetrics text version 1.0.0. + */ + CONTENT_TYPE_OPENMETRICS_100(TextFormat.CONTENT_TYPE_OPENMETRICS_100) { + + @Override + void write(Writer writer, Enumeration samples) throws IOException { + TextFormat.writeOpenMetrics100(writer, samples); + } + + }; + + private final MimeType mimeType; + + TextOutputFormat(String mimeType) { + this.mimeType = MimeTypeUtils.parseMimeType(mimeType); + } + + @Override + public MimeType getProducedMimeType() { + return this.mimeType; + } + + abstract void write(Writer writer, Enumeration samples) throws IOException; + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/http/package-info.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/http/package-info.java index 3675c4bbe776..15cd9b28d012 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/http/package-info.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/http/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,6 @@ */ /** - * Support classes HTTP-related metrics. + * Support classes for HTTP-related metrics. */ package org.springframework.boot.actuate.metrics.http; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jdbc/DataSourcePoolMetrics.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jdbc/DataSourcePoolMetrics.java index 92cf4243b2d4..3ebdccd0fff9 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jdbc/DataSourcePoolMetrics.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jdbc/DataSourcePoolMetrics.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import javax.sql.DataSource; +import io.micrometer.core.instrument.Gauge; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Tag; import io.micrometer.core.instrument.Tags; @@ -65,23 +66,29 @@ public DataSourcePoolMetrics(DataSource dataSource, DataSourcePoolMetadataProvid @Override public void bindTo(MeterRegistry registry) { if (this.metadataProvider.getDataSourcePoolMetadata(this.dataSource) != null) { - bindPoolMetadata(registry, "active", DataSourcePoolMetadata::getActive); - bindPoolMetadata(registry, "idle", DataSourcePoolMetadata::getIdle); - bindPoolMetadata(registry, "max", DataSourcePoolMetadata::getMax); - bindPoolMetadata(registry, "min", DataSourcePoolMetadata::getMin); + bindPoolMetadata(registry, "active", + "Current number of active connections that have been allocated from the data source.", + DataSourcePoolMetadata::getActive); + bindPoolMetadata(registry, "idle", "Number of established but idle connections.", + DataSourcePoolMetadata::getIdle); + bindPoolMetadata(registry, "max", + "Maximum number of active connections that can be allocated at the same time.", + DataSourcePoolMetadata::getMax); + bindPoolMetadata(registry, "min", "Minimum number of idle connections in the pool.", + DataSourcePoolMetadata::getMin); } } - private void bindPoolMetadata(MeterRegistry registry, String metricName, + private void bindPoolMetadata(MeterRegistry registry, String metricName, String description, Function function) { - bindDataSource(registry, metricName, this.metadataProvider.getValueFunction(function)); + bindDataSource(registry, metricName, description, this.metadataProvider.getValueFunction(function)); } - private void bindDataSource(MeterRegistry registry, String metricName, + private void bindDataSource(MeterRegistry registry, String metricName, String description, Function function) { if (function.apply(this.dataSource) != null) { - registry.gauge("jdbc.connections." + metricName, this.tags, this.dataSource, - (m) -> function.apply(m).doubleValue()); + Gauge.builder("jdbc.connections." + metricName, this.dataSource, (m) -> function.apply(m).doubleValue()) + .tags(this.tags).description(description).register(registry); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/r2dbc/ConnectionPoolMetrics.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/r2dbc/ConnectionPoolMetrics.java new file mode 100644 index 000000000000..845ceae8eef9 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/r2dbc/ConnectionPoolMetrics.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.r2dbc; + +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.Gauge.Builder; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.binder.MeterBinder; +import io.r2dbc.pool.ConnectionPool; +import io.r2dbc.pool.PoolMetrics; + +/** + * A {@link MeterBinder} for a {@link ConnectionPool}. + * + * @author Tadaya Tsuyukubo + * @author Stephane Nicoll + * @since 2.3.0 + */ +public class ConnectionPoolMetrics implements MeterBinder { + + private static final String CONNECTIONS = "connections"; + + private final ConnectionPool pool; + + private final Iterable tags; + + public ConnectionPoolMetrics(ConnectionPool pool, String name, Iterable tags) { + this.pool = pool; + this.tags = Tags.concat(tags, "name", name); + } + + @Override + public void bindTo(MeterRegistry registry) { + this.pool.getMetrics().ifPresent((poolMetrics) -> { + bindConnectionPoolMetric(registry, + Gauge.builder(metricKey("acquired"), poolMetrics, PoolMetrics::acquiredSize) + .description("Size of successfully acquired connections which are in active use.")); + bindConnectionPoolMetric(registry, + Gauge.builder(metricKey("allocated"), poolMetrics, PoolMetrics::allocatedSize) + .description("Size of allocated connections in the pool which are in active use or idle.")); + bindConnectionPoolMetric(registry, Gauge.builder(metricKey("idle"), poolMetrics, PoolMetrics::idleSize) + .description("Size of idle connections in the pool.")); + bindConnectionPoolMetric(registry, + Gauge.builder(metricKey("pending"), poolMetrics, PoolMetrics::pendingAcquireSize).description( + "Size of pending to acquire connections from the underlying connection factory.")); + bindConnectionPoolMetric(registry, + Gauge.builder(metricKey("max.allocated"), poolMetrics, PoolMetrics::getMaxAllocatedSize) + .description("Maximum size of allocated connections that this pool allows.")); + bindConnectionPoolMetric(registry, + Gauge.builder(metricKey("max.pending"), poolMetrics, PoolMetrics::getMaxPendingAcquireSize) + .description( + "Maximum size of pending state to acquire connections that this pool allows.")); + }); + } + + private void bindConnectionPoolMetric(MeterRegistry registry, Builder builder) { + builder.tags(this.tags).baseUnit(CONNECTIONS).register(registry); + } + + private static String metricKey(String name) { + return "r2dbc.pool." + name; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/r2dbc/package-info.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/r2dbc/package-info.java new file mode 100644 index 000000000000..d0545dbb1615 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/r2dbc/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Actuator support for R2DBC metrics. + */ +package org.springframework.boot.actuate.metrics.r2dbc; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/client/MetricsClientHttpRequestInterceptor.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/client/MetricsClientHttpRequestInterceptor.java index b5e689866a47..8da5d69a4847 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/client/MetricsClientHttpRequestInterceptor.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/client/MetricsClientHttpRequestInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,13 +18,18 @@ import java.io.IOException; import java.net.URI; +import java.util.Deque; +import java.util.LinkedList; import java.util.Map; import java.util.concurrent.TimeUnit; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Timer; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.boot.actuate.metrics.AutoTimer; +import org.springframework.boot.web.client.RootUriTemplateHandler; import org.springframework.core.NamedThreadLocal; import org.springframework.http.HttpRequest; import org.springframework.http.client.ClientHttpRequestExecution; @@ -41,7 +46,9 @@ */ class MetricsClientHttpRequestInterceptor implements ClientHttpRequestInterceptor { - private static final ThreadLocal urlTemplate = new NamedThreadLocal<>("Rest Template URL Template"); + private static final Log logger = LogFactory.getLog(MetricsClientHttpRequestInterceptor.class); + + private static final ThreadLocal> urlTemplate = new UrlTemplateThreadLocal(); private final MeterRegistry meterRegistry; @@ -51,20 +58,6 @@ class MetricsClientHttpRequestInterceptor implements ClientHttpRequestIntercepto private final AutoTimer autoTimer; - /** - * Create a new {@code MetricsClientHttpRequestInterceptor}. - * @param meterRegistry the registry to which metrics are recorded - * @param tagProvider provider for metrics tags - * @param metricName name of the metric to record - * @deprecated since 2.2.0 in favor of - * {@link #MetricsClientHttpRequestInterceptor(MeterRegistry, RestTemplateExchangeTagsProvider, String, AutoTimer)} - */ - @Deprecated - MetricsClientHttpRequestInterceptor(MeterRegistry meterRegistry, RestTemplateExchangeTagsProvider tagProvider, - String metricName) { - this(meterRegistry, tagProvider, metricName, AutoTimer.ENABLED); - } - /** * Create a new {@code MetricsClientHttpRequestInterceptor}. * @param meterRegistry the registry to which metrics are recorded @@ -84,7 +77,7 @@ class MetricsClientHttpRequestInterceptor implements ClientHttpRequestIntercepto @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { - if (!this.autoTimer.isEnabled()) { + if (!enabled()) { return execution.execute(request, body); } long startTime = System.nanoTime(); @@ -94,34 +87,73 @@ public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttp return response; } finally { - getTimeBuilder(request, response).register(this.meterRegistry).record(System.nanoTime() - startTime, - TimeUnit.NANOSECONDS); - urlTemplate.remove(); + try { + getTimeBuilder(request, response).register(this.meterRegistry).record(System.nanoTime() - startTime, + TimeUnit.NANOSECONDS); + } + catch (Exception ex) { + logger.info("Failed to record metrics.", ex); + } + if (urlTemplate.get().isEmpty()) { + urlTemplate.remove(); + } } } + private boolean enabled() { + return this.autoTimer.isEnabled(); + } + UriTemplateHandler createUriTemplateHandler(UriTemplateHandler delegate) { - return new UriTemplateHandler() { + if (delegate instanceof RootUriTemplateHandler) { + return ((RootUriTemplateHandler) delegate).withHandlerWrapper(CapturingUriTemplateHandler::new); + } + return new CapturingUriTemplateHandler(delegate); + } + + private Timer.Builder getTimeBuilder(HttpRequest request, ClientHttpResponse response) { + return this.autoTimer.builder(this.metricName) + .tags(this.tagProvider.getTags(urlTemplate.get().poll(), request, response)) + .description("Timer of RestTemplate operation"); + } + + private final class CapturingUriTemplateHandler implements UriTemplateHandler { - @Override - public URI expand(String url, Map arguments) { - urlTemplate.set(url); - return delegate.expand(url, arguments); + private final UriTemplateHandler delegate; + + private CapturingUriTemplateHandler(UriTemplateHandler delegate) { + this.delegate = delegate; + } + + @Override + public URI expand(String url, Map arguments) { + if (enabled()) { + urlTemplate.get().push(url); } + return this.delegate.expand(url, arguments); + } - @Override - public URI expand(String url, Object... arguments) { - urlTemplate.set(url); - return delegate.expand(url, arguments); + @Override + public URI expand(String url, Object... arguments) { + if (enabled()) { + urlTemplate.get().push(url); } + return this.delegate.expand(url, arguments); + } - }; } - private Timer.Builder getTimeBuilder(HttpRequest request, ClientHttpResponse response) { - return this.autoTimer.builder(this.metricName) - .tags(this.tagProvider.getTags(urlTemplate.get(), request, response)) - .description("Timer of RestTemplate operation"); + private static final class UrlTemplateThreadLocal extends NamedThreadLocal> { + + private UrlTemplateThreadLocal() { + super("Rest Template URL Template"); + } + + @Override + protected Deque initialValue() { + return new LinkedList<>(); + } + } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/client/MetricsRestTemplateCustomizer.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/client/MetricsRestTemplateCustomizer.java index 889d9b28d0ba..1cacb6416459 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/client/MetricsRestTemplateCustomizer.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/client/MetricsRestTemplateCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,22 +39,6 @@ public class MetricsRestTemplateCustomizer implements RestTemplateCustomizer { private final MetricsClientHttpRequestInterceptor interceptor; - /** - * Creates a new {@code MetricsRestTemplateInterceptor} that will record metrics using - * the given {@code meterRegistry} with tags provided by the given - * {@code tagProvider}. - * @param meterRegistry the meter registry - * @param tagProvider the tag provider - * @param metricName the name of the recorded metric - * @deprecated since 2.2.0 in favor of - * {@link #MetricsRestTemplateCustomizer(MeterRegistry, RestTemplateExchangeTagsProvider, String, AutoTimer)} - */ - @Deprecated - public MetricsRestTemplateCustomizer(MeterRegistry meterRegistry, RestTemplateExchangeTagsProvider tagProvider, - String metricName) { - this(meterRegistry, tagProvider, metricName, AutoTimer.ENABLED); - } - /** * Creates a new {@code MetricsRestTemplateInterceptor}. When {@code autoTimeRequests} * is set to {@code true}, the interceptor records metrics using the given diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/DefaultWebClientExchangeTagsProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/DefaultWebClientExchangeTagsProvider.java index b746b8ac4f6d..6ea9f0c01242 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/DefaultWebClientExchangeTagsProvider.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/DefaultWebClientExchangeTagsProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,9 +37,9 @@ public Iterable tags(ClientRequest request, ClientResponse response, Throwa Tag method = WebClientExchangeTags.method(request); Tag uri = WebClientExchangeTags.uri(request); Tag clientName = WebClientExchangeTags.clientName(request); - return Arrays.asList(method, uri, clientName, - (response != null) ? WebClientExchangeTags.status(response) : WebClientExchangeTags.status(throwable), - WebClientExchangeTags.outcome(response)); + Tag status = WebClientExchangeTags.status(response, throwable); + Tag outcome = WebClientExchangeTags.outcome(response); + return Arrays.asList(method, uri, clientName, status, outcome); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientCustomizer.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientCustomizer.java index 911c0c71bd12..5b4be59ba1ab 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientCustomizer.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,22 +33,6 @@ public class MetricsWebClientCustomizer implements WebClientCustomizer { private final MetricsWebClientFilterFunction filterFunction; - /** - * Create a new {@code MetricsWebClientFilterFunction} that will record metrics using - * the given {@code meterRegistry} with tags provided by the given - * {@code tagProvider}. - * @param meterRegistry the meter registry - * @param tagProvider the tag provider - * @param metricName the name of the recorded metric - * @deprecated since 2.2.0 in favor of - * {@link #MetricsWebClientCustomizer(MeterRegistry, WebClientExchangeTagsProvider, String, AutoTimer)} - */ - @Deprecated - public MetricsWebClientCustomizer(MeterRegistry meterRegistry, WebClientExchangeTagsProvider tagProvider, - String metricName) { - this(meterRegistry, tagProvider, metricName, AutoTimer.ENABLED); - } - /** * Create a new {@code MetricsWebClientFilterFunction} that will record metrics using * the given {@code meterRegistry} with tags provided by the given diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientFilterFunction.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientFilterFunction.java index 8cc9506cf4ed..836dd6548473 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientFilterFunction.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientFilterFunction.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,11 +17,14 @@ package org.springframework.boot.actuate.metrics.web.reactive.client; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Tag; import reactor.core.publisher.Mono; +import reactor.core.publisher.SignalType; import reactor.util.context.Context; +import reactor.util.context.ContextView; import org.springframework.boot.actuate.metrics.AutoTimer; import org.springframework.web.reactive.function.client.ClientRequest; @@ -50,20 +53,6 @@ public class MetricsWebClientFilterFunction implements ExchangeFilterFunction { private final AutoTimer autoTimer; - /** - * Create a new {@code MetricsWebClientFilterFunction}. - * @param meterRegistry the registry to which metrics are recorded - * @param tagProvider provider for metrics tags - * @param metricName name of the metric to record - * @deprecated since 2.2.0 in favor of - * {@link #MetricsWebClientFilterFunction(MeterRegistry, WebClientExchangeTagsProvider, String, AutoTimer)} - */ - @Deprecated - public MetricsWebClientFilterFunction(MeterRegistry meterRegistry, WebClientExchangeTagsProvider tagProvider, - String metricName) { - this(meterRegistry, tagProvider, metricName, AutoTimer.ENABLED); - } - /** * Create a new {@code MetricsWebClientFilterFunction}. * @param meterRegistry the registry to which metrics are recorded @@ -85,19 +74,32 @@ public Mono filter(ClientRequest request, ExchangeFunction next) if (!this.autoTimer.isEnabled()) { return next.exchange(request); } - return next.exchange(request).doOnEach((signal) -> { - if (!signal.isOnComplete()) { - Long startTime = getStartTime(signal.getContext()); - ClientResponse response = signal.get(); - Throwable throwable = signal.getThrowable(); - Iterable tags = this.tagProvider.tags(request, response, throwable); - this.autoTimer.builder(this.metricName).tags(tags).description("Timer of WebClient operation") - .register(this.meterRegistry).record(System.nanoTime() - startTime, TimeUnit.NANOSECONDS); + return next.exchange(request).as((responseMono) -> instrumentResponse(request, responseMono)) + .contextWrite(this::putStartTime); + } + + private Mono instrumentResponse(ClientRequest request, Mono responseMono) { + final AtomicBoolean responseReceived = new AtomicBoolean(); + return Mono.deferContextual((ctx) -> responseMono.doOnEach((signal) -> { + if (signal.isOnNext() || signal.isOnError()) { + responseReceived.set(true); + Iterable tags = this.tagProvider.tags(request, signal.get(), signal.getThrowable()); + recordTimer(tags, getStartTime(ctx)); } - }).subscriberContext(this::putStartTime); + }).doFinally((signalType) -> { + if (!responseReceived.get() && SignalType.CANCEL.equals(signalType)) { + Iterable tags = this.tagProvider.tags(request, null, null); + recordTimer(tags, getStartTime(ctx)); + } + })); + } + + private void recordTimer(Iterable tags, Long startTime) { + this.autoTimer.builder(this.metricName).tags(tags).description("Timer of WebClient operation") + .register(this.meterRegistry).record(System.nanoTime() - startTime, TimeUnit.NANOSECONDS); } - private Long getStartTime(Context context) { + private Long getStartTime(ContextView context) { return context.get(METRICS_WEBCLIENT_START_TIME); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTags.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTags.java index b6c0469a0866..6d0af88c6b7e 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTags.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTags.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -66,7 +66,7 @@ public static Tag method(ClientRequest request) { * @return the uri tag */ public static Tag uri(ClientRequest request) { - String uri = (String) request.attribute(URI_TEMPLATE_ATTRIBUTE).orElseGet(() -> request.url().getPath()); + String uri = (String) request.attribute(URI_TEMPLATE_ATTRIBUTE).orElseGet(() -> request.url().toString()); return Tag.of("uri", extractPath(uri)); } @@ -77,22 +77,21 @@ private static String extractPath(String url) { /** * Creates a {@code status} {@code Tag} derived from the - * {@link ClientResponse#statusCode()} of the given {@code response}. + * {@link ClientResponse#statusCode()} of the given {@code response} if available, the + * thrown exception otherwise, or considers the request as Cancelled as a last resort. * @param response the response - * @return the status tag - */ - public static Tag status(ClientResponse response) { - return Tag.of("status", String.valueOf(response.rawStatusCode())); - } - - /** - * Creates a {@code status} {@code Tag} derived from the exception thrown by the - * client. * @param throwable the exception * @return the status tag + * @since 2.3.0 */ - public static Tag status(Throwable throwable) { - return (throwable instanceof IOException) ? IO_ERROR : CLIENT_ERROR; + public static Tag status(ClientResponse response, Throwable throwable) { + if (response != null) { + return Tag.of("status", String.valueOf(response.rawStatusCode())); + } + if (throwable != null) { + return (throwable instanceof IOException) ? IO_ERROR : CLIENT_ERROR; + } + return CLIENT_ERROR; } /** diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/CancelledServerWebExchangeException.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/CancelledServerWebExchangeException.java new file mode 100644 index 000000000000..b0e6c40c7d58 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/CancelledServerWebExchangeException.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.web.reactive.server; + +/** + * Runtime exception that materializes a {@link reactor.core.publisher.SignalType#CANCEL + * cancel signal} for the WebFlux server metrics instrumentation. + * + * @author Brian Clozel + * @since 2.5.0 + * @see MetricsWebFilter + */ +public class CancelledServerWebExchangeException extends RuntimeException { + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/DefaultWebFluxTagsProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/DefaultWebFluxTagsProvider.java index fb54c496b9b0..c3407a3da738 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/DefaultWebFluxTagsProvider.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/DefaultWebFluxTagsProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,11 @@ package org.springframework.boot.actuate.metrics.web.reactive.server; -import java.util.Arrays; +import java.util.Collections; +import java.util.List; import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; import org.springframework.web.server.ServerWebExchange; @@ -31,10 +33,53 @@ */ public class DefaultWebFluxTagsProvider implements WebFluxTagsProvider { + private final boolean ignoreTrailingSlash; + + private final List contributors; + + public DefaultWebFluxTagsProvider() { + this(false); + } + + /** + * Creates a new {@link DefaultWebFluxTagsProvider} that will provide tags from the + * given {@code contributors} in addition to its own. + * @param contributors the contributors that will provide additional tags + * @since 2.3.0 + */ + public DefaultWebFluxTagsProvider(List contributors) { + this(false, contributors); + } + + public DefaultWebFluxTagsProvider(boolean ignoreTrailingSlash) { + this(ignoreTrailingSlash, Collections.emptyList()); + } + + /** + * Creates a new {@link DefaultWebFluxTagsProvider} that will provide tags from the + * given {@code contributors} in addition to its own. + * @param ignoreTrailingSlash whether trailing slashes should be ignored when + * determining the {@code uri} tag. + * @param contributors the contributors that will provide additional tags + * @since 2.3.0 + */ + public DefaultWebFluxTagsProvider(boolean ignoreTrailingSlash, List contributors) { + this.ignoreTrailingSlash = ignoreTrailingSlash; + this.contributors = contributors; + } + @Override public Iterable httpRequestTags(ServerWebExchange exchange, Throwable exception) { - return Arrays.asList(WebFluxTags.method(exchange), WebFluxTags.uri(exchange), WebFluxTags.exception(exception), - WebFluxTags.status(exchange), WebFluxTags.outcome(exchange)); + Tags tags = Tags.empty(); + tags = tags.and(WebFluxTags.method(exchange)); + tags = tags.and(WebFluxTags.uri(exchange, this.ignoreTrailingSlash)); + tags = tags.and(WebFluxTags.exception(exception)); + tags = tags.and(WebFluxTags.status(exchange)); + tags = tags.and(WebFluxTags.outcome(exchange, exception)); + for (WebFluxTagsContributor contributor : this.contributors) { + tags = tags.and(contributor.httpRequestTags(exchange, exception)); + } + return tags; } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/MetricsWebFilter.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/MetricsWebFilter.java index faf21ebfb713..357ca2f76005 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/MetricsWebFilter.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/MetricsWebFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,23 +16,33 @@ package org.springframework.boot.actuate.metrics.web.reactive.server; +import java.util.Collections; +import java.util.Set; import java.util.concurrent.TimeUnit; +import io.micrometer.core.annotation.Timed; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Tag; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import org.springframework.boot.actuate.metrics.AutoTimer; +import org.springframework.boot.actuate.metrics.annotation.TimedAnnotations; +import org.springframework.boot.web.reactive.error.ErrorAttributes; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; /** - * Intercepts incoming HTTP requests handled by Spring WebFlux handlers. + * Intercepts incoming HTTP requests handled by Spring WebFlux handlers and records + * metrics about execution time and results. * * @author Jon Schneider * @author Brian Clozel @@ -41,6 +51,8 @@ @Order(Ordered.HIGHEST_PRECEDENCE + 1) public class MetricsWebFilter implements WebFilter { + private static Log logger = LogFactory.getLog(MetricsWebFilter.class); + private final MeterRegistry registry; private final WebFluxTagsProvider tagsProvider; @@ -49,21 +61,6 @@ public class MetricsWebFilter implements WebFilter { private final AutoTimer autoTimer; - /** - * Create a new {@code MetricsWebFilter}. - * @param registry the registry to which metrics are recorded - * @param tagsProvider provider for metrics tags - * @param metricName name of the metric to record - * @param autoTimeRequests if requests should be automatically timed - * @deprecated since 2.2.0 in favor of - * {@link #MetricsWebFilter(MeterRegistry, WebFluxTagsProvider, String, AutoTimer)} - */ - @Deprecated - public MetricsWebFilter(MeterRegistry registry, WebFluxTagsProvider tagsProvider, String metricName, - boolean autoTimeRequests) { - this(registry, tagsProvider, metricName, AutoTimer.ENABLED); - } - /** * Create a new {@code MetricsWebFilter}. * @param registry the registry to which metrics are recorded @@ -82,39 +79,50 @@ public MetricsWebFilter(MeterRegistry registry, WebFluxTagsProvider tagsProvider @Override public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { - if (!this.autoTimer.isEnabled()) { - return chain.filter(exchange); - } return chain.filter(exchange).transformDeferred((call) -> filter(exchange, call)); } private Publisher filter(ServerWebExchange exchange, Mono call) { long start = System.nanoTime(); - return call.doOnSuccess((done) -> onSuccess(exchange, start)) - .doOnError((cause) -> onError(exchange, start, cause)); + return call.doOnEach((signal) -> onTerminalSignal(exchange, signal.getThrowable(), start)) + .doOnCancel(() -> onTerminalSignal(exchange, new CancelledServerWebExchangeException(), start)); } - private void onSuccess(ServerWebExchange exchange, long start) { - record(exchange, start, null); - } - - private void onError(ServerWebExchange exchange, long start, Throwable cause) { + private void onTerminalSignal(ServerWebExchange exchange, Throwable cause, long start) { ServerHttpResponse response = exchange.getResponse(); - if (response.isCommitted()) { - record(exchange, start, cause); + if (response.isCommitted() || cause instanceof CancelledServerWebExchangeException) { + record(exchange, cause, start); } else { response.beforeCommit(() -> { - record(exchange, start, cause); + record(exchange, cause, start); return Mono.empty(); }); } } - private void record(ServerWebExchange exchange, long start, Throwable cause) { - Iterable tags = this.tagsProvider.httpRequestTags(exchange, cause); - this.autoTimer.builder(this.metricName).tags(tags).register(this.registry).record(System.nanoTime() - start, - TimeUnit.NANOSECONDS); + private void record(ServerWebExchange exchange, Throwable cause, long start) { + try { + cause = (cause != null) ? cause : exchange.getAttribute(ErrorAttributes.ERROR_ATTRIBUTE); + Object handler = exchange.getAttribute(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE); + Set annotations = getTimedAnnotations(handler); + Iterable tags = this.tagsProvider.httpRequestTags(exchange, cause); + long duration = System.nanoTime() - start; + AutoTimer.apply(this.autoTimer, this.metricName, annotations, + (builder) -> builder.tags(tags).register(this.registry).record(duration, TimeUnit.NANOSECONDS)); + } + catch (Exception ex) { + logger.warn("Failed to record timer metrics", ex); + // Allow exchange to continue, unaffected by metrics problem + } + } + + private Set getTimedAnnotations(Object handler) { + if (handler instanceof HandlerMethod) { + HandlerMethod handlerMethod = (HandlerMethod) handler; + return TimedAnnotations.get(handlerMethod.getMethod(), handlerMethod.getBeanType()); + } + return Collections.emptySet(); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTags.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTags.java index f01ac80675b7..7ae68adcab45 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTags.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTags.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,15 @@ package org.springframework.boot.actuate.metrics.web.reactive.server; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Pattern; + import io.micrometer.core.instrument.Tag; import org.springframework.boot.actuate.metrics.http.Outcome; import org.springframework.http.HttpStatus; -import org.springframework.http.server.reactive.AbstractServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.StringUtils; import org.springframework.web.reactive.HandlerMapping; @@ -34,6 +38,7 @@ * @author Jon Schneider * @author Andy Wilkinson * @author Michael McFadyen + * @author Brian Clozel * @since 2.0.0 */ public final class WebFluxTags { @@ -48,6 +53,11 @@ public final class WebFluxTags { private static final Tag EXCEPTION_NONE = Tag.of("exception", "None"); + private static final Pattern FORWARD_SLASHES_PATTERN = Pattern.compile("//+"); + + private static final Set DISCONNECTED_CLIENT_EXCEPTIONS = new HashSet<>( + Arrays.asList("AbortedException", "ClientAbortException", "EOFException", "EofException")); + private WebFluxTags() { } @@ -87,9 +97,30 @@ public static Tag status(ServerWebExchange exchange) { * @return the uri tag derived from the exchange */ public static Tag uri(ServerWebExchange exchange) { + return uri(exchange, false); + } + + /** + * Creates a {@code uri} tag based on the URI of the given {@code exchange}. Uses the + * {@link HandlerMapping#BEST_MATCHING_PATTERN_ATTRIBUTE} best matching pattern if + * available. Falling back to {@code REDIRECTION} for 3xx responses, {@code NOT_FOUND} + * for 404 responses, {@code root} for requests with no path info, and {@code UNKNOWN} + * for all other requests. + * @param exchange the exchange + * @param ignoreTrailingSlash whether to ignore the trailing slash + * @return the uri tag derived from the exchange + */ + public static Tag uri(ServerWebExchange exchange, boolean ignoreTrailingSlash) { PathPattern pathPattern = exchange.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE); if (pathPattern != null) { - return Tag.of("uri", pathPattern.getPatternString()); + String patternString = pathPattern.getPatternString(); + if (ignoreTrailingSlash && patternString.length() > 1) { + patternString = removeTrailingSlash(patternString); + } + if (patternString.isEmpty()) { + return URI_ROOT; + } + return Tag.of("uri", patternString); } HttpStatus status = exchange.getResponse().getStatusCode(); if (status != null) { @@ -110,7 +141,15 @@ public static Tag uri(ServerWebExchange exchange) { private static String getPathInfo(ServerWebExchange exchange) { String path = exchange.getRequest().getPath().value(); String uri = StringUtils.hasText(path) ? path : "/"; - return uri.replaceAll("//+", "/").replaceAll("/$", ""); + String singleSlashes = FORWARD_SLASHES_PATTERN.matcher(uri).replaceAll("/"); + return removeTrailingSlash(singleSlashes); + } + + private static String removeTrailingSlash(String text) { + if (!StringUtils.hasLength(text)) { + return text; + } + return text.endsWith("/") ? text.substring(0, text.length() - 1) : text; } /** @@ -133,17 +172,39 @@ public static Tag exception(Throwable exception) { * @param exchange the exchange * @return the outcome tag derived from the response status * @since 2.1.0 + * @deprecated since 2.5.0 for removal in 2.7.0 in favor of + * {@link #outcome(ServerWebExchange, Throwable)} */ + @Deprecated public static Tag outcome(ServerWebExchange exchange) { + return outcome(exchange, null); + } + + /** + * Creates an {@code outcome} tag based on the response status of the given + * {@code exchange} and the exception thrown during request processing. + * @param exchange the exchange + * @param exception the termination signal sent by the publisher + * @return the outcome tag derived from the response status + * @since 2.5.0 + */ + public static Tag outcome(ServerWebExchange exchange, Throwable exception) { + if (exception != null) { + if (exception instanceof CancelledServerWebExchangeException + || DISCONNECTED_CLIENT_EXCEPTIONS.contains(exception.getClass().getSimpleName())) { + return Outcome.UNKNOWN.asTag(); + } + } Integer statusCode = extractStatusCode(exchange); - Outcome outcome = (statusCode != null) ? Outcome.forStatus(statusCode) : Outcome.UNKNOWN; + Outcome outcome = (statusCode != null) ? Outcome.forStatus(statusCode) : Outcome.SUCCESS; return outcome.asTag(); } private static Integer extractStatusCode(ServerWebExchange exchange) { ServerHttpResponse response = exchange.getResponse(); - if (response instanceof AbstractServerHttpResponse) { - return ((AbstractServerHttpResponse) response).getStatusCodeValue(); + Integer statusCode = response.getRawStatusCode(); + if (statusCode != null) { + return statusCode; } HttpStatus status = response.getStatusCode(); return (status != null) ? status.value() : null; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsContributor.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsContributor.java new file mode 100644 index 000000000000..a0ff28daa395 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsContributor.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.web.reactive.server; + +import io.micrometer.core.instrument.Tag; + +import org.springframework.web.server.ServerWebExchange; + +/** + * A contributor of {@link Tag Tags} for WebFlux-based request handling. Typically used by + * a {@link WebFluxTagsProvider} to provide tags in addition to its defaults. + * + * @author Andy Wilkinson + * @since 2.3.0 + */ +@FunctionalInterface +public interface WebFluxTagsContributor { + + /** + * Provides tags to be associated with metrics for the given {@code exchange}. + * @param exchange the exchange + * @param ex the current exception (may be {@code null}) + * @return tags to associate with metrics for the request and response exchange + */ + Iterable httpRequestTags(ServerWebExchange exchange, Throwable ex); + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/DefaultWebMvcTagsProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/DefaultWebMvcTagsProvider.java index 6d7ed11d2493..3336e89d023e 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/DefaultWebMvcTagsProvider.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/DefaultWebMvcTagsProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,9 @@ package org.springframework.boot.actuate.metrics.web.servlet; +import java.util.Collections; +import java.util.List; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -30,16 +33,59 @@ */ public class DefaultWebMvcTagsProvider implements WebMvcTagsProvider { + private final boolean ignoreTrailingSlash; + + private final List contributors; + + public DefaultWebMvcTagsProvider() { + this(false); + } + + /** + * Creates a new {@link DefaultWebMvcTagsProvider} that will provide tags from the + * given {@code contributors} in addition to its own. + * @param contributors the contributors that will provide additional tags + * @since 2.3.0 + */ + public DefaultWebMvcTagsProvider(List contributors) { + this(false, contributors); + } + + public DefaultWebMvcTagsProvider(boolean ignoreTrailingSlash) { + this(ignoreTrailingSlash, Collections.emptyList()); + } + + /** + * Creates a new {@link DefaultWebMvcTagsProvider} that will provide tags from the + * given {@code contributors} in addition to its own. + * @param ignoreTrailingSlash whether trailing slashes should be ignored when + * determining the {@code uri} tag. + * @param contributors the contributors that will provide additional tags + * @since 2.3.0 + */ + public DefaultWebMvcTagsProvider(boolean ignoreTrailingSlash, List contributors) { + this.ignoreTrailingSlash = ignoreTrailingSlash; + this.contributors = contributors; + } + @Override public Iterable getTags(HttpServletRequest request, HttpServletResponse response, Object handler, Throwable exception) { - return Tags.of(WebMvcTags.method(request), WebMvcTags.uri(request, response), WebMvcTags.exception(exception), - WebMvcTags.status(response), WebMvcTags.outcome(response)); + Tags tags = Tags.of(WebMvcTags.method(request), WebMvcTags.uri(request, response, this.ignoreTrailingSlash), + WebMvcTags.exception(exception), WebMvcTags.status(response), WebMvcTags.outcome(response)); + for (WebMvcTagsContributor contributor : this.contributors) { + tags = tags.and(contributor.getTags(request, response, handler, exception)); + } + return tags; } @Override public Iterable getLongRequestTags(HttpServletRequest request, Object handler) { - return Tags.of(WebMvcTags.method(request), WebMvcTags.uri(request, null)); + Tags tags = Tags.of(WebMvcTags.method(request), WebMvcTags.uri(request, null, this.ignoreTrailingSlash)); + for (WebMvcTagsContributor contributor : this.contributors) { + tags = tags.and(contributor.getLongRequestTags(request, handler)); + } + return tags; } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/LongTaskTimingHandlerInterceptor.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/LongTaskTimingHandlerInterceptor.java index 524a09b4491c..7f5be970bb7a 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/LongTaskTimingHandlerInterceptor.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/LongTaskTimingHandlerInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,8 @@ import io.micrometer.core.instrument.LongTaskTimer; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Tag; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.core.annotation.MergedAnnotationCollectors; import org.springframework.core.annotation.MergedAnnotations; @@ -46,6 +48,8 @@ */ public class LongTaskTimingHandlerInterceptor implements HandlerInterceptor { + private static final Log logger = LogFactory.getLog(LongTaskTimingHandlerInterceptor.class); + private final MeterRegistry registry; private final WebMvcTagsProvider tagsProvider; @@ -90,12 +94,18 @@ private void startAndAttachTimingContext(HttpServletRequest request, Object hand private Collection getLongTaskTimerSamples(HttpServletRequest request, Object handler, Set annotations) { List samples = new ArrayList<>(); - annotations.stream().filter(Timed::longTask).forEach((annotation) -> { - Iterable tags = this.tagsProvider.getLongRequestTags(request, handler); - LongTaskTimer.Builder builder = LongTaskTimer.builder(annotation).tags(tags); - LongTaskTimer timer = builder.register(this.registry); - samples.add(timer.start()); - }); + try { + annotations.stream().filter(Timed::longTask).forEach((annotation) -> { + Iterable tags = this.tagsProvider.getLongRequestTags(request, handler); + LongTaskTimer.Builder builder = LongTaskTimer.builder(annotation).tags(tags); + LongTaskTimer timer = builder.register(this.registry); + samples.add(timer.start()); + }); + } + catch (Exception ex) { + logger.warn("Failed to start long task timers", ex); + // Allow request-response exchange to continue, unaffected by metrics problem + } return samples; } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcMetricsFilter.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcMetricsFilter.java index 7cc029ffafcc..8838253d0dfc 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcMetricsFilter.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcMetricsFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package org.springframework.boot.actuate.metrics.web.servlet; import java.io.IOException; -import java.lang.reflect.AnnotatedElement; import java.util.Collections; import java.util.Set; @@ -31,10 +30,12 @@ import io.micrometer.core.instrument.Timer; import io.micrometer.core.instrument.Timer.Builder; import io.micrometer.core.instrument.Timer.Sample; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.boot.actuate.metrics.AutoTimer; -import org.springframework.core.annotation.MergedAnnotationCollectors; -import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.boot.actuate.metrics.annotation.TimedAnnotations; +import org.springframework.boot.web.servlet.error.ErrorAttributes; import org.springframework.http.HttpStatus; import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.method.HandlerMethod; @@ -43,15 +44,18 @@ import org.springframework.web.util.NestedServletException; /** - * Intercepts incoming HTTP requests and records metrics about Spring MVC execution time - * and results. + * Intercepts incoming HTTP requests handled by Spring MVC handlers and records metrics + * about execution time and results. * * @author Jon Schneider * @author Phillip Webb + * @author Chanhyeong LEE * @since 2.0.0 */ public class WebMvcMetricsFilter extends OncePerRequestFilter { + private static final Log logger = LogFactory.getLog(WebMvcMetricsFilter.class); + private final MeterRegistry registry; private final WebMvcTagsProvider tagsProvider; @@ -60,22 +64,6 @@ public class WebMvcMetricsFilter extends OncePerRequestFilter { private final AutoTimer autoTimer; - /** - * Create a new {@link WebMvcMetricsFilter} instance. - * @param registry the meter registry - * @param tagsProvider the tags provider - * @param metricName the metric name - * @param autoTimeRequests if requests should be automatically timed - * @since 2.0.7 - * @deprecated since 2.2.0 in favor of - * {@link #WebMvcMetricsFilter(MeterRegistry, WebMvcTagsProvider, String, AutoTimer)} - */ - @Deprecated - public WebMvcMetricsFilter(MeterRegistry registry, WebMvcTagsProvider tagsProvider, String metricName, - boolean autoTimeRequests) { - this(registry, tagsProvider, metricName, AutoTimer.ENABLED); - } - /** * Create a new {@link WebMvcMetricsFilter} instance. * @param registry the meter registry @@ -111,21 +99,21 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse // If async was started by something further down the chain we wait // until the second filter invocation (but we'll be using the // TimingContext that was attached to the first) - Throwable exception = (Throwable) request.getAttribute(DispatcherServlet.EXCEPTION_ATTRIBUTE); + Throwable exception = fetchException(request); record(timingContext, request, response, exception); } } - catch (NestedServletException ex) { + catch (Exception ex) { response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); - record(timingContext, request, response, ex.getCause()); - throw ex; - } - catch (ServletException | IOException | RuntimeException ex) { - record(timingContext, request, response, ex); + record(timingContext, request, response, unwrapNestedServletException(ex)); throw ex; } } + private Throwable unwrapNestedServletException(Throwable ex) { + return (ex instanceof NestedServletException) ? ex.getCause() : ex; + } + private TimingContext startAndAttachTimingContext(HttpServletRequest request) { Timer.Sample timerSample = Timer.start(this.registry); TimingContext timingContext = new TimingContext(timerSample); @@ -133,19 +121,26 @@ private TimingContext startAndAttachTimingContext(HttpServletRequest request) { return timingContext; } + private Throwable fetchException(HttpServletRequest request) { + Throwable exception = (Throwable) request.getAttribute(ErrorAttributes.ERROR_ATTRIBUTE); + if (exception == null) { + exception = (Throwable) request.getAttribute(DispatcherServlet.EXCEPTION_ATTRIBUTE); + } + return exception; + } + private void record(TimingContext timingContext, HttpServletRequest request, HttpServletResponse response, Throwable exception) { - Object handler = getHandler(request); - Set annotations = getTimedAnnotations(handler); - Timer.Sample timerSample = timingContext.getTimerSample(); - if (annotations.isEmpty()) { - Builder builder = this.autoTimer.builder(this.metricName); - timerSample.stop(getTimer(builder, handler, request, response, exception)); - return; + try { + Object handler = getHandler(request); + Set annotations = getTimedAnnotations(handler); + Timer.Sample timerSample = timingContext.getTimerSample(); + AutoTimer.apply(this.autoTimer, this.metricName, annotations, + (builder) -> timerSample.stop(getTimer(builder, handler, request, response, exception))); } - for (Timed annotation : annotations) { - Builder builder = Timer.builder(annotation, this.metricName); - timerSample.stop(getTimer(builder, handler, request, response, exception)); + catch (Exception ex) { + logger.warn("Failed to record timer metrics", ex); + // Allow request-response exchange to continue, unaffected by metrics problem } } @@ -154,26 +149,11 @@ private Object getHandler(HttpServletRequest request) { } private Set getTimedAnnotations(Object handler) { - if (!(handler instanceof HandlerMethod)) { - return Collections.emptySet(); - } - return getTimedAnnotations((HandlerMethod) handler); - } - - private Set getTimedAnnotations(HandlerMethod handler) { - Set methodAnnotations = findTimedAnnotations(handler.getMethod()); - if (!methodAnnotations.isEmpty()) { - return methodAnnotations; - } - return findTimedAnnotations(handler.getBeanType()); - } - - private Set findTimedAnnotations(AnnotatedElement element) { - MergedAnnotations annotations = MergedAnnotations.from(element); - if (!annotations.isPresent(Timed.class)) { - return Collections.emptySet(); + if (handler instanceof HandlerMethod) { + HandlerMethod handlerMethod = (HandlerMethod) handler; + return TimedAnnotations.get(handlerMethod.getMethod(), handlerMethod.getBeanType()); } - return annotations.stream(Timed.class).collect(MergedAnnotationCollectors.toAnnotationSet()); + return Collections.emptySet(); } private Timer getTimer(Builder builder, Object handler, HttpServletRequest request, HttpServletResponse response, diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTags.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTags.java index 78c4c0b970c1..e5e2147bf8bb 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTags.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTags.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -94,9 +94,30 @@ public static Tag status(HttpServletResponse response) { * @return the uri tag derived from the request */ public static Tag uri(HttpServletRequest request, HttpServletResponse response) { + return uri(request, response, false); + } + + /** + * Creates a {@code uri} tag based on the URI of the given {@code request}. Uses the + * {@link HandlerMapping#BEST_MATCHING_PATTERN_ATTRIBUTE} best matching pattern if + * available. Falling back to {@code REDIRECTION} for 3xx responses, {@code NOT_FOUND} + * for 404 responses, {@code root} for requests with no path info, and {@code UNKNOWN} + * for all other requests. + * @param request the request + * @param response the response + * @param ignoreTrailingSlash whether to ignore the trailing slash + * @return the uri tag derived from the request + */ + public static Tag uri(HttpServletRequest request, HttpServletResponse response, boolean ignoreTrailingSlash) { if (request != null) { String pattern = getMatchingPattern(request); if (pattern != null) { + if (ignoreTrailingSlash && pattern.length() > 1) { + pattern = TRAILING_SLASH_PATTERN.matcher(pattern).replaceAll(""); + } + if (pattern.isEmpty()) { + return URI_ROOT; + } return Tag.of("uri", pattern); } if (response != null) { @@ -143,7 +164,7 @@ private static String getPathInfo(HttpServletRequest request) { } /** - * Creates a {@code exception} tag based on the {@link Class#getSimpleName() simple + * Creates an {@code exception} tag based on the {@link Class#getSimpleName() simple * name} of the class of the given {@code exception}. * @param exception the exception, may be {@code null} * @return the exception tag derived from the exception diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTagsContributor.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTagsContributor.java new file mode 100644 index 000000000000..0b0d3d5ef7b2 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTagsContributor.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.web.servlet; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import io.micrometer.core.instrument.LongTaskTimer; +import io.micrometer.core.instrument.Tag; + +/** + * A contributor of {@link Tag Tags} for Spring MVC-based request handling. Typically used + * by a {@link WebMvcTagsProvider} to provide tags in addition to its defaults. + * + * @author Andy Wilkinson + * @since 2.3.0 + */ +public interface WebMvcTagsContributor { + + /** + * Provides tags to be associated with metrics for the given {@code request} and + * {@code response} exchange. + * @param request the request + * @param response the response + * @param handler the handler for the request or {@code null} if the handler is + * unknown + * @param exception the current exception, if any + * @return tags to associate with metrics for the request and response exchange + */ + Iterable getTags(HttpServletRequest request, HttpServletResponse response, Object handler, + Throwable exception); + + /** + * Provides tags to be used by {@link LongTaskTimer long task timers}. + * @param request the HTTP request + * @param handler the handler for the request or {@code null} if the handler is + * unknown + * @return tags to associate with metrics recorded for the request + */ + Iterable getLongRequestTags(HttpServletRequest request, Object handler); + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/tomcat/TomcatMetricsBinder.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/tomcat/TomcatMetricsBinder.java index 689c41cdca4a..0932218080fe 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/tomcat/TomcatMetricsBinder.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/tomcat/TomcatMetricsBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import org.apache.catalina.Context; import org.apache.catalina.Manager; +import org.springframework.beans.factory.DisposableBean; import org.springframework.boot.context.event.ApplicationStartedEvent; import org.springframework.boot.web.context.WebServerApplicationContext; import org.springframework.boot.web.embedded.tomcat.TomcatWebServer; @@ -38,12 +39,14 @@ * @author Andy Wilkinson * @since 2.1.0 */ -public class TomcatMetricsBinder implements ApplicationListener { +public class TomcatMetricsBinder implements ApplicationListener, DisposableBean { private final MeterRegistry meterRegistry; private final Iterable tags; + private volatile TomcatMetrics tomcatMetrics; + public TomcatMetricsBinder(MeterRegistry meterRegistry) { this(meterRegistry, Collections.emptyList()); } @@ -57,7 +60,8 @@ public TomcatMetricsBinder(MeterRegistry meterRegistry, Iterable tags) { public void onApplicationEvent(ApplicationStartedEvent event) { ApplicationContext applicationContext = event.getApplicationContext(); Manager manager = findManager(applicationContext); - new TomcatMetrics(manager, this.tags).bindTo(this.meterRegistry); + this.tomcatMetrics = new TomcatMetrics(manager, this.tags); + this.tomcatMetrics.bindTo(this.meterRegistry); } private Manager findManager(ApplicationContext applicationContext) { @@ -65,7 +69,9 @@ private Manager findManager(ApplicationContext applicationContext) { WebServer webServer = ((WebServerApplicationContext) applicationContext).getWebServer(); if (webServer instanceof TomcatWebServer) { Context context = findContext((TomcatWebServer) webServer); - return context.getManager(); + if (context != null) { + return context.getManager(); + } } } return null; @@ -80,4 +86,11 @@ private Context findContext(TomcatWebServer tomcatWebServer) { return null; } + @Override + public void destroy() { + if (this.tomcatMetrics != null) { + this.tomcatMetrics.close(); + } + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jHealthDetailsHandler.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jHealthDetailsHandler.java new file mode 100644 index 000000000000..5bf908a3cbb7 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jHealthDetailsHandler.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.neo4j; + +import org.neo4j.driver.summary.DatabaseInfo; +import org.neo4j.driver.summary.ResultSummary; +import org.neo4j.driver.summary.ServerInfo; + +import org.springframework.boot.actuate.health.Health.Builder; +import org.springframework.util.StringUtils; + +/** + * Handle health check details for a Neo4j server. + * + * @author Stephane Nicoll + */ +class Neo4jHealthDetailsHandler { + + /** + * Add health details for the specified {@link ResultSummary} and {@code edition}. + * @param builder the {@link Builder} to use + * @param edition the edition of the server + * @param resultSummary server information + */ + void addHealthDetails(Builder builder, String edition, ResultSummary resultSummary) { + ServerInfo serverInfo = resultSummary.server(); + builder.up().withDetail("server", serverInfo.version() + "@" + serverInfo.address()).withDetail("edition", + edition); + DatabaseInfo databaseInfo = resultSummary.database(); + if (StringUtils.hasText(databaseInfo.name())) { + builder.withDetail("database", databaseInfo.name()); + } + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jHealthIndicator.java index a2824025a87c..782be4cb2b2e 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jHealthIndicator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,60 +16,85 @@ package org.springframework.boot.actuate.neo4j; -import java.util.Collections; - -import org.neo4j.ogm.model.Result; -import org.neo4j.ogm.session.Session; -import org.neo4j.ogm.session.SessionFactory; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.neo4j.driver.AccessMode; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Result; +import org.neo4j.driver.Session; +import org.neo4j.driver.SessionConfig; +import org.neo4j.driver.exceptions.SessionExpiredException; +import org.neo4j.driver.summary.ResultSummary; import org.springframework.boot.actuate.health.AbstractHealthIndicator; import org.springframework.boot.actuate.health.Health; -import org.springframework.boot.actuate.health.Health.Builder; import org.springframework.boot.actuate.health.HealthIndicator; /** * {@link HealthIndicator} that tests the status of a Neo4j by executing a Cypher - * statement. + * statement and extracting server and database information. * * @author Eric Spiegelberg * @author Stephane Nicoll + * @author Michael J. Simons * @since 2.0.0 */ public class Neo4jHealthIndicator extends AbstractHealthIndicator { + private static final Log logger = LogFactory.getLog(Neo4jHealthIndicator.class); + /** * The Cypher statement used to verify Neo4j is up. */ - static final String CYPHER = "match (n) return count(n) as nodes"; + static final String CYPHER = "CALL dbms.components() YIELD name, edition WHERE name = 'Neo4j Kernel' RETURN edition"; - private final SessionFactory sessionFactory; + /** + * Message logged before retrying a health check. + */ + static final String MESSAGE_SESSION_EXPIRED = "Neo4j session has expired, retrying one single time to retrieve server health."; /** - * Create a new {@link Neo4jHealthIndicator} using the specified - * {@link SessionFactory}. - * @param sessionFactory the SessionFactory + * The default session config to use while connecting. */ - public Neo4jHealthIndicator(SessionFactory sessionFactory) { - super("Neo4J health check failed"); - this.sessionFactory = sessionFactory; + static final SessionConfig DEFAULT_SESSION_CONFIG = SessionConfig.builder().withDefaultAccessMode(AccessMode.WRITE) + .build(); + + private final Driver driver; + + private final Neo4jHealthDetailsHandler healthDetailsHandler; + + public Neo4jHealthIndicator(Driver driver) { + super("Neo4j health check failed"); + this.driver = driver; + this.healthDetailsHandler = new Neo4jHealthDetailsHandler(); } @Override - protected void doHealthCheck(Health.Builder builder) throws Exception { - Session session = this.sessionFactory.openSession(); - extractResult(session, builder); + protected void doHealthCheck(Health.Builder builder) { + try { + try { + runHealthCheckQuery(builder); + } + catch (SessionExpiredException ex) { + // Retry one time when the session has been expired + logger.warn(MESSAGE_SESSION_EXPIRED); + runHealthCheckQuery(builder); + } + } + catch (Exception ex) { + builder.down().withException(ex); + } } - /** - * Provide health details using the specified {@link Session} and {@link Builder - * Builder}. - * @param session the session to use to execute a cypher statement - * @param builder the builder to add details to - * @throws Exception if getting health details failed - */ - protected void extractResult(Session session, Health.Builder builder) throws Exception { - Result result = session.query(CYPHER, Collections.emptyMap()); - builder.up().withDetail("nodes", result.queryResults().iterator().next().get("nodes")); + private void runHealthCheckQuery(Health.Builder builder) { + // We use WRITE here to make sure UP is returned for a server that supports + // all possible workloads + try (Session session = this.driver.session(DEFAULT_SESSION_CONFIG)) { + Result result = session.run(CYPHER); + String edition = result.single().get("edition").asString(); + ResultSummary resultSummary = result.consume(); + this.healthDetailsHandler.addHealthDetails(builder, edition, resultSummary); + } } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jReactiveHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jReactiveHealthIndicator.java new file mode 100644 index 000000000000..77b8d51bcbc8 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/Neo4jReactiveHealthIndicator.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.neo4j; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.neo4j.driver.Driver; +import org.neo4j.driver.exceptions.SessionExpiredException; +import org.neo4j.driver.reactive.RxResult; +import org.neo4j.driver.reactive.RxSession; +import org.neo4j.driver.summary.ResultSummary; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; +import reactor.util.retry.Retry; + +import org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.ReactiveHealthIndicator; + +/** + * {@link ReactiveHealthIndicator} that tests the status of a Neo4j by executing a Cypher + * statement and extracting server and database information. + * + * @author Michael J. Simons + * @author Stephane Nicoll + * @since 2.4.0 + */ +public final class Neo4jReactiveHealthIndicator extends AbstractReactiveHealthIndicator { + + private static final Log logger = LogFactory.getLog(Neo4jReactiveHealthIndicator.class); + + private final Driver driver; + + private final Neo4jHealthDetailsHandler healthDetailsHandler; + + public Neo4jReactiveHealthIndicator(Driver driver) { + this.driver = driver; + this.healthDetailsHandler = new Neo4jHealthDetailsHandler(); + } + + @Override + protected Mono doHealthCheck(Health.Builder builder) { + return runHealthCheckQuery() + .doOnError(SessionExpiredException.class, + (e) -> logger.warn(Neo4jHealthIndicator.MESSAGE_SESSION_EXPIRED)) + .retryWhen(Retry.max(1).filter(SessionExpiredException.class::isInstance)).map((result) -> { + this.healthDetailsHandler.addHealthDetails(builder, result.getT1(), result.getT2()); + return builder.build(); + }); + } + + Mono> runHealthCheckQuery() { + // We use WRITE here to make sure UP is returned for a server that supports + // all possible workloads + return Mono.using(() -> this.driver.rxSession(Neo4jHealthIndicator.DEFAULT_SESSION_CONFIG), (session) -> { + RxResult result = session.run(Neo4jHealthIndicator.CYPHER); + return Mono.from(result.records()).map((record) -> record.get("edition").asString()) + .zipWhen((edition) -> Mono.from(result.consume())); + }, RxSession::close); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/package-info.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/package-info.java index 340170eff5f1..7b89b60097fe 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/package-info.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/neo4j/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/quartz/QuartzEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/quartz/QuartzEndpoint.java new file mode 100644 index 000000000000..8e22ba4c5165 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/quartz/QuartzEndpoint.java @@ -0,0 +1,802 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.quartz; + +import java.time.Duration; +import java.time.LocalTime; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalUnit; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.quartz.CalendarIntervalTrigger; +import org.quartz.CronTrigger; +import org.quartz.DailyTimeIntervalTrigger; +import org.quartz.DateBuilder.IntervalUnit; +import org.quartz.Job; +import org.quartz.JobDataMap; +import org.quartz.JobDetail; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.SimpleTrigger; +import org.quartz.TimeOfDay; +import org.quartz.Trigger; +import org.quartz.Trigger.TriggerState; +import org.quartz.TriggerKey; +import org.quartz.impl.matchers.GroupMatcher; + +import org.springframework.boot.actuate.endpoint.Sanitizer; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.util.Assert; + +/** + * {@link Endpoint} to expose Quartz Scheduler jobs and triggers. + * + * @author Vedran Pavic + * @author Stephane Nicoll + * @since 2.5.0 + */ +@Endpoint(id = "quartz") +public class QuartzEndpoint { + + private static final Comparator TRIGGER_COMPARATOR = Comparator + .comparing(Trigger::getNextFireTime, Comparator.nullsLast(Comparator.naturalOrder())) + .thenComparing(Comparator.comparingInt(Trigger::getPriority).reversed()); + + private final Scheduler scheduler; + + private final Sanitizer sanitizer; + + /** + * Create an instance for the specified {@link Scheduler} and {@link Sanitizer}. + * @param scheduler the scheduler to use to retrieve jobs and triggers details + * @param sanitizer the sanitizer to use to sanitize data maps + */ + public QuartzEndpoint(Scheduler scheduler, Sanitizer sanitizer) { + Assert.notNull(scheduler, "Scheduler must not be null"); + Assert.notNull(sanitizer, "Sanitizer must not be null"); + this.scheduler = scheduler; + this.sanitizer = sanitizer; + } + + /** + * Create an instance for the specified {@link Scheduler} using a default + * {@link Sanitizer}. + * @param scheduler the scheduler to use to retrieve jobs and triggers details + */ + public QuartzEndpoint(Scheduler scheduler) { + this(scheduler, new Sanitizer()); + } + + /** + * Return the available job and trigger group names. + * @return a report of the available group names + * @throws SchedulerException if retrieving the information from the scheduler failed + */ + @ReadOperation + public QuartzReport quartzReport() throws SchedulerException { + return new QuartzReport(new GroupNames(this.scheduler.getJobGroupNames()), + new GroupNames(this.scheduler.getTriggerGroupNames())); + } + + /** + * Return the available job names, identified by group name. + * @return the available job names + * @throws SchedulerException if retrieving the information from the scheduler failed + */ + public QuartzGroups quartzJobGroups() throws SchedulerException { + Map result = new LinkedHashMap<>(); + for (String groupName : this.scheduler.getJobGroupNames()) { + List jobs = this.scheduler.getJobKeys(GroupMatcher.jobGroupEquals(groupName)).stream() + .map((key) -> key.getName()).collect(Collectors.toList()); + result.put(groupName, Collections.singletonMap("jobs", jobs)); + } + return new QuartzGroups(result); + } + + /** + * Return the available trigger names, identified by group name. + * @return the available trigger names + * @throws SchedulerException if retrieving the information from the scheduler failed + */ + public QuartzGroups quartzTriggerGroups() throws SchedulerException { + Map result = new LinkedHashMap<>(); + Set pausedTriggerGroups = this.scheduler.getPausedTriggerGroups(); + for (String groupName : this.scheduler.getTriggerGroupNames()) { + Map groupDetails = new LinkedHashMap<>(); + groupDetails.put("paused", pausedTriggerGroups.contains(groupName)); + groupDetails.put("triggers", this.scheduler.getTriggerKeys(GroupMatcher.triggerGroupEquals(groupName)) + .stream().map((key) -> key.getName()).collect(Collectors.toList())); + result.put(groupName, groupDetails); + } + return new QuartzGroups(result); + } + + /** + * Return a summary of the jobs group with the specified name or {@code null} if no + * such group exists. + * @param group the name of a jobs group + * @return a summary of the jobs in the given {@code group} + * @throws SchedulerException if retrieving the information from the scheduler failed + */ + public QuartzJobGroupSummary quartzJobGroupSummary(String group) throws SchedulerException { + List jobs = findJobsByGroup(group); + if (jobs.isEmpty() && !this.scheduler.getJobGroupNames().contains(group)) { + return null; + } + Map result = new LinkedHashMap<>(); + for (JobDetail job : jobs) { + result.put(job.getKey().getName(), QuartzJobSummary.of(job)); + } + return new QuartzJobGroupSummary(group, result); + } + + private List findJobsByGroup(String group) throws SchedulerException { + List jobs = new ArrayList<>(); + Set jobKeys = this.scheduler.getJobKeys(GroupMatcher.jobGroupEquals(group)); + for (JobKey jobKey : jobKeys) { + jobs.add(this.scheduler.getJobDetail(jobKey)); + } + return jobs; + } + + /** + * Return a summary of the triggers group with the specified name or {@code null} if + * no such group exists. + * @param group the name of a triggers group + * @return a summary of the triggers in the given {@code group} + * @throws SchedulerException if retrieving the information from the scheduler failed + */ + public QuartzTriggerGroupSummary quartzTriggerGroupSummary(String group) throws SchedulerException { + List triggers = findTriggersByGroup(group); + if (triggers.isEmpty() && !this.scheduler.getTriggerGroupNames().contains(group)) { + return null; + } + Map> result = new LinkedHashMap<>(); + triggers.forEach((trigger) -> { + TriggerDescription triggerDescription = TriggerDescription.of(trigger); + Map triggerTypes = result.computeIfAbsent(triggerDescription.getType(), + (key) -> new LinkedHashMap<>()); + triggerTypes.put(trigger.getKey().getName(), triggerDescription.buildSummary(true)); + }); + boolean paused = this.scheduler.getPausedTriggerGroups().contains(group); + return new QuartzTriggerGroupSummary(group, paused, result); + } + + private List findTriggersByGroup(String group) throws SchedulerException { + List triggers = new ArrayList<>(); + Set triggerKeys = this.scheduler.getTriggerKeys(GroupMatcher.triggerGroupEquals(group)); + for (TriggerKey triggerKey : triggerKeys) { + triggers.add(this.scheduler.getTrigger(triggerKey)); + } + return triggers; + } + + /** + * Return the {@link QuartzJobDetails details of the job} identified with the given + * group name and job name. + * @param groupName the name of the group + * @param jobName the name of the job + * @return the details of the job or {@code null} if such job does not exist + * @throws SchedulerException if retrieving the information from the scheduler failed + */ + public QuartzJobDetails quartzJob(String groupName, String jobName) throws SchedulerException { + JobKey jobKey = JobKey.jobKey(jobName, groupName); + JobDetail jobDetail = this.scheduler.getJobDetail(jobKey); + if (jobDetail != null) { + List triggers = this.scheduler.getTriggersOfJob(jobKey); + return new QuartzJobDetails(jobDetail.getKey().getGroup(), jobDetail.getKey().getName(), + jobDetail.getDescription(), jobDetail.getJobClass().getName(), jobDetail.isDurable(), + jobDetail.requestsRecovery(), sanitizeJobDataMap(jobDetail.getJobDataMap()), + extractTriggersSummary(triggers)); + } + return null; + } + + private static List> extractTriggersSummary(List triggers) { + List triggersToSort = new ArrayList<>(triggers); + triggersToSort.sort(TRIGGER_COMPARATOR); + List> result = new ArrayList<>(); + triggersToSort.forEach((trigger) -> { + Map triggerSummary = new LinkedHashMap<>(); + triggerSummary.put("group", trigger.getKey().getGroup()); + triggerSummary.put("name", trigger.getKey().getName()); + triggerSummary.putAll(TriggerDescription.of(trigger).buildSummary(false)); + result.add(triggerSummary); + }); + return result; + } + + /** + * Return the details of the trigger identified by the given group name and trigger + * name. + * @param groupName the name of the group + * @param triggerName the name of the trigger + * @return the details of the trigger or {@code null} if such trigger does not exist + * @throws SchedulerException if retrieving the information from the scheduler failed + */ + public Map quartzTrigger(String groupName, String triggerName) throws SchedulerException { + TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, groupName); + Trigger trigger = this.scheduler.getTrigger(triggerKey); + return (trigger != null) ? TriggerDescription.of(trigger).buildDetails( + this.scheduler.getTriggerState(triggerKey), sanitizeJobDataMap(trigger.getJobDataMap())) : null; + } + + private static Duration getIntervalDuration(long amount, IntervalUnit unit) { + return temporalUnit(unit).getDuration().multipliedBy(amount); + } + + private static LocalTime getLocalTime(TimeOfDay timeOfDay) { + return (timeOfDay != null) ? LocalTime.of(timeOfDay.getHour(), timeOfDay.getMinute(), timeOfDay.getSecond()) + : null; + } + + private Map sanitizeJobDataMap(JobDataMap dataMap) { + if (dataMap != null) { + Map map = new LinkedHashMap<>(dataMap.getWrappedMap()); + map.replaceAll(this.sanitizer::sanitize); + return map; + } + return null; + } + + private static TemporalUnit temporalUnit(IntervalUnit unit) { + switch (unit) { + case DAY: + return ChronoUnit.DAYS; + case HOUR: + return ChronoUnit.HOURS; + case MINUTE: + return ChronoUnit.MINUTES; + case MONTH: + return ChronoUnit.MONTHS; + case SECOND: + return ChronoUnit.SECONDS; + case MILLISECOND: + return ChronoUnit.MILLIS; + case WEEK: + return ChronoUnit.WEEKS; + case YEAR: + return ChronoUnit.YEARS; + default: + throw new IllegalArgumentException("Unknown IntervalUnit"); + } + } + + /** + * A report of available job and trigger group names, primarily intended for + * serialization to JSON. + */ + public static final class QuartzReport { + + private final GroupNames jobs; + + private final GroupNames triggers; + + QuartzReport(GroupNames jobs, GroupNames triggers) { + this.jobs = jobs; + this.triggers = triggers; + } + + public GroupNames getJobs() { + return this.jobs; + } + + public GroupNames getTriggers() { + return this.triggers; + } + + } + + /** + * A set of group names, primarily intended for serialization to JSON. + */ + public static class GroupNames { + + private final Set groups; + + public GroupNames(List groups) { + this.groups = new LinkedHashSet<>(groups); + } + + public Set getGroups() { + return this.groups; + } + + } + + /** + * A summary for each group identified by name, primarily intended for serialization + * to JSON. + */ + public static class QuartzGroups { + + private final Map groups; + + public QuartzGroups(Map groups) { + this.groups = groups; + } + + public Map getGroups() { + return this.groups; + } + + } + + /** + * A summary report of the {@link JobDetail jobs} in a given group. + */ + public static final class QuartzJobGroupSummary { + + private final String group; + + private final Map jobs; + + private QuartzJobGroupSummary(String group, Map jobs) { + this.group = group; + this.jobs = jobs; + } + + public String getGroup() { + return this.group; + } + + public Map getJobs() { + return this.jobs; + } + + } + + /** + * Details of a {@link Job Quartz Job}, primarily intended for serialization to JSON. + */ + public static final class QuartzJobSummary { + + private final String className; + + private QuartzJobSummary(JobDetail job) { + this.className = job.getJobClass().getName(); + } + + private static QuartzJobSummary of(JobDetail job) { + return new QuartzJobSummary(job); + } + + public String getClassName() { + return this.className; + } + + } + + /** + * Details of a {@link Job Quartz Job}, primarily intended for serialization to JSON. + */ + public static final class QuartzJobDetails { + + private final String group; + + private final String name; + + private final String description; + + private final String className; + + private final boolean durable; + + private final boolean requestRecovery; + + private final Map data; + + private final List> triggers; + + QuartzJobDetails(String group, String name, String description, String className, boolean durable, + boolean requestRecovery, Map data, List> triggers) { + this.group = group; + this.name = name; + this.description = description; + this.className = className; + this.durable = durable; + this.requestRecovery = requestRecovery; + this.data = data; + this.triggers = triggers; + } + + public String getGroup() { + return this.group; + } + + public String getName() { + return this.name; + } + + public String getDescription() { + return this.description; + } + + public String getClassName() { + return this.className; + } + + public boolean isDurable() { + return this.durable; + } + + public boolean isRequestRecovery() { + return this.requestRecovery; + } + + public Map getData() { + return this.data; + } + + public List> getTriggers() { + return this.triggers; + } + + } + + /** + * A summary report of the {@link Trigger triggers} in a given group. + */ + public static final class QuartzTriggerGroupSummary { + + private final String group; + + private final boolean paused; + + private final Triggers triggers; + + private QuartzTriggerGroupSummary(String group, boolean paused, + Map> descriptionsByType) { + this.group = group; + this.paused = paused; + this.triggers = new Triggers(descriptionsByType); + + } + + public String getGroup() { + return this.group; + } + + public boolean isPaused() { + return this.paused; + } + + public Triggers getTriggers() { + return this.triggers; + } + + public static final class Triggers { + + private final Map cron; + + private final Map simple; + + private final Map dailyTimeInterval; + + private final Map calendarInterval; + + private final Map custom; + + private Triggers(Map> descriptionsByType) { + this.cron = descriptionsByType.getOrDefault(TriggerType.CRON, Collections.emptyMap()); + this.dailyTimeInterval = descriptionsByType.getOrDefault(TriggerType.DAILY_INTERVAL, + Collections.emptyMap()); + this.calendarInterval = descriptionsByType.getOrDefault(TriggerType.CALENDAR_INTERVAL, + Collections.emptyMap()); + this.simple = descriptionsByType.getOrDefault(TriggerType.SIMPLE, Collections.emptyMap()); + this.custom = descriptionsByType.getOrDefault(TriggerType.CUSTOM_TRIGGER, Collections.emptyMap()); + } + + public Map getCron() { + return this.cron; + } + + public Map getSimple() { + return this.simple; + } + + public Map getDailyTimeInterval() { + return this.dailyTimeInterval; + } + + public Map getCalendarInterval() { + return this.calendarInterval; + } + + public Map getCustom() { + return this.custom; + } + + } + + } + + private enum TriggerType { + + CRON("cron"), + + CUSTOM_TRIGGER("custom"), + + CALENDAR_INTERVAL("calendarInterval"), + + DAILY_INTERVAL("dailyTimeInterval"), + + SIMPLE("simple"); + + private final String id; + + TriggerType(String id) { + this.id = id; + } + + public String getId() { + return this.id; + } + + } + + /** + * Base class for descriptions of a {@link Trigger}. + */ + public abstract static class TriggerDescription { + + private static final Map, Function> DESCRIBERS = new LinkedHashMap<>(); + + static { + DESCRIBERS.put(CronTrigger.class, (trigger) -> new CronTriggerDescription((CronTrigger) trigger)); + DESCRIBERS.put(SimpleTrigger.class, (trigger) -> new SimpleTriggerDescription((SimpleTrigger) trigger)); + DESCRIBERS.put(DailyTimeIntervalTrigger.class, + (trigger) -> new DailyTimeIntervalTriggerDescription((DailyTimeIntervalTrigger) trigger)); + DESCRIBERS.put(CalendarIntervalTrigger.class, + (trigger) -> new CalendarIntervalTriggerDescription((CalendarIntervalTrigger) trigger)); + } + + private final Trigger trigger; + + private final TriggerType type; + + private static TriggerDescription of(Trigger trigger) { + return DESCRIBERS.entrySet().stream().filter((entry) -> entry.getKey().isInstance(trigger)) + .map((entry) -> entry.getValue().apply(trigger)).findFirst() + .orElse(new CustomTriggerDescription(trigger)); + } + + protected TriggerDescription(Trigger trigger, TriggerType type) { + this.trigger = trigger; + this.type = type; + } + + /** + * Build the summary of the trigger. + * @param addTriggerSpecificSummary whether to add trigger-implementation specific + * summary. + * @return basic properties of the trigger + */ + public Map buildSummary(boolean addTriggerSpecificSummary) { + Map summary = new LinkedHashMap<>(); + putIfNoNull(summary, "previousFireTime", this.trigger.getPreviousFireTime()); + putIfNoNull(summary, "nextFireTime", this.trigger.getNextFireTime()); + summary.put("priority", this.trigger.getPriority()); + if (addTriggerSpecificSummary) { + appendSummary(summary); + } + return summary; + } + + /** + * Append trigger-implementation specific summary items to the specified + * {@code content}. + * @param content the summary of the trigger + */ + protected abstract void appendSummary(Map content); + + /** + * Build the full details of the trigger. + * @param triggerState the current state of the trigger + * @param sanitizedDataMap a sanitized data map or {@code null} + * @return all properties of the trigger + */ + public Map buildDetails(TriggerState triggerState, Map sanitizedDataMap) { + Map details = new LinkedHashMap<>(); + details.put("group", this.trigger.getKey().getGroup()); + details.put("name", this.trigger.getKey().getName()); + putIfNoNull(details, "description", this.trigger.getDescription()); + details.put("state", triggerState); + details.put("type", getType().getId()); + putIfNoNull(details, "calendarName", this.trigger.getCalendarName()); + putIfNoNull(details, "startTime", this.trigger.getStartTime()); + putIfNoNull(details, "endTime", this.trigger.getEndTime()); + putIfNoNull(details, "previousFireTime", this.trigger.getPreviousFireTime()); + putIfNoNull(details, "nextFireTime", this.trigger.getNextFireTime()); + putIfNoNull(details, "priority", this.trigger.getPriority()); + putIfNoNull(details, "finalFireTime", this.trigger.getFinalFireTime()); + putIfNoNull(details, "data", sanitizedDataMap); + Map typeDetails = new LinkedHashMap<>(); + appendDetails(typeDetails); + details.put(getType().getId(), typeDetails); + return details; + } + + /** + * Append trigger-implementation specific details to the specified + * {@code content}. + * @param content the details of the trigger + */ + protected abstract void appendDetails(Map content); + + protected void putIfNoNull(Map content, String key, Object value) { + if (value != null) { + content.put(key, value); + } + } + + protected Trigger getTrigger() { + return this.trigger; + } + + protected TriggerType getType() { + return this.type; + } + + } + + /** + * A description of a {@link CronTrigger}. + */ + public static final class CronTriggerDescription extends TriggerDescription { + + private final CronTrigger trigger; + + public CronTriggerDescription(CronTrigger trigger) { + super(trigger, TriggerType.CRON); + this.trigger = trigger; + } + + @Override + protected void appendSummary(Map content) { + content.put("expression", this.trigger.getCronExpression()); + putIfNoNull(content, "timeZone", this.trigger.getTimeZone()); + } + + @Override + protected void appendDetails(Map content) { + appendSummary(content); + } + + } + + /** + * A description of a {@link SimpleTrigger}. + */ + public static final class SimpleTriggerDescription extends TriggerDescription { + + private final SimpleTrigger trigger; + + public SimpleTriggerDescription(SimpleTrigger trigger) { + super(trigger, TriggerType.SIMPLE); + this.trigger = trigger; + } + + @Override + protected void appendSummary(Map content) { + content.put("interval", this.trigger.getRepeatInterval()); + } + + @Override + protected void appendDetails(Map content) { + appendSummary(content); + content.put("repeatCount", this.trigger.getRepeatCount()); + content.put("timesTriggered", this.trigger.getTimesTriggered()); + } + + } + + /** + * A description of a {@link DailyTimeIntervalTrigger}. + */ + public static final class DailyTimeIntervalTriggerDescription extends TriggerDescription { + + private final DailyTimeIntervalTrigger trigger; + + public DailyTimeIntervalTriggerDescription(DailyTimeIntervalTrigger trigger) { + super(trigger, TriggerType.DAILY_INTERVAL); + this.trigger = trigger; + } + + @Override + protected void appendSummary(Map content) { + content.put("interval", + getIntervalDuration(this.trigger.getRepeatInterval(), this.trigger.getRepeatIntervalUnit()) + .toMillis()); + putIfNoNull(content, "daysOfWeek", this.trigger.getDaysOfWeek()); + putIfNoNull(content, "startTimeOfDay", getLocalTime(this.trigger.getStartTimeOfDay())); + putIfNoNull(content, "endTimeOfDay", getLocalTime(this.trigger.getEndTimeOfDay())); + } + + @Override + protected void appendDetails(Map content) { + appendSummary(content); + content.put("repeatCount", this.trigger.getRepeatCount()); + content.put("timesTriggered", this.trigger.getTimesTriggered()); + } + + } + + /** + * A description of a {@link CalendarIntervalTrigger}. + */ + public static final class CalendarIntervalTriggerDescription extends TriggerDescription { + + private final CalendarIntervalTrigger trigger; + + public CalendarIntervalTriggerDescription(CalendarIntervalTrigger trigger) { + super(trigger, TriggerType.CALENDAR_INTERVAL); + this.trigger = trigger; + } + + @Override + protected void appendSummary(Map content) { + content.put("interval", + getIntervalDuration(this.trigger.getRepeatInterval(), this.trigger.getRepeatIntervalUnit()) + .toMillis()); + putIfNoNull(content, "timeZone", this.trigger.getTimeZone()); + } + + @Override + protected void appendDetails(Map content) { + appendSummary(content); + content.put("timesTriggered", this.trigger.getTimesTriggered()); + content.put("preserveHourOfDayAcrossDaylightSavings", + this.trigger.isPreserveHourOfDayAcrossDaylightSavings()); + content.put("skipDayIfHourDoesNotExist", this.trigger.isSkipDayIfHourDoesNotExist()); + } + + } + + /** + * A description of a custom {@link Trigger}. + */ + public static final class CustomTriggerDescription extends TriggerDescription { + + public CustomTriggerDescription(Trigger trigger) { + super(trigger, TriggerType.CUSTOM_TRIGGER); + } + + @Override + protected void appendSummary(Map content) { + content.put("trigger", getTrigger().toString()); + } + + @Override + protected void appendDetails(Map content) { + appendSummary(content); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/quartz/QuartzEndpointWebExtension.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/quartz/QuartzEndpointWebExtension.java new file mode 100644 index 000000000000..ab52b8707dd2 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/quartz/QuartzEndpointWebExtension.java @@ -0,0 +1,87 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.quartz; + +import org.quartz.SchedulerException; + +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.annotation.Selector; +import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; +import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension; +import org.springframework.boot.actuate.quartz.QuartzEndpoint.QuartzGroups; + +/** + * {@link EndpointWebExtension @EndpointWebExtension} for the {@link QuartzEndpoint}. + * + * @author Stephane Nicoll + * @since 2.5.0 + */ +@EndpointWebExtension(endpoint = QuartzEndpoint.class) +public class QuartzEndpointWebExtension { + + private final QuartzEndpoint delegate; + + public QuartzEndpointWebExtension(QuartzEndpoint delegate) { + this.delegate = delegate; + } + + @ReadOperation + public WebEndpointResponse quartzJobOrTriggerGroups(@Selector String jobsOrTriggers) + throws SchedulerException { + return handle(jobsOrTriggers, this.delegate::quartzJobGroups, this.delegate::quartzTriggerGroups); + } + + @ReadOperation + public WebEndpointResponse quartzJobOrTriggerGroup(@Selector String jobsOrTriggers, @Selector String group) + throws SchedulerException { + return handle(jobsOrTriggers, () -> this.delegate.quartzJobGroupSummary(group), + () -> this.delegate.quartzTriggerGroupSummary(group)); + } + + @ReadOperation + public WebEndpointResponse quartzJobOrTrigger(@Selector String jobsOrTriggers, @Selector String group, + @Selector String name) throws SchedulerException { + return handle(jobsOrTriggers, () -> this.delegate.quartzJob(group, name), + () -> this.delegate.quartzTrigger(group, name)); + } + + private WebEndpointResponse handle(String jobsOrTriggers, ResponseSupplier jobAction, + ResponseSupplier triggerAction) throws SchedulerException { + if ("jobs".equals(jobsOrTriggers)) { + return handleNull(jobAction.get()); + } + if ("triggers".equals(jobsOrTriggers)) { + return handleNull(triggerAction.get()); + } + return new WebEndpointResponse<>(WebEndpointResponse.STATUS_BAD_REQUEST); + } + + private WebEndpointResponse handleNull(T value) { + if (value != null) { + return new WebEndpointResponse<>(value); + } + return new WebEndpointResponse<>(WebEndpointResponse.STATUS_NOT_FOUND); + } + + @FunctionalInterface + private interface ResponseSupplier { + + T get() throws SchedulerException; + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/quartz/package-info.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/quartz/package-info.java new file mode 100644 index 000000000000..c6d38639523f --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/quartz/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Actuator support for Quartz Scheduler. + */ +package org.springframework.boot.actuate.quartz; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/r2dbc/ConnectionFactoryHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/r2dbc/ConnectionFactoryHealthIndicator.java new file mode 100644 index 000000000000..55417c7f4ee5 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/r2dbc/ConnectionFactoryHealthIndicator.java @@ -0,0 +1,104 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.r2dbc; + +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.Row; +import io.r2dbc.spi.RowMetadata; +import io.r2dbc.spi.ValidationDepth; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.Health.Builder; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.actuate.health.Status; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * A {@link HealthIndicator} to validate a R2DBC {@link ConnectionFactory}. + * + * @author Mark Paluch + * @author Stephane Nicoll + * @since 2.3.0 + */ +public class ConnectionFactoryHealthIndicator extends AbstractReactiveHealthIndicator { + + private final ConnectionFactory connectionFactory; + + private final String validationQuery; + + /** + * Create a new {@link ConnectionFactoryHealthIndicator} using the specified + * {@link ConnectionFactory} and no validation query. + * @param connectionFactory the connection factory + * @see Connection#validate(ValidationDepth) + */ + public ConnectionFactoryHealthIndicator(ConnectionFactory connectionFactory) { + this(connectionFactory, null); + } + + /** + * Create a new {@link ConnectionFactoryHealthIndicator} using the specified + * {@link ConnectionFactory} and validation query. + * @param connectionFactory the connection factory + * @param validationQuery the validation query, can be {@code null} to use connection + * validation + */ + public ConnectionFactoryHealthIndicator(ConnectionFactory connectionFactory, String validationQuery) { + Assert.notNull(connectionFactory, "ConnectionFactory must not be null"); + this.connectionFactory = connectionFactory; + this.validationQuery = validationQuery; + } + + @Override + protected final Mono doHealthCheck(Builder builder) { + return validate(builder).defaultIfEmpty(builder.build()).onErrorResume(Exception.class, + (ex) -> Mono.just(builder.down(ex).build())); + } + + private Mono validate(Builder builder) { + builder.withDetail("database", this.connectionFactory.getMetadata().getName()); + return (StringUtils.hasText(this.validationQuery)) ? validateWithQuery(builder) + : validateWithConnectionValidation(builder); + } + + private Mono validateWithQuery(Builder builder) { + builder.withDetail("validationQuery", this.validationQuery); + Mono connectionValidation = Mono.usingWhen(this.connectionFactory.create(), + (conn) -> Flux.from(conn.createStatement(this.validationQuery).execute()) + .flatMap((it) -> it.map(this::extractResult)).next(), + Connection::close, (o, throwable) -> o.close(), Connection::close); + return connectionValidation.map((result) -> builder.up().withDetail("result", result).build()); + } + + private Mono validateWithConnectionValidation(Builder builder) { + builder.withDetail("validationQuery", "validate(REMOTE)"); + Mono connectionValidation = Mono.usingWhen(this.connectionFactory.create(), + (connection) -> Mono.from(connection.validate(ValidationDepth.REMOTE)), Connection::close, + (connection, ex) -> connection.close(), Connection::close); + return connectionValidation.map((valid) -> builder.status((valid) ? Status.UP : Status.DOWN).build()); + } + + private Object extractResult(Row row, RowMetadata metadata) { + return row.get(metadata.getColumnMetadatas().iterator().next().getName()); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/r2dbc/package-info.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/r2dbc/package-info.java new file mode 100644 index 000000000000..f40b99451b74 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/r2dbc/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Actuator support for R2DBC. + */ +package org.springframework.boot.actuate.r2dbc; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/redis/RedisHealth.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/redis/RedisHealth.java new file mode 100644 index 000000000000..943c2a19aa79 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/redis/RedisHealth.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.redis; + +import java.util.Properties; + +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.Health.Builder; +import org.springframework.data.redis.connection.ClusterInfo; + +/** + * Shared class used by {@link RedisHealthIndicator} and + * {@link RedisReactiveHealthIndicator} to provide health details. + * + * @author Phillip Webb + */ +final class RedisHealth { + + private RedisHealth() { + } + + static Builder up(Health.Builder builder, Properties info) { + builder.withDetail("version", info.getProperty("redis_version")); + return builder.up(); + } + + static Builder fromClusterInfo(Health.Builder builder, ClusterInfo clusterInfo) { + builder.withDetail("cluster_size", clusterInfo.getClusterSize()); + builder.withDetail("slots_up", clusterInfo.getSlotsOk()); + builder.withDetail("slots_fail", clusterInfo.getSlotsFail()); + + if ("fail".equalsIgnoreCase(clusterInfo.getState())) { + return builder.down(); + } + else { + return builder.up(); + } + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/redis/RedisHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/redis/RedisHealthIndicator.java index 83648a9f5941..63486870c234 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/redis/RedisHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/redis/RedisHealthIndicator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,9 @@ package org.springframework.boot.actuate.redis; -import java.util.Properties; - import org.springframework.boot.actuate.health.AbstractHealthIndicator; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; -import org.springframework.data.redis.connection.ClusterInfo; import org.springframework.data.redis.connection.RedisClusterConnection; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; @@ -34,14 +31,11 @@ * * @author Christian Dupuis * @author Richard Santana + * @author Scott Frederick * @since 2.0.0 */ public class RedisHealthIndicator extends AbstractHealthIndicator { - static final String VERSION = "version"; - - static final String REDIS_VERSION = "redis_version"; - private final RedisConnectionFactory redisConnectionFactory; public RedisHealthIndicator(RedisConnectionFactory connectionFactory) { @@ -54,19 +48,19 @@ public RedisHealthIndicator(RedisConnectionFactory connectionFactory) { protected void doHealthCheck(Health.Builder builder) throws Exception { RedisConnection connection = RedisConnectionUtils.getConnection(this.redisConnectionFactory); try { - if (connection instanceof RedisClusterConnection) { - ClusterInfo clusterInfo = ((RedisClusterConnection) connection).clusterGetClusterInfo(); - builder.up().withDetail("cluster_size", clusterInfo.getClusterSize()) - .withDetail("slots_up", clusterInfo.getSlotsOk()) - .withDetail("slots_fail", clusterInfo.getSlotsFail()); - } - else { - Properties info = connection.info(); - builder.up().withDetail(VERSION, info.getProperty(REDIS_VERSION)); - } + doHealthCheck(builder, connection); } finally { - RedisConnectionUtils.releaseConnection(connection, this.redisConnectionFactory, false); + RedisConnectionUtils.releaseConnection(connection, this.redisConnectionFactory); + } + } + + private void doHealthCheck(Health.Builder builder, RedisConnection connection) { + if (connection instanceof RedisClusterConnection) { + RedisHealth.fromClusterInfo(builder, ((RedisClusterConnection) connection).clusterGetClusterInfo()); + } + else { + RedisHealth.up(builder, connection.info("server")); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/redis/RedisReactiveHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/redis/RedisReactiveHealthIndicator.java index 7708fa878093..e91553170828 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/redis/RedisReactiveHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/redis/RedisReactiveHealthIndicator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,8 @@ import org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.ReactiveHealthIndicator; +import org.springframework.data.redis.connection.ClusterInfo; +import org.springframework.data.redis.connection.ReactiveRedisClusterConnection; import org.springframework.data.redis.connection.ReactiveRedisConnection; import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory; @@ -33,6 +35,7 @@ * @author Stephane Nicoll * @author Mark Paluch * @author Artsiom Yudovin + * @author Scott Frederick * @since 2.0.0 */ public class RedisReactiveHealthIndicator extends AbstractReactiveHealthIndicator { @@ -49,24 +52,30 @@ protected Mono doHealthCheck(Health.Builder builder) { return getConnection().flatMap((connection) -> doHealthCheck(builder, connection)); } + private Mono getConnection() { + return Mono.fromSupplier(this.connectionFactory::getReactiveConnection) + .subscribeOn(Schedulers.boundedElastic()); + } + private Mono doHealthCheck(Health.Builder builder, ReactiveRedisConnection connection) { - return connection.serverCommands().info().map((info) -> up(builder, info)) - .onErrorResume((ex) -> Mono.just(down(builder, ex))) + return getHealth(builder, connection).onErrorResume((ex) -> Mono.just(builder.down(ex).build())) .flatMap((health) -> connection.closeLater().thenReturn(health)); } - private Mono getConnection() { - return Mono.fromSupplier(this.connectionFactory::getReactiveConnection) - .subscribeOn(Schedulers.boundedElastic()); + private Mono getHealth(Health.Builder builder, ReactiveRedisConnection connection) { + if (connection instanceof ReactiveRedisClusterConnection) { + return ((ReactiveRedisClusterConnection) connection).clusterGetClusterInfo() + .map((info) -> fromClusterInfo(builder, info)); + } + return connection.serverCommands().info("server").map((info) -> up(builder, info)); } private Health up(Health.Builder builder, Properties info) { - return builder.up() - .withDetail(RedisHealthIndicator.VERSION, info.getProperty(RedisHealthIndicator.REDIS_VERSION)).build(); + return RedisHealth.up(builder, info).build(); } - private Health down(Health.Builder builder, Throwable cause) { - return builder.down(cause).build(); + private Health fromClusterInfo(Health.Builder builder, ClusterInfo clusterInfo) { + return RedisHealth.fromClusterInfo(builder, clusterInfo).build(); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/solr/SolrHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/solr/SolrHealthIndicator.java index 205fb9abe216..83d44821944a 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/solr/SolrHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/solr/SolrHealthIndicator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -59,7 +59,7 @@ protected void doHealthCheck(Health.Builder builder) throws Exception { private int initializeStatusCheck() throws Exception { StatusCheck statusCheck = this.statusCheck; if (statusCheck != null) { - // Already initilized + // Already initialized return statusCheck.getStatus(this.solrClient); } try { diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/startup/StartupEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/startup/StartupEndpoint.java new file mode 100644 index 000000000000..51b0412a651c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/startup/StartupEndpoint.java @@ -0,0 +1,86 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.startup; + +import org.springframework.boot.SpringBootVersion; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.annotation.WriteOperation; +import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup; +import org.springframework.boot.context.metrics.buffering.StartupTimeline; + +/** + * {@link Endpoint @Endpoint} to expose the timeline of the + * {@link org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup + * application startup}. + * + * @author Brian Clozel + * @author Chris Bono + * @since 2.4.0 + */ +@Endpoint(id = "startup") +public class StartupEndpoint { + + private final BufferingApplicationStartup applicationStartup; + + /** + * Creates a new {@code StartupEndpoint} that will describe the timeline of buffered + * application startup events. + * @param applicationStartup the application startup + */ + public StartupEndpoint(BufferingApplicationStartup applicationStartup) { + this.applicationStartup = applicationStartup; + } + + @ReadOperation + public StartupResponse startupSnapshot() { + StartupTimeline startupTimeline = this.applicationStartup.getBufferedTimeline(); + return new StartupResponse(startupTimeline); + } + + @WriteOperation + public StartupResponse startup() { + StartupTimeline startupTimeline = this.applicationStartup.drainBufferedTimeline(); + return new StartupResponse(startupTimeline); + } + + /** + * A description of an application startup, primarily intended for serialization to + * JSON. + */ + public static final class StartupResponse { + + private final String springBootVersion; + + private final StartupTimeline timeline; + + private StartupResponse(StartupTimeline timeline) { + this.timeline = timeline; + this.springBootVersion = SpringBootVersion.getVersion(); + } + + public String getSpringBootVersion() { + return this.springBootVersion; + } + + public StartupTimeline getTimeline() { + return this.timeline; + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/startup/package-info.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/startup/package-info.java new file mode 100644 index 000000000000..ca8f82edbbae --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/startup/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Actuator support for {@link org.springframework.core.metrics.ApplicationStartup}. + */ +package org.springframework.boot.actuate.startup; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/system/DiskSpaceHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/system/DiskSpaceHealthIndicator.java index ae1f38f38bc4..bf3919a5e565 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/system/DiskSpaceHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/system/DiskSpaceHealthIndicator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -68,7 +68,7 @@ protected void doHealthCheck(Health.Builder builder) throws Exception { builder.down(); } builder.withDetail("total", this.path.getTotalSpace()).withDetail("free", diskFreeInBytes) - .withDetail("threshold", this.threshold.toBytes()); + .withDetail("threshold", this.threshold.toBytes()).withDetail("exists", this.path.exists()); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/HttpExchangeTracer.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/HttpExchangeTracer.java index 6929e9535d8d..97917c0cf578 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/HttpExchangeTracer.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/HttpExchangeTracer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.function.Supplier; @@ -68,8 +69,7 @@ public final HttpTrace receivedRequest(TraceableRequest request) { */ public final void sendingResponse(HttpTrace trace, TraceableResponse response, Supplier principal, Supplier sessionId) { - setIfIncluded(Include.TIME_TAKEN, () -> System.currentTimeMillis() - trace.getTimestamp().toEpochMilli(), - trace::setTimeTaken); + setIfIncluded(Include.TIME_TAKEN, () -> calculateTimeTaken(trace), trace::setTimeTaken); setIfIncluded(Include.SESSION_ID, sessionId, trace::setSessionId); setIfIncluded(Include.PRINCIPAL, principal, trace::setPrincipal); trace.setResponse(new HttpTrace.Response(new FilteredTraceableResponse(response))); @@ -102,6 +102,10 @@ private Map> getHeadersIfIncluded(Include include, .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } + private long calculateTimeTaken(HttpTrace trace) { + return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - trace.getStartNanoTime()); + } + private final class FilteredTraceableRequest implements TraceableRequest { private final TraceableRequest delegate; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/HttpTrace.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/HttpTrace.java index 7ef35f5c079a..1341ae9fab31 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/HttpTrace.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/HttpTrace.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,6 +46,8 @@ public final class HttpTrace { private volatile Long timeTaken; + private final long startNanoTime; + /** * Creates a fully-configured {@code HttpTrace} instance. Primarily for use by * {@link HttpTraceRepository} implementations when recreating a trace from a @@ -67,11 +69,13 @@ public HttpTrace(Request request, Response response, Instant timestamp, Principa this.principal = principal; this.session = session; this.timeTaken = timeTaken; + this.startNanoTime = 0; } HttpTrace(TraceableRequest request) { this.request = new Request(request); this.timestamp = Instant.now(); + this.startNanoTime = System.nanoTime(); } public Instant getTimestamp() { @@ -118,6 +122,10 @@ void setTimeTaken(long timeTaken) { this.timeTaken = timeTaken; } + long getStartNanoTime() { + return this.startNanoTime; + } + /** * Trace of an HTTP request. */ diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/Include.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/Include.java index a33ef0bf2aec..86e0ae5d3a9c 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/Include.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/Include.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,8 @@ * Include options for HTTP tracing. * * @author Wallace Wadge + * @author Emily Tsanova + * @author Joseph Beeton * @since 2.0.0 */ public enum Include { @@ -75,7 +77,6 @@ public enum Include { Set defaultIncludes = new LinkedHashSet<>(); defaultIncludes.add(Include.REQUEST_HEADERS); defaultIncludes.add(Include.RESPONSE_HEADERS); - defaultIncludes.add(Include.COOKIE_HEADERS); defaultIncludes.add(Include.TIME_TAKEN); DEFAULT_INCLUDES = Collections.unmodifiableSet(defaultIncludes); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/TraceableRequest.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/TraceableRequest.java index 16257e6ee730..4dbe55a23ccb 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/TraceableRequest.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/trace/http/TraceableRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ public interface TraceableRequest { /** - * Returns the method (GET, POST, etc) of the request. + * Returns the method (GET, POST, etc.) of the request. * @return the method */ String getMethod(); diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/reactive/DispatcherHandlersMappingDescriptionProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/reactive/DispatcherHandlersMappingDescriptionProvider.java index fea1618eb997..49ef36118511 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/reactive/DispatcherHandlersMappingDescriptionProvider.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/reactive/DispatcherHandlersMappingDescriptionProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -182,6 +182,12 @@ public void route(RequestPredicate predicate, HandlerFunction handlerFunction public void resources(Function> lookupFunction) { } + @Override + public void attributes(Map attributes) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Auto-generated method stub"); + } + @Override public void unknown(RouterFunction routerFunction) { } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/reactive/HandlerFunctionDescription.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/reactive/HandlerFunctionDescription.java index 044e27da9c3c..694e69a98ade 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/reactive/HandlerFunctionDescription.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/reactive/HandlerFunctionDescription.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,13 @@ public class HandlerFunctionDescription { private final String className; HandlerFunctionDescription(HandlerFunction handlerFunction) { - this.className = handlerFunction.getClass().getCanonicalName(); + this.className = getHandlerFunctionClassName(handlerFunction); + } + + private static String getHandlerFunctionClassName(HandlerFunction handlerFunction) { + Class functionClass = handlerFunction.getClass(); + String canonicalName = functionClass.getCanonicalName(); + return (canonicalName != null) ? canonicalName : functionClass.getName(); } public String getClassName() { diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/servlet/DispatcherServletsMappingDescriptionProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/servlet/DispatcherServletsMappingDescriptionProvider.java index 0d40fa520160..8644d8366b0e 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/servlet/DispatcherServletsMappingDescriptionProvider.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/servlet/DispatcherServletsMappingDescriptionProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,8 +32,6 @@ import org.springframework.boot.actuate.web.mappings.MappingDescriptionProvider; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.ApplicationContext; -import org.springframework.data.rest.webmvc.support.DelegatingHandlerMapping; -import org.springframework.util.ClassUtils; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.DispatcherServlet; @@ -53,15 +51,13 @@ */ public class DispatcherServletsMappingDescriptionProvider implements MappingDescriptionProvider { - private static final List> descriptionProviders; + private static final List> descriptionProviders; static { - List> providers = new ArrayList<>(); + List> providers = new ArrayList<>(); providers.add(new RequestMappingInfoHandlerMappingDescriptionProvider()); providers.add(new UrlHandlerMappingDescriptionProvider()); - if (ClassUtils.isPresent("org.springframework.data.rest.webmvc.support.DelegatingHandlerMapping", null)) { - providers.add(new DelegatingHandlerMappingDescriptionProvider(new ArrayList<>(providers))); - } + providers.add(new IterableDelegatesHandlerMappingDescriptionProvider(new ArrayList<>(providers))); descriptionProviders = Collections.unmodifiableList(providers); } @@ -105,12 +101,12 @@ private List describeMappings(DispatcherSer return mappings.getHandlerMappings().stream().flatMap(this::describe).collect(Collectors.toList()); } - private Stream describe(T handlerMapping) { + private Stream describe(T handlerMapping) { return describe(handlerMapping, descriptionProviders).stream(); } @SuppressWarnings("unchecked") - private static List describe(T handlerMapping, + private static List describe(T handlerMapping, List> descriptionProviders) { for (HandlerMappingDescriptionProvider descriptionProvider : descriptionProviders) { if (descriptionProvider.getMappingClass().isInstance(handlerMapping)) { @@ -120,7 +116,7 @@ private static List { + private interface HandlerMappingDescriptionProvider { Class getMappingClass(); @@ -171,25 +167,26 @@ private DispatcherServletMappingDescription describe(Entry mappi } - private static final class DelegatingHandlerMappingDescriptionProvider - implements HandlerMappingDescriptionProvider { + @SuppressWarnings("rawtypes") + private static final class IterableDelegatesHandlerMappingDescriptionProvider + implements HandlerMappingDescriptionProvider { private final List> descriptionProviders; - private DelegatingHandlerMappingDescriptionProvider( + private IterableDelegatesHandlerMappingDescriptionProvider( List> descriptionProviders) { this.descriptionProviders = descriptionProviders; } @Override - public Class getMappingClass() { - return DelegatingHandlerMapping.class; + public Class getMappingClass() { + return Iterable.class; } @Override - public List describe(DelegatingHandlerMapping handlerMapping) { + public List describe(Iterable handlerMapping) { List descriptions = new ArrayList<>(); - for (HandlerMapping delegate : handlerMapping.getDelegates()) { + for (Object delegate : handlerMapping) { descriptions.addAll( DispatcherServletsMappingDescriptionProvider.describe(delegate, this.descriptionProviders)); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/servlet/FilterRegistrationMappingDescription.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/servlet/FilterRegistrationMappingDescription.java index 8943b3d1bf51..61a4cd503c32 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/servlet/FilterRegistrationMappingDescription.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/servlet/FilterRegistrationMappingDescription.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,7 +42,7 @@ public FilterRegistrationMappingDescription(FilterRegistration filterRegistratio * @return the mappings */ public Collection getServletNameMappings() { - return this.getRegistration().getServletNameMappings(); + return getRegistration().getServletNameMappings(); } /** @@ -50,7 +50,7 @@ public Collection getServletNameMappings() { * @return the mappings */ public Collection getUrlPatternMappings() { - return this.getRegistration().getUrlPatternMappings(); + return getRegistration().getUrlPatternMappings(); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/servlet/RequestMappingConditionsDescription.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/servlet/RequestMappingConditionsDescription.java index f8c21f03769b..46cafd0474ed 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/servlet/RequestMappingConditionsDescription.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/web/mappings/servlet/RequestMappingConditionsDescription.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.servlet.mvc.condition.MediaTypeExpression; import org.springframework.web.servlet.mvc.condition.NameValueExpression; +import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; /** @@ -53,11 +54,17 @@ public class RequestMappingConditionsDescription { this.methods = requestMapping.getMethodsCondition().getMethods(); this.params = requestMapping.getParamsCondition().getExpressions().stream() .map(NameValueExpressionDescription::new).collect(Collectors.toList()); - this.patterns = requestMapping.getPatternsCondition().getPatterns(); + this.patterns = extractPathPatterns(requestMapping); this.produces = requestMapping.getProducesCondition().getExpressions().stream() .map(MediaTypeExpressionDescription::new).collect(Collectors.toList()); } + private Set extractPathPatterns(RequestMappingInfo requestMapping) { + PatternsRequestCondition patternsCondition = requestMapping.getPatternsCondition(); + return (patternsCondition != null) ? patternsCondition.getPatterns() + : requestMapping.getPathPatternsCondition().getPatternValues(); + } + public List getConsumes() { return this.consumes; } diff --git a/spring-boot-project/spring-boot-actuator/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-actuator/src/main/resources/META-INF/additional-spring-configuration-metadata.json index c50a6d86e8d1..cd08d79f2272 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-actuator/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -1,4 +1,5 @@ { + "groups": [], "properties": [ { "name": "management.endpoints.migrate-legacy-ids", @@ -6,5 +7,6 @@ "description": "Whether to transparently migrate legacy endpoint IDs.", "defaultValue": false } - ] + ], + "hints": [] } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/amqp/RabbitHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/amqp/RabbitHealthIndicatorTests.java index 4beabea7bf0d..0e7ab62d5cf5 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/amqp/RabbitHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/amqp/RabbitHealthIndicatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,10 +20,10 @@ import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.amqp.rabbit.core.ChannelCallback; import org.springframework.amqp.rabbit.core.RabbitTemplate; @@ -41,6 +41,7 @@ * * @author Phillip Webb */ +@ExtendWith(MockitoExtension.class) class RabbitHealthIndicatorTests { @Mock @@ -49,15 +50,6 @@ class RabbitHealthIndicatorTests { @Mock private Channel channel; - @BeforeEach - void setup() { - MockitoAnnotations.initMocks(this); - given(this.rabbitTemplate.execute(any())).willAnswer((invocation) -> { - ChannelCallback callback = invocation.getArgument(0); - return callback.doInRabbit(this.channel); - }); - } - @Test void createWhenRabbitTemplateIsNullShouldThrowException() { assertThatIllegalArgumentException().isThrownBy(() -> new RabbitHealthIndicator(null)) @@ -66,6 +58,7 @@ void createWhenRabbitTemplateIsNullShouldThrowException() { @Test void healthWhenConnectionSucceedsShouldReturnUpWithVersion() { + givenTemplateExecutionWillInvokeCallback(); Connection connection = mock(Connection.class); given(this.channel.getConnection()).willReturn(connection); given(connection.getServerProperties()).willReturn(Collections.singletonMap("version", "123")); @@ -76,9 +69,17 @@ void healthWhenConnectionSucceedsShouldReturnUpWithVersion() { @Test void healthWhenConnectionFailsShouldReturnDown() { + givenTemplateExecutionWillInvokeCallback(); given(this.channel.getConnection()).willThrow(new RuntimeException()); Health health = new RabbitHealthIndicator(this.rabbitTemplate).health(); assertThat(health.getStatus()).isEqualTo(Status.DOWN); } + private void givenTemplateExecutionWillInvokeCallback() { + given(this.rabbitTemplate.execute(any())).willAnswer((invocation) -> { + ChannelCallback callback = invocation.getArgument(0); + return callback.doInRabbit(this.channel); + }); + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/audit/listener/AuditListenerTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/audit/listener/AuditListenerTests.java index f241a4f03159..6984a24a8a53 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/audit/listener/AuditListenerTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/audit/listener/AuditListenerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,8 +23,8 @@ import org.springframework.boot.actuate.audit.AuditEvent; import org.springframework.boot.actuate.audit.AuditEventRepository; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link AuditListener}. @@ -39,7 +39,7 @@ void testStoredEvents() { AuditEvent event = new AuditEvent("principal", "type", Collections.emptyMap()); AuditListener listener = new AuditListener(repository); listener.onApplicationEvent(new AuditApplicationEvent(event)); - verify(repository).add(event); + then(repository).should().add(event); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/availability/AvailabilityStateHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/availability/AvailabilityStateHealthIndicatorTests.java new file mode 100644 index 000000000000..e7823e4f6cd0 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/availability/AvailabilityStateHealthIndicatorTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.availability; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.boot.actuate.health.Status; +import org.springframework.boot.availability.ApplicationAvailability; +import org.springframework.boot.availability.AvailabilityState; +import org.springframework.boot.availability.LivenessState; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; + +/** + * Tests for {@link AvailabilityStateHealthIndicator}. + * + * @author Phillip Webb + */ +@ExtendWith(MockitoExtension.class) +class AvailabilityStateHealthIndicatorTests { + + @Mock + private ApplicationAvailability applicationAvailability; + + @Test + void createWhenApplicationAvailabilityIsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new AvailabilityStateHealthIndicator(null, LivenessState.class, (statusMappings) -> { + })).withMessage("ApplicationAvailability must not be null"); + } + + @Test + void createWhenStateTypeIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy( + () -> new AvailabilityStateHealthIndicator(this.applicationAvailability, null, (statusMappings) -> { + })).withMessage("StateType must not be null"); + } + + @Test + void createWhenStatusMappingIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy( + () -> new AvailabilityStateHealthIndicator(this.applicationAvailability, LivenessState.class, null)) + .withMessage("StatusMappings must not be null"); + } + + @Test + void createWhenStatusMappingDoesNotCoverAllEnumsThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new AvailabilityStateHealthIndicator(this.applicationAvailability, + LivenessState.class, (statusMappings) -> statusMappings.add(LivenessState.CORRECT, Status.UP))) + .withMessage("StatusMappings does not include BROKEN"); + } + + @Test + void healthReturnsMappedStatus() { + AvailabilityStateHealthIndicator indicator = new AvailabilityStateHealthIndicator(this.applicationAvailability, + LivenessState.class, (statusMappings) -> { + statusMappings.add(LivenessState.CORRECT, Status.UP); + statusMappings.add(LivenessState.BROKEN, Status.DOWN); + }); + given(this.applicationAvailability.getState(LivenessState.class)).willReturn(LivenessState.BROKEN); + assertThat(indicator.getHealth(false).getStatus()).isEqualTo(Status.DOWN); + } + + @Test + void healthReturnsDefaultStatus() { + AvailabilityStateHealthIndicator indicator = new AvailabilityStateHealthIndicator(this.applicationAvailability, + LivenessState.class, (statusMappings) -> { + statusMappings.add(LivenessState.CORRECT, Status.UP); + statusMappings.addDefaultStatus(Status.UNKNOWN); + }); + given(this.applicationAvailability.getState(LivenessState.class)).willReturn(LivenessState.BROKEN); + assertThat(indicator.getHealth(false).getStatus()).isEqualTo(Status.UNKNOWN); + } + + @Test + void healthWhenNotEnumReturnsMappedStatus() { + AvailabilityStateHealthIndicator indicator = new AvailabilityStateHealthIndicator(this.applicationAvailability, + TestAvailabilityState.class, (statusMappings) -> { + statusMappings.add(TestAvailabilityState.ONE, Status.UP); + statusMappings.addDefaultStatus(Status.DOWN); + }); + given(this.applicationAvailability.getState(TestAvailabilityState.class)).willReturn(TestAvailabilityState.TWO); + assertThat(indicator.getHealth(false).getStatus()).isEqualTo(Status.DOWN); + } + + static class TestAvailabilityState implements AvailabilityState { + + static final TestAvailabilityState ONE = new TestAvailabilityState(); + + static final TestAvailabilityState TWO = new TestAvailabilityState(); + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/availability/LivenessStateHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/availability/LivenessStateHealthIndicatorTests.java new file mode 100644 index 000000000000..4770fa64ca9c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/availability/LivenessStateHealthIndicatorTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.availability; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.health.Status; +import org.springframework.boot.availability.ApplicationAvailability; +import org.springframework.boot.availability.LivenessState; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link LivenessStateHealthIndicator} + * + * @author Brian Clozel + */ +class LivenessStateHealthIndicatorTests { + + private ApplicationAvailability availability; + + private LivenessStateHealthIndicator healthIndicator; + + @BeforeEach + void setUp() { + this.availability = mock(ApplicationAvailability.class); + this.healthIndicator = new LivenessStateHealthIndicator(this.availability); + } + + @Test + void livenessIsLive() { + given(this.availability.getLivenessState()).willReturn(LivenessState.CORRECT); + assertThat(this.healthIndicator.health().getStatus()).isEqualTo(Status.UP); + } + + @Test + void livenessIsBroken() { + given(this.availability.getLivenessState()).willReturn(LivenessState.BROKEN); + assertThat(this.healthIndicator.health().getStatus()).isEqualTo(Status.DOWN); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/availability/ReadinessStateHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/availability/ReadinessStateHealthIndicatorTests.java new file mode 100644 index 000000000000..7147bc5ad6de --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/availability/ReadinessStateHealthIndicatorTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.availability; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.health.Status; +import org.springframework.boot.availability.ApplicationAvailability; +import org.springframework.boot.availability.ReadinessState; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ReadinessStateHealthIndicator} + * + * @author Brian Clozel + */ +class ReadinessStateHealthIndicatorTests { + + private ApplicationAvailability availability; + + private ReadinessStateHealthIndicator healthIndicator; + + @BeforeEach + void setUp() { + this.availability = mock(ApplicationAvailability.class); + this.healthIndicator = new ReadinessStateHealthIndicator(this.availability); + } + + @Test + void readinessIsReady() { + given(this.availability.getReadinessState()).willReturn(ReadinessState.ACCEPTING_TRAFFIC); + assertThat(this.healthIndicator.health().getStatus()).isEqualTo(Status.UP); + } + + @Test + void readinessIsUnready() { + given(this.availability.getReadinessState()).willReturn(ReadinessState.REFUSING_TRAFFIC); + assertThat(this.healthIndicator.health().getStatus()).isEqualTo(Status.OUT_OF_SERVICE); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cache/CachesEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cache/CachesEndpointTests.java index e05af5ad1e7f..bbbd0e5b42e4 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cache/CachesEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cache/CachesEndpointTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,9 +34,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; /** * Tests for {@link CachesEndpoint}. @@ -126,8 +126,8 @@ void clearAllCaches() { Cache b = mockCache("b"); CachesEndpoint endpoint = new CachesEndpoint(Collections.singletonMap("test", cacheManager(a, b))); endpoint.clearCaches(); - verify(a).clear(); - verify(b).clear(); + then(a).should().clear(); + then(b).should().clear(); } @Test @@ -136,8 +136,8 @@ void clearCache() { Cache b = mockCache("b"); CachesEndpoint endpoint = new CachesEndpoint(Collections.singletonMap("test", cacheManager(a, b))); assertThat(endpoint.clearCache("a", null)).isTrue(); - verify(a).clear(); - verify(b, never()).clear(); + then(a).should().clear(); + then(b).should(never()).clear(); } @Test @@ -161,9 +161,9 @@ void clearCacheWithSeveralCacheManagersWithCacheManagerFilter() { cacheManagers.put("another", cacheManager(anotherA)); CachesEndpoint endpoint = new CachesEndpoint(cacheManagers); assertThat(endpoint.clearCache("a", "another")).isTrue(); - verify(a, never()).clear(); - verify(anotherA).clear(); - verify(b, never()).clear(); + then(a).should(never()).clear(); + then(anotherA).should().clear(); + then(b).should(never()).clear(); } @Test @@ -171,7 +171,7 @@ void clearCacheWithUnknownCache() { Cache a = mockCache("a"); CachesEndpoint endpoint = new CachesEndpoint(Collections.singletonMap("test", cacheManager(a))); assertThat(endpoint.clearCache("unknown", null)).isFalse(); - verify(a, never()).clear(); + then(a).should(never()).clear(); } @Test @@ -179,7 +179,7 @@ void clearCacheWithUnknownCacheManager() { Cache a = mockCache("a"); CachesEndpoint endpoint = new CachesEndpoint(Collections.singletonMap("test", cacheManager(a))); assertThat(endpoint.clearCache("a", "unknown")).isFalse(); - verify(a, never()).clear(); + then(a).should(never()).clear(); } private CacheManager cacheManager(Cache... caches) { diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraDriverHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraDriverHealthIndicatorTests.java new file mode 100644 index 000000000000..f9d8827c8b84 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraDriverHealthIndicatorTests.java @@ -0,0 +1,166 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.cassandra; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.DriverTimeoutException; +import com.datastax.oss.driver.api.core.Version; +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeState; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.Status; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link CassandraDriverHealthIndicator}. + * + * @author Alexandre Dutra + * @author Stephane Nicoll + */ +class CassandraDriverHealthIndicatorTests { + + @Test + void createWhenCqlSessionIsNullShouldThrowException() { + assertThatIllegalArgumentException().isThrownBy(() -> new CassandraDriverHealthIndicator(null)); + } + + @Test + void healthWithOneHealthyNodeShouldReturnUp() { + CqlSession session = mockCqlSessionWithNodeState(NodeState.UP); + CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session); + Health health = healthIndicator.health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + } + + @Test + void healthWithOneUnhealthyNodeShouldReturnDown() { + CqlSession session = mockCqlSessionWithNodeState(NodeState.DOWN); + CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session); + Health health = healthIndicator.health(); + assertThat(health.getStatus()).isEqualTo(Status.DOWN); + } + + @Test + void healthWithOneUnknownNodeShouldReturnDown() { + CqlSession session = mockCqlSessionWithNodeState(NodeState.UNKNOWN); + CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session); + Health health = healthIndicator.health(); + assertThat(health.getStatus()).isEqualTo(Status.DOWN); + } + + @Test + void healthWithOneForcedDownNodeShouldReturnDown() { + CqlSession session = mockCqlSessionWithNodeState(NodeState.FORCED_DOWN); + CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session); + Health health = healthIndicator.health(); + assertThat(health.getStatus()).isEqualTo(Status.DOWN); + } + + @Test + void healthWithOneHealthyNodeAndOneUnhealthyNodeShouldReturnUp() { + CqlSession session = mockCqlSessionWithNodeState(NodeState.UP, NodeState.DOWN); + CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session); + Health health = healthIndicator.health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + } + + @Test + void healthWithOneHealthyNodeAndOneUnknownNodeShouldReturnUp() { + CqlSession session = mockCqlSessionWithNodeState(NodeState.UP, NodeState.UNKNOWN); + CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session); + Health health = healthIndicator.health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + } + + @Test + void healthWithOneHealthyNodeAndOneForcedDownNodeShouldReturnUp() { + CqlSession session = mockCqlSessionWithNodeState(NodeState.UP, NodeState.FORCED_DOWN); + CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session); + Health health = healthIndicator.health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + } + + @Test + void healthWithNodeVersionShouldAddVersionDetail() { + CqlSession session = mock(CqlSession.class); + Metadata metadata = mock(Metadata.class); + given(session.getMetadata()).willReturn(metadata); + Node node = mock(Node.class); + given(node.getState()).willReturn(NodeState.UP); + given(node.getCassandraVersion()).willReturn(Version.V4_0_0); + given(metadata.getNodes()).willReturn(createNodesWithRandomUUID(Collections.singletonList(node))); + CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session); + Health health = healthIndicator.health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertThat(health.getDetails().get("version")).isEqualTo(Version.V4_0_0); + } + + @Test + void healthWithoutNodeVersionShouldNotAddVersionDetail() { + CqlSession session = mockCqlSessionWithNodeState(NodeState.UP); + CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session); + Health health = healthIndicator.health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertThat(health.getDetails().get("version")).isNull(); + } + + @Test + void healthWithcassandraDownShouldReturnDown() { + CqlSession session = mock(CqlSession.class); + given(session.getMetadata()).willThrow(new DriverTimeoutException("Test Exception")); + CassandraDriverHealthIndicator healthIndicator = new CassandraDriverHealthIndicator(session); + Health health = healthIndicator.health(); + assertThat(health.getStatus()).isEqualTo(Status.DOWN); + assertThat(health.getDetails().get("error")) + .isEqualTo(DriverTimeoutException.class.getName() + ": Test Exception"); + } + + private CqlSession mockCqlSessionWithNodeState(NodeState... nodeStates) { + CqlSession session = mock(CqlSession.class); + Metadata metadata = mock(Metadata.class); + List nodes = new ArrayList<>(); + for (NodeState nodeState : nodeStates) { + Node node = mock(Node.class); + given(node.getState()).willReturn(nodeState); + nodes.add(node); + } + given(session.getMetadata()).willReturn(metadata); + given(metadata.getNodes()).willReturn(createNodesWithRandomUUID(nodes)); + return session; + } + + private Map createNodesWithRandomUUID(List nodes) { + Map indexedNodes = new HashMap<>(); + nodes.forEach((node) -> indexedNodes.put(UUID.randomUUID(), node)); + return indexedNodes; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraDriverReactiveHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraDriverReactiveHealthIndicatorTests.java new file mode 100644 index 000000000000..39542e5a2c66 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraDriverReactiveHealthIndicatorTests.java @@ -0,0 +1,184 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.cassandra; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.DriverTimeoutException; +import com.datastax.oss.driver.api.core.Version; +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeState; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.Status; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link CassandraDriverReactiveHealthIndicator}. + * + * @author Alexandre Dutra + * @author Stephane Nicoll + */ +class CassandraDriverReactiveHealthIndicatorTests { + + @Test + void createWhenCqlSessionIsNullShouldThrowException() { + assertThatIllegalArgumentException().isThrownBy(() -> new CassandraDriverReactiveHealthIndicator(null)); + } + + @Test + void healthWithOneHealthyNodeShouldReturnUp() { + CqlSession session = mockCqlSessionWithNodeState(NodeState.UP); + CassandraDriverReactiveHealthIndicator healthIndicator = new CassandraDriverReactiveHealthIndicator(session); + Mono health = healthIndicator.health(); + StepVerifier.create(health).consumeNextWith((h) -> assertThat(h.getStatus()).isEqualTo(Status.UP)) + .verifyComplete(); + } + + @Test + void healthWithOneUnhealthyNodeShouldReturnDown() { + CqlSession session = mockCqlSessionWithNodeState(NodeState.DOWN); + CassandraDriverReactiveHealthIndicator healthIndicator = new CassandraDriverReactiveHealthIndicator(session); + Mono health = healthIndicator.health(); + StepVerifier.create(health).consumeNextWith((h) -> assertThat(h.getStatus()).isEqualTo(Status.DOWN)) + .verifyComplete(); + } + + @Test + void healthWithOneUnknownNodeShouldReturnDown() { + CqlSession session = mockCqlSessionWithNodeState(NodeState.UNKNOWN); + CassandraDriverReactiveHealthIndicator healthIndicator = new CassandraDriverReactiveHealthIndicator(session); + Mono health = healthIndicator.health(); + StepVerifier.create(health).consumeNextWith((h) -> assertThat(h.getStatus()).isEqualTo(Status.DOWN)) + .verifyComplete(); + } + + @Test + void healthWithOneForcedDownNodeShouldReturnDown() { + CqlSession session = mockCqlSessionWithNodeState(NodeState.FORCED_DOWN); + CassandraDriverReactiveHealthIndicator healthIndicator = new CassandraDriverReactiveHealthIndicator(session); + Mono health = healthIndicator.health(); + StepVerifier.create(health).consumeNextWith((h) -> assertThat(h.getStatus()).isEqualTo(Status.DOWN)) + .verifyComplete(); + } + + @Test + void healthWithOneHealthyNodeAndOneUnhealthyNodeShouldReturnUp() { + CqlSession session = mockCqlSessionWithNodeState(NodeState.UP, NodeState.DOWN); + CassandraDriverReactiveHealthIndicator healthIndicator = new CassandraDriverReactiveHealthIndicator(session); + Mono health = healthIndicator.health(); + StepVerifier.create(health).consumeNextWith((h) -> assertThat(h.getStatus()).isEqualTo(Status.UP)) + .verifyComplete(); + } + + @Test + void healthWithOneHealthyNodeAndOneUnknownNodeShouldReturnUp() { + CqlSession session = mockCqlSessionWithNodeState(NodeState.UP, NodeState.UNKNOWN); + CassandraDriverReactiveHealthIndicator healthIndicator = new CassandraDriverReactiveHealthIndicator(session); + Mono health = healthIndicator.health(); + StepVerifier.create(health).consumeNextWith((h) -> assertThat(h.getStatus()).isEqualTo(Status.UP)) + .verifyComplete(); + } + + @Test + void healthWithOneHealthyNodeAndOneForcedDownNodeShouldReturnUp() { + CqlSession session = mockCqlSessionWithNodeState(NodeState.UP, NodeState.FORCED_DOWN); + CassandraDriverReactiveHealthIndicator healthIndicator = new CassandraDriverReactiveHealthIndicator(session); + Mono health = healthIndicator.health(); + StepVerifier.create(health).consumeNextWith((h) -> assertThat(h.getStatus()).isEqualTo(Status.UP)) + .verifyComplete(); + } + + @Test + void healthWithNodeVersionShouldAddVersionDetail() { + CqlSession session = mock(CqlSession.class); + Metadata metadata = mock(Metadata.class); + given(session.getMetadata()).willReturn(metadata); + Node node = mock(Node.class); + given(node.getState()).willReturn(NodeState.UP); + given(node.getCassandraVersion()).willReturn(Version.V4_0_0); + given(metadata.getNodes()).willReturn(createNodesWithRandomUUID(Collections.singletonList(node))); + CassandraDriverReactiveHealthIndicator healthIndicator = new CassandraDriverReactiveHealthIndicator(session); + Mono health = healthIndicator.health(); + StepVerifier.create(health).consumeNextWith((h) -> { + assertThat(h.getStatus()).isEqualTo(Status.UP); + assertThat(h.getDetails()).containsOnlyKeys("version"); + assertThat(h.getDetails().get("version")).isEqualTo(Version.V4_0_0); + }).verifyComplete(); + } + + @Test + void healthWithoutNodeVersionShouldNotAddVersionDetail() { + CqlSession session = mockCqlSessionWithNodeState(NodeState.UP); + CassandraDriverReactiveHealthIndicator healthIndicator = new CassandraDriverReactiveHealthIndicator(session); + Mono health = healthIndicator.health(); + StepVerifier.create(health).consumeNextWith((h) -> { + assertThat(h.getStatus()).isEqualTo(Status.UP); + assertThat(h.getDetails().get("version")).isNull(); + }).verifyComplete(); + } + + @Test + void healthWithCassandraDownShouldReturnDown() { + CqlSession session = mock(CqlSession.class); + given(session.getMetadata()).willThrow(new DriverTimeoutException("Test Exception")); + CassandraDriverReactiveHealthIndicator cassandraReactiveHealthIndicator = new CassandraDriverReactiveHealthIndicator( + session); + Mono health = cassandraReactiveHealthIndicator.health(); + StepVerifier.create(health).consumeNextWith((h) -> { + assertThat(h.getStatus()).isEqualTo(Status.DOWN); + assertThat(h.getDetails()).containsOnlyKeys("error"); + assertThat(h.getDetails().get("error")) + .isEqualTo(DriverTimeoutException.class.getName() + ": Test Exception"); + }).verifyComplete(); + } + + private CqlSession mockCqlSessionWithNodeState(NodeState... nodeStates) { + CqlSession session = mock(CqlSession.class); + Metadata metadata = mock(Metadata.class); + List nodes = new ArrayList<>(); + for (NodeState nodeState : nodeStates) { + Node node = mock(Node.class); + given(node.getState()).willReturn(nodeState); + nodes.add(node); + } + given(session.getMetadata()).willReturn(metadata); + given(metadata.getNodes()).willReturn(createNodesWithRandomUUID(nodes)); + return session; + } + + private Map createNodesWithRandomUUID(List nodes) { + Map indexedNodes = new HashMap<>(); + nodes.forEach((node) -> indexedNodes.put(UUID.randomUUID(), node)); + return indexedNodes; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicatorTests.java index 61889d385a62..8044c99fc327 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraHealthIndicatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,19 +16,19 @@ package org.springframework.boot.actuate.cassandra; -import com.datastax.driver.core.ResultSet; -import com.datastax.driver.core.Row; -import com.datastax.driver.core.querybuilder.Select; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.Status; +import org.springframework.data.cassandra.CassandraInternalException; import org.springframework.data.cassandra.core.CassandraOperations; import org.springframework.data.cassandra.core.cql.CqlOperations; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -36,7 +36,9 @@ * Tests for {@link CassandraHealthIndicator}. * * @author Oleksii Bondar + * @author Stephane Nicoll */ +@Deprecated class CassandraHealthIndicatorTests { @Test @@ -45,34 +47,26 @@ void createWhenCassandraOperationsIsNullShouldThrowException() { } @Test - void verifyHealthStatusWhenExhausted() { + void healthWithCassandraUp() { CassandraOperations cassandraOperations = mock(CassandraOperations.class); CqlOperations cqlOperations = mock(CqlOperations.class); - ResultSet resultSet = mock(ResultSet.class); CassandraHealthIndicator healthIndicator = new CassandraHealthIndicator(cassandraOperations); given(cassandraOperations.getCqlOperations()).willReturn(cqlOperations); - given(cqlOperations.queryForResultSet(any(Select.class))).willReturn(resultSet); - given(resultSet.isExhausted()).willReturn(true); + given(cqlOperations.queryForObject(any(SimpleStatement.class), eq(String.class))).willReturn("1.0.0"); Health health = healthIndicator.health(); assertThat(health.getStatus()).isEqualTo(Status.UP); + assertThat(health.getDetails().get("version")).isEqualTo("1.0.0"); } @Test - void verifyHealthStatusWithVersion() { + void healthWithCassandraDown() { CassandraOperations cassandraOperations = mock(CassandraOperations.class); - CqlOperations cqlOperations = mock(CqlOperations.class); - ResultSet resultSet = mock(ResultSet.class); - Row row = mock(Row.class); + given(cassandraOperations.getCqlOperations()).willThrow(new CassandraInternalException("Connection failed")); CassandraHealthIndicator healthIndicator = new CassandraHealthIndicator(cassandraOperations); - given(cassandraOperations.getCqlOperations()).willReturn(cqlOperations); - given(cqlOperations.queryForResultSet(any(Select.class))).willReturn(resultSet); - given(resultSet.isExhausted()).willReturn(false); - given(resultSet.one()).willReturn(row); - String expectedVersion = "1.0.0"; - given(row.getString(0)).willReturn(expectedVersion); Health health = healthIndicator.health(); - assertThat(health.getStatus()).isEqualTo(Status.UP); - assertThat(health.getDetails().get("version")).isEqualTo(expectedVersion); + assertThat(health.getStatus()).isEqualTo(Status.DOWN); + assertThat(health.getDetails().get("error")) + .isEqualTo(CassandraInternalException.class.getName() + ": Connection failed"); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicatorTests.java index a480f644310d..06b7948d04d3 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cassandra/CassandraReactiveHealthIndicatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.actuate.cassandra; -import com.datastax.driver.core.querybuilder.Select; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -37,12 +38,14 @@ * * @author Artsiom Yudovin */ +@Deprecated class CassandraReactiveHealthIndicatorTests { @Test void testCassandraIsUp() { ReactiveCqlOperations reactiveCqlOperations = mock(ReactiveCqlOperations.class); - given(reactiveCqlOperations.queryForObject(any(Select.class), eq(String.class))).willReturn(Mono.just("6.0.0")); + given(reactiveCqlOperations.queryForObject(any(SimpleStatement.class), eq(String.class))) + .willReturn(Mono.just("6.0.0")); ReactiveCassandraOperations reactiveCassandraOperations = mock(ReactiveCassandraOperations.class); given(reactiveCassandraOperations.getReactiveCqlOperations()).willReturn(reactiveCqlOperations); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointFilteringTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointFilteringTests.java new file mode 100644 index 000000000000..ec2f5c7a0b2a --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointFilteringTests.java @@ -0,0 +1,136 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.context.properties; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ApplicationConfigurationProperties; +import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ContextConfigurationProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConfigurationPropertiesReportEndpoint} when filtering by prefix. + * + * @author Chris Bono + */ +class ConfigurationPropertiesReportEndpointFilteringTests { + + @Test + void filterByPrefixSingleMatch() { + ApplicationContextRunner contextRunner = new ApplicationContextRunner().withUserConfiguration(Config.class) + .withPropertyValues("foo.primary.name:foo1", "foo.secondary.name:foo2", "only.bar.name:solo1"); + contextRunner.run((context) -> { + ConfigurationPropertiesReportEndpoint endpoint = context + .getBean(ConfigurationPropertiesReportEndpoint.class); + ApplicationConfigurationProperties applicationProperties = endpoint + .configurationPropertiesWithPrefix("only.bar"); + assertThat(applicationProperties.getContexts()).containsOnlyKeys(context.getId()); + ContextConfigurationProperties contextProperties = applicationProperties.getContexts().get(context.getId()); + assertThat(contextProperties.getBeans().values()).singleElement().hasFieldOrPropertyWithValue("prefix", + "only.bar"); + }); + } + + @Test + void filterByPrefixMultipleMatches() { + ApplicationContextRunner contextRunner = new ApplicationContextRunner().withUserConfiguration(Config.class) + .withPropertyValues("foo.primary.name:foo1", "foo.secondary.name:foo2", "only.bar.name:solo1"); + contextRunner.run((context) -> { + ConfigurationPropertiesReportEndpoint endpoint = context + .getBean(ConfigurationPropertiesReportEndpoint.class); + ApplicationConfigurationProperties applicationProperties = endpoint + .configurationPropertiesWithPrefix("foo."); + assertThat(applicationProperties.getContexts()).containsOnlyKeys(context.getId()); + ContextConfigurationProperties contextProperties = applicationProperties.getContexts().get(context.getId()); + assertThat(contextProperties.getBeans()).containsOnlyKeys("primaryFoo", "secondaryFoo"); + }); + } + + @Test + void filterByPrefixNoMatches() { + ApplicationContextRunner contextRunner = new ApplicationContextRunner().withUserConfiguration(Config.class) + .withPropertyValues("foo.primary.name:foo1", "foo.secondary.name:foo2", "only.bar.name:solo1"); + contextRunner.run((context) -> { + ConfigurationPropertiesReportEndpoint endpoint = context + .getBean(ConfigurationPropertiesReportEndpoint.class); + ApplicationConfigurationProperties applicationProperties = endpoint + .configurationPropertiesWithPrefix("foo.third"); + assertThat(applicationProperties.getContexts()).containsOnlyKeys(context.getId()); + ContextConfigurationProperties contextProperties = applicationProperties.getContexts().get(context.getId()); + assertThat(contextProperties.getBeans()).isEmpty(); + }); + } + + @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties(Bar.class) + static class Config { + + @Bean + ConfigurationPropertiesReportEndpoint endpoint() { + return new ConfigurationPropertiesReportEndpoint(); + } + + @Bean + @ConfigurationProperties(prefix = "foo.primary") + Foo primaryFoo() { + return new Foo(); + } + + @Bean + @ConfigurationProperties(prefix = "foo.secondary") + Foo secondaryFoo() { + return new Foo(); + } + + } + + public static class Foo { + + private String name = "5150"; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + } + + @ConfigurationProperties(prefix = "only.bar") + public static class Bar { + + private String name = "123456"; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointProxyTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointProxyTests.java index 9a1a3c46f127..61c7a827097f 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointProxyTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointProxyTests.java @@ -16,6 +16,8 @@ package org.springframework.boot.actuate.context.properties; +import java.util.Map; + import javax.sql.DataSource; import org.junit.jupiter.api.Test; @@ -27,6 +29,7 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; @@ -35,6 +38,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; import static org.assertj.core.api.Assertions.assertThat; @@ -44,6 +48,7 @@ * * @author Phillip Webb * @author Andy Wilkinson + * @author Madhura Bhave */ class ConfigurationPropertiesReportEndpointProxyTests { @@ -60,6 +65,19 @@ void testWithProxyClass() { }); } + @Test + void proxiedConstructorBoundPropertiesShouldBeAvailableInReport() { + ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withUserConfiguration(ValidatedConfiguration.class).withPropertyValues("validated.name=baz"); + contextRunner.run((context) -> { + ApplicationConfigurationProperties applicationProperties = context + .getBean(ConfigurationPropertiesReportEndpoint.class).configurationProperties(); + Map properties = applicationProperties.getContexts().get(context.getId()).getBeans() + .values().stream().map(ConfigurationPropertiesBeanDescriptor::getProperties).findFirst().get(); + assertThat(properties.get("name")).isEqualTo("baz"); + }); + } + @Configuration(proxyBeanMethods = false) @EnableTransactionManagement(proxyTargetClass = false) @EnableConfigurationProperties @@ -75,6 +93,11 @@ PlatformTransactionManager transactionManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } + @Bean + MethodValidationPostProcessor testPostProcessor() { + return new MethodValidationPostProcessor(); + } + @Bean DataSource dataSource() { return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.HSQL).build(); @@ -103,4 +126,11 @@ public void execute() { } + @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties(ValidatedConstructorBindingProperties.class) + @Import(Config.class) + static class ValidatedConfiguration { + + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointSerializationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointSerializationTests.java index 531bfa2eb311..db963815e54b 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointSerializationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointSerializationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,12 @@ package org.springframework.boot.actuate.context.properties; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; import java.net.InetAddress; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -28,11 +32,16 @@ import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ApplicationConfigurationProperties; import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ConfigurationPropertiesBeanDescriptor; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConfigurationPropertiesBinding; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.io.InputStreamSource; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; @@ -238,6 +247,43 @@ void hikariDataSourceConfigurationPropertiesBeanCanBeSerialized() { }); } + @Test + @SuppressWarnings("unchecked") + void endpointResponseUsesToStringOfCharSequenceAsPropertyValue() throws IOException { + ApplicationContextRunner contextRunner = new ApplicationContextRunner().withInitializer((context) -> { + ConfigurableEnvironment environment = context.getEnvironment(); + environment.getPropertySources().addFirst(new MapPropertySource("test", + Collections.singletonMap("foo.name", new CharSequenceProperty("Spring Boot")))); + }).withUserConfiguration(FooConfig.class); + contextRunner.run((context) -> { + ConfigurationPropertiesReportEndpoint endpoint = context + .getBean(ConfigurationPropertiesReportEndpoint.class); + ApplicationConfigurationProperties applicationProperties = endpoint.configurationProperties(); + ConfigurationPropertiesBeanDescriptor descriptor = applicationProperties.getContexts().get(context.getId()) + .getBeans().get("foo"); + assertThat((Map) descriptor.getInputs().get("name")).containsEntry("value", "Spring Boot"); + }); + } + + @Test + @SuppressWarnings("unchecked") + void endpointResponseUsesPlaceholderForComplexValueAsPropertyValue() throws IOException { + ApplicationContextRunner contextRunner = new ApplicationContextRunner().withInitializer((context) -> { + ConfigurableEnvironment environment = context.getEnvironment(); + environment.getPropertySources().addFirst(new MapPropertySource("test", + Collections.singletonMap("foo.name", new ComplexProperty("Spring Boot")))); + }).withUserConfiguration(ComplexPropertyToStringConverter.class, FooConfig.class); + contextRunner.run((context) -> { + ConfigurationPropertiesReportEndpoint endpoint = context + .getBean(ConfigurationPropertiesReportEndpoint.class); + ApplicationConfigurationProperties applicationProperties = endpoint.configurationProperties(); + ConfigurationPropertiesBeanDescriptor descriptor = applicationProperties.getContexts().get(context.getId()) + .getBeans().get("foo"); + assertThat((Map) descriptor.getInputs().get("name")).containsEntry("value", + "Complex property value " + ComplexProperty.class.getName()); + }); + } + @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties static class Base { @@ -518,4 +564,59 @@ HikariDataSource hikariDataSource() { } + static class CharSequenceProperty implements CharSequence, InputStreamSource { + + private final String value; + + CharSequenceProperty(String value) { + this.value = value; + } + + @Override + public int length() { + return this.value.length(); + } + + @Override + public char charAt(int index) { + return this.value.charAt(index); + } + + @Override + public CharSequence subSequence(int start, int end) { + return this.value.subSequence(start, end); + } + + @Override + public String toString() { + return this.value; + } + + @Override + public InputStream getInputStream() throws IOException { + return new ByteArrayInputStream(this.value.getBytes()); + } + + } + + static class ComplexProperty { + + private final String value; + + ComplexProperty(String value) { + this.value = value; + } + + } + + @ConfigurationPropertiesBinding + static class ComplexPropertyToStringConverter implements Converter { + + @Override + public String convert(ComplexProperty source) { + return source.value; + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointTests.java index db9e661e0d72..163d252c53d1 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,12 +34,17 @@ import org.springframework.boot.context.properties.ConstructorBinding; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.bind.DefaultValue; +import org.springframework.boot.context.properties.bind.Name; +import org.springframework.boot.origin.Origin; +import org.springframework.boot.origin.OriginLookup; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ContextConsumer; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; +import org.springframework.mock.env.MockPropertySource; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; @@ -51,7 +56,9 @@ * @author Andy Wilkinson * @author Stephane Nicoll * @author HaiTao Zhang + * @author Chris Bono */ +@SuppressWarnings("unchecked") class ConfigurationPropertiesReportEndpointTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() @@ -67,7 +74,7 @@ void descriptorWithJavaBeanBindMethodDetectsRelevantProperties() { void descriptorWithValueObjectBindMethodDetectsRelevantProperties() { this.contextRunner.withUserConfiguration(ImmutablePropertiesConfiguration.class).run(assertProperties( "immutable", - (properties) -> assertThat(properties).containsOnlyKeys("dbPassword", "myTestProperty", "duration"))); + (properties) -> assertThat(properties).containsOnlyKeys("dbPassword", "myTestProperty", "for"))); } @Test @@ -84,6 +91,36 @@ void descriptorWithValueObjectBindMethodHandleNestedType() { assertThat(properties).containsOnlyKeys("name", "nested"); Map nested = (Map) properties.get("nested"); assertThat(nested).containsOnly(entry("name", "nested"), entry("counter", 42)); + }, (inputs) -> { + Map nested = (Map) inputs.get("nested"); + Map name = (Map) nested.get("name"); + Map counter = (Map) nested.get("counter"); + assertThat(name.get("value")).isEqualTo("nested"); + assertThat(name.get("origin")) + .isEqualTo("\"immutablenested.nested.name\" from property source \"test\""); + assertThat(counter.get("origin")) + .isEqualTo("\"immutablenested.nested.counter\" from property source \"test\""); + assertThat(counter.get("value")).isEqualTo("42"); + })); + } + + @Test + void descriptorWithSimpleList() { + this.contextRunner.withUserConfiguration(SensiblePropertiesConfiguration.class) + .withPropertyValues("sensible.simpleList=a,b").run(assertProperties("sensible", (properties) -> { + assertThat(properties.get("simpleList")).isInstanceOf(List.class); + List list = (List) properties.get("simpleList"); + assertThat(list).hasSize(2); + assertThat(list.get(0)).isEqualTo("a"); + assertThat(list.get(1)).isEqualTo("b"); + }, (inputs) -> { + List list = (List) inputs.get("simpleList"); + assertThat(list).hasSize(2); + Map item = (Map) list.get(0); + String origin = item.get("origin"); + String value = item.get("value"); + assertThat(value).isEqualTo("a,b"); + assertThat(origin).isEqualTo("\"sensible.simpleList\" from property source \"test\""); })); } @@ -170,34 +207,63 @@ void sanitizeWithCustomPatternUsingCompositeKeys() { } @Test - void sanitizedUriWithSensitiveInfo() { + void sanitizeUriWithSensitiveInfo() { this.contextRunner.withUserConfiguration(SensiblePropertiesConfiguration.class) + .withPropertyValues("sensible.sensitiveUri=http://user:password@localhost:8080") .run(assertProperties("sensible", (properties) -> assertThat(properties.get("sensitiveUri")) - .isEqualTo("http://user:******@localhost:8080"))); + .isEqualTo("http://user:******@localhost:8080"), (inputs) -> { + Map sensitiveUri = (Map) inputs.get("sensitiveUri"); + assertThat(sensitiveUri.get("value")).isEqualTo("http://user:******@localhost:8080"); + assertThat(sensitiveUri.get("origin")) + .isEqualTo("\"sensible.sensitiveUri\" from property source \"test\""); + })); } @Test - void sanitizedUriWithNoPassword() { + void sanitizeUriWithNoPassword() { this.contextRunner.withUserConfiguration(SensiblePropertiesConfiguration.class) + .withPropertyValues("sensible.noPasswordUri=http://user:@localhost:8080") .run(assertProperties("sensible", (properties) -> assertThat(properties.get("noPasswordUri")) - .isEqualTo("http://user:******@localhost:8080"))); + .isEqualTo("http://user:******@localhost:8080"), (inputs) -> { + Map noPasswordUri = (Map) inputs.get("noPasswordUri"); + assertThat(noPasswordUri.get("value")).isEqualTo("http://user:******@localhost:8080"); + assertThat(noPasswordUri.get("origin")) + .isEqualTo("\"sensible.noPasswordUri\" from property source \"test\""); + })); + } + + @Test + void sanitizeAddressesFieldContainingMultipleRawSensitiveUris() { + this.contextRunner.withUserConfiguration(SensiblePropertiesConfiguration.class) + .run(assertProperties("sensible", (properties) -> assertThat(properties.get("rawSensitiveAddresses")) + .isEqualTo("http://user:******@localhost:8080,http://user2:******@localhost:8082"))); } @Test void sanitizeLists() { this.contextRunner.withUserConfiguration(SensiblePropertiesConfiguration.class) + .withPropertyValues("sensible.listItems[0].some-password=password") .run(assertProperties("sensible", (properties) -> { assertThat(properties.get("listItems")).isInstanceOf(List.class); List list = (List) properties.get("listItems"); assertThat(list).hasSize(1); Map item = (Map) list.get(0); assertThat(item.get("somePassword")).isEqualTo("******"); + }, (inputs) -> { + List list = (List) inputs.get("listItems"); + assertThat(list).hasSize(1); + Map item = (Map) list.get(0); + Map somePassword = (Map) item.get("somePassword"); + assertThat(somePassword.get("value")).isEqualTo("******"); + assertThat(somePassword.get("origin")) + .isEqualTo("\"sensible.listItems[0].some-password\" from property source \"test\""); })); } @Test void listsOfListsAreSanitized() { this.contextRunner.withUserConfiguration(SensiblePropertiesConfiguration.class) + .withPropertyValues("sensible.listOfListItems[0][0].some-password=password") .run(assertProperties("sensible", (properties) -> { assertThat(properties.get("listOfListItems")).isInstanceOf(List.class); List> listOfLists = (List>) properties.get("listOfListItems"); @@ -206,11 +272,45 @@ void listsOfListsAreSanitized() { assertThat(list).hasSize(1); Map item = (Map) list.get(0); assertThat(item.get("somePassword")).isEqualTo("******"); + }, (inputs) -> { + assertThat(inputs.get("listOfListItems")).isInstanceOf(List.class); + List> listOfLists = (List>) inputs.get("listOfListItems"); + assertThat(listOfLists).hasSize(1); + List list = listOfLists.get(0); + assertThat(list).hasSize(1); + Map item = (Map) list.get(0); + Map somePassword = (Map) item.get("somePassword"); + assertThat(somePassword.get("value")).isEqualTo("******"); + assertThat(somePassword.get("origin")).isEqualTo( + "\"sensible.listOfListItems[0][0].some-password\" from property source \"test\""); })); } + @Test + void originParents() { + this.contextRunner.withUserConfiguration(SensiblePropertiesConfiguration.class) + .withInitializer(this::initializeOriginParents).run(assertProperties("sensible", (properties) -> { + }, (inputs) -> { + Map stringInputs = (Map) inputs.get("string"); + String[] originParents = (String[]) stringInputs.get("originParents"); + assertThat(originParents).containsExactly("spring", "boot"); + })); + } + + private void initializeOriginParents(ConfigurableApplicationContext context) { + MockPropertySource propertySource = new OriginParentMockPropertySource(); + propertySource.setProperty("sensible.string", "spring"); + context.getEnvironment().getPropertySources().addFirst(propertySource); + } + private ContextConsumer assertProperties(String prefix, Consumer> properties) { + return assertProperties(prefix, properties, (inputs) -> { + }); + } + + private ContextConsumer assertProperties(String prefix, + Consumer> properties, Consumer> inputs) { return (context) -> { ConfigurationPropertiesReportEndpoint endpoint = context .getBean(ConfigurationPropertiesReportEndpoint.class); @@ -222,6 +322,7 @@ private ContextConsumer assertProperties(String pr ConfigurationPropertiesBeanDescriptor descriptor = allProperties.getBeans().get(key.get()); assertThat(descriptor.getPrefix()).isEqualTo(prefix); properties.accept(descriptor.getProperties()); + inputs.accept(descriptor.getInputs()); }; } @@ -231,6 +332,38 @@ private boolean findIdFromPrefix(String prefix, String id) { return prefix.equals(candidate); } + static class OriginParentMockPropertySource extends MockPropertySource implements OriginLookup { + + @Override + public Origin getOrigin(String key) { + return new MockOrigin(key, new MockOrigin("spring", new MockOrigin("boot", null))); + } + + } + + static class MockOrigin implements Origin { + + private final String value; + + private final MockOrigin parent; + + MockOrigin(String value, MockOrigin parent) { + this.value = value; + this.parent = parent; + } + + @Override + public Origin getParent() { + return this.parent; + } + + @Override + public String toString() { + return this.value; + } + + } + @Configuration(proxyBeanMethods = false) static class EndpointConfig { @@ -319,16 +452,16 @@ public static class ImmutableProperties { private final String nullValue; - private final Duration duration; + private final Duration forDuration; private final String ignored; ImmutableProperties(@DefaultValue("123456") String dbPassword, @DefaultValue("654321") String myTestProperty, - String nullValue, @DefaultValue("10s") Duration duration) { + String nullValue, @DefaultValue("10s") @Name("for") Duration forDuration) { this.dbPassword = dbPassword; this.myTestProperty = myTestProperty; this.nullValue = nullValue; - this.duration = duration; + this.forDuration = forDuration; this.ignored = "dummy"; } @@ -344,8 +477,8 @@ public String getNullValue() { return this.nullValue; } - public Duration getDuration() { - return this.duration; + public Duration getFor() { + return this.forDuration; } public String getIgnored() { @@ -570,10 +703,16 @@ static class SensiblePropertiesConfiguration { @ConfigurationProperties("sensible") public static class SensibleProperties { + private String string; + private URI sensitiveUri = URI.create("http://user:password@localhost:8080"); private URI noPasswordUri = URI.create("http://user:@localhost:8080"); + private List simpleList = new ArrayList<>(); + + private String rawSensitiveAddresses = "http://user:password@localhost:8080,http://user2:password2@localhost:8082"; + private List listItems = new ArrayList<>(); private List> listOfListItems = new ArrayList<>(); @@ -583,6 +722,14 @@ public static class SensibleProperties { this.listOfListItems.add(Collections.singletonList(new ListItem())); } + public void setString(String string) { + this.string = string; + } + + public String getString() { + return this.string; + } + public void setSensitiveUri(URI sensitiveUri) { this.sensitiveUri = sensitiveUri; } @@ -599,6 +746,14 @@ public URI getNoPasswordUri() { return this.noPasswordUri; } + public String getRawSensitiveAddresses() { + return this.rawSensitiveAddresses; + } + + public void setRawSensitiveAddresses(final String rawSensitiveAddresses) { + this.rawSensitiveAddresses = rawSensitiveAddresses; + } + public List getListItems() { return this.listItems; } @@ -615,6 +770,10 @@ public void setListOfListItems(List> listOfListItems) { this.listOfListItems = listOfListItems; } + public List getSimpleList() { + return this.simpleList; + } + public static class ListItem { private String somePassword = "secret"; diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointWebIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointWebIntegrationTests.java new file mode 100644 index 000000000000..80b6b16da45d --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointWebIntegrationTests.java @@ -0,0 +1,131 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.context.properties; + +import org.junit.jupiter.api.BeforeEach; + +import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.hasSize; + +/** + * Integration tests for {@link ConfigurationPropertiesReportEndpoint} exposed by Jersey, + * Spring MVC, and WebFlux. + * + * @author Chris Bono + */ +class ConfigurationPropertiesReportEndpointWebIntegrationTests { + + private WebTestClient client; + + @BeforeEach + void prepareEnvironment(ConfigurableApplicationContext context, WebTestClient client) { + TestPropertyValues.of("com.foo.name=fooz", "com.bar.name=barz").applyTo(context); + this.client = client; + } + + @WebEndpointTest + void noFilters() { + this.client.get().uri("/actuator/configprops").exchange().expectStatus().isOk().expectBody() + .jsonPath("$..beans[*]").value(hasSize(greaterThanOrEqualTo(2))).jsonPath("$..beans['fooDotCom']") + .exists().jsonPath("$..beans['barDotCom']").exists(); + } + + @WebEndpointTest + void filterByExactPrefix() { + this.client.get().uri("/actuator/configprops/com.foo").exchange().expectStatus().isOk().expectBody() + .jsonPath("$..beans[*]").value(hasSize(1)).jsonPath("$..beans['fooDotCom']").exists(); + } + + @WebEndpointTest + void filterByGeneralPrefix() { + this.client.get().uri("/actuator/configprops/com.").exchange().expectStatus().isOk().expectBody() + .jsonPath("$..beans[*]").value(hasSize(2)).jsonPath("$..beans['fooDotCom']").exists() + .jsonPath("$..beans['barDotCom']").exists(); + } + + @WebEndpointTest + void filterByNonExistentPrefix() { + this.client.get().uri("/actuator/configprops/com.zoo").exchange().expectStatus().isNotFound(); + } + + @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties + static class TestConfiguration { + + @Bean + ConfigurationPropertiesReportEndpoint endpoint() { + return new ConfigurationPropertiesReportEndpoint(); + } + + @Bean + ConfigurationPropertiesReportEndpointWebExtension endpointWebExtension( + ConfigurationPropertiesReportEndpoint endpoint) { + return new ConfigurationPropertiesReportEndpointWebExtension(endpoint); + } + + @Bean + @ConfigurationProperties(prefix = "com.foo") + Foo fooDotCom() { + return new Foo(); + } + + @Bean + @ConfigurationProperties(prefix = "com.bar") + Bar barDotCom() { + return new Bar(); + } + + } + + public static class Foo { + + private String name = "5150"; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + } + + public static class Bar { + + private String name = "6160"; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ValidatedConstructorBindingProperties.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ValidatedConstructorBindingProperties.java new file mode 100644 index 000000000000..3bb8722ad476 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ValidatedConstructorBindingProperties.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.context.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; +import org.springframework.validation.annotation.Validated; + +/** + * Used for testing the {@link ConfigurationPropertiesReportEndpoint} endpoint with + * validated {@link ConfigurationProperties @ConfigurationProperties}. + * + * @author Madhura Bhave + */ +@Validated +@ConstructorBinding +@ConfigurationProperties(prefix = "validated") +public class ValidatedConstructorBindingProperties { + + private final String name; + + ValidatedConstructorBindingProperties(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/couchbase/CouchbaseHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/couchbase/CouchbaseHealthIndicatorTests.java index c6f00adcbdf7..e3345aa878d6 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/couchbase/CouchbaseHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/couchbase/CouchbaseHealthIndicatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,15 +16,16 @@ package org.springframework.boot.actuate.couchbase; -import java.net.InetSocketAddress; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; -import com.couchbase.client.core.message.internal.DiagnosticsReport; -import com.couchbase.client.core.message.internal.EndpointHealth; +import com.couchbase.client.core.diagnostics.DiagnosticsResult; +import com.couchbase.client.core.diagnostics.EndpointDiagnostics; +import com.couchbase.client.core.endpoint.EndpointState; import com.couchbase.client.core.service.ServiceType; -import com.couchbase.client.core.state.LifecycleState; import com.couchbase.client.java.Cluster; import org.junit.jupiter.api.Test; @@ -33,8 +34,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link CouchbaseHealthIndicator} @@ -49,16 +50,18 @@ class CouchbaseHealthIndicatorTests { void couchbaseClusterIsUp() { Cluster cluster = mock(Cluster.class); CouchbaseHealthIndicator healthIndicator = new CouchbaseHealthIndicator(cluster); - List endpoints = Arrays.asList(new EndpointHealth(ServiceType.BINARY, LifecycleState.CONNECTED, - new InetSocketAddress(0), new InetSocketAddress(0), 1234, "endpoint-1")); - DiagnosticsReport diagnostics = new DiagnosticsReport(endpoints, "test-sdk", "test-id", null); + Map> endpoints = Collections.singletonMap(ServiceType.KV, + Collections.singletonList(new EndpointDiagnostics(ServiceType.KV, EndpointState.CONNECTED, "127.0.0.1", + "127.0.0.1", Optional.empty(), Optional.of(1234L), Optional.of("endpoint-1")))); + + DiagnosticsResult diagnostics = new DiagnosticsResult(endpoints, "test-sdk", "test-id"); given(cluster.diagnostics()).willReturn(diagnostics); Health health = healthIndicator.health(); assertThat(health.getStatus()).isEqualTo(Status.UP); assertThat(health.getDetails()).containsEntry("sdk", "test-sdk"); assertThat(health.getDetails()).containsKey("endpoints"); assertThat((List>) health.getDetails().get("endpoints")).hasSize(1); - verify(cluster).diagnostics(); + then(cluster).should().diagnostics(); } @Test @@ -66,19 +69,20 @@ void couchbaseClusterIsUp() { void couchbaseClusterIsDown() { Cluster cluster = mock(Cluster.class); CouchbaseHealthIndicator healthIndicator = new CouchbaseHealthIndicator(cluster); - List endpoints = Arrays.asList( - new EndpointHealth(ServiceType.BINARY, LifecycleState.CONNECTED, new InetSocketAddress(0), - new InetSocketAddress(0), 1234, "endpoint-1"), - new EndpointHealth(ServiceType.BINARY, LifecycleState.CONNECTING, new InetSocketAddress(0), - new InetSocketAddress(0), 1234, "endpoint-2")); - DiagnosticsReport diagnostics = new DiagnosticsReport(endpoints, "test-sdk", "test-id", null); + Map> endpoints = Collections.singletonMap(ServiceType.KV, + Arrays.asList( + new EndpointDiagnostics(ServiceType.KV, EndpointState.CONNECTED, "127.0.0.1", "127.0.0.1", + Optional.empty(), Optional.of(1234L), Optional.of("endpoint-1")), + new EndpointDiagnostics(ServiceType.KV, EndpointState.CONNECTING, "127.0.0.1", "127.0.0.1", + Optional.empty(), Optional.of(1234L), Optional.of("endpoint-2")))); + DiagnosticsResult diagnostics = new DiagnosticsResult(endpoints, "test-sdk", "test-id"); given(cluster.diagnostics()).willReturn(diagnostics); Health health = healthIndicator.health(); assertThat(health.getStatus()).isEqualTo(Status.DOWN); assertThat(health.getDetails()).containsEntry("sdk", "test-sdk"); assertThat(health.getDetails()).containsKey("endpoints"); assertThat((List>) health.getDetails().get("endpoints")).hasSize(2); - verify(cluster).diagnostics(); + then(cluster).should().diagnostics(); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/couchbase/CouchbaseReactiveHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/couchbase/CouchbaseReactiveHealthIndicatorTests.java index fe3923120bdc..adc00a875134 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/couchbase/CouchbaseReactiveHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/couchbase/CouchbaseReactiveHealthIndicatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,18 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.actuate.couchbase; -import java.net.InetSocketAddress; import java.time.Duration; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; -import com.couchbase.client.core.message.internal.DiagnosticsReport; -import com.couchbase.client.core.message.internal.EndpointHealth; +import com.couchbase.client.core.diagnostics.DiagnosticsResult; +import com.couchbase.client.core.diagnostics.EndpointDiagnostics; +import com.couchbase.client.core.endpoint.EndpointState; import com.couchbase.client.core.service.ServiceType; -import com.couchbase.client.core.state.LifecycleState; import com.couchbase.client.java.Cluster; import org.junit.jupiter.api.Test; @@ -33,8 +35,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link CouchbaseReactiveHealthIndicator}. @@ -46,16 +48,17 @@ class CouchbaseReactiveHealthIndicatorTests { void couchbaseClusterIsUp() { Cluster cluster = mock(Cluster.class); CouchbaseReactiveHealthIndicator healthIndicator = new CouchbaseReactiveHealthIndicator(cluster); - List endpoints = Arrays.asList(new EndpointHealth(ServiceType.BINARY, LifecycleState.CONNECTED, - new InetSocketAddress(0), new InetSocketAddress(0), 1234, "endpoint-1")); - DiagnosticsReport diagnostics = new DiagnosticsReport(endpoints, "test-sdk", "test-id", null); + Map> endpoints = Collections.singletonMap(ServiceType.KV, + Collections.singletonList(new EndpointDiagnostics(ServiceType.KV, EndpointState.CONNECTED, "127.0.0.1", + "127.0.0.1", Optional.empty(), Optional.of(1234L), Optional.of("endpoint-1")))); + DiagnosticsResult diagnostics = new DiagnosticsResult(endpoints, "test-sdk", "test-id"); given(cluster.diagnostics()).willReturn(diagnostics); Health health = healthIndicator.health().block(Duration.ofSeconds(30)); assertThat(health.getStatus()).isEqualTo(Status.UP); assertThat(health.getDetails()).containsEntry("sdk", "test-sdk"); assertThat(health.getDetails()).containsKey("endpoints"); assertThat((List>) health.getDetails().get("endpoints")).hasSize(1); - verify(cluster).diagnostics(); + then(cluster).should().diagnostics(); } @Test @@ -63,19 +66,20 @@ void couchbaseClusterIsUp() { void couchbaseClusterIsDown() { Cluster cluster = mock(Cluster.class); CouchbaseReactiveHealthIndicator healthIndicator = new CouchbaseReactiveHealthIndicator(cluster); - List endpoints = Arrays.asList( - new EndpointHealth(ServiceType.BINARY, LifecycleState.CONNECTED, new InetSocketAddress(0), - new InetSocketAddress(0), 1234, "endpoint-1"), - new EndpointHealth(ServiceType.BINARY, LifecycleState.CONNECTING, new InetSocketAddress(0), - new InetSocketAddress(0), 1234, "endpoint-2")); - DiagnosticsReport diagnostics = new DiagnosticsReport(endpoints, "test-sdk", "test-id", null); + Map> endpoints = Collections.singletonMap(ServiceType.KV, + Arrays.asList( + new EndpointDiagnostics(ServiceType.KV, EndpointState.CONNECTED, "127.0.0.1", "127.0.0.1", + Optional.empty(), Optional.of(1234L), Optional.of("endpoint-1")), + new EndpointDiagnostics(ServiceType.KV, EndpointState.CONNECTING, "127.0.0.1", "127.0.0.1", + Optional.empty(), Optional.of(1234L), Optional.of("endpoint-2")))); + DiagnosticsResult diagnostics = new DiagnosticsResult(endpoints, "test-sdk", "test-id"); given(cluster.diagnostics()).willReturn(diagnostics); Health health = healthIndicator.health().block(Duration.ofSeconds(30)); assertThat(health.getStatus()).isEqualTo(Status.DOWN); assertThat(health.getDetails()).containsEntry("sdk", "test-sdk"); assertThat(health.getDetails()).containsKey("endpoints"); assertThat((List>) health.getDetails().get("endpoints")).hasSize(2); - verify(cluster).diagnostics(); + then(cluster).should().diagnostics(); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchHealthIndicatorTests.java deleted file mode 100644 index 7f49fa29bf27..000000000000 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchHealthIndicatorTests.java +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.elasticsearch; - -import java.util.Map; - -import org.elasticsearch.ElasticsearchException; -import org.elasticsearch.ElasticsearchTimeoutException; -import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest; -import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse; -import org.elasticsearch.action.support.PlainActionFuture; -import org.elasticsearch.client.AdminClient; -import org.elasticsearch.client.Client; -import org.elasticsearch.client.ClusterAdminClient; -import org.elasticsearch.cluster.ClusterState; -import org.elasticsearch.cluster.block.ClusterBlocks; -import org.elasticsearch.cluster.health.ClusterHealthStatus; -import org.elasticsearch.cluster.node.DiscoveryNodes; -import org.elasticsearch.cluster.routing.RoutingTable; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import org.springframework.boot.actuate.health.Health; -import org.springframework.boot.actuate.health.Status; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; - -/** - * Test for {@link ElasticsearchHealthIndicator}. - * - * @author Andy Wilkinson - */ -@Deprecated -class ElasticsearchHealthIndicatorTests { - - @Mock - private Client client; - - @Mock - private AdminClient admin; - - @Mock - private ClusterAdminClient cluster; - - private ElasticsearchHealthIndicator indicator; - - @BeforeEach - void setUp() { - MockitoAnnotations.initMocks(this); - given(this.client.admin()).willReturn(this.admin); - given(this.admin.cluster()).willReturn(this.cluster); - this.indicator = new ElasticsearchHealthIndicator(this.client, 100L); - } - - @Test - void defaultConfigurationQueriesAllIndicesWith100msTimeout() { - TestActionFuture responseFuture = new TestActionFuture(); - responseFuture.onResponse(new StubClusterHealthResponse()); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(ClusterHealthRequest.class); - given(this.cluster.health(requestCaptor.capture())).willReturn(responseFuture); - Health health = this.indicator.health(); - assertThat(responseFuture.getTimeout).isEqualTo(100L); - assertThat(requestCaptor.getValue().indices()).contains("_all"); - assertThat(health.getStatus()).isEqualTo(Status.UP); - } - - @Test - void certainIndices() { - this.indicator = new ElasticsearchHealthIndicator(this.client, 100L, "test-index-1", "test-index-2"); - PlainActionFuture responseFuture = new PlainActionFuture<>(); - responseFuture.onResponse(new StubClusterHealthResponse()); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(ClusterHealthRequest.class); - given(this.cluster.health(requestCaptor.capture())).willReturn(responseFuture); - Health health = this.indicator.health(); - assertThat(requestCaptor.getValue().indices()).contains("test-index-1", "test-index-2"); - assertThat(health.getStatus()).isEqualTo(Status.UP); - } - - @Test - void customTimeout() { - this.indicator = new ElasticsearchHealthIndicator(this.client, 1000L); - TestActionFuture responseFuture = new TestActionFuture(); - responseFuture.onResponse(new StubClusterHealthResponse()); - ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(ClusterHealthRequest.class); - given(this.cluster.health(requestCaptor.capture())).willReturn(responseFuture); - this.indicator.health(); - assertThat(responseFuture.getTimeout).isEqualTo(1000L); - } - - @Test - void healthDetails() { - PlainActionFuture responseFuture = new PlainActionFuture<>(); - responseFuture.onResponse(new StubClusterHealthResponse()); - given(this.cluster.health(any(ClusterHealthRequest.class))).willReturn(responseFuture); - Health health = this.indicator.health(); - assertThat(health.getStatus()).isEqualTo(Status.UP); - Map details = health.getDetails(); - assertDetail(details, "clusterName", "test-cluster"); - assertDetail(details, "activeShards", 1); - assertDetail(details, "relocatingShards", 2); - assertDetail(details, "activePrimaryShards", 3); - assertDetail(details, "initializingShards", 4); - assertDetail(details, "unassignedShards", 5); - assertDetail(details, "numberOfNodes", 6); - assertDetail(details, "numberOfDataNodes", 7); - } - - @Test - void redResponseMapsToDown() { - PlainActionFuture responseFuture = new PlainActionFuture<>(); - responseFuture.onResponse(new StubClusterHealthResponse(ClusterHealthStatus.RED)); - given(this.cluster.health(any(ClusterHealthRequest.class))).willReturn(responseFuture); - assertThat(this.indicator.health().getStatus()).isEqualTo(Status.DOWN); - } - - @Test - void yellowResponseMapsToUp() { - PlainActionFuture responseFuture = new PlainActionFuture<>(); - responseFuture.onResponse(new StubClusterHealthResponse(ClusterHealthStatus.YELLOW)); - given(this.cluster.health(any(ClusterHealthRequest.class))).willReturn(responseFuture); - assertThat(this.indicator.health().getStatus()).isEqualTo(Status.UP); - } - - @Test - void responseTimeout() { - PlainActionFuture responseFuture = new PlainActionFuture<>(); - given(this.cluster.health(any(ClusterHealthRequest.class))).willReturn(responseFuture); - Health health = this.indicator.health(); - assertThat(health.getStatus()).isEqualTo(Status.DOWN); - assertThat((String) health.getDetails().get("error")).contains(ElasticsearchTimeoutException.class.getName()); - } - - @SuppressWarnings("unchecked") - private void assertDetail(Map details, String detail, T value) { - assertThat((T) details.get(detail)).isEqualTo(value); - } - - private static final class StubClusterHealthResponse extends ClusterHealthResponse { - - private final ClusterHealthStatus status; - - private StubClusterHealthResponse() { - this(ClusterHealthStatus.GREEN); - } - - private StubClusterHealthResponse(ClusterHealthStatus status) { - super("test-cluster", new String[0], new ClusterState(null, 0, null, null, RoutingTable.builder().build(), - DiscoveryNodes.builder().build(), ClusterBlocks.builder().build(), null, 1, false)); - this.status = status; - } - - @Override - public int getActiveShards() { - return 1; - } - - @Override - public int getRelocatingShards() { - return 2; - } - - @Override - public int getActivePrimaryShards() { - return 3; - } - - @Override - public int getInitializingShards() { - return 4; - } - - @Override - public int getUnassignedShards() { - return 5; - } - - @Override - public int getNumberOfNodes() { - return 6; - } - - @Override - public int getNumberOfDataNodes() { - return 7; - } - - @Override - public ClusterHealthStatus getStatus() { - return this.status; - } - - } - - static class TestActionFuture extends PlainActionFuture { - - private long getTimeout = -1L; - - @Override - public ClusterHealthResponse actionGet(long timeoutMillis) throws ElasticsearchException { - this.getTimeout = timeoutMillis; - return super.actionGet(timeoutMillis); - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchJestHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchJestHealthIndicatorTests.java deleted file mode 100644 index 0f84f5813403..000000000000 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchJestHealthIndicatorTests.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.elasticsearch; - -import java.io.IOException; -import java.util.Map; - -import com.google.gson.Gson; -import com.google.gson.JsonParser; -import io.searchbox.action.Action; -import io.searchbox.client.JestClient; -import io.searchbox.client.JestResult; -import io.searchbox.client.config.exception.CouldNotConnectException; -import io.searchbox.core.SearchResult; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.actuate.health.Health; -import org.springframework.boot.actuate.health.Status; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.entry; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link ElasticsearchJestHealthIndicator}. - * - * @author Stephane Nicoll - * @author Julian Devia Serna - * @author Brian Clozel - */ -class ElasticsearchJestHealthIndicatorTests { - - private final JestClient jestClient = mock(JestClient.class); - - private final ElasticsearchJestHealthIndicator healthIndicator = new ElasticsearchJestHealthIndicator( - this.jestClient); - - @SuppressWarnings("unchecked") - @Test - void elasticsearchIsUp() throws IOException { - given(this.jestClient.execute(any(Action.class))).willReturn(createJestResult(200, true, "green")); - Health health = this.healthIndicator.health(); - assertThat(health.getStatus()).isEqualTo(Status.UP); - assertHealthDetailsWithStatus(health.getDetails(), "green"); - } - - @Test - @SuppressWarnings("unchecked") - void elasticsearchWithYellowStatusIsUp() throws IOException { - given(this.jestClient.execute(any(Action.class))).willReturn(createJestResult(200, true, "yellow")); - Health health = this.healthIndicator.health(); - assertThat(health.getStatus()).isEqualTo(Status.UP); - assertHealthDetailsWithStatus(health.getDetails(), "yellow"); - } - - @SuppressWarnings("unchecked") - @Test - void elasticsearchIsDown() throws IOException { - given(this.jestClient.execute(any(Action.class))) - .willThrow(new CouldNotConnectException("http://localhost:9200", new IOException())); - Health health = this.healthIndicator.health(); - assertThat(health.getStatus()).isEqualTo(Status.DOWN); - } - - @SuppressWarnings("unchecked") - @Test - void elasticsearchIsDownWhenQueryDidNotSucceed() throws IOException { - given(this.jestClient.execute(any(Action.class))).willReturn(createJestResult(200, false, "")); - Health health = this.healthIndicator.health(); - assertThat(health.getStatus()).isEqualTo(Status.DOWN); - } - - @SuppressWarnings("unchecked") - @Test - void elasticsearchIsDownByResponseCode() throws IOException { - given(this.jestClient.execute(any(Action.class))).willReturn(createJestResult(500, false, "")); - Health health = this.healthIndicator.health(); - assertThat(health.getStatus()).isEqualTo(Status.DOWN); - assertThat(health.getDetails()).contains(entry("statusCode", 500)); - } - - @SuppressWarnings("unchecked") - @Test - void elasticsearchIsOutOfServiceByStatus() throws IOException { - given(this.jestClient.execute(any(Action.class))).willReturn(createJestResult(200, true, "red")); - Health health = this.healthIndicator.health(); - assertThat(health.getStatus()).isEqualTo(Status.OUT_OF_SERVICE); - assertHealthDetailsWithStatus(health.getDetails(), "red"); - } - - private void assertHealthDetailsWithStatus(Map details, String status) { - assertThat(details).contains(entry("cluster_name", "elasticsearch"), entry("status", status), - entry("timed_out", false), entry("number_of_nodes", 1), entry("number_of_data_nodes", 1), - entry("active_primary_shards", 0), entry("active_shards", 0), entry("relocating_shards", 0), - entry("initializing_shards", 0), entry("unassigned_shards", 0), entry("delayed_unassigned_shards", 0), - entry("number_of_pending_tasks", 0), entry("number_of_in_flight_fetch", 0), - entry("task_max_waiting_in_queue_millis", 0), entry("active_shards_percent_as_number", 100.0)); - } - - private static JestResult createJestResult(int responseCode, boolean succeeded, String status) { - - SearchResult searchResult = new SearchResult(new Gson()); - String json; - if (responseCode == 200) { - json = String.format( - "{\"cluster_name\":\"elasticsearch\"," - + "\"status\":\"%s\",\"timed_out\":false,\"number_of_nodes\":1," - + "\"number_of_data_nodes\":1,\"active_primary_shards\":0," - + "\"active_shards\":0,\"relocating_shards\":0,\"initializing_shards\":0," - + "\"unassigned_shards\":0,\"delayed_unassigned_shards\":0," - + "\"number_of_pending_tasks\":0,\"number_of_in_flight_fetch\":0," - + "\"task_max_waiting_in_queue_millis\":0,\"active_shards_percent_as_number\":100.0}", - status); - } - else { - json = "{\n \"error\": \"Server Error\",\n \"status\": \"" + status + "\"\n}"; - } - searchResult.setJsonString(json); - searchResult.setJsonObject(JsonParser.parseString(json).getAsJsonObject()); - searchResult.setResponseCode(responseCode); - searchResult.setSucceeded(succeeded); - return searchResult; - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchReactiveHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchReactiveHealthIndicatorTests.java new file mode 100644 index 000000000000..97da21c8cce8 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/elasticsearch/ElasticsearchReactiveHealthIndicatorTests.java @@ -0,0 +1,144 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.elasticsearch; + +import java.util.Map; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.Status; +import org.springframework.data.elasticsearch.client.ClientConfiguration; +import org.springframework.data.elasticsearch.client.reactive.DefaultReactiveElasticsearchClient; +import org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +/** + * Tests for {@link ElasticsearchReactiveHealthIndicator} + * + * @author Brian Clozel + * @author Scott Frederick + */ +class ElasticsearchReactiveHealthIndicatorTests { + + private MockWebServer server; + + private ElasticsearchReactiveHealthIndicator healthIndicator; + + @BeforeEach + void setup() throws Exception { + this.server = new MockWebServer(); + this.server.start(); + ReactiveElasticsearchClient client = DefaultReactiveElasticsearchClient + .create(ClientConfiguration.create(this.server.getHostName() + ":" + this.server.getPort())); + this.healthIndicator = new ElasticsearchReactiveHealthIndicator(client); + } + + @AfterEach + void shutdown() throws Exception { + this.server.shutdown(); + } + + @Test + void elasticsearchIsUp() { + setupMockResponse(200, "green"); + Health health = this.healthIndicator.health().block(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertHealthDetailsWithStatus(health.getDetails(), "green"); + } + + @Test + void elasticsearchWithYellowStatusIsUp() { + setupMockResponse(200, "yellow"); + Health health = this.healthIndicator.health().block(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertHealthDetailsWithStatus(health.getDetails(), "yellow"); + } + + @Test + void elasticsearchIsDown() throws Exception { + this.server.shutdown(); + Health health = this.healthIndicator.health().block(); + assertThat(health.getStatus()).isEqualTo(Status.DOWN); + assertThat(health.getDetails().get("error")).asString() + .contains("org.springframework.data.elasticsearch.client.NoReachableHostException"); + } + + @Test + void elasticsearchIsDownByResponseCode() { + // first enqueue an OK response since the HostChecker first sends a HEAD request + // to "/" + this.server.enqueue(new MockResponse().setResponseCode(HttpStatus.OK.value())); + this.server.enqueue(new MockResponse().setResponseCode(HttpStatus.INTERNAL_SERVER_ERROR.value())); + Health health = this.healthIndicator.health().block(); + assertThat(health.getStatus()).isEqualTo(Status.DOWN); + assertThat(health.getDetails().get("statusCode")).asString().isEqualTo("500"); + assertThat(health.getDetails().get("reasonPhrase")).asString().isEqualTo("Internal Server Error"); + } + + @Test + void elasticsearchIsOutOfServiceByStatus() { + setupMockResponse(200, "red"); + Health health = this.healthIndicator.health().block(); + assertThat(health.getStatus()).isEqualTo(Status.OUT_OF_SERVICE); + assertHealthDetailsWithStatus(health.getDetails(), "red"); + } + + private void assertHealthDetailsWithStatus(Map details, String status) { + assertThat(details).contains(entry("cluster_name", "elasticsearch"), entry("status", status), + entry("timed_out", false), entry("number_of_nodes", 1), entry("number_of_data_nodes", 1), + entry("active_primary_shards", 0), entry("active_shards", 0), entry("relocating_shards", 0), + entry("initializing_shards", 0), entry("unassigned_shards", 0), entry("delayed_unassigned_shards", 0), + entry("number_of_pending_tasks", 0), entry("number_of_in_flight_fetch", 0), + entry("task_max_waiting_in_queue_millis", 0), entry("active_shards_percent_as_number", 100.0)); + } + + private void setupMockResponse(int responseCode, String status) { + // first enqueue an OK response since the HostChecker first sends a HEAD request + // to "/" + this.server.enqueue(new MockResponse()); + MockResponse mockResponse = new MockResponse().setResponseCode(HttpStatus.valueOf(responseCode).value()) + .setBody(createJsonResult(responseCode, status)) + .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + this.server.enqueue(mockResponse); + } + + private String createJsonResult(int responseCode, String status) { + if (responseCode == 200) { + return String.format( + "{\"cluster_name\":\"elasticsearch\"," + + "\"status\":\"%s\",\"timed_out\":false,\"number_of_nodes\":1," + + "\"number_of_data_nodes\":1,\"active_primary_shards\":0," + + "\"active_shards\":0,\"relocating_shards\":0,\"initializing_shards\":0," + + "\"unassigned_shards\":0,\"delayed_unassigned_shards\":0," + + "\"number_of_pending_tasks\":0,\"number_of_in_flight_fetch\":0," + + "\"task_max_waiting_in_queue_millis\":0,\"active_shards_percent_as_number\":100.0}", + status); + } + return "{\n \"error\": \"Server Error\",\n \"status\": " + responseCode + "\n}"; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/EndpointIdTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/EndpointIdTests.java index a25ee122a1c6..3f272ad1f97f 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/EndpointIdTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/EndpointIdTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,6 +51,12 @@ void ofWhenContainsSlashThrowsException() { .withMessage("Value must only contain valid chars"); } + @Test + void ofWhenContainsBackslashThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> EndpointId.of("foo\\bar")) + .withMessage("Value must only contain valid chars"); + } + @Test void ofWhenHasBadCharThrowsException() { assertThatIllegalArgumentException().isThrownBy(() -> EndpointId.of("foo!bar")) @@ -95,12 +101,30 @@ void ofWhenContainsDeprecatedCharsLogsWarning(CapturedOutput output) { @Test void ofWhenMigratingLegacyNameRemovesDots(CapturedOutput output) { + EndpointId endpointId = migrateLegacyName("one.two.three"); + assertThat(endpointId.toString()).isEqualTo("onetwothree"); + assertThat(output).doesNotContain("contains invalid characters"); + } + + @Test + void ofWhenMigratingLegacyNameRemovesHyphens(CapturedOutput output) { + EndpointId endpointId = migrateLegacyName("one-two-three"); + assertThat(endpointId.toString()).isEqualTo("onetwothree"); + assertThat(output).doesNotContain("contains invalid characters"); + } + + @Test + void ofWhenMigratingLegacyNameRemovesMixOfDashAndDot(CapturedOutput output) { + EndpointId endpointId = migrateLegacyName("one.two-three"); + assertThat(endpointId.toString()).isEqualTo("onetwothree"); + assertThat(output).doesNotContain("contains invalid characters"); + } + + private EndpointId migrateLegacyName(String name) { EndpointId.resetLoggedWarnings(); MockEnvironment environment = new MockEnvironment(); environment.setProperty("management.endpoints.migrate-legacy-ids", "true"); - EndpointId endpointId = EndpointId.of(environment, "foo.bar"); - assertThat(endpointId.toString()).isEqualTo("foobar"); - assertThat(output).doesNotContain("contains invalid characters"); + return EndpointId.of(environment, name); } @Test diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/InvocationContextTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/InvocationContextTests.java index b71184c10971..1a265cd77fda 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/InvocationContextTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/InvocationContextTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,8 +21,6 @@ import org.junit.jupiter.api.Test; -import org.springframework.boot.actuate.endpoint.http.ApiVersion; - import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.Mockito.mock; @@ -39,9 +37,23 @@ class InvocationContextTests { private final Map arguments = Collections.singletonMap("test", "value"); @Test + @SuppressWarnings("deprecation") void createWhenApiVersionIsNullUsesLatestVersion() { InvocationContext context = new InvocationContext(null, this.securityContext, this.arguments); - assertThat(context.getApiVersion()).isEqualTo(ApiVersion.LATEST); + assertThat(context.getApiVersion()).isEqualTo(org.springframework.boot.actuate.endpoint.http.ApiVersion.LATEST); + } + + @Test + @Deprecated + void whenCreatedWithoutApiVersionThenGetApiVersionReturnsLatestVersion() { + InvocationContext context = new InvocationContext(this.securityContext, this.arguments); + assertThat(context.getApiVersion()).isEqualTo(org.springframework.boot.actuate.endpoint.http.ApiVersion.LATEST); + } + + @Test + void whenCreatedWithoutApiVersionThenResolveApiVersionReturnsLatestVersion() { + InvocationContext context = new InvocationContext(this.securityContext, this.arguments); + assertThat(context.resolveArgument(ApiVersion.class)).isEqualTo(ApiVersion.LATEST); } @Test @@ -57,17 +69,26 @@ void createWhenArgumentsIsNullThrowsException() { } @Test + @SuppressWarnings("deprecation") void getApiVersionReturnsApiVersion() { - InvocationContext context = new InvocationContext(ApiVersion.V2, this.securityContext, this.arguments); - assertThat(context.getApiVersion()).isEqualTo(ApiVersion.V2); + InvocationContext context = new InvocationContext(org.springframework.boot.actuate.endpoint.http.ApiVersion.V2, + this.securityContext, this.arguments); + assertThat(context.getApiVersion()).isEqualTo(org.springframework.boot.actuate.endpoint.http.ApiVersion.V2); } @Test + @SuppressWarnings("deprecation") void getSecurityContextReturnsSecurityContext() { InvocationContext context = new InvocationContext(this.securityContext, this.arguments); assertThat(context.getSecurityContext()).isEqualTo(this.securityContext); } + @Test + void resolveSecurityContextReturnsSecurityContext() { + InvocationContext context = new InvocationContext(this.securityContext, this.arguments); + assertThat(context.resolveArgument(SecurityContext.class)).isEqualTo(this.securityContext); + } + @Test void getArgumentsReturnsArguments() { InvocationContext context = new InvocationContext(this.securityContext, this.arguments); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/ProducibleOperationArgumentResolverTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/ProducibleOperationArgumentResolverTests.java new file mode 100644 index 000000000000..a63dd31640a0 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/ProducibleOperationArgumentResolverTests.java @@ -0,0 +1,153 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.endpoint; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Supplier; + +import org.junit.jupiter.api.Test; + +import org.springframework.util.MimeType; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Test for {@link ProducibleOperationArgumentResolver}. + * + * @author Andy Wilkinson + * @author Phillip Webb + */ +class ProducibleOperationArgumentResolverTests { + + private static final String V2_JSON = ApiVersion.V2.getProducedMimeType().toString(); + + private static final String V3_JSON = ApiVersion.V3.getProducedMimeType().toString(); + + @Test + void whenAcceptHeaderIsEmptyThenHighestOrdinalIsReturned() { + assertThat(resolve(acceptHeader())).isEqualTo(ApiVersion.V3); + } + + @Test + void whenAcceptHeaderIsEmptyAndWithDefaultThenDefaultIsReturned() { + assertThat(resolve(acceptHeader(), WithDefault.class)).isEqualTo(WithDefault.TWO); + } + + @Test + void whenEverythingIsAcceptableThenHighestOrdinalIsReturned() { + assertThat(resolve(acceptHeader("*/*"))).isEqualTo(ApiVersion.V3); + } + + @Test + void whenEverythingIsAcceptableWithDefaultThenDefaultIsReturned() { + assertThat(resolve(acceptHeader("*/*"), WithDefault.class)).isEqualTo(WithDefault.TWO); + } + + @Test + void whenNothingIsAcceptableThenNullIsReturned() { + assertThat(resolve(acceptHeader("image/png"))).isEqualTo(null); + } + + @Test + void whenSingleValueIsAcceptableThenMatchingEnumValueIsReturned() { + assertThat(new ProducibleOperationArgumentResolver(acceptHeader(V2_JSON)).resolve(ApiVersion.class)) + .isEqualTo(ApiVersion.V2); + assertThat(new ProducibleOperationArgumentResolver(acceptHeader(V3_JSON)).resolve(ApiVersion.class)) + .isEqualTo(ApiVersion.V3); + } + + @Test + void whenMultipleValuesAreAcceptableThenHighestOrdinalIsReturned() { + assertThat(resolve(acceptHeader(V2_JSON, V3_JSON))).isEqualTo(ApiVersion.V3); + } + + @Test + void whenMultipleValuesAreAcceptableAsSingleHeaderThenHighestOrdinalIsReturned() { + assertThat(resolve(acceptHeader(V2_JSON + "," + V3_JSON))).isEqualTo(ApiVersion.V3); + } + + @Test + void withMultipleValuesOneOfWhichIsAllReturnsDefault() { + assertThat(resolve(acceptHeader("one/one", "*/*"), WithDefault.class)).isEqualTo(WithDefault.TWO); + } + + @Test + void whenMultipleDefaultsThrowsException() { + assertThatIllegalStateException().isThrownBy(() -> resolve(acceptHeader("one/one"), WithMultipleDefaults.class)) + .withMessageContaining("Multiple default values"); + } + + private Supplier> acceptHeader(String... types) { + List value = Arrays.asList(types); + return () -> (value.isEmpty() ? null : value); + } + + private ApiVersion resolve(Supplier> accepts) { + return resolve(accepts, ApiVersion.class); + } + + private T resolve(Supplier> accepts, Class type) { + return new ProducibleOperationArgumentResolver(accepts).resolve(type); + } + + enum WithDefault implements Producible { + + ONE("one/one"), + + TWO("two/two") { + + @Override + public boolean isDefault() { + return true; + } + + }, + + THREE("three/three"); + + private final MimeType mimeType; + + WithDefault(String mimeType) { + this.mimeType = MimeType.valueOf(mimeType); + } + + @Override + public MimeType getProducedMimeType() { + return this.mimeType; + } + + } + + enum WithMultipleDefaults implements Producible { + + ONE, TWO, THREE; + + @Override + public boolean isDefault() { + return true; + } + + @Override + public MimeType getProducedMimeType() { + return MimeType.valueOf("image/jpeg"); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/SanitizerTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/SanitizerTests.java index deeab48b5756..5d5299935106 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/SanitizerTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/SanitizerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,11 @@ package org.springframework.boot.actuate.endpoint; +import java.util.stream.Stream; + import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import static org.assertj.core.api.Assertions.assertThat; @@ -25,11 +29,13 @@ * * @author Phillip Webb * @author Stephane Nicoll + * @author Chris Bono + * @author David Good */ class SanitizerTests { @Test - void defaults() { + void defaultNonUriKeys() { Sanitizer sanitizer = new Sanitizer(); assertThat(sanitizer.sanitize("password", "secret")).isEqualTo("******"); assertThat(sanitizer.sanitize("my-password", "secret")).isEqualTo("******"); @@ -40,23 +46,116 @@ void defaults() { assertThat(sanitizer.sanitize("sometoken", "secret")).isEqualTo("******"); assertThat(sanitizer.sanitize("find", "secret")).isEqualTo("secret"); assertThat(sanitizer.sanitize("sun.java.command", "--spring.redis.password=pa55w0rd")).isEqualTo("******"); - assertThat(sanitizer.sanitize("my.uri", "http://user:password@localhost:8080")) - .isEqualTo("http://user:******@localhost:8080"); + assertThat(sanitizer.sanitize("SPRING_APPLICATION_JSON", "{password:123}")).isEqualTo("******"); + assertThat(sanitizer.sanitize("spring.application.json", "{password:123}")).isEqualTo("******"); + assertThat(sanitizer.sanitize("VCAP_SERVICES", "{json}")).isEqualTo("******"); + assertThat(sanitizer.sanitize("vcap.services.db.codeword", "secret")).isEqualTo("******"); } @Test - void uriWithNoPasswordShouldNotBeSanitized() { + void whenAdditionalKeysAreAddedValuesOfBothThemAndTheDefaultKeysAreSanitized() { Sanitizer sanitizer = new Sanitizer(); - assertThat(sanitizer.sanitize("my.uri", "http://localhost:8080")).isEqualTo("http://localhost:8080"); + sanitizer.keysToSanitize("find", "confidential"); + assertThat(sanitizer.sanitize("password", "secret")).isEqualTo("******"); + assertThat(sanitizer.sanitize("my-password", "secret")).isEqualTo("******"); + assertThat(sanitizer.sanitize("my-OTHER.paSSword", "secret")).isEqualTo("******"); + assertThat(sanitizer.sanitize("somesecret", "secret")).isEqualTo("******"); + assertThat(sanitizer.sanitize("somekey", "secret")).isEqualTo("******"); + assertThat(sanitizer.sanitize("token", "secret")).isEqualTo("******"); + assertThat(sanitizer.sanitize("sometoken", "secret")).isEqualTo("******"); + assertThat(sanitizer.sanitize("find", "secret")).isEqualTo("******"); + assertThat(sanitizer.sanitize("sun.java.command", "--spring.redis.password=pa55w0rd")).isEqualTo("******"); + assertThat(sanitizer.sanitize("confidential", "secret")).isEqualTo("******"); + assertThat(sanitizer.sanitize("private", "secret")).isEqualTo("secret"); } - @Test - void uriWithPasswordMatchingOtherPartsOfString() { + @ParameterizedTest(name = "key = {0}") + @MethodSource("matchingUriUserInfoKeys") + void uriWithSingleValueWithPasswordShouldBeSanitized(String key) { + Sanitizer sanitizer = new Sanitizer(); + assertThat(sanitizer.sanitize(key, "http://user:password@localhost:8080")) + .isEqualTo("http://user:******@localhost:8080"); + } + + @ParameterizedTest(name = "key = {0}") + @MethodSource("matchingUriUserInfoKeys") + void uriWithNonAlphaSchemeCharactersAndSingleValueWithPasswordShouldBeSanitized(String key) { + Sanitizer sanitizer = new Sanitizer(); + assertThat(sanitizer.sanitize(key, "s-ch3m.+-e://user:password@localhost:8080")) + .isEqualTo("s-ch3m.+-e://user:******@localhost:8080"); + } + + @ParameterizedTest(name = "key = {0}") + @MethodSource("matchingUriUserInfoKeys") + void uriWithSingleValueWithNoPasswordShouldNotBeSanitized(String key) { + Sanitizer sanitizer = new Sanitizer(); + assertThat(sanitizer.sanitize(key, "http://localhost:8080")).isEqualTo("http://localhost:8080"); + assertThat(sanitizer.sanitize(key, "http://user@localhost:8080")).isEqualTo("http://user@localhost:8080"); + } + + @ParameterizedTest(name = "key = {0}") + @MethodSource("matchingUriUserInfoKeys") + void uriWithSingleValueWithPasswordMatchingOtherPartsOfStringShouldBeSanitized(String key) { Sanitizer sanitizer = new Sanitizer(); - assertThat(sanitizer.sanitize("my.uri", "http://user://@localhost:8080")) + assertThat(sanitizer.sanitize(key, "http://user://@localhost:8080")) .isEqualTo("http://user:******@localhost:8080"); } + @ParameterizedTest(name = "key = {0}") + @MethodSource("matchingUriUserInfoKeys") + void uriWithMultipleValuesEachWithPasswordShouldHaveAllSanitized(String key) { + Sanitizer sanitizer = new Sanitizer(); + assertThat( + sanitizer.sanitize(key, "http://user1:password1@localhost:8080,http://user2:password2@localhost:8082")) + .isEqualTo("http://user1:******@localhost:8080,http://user2:******@localhost:8082"); + } + + @ParameterizedTest(name = "key = {0}") + @MethodSource("matchingUriUserInfoKeys") + void uriWithMultipleValuesNoneWithPasswordShouldHaveNoneSanitized(String key) { + Sanitizer sanitizer = new Sanitizer(); + assertThat(sanitizer.sanitize(key, "http://user@localhost:8080,http://localhost:8082")) + .isEqualTo("http://user@localhost:8080,http://localhost:8082"); + } + + @ParameterizedTest(name = "key = {0}") + @MethodSource("matchingUriUserInfoKeys") + void uriWithMultipleValuesSomeWithPasswordShouldHaveThoseSanitized(String key) { + Sanitizer sanitizer = new Sanitizer(); + assertThat(sanitizer.sanitize(key, + "http://user1:password1@localhost:8080,http://user2@localhost:8082,http://localhost:8083")).isEqualTo( + "http://user1:******@localhost:8080,http://user2@localhost:8082,http://localhost:8083"); + } + + @ParameterizedTest(name = "key = {0}") + @MethodSource("matchingUriUserInfoKeys") + void uriWithMultipleValuesWithPasswordMatchingOtherPartsOfStringShouldBeSanitized(String key) { + Sanitizer sanitizer = new Sanitizer(); + assertThat(sanitizer.sanitize(key, "http://user1://@localhost:8080,http://user2://@localhost:8082")) + .isEqualTo("http://user1:******@localhost:8080,http://user2:******@localhost:8082"); + } + + @ParameterizedTest(name = "key = {0}") + @MethodSource("matchingUriUserInfoKeys") + void uriKeyWithUserProvidedListLiteralShouldBeSanitized(String key) { + Sanitizer sanitizer = new Sanitizer(); + assertThat(sanitizer.sanitize(key, "[amqp://username:password@host/]")) + .isEqualTo("[amqp://username:******@host/]"); + assertThat(sanitizer.sanitize(key, + "[http://user1:password1@localhost:8080,http://user2@localhost:8082,http://localhost:8083]")).isEqualTo( + "[http://user1:******@localhost:8080,http://user2@localhost:8082,http://localhost:8083]"); + assertThat(sanitizer.sanitize(key, + "[http://user1:password1@localhost:8080,http://user2:password2@localhost:8082]")) + .isEqualTo("[http://user1:******@localhost:8080,http://user2:******@localhost:8082]"); + assertThat(sanitizer.sanitize(key, "[http://user1@localhost:8080,http://user2@localhost:8082]")) + .isEqualTo("[http://user1@localhost:8080,http://user2@localhost:8082]"); + } + + private static Stream matchingUriUserInfoKeys() { + return Stream.of("uri", "my.uri", "myuri", "uris", "my.uris", "myuris", "url", "my.url", "myurl", "urls", + "my.urls", "myurls", "address", "my.address", "myaddress", "addresses", "my.addresses", "myaddresses"); + } + @Test void regex() { Sanitizer sanitizer = new Sanitizer(".*lock.*"); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationMethodTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationMethodTests.java index 0cbcd19b71e7..ca17253b3a55 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationMethodTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationMethodTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,9 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.endpoint.OperationType; +import org.springframework.boot.actuate.endpoint.Producible; import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.util.MimeType; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -48,12 +50,35 @@ void getProducesMediaTypesShouldReturnMediaTypes() { AnnotationAttributes annotationAttributes = new AnnotationAttributes(); String[] produces = new String[] { "application/json" }; annotationAttributes.put("produces", produces); + annotationAttributes.put("producesFrom", Producible.class); DiscoveredOperationMethod discovered = new DiscoveredOperationMethod(method, OperationType.READ, annotationAttributes); assertThat(discovered.getProducesMediaTypes()).containsExactly("application/json"); } + @Test + void getProducesMediaTypesWhenProducesFromShouldReturnMediaTypes() { + Method method = ReflectionUtils.findMethod(getClass(), "example"); + AnnotationAttributes annotationAttributes = new AnnotationAttributes(); + annotationAttributes.put("produces", new String[0]); + annotationAttributes.put("producesFrom", ExampleProducible.class); + DiscoveredOperationMethod discovered = new DiscoveredOperationMethod(method, OperationType.READ, + annotationAttributes); + assertThat(discovered.getProducesMediaTypes()).containsExactly("one/*", "two/*", "three/*"); + } + void example() { } + enum ExampleProducible implements Producible { + + ONE, TWO, THREE; + + @Override + public MimeType getProducedMimeType() { + return new MimeType(toString().toLowerCase()); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationsFactoryTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationsFactoryTests.java index 033c40d346cd..3fcefeb69f24 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationsFactoryTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationsFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,12 +28,14 @@ import org.springframework.boot.actuate.endpoint.EndpointId; import org.springframework.boot.actuate.endpoint.InvocationContext; import org.springframework.boot.actuate.endpoint.OperationType; +import org.springframework.boot.actuate.endpoint.Producible; import org.springframework.boot.actuate.endpoint.SecurityContext; import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; import org.springframework.boot.actuate.endpoint.invoke.OperationInvokerAdvisor; import org.springframework.boot.actuate.endpoint.invoke.OperationParameters; import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper; import org.springframework.boot.actuate.endpoint.invoke.reflect.OperationMethod; +import org.springframework.util.MimeType; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -121,6 +123,14 @@ void createOperationShouldApplyAdvisors() { assertThat(advisor.getParameters()).isEmpty(); } + @Test + void createOperationShouldApplyProducesFrom() { + TestOperation operation = getFirst( + this.factory.createOperations(EndpointId.of("test"), new ExampleWithProducesFrom())); + DiscoveredOperationMethod method = (DiscoveredOperationMethod) operation.getOperationMethod(); + assertThat(method.getProducesMediaTypes()).containsExactly("one/*", "two/*", "three/*"); + } + private T getFirst(Iterable iterable) { return iterable.iterator().next(); } @@ -175,6 +185,15 @@ String read(String name) { } + static class ExampleWithProducesFrom { + + @ReadOperation(producesFrom = ExampleProducible.class) + String read() { + return "read"; + } + + } + static class TestDiscoveredOperationsFactory extends DiscoveredOperationsFactory { TestDiscoveredOperationsFactory(ParameterValueMapper parameterValueMapper, @@ -229,4 +248,15 @@ OperationParameters getParameters() { } + enum ExampleProducible implements Producible { + + ONE, TWO, THREE; + + @Override + public MimeType getProducedMimeType() { + return new MimeType(toString().toLowerCase()); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/EndpointDiscovererTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/EndpointDiscovererTests.java index 747ef672cf66..fde226e419d4 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/EndpointDiscovererTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/annotation/EndpointDiscovererTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -267,8 +267,7 @@ private void hasTestEndpoint(AnnotationConfigApplicationContext context) { Map endpoints = mapEndpoints(discoverer.getEndpoints()); assertThat(endpoints).containsOnlyKeys(EndpointId.of("test")); Map operations = mapOperations(endpoints.get(EndpointId.of("test"))); - assertThat(operations).hasSize(4); - assertThat(operations).containsKeys(); + assertThat(operations).containsOnlyKeys(testEndpointMethods()); } private Method[] testEndpointMethods() { diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/http/ApiVersionTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/http/ApiVersionTests.java index 45408d21581c..d7bf34f56754 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/http/ApiVersionTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/http/ApiVersionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ * * @author Phillip Webb */ +@Deprecated class ApiVersionTests { @Test @@ -39,24 +40,28 @@ void latestIsLatestVersion() { } @Test + @Deprecated void fromHttpHeadersWhenEmptyReturnsLatest() { ApiVersion version = ApiVersion.fromHttpHeaders(Collections.emptyMap()); assertThat(version).isEqualTo(ApiVersion.V3); } @Test + @Deprecated void fromHttpHeadersWhenHasSingleV2HeaderReturnsV2() { ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader(ActuatorMediaType.V2_JSON)); assertThat(version).isEqualTo(ApiVersion.V2); } @Test + @Deprecated void fromHttpHeadersWhenHasSingleV3HeaderReturnsV3() { ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader(ActuatorMediaType.V3_JSON)); assertThat(version).isEqualTo(ApiVersion.V3); } @Test + @Deprecated void fromHttpHeadersWhenHasV2AndV3HeaderReturnsV3() { ApiVersion version = ApiVersion .fromHttpHeaders(acceptHeader(ActuatorMediaType.V2_JSON, ActuatorMediaType.V3_JSON)); @@ -64,6 +69,7 @@ void fromHttpHeadersWhenHasV2AndV3HeaderReturnsV3() { } @Test + @Deprecated void fromHttpHeadersWhenHasV2AndV3AsOneHeaderReturnsV3() { ApiVersion version = ApiVersion .fromHttpHeaders(acceptHeader(ActuatorMediaType.V2_JSON + "," + ActuatorMediaType.V3_JSON)); @@ -71,17 +77,26 @@ void fromHttpHeadersWhenHasV2AndV3AsOneHeaderReturnsV3() { } @Test + @Deprecated void fromHttpHeadersWhenHasSingleHeaderWithoutJsonReturnsHeader() { ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader("application/vnd.spring-boot.actuator.v2")); assertThat(version).isEqualTo(ApiVersion.V2); } @Test + @Deprecated void fromHttpHeadersWhenHasUnknownVersionReturnsLatest() { ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader("application/vnd.spring-boot.actuator.v200")); assertThat(version).isEqualTo(ApiVersion.V3); } + @Test + @Deprecated + void fromHttpHeadersWhenAcceptsEverythingReturnsLatest() { + ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader("*/*")); + assertThat(version).isEqualTo(ApiVersion.V3); + } + private Map> acceptHeader(String... types) { List value = Arrays.asList(types); return value.isEmpty() ? Collections.emptyMap() : Collections.singletonMap("Accept", value); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/convert/ConversionServiceParameterValueMapperTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/convert/ConversionServiceParameterValueMapperTests.java index 3b82e15475d9..c2edffcee88d 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/convert/ConversionServiceParameterValueMapperTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/convert/ConversionServiceParameterValueMapperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,9 +30,9 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; /** * Tests for {@link ConversionServiceParameterValueMapper}. @@ -47,7 +47,7 @@ void mapParameterShouldDelegateToConversionService() { ConversionServiceParameterValueMapper mapper = new ConversionServiceParameterValueMapper(conversionService); Object mapped = mapper.mapParameterValue(new TestOperationParameter(Integer.class), "123"); assertThat(mapped).isEqualTo(123); - verify(conversionService).convert("123", Integer.class); + then(conversionService).should().convert("123", Integer.class); } @Test diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameterTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameterTests.java index 5a5a3695457d..d1c324b074aa 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameterTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,14 @@ package org.springframework.boot.actuate.endpoint.invoke.reflect; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Method; +import javax.annotation.Nonnull; +import javax.annotation.meta.TypeQualifier; +import javax.annotation.meta.When; + import org.junit.jupiter.api.Test; import org.springframework.lang.Nullable; @@ -32,33 +38,77 @@ */ class OperationMethodParameterTests { - private Method method = ReflectionUtils.findMethod(getClass(), "example", String.class, String.class); + private Method example = ReflectionUtils.findMethod(getClass(), "example", String.class, String.class); + + private Method exampleJsr305 = ReflectionUtils.findMethod(getClass(), "exampleJsr305", String.class, String.class); + + private Method exampleMetaJsr305 = ReflectionUtils.findMethod(getClass(), "exampleMetaJsr305", String.class, + String.class); + + private Method exampleJsr305NonNull = ReflectionUtils.findMethod(getClass(), "exampleJsr305NonNull", String.class, + String.class); @Test void getNameShouldReturnName() { - OperationMethodParameter parameter = new OperationMethodParameter("name", this.method.getParameters()[0]); + OperationMethodParameter parameter = new OperationMethodParameter("name", this.example.getParameters()[0]); assertThat(parameter.getName()).isEqualTo("name"); } @Test void getTypeShouldReturnType() { - OperationMethodParameter parameter = new OperationMethodParameter("name", this.method.getParameters()[0]); + OperationMethodParameter parameter = new OperationMethodParameter("name", this.example.getParameters()[0]); assertThat(parameter.getType()).isEqualTo(String.class); } @Test void isMandatoryWhenNoAnnotationShouldReturnTrue() { - OperationMethodParameter parameter = new OperationMethodParameter("name", this.method.getParameters()[0]); + OperationMethodParameter parameter = new OperationMethodParameter("name", this.example.getParameters()[0]); assertThat(parameter.isMandatory()).isTrue(); } @Test void isMandatoryWhenNullableAnnotationShouldReturnFalse() { - OperationMethodParameter parameter = new OperationMethodParameter("name", this.method.getParameters()[1]); + OperationMethodParameter parameter = new OperationMethodParameter("name", this.example.getParameters()[1]); + assertThat(parameter.isMandatory()).isFalse(); + } + + @Test + void isMandatoryWhenJsrNullableAnnotationShouldReturnFalse() { + OperationMethodParameter parameter = new OperationMethodParameter("name", + this.exampleJsr305.getParameters()[1]); + assertThat(parameter.isMandatory()).isFalse(); + } + + @Test + void isMandatoryWhenJsrMetaNullableAnnotationShouldReturnFalse() { + OperationMethodParameter parameter = new OperationMethodParameter("name", + this.exampleMetaJsr305.getParameters()[1]); assertThat(parameter.isMandatory()).isFalse(); } + @Test + void isMandatoryWhenJsrNonnullAnnotationShouldReturnTrue() { + OperationMethodParameter parameter = new OperationMethodParameter("name", + this.exampleJsr305NonNull.getParameters()[1]); + assertThat(parameter.isMandatory()).isTrue(); + } + void example(String one, @Nullable String two) { + } + + void exampleJsr305(String one, @javax.annotation.Nullable String two) { + } + + void exampleMetaJsr305(String one, @MetaNullable String two) { + } + + void exampleJsr305NonNull(String one, @javax.annotation.Nonnull String two) { + } + + @TypeQualifier + @Retention(RetentionPolicy.RUNTIME) + @Nonnull(when = When.MAYBE) + @interface MetaNullable { } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/ReflectiveOperationInvokerTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/ReflectiveOperationInvokerTests.java index 11eb5bae88fa..25aa5869ad95 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/ReflectiveOperationInvokerTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/reflect/ReflectiveOperationInvokerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,10 +21,10 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.boot.actuate.endpoint.ApiVersion; import org.springframework.boot.actuate.endpoint.InvocationContext; import org.springframework.boot.actuate.endpoint.OperationType; import org.springframework.boot.actuate.endpoint.SecurityContext; -import org.springframework.boot.actuate.endpoint.http.ApiVersion; import org.springframework.boot.actuate.endpoint.invoke.MissingParametersException; import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper; import org.springframework.lang.Nullable; diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerAdvisorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerAdvisorTests.java index 9ac20f5da67f..1d468ebbbef9 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerAdvisorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerAdvisorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,9 +21,11 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.actuate.endpoint.ApiVersion; import org.springframework.boot.actuate.endpoint.EndpointId; import org.springframework.boot.actuate.endpoint.OperationType; import org.springframework.boot.actuate.endpoint.SecurityContext; @@ -36,7 +38,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.verify; +import static org.mockito.BDDMockito.then; /** * Tests for {@link CachingOperationInvokerAdvisor}. @@ -44,6 +46,7 @@ * @author Phillip Webb * @author Stephane Nicoll */ +@ExtendWith(MockitoExtension.class) class CachingOperationInvokerAdvisorTests { @Mock @@ -56,7 +59,6 @@ class CachingOperationInvokerAdvisorTests { @BeforeEach void setup() { - MockitoAnnotations.initMocks(this); this.advisor = new CachingOperationInvokerAdvisor(this.timeToLive); } @@ -83,7 +85,7 @@ void applyWhenTimeToLiveReturnsNullShouldNotAddAdvise() { OperationInvoker advised = this.advisor.apply(EndpointId.of("foo"), OperationType.READ, parameters, this.invoker); assertThat(advised).isSameAs(this.invoker); - verify(this.timeToLive).apply(EndpointId.of("foo")); + then(this.timeToLive).should().apply(EndpointId.of("foo")); } @Test @@ -93,7 +95,7 @@ void applyWhenTimeToLiveIsZeroShouldNotAddAdvise() { OperationInvoker advised = this.advisor.apply(EndpointId.of("foo"), OperationType.READ, parameters, this.invoker); assertThat(advised).isSameAs(this.invoker); - verify(this.timeToLive).apply(EndpointId.of("foo")); + then(this.timeToLive).should().apply(EndpointId.of("foo")); } @Test @@ -117,6 +119,13 @@ void applyWithSecurityContextShouldAddAdvise() { assertAdviseIsApplied(parameters); } + @Test + void applyWithApiVersionShouldAddAdvise() { + OperationParameters parameters = getParameters("getWithApiVersion", ApiVersion.class, String.class); + given(this.timeToLive.apply(any())).willReturn(100L); + assertAdviseIsApplied(parameters); + } + private void assertAdviseIsApplied(OperationParameters parameters) { OperationInvoker advised = this.advisor.apply(EndpointId.of("foo"), OperationType.READ, parameters, this.invoker); @@ -152,6 +161,10 @@ String getWithSecurityContext(SecurityContext securityContext, @Nullable String return ""; } + String getWithApiVersion(ApiVersion apiVersion, @Nullable String bar) { + return ""; + } + } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerTests.java index fabefe5c5d67..77549d036851 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoker/cache/CachingOperationInvokerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,27 +17,29 @@ package org.springframework.boot.actuate.endpoint.invoker.cache; import java.security.Principal; -import java.util.Arrays; +import java.time.Duration; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.boot.actuate.endpoint.ApiVersion; import org.springframework.boot.actuate.endpoint.InvocationContext; +import org.springframework.boot.actuate.endpoint.OperationArgumentResolver; import org.springframework.boot.actuate.endpoint.SecurityContext; -import org.springframework.boot.actuate.endpoint.invoke.MissingParametersException; import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; /** * Tests for {@link CachingOperationInvoker}. @@ -48,6 +50,8 @@ */ class CachingOperationInvokerTests { + private static final long CACHE_TTL = Duration.ofHours(1).toMillis(); + @Test void createInstanceWithTtlSetToZero() { assertThatIllegalArgumentException() @@ -60,6 +64,11 @@ void cacheInTtlRangeWithNoParameter() { assertCacheIsUsed(Collections.emptyMap()); } + @Test + void cacheInTtlWithPrincipal() { + assertCacheIsUsed(Collections.emptyMap(), mock(Principal.class)); + } + @Test void cacheInTtlWithNullParameters() { Map parameters = new HashMap<>(); @@ -70,40 +79,73 @@ void cacheInTtlWithNullParameters() { @Test void cacheInTtlWithMonoResponse() { - MonoOperationInvoker.invocations = 0; + MonoOperationInvoker.invocations = new AtomicInteger(); MonoOperationInvoker target = new MonoOperationInvoker(); InvocationContext context = new InvocationContext(mock(SecurityContext.class), Collections.emptyMap()); - CachingOperationInvoker invoker = new CachingOperationInvoker(target, 500L); + CachingOperationInvoker invoker = new CachingOperationInvoker(target, CACHE_TTL); Object response = ((Mono) invoker.invoke(context)).block(); Object cachedResponse = ((Mono) invoker.invoke(context)).block(); - assertThat(MonoOperationInvoker.invocations).isEqualTo(1); + assertThat(MonoOperationInvoker.invocations).hasValue(1); assertThat(response).isSameAs(cachedResponse); } @Test void cacheInTtlWithFluxResponse() { - FluxOperationInvoker.invocations = 0; + FluxOperationInvoker.invocations = new AtomicInteger(); FluxOperationInvoker target = new FluxOperationInvoker(); InvocationContext context = new InvocationContext(mock(SecurityContext.class), Collections.emptyMap()); - CachingOperationInvoker invoker = new CachingOperationInvoker(target, 500L); + CachingOperationInvoker invoker = new CachingOperationInvoker(target, CACHE_TTL); Object response = ((Flux) invoker.invoke(context)).blockLast(); Object cachedResponse = ((Flux) invoker.invoke(context)).blockLast(); - assertThat(FluxOperationInvoker.invocations).isEqualTo(1); + assertThat(FluxOperationInvoker.invocations).hasValue(1); assertThat(response).isSameAs(cachedResponse); } + @Test // gh-28313 + void cacheWhenEachPrincipalIsUniqueDoesNotConsumeTooMuchMemory() throws Exception { + MonoOperationInvoker target = new MonoOperationInvoker(); + CachingOperationInvoker invoker = new CachingOperationInvoker(target, 50L); + int count = 1000; + for (int i = 0; i < count; i++) { + invokeWithUniquePrincipal(invoker); + } + long expired = System.currentTimeMillis() + 50; + while (System.currentTimeMillis() < expired) { + Thread.sleep(10); + } + invokeWithUniquePrincipal(invoker); + assertThat(invoker).extracting("cachedResponses").asInstanceOf(InstanceOfAssertFactories.MAP) + .hasSizeLessThan(count); + } + + private void invokeWithUniquePrincipal(CachingOperationInvoker invoker) { + SecurityContext securityContext = mock(SecurityContext.class); + Principal principal = mock(Principal.class); + given(securityContext.getPrincipal()).willReturn(principal); + InvocationContext context = new InvocationContext(securityContext, Collections.emptyMap()); + ((Mono) invoker.invoke(context)).block(); + } + private void assertCacheIsUsed(Map parameters) { + assertCacheIsUsed(parameters, null); + } + + private void assertCacheIsUsed(Map parameters, Principal principal) { OperationInvoker target = mock(OperationInvoker.class); Object expected = new Object(); - InvocationContext context = new InvocationContext(mock(SecurityContext.class), parameters); + SecurityContext securityContext = mock(SecurityContext.class); + if (principal != null) { + given(securityContext.getPrincipal()).willReturn(principal); + } + InvocationContext context = new InvocationContext(securityContext, parameters); given(target.invoke(context)).willReturn(expected); - CachingOperationInvoker invoker = new CachingOperationInvoker(target, 500L); + CachingOperationInvoker invoker = new CachingOperationInvoker(target, CACHE_TTL); Object response = invoker.invoke(context); assertThat(response).isSameAs(expected); - verify(target, times(1)).invoke(context); + then(target).should().invoke(context); Object cachedResponse = invoker.invoke(context); assertThat(cachedResponse).isSameAs(response); - verifyNoMoreInteractions(target); + then(target).shouldHaveNoMoreInteractions(); } @Test @@ -114,26 +156,52 @@ void targetAlwaysInvokedWithParameters() { parameters.put("something", null); InvocationContext context = new InvocationContext(mock(SecurityContext.class), parameters); given(target.invoke(context)).willReturn(new Object()); - CachingOperationInvoker invoker = new CachingOperationInvoker(target, 500L); + CachingOperationInvoker invoker = new CachingOperationInvoker(target, CACHE_TTL); invoker.invoke(context); invoker.invoke(context); invoker.invoke(context); - verify(target, times(3)).invoke(context); + then(target).should(times(3)).invoke(context); } @Test - void targetAlwaysInvokedWithPrincipal() { + void targetAlwaysInvokedWithDifferentPrincipals() { OperationInvoker target = mock(OperationInvoker.class); Map parameters = new HashMap<>(); SecurityContext securityContext = mock(SecurityContext.class); - given(securityContext.getPrincipal()).willReturn(mock(Principal.class)); + given(securityContext.getPrincipal()).willReturn(mock(Principal.class), mock(Principal.class), + mock(Principal.class)); InvocationContext context = new InvocationContext(securityContext, parameters); - given(target.invoke(context)).willReturn(new Object()); - CachingOperationInvoker invoker = new CachingOperationInvoker(target, 500L); - invoker.invoke(context); - invoker.invoke(context); - invoker.invoke(context); - verify(target, times(3)).invoke(context); + Object result1 = new Object(); + Object result2 = new Object(); + Object result3 = new Object(); + given(target.invoke(context)).willReturn(result1, result2, result3); + CachingOperationInvoker invoker = new CachingOperationInvoker(target, CACHE_TTL); + assertThat(invoker.invoke(context)).isEqualTo(result1); + assertThat(invoker.invoke(context)).isEqualTo(result2); + assertThat(invoker.invoke(context)).isEqualTo(result3); + then(target).should(times(3)).invoke(context); + } + + @Test + void targetInvokedWhenCalledWithAndWithoutPrincipal() { + OperationInvoker target = mock(OperationInvoker.class); + Map parameters = new HashMap<>(); + SecurityContext anonymous = mock(SecurityContext.class); + SecurityContext authenticated = mock(SecurityContext.class); + given(authenticated.getPrincipal()).willReturn(mock(Principal.class)); + InvocationContext anonymousContext = new InvocationContext(anonymous, parameters); + Object anonymousResult = new Object(); + given(target.invoke(anonymousContext)).willReturn(anonymousResult); + InvocationContext authenticatedContext = new InvocationContext(authenticated, parameters); + Object authenticatedResult = new Object(); + given(target.invoke(authenticatedContext)).willReturn(authenticatedResult); + CachingOperationInvoker invoker = new CachingOperationInvoker(target, CACHE_TTL); + assertThat(invoker.invoke(anonymousContext)).isEqualTo(anonymousResult); + assertThat(invoker.invoke(authenticatedContext)).isEqualTo(authenticatedResult); + assertThat(invoker.invoke(anonymousContext)).isEqualTo(anonymousResult); + assertThat(invoker.invoke(authenticatedContext)).isEqualTo(authenticatedResult); + then(target).should().invoke(anonymousContext); + then(target).should().invoke(authenticatedContext); } @Test @@ -149,18 +217,38 @@ void targetInvokedWhenCacheExpires() throws InterruptedException { Thread.sleep(10); } invoker.invoke(context); - verify(target, times(2)).invoke(context); + then(target).should(times(2)).invoke(context); + } + + @Test + void targetInvokedWithDifferentApiVersion() { + OperationInvoker target = mock(OperationInvoker.class); + Object expectedV2 = new Object(); + Object expectedV3 = new Object(); + InvocationContext contextV2 = new InvocationContext(mock(SecurityContext.class), Collections.emptyMap(), + new ApiVersionArgumentResolver(ApiVersion.V2)); + InvocationContext contextV3 = new InvocationContext(mock(SecurityContext.class), Collections.emptyMap(), + new ApiVersionArgumentResolver(ApiVersion.V3)); + given(target.invoke(contextV2)).willReturn(expectedV2); + given(target.invoke(contextV3)).willReturn(expectedV3); + CachingOperationInvoker invoker = new CachingOperationInvoker(target, CACHE_TTL); + Object response = invoker.invoke(contextV2); + assertThat(response).isSameAs(expectedV2); + then(target).should().invoke(contextV2); + Object cachedResponse = invoker.invoke(contextV3); + assertThat(cachedResponse).isNotSameAs(response); + then(target).should().invoke(contextV3); } private static class MonoOperationInvoker implements OperationInvoker { - static int invocations; + static AtomicInteger invocations = new AtomicInteger(); @Override - public Object invoke(InvocationContext context) throws MissingParametersException { + public Mono invoke(InvocationContext context) { return Mono.fromCallable(() -> { - invocations++; - return Mono.just("test"); + invocations.incrementAndGet(); + return "test"; }); } @@ -168,14 +256,32 @@ public Object invoke(InvocationContext context) throws MissingParametersExceptio private static class FluxOperationInvoker implements OperationInvoker { - static int invocations; + static AtomicInteger invocations = new AtomicInteger(); @Override - public Object invoke(InvocationContext context) throws MissingParametersException { - return Flux.fromIterable(() -> { - invocations++; - return Arrays.asList("spring", "boot").iterator(); - }); + public Flux invoke(InvocationContext context) { + return Flux.just("spring", "boot").hide().doFirst(invocations::incrementAndGet); + } + + } + + private static final class ApiVersionArgumentResolver implements OperationArgumentResolver { + + private final ApiVersion apiVersion; + + private ApiVersionArgumentResolver(ApiVersion apiVersion) { + this.apiVersion = apiVersion; + } + + @SuppressWarnings("unchecked") + @Override + public T resolve(Class type) { + return (T) this.apiVersion; + } + + @Override + public boolean canResolve(Class type) { + return ApiVersion.class.equals(type); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanTests.java index fa32a928a7fd..227229154c0d 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,12 +22,12 @@ import javax.management.Attribute; import javax.management.AttributeList; import javax.management.AttributeNotFoundException; -import javax.management.InvalidAttributeValueException; import javax.management.MBeanException; import javax.management.MBeanInfo; import javax.management.ReflectionException; import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.beans.FatalBeanException; @@ -38,9 +38,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; /** * Tests for {@link EndpointMBean}. @@ -87,7 +87,7 @@ void invokeShouldInvokeJmxOperation() throws MBeanException, ReflectionException } @Test - void invokeWhenOperationFailedShouldTranslateException() throws MBeanException, ReflectionException { + void invokeWhenOperationFailedShouldTranslateException() { TestExposableJmxEndpoint endpoint = new TestExposableJmxEndpoint(new TestJmxOperation((arguments) -> { throw new FatalBeanException("test failure"); })); @@ -99,7 +99,7 @@ void invokeWhenOperationFailedShouldTranslateException() throws MBeanException, } @Test - void invokeWhenOperationFailedWithJdkExceptionShouldReuseException() throws MBeanException, ReflectionException { + void invokeWhenOperationFailedWithJdkExceptionShouldReuseException() { TestExposableJmxEndpoint endpoint = new TestExposableJmxEndpoint(new TestJmxOperation((arguments) -> { throw new UnsupportedOperationException("test failure"); })); @@ -110,7 +110,7 @@ void invokeWhenOperationFailedWithJdkExceptionShouldReuseException() throws MBea } @Test - void invokeWhenActionNameIsNotAnOperationShouldThrowException() throws MBeanException, ReflectionException { + void invokeWhenActionNameIsNotAnOperationShouldThrowException() { EndpointMBean bean = createEndpointMBean(); assertThatExceptionOfType(ReflectionException.class) .isThrownBy(() -> bean.invoke("missingOperation", NO_PARAMS, NO_SIGNATURE)) @@ -131,7 +131,7 @@ void invokeShouldInvokeJmxOperationWithBeanClassLoader() throws ReflectionExcept } @Test - void invokeWhenOperationIsInvalidShouldThrowException() throws MBeanException, ReflectionException { + void invokeWhenOperationIsInvalidShouldThrowException() { TestJmxOperation operation = new TestJmxOperation() { @Override @@ -156,25 +156,33 @@ void invokeWhenMonoResultShouldBlockOnMono() throws MBeanException, ReflectionEx assertThat(result).isEqualTo("monoResult"); } + @Test + void invokeWhenFluxResultShouldCollectToMonoListAndBlockOnMono() throws MBeanException, ReflectionException { + TestExposableJmxEndpoint endpoint = new TestExposableJmxEndpoint( + new TestJmxOperation((arguments) -> Flux.just("flux", "result"))); + EndpointMBean bean = new EndpointMBean(this.responseMapper, null, endpoint); + Object result = bean.invoke("testOperation", NO_PARAMS, NO_SIGNATURE); + assertThat(result).asList().containsExactly("flux", "result"); + } + @Test void invokeShouldCallResponseMapper() throws MBeanException, ReflectionException { TestJmxOperationResponseMapper responseMapper = spy(this.responseMapper); EndpointMBean bean = new EndpointMBean(responseMapper, null, this.endpoint); bean.invoke("testOperation", NO_PARAMS, NO_SIGNATURE); - verify(responseMapper).mapResponseType(String.class); - verify(responseMapper).mapResponse("result"); + then(responseMapper).should().mapResponseType(String.class); + then(responseMapper).should().mapResponse("result"); } @Test - void getAttributeShouldThrowException() throws AttributeNotFoundException, MBeanException, ReflectionException { + void getAttributeShouldThrowException() { EndpointMBean bean = createEndpointMBean(); assertThatExceptionOfType(AttributeNotFoundException.class).isThrownBy(() -> bean.getAttribute("test")) .withMessageContaining("EndpointMBeans do not support attributes"); } @Test - void setAttributeShouldThrowException() - throws AttributeNotFoundException, InvalidAttributeValueException, MBeanException, ReflectionException { + void setAttributeShouldThrowException() { EndpointMBean bean = createEndpointMBean(); assertThatExceptionOfType(AttributeNotFoundException.class) .isThrownBy(() -> bean.setAttribute(new Attribute("test", "test"))) diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/JacksonJmxOperationResponseMapperTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/JacksonJmxOperationResponseMapperTests.java index 09fbc96a4188..2b3b7273f783 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/JacksonJmxOperationResponseMapperTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/JacksonJmxOperationResponseMapperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,8 +32,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; /** * Tests for {@link JacksonJmxOperationResponseMapper} @@ -59,7 +59,7 @@ void createWhenObjectMapperIsSpecifiedShouldUseObjectMapper() { JacksonJmxOperationResponseMapper mapper = new JacksonJmxOperationResponseMapper(objectMapper); Set response = Collections.singleton("test"); mapper.mapResponse(response); - verify(objectMapper).convertValue(eq(response), any(JavaType.class)); + then(objectMapper).should().convertValue(eq(response), any(JavaType.class)); } @Test diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/JmxEndpointExporterTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/JmxEndpointExporterTests.java index c9a102ddb4d5..b4765c688207 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/JmxEndpointExporterTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/JmxEndpointExporterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,10 +27,12 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.jmx.JmxException; import org.springframework.jmx.export.MBeanExportException; @@ -41,9 +43,8 @@ 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.spy; -import static org.mockito.Mockito.verify; /** * Tests for {@link JmxEndpointExporter}. @@ -51,12 +52,14 @@ * @author Stephane Nicoll * @author Phillip Webb */ +@ExtendWith(MockitoExtension.class) class JmxEndpointExporterTests { @Mock private MBeanServer mBeanServer; - private EndpointObjectNameFactory objectNameFactory = spy(new TestEndpointObjectNameFactory()); + @Spy + private EndpointObjectNameFactory objectNameFactory = new TestEndpointObjectNameFactory(); private JmxOperationResponseMapper responseMapper = new TestJmxOperationResponseMapper(); @@ -72,7 +75,6 @@ class JmxEndpointExporterTests { @BeforeEach void setup() { - MockitoAnnotations.initMocks(this); this.exporter = new JmxEndpointExporter(this.mBeanServer, this.objectNameFactory, this.responseMapper, this.endpoints); } @@ -110,7 +112,7 @@ void createWhenEndpointsIsNullShouldThrowException() { void afterPropertiesSetShouldRegisterMBeans() throws Exception { this.endpoints.add(new TestExposableJmxEndpoint(new TestJmxOperation())); this.exporter.afterPropertiesSet(); - verify(this.mBeanServer).registerMBean(this.objectCaptor.capture(), this.objectNameCaptor.capture()); + then(this.mBeanServer).should().registerMBean(this.objectCaptor.capture(), this.objectNameCaptor.capture()); assertThat(this.objectCaptor.getValue()).isInstanceOf(EndpointMBean.class); assertThat(this.objectNameCaptor.getValue().getKeyProperty("name")).isEqualTo("test"); } @@ -119,7 +121,7 @@ void afterPropertiesSetShouldRegisterMBeans() throws Exception { void registerShouldUseObjectNameFactory() throws Exception { this.endpoints.add(new TestExposableJmxEndpoint(new TestJmxOperation())); this.exporter.afterPropertiesSet(); - verify(this.objectNameFactory).getObjectName(any(ExposableJmxEndpoint.class)); + then(this.objectNameFactory).should().getObjectName(any(ExposableJmxEndpoint.class)); } @Test @@ -145,7 +147,7 @@ void destroyShouldUnregisterMBeans() throws Exception { this.endpoints.add(new TestExposableJmxEndpoint(new TestJmxOperation())); this.exporter.afterPropertiesSet(); this.exporter.destroy(); - verify(this.mBeanServer).unregisterMBean(this.objectNameCaptor.capture()); + then(this.mBeanServer).should().unregisterMBean(this.objectNameCaptor.capture()); assertThat(this.objectNameCaptor.getValue().getKeyProperty("name")).isEqualTo("test"); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/annotation/JmxEndpointDiscovererTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/annotation/JmxEndpointDiscovererTests.java index 13a21ba845b5..3fc4eaa9a3c1 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/annotation/JmxEndpointDiscovererTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/annotation/JmxEndpointDiscovererTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -79,18 +79,18 @@ void getEndpointsShouldDiscoverStandardEndpoints() { assertThat(getSomething.getDescription()).isEqualTo("Invoke getSomething for endpoint test"); assertThat(getSomething.getOutputType()).isEqualTo(String.class); assertThat(getSomething.getParameters()).hasSize(1); - hasDefaultParameter(getSomething, 0, String.class); + assertThat(getSomething.getParameters().get(0).getType()).isEqualTo(String.class); JmxOperation update = operationByName.get("update"); assertThat(update.getDescription()).isEqualTo("Invoke update for endpoint test"); assertThat(update.getOutputType()).isEqualTo(Void.TYPE); assertThat(update.getParameters()).hasSize(2); - hasDefaultParameter(update, 0, String.class); - hasDefaultParameter(update, 1, String.class); + assertThat(update.getParameters().get(0).getType()).isEqualTo(String.class); + assertThat(update.getParameters().get(1).getType()).isEqualTo(String.class); JmxOperation deleteSomething = operationByName.get("deleteSomething"); assertThat(deleteSomething.getDescription()).isEqualTo("Invoke deleteSomething for endpoint test"); assertThat(deleteSomething.getOutputType()).isEqualTo(Void.TYPE); assertThat(deleteSomething.getParameters()).hasSize(1); - hasDefaultParameter(deleteSomething, 0, String.class); + assertThat(deleteSomething.getParameters().get(0).getType()).isEqualTo(String.class); }); } @@ -239,12 +239,6 @@ private void hasDocumentedParameter(JmxOperation operation, int index, String na assertThat(parameter.getDescription()).isEqualTo(description); } - // FIXME rename - private void hasDefaultParameter(JmxOperation operation, int index, Class type) { - JmxOperationParameter parameter = operation.getParameters().get(index); - assertThat(parameter.getType()).isEqualTo(type); - } - private Map discover(JmxEndpointDiscoverer discoverer) { Map byId = new HashMap<>(); discoverer.getEndpoints().forEach((endpoint) -> byId.put(endpoint.getEndpointId(), endpoint)); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/EndpointMediaTypesTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/EndpointMediaTypesTests.java index cee1d83bd922..2f351a0a69b9 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/EndpointMediaTypesTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/EndpointMediaTypesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ import org.junit.jupiter.api.Test; -import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; +import org.springframework.boot.actuate.endpoint.ApiVersion; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -34,12 +34,14 @@ */ class EndpointMediaTypesTests { + private static final String V2_JSON = ApiVersion.V2.getProducedMimeType().toString(); + + private static final String V3_JSON = ApiVersion.V3.getProducedMimeType().toString(); + @Test void defaultReturnsExpectedProducedAndConsumedTypes() { - assertThat(EndpointMediaTypes.DEFAULT.getProduced()).containsExactly(ActuatorMediaType.V3_JSON, - ActuatorMediaType.V2_JSON, "application/json"); - assertThat(EndpointMediaTypes.DEFAULT.getConsumed()).containsExactly(ActuatorMediaType.V3_JSON, - ActuatorMediaType.V2_JSON, "application/json"); + assertThat(EndpointMediaTypes.DEFAULT.getProduced()).containsExactly(V3_JSON, V2_JSON, "application/json"); + assertThat(EndpointMediaTypes.DEFAULT.getConsumed()).containsExactly(V3_JSON, V2_JSON, "application/json"); } @Test diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/ServletEndpointRegistrarTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/ServletEndpointRegistrarTests.java index 861293c57d43..cbbc52befe13 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/ServletEndpointRegistrarTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/ServletEndpointRegistrarTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,12 +26,12 @@ import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.boot.actuate.endpoint.EndpointId; @@ -40,8 +40,8 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link ServletEndpointRegistrar}. @@ -49,6 +49,7 @@ * @author Phillip Webb * @author Stephane Nicoll */ +@ExtendWith(MockitoExtension.class) class ServletEndpointRegistrarTests { @Mock @@ -60,12 +61,6 @@ class ServletEndpointRegistrarTests { @Captor private ArgumentCaptor servlet; - @BeforeEach - void setup() { - MockitoAnnotations.initMocks(this); - given(this.servletContext.addServlet(any(String.class), any(Servlet.class))).willReturn(this.dynamic); - } - @Test void createWhenServletEndpointsIsNullShouldThrowException() { assertThatIllegalArgumentException().isThrownBy(() -> new ServletEndpointRegistrar(null, null)) @@ -93,37 +88,41 @@ void onStartupWhenHasRootBasePathShouldNotAddDuplicateSlash() throws ServletExce } private void assertBasePath(String basePath, String expectedMapping) throws ServletException { + given(this.servletContext.addServlet(any(String.class), any(Servlet.class))).willReturn(this.dynamic); ExposableServletEndpoint endpoint = mockEndpoint(new EndpointServlet(TestServlet.class)); ServletEndpointRegistrar registrar = new ServletEndpointRegistrar(basePath, Collections.singleton(endpoint)); registrar.onStartup(this.servletContext); - verify(this.servletContext).addServlet(eq("test-actuator-endpoint"), this.servlet.capture()); + then(this.servletContext).should().addServlet(eq("test-actuator-endpoint"), this.servlet.capture()); assertThat(this.servlet.getValue()).isInstanceOf(TestServlet.class); - verify(this.dynamic).addMapping(expectedMapping); + then(this.dynamic).should().addMapping(expectedMapping); } @Test void onStartupWhenHasInitParametersShouldRegisterInitParameters() throws Exception { + given(this.servletContext.addServlet(any(String.class), any(Servlet.class))).willReturn(this.dynamic); ExposableServletEndpoint endpoint = mockEndpoint( new EndpointServlet(TestServlet.class).withInitParameter("a", "b")); ServletEndpointRegistrar registrar = new ServletEndpointRegistrar("/actuator", Collections.singleton(endpoint)); registrar.onStartup(this.servletContext); - verify(this.dynamic).setInitParameters(Collections.singletonMap("a", "b")); + then(this.dynamic).should().setInitParameters(Collections.singletonMap("a", "b")); } @Test void onStartupWhenHasLoadOnStartupShouldRegisterLoadOnStartup() throws Exception { + given(this.servletContext.addServlet(any(String.class), any(Servlet.class))).willReturn(this.dynamic); ExposableServletEndpoint endpoint = mockEndpoint(new EndpointServlet(TestServlet.class).withLoadOnStartup(7)); ServletEndpointRegistrar registrar = new ServletEndpointRegistrar("/actuator", Collections.singleton(endpoint)); registrar.onStartup(this.servletContext); - verify(this.dynamic).setLoadOnStartup(7); + then(this.dynamic).should().setLoadOnStartup(7); } @Test void onStartupWhenHasNotLoadOnStartupShouldRegisterDefaultValue() throws Exception { + given(this.servletContext.addServlet(any(String.class), any(Servlet.class))).willReturn(this.dynamic); ExposableServletEndpoint endpoint = mockEndpoint(new EndpointServlet(TestServlet.class)); ServletEndpointRegistrar registrar = new ServletEndpointRegistrar("/actuator", Collections.singleton(endpoint)); registrar.onStartup(this.servletContext); - verify(this.dynamic).setLoadOnStartup(-1); + then(this.dynamic).should().setLoadOnStartup(-1); } private ExposableServletEndpoint mockEndpoint(EndpointServlet endpointServlet) { diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/WebOperationRequestPredicateTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/WebOperationRequestPredicateTests.java index 9ce6f80d419c..ebd7b24ed008 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/WebOperationRequestPredicateTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/WebOperationRequestPredicateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,22 +51,22 @@ void predicatesWhereOneHasAPathAndTheOtherHasAVariableAreNotEqual() { } @Test - void predicatesWithSinglePathVariablesInTheSamplePlaceAreEqual() { + void predicatesWithSinglePathVariablesInTheSamePlaceAreEqual() { assertThat(predicateWithPath("/path/{foo1}")).isEqualTo(predicateWithPath("/path/{foo2}")); } @Test - void predicatesWithSingleWildcardPathVariablesInTheSamplePlaceAreEqual() { + void predicatesWithSingleWildcardPathVariablesInTheSamePlaceAreEqual() { assertThat(predicateWithPath("/path/{*foo1}")).isEqualTo(predicateWithPath("/path/{*foo2}")); } @Test - void predicatesWithSingleWildcardPathVariableAndRegularVariableInTheSamplePlaceAreNotEqual() { + void predicatesWithSingleWildcardPathVariableAndRegularVariableInTheSamePlaceAreNotEqual() { assertThat(predicateWithPath("/path/{*foo1}")).isNotEqualTo(predicateWithPath("/path/{foo2}")); } @Test - void predicatesWithMultiplePathVariablesInTheSamplePlaceAreEqual() { + void predicatesWithMultiplePathVariablesInTheSamePlaceAreEqual() { assertThat(predicateWithPath("/path/{foo1}/more/{bar1}")) .isEqualTo(predicateWithPath("/path/{foo2}/more/{bar2}")); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/AbstractWebEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/AbstractWebEndpointIntegrationTests.java index dcef4df5b5b7..488c1c514c3a 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/AbstractWebEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/AbstractWebEndpointIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ import java.util.function.Supplier; import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.boot.actuate.endpoint.SecurityContext; @@ -54,17 +55,18 @@ import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.verify; +import static org.mockito.BDDMockito.then; /** * Abstract base class for web endpoint integration tests. * * @param the type of application context used by the tests * @author Andy Wilkinson + * @author Scott Frederick */ public abstract class AbstractWebEndpointIntegrationTests { - private static final Duration TIMEOUT = Duration.ofMinutes(6); + private static final Duration TIMEOUT = Duration.ofMinutes(5); private static final String ACTUATOR_MEDIA_TYPE_PATTERN = "application/vnd.test\\+json(;charset=UTF-8)?"; @@ -187,7 +189,7 @@ void writeOperation() { void writeOperationWithVoidResponse() { load(VoidWriteResponseEndpointConfiguration.class, (context, client) -> { client.post().uri("/voidwrite").exchange().expectStatus().isNoContent().expectBody().isEmpty(); - verify(context.getBean(EndpointDelegate.class)).write(); + then(context.getBean(EndpointDelegate.class)).should().write(); }); } @@ -201,7 +203,7 @@ void deleteOperation() { void deleteOperationWithVoidResponse() { load(VoidDeleteResponseEndpointConfiguration.class, (context, client) -> { client.delete().uri("/voiddelete").exchange().expectStatus().isNoContent().expectBody().isEmpty(); - verify(context.getBean(EndpointDelegate.class)).delete(); + then(context.getBean(EndpointDelegate.class)).should().delete(); }); } @@ -211,7 +213,7 @@ void nullIsPassedToTheOperationWhenArgumentIsNotFoundInPostRequestBody() { Map body = new HashMap<>(); body.put("foo", "one"); client.post().uri("/test").bodyValue(body).exchange().expectStatus().isNoContent().expectBody().isEmpty(); - verify(context.getBean(EndpointDelegate.class)).write("one", null); + then(context.getBean(EndpointDelegate.class)).should().write("one", null); }); } @@ -220,7 +222,7 @@ void nullsArePassedToTheOperationWhenPostRequestHasNoBody() { load(TestEndpointConfiguration.class, (context, client) -> { client.post().uri("/test").contentType(MediaType.APPLICATION_JSON).exchange().expectStatus().isNoContent() .expectBody().isEmpty(); - verify(context.getBean(EndpointDelegate.class)).write(null, null); + then(context.getBean(EndpointDelegate.class)).should().write(null, null); }); } @@ -268,6 +270,14 @@ void readOperationWithMonoResponse() { .isOk().expectBody().jsonPath("a").isEqualTo("alpha")); } + @Test + void readOperationWithFluxResponse() { + load(FluxResponseEndpointConfiguration.class, + (client) -> client.get().uri("/flux").exchange().expectStatus().isOk().expectBody().jsonPath("[0].a") + .isEqualTo("alpha").jsonPath("[1].b").isEqualTo("bravo").jsonPath("[2].c") + .isEqualTo("charlie")); + } + @Test void readOperationWithCustomMediaType() { load(CustomMediaTypesEndpointConfiguration.class, (client) -> client.get().uri("/custommediatypes").exchange() @@ -379,6 +389,12 @@ void userInRoleReturnsTrueWhenUserIsInRole() { .expectStatus().isOk().expectBody(String.class).isEqualTo("ACTUATOR: true")); } + @Test + void endpointCanProduceAResponseWithACustomStatus() { + load((context) -> context.register(CustomResponseStatusEndpointConfiguration.class), + (client) -> client.get().uri("/customstatus").exchange().expectStatus().isEqualTo(234)); + } + protected abstract int getPort(T context); protected void validateErrorBody(WebTestClient.BodyContentSpec body, HttpStatus status, String path, @@ -409,8 +425,10 @@ private void load(Consumer contextCustomizer, String endpointPath, BiConsumer consumer) { T applicationContext = this.applicationContextSupplier.get(); contextCustomizer.accept(applicationContext); - applicationContext.getEnvironment().getPropertySources() - .addLast(new MapPropertySource("test", Collections.singletonMap("endpointPath", endpointPath))); + Map properties = new HashMap<>(); + properties.put("endpointPath", endpointPath); + properties.put("server.error.include-message", "always"); + applicationContext.getEnvironment().getPropertySources().addLast(new MapPropertySource("test", properties)); applicationContext.refresh(); try { InetSocketAddress address = new InetSocketAddress(getPort(applicationContext)); @@ -555,6 +573,17 @@ MonoResponseEndpoint testEndpoint(EndpointDelegate endpointDelegate) { } + @Configuration(proxyBeanMethods = false) + @Import(BaseConfiguration.class) + static class FluxResponseEndpointConfiguration { + + @Bean + FluxResponseEndpoint testEndpoint(EndpointDelegate endpointDelegate) { + return new FluxResponseEndpoint(); + } + + } + @Configuration(proxyBeanMethods = false) @Import(BaseConfiguration.class) static class CustomMediaTypesEndpointConfiguration { @@ -621,6 +650,17 @@ UserInRoleEndpoint userInRoleEndpoint() { } + @Configuration(proxyBeanMethods = false) + @Import(BaseConfiguration.class) + static class CustomResponseStatusEndpointConfiguration { + + @Bean + CustomResponseStatusEndpoint customResponseStatusEndpoint() { + return new CustomResponseStatusEndpoint(); + } + + } + @Endpoint(id = "test") static class TestEndpoint { @@ -786,6 +826,17 @@ Mono> operation() { } + @Endpoint(id = "flux") + static class FluxResponseEndpoint { + + @ReadOperation + Flux> operation() { + return Flux.just(Collections.singletonMap("a", "alpha"), Collections.singletonMap("b", "bravo"), + Collections.singletonMap("c", "charlie")); + } + + } + @Endpoint(id = "custommediatypes") static class CustomMediaTypesEndpoint { @@ -847,6 +898,16 @@ String read(SecurityContext securityContext, String role) { } + @Endpoint(id = "customstatus") + static class CustomResponseStatusEndpoint { + + @ReadOperation + WebEndpointResponse read() { + return new WebEndpointResponse<>("Custom status", 234); + } + + } + interface EndpointDelegate { void write(); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/ControllerEndpointDiscovererTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/ControllerEndpointDiscovererTests.java index cf62b73c6550..94021c018c0a 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/ControllerEndpointDiscovererTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/ControllerEndpointDiscovererTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,6 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.endpoint.EndpointId; -import org.springframework.boot.actuate.endpoint.ExposableEndpoint; import org.springframework.boot.actuate.endpoint.annotation.DiscoveredEndpoint; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; @@ -114,7 +113,7 @@ void getEndpointsShouldNotDiscoverRegularEndpoints() { this.contextRunner.withUserConfiguration(WithRegularEndpointConfiguration.class) .run(assertDiscoverer((discoverer) -> { Collection endpoints = discoverer.getEndpoints(); - List ids = endpoints.stream().map(ExposableEndpoint::getEndpointId) + List ids = endpoints.stream().map(ExposableControllerEndpoint::getEndpointId) .collect(Collectors.toList()); assertThat(ids).containsOnly(EndpointId.of("testcontroller"), EndpointId.of("testrestcontroller")); })); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/RequestPredicateFactoryTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/RequestPredicateFactoryTests.java index 265af6319786..17170e19dcf3 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/RequestPredicateFactoryTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/RequestPredicateFactoryTests.java @@ -61,7 +61,7 @@ void getRequestPredicateWhenMatchAllIsNotLastParameterThrowsException() { } @Test - void getRequestPredicateReturnsRedicateWithPath() { + void getRequestPredicateReturnsPredicateWithPath() { DiscoveredOperationMethod operationMethod = getDiscoveredOperationMethod(ValidSelectors.class); WebOperationRequestPredicate requestPredicate = this.factory.getRequestPredicate(this.rootPath, operationMethod); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/ServletEndpointDiscovererTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/ServletEndpointDiscovererTests.java index 7bb6ff9aec36..745465ccae93 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/ServletEndpointDiscovererTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/ServletEndpointDiscovererTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,6 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.endpoint.EndpointId; -import org.springframework.boot.actuate.endpoint.ExposableEndpoint; import org.springframework.boot.actuate.endpoint.annotation.DiscoveredEndpoint; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; @@ -97,7 +96,7 @@ void getEndpointsShouldNotDiscoverRegularEndpoints() { this.contextRunner.withUserConfiguration(WithRegularEndpointConfiguration.class) .run(assertDiscoverer((discoverer) -> { Collection endpoints = discoverer.getEndpoints(); - List ids = endpoints.stream().map(ExposableEndpoint::getEndpointId) + List ids = endpoints.stream().map(ExposableServletEndpoint::getEndpointId) .collect(Collectors.toList()); assertThat(ids).containsOnly(EndpointId.of("testservlet")); })); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/WebEndpointDiscovererTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/WebEndpointDiscovererTests.java index c513128b8e84..552c7a09cb46 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/WebEndpointDiscovererTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/annotation/WebEndpointDiscovererTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -217,7 +217,7 @@ void getEndpointsWhenHasCustomPathShouldReturnCustomPath() { } private void load(Class configuration, Consumer consumer) { - this.load((id) -> null, EndpointId::toString, configuration, consumer); + load((id) -> null, EndpointId::toString, configuration, consumer); } private void load(Function timeToLive, PathMapper endpointPathMapper, Class configuration, diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/ControllerEndpointHandlerMappingIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/ControllerEndpointHandlerMappingIntegrationTests.java index bcbffcee658c..1b7517b6619e 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/ControllerEndpointHandlerMappingIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/ControllerEndpointHandlerMappingIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -97,7 +97,7 @@ private ContextConsumer withWebTestClie private WebTestClient createWebTestClient(int port) { DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory("http://localhost:" + port); uriBuilderFactory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE); - return WebTestClient.bindToServer().uriBuilderFactory(uriBuilderFactory).responseTimeout(Duration.ofMinutes(2)) + return WebTestClient.bindToServer().uriBuilderFactory(uriBuilderFactory).responseTimeout(Duration.ofMinutes(5)) .build(); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/ControllerEndpointHandlerMappingTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/ControllerEndpointHandlerMappingTests.java index 23d0161b2a84..635b58c15d2f 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/ControllerEndpointHandlerMappingTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/ControllerEndpointHandlerMappingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,7 +51,7 @@ class ControllerEndpointHandlerMappingTests { private final StaticApplicationContext context = new StaticApplicationContext(); @Test - void mappingWithNoPrefix() throws Exception { + void mappingWithNoPrefix() { ExposableControllerEndpoint first = firstEndpoint(); ExposableControllerEndpoint second = secondEndpoint(); ControllerEndpointHandlerMapping mapping = createMapping("", first, second); @@ -62,7 +62,7 @@ void mappingWithNoPrefix() throws Exception { } @Test - void mappingWithPrefix() throws Exception { + void mappingWithPrefix() { ExposableControllerEndpoint first = firstEndpoint(); ExposableControllerEndpoint second = secondEndpoint(); ControllerEndpointHandlerMapping mapping = createMapping("actuator", first, second); @@ -75,7 +75,7 @@ void mappingWithPrefix() throws Exception { } @Test - void mappingWithNoPath() throws Exception { + void mappingWithNoPath() { ExposableControllerEndpoint pathless = pathlessEndpoint(); ControllerEndpointHandlerMapping mapping = createMapping("actuator", pathless); assertThat(getHandler(mapping, HttpMethod.GET, "/actuator/pathless")) @@ -85,7 +85,7 @@ void mappingWithNoPath() throws Exception { } @Test - void mappingNarrowedToMethod() throws Exception { + void mappingNarrowedToMethod() { ExposableControllerEndpoint first = firstEndpoint(); ControllerEndpointHandlerMapping mapping = createMapping("actuator", first); assertThatExceptionOfType(MethodNotAllowedException.class) diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointIntegrationTests.java index 40bae1280446..b6aaf05a7072 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/reactive/WebFluxEndpointIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -141,7 +141,7 @@ static class AuthenticatedConfiguration { @Bean WebFilter webFilter() { - return (exchange, chain) -> chain.filter(exchange).subscriberContext( + return (exchange, chain) -> chain.filter(exchange).contextWrite( ReactiveSecurityContextHolder.withAuthentication(new UsernamePasswordAuthenticationToken("Alice", "secret", Arrays.asList(new SimpleGrantedAuthority("ROLE_ACTUATOR"))))); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/ControllerEndpointHandlerMappingIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/ControllerEndpointHandlerMappingIntegrationTests.java index ef42dc9783e7..bf8b98b736d9 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/ControllerEndpointHandlerMappingIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/ControllerEndpointHandlerMappingIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -95,7 +95,7 @@ private ContextConsumer withWebTestClient(Consu private WebTestClient createWebTestClient(int port) { DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory("http://localhost:" + port); uriBuilderFactory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE); - return WebTestClient.bindToServer().uriBuilderFactory(uriBuilderFactory).responseTimeout(Duration.ofMinutes(2)) + return WebTestClient.bindToServer().uriBuilderFactory(uriBuilderFactory).responseTimeout(Duration.ofMinutes(5)) .build(); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/ControllerEndpointHandlerMappingTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/ControllerEndpointHandlerMappingTests.java index f0fae4c17a61..b9b1b09ec546 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/ControllerEndpointHandlerMappingTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/ControllerEndpointHandlerMappingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -73,7 +73,7 @@ void mappingWithPrefix() throws Exception { } @Test - void mappingNarrowedToMethod() throws Exception { + void mappingNarrowedToMethod() { ExposableControllerEndpoint first = firstEndpoint(); ControllerEndpointHandlerMapping mapping = createMapping("actuator", first); assertThatExceptionOfType(HttpRequestMethodNotSupportedException.class) diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/DefaultWebMvcTagsProviderTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/DefaultWebMvcTagsProviderTests.java new file mode 100644 index 000000000000..fd5de979eeff --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/DefaultWebMvcTagsProviderTests.java @@ -0,0 +1,99 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.endpoint.web.servlet; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import io.micrometer.core.instrument.Tag; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.metrics.web.servlet.DefaultWebMvcTagsProvider; +import org.springframework.boot.actuate.metrics.web.servlet.WebMvcTagsContributor; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DefaultWebMvcTagsProvider}. + * + * @author Andy Wilkinson + */ +class DefaultWebMvcTagsProviderTests { + + @Test + void whenTagsAreProvidedThenDefaultTagsArePresent() { + Map tags = asMap(new DefaultWebMvcTagsProvider().getTags(null, null, null, null)); + assertThat(tags).containsOnlyKeys("exception", "method", "outcome", "status", "uri"); + } + + @Test + void givenSomeContributorsWhenTagsAreProvidedThenDefaultTagsAndContributedTagsArePresent() { + Map tags = asMap( + new DefaultWebMvcTagsProvider(Arrays.asList(new TestWebMvcTagsContributor("alpha"), + new TestWebMvcTagsContributor("bravo", "charlie"))).getTags(null, null, null, null)); + assertThat(tags).containsOnlyKeys("exception", "method", "outcome", "status", "uri", "alpha", "bravo", + "charlie"); + } + + @Test + void whenLongRequestTagsAreProvidedThenDefaultTagsArePresent() { + Map tags = asMap(new DefaultWebMvcTagsProvider().getLongRequestTags(null, null)); + assertThat(tags).containsOnlyKeys("method", "uri"); + } + + @Test + void givenSomeContributorsWhenLongRequestTagsAreProvidedThenDefaultTagsAndContributedTagsArePresent() { + Map tags = asMap( + new DefaultWebMvcTagsProvider(Arrays.asList(new TestWebMvcTagsContributor("alpha"), + new TestWebMvcTagsContributor("bravo", "charlie"))).getLongRequestTags(null, null)); + assertThat(tags).containsOnlyKeys("method", "uri", "alpha", "bravo", "charlie"); + } + + private Map asMap(Iterable tags) { + return StreamSupport.stream(tags.spliterator(), false) + .collect(Collectors.toMap(Tag::getKey, Function.identity())); + } + + private static final class TestWebMvcTagsContributor implements WebMvcTagsContributor { + + private final List tagNames; + + private TestWebMvcTagsContributor(String... tagNames) { + this.tagNames = Arrays.asList(tagNames); + } + + @Override + public Iterable getTags(HttpServletRequest request, HttpServletResponse response, Object handler, + Throwable exception) { + return this.tagNames.stream().map((name) -> Tag.of(name, "value")).collect(Collectors.toList()); + } + + @Override + public Iterable getLongRequestTags(HttpServletRequest request, Object handler) { + return this.tagNames.stream().map((name) -> Tag.of(name, "value")).collect(Collectors.toList()); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/MvcWebEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/MvcWebEndpointIntegrationTests.java index 9ff0ed2fcb18..1c4bda2425b7 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/MvcWebEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/MvcWebEndpointIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -121,6 +121,13 @@ private RequestMatchResult getMatchResult(String servletPath) { context.register(TestEndpointConfiguration.class); context.refresh(); WebMvcEndpointHandlerMapping bean = context.getBean(WebMvcEndpointHandlerMapping.class); + try { + // Trigger initLookupPath + bean.getHandler(request); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } return bean.match(request, "/spring"); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/WebMvcTagsTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/WebMvcTagsTests.java index 55e27a6b913d..36ba42db687b 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/WebMvcTagsTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/WebMvcTagsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,12 +52,34 @@ void uriTagIsDataRestsEffectiveRepositoryLookupPathWhenAvailable() { @Test void uriTagValueIsBestMatchingPatternWhenAvailable() { - this.request.setAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, "/spring"); + this.request.setAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, "/spring/"); this.response.setStatus(301); Tag tag = WebMvcTags.uri(this.request, this.response); + assertThat(tag.getValue()).isEqualTo("/spring/"); + } + + @Test + void uriTagValueIsRootWhenBestMatchingPatternIsEmpty() { + this.request.setAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, ""); + this.response.setStatus(301); + Tag tag = WebMvcTags.uri(this.request, this.response); + assertThat(tag.getValue()).isEqualTo("root"); + } + + @Test + void uriTagValueWithBestMatchingPatternAndIgnoreTrailingSlashRemoveTrailingSlash() { + this.request.setAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, "/spring/"); + Tag tag = WebMvcTags.uri(this.request, this.response, true); assertThat(tag.getValue()).isEqualTo("/spring"); } + @Test + void uriTagValueWithBestMatchingPatternAndIgnoreTrailingSlashKeepSingleSlash() { + this.request.setAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, "/"); + Tag tag = WebMvcTags.uri(this.request, this.response, true); + assertThat(tag.getValue()).isEqualTo("/"); + } + @Test void uriTagValueIsRootWhenRequestHasNoPatternOrPathInfo() { assertThat(WebMvcTags.uri(this.request, null).getValue()).isEqualTo("root"); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebEndpointTestInvocationContextProvider.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebEndpointTestInvocationContextProvider.java index 41bf39c07faf..88fe50f32bcd 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebEndpointTestInvocationContextProvider.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebEndpointTestInvocationContextProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,6 @@ import org.junit.jupiter.api.extension.Extension; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ParameterContext; -import org.junit.jupiter.api.extension.ParameterResolutionException; import org.junit.jupiter.api.extension.ParameterResolver; import org.junit.jupiter.api.extension.TestTemplateInvocationContext; import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider; @@ -66,6 +65,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; +import org.springframework.http.HttpMethod; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.util.ClassUtils; @@ -126,7 +126,7 @@ private static ConfigurableApplicationContext createWebFluxContext(List static class WebEndpointsInvocationContext implements TestTemplateInvocationContext, BeforeEachCallback, AfterEachCallback, ParameterResolver { - private static final Duration TIMEOUT = Duration.ofMinutes(6); + private static final Duration TIMEOUT = Duration.ofMinutes(5); private final String name; @@ -160,15 +160,13 @@ public void afterEach(ExtensionContext context) throws Exception { } @Override - public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) - throws ParameterResolutionException { + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { Class type = parameterContext.getParameter().getType(); return type.equals(WebTestClient.class) || type.isAssignableFrom(ConfigurableApplicationContext.class); } @Override - public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) - throws ParameterResolutionException { + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { Class type = parameterContext.getParameter().getType(); if (type.equals(WebTestClient.class)) { return createWebTestClient(); @@ -192,7 +190,13 @@ private WebTestClient createWebTestClient() { DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory( "http://localhost:" + determinePort()); uriBuilderFactory.setEncodingMode(EncodingMode.NONE); - return WebTestClient.bindToServer().uriBuilderFactory(uriBuilderFactory).responseTimeout(TIMEOUT).build(); + return WebTestClient.bindToServer().uriBuilderFactory(uriBuilderFactory).responseTimeout(TIMEOUT) + .codecs((codecs) -> codecs.defaultCodecs().maxInMemorySize(-1)).filter((request, next) -> { + if (HttpMethod.GET == request.method()) { + return next.exchange(request).retry(10); + } + return next.exchange(request); + }).build(); } private int determinePort() { diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/env/EnvironmentEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/env/EnvironmentEndpointTests.java index ecaa47813cb7..8d3cfd17ced9 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/env/EnvironmentEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/env/EnvironmentEndpointTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,10 @@ package org.springframework.boot.actuate.env; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; @@ -29,6 +33,8 @@ import org.springframework.boot.actuate.env.EnvironmentEndpoint.PropertySourceEntryDescriptor; import org.springframework.boot.actuate.env.EnvironmentEndpoint.PropertyValueDescriptor; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.origin.Origin; +import org.springframework.boot.origin.OriginLookup; import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -37,6 +43,8 @@ import org.springframework.core.env.Environment; import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.InputStreamSource; +import org.springframework.mock.env.MockPropertySource; import static org.assertj.core.api.Assertions.assertThat; @@ -50,6 +58,8 @@ * @author Madhura Bhave * @author Andy Wilkinson * @author HaiTao Zhang + * @author Chris Bono + * @author Scott Frederick */ class EnvironmentEndpointTests { @@ -190,15 +200,39 @@ void propertyWithSensitivePlaceholderNotResolved() { } @Test - @SuppressWarnings("unchecked") - void propertyWithTypeOtherThanStringShouldNotFail() { + void propertyWithComplexTypeShouldNotFail() { ConfigurableEnvironment environment = emptyEnvironment(); environment.getPropertySources() .addFirst(singleKeyPropertySource("test", "foo", Collections.singletonMap("bar", "baz"))); EnvironmentDescriptor descriptor = new EnvironmentEndpoint(environment).environment(null); - Map foo = (Map) propertySources(descriptor).get("test").getProperties() - .get("foo").getValue(); - assertThat(foo.get("bar")).isEqualTo("baz"); + String value = (String) propertySources(descriptor).get("test").getProperties().get("foo").getValue(); + assertThat(value).isEqualTo("Complex property type java.util.Collections$SingletonMap"); + } + + @Test + void propertyWithPrimitiveOrWrapperTypeIsHandledCorrectly() { + ConfigurableEnvironment environment = emptyEnvironment(); + Map map = new LinkedHashMap<>(); + map.put("char", 'a'); + map.put("integer", 100); + map.put("boolean", true); + map.put("biginteger", BigInteger.valueOf(200)); + environment.getPropertySources().addFirst(new MapPropertySource("test", map)); + EnvironmentDescriptor descriptor = new EnvironmentEndpoint(environment).environment(null); + Map properties = propertySources(descriptor).get("test").getProperties(); + assertThat(properties.get("char").getValue()).isEqualTo('a'); + assertThat(properties.get("integer").getValue()).isEqualTo(100); + assertThat(properties.get("boolean").getValue()).isEqualTo(true); + assertThat(properties.get("biginteger").getValue()).isEqualTo(BigInteger.valueOf(200)); + } + + @Test + void propertyWithCharSequenceTypeIsConvertedToString() { + ConfigurableEnvironment environment = emptyEnvironment(); + environment.getPropertySources().addFirst(singleKeyPropertySource("test", "foo", new CharSequenceProperty())); + EnvironmentDescriptor descriptor = new EnvironmentEndpoint(environment).environment(null); + String value = (String) propertySources(descriptor).get("test").getProperties().get("foo").getValue(); + assertThat(value).isEqualTo("test value"); } @Test @@ -221,6 +255,18 @@ void propertyEntry() { }); } + @Test + void originAndOriginParents() { + StandardEnvironment environment = new StandardEnvironment(); + OriginParentMockPropertySource propertySource = new OriginParentMockPropertySource(); + propertySource.setProperty("name", "test"); + environment.getPropertySources().addFirst(propertySource); + EnvironmentEntryDescriptor descriptor = new EnvironmentEndpoint(environment).environmentEntry("name"); + PropertySourceEntryDescriptor entryDescriptor = propertySources(descriptor).get("mockProperties"); + assertThat(entryDescriptor.getProperty().getOrigin()).isEqualTo("name"); + assertThat(entryDescriptor.getProperty().getOriginParents()).containsExactly("spring", "boot"); + } + @Test void propertyEntryNotFound() { ConfigurableEnvironment environment = emptyEnvironment(); @@ -246,13 +292,25 @@ void multipleSourcesWithSameProperty() { } @Test - void uriPropertryWithSensitiveInfo() { + void uriPropertyWithSensitiveInfo() { ConfigurableEnvironment environment = new StandardEnvironment(); TestPropertyValues.of("sensitive.uri=http://user:password@localhost:8080").applyTo(environment); EnvironmentEntryDescriptor descriptor = new EnvironmentEndpoint(environment).environmentEntry("sensitive.uri"); assertThat(descriptor.getProperty().getValue()).isEqualTo("http://user:******@localhost:8080"); } + @Test + void addressesPropertyWithMultipleEntriesEachWithSensitiveInfo() { + ConfigurableEnvironment environment = new StandardEnvironment(); + TestPropertyValues + .of("sensitive.addresses=http://user:password@localhost:8080,http://user2:password2@localhost:8082") + .applyTo(environment); + EnvironmentEntryDescriptor descriptor = new EnvironmentEndpoint(environment) + .environmentEntry("sensitive.addresses"); + assertThat(descriptor.getProperty().getValue()) + .isEqualTo("http://user:******@localhost:8080,http://user2:******@localhost:8082"); + } + private static ConfigurableEnvironment emptyEnvironment() { StandardEnvironment environment = new StandardEnvironment(); environment.getPropertySources().remove(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME); @@ -289,6 +347,38 @@ private void assertPropertySourceEntryDescriptor(PropertySourceEntryDescriptor a } + static class OriginParentMockPropertySource extends MockPropertySource implements OriginLookup { + + @Override + public Origin getOrigin(String key) { + return new MockOrigin(key, new MockOrigin("spring", new MockOrigin("boot", null))); + } + + } + + static class MockOrigin implements Origin { + + private final String value; + + private final MockOrigin parent; + + MockOrigin(String value, MockOrigin parent) { + this.value = value; + this.parent = parent; + } + + @Override + public Origin getParent() { + return this.parent; + } + + @Override + public String toString() { + return this.value; + } + + } + @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties static class Config { @@ -300,4 +390,35 @@ EnvironmentEndpoint environmentEndpoint(Environment environment) { } + public static class CharSequenceProperty implements CharSequence, InputStreamSource { + + private final String value = "test value"; + + @Override + public int length() { + return this.value.length(); + } + + @Override + public char charAt(int index) { + return this.value.charAt(index); + } + + @Override + public CharSequence subSequence(int start, int end) { + return this.value.subSequence(start, end); + } + + @Override + public String toString() { + return this.value; + } + + @Override + public InputStream getInputStream() throws IOException { + return new ByteArrayInputStream(this.value.getBytes()); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/env/EnvironmentEndpointWebIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/env/EnvironmentEndpointWebIntegrationTests.java index ee9bbc3718ef..f46d8d5569ca 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/env/EnvironmentEndpointWebIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/env/EnvironmentEndpointWebIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -88,11 +88,8 @@ void nestedPathWithSensitivePlaceholderShouldSanitize() { } @WebEndpointTest - void nestedPathForUnknownKeyShouldReturn404AndBody() { - this.client.get().uri("/actuator/env/this.does.not.exist").exchange().expectStatus().isNotFound().expectBody() - .jsonPath("property").doesNotExist().jsonPath("propertySources[?(@.name=='test')]").exists() - .jsonPath("propertySources[?(@.name=='systemProperties')]").exists() - .jsonPath("propertySources[?(@.name=='systemEnvironment')]").exists(); + void nestedPathForUnknownKeyShouldReturn404() { + this.client.get().uri("/actuator/env/this.does.not.exist").exchange().expectStatus().isNotFound(); } @WebEndpointTest diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/hazelcast/Hazelcast3HazelcastHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/hazelcast/Hazelcast3HazelcastHealthIndicatorTests.java new file mode 100644 index 000000000000..6c9e90274219 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/hazelcast/Hazelcast3HazelcastHealthIndicatorTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.hazelcast; + +import com.hazelcast.core.HazelcastException; +import com.hazelcast.core.HazelcastInstance; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.Status; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.testsupport.classpath.ClassPathExclusions; +import org.springframework.boot.testsupport.classpath.ClassPathOverrides; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link HazelcastHealthIndicator} with Hazelcast 3. + * + * @author Dmytro Nosan + * @author Stephane Nicoll + */ +@ClassPathExclusions("hazelcast*.jar") +@ClassPathOverrides("com.hazelcast:hazelcast:3.12.8") +class Hazelcast3HazelcastHealthIndicatorTests { + + @Test + void hazelcastUp() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(HazelcastAutoConfiguration.class)) + .withPropertyValues("spring.hazelcast.config=hazelcast-3.xml").run((context) -> { + HazelcastInstance hazelcast = context.getBean(HazelcastInstance.class); + Health health = new HazelcastHealthIndicator(hazelcast).health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertThat(health.getDetails()).containsOnlyKeys("name", "uuid").containsEntry("name", + "actuator-hazelcast-3"); + assertThat(health.getDetails().get("uuid")).asString().isNotEmpty(); + }); + } + + @Test + void hazelcastDown() { + HazelcastInstance hazelcast = mock(HazelcastInstance.class); + given(hazelcast.executeTransaction(any())).willThrow(new HazelcastException()); + Health health = new HazelcastHealthIndicator(hazelcast).health(); + assertThat(health.getStatus()).isEqualTo(Status.DOWN); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/hazelcast/HazelcastHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/hazelcast/HazelcastHealthIndicatorTests.java index 1340e6058f50..053db98a3a9d 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/hazelcast/HazelcastHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/hazelcast/HazelcastHealthIndicatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,18 +16,19 @@ package org.springframework.boot.actuate.hazelcast; -import com.hazelcast.core.Endpoint; import com.hazelcast.core.HazelcastException; import com.hazelcast.core.HazelcastInstance; -import com.hazelcast.transaction.TransactionalTask; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.Status; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.when; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; /** @@ -38,28 +39,24 @@ */ class HazelcastHealthIndicatorTests { - private final HazelcastInstance hazelcast = mock(HazelcastInstance.class); - @Test void hazelcastUp() { - Endpoint endpoint = mock(Endpoint.class); - when(this.hazelcast.getName()).thenReturn("hz0-instance"); - when(this.hazelcast.getLocalEndpoint()).thenReturn(endpoint); - when(endpoint.getUuid()).thenReturn("7581bb2f-879f-413f-b574-0071d7519eb0"); - when(this.hazelcast.executeTransaction(any())).thenAnswer((invocation) -> { - TransactionalTask task = invocation.getArgument(0); - return task.execute(null); - }); - Health health = new HazelcastHealthIndicator(this.hazelcast).health(); - assertThat(health.getStatus()).isEqualTo(Status.UP); - assertThat(health.getDetails()).containsOnlyKeys("name", "uuid").containsEntry("name", "hz0-instance") - .containsEntry("uuid", "7581bb2f-879f-413f-b574-0071d7519eb0"); + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(HazelcastAutoConfiguration.class)) + .withPropertyValues("spring.hazelcast.config=hazelcast.xml").run((context) -> { + HazelcastInstance hazelcast = context.getBean(HazelcastInstance.class); + Health health = new HazelcastHealthIndicator(hazelcast).health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertThat(health.getDetails()).containsOnlyKeys("name", "uuid").containsEntry("name", + "actuator-hazelcast"); + assertThat(health.getDetails().get("uuid")).asString().isNotEmpty(); + }); } @Test void hazelcastDown() { - when(this.hazelcast.executeTransaction(any())).thenThrow(new HazelcastException()); - Health health = new HazelcastHealthIndicator(this.hazelcast).health(); + HazelcastInstance hazelcast = mock(HazelcastInstance.class); + given(hazelcast.executeTransaction(any())).willThrow(new HazelcastException()); + Health health = new HazelcastHealthIndicator(hazelcast).health(); assertThat(health.getStatus()).isEqualTo(Status.DOWN); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ApplicationHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ApplicationHealthIndicatorTests.java deleted file mode 100644 index eb257665e650..000000000000 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ApplicationHealthIndicatorTests.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.health; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link ApplicationHealthIndicator}. - * - * @author Phillip Webb - */ -@Deprecated -class ApplicationHealthIndicatorTests { - - @Test - void indicatesUp() { - ApplicationHealthIndicator healthIndicator = new ApplicationHealthIndicator(); - assertThat(healthIndicator.health().getStatus()).isEqualTo(Status.UP); - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/CompositeHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/CompositeHealthIndicatorTests.java deleted file mode 100644 index 9d9ea8572ea1..000000000000 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/CompositeHealthIndicatorTests.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.health; - -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; - -/** - * Tests for {@link CompositeHealthIndicator} - * - * @author Tyler J. Frederick - * @author Phillip Webb - * @author Christian Dupuis - */ -@Deprecated -class CompositeHealthIndicatorTests { - - private HealthAggregator healthAggregator; - - @Mock - private HealthIndicator one; - - @Mock - private HealthIndicator two; - - @BeforeEach - void setup() { - MockitoAnnotations.initMocks(this); - given(this.one.health()).willReturn(new Health.Builder().unknown().withDetail("1", "1").build()); - given(this.two.health()).willReturn(new Health.Builder().unknown().withDetail("2", "2").build()); - - this.healthAggregator = new OrderedHealthAggregator(); - } - - @Test - void createWithIndicators() { - Map indicators = new HashMap<>(); - indicators.put("one", this.one); - indicators.put("two", this.two); - CompositeHealthIndicator composite = new CompositeHealthIndicator(this.healthAggregator, indicators); - Health result = composite.health(); - assertThat(result.getDetails()).hasSize(2); - assertThat(result.getDetails()).containsEntry("one", - new Health.Builder().unknown().withDetail("1", "1").build()); - assertThat(result.getDetails()).containsEntry("two", - new Health.Builder().unknown().withDetail("2", "2").build()); - } - - @Test - void testSerialization() throws Exception { - Map indicators = new LinkedHashMap<>(); - indicators.put("db1", this.one); - indicators.put("db2", this.two); - CompositeHealthIndicator innerComposite = new CompositeHealthIndicator(this.healthAggregator, indicators); - CompositeHealthIndicator composite = new CompositeHealthIndicator(this.healthAggregator, - Collections.singletonMap("db", innerComposite)); - Health result = composite.health(); - ObjectMapper mapper = new ObjectMapper(); - assertThat(mapper.writeValueAsString(result)) - .isEqualTo("{\"status\":\"UNKNOWN\",\"details\":{\"db\":{\"status\":\"UNKNOWN\"" - + ",\"details\":{\"db1\":{\"status\":\"UNKNOWN\",\"details\"" - + ":{\"1\":\"1\"}},\"db2\":{\"status\":\"UNKNOWN\",\"details\":{\"2\":\"2\"}}}}}}"); - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/CompositeHealthTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/CompositeHealthTests.java index ec4e1ee3bea9..236d123d8a9d 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/CompositeHealthTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/CompositeHealthTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,10 +20,11 @@ import java.util.LinkedHashMap; import java.util.Map; +import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; -import org.springframework.boot.actuate.endpoint.http.ApiVersion; +import org.springframework.boot.actuate.endpoint.ApiVersion; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -64,7 +65,7 @@ void serializeV3WithJacksonReturnsValidJson() throws Exception { CompositeHealth health = new CompositeHealth(ApiVersion.V3, Status.UP, components); ObjectMapper mapper = new ObjectMapper(); String json = mapper.writeValueAsString(health); - assertThat(json).isEqualTo("{\"status\":\"UP\",\"components\":{" + "\"db1\":{\"status\":\"UP\"}," + assertThat(json).isEqualTo("{\"status\":\"UP\",\"components\":{\"db1\":{\"status\":\"UP\"}," + "\"db2\":{\"status\":\"DOWN\",\"details\":{\"a\":\"b\"}}}}"); } @@ -76,7 +77,20 @@ void serializeV2WithJacksonReturnsValidJson() throws Exception { CompositeHealth health = new CompositeHealth(ApiVersion.V2, Status.UP, components); ObjectMapper mapper = new ObjectMapper(); String json = mapper.writeValueAsString(health); - assertThat(json).isEqualTo("{\"status\":\"UP\",\"details\":{" + "\"db1\":{\"status\":\"UP\"}," + assertThat(json).isEqualTo("{\"status\":\"UP\",\"details\":{\"db1\":{\"status\":\"UP\"}," + + "\"db2\":{\"status\":\"DOWN\",\"details\":{\"a\":\"b\"}}}}"); + } + + @Test // gh-26797 + void serializeV2WithJacksonAndDisabledCanOverrideAccessModifiersReturnsValidJson() throws Exception { + Map components = new LinkedHashMap<>(); + components.put("db1", Health.up().build()); + components.put("db2", Health.down().withDetail("a", "b").build()); + CompositeHealth health = new CompositeHealth(ApiVersion.V2, Status.UP, components); + ObjectMapper mapper = new ObjectMapper(); + mapper.disable(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS); + String json = mapper.writeValueAsString(health); + assertThat(json).isEqualTo("{\"status\":\"UP\",\"details\":{\"db1\":{\"status\":\"UP\"}," + "\"db2\":{\"status\":\"DOWN\",\"details\":{\"a\":\"b\"}}}}"); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/CompositeReactiveHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/CompositeReactiveHealthIndicatorTests.java deleted file mode 100644 index f8818b560f20..000000000000 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/CompositeReactiveHealthIndicatorTests.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.health; - -import java.time.Duration; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link CompositeReactiveHealthIndicator}. - * - * @author Stephane Nicoll - */ -@Deprecated -class CompositeReactiveHealthIndicatorTests { - - private static final Health UNKNOWN_HEALTH = Health.unknown().withDetail("detail", "value").build(); - - private static final Health HEALTHY = Health.up().build(); - - private OrderedHealthAggregator healthAggregator = new OrderedHealthAggregator(); - - @Test - void singleIndicator() { - CompositeReactiveHealthIndicator indicator = new CompositeReactiveHealthIndicator(this.healthAggregator, - new DefaultReactiveHealthIndicatorRegistry(Collections.singletonMap("test", () -> Mono.just(HEALTHY)))); - StepVerifier.create(indicator.health()).consumeNextWith((h) -> { - assertThat(h.getStatus()).isEqualTo(Status.UP); - assertThat(h.getDetails()).containsOnlyKeys("test"); - assertThat(h.getDetails().get("test")).isEqualTo(HEALTHY); - }).verifyComplete(); - } - - @Test - void longHealth() { - Map indicators = new HashMap<>(); - for (int i = 0; i < 50; i++) { - indicators.put("test" + i, new TimeoutHealth(10000, Status.UP)); - } - CompositeReactiveHealthIndicator indicator = new CompositeReactiveHealthIndicator(this.healthAggregator, - new DefaultReactiveHealthIndicatorRegistry(indicators)); - StepVerifier.withVirtualTime(indicator::health).expectSubscription().thenAwait(Duration.ofMillis(10000)) - .consumeNextWith((h) -> { - assertThat(h.getStatus()).isEqualTo(Status.UP); - assertThat(h.getDetails()).hasSize(50); - }).verifyComplete(); - - } - - @Test - void timeoutReachedUsesFallback() { - Map indicators = new HashMap<>(); - indicators.put("slow", new TimeoutHealth(10000, Status.UP)); - indicators.put("fast", new TimeoutHealth(10, Status.UP)); - CompositeReactiveHealthIndicator indicator = new CompositeReactiveHealthIndicator(this.healthAggregator, - new DefaultReactiveHealthIndicatorRegistry(indicators)).timeoutStrategy(100, UNKNOWN_HEALTH); - StepVerifier.create(indicator.health()).consumeNextWith((h) -> { - assertThat(h.getStatus()).isEqualTo(Status.UP); - assertThat(h.getDetails()).containsOnlyKeys("slow", "fast"); - assertThat(h.getDetails().get("slow")).isEqualTo(UNKNOWN_HEALTH); - assertThat(h.getDetails().get("fast")).isEqualTo(HEALTHY); - }).verifyComplete(); - } - - @Test - void timeoutNotReached() { - Map indicators = new HashMap<>(); - indicators.put("slow", new TimeoutHealth(10000, Status.UP)); - indicators.put("fast", new TimeoutHealth(10, Status.UP)); - CompositeReactiveHealthIndicator indicator = new CompositeReactiveHealthIndicator(this.healthAggregator, - new DefaultReactiveHealthIndicatorRegistry(indicators)).timeoutStrategy(20000, null); - StepVerifier.withVirtualTime(indicator::health).expectSubscription().thenAwait(Duration.ofMillis(10000)) - .consumeNextWith((h) -> { - assertThat(h.getStatus()).isEqualTo(Status.UP); - assertThat(h.getDetails()).containsOnlyKeys("slow", "fast"); - assertThat(h.getDetails().get("slow")).isEqualTo(HEALTHY); - assertThat(h.getDetails().get("fast")).isEqualTo(HEALTHY); - }).verifyComplete(); - } - - static class TimeoutHealth implements ReactiveHealthIndicator { - - private final long timeout; - - private final Status status; - - TimeoutHealth(long timeout, Status status) { - this.timeout = timeout; - this.status = status; - } - - @Override - public Mono health() { - return Mono.delay(Duration.ofMillis(this.timeout)).map((l) -> Health.status(this.status).build()); - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/DefaultContributorRegistryTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/DefaultContributorRegistryTests.java index 512c2bf2c839..00e1e6b1959d 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/DefaultContributorRegistryTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/DefaultContributorRegistryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -90,7 +90,7 @@ void registerContributorWhenContributorIsNullThrowsException() { } @Test - void registerContributorRegisteresContributors() { + void registerContributorRegistersContributors() { this.registry.registerContributor("one", this.one); this.registry.registerContributor("two", this.two); assertThat(this.registry).hasSize(2); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/DefaultHealthIndicatorRegistryTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/DefaultHealthIndicatorRegistryTests.java deleted file mode 100644 index 53a68f89126f..000000000000 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/DefaultHealthIndicatorRegistryTests.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.health; - -import java.util.Map; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -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.BDDMockito.given; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link DefaultHealthIndicatorRegistry}. - * - * @author Vedran Pavic - * @author Stephane Nicoll - */ -@Deprecated -class DefaultHealthIndicatorRegistryTests { - - private HealthIndicator one = mock(HealthIndicator.class); - - private HealthIndicator two = mock(HealthIndicator.class); - - private DefaultHealthIndicatorRegistry registry; - - @BeforeEach - void setUp() { - given(this.one.health()).willReturn(new Health.Builder().unknown().withDetail("1", "1").build()); - given(this.two.health()).willReturn(new Health.Builder().unknown().withDetail("2", "2").build()); - this.registry = new DefaultHealthIndicatorRegistry(); - } - - @Test - void register() { - this.registry.register("one", this.one); - this.registry.register("two", this.two); - assertThat(this.registry.getAll()).hasSize(2); - assertThat(this.registry.get("one")).isSameAs(this.one); - assertThat(this.registry.get("two")).isSameAs(this.two); - } - - @Test - void registerAlreadyUsedName() { - this.registry.register("one", this.one); - assertThatIllegalStateException().isThrownBy(() -> this.registry.register("one", this.two)) - .withMessageContaining("HealthIndicator with name 'one' already registered"); - } - - @Test - void unregister() { - this.registry.register("one", this.one); - this.registry.register("two", this.two); - assertThat(this.registry.getAll()).hasSize(2); - HealthIndicator two = this.registry.unregister("two"); - assertThat(two).isSameAs(this.two); - assertThat(this.registry.getAll()).hasSize(1); - } - - @Test - void unregisterUnknown() { - this.registry.register("one", this.one); - assertThat(this.registry.getAll()).hasSize(1); - HealthIndicator two = this.registry.unregister("two"); - assertThat(two).isNull(); - assertThat(this.registry.getAll()).hasSize(1); - } - - @Test - void getAllIsASnapshot() { - this.registry.register("one", this.one); - Map snapshot = this.registry.getAll(); - assertThat(snapshot).containsOnlyKeys("one"); - this.registry.register("two", this.two); - assertThat(snapshot).containsOnlyKeys("one"); - } - - @Test - void getAllIsImmutable() { - this.registry.register("one", this.one); - Map snapshot = this.registry.getAll(); - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(snapshot::clear); - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/DefaultReactiveHealthIndicatorRegistryTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/DefaultReactiveHealthIndicatorRegistryTests.java deleted file mode 100644 index 117d8ffc6f82..000000000000 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/DefaultReactiveHealthIndicatorRegistryTests.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.health; - -import java.util.Map; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; - -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.BDDMockito.given; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link DefaultReactiveHealthIndicatorRegistry}. - * - * @author Vedran Pavic - * @author Stephane Nicoll - */ -@Deprecated -class DefaultReactiveHealthIndicatorRegistryTests { - - private ReactiveHealthIndicator one = mock(ReactiveHealthIndicator.class); - - private ReactiveHealthIndicator two = mock(ReactiveHealthIndicator.class); - - private DefaultReactiveHealthIndicatorRegistry registry; - - @BeforeEach - void setUp() { - given(this.one.health()).willReturn(Mono.just(new Health.Builder().unknown().withDetail("1", "1").build())); - given(this.two.health()).willReturn(Mono.just(new Health.Builder().unknown().withDetail("2", "2").build())); - this.registry = new DefaultReactiveHealthIndicatorRegistry(); - } - - @Test - void register() { - this.registry.register("one", this.one); - this.registry.register("two", this.two); - assertThat(this.registry.getAll()).hasSize(2); - assertThat(this.registry.get("one")).isSameAs(this.one); - assertThat(this.registry.get("two")).isSameAs(this.two); - } - - @Test - void registerAlreadyUsedName() { - this.registry.register("one", this.one); - assertThatIllegalStateException().isThrownBy(() -> this.registry.register("one", this.two)) - .withMessageContaining("HealthIndicator with name 'one' already registered"); - } - - @Test - void unregister() { - this.registry.register("one", this.one); - this.registry.register("two", this.two); - assertThat(this.registry.getAll()).hasSize(2); - ReactiveHealthIndicator two = this.registry.unregister("two"); - assertThat(two).isSameAs(this.two); - assertThat(this.registry.getAll()).hasSize(1); - } - - @Test - void unregisterUnknown() { - this.registry.register("one", this.one); - assertThat(this.registry.getAll()).hasSize(1); - ReactiveHealthIndicator two = this.registry.unregister("two"); - assertThat(two).isNull(); - assertThat(this.registry.getAll()).hasSize(1); - } - - @Test - void getAllIsASnapshot() { - this.registry.register("one", this.one); - Map snapshot = this.registry.getAll(); - assertThat(snapshot).containsOnlyKeys("one"); - this.registry.register("two", this.two); - assertThat(snapshot).containsOnlyKeys("one"); - } - - @Test - void getAllIsImmutable() { - this.registry.register("one", this.one); - Map snapshot = this.registry.getAll(); - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(snapshot::clear); - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointSupportTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointSupportTests.java index 2f65c871db2f..f60de783bc12 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointSupportTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointSupportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,12 +20,10 @@ import java.util.LinkedHashMap; import java.util.Map; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.MockitoAnnotations; +import org.springframework.boot.actuate.endpoint.ApiVersion; import org.springframework.boot.actuate.endpoint.SecurityContext; -import org.springframework.boot.actuate.endpoint.http.ApiVersion; import org.springframework.boot.actuate.health.HealthEndpointSupport.HealthResult; import static org.assertj.core.api.Assertions.assertThat; @@ -38,6 +36,7 @@ * @param the contributor type * @param the contributed health component type * @author Phillip Webb + * @author Madhura Bhave */ abstract class HealthEndpointSupportTests, C, T> { @@ -58,11 +57,6 @@ abstract class HealthEndpointSupportTests, C, T this.registry = createRegistry(); } - @BeforeEach - void setup() { - MockitoAnnotations.initMocks(this); - } - @Test void createWhenRegistryIsNullThrowsException() { assertThatIllegalArgumentException().isThrownBy(() -> create(null, this.groups)) @@ -215,6 +209,20 @@ void getHealthWithEmptyCompositeReturnsNullResult() { // gh-18687 assertThat(result).isNull(); } + @Test + void getHealthWhenGroupContainsCompositeContributorReturnsHealth() { + C contributor = createContributor(this.up); + C compositeContributor = createCompositeContributor(Collections.singletonMap("spring", contributor)); + this.registry.registerContributor("test", compositeContributor); + TestHealthEndpointGroup testGroup = new TestHealthEndpointGroup((name) -> name.startsWith("test")); + HealthEndpointGroups groups = HealthEndpointGroups.of(this.primaryGroup, + Collections.singletonMap("testGroup", testGroup)); + HealthResult result = create(this.registry, groups).getHealth(ApiVersion.V3, SecurityContext.NONE, false, + "testGroup"); + CompositeHealth health = (CompositeHealth) getHealth(result); + assertThat(health.getComponents()).containsKey("test"); + } + protected abstract HealthEndpointSupport create(R registry, HealthEndpointGroups groups); protected abstract R createRegistry(); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointTests.java index 40f8578b6283..776d29c83f31 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,26 +24,17 @@ import org.springframework.boot.actuate.health.HealthEndpointSupport.HealthResult; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.mockito.Mockito.mock; /** * Tests for {@link HealthEndpoint}. * * @author Phillip Webb + * @author Scott Frederick */ class HealthEndpointTests extends HealthEndpointSupportTests { - @Test - @SuppressWarnings("deprecation") - void createWhenUsingDeprecatedConstructorThrowsException() { - HealthIndicator healthIndicator = mock(HealthIndicator.class); - assertThatIllegalStateException().isThrownBy(() -> new HealthEndpoint(healthIndicator)) - .withMessage("Unable to create class org.springframework.boot.actuate.health.HealthEndpoint " - + "using deprecated constructor"); - } - @Test void healthReturnsSystemHealth() { this.registry.registerContributor("test", createContributor(this.up)); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebExtensionTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebExtensionTests.java index b80241cbe66a..d791a7686b34 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebExtensionTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebExtensionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,33 +21,23 @@ import org.junit.jupiter.api.Test; +import org.springframework.boot.actuate.endpoint.ApiVersion; import org.springframework.boot.actuate.endpoint.SecurityContext; -import org.springframework.boot.actuate.endpoint.http.ApiVersion; import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; import org.springframework.boot.actuate.health.HealthEndpointSupport.HealthResult; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.mockito.Mockito.mock; /** * Tests for {@link HealthEndpointWebExtension}. * * @author Phillip Webb + * @author Scott Frederick */ class HealthEndpointWebExtensionTests extends HealthEndpointSupportTests { - @Test - @SuppressWarnings("deprecation") - void createWhenUsingDeprecatedConstructorThrowsException() { - HealthEndpoint delegate = mock(HealthEndpoint.class); - HealthWebEndpointResponseMapper responseMapper = mock(HealthWebEndpointResponseMapper.class); - assertThatIllegalStateException().isThrownBy(() -> new HealthEndpointWebExtension(delegate, responseMapper)) - .withMessage("Unable to create class org.springframework.boot.actuate." - + "health.HealthEndpointWebExtension using deprecated constructor"); - } - @Test void healthReturnsSystemHealth() { this.registry.registerContributor("test", createContributor(this.up)); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebIntegrationTests.java index 9d95c6cf523f..ab6c24431eb9 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthEndpointWebIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ import org.assertj.core.api.ThrowableAssert.ThrowingCallable; import reactor.core.publisher.Mono; -import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; +import org.springframework.boot.actuate.endpoint.ApiVersion; import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; @@ -45,6 +45,10 @@ */ class HealthEndpointWebIntegrationTests { + private static final String V2_JSON = ApiVersion.V2.getProducedMimeType().toString(); + + private static final String V3_JSON = ApiVersion.V3.getProducedMimeType().toString(); + @WebEndpointTest void whenHealthIsUp200ResponseIsReturned(WebTestClient client) { client.get().uri("/actuator/health").accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk() @@ -54,8 +58,7 @@ void whenHealthIsUp200ResponseIsReturned(WebTestClient client) { @WebEndpointTest void whenHealthIsUpAndAcceptsV3Request200ResponseIsReturned(WebTestClient client) { - client.get().uri("/actuator/health") - .headers((headers) -> headers.set(HttpHeaders.ACCEPT, ActuatorMediaType.V3_JSON)).exchange() + client.get().uri("/actuator/health").headers((headers) -> headers.set(HttpHeaders.ACCEPT, V3_JSON)).exchange() .expectStatus().isOk().expectBody().jsonPath("status").isEqualTo("UP") .jsonPath("components.alpha.status").isEqualTo("UP").jsonPath("components.bravo.status") .isEqualTo("UP"); @@ -71,8 +74,7 @@ void whenHealthIsUpAndAcceptsAllRequest200ResponseIsReturned(WebTestClient clien @WebEndpointTest void whenHealthIsUpAndV2Request200ResponseIsReturnedInV2Format(WebTestClient client) { - client.get().uri("/actuator/health") - .headers((headers) -> headers.set(HttpHeaders.ACCEPT, ActuatorMediaType.V2_JSON)).exchange() + client.get().uri("/actuator/health").headers((headers) -> headers.set(HttpHeaders.ACCEPT, V2_JSON)).exchange() .expectStatus().isOk().expectBody().jsonPath("status").isEqualTo("UP").jsonPath("details.alpha.status") .isEqualTo("UP").jsonPath("details.bravo.status").isEqualTo("UP"); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthIndicatorReactiveAdapterTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthIndicatorReactiveAdapterTests.java index 72754f953c4b..1506c751f9b8 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthIndicatorReactiveAdapterTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthIndicatorReactiveAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.boot.actuate.health; +import java.time.Duration; + import org.junit.jupiter.api.Test; import reactor.test.StepVerifier; @@ -27,7 +29,6 @@ * * @author Stephane Nicoll */ -@SuppressWarnings("deprecation") class HealthIndicatorReactiveAdapterTests { @Test @@ -44,7 +45,7 @@ void delegateThrowError() { HealthIndicator delegate = mock(HealthIndicator.class); HealthIndicatorReactiveAdapter adapter = new HealthIndicatorReactiveAdapter(delegate); given(delegate.health()).willThrow(new IllegalStateException("Expected")); - StepVerifier.create(adapter.health()).expectError(IllegalStateException.class); + StepVerifier.create(adapter.health()).expectError(IllegalStateException.class).verify(Duration.ofSeconds(10)); } @Test diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthWebEndpointResponseMapperTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthWebEndpointResponseMapperTests.java deleted file mode 100644 index 66cd6ab428ea..000000000000 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/HealthWebEndpointResponseMapperTests.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.health; - -import java.security.Principal; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.function.Supplier; - -import org.junit.jupiter.api.Test; -import org.mockito.stubbing.Answer; - -import org.springframework.boot.actuate.endpoint.SecurityContext; -import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; -import org.springframework.http.HttpStatus; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; - -/** - * Tests for {@link HealthWebEndpointResponseMapper}. - * - * @author Stephane Nicoll - */ -@Deprecated -class HealthWebEndpointResponseMapperTests { - - private final HealthStatusHttpMapper statusHttpMapper = new HealthStatusHttpMapper(); - - private Set authorizedRoles = Collections.singleton("ACTUATOR"); - - @Test - void mapDetailsWithDisableDetailsDoesNotInvokeSupplier() { - HealthWebEndpointResponseMapper mapper = createMapper(ShowDetails.NEVER); - Supplier supplier = mockSupplier(); - SecurityContext securityContext = mock(SecurityContext.class); - WebEndpointResponse response = mapper.mapDetails(supplier, securityContext); - assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value()); - verifyNoInteractions(supplier); - verifyNoInteractions(securityContext); - } - - @Test - void mapDetailsWithUnauthorizedUserDoesNotInvokeSupplier() { - HealthWebEndpointResponseMapper mapper = createMapper(ShowDetails.WHEN_AUTHORIZED); - Supplier supplier = mockSupplier(); - SecurityContext securityContext = mockSecurityContext("USER"); - WebEndpointResponse response = mapper.mapDetails(supplier, securityContext); - assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value()); - assertThat(response.getBody()).isNull(); - verifyNoInteractions(supplier); - verify(securityContext).isUserInRole("ACTUATOR"); - } - - @Test - void mapDetailsWithAuthorizedUserInvokeSupplier() { - HealthWebEndpointResponseMapper mapper = createMapper(ShowDetails.WHEN_AUTHORIZED); - Supplier supplier = mockSupplier(); - given(supplier.get()).willReturn(Health.down().build()); - SecurityContext securityContext = mockSecurityContext("ACTUATOR"); - WebEndpointResponse response = mapper.mapDetails(supplier, securityContext); - assertThat(response.getStatus()).isEqualTo(HttpStatus.SERVICE_UNAVAILABLE.value()); - assertThat(response.getBody().getStatus()).isEqualTo(Status.DOWN); - verify(supplier).get(); - verify(securityContext).isUserInRole("ACTUATOR"); - } - - @Test - void mapDetailsWithUnavailableHealth() { - HealthWebEndpointResponseMapper mapper = createMapper(ShowDetails.ALWAYS); - Supplier supplier = mockSupplier(); - SecurityContext securityContext = mock(SecurityContext.class); - WebEndpointResponse response = mapper.mapDetails(supplier, securityContext); - assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value()); - assertThat(response.getBody()).isNull(); - verify(supplier).get(); - verifyNoInteractions(securityContext); - } - - @SuppressWarnings("unchecked") - private Supplier mockSupplier() { - return mock(Supplier.class); - } - - private SecurityContext mockSecurityContext(String... roles) { - List associatedRoles = Arrays.asList(roles); - SecurityContext securityContext = mock(SecurityContext.class); - given(securityContext.getPrincipal()).willReturn(mock(Principal.class)); - given(securityContext.isUserInRole(anyString())).will((Answer) (invocation) -> { - String expectedRole = invocation.getArgument(0); - return associatedRoles.contains(expectedRole); - }); - return securityContext; - } - - private HealthWebEndpointResponseMapper createMapper(ShowDetails showDetails) { - return new HealthWebEndpointResponseMapper(this.statusHttpMapper, showDetails, this.authorizedRoles); - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/OrderedHealthAggregatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/OrderedHealthAggregatorTests.java deleted file mode 100644 index d98eb6751406..000000000000 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/OrderedHealthAggregatorTests.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.health; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link OrderedHealthAggregator}. - * - * @author Christian Dupuis - */ -@Deprecated -class OrderedHealthAggregatorTests { - - private OrderedHealthAggregator healthAggregator; - - @BeforeEach - void setup() { - this.healthAggregator = new OrderedHealthAggregator(); - } - - @Test - void defaultOrder() { - Map healths = new HashMap<>(); - healths.put("h1", new Health.Builder().status(Status.DOWN).build()); - healths.put("h2", new Health.Builder().status(Status.UP).build()); - healths.put("h3", new Health.Builder().status(Status.UNKNOWN).build()); - healths.put("h4", new Health.Builder().status(Status.OUT_OF_SERVICE).build()); - assertThat(this.healthAggregator.aggregate(healths).getStatus()).isEqualTo(Status.DOWN); - } - - @Test - void customOrder() { - this.healthAggregator.setStatusOrder(Status.UNKNOWN, Status.UP, Status.OUT_OF_SERVICE, Status.DOWN); - Map healths = new HashMap<>(); - healths.put("h1", new Health.Builder().status(Status.DOWN).build()); - healths.put("h2", new Health.Builder().status(Status.UP).build()); - healths.put("h3", new Health.Builder().status(Status.UNKNOWN).build()); - healths.put("h4", new Health.Builder().status(Status.OUT_OF_SERVICE).build()); - assertThat(this.healthAggregator.aggregate(healths).getStatus()).isEqualTo(Status.UNKNOWN); - } - - @Test - void defaultOrderWithCustomStatus() { - Map healths = new HashMap<>(); - healths.put("h1", new Health.Builder().status(Status.DOWN).build()); - healths.put("h2", new Health.Builder().status(Status.UP).build()); - healths.put("h3", new Health.Builder().status(Status.UNKNOWN).build()); - healths.put("h4", new Health.Builder().status(Status.OUT_OF_SERVICE).build()); - healths.put("h5", new Health.Builder().status(new Status("CUSTOM")).build()); - assertThat(this.healthAggregator.aggregate(healths).getStatus()).isEqualTo(Status.DOWN); - } - - @Test - void customOrderWithCustomStatus() { - this.healthAggregator.setStatusOrder(Arrays.asList("DOWN", "OUT_OF_SERVICE", "UP", "UNKNOWN", "CUSTOM")); - Map healths = new HashMap<>(); - healths.put("h1", new Health.Builder().status(Status.DOWN).build()); - healths.put("h2", new Health.Builder().status(Status.UP).build()); - healths.put("h3", new Health.Builder().status(Status.UNKNOWN).build()); - healths.put("h4", new Health.Builder().status(Status.OUT_OF_SERVICE).build()); - healths.put("h5", new Health.Builder().status(new Status("CUSTOM")).build()); - assertThat(this.healthAggregator.aggregate(healths).getStatus()).isEqualTo(Status.DOWN); - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveHealthContributorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveHealthContributorTests.java index 8f89f860d70c..201d79e0acb5 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveHealthContributorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveHealthContributorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,7 +39,6 @@ void adaptWhenNullThrowsException() { } @Test - @SuppressWarnings("deprecation") void adaptWhenHealthIndicatorReturnsHealthIndicatorReactiveAdapter() { HealthIndicator indicator = () -> Health.outOfService().build(); ReactiveHealthContributor adapted = ReactiveHealthContributor.adapt(indicator); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtensionTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtensionTests.java index ce79a32e1fb9..1d3a5718db00 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtensionTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveHealthEndpointWebExtensionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,34 +22,23 @@ import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; +import org.springframework.boot.actuate.endpoint.ApiVersion; import org.springframework.boot.actuate.endpoint.SecurityContext; -import org.springframework.boot.actuate.endpoint.http.ApiVersion; import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; import org.springframework.boot.actuate.health.HealthEndpointSupport.HealthResult; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.mockito.Mockito.mock; /** * Tests for {@link ReactiveHealthEndpointWebExtension}. * * @author Phillip Webb + * @author Scott Frederick */ class ReactiveHealthEndpointWebExtensionTests extends HealthEndpointSupportTests> { - @Test - @SuppressWarnings("deprecation") - void createWhenUsingDeprecatedConstructorThrowsException() { - ReactiveHealthIndicator delegate = mock(ReactiveHealthIndicator.class); - HealthWebEndpointResponseMapper responseMapper = mock(HealthWebEndpointResponseMapper.class); - assertThatIllegalStateException() - .isThrownBy(() -> new ReactiveHealthEndpointWebExtension(delegate, responseMapper)).withMessage( - "Unable to create class org.springframework.boot.actuate.health.ReactiveHealthEndpointWebExtension " - + "using deprecated constructor"); - } - @Test void healthReturnsSystemHealth() { this.registry.registerContributor("test", createContributor(this.up)); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveHealthIndicatorRegistryFactoryTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveHealthIndicatorRegistryFactoryTests.java deleted file mode 100644 index 5939865f9e00..000000000000 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/ReactiveHealthIndicatorRegistryFactoryTests.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.actuate.health; - -import java.util.Collections; - -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link ReactiveHealthIndicatorRegistryFactory}. - * - * @author Stephane Nicoll - */ -@Deprecated -class ReactiveHealthIndicatorRegistryFactoryTests { - - private static final Health UP = new Health.Builder().status(Status.UP).build(); - - private static final Health DOWN = new Health.Builder().status(Status.DOWN).build(); - - private final ReactiveHealthIndicatorRegistryFactory factory = new ReactiveHealthIndicatorRegistryFactory(); - - @Test - void defaultHealthIndicatorNameFactory() { - ReactiveHealthIndicatorRegistry registry = this.factory.createReactiveHealthIndicatorRegistry( - Collections.singletonMap("myHealthIndicator", () -> Mono.just(UP)), null); - assertThat(registry.getAll()).containsOnlyKeys("my"); - } - - @Test - void healthIndicatorIsAdapted() { - ReactiveHealthIndicatorRegistry registry = this.factory.createReactiveHealthIndicatorRegistry( - Collections.singletonMap("test", () -> Mono.just(UP)), Collections.singletonMap("regular", () -> DOWN)); - assertThat(registry.getAll()).containsOnlyKeys("test", "regular"); - StepVerifier.create(registry.get("regular").health()).consumeNextWith((h) -> { - assertThat(h.getStatus()).isEqualTo(Status.DOWN); - assertThat(h.getDetails()).isEmpty(); - }).verifyComplete(); - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/SimpleStatusAggregatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/SimpleStatusAggregatorTests.java index 2a9ccf4c0687..1e0b9ab44120 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/SimpleStatusAggregatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/SimpleStatusAggregatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,14 @@ class SimpleStatusAggregatorTests { @Test - void getAggregateStatusWhenUsingDefaultOrder() { + void getAggregateStatusWhenUsingDefaultInstance() { + StatusAggregator aggregator = StatusAggregator.getDefault(); + Status status = aggregator.getAggregateStatus(Status.DOWN, Status.UP, Status.UNKNOWN, Status.OUT_OF_SERVICE); + assertThat(status).isEqualTo(Status.DOWN); + } + + @Test + void getAggregateStatusWhenUsingNewDefaultOrder() { SimpleStatusAggregator aggregator = new SimpleStatusAggregator(); Status status = aggregator.getAggregateStatus(Status.DOWN, Status.UP, Status.UNKNOWN, Status.OUT_OF_SERVICE); assertThat(status).isEqualTo(Status.DOWN); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/StatusTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/StatusTests.java index eb17a9293b25..0125682c68d2 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/StatusTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/StatusTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ package org.springframework.boot.actuate.health; -import com.couchbase.client.deps.com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -72,7 +72,7 @@ void serializeWithJacksonReturnsValidJson() throws Exception { Status status = new Status("spring", "boot"); ObjectMapper mapper = new ObjectMapper(); String json = mapper.writeValueAsString(status); - assertThat(json).isEqualTo("{\"code\":\"spring\",\"description\":\"boot\"}"); + assertThat(json).isEqualTo("{\"description\":\"boot\",\"status\":\"spring\"}"); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/SystemHealthTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/SystemHealthTests.java index 45eafce64e54..401719b52efa 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/SystemHealthTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/health/SystemHealthTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; -import org.springframework.boot.actuate.endpoint.http.ApiVersion; +import org.springframework.boot.actuate.endpoint.ApiVersion; import static org.assertj.core.api.Assertions.assertThat; @@ -45,7 +45,7 @@ void serializeWithJacksonReturnsValidJson() throws Exception { CompositeHealth health = new SystemHealth(ApiVersion.V3, Status.UP, components, groups); ObjectMapper mapper = new ObjectMapper(); String json = mapper.writeValueAsString(health); - assertThat(json).isEqualTo("{\"status\":\"UP\",\"components\":{" + "\"db1\":{\"status\":\"UP\"}," + assertThat(json).isEqualTo("{\"status\":\"UP\",\"components\":{\"db1\":{\"status\":\"UP\"}," + "\"db2\":{\"status\":\"DOWN\",\"details\":{\"a\":\"b\"}}}," + "\"groups\":[\"liveness\",\"readiness\"]}"); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/influx/InfluxDbHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/influx/InfluxDbHealthIndicatorTests.java index 6f94f0d11fcd..b991ceb5e0c8 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/influx/InfluxDbHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/influx/InfluxDbHealthIndicatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,8 +28,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link InfluxDbHealthIndicator}. @@ -42,24 +42,24 @@ class InfluxDbHealthIndicatorTests { void influxDbIsUp() { Pong pong = mock(Pong.class); given(pong.getVersion()).willReturn("0.9"); - InfluxDB influxDB = mock(InfluxDB.class); - given(influxDB.ping()).willReturn(pong); - InfluxDbHealthIndicator healthIndicator = new InfluxDbHealthIndicator(influxDB); + InfluxDB influxDb = mock(InfluxDB.class); + given(influxDb.ping()).willReturn(pong); + InfluxDbHealthIndicator healthIndicator = new InfluxDbHealthIndicator(influxDb); Health health = healthIndicator.health(); assertThat(health.getStatus()).isEqualTo(Status.UP); assertThat(health.getDetails().get("version")).isEqualTo("0.9"); - verify(influxDB).ping(); + then(influxDb).should().ping(); } @Test void influxDbIsDown() { - InfluxDB influxDB = mock(InfluxDB.class); - given(influxDB.ping()).willThrow(new InfluxDBException(new IOException("Connection failed"))); - InfluxDbHealthIndicator healthIndicator = new InfluxDbHealthIndicator(influxDB); + InfluxDB influxDb = mock(InfluxDB.class); + given(influxDb.ping()).willThrow(new InfluxDBException(new IOException("Connection failed"))); + InfluxDbHealthIndicator healthIndicator = new InfluxDbHealthIndicator(influxDb); Health health = healthIndicator.health(); assertThat(health.getStatus()).isEqualTo(Status.DOWN); assertThat((String) health.getDetails().get("error")).contains("Connection failed"); - verify(influxDB).ping(); + then(influxDb).should().ping(); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/integration/IntegrationGraphEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/integration/IntegrationGraphEndpointTests.java index fc9468b71ff6..c832b2d0547d 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/integration/IntegrationGraphEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/integration/IntegrationGraphEndpointTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,8 +23,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link IntegrationGraphEndpoint}. @@ -42,14 +42,14 @@ void readOperationShouldReturnGraph() { Graph mockedGraph = mock(Graph.class); given(this.server.getGraph()).willReturn(mockedGraph); Graph graph = this.endpoint.graph(); - verify(this.server).getGraph(); + then(this.server).should().getGraph(); assertThat(graph).isEqualTo(mockedGraph); } @Test void writeOperationShouldRebuildGraph() { this.endpoint.rebuild(); - verify(this.server).rebuild(); + then(this.server).should().rebuild(); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/integration/IntegrationGraphEndpointWebIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/integration/IntegrationGraphEndpointWebIntegrationTests.java index 0189fb596c23..76593213de20 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/integration/IntegrationGraphEndpointWebIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/integration/IntegrationGraphEndpointWebIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,7 @@ class IntegrationGraphEndpointWebIntegrationTests { void graph(WebTestClient client) { client.get().uri("/actuator/integrationgraph").accept(MediaType.APPLICATION_JSON).exchange().expectStatus() .isOk().expectBody().jsonPath("contentDescriptor.providerVersion").isNotEmpty() - .jsonPath("contentDescriptor.providerFormatVersion").isEqualTo(1.1f) + .jsonPath("contentDescriptor.providerFormatVersion").isEqualTo(1.2f) .jsonPath("contentDescriptor.provider").isEqualTo("spring-integration"); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/jdbc/DataSourceHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/jdbc/DataSourceHealthIndicatorTests.java index 865d28d8aebf..df69e11813d1 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/jdbc/DataSourceHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/jdbc/DataSourceHealthIndicatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.boot.actuate.jdbc; import java.sql.Connection; +import java.sql.SQLException; import javax.sql.DataSource; @@ -26,7 +27,6 @@ import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.Status; -import org.springframework.boot.jdbc.DatabaseDriver; import org.springframework.boot.jdbc.EmbeddedDatabaseConnection; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.SingleConnectionDataSource; @@ -34,9 +34,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; /** * Tests for {@link DataSourceHealthIndicator}. @@ -52,7 +52,7 @@ class DataSourceHealthIndicatorTests { @BeforeEach void init() { - EmbeddedDatabaseConnection db = EmbeddedDatabaseConnection.HSQL; + EmbeddedDatabaseConnection db = EmbeddedDatabaseConnection.HSQLDB; this.dataSource = new SingleConnectionDataSource(db.getUrl("testdb") + ";shutdown=true", "sa", "", false); this.dataSource.setDriverClassName(db.getDriverClassName()); } @@ -69,8 +69,8 @@ void healthIndicatorWithDefaultSettings() { this.indicator.setDataSource(this.dataSource); Health health = this.indicator.health(); assertThat(health.getStatus()).isEqualTo(Status.UP); - assertThat(health.getDetails()).containsOnly(entry("database", "HSQL Database Engine"), entry("result", 1L), - entry("validationQuery", DatabaseDriver.HSQLDB.getValidationQuery())); + assertThat(health.getDetails()).containsOnly(entry("database", "HSQL Database Engine"), + entry("validationQuery", "isValid()")); } @Test @@ -106,7 +106,21 @@ void healthIndicatorCloseConnection() throws Exception { this.indicator.setDataSource(dataSource); Health health = this.indicator.health(); assertThat(health.getDetails().get("database")).isNotNull(); - verify(connection, times(2)).close(); + then(connection).should(times(2)).close(); + } + + @Test + void healthIndicatorWithConnectionValidationFailure() throws SQLException { + DataSource dataSource = mock(DataSource.class); + Connection connection = mock(Connection.class); + given(connection.isValid(0)).willReturn(false); + given(connection.getMetaData()).willReturn(this.dataSource.getConnection().getMetaData()); + given(dataSource.getConnection()).willReturn(connection); + this.indicator.setDataSource(dataSource); + Health health = this.indicator.health(); + assertThat(health.getStatus()).isEqualTo(Status.DOWN); + assertThat(health.getDetails()).containsOnly(entry("database", "HSQL Database Engine"), + entry("validationQuery", "isValid()")); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/jms/JmsHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/jms/JmsHealthIndicatorTests.java index 93d39f95c678..151dc7077852 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/jms/JmsHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/jms/JmsHealthIndicatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,11 +30,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.BDDMockito.willAnswer; import static org.mockito.BDDMockito.willThrow; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; /** * Tests for {@link JmsHealthIndicator}. @@ -55,7 +54,7 @@ void jmsBrokerIsUp() throws JMSException { Health health = indicator.health(); assertThat(health.getStatus()).isEqualTo(Status.UP); assertThat(health.getDetails().get("provider")).isEqualTo("JMS test provider"); - verify(connection, times(1)).close(); + then(connection).should().close(); } @Test @@ -80,7 +79,7 @@ void jmsBrokerCouldNotRetrieveProviderMetadata() throws JMSException { Health health = indicator.health(); assertThat(health.getStatus()).isEqualTo(Status.DOWN); assertThat(health.getDetails().get("provider")).isNull(); - verify(connection, times(1)).close(); + then(connection).should().close(); } @Test diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/ldap/LdapHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/ldap/LdapHealthIndicatorTests.java index 76e93fe1779f..9af6b7950a63 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/ldap/LdapHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/ldap/LdapHealthIndicatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,8 +27,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link LdapHealthIndicator} @@ -46,7 +46,7 @@ void ldapIsUp() { Health health = healthIndicator.health(); assertThat(health.getStatus()).isEqualTo(Status.UP); assertThat(health.getDetails().get("version")).isEqualTo("3"); - verify(ldapTemplate).executeReadOnly((ContextExecutor) any()); + then(ldapTemplate).should().executeReadOnly((ContextExecutor) any()); } @Test @@ -59,7 +59,7 @@ void ldapIsDown() { Health health = healthIndicator.health(); assertThat(health.getStatus()).isEqualTo(Status.DOWN); assertThat((String) health.getDetails().get("error")).contains("Connection failed"); - verify(ldapTemplate).executeReadOnly((ContextExecutor) any()); + then(ldapTemplate).should().executeReadOnly((ContextExecutor) any()); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/liquibase/LiquibaseEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/liquibase/LiquibaseEndpointTests.java index 4e4adf9aad5e..4410b30f27e5 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/liquibase/LiquibaseEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/liquibase/LiquibaseEndpointTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,20 +18,27 @@ import java.sql.Connection; import java.sql.SQLException; +import java.util.Arrays; import java.util.Map; +import java.util.UUID; import javax.sql.DataSource; +import liquibase.integration.spring.SpringLiquibase; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.liquibase.LiquibaseEndpoint.LiquibaseBean; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration; +import org.springframework.boot.jdbc.EmbeddedDatabaseConnection; +import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer; +import org.springframework.boot.sql.init.DatabaseInitializationSettings; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; import static org.assertj.core.api.Assertions.assertThat; @@ -41,6 +48,7 @@ * @author Eddú Meléndez * @author Andy Wilkinson * @author Stephane Nicoll + * @author Leo Li */ class LiquibaseEndpointTests { @@ -58,12 +66,21 @@ void liquibaseReportIsReturned() { }); } + @Test + void liquibaseReportIsReturnedForContextHierarchy() { + this.contextRunner.withUserConfiguration().run((parent) -> { + this.contextRunner.withUserConfiguration(Config.class).withParent(parent).run((context) -> { + Map liquibaseBeans = context.getBean(LiquibaseEndpoint.class).liquibaseBeans() + .getContexts().get(parent.getId()).getLiquibaseBeans(); + assertThat(liquibaseBeans.get("liquibase").getChangeSets()).hasSize(1); + }); + }); + } + @Test void invokeWithCustomSchema() { - this.contextRunner.withUserConfiguration(Config.class) - .withPropertyValues("spring.liquibase.default-schema=CUSTOMSCHEMA", - "spring.datasource.schema=classpath:/db/create-custom-schema.sql") - .run((context) -> { + this.contextRunner.withUserConfiguration(Config.class, DataSourceWithSchemaConfiguration.class) + .withPropertyValues("spring.liquibase.default-schema=CUSTOMSCHEMA").run((context) -> { Map liquibaseBeans = context.getBean(LiquibaseEndpoint.class) .liquibaseBeans().getContexts().get(context.getId()).getLiquibaseBeans(); assertThat(liquibaseBeans.get("liquibase").getChangeSets()).hasSize(1); @@ -92,6 +109,21 @@ void connectionAutoCommitPropertyIsReset() { }); } + @Test + void whenMultipleLiquibaseBeansArePresentChangeSetsAreCorrectlyReportedForEachBean() { + this.contextRunner.withUserConfiguration(Config.class, MultipleDataSourceLiquibaseConfiguration.class) + .run((context) -> { + Map liquibaseBeans = context.getBean(LiquibaseEndpoint.class) + .liquibaseBeans().getContexts().get(context.getId()).getLiquibaseBeans(); + assertThat(liquibaseBeans.get("liquibase").getChangeSets()).hasSize(1); + assertThat(liquibaseBeans.get("liquibase").getChangeSets().get(0).getChangeLog()) + .isEqualTo("db/changelog/db.changelog-master.yaml"); + assertThat(liquibaseBeans.get("liquibaseBackup").getChangeSets()).hasSize(1); + assertThat(liquibaseBeans.get("liquibaseBackup").getChangeSets().get(0).getChangeLog()) + .isEqualTo("db/changelog/db.changelog-master-backup.yaml"); + }); + } + private boolean getAutoCommit(DataSource dataSource) throws SQLException { try (Connection connection = dataSource.getConnection()) { return connection.getAutoCommit(); @@ -108,4 +140,60 @@ LiquibaseEndpoint endpoint(ApplicationContext context) { } + @Configuration(proxyBeanMethods = false) + static class DataSourceWithSchemaConfiguration { + + @Bean + DataSource dataSource() { + DataSource dataSource = new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseConnection.get(getClass().getClassLoader()).getType()) + .setName(UUID.randomUUID().toString()).build(); + DatabaseInitializationSettings settings = new DatabaseInitializationSettings(); + settings.setSchemaLocations(Arrays.asList("classpath:/db/create-custom-schema.sql")); + DataSourceScriptDatabaseInitializer initializer = new DataSourceScriptDatabaseInitializer(dataSource, + settings); + initializer.initializeDatabase(); + return dataSource; + } + + } + + @Configuration(proxyBeanMethods = false) + static class MultipleDataSourceLiquibaseConfiguration { + + @Bean + DataSource dataSource() { + return createEmbeddedDatabase(); + } + + @Bean + DataSource dataSourceBackup() { + return createEmbeddedDatabase(); + } + + @Bean + SpringLiquibase liquibase(DataSource dataSource) { + return createSpringLiquibase("db.changelog-master.yaml", dataSource); + } + + @Bean + SpringLiquibase liquibaseBackup(DataSource dataSourceBackup) { + return createSpringLiquibase("db.changelog-master-backup.yaml", dataSourceBackup); + } + + private DataSource createEmbeddedDatabase() { + return new EmbeddedDatabaseBuilder().generateUniqueName(true) + .setType(EmbeddedDatabaseConnection.HSQLDB.getType()).build(); + } + + private SpringLiquibase createSpringLiquibase(String changeLog, DataSource dataSource) { + SpringLiquibase liquibase = new SpringLiquibase(); + liquibase.setChangeLog("classpath:/db/changelog/" + changeLog); + liquibase.setShouldRun(true); + liquibase.setDataSource(dataSource); + return liquibase; + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/logging/LogFileWebEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/logging/LogFileWebEndpointTests.java index 432256b55f4e..2e9139be15c7 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/logging/LogFileWebEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/logging/LogFileWebEndpointTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -74,16 +74,6 @@ void resourceResponseWithLogFile() throws Exception { assertThat(contentOf(resource.getFile())).isEqualTo("--TEST--"); } - @Test - @Deprecated - void resourceResponseWithLogFileAndDeprecatedProperty() throws Exception { - this.environment.setProperty("logging.file", this.logFile.getAbsolutePath()); - LogFileWebEndpoint endpoint = new LogFileWebEndpoint(LogFile.get(this.environment), null); - Resource resource = endpoint.logFile(); - assertThat(resource).isNotNull(); - assertThat(contentOf(resource.getFile())).isEqualTo("--TEST--"); - } - @Test void resourceResponseWithExternalLogFile() throws Exception { LogFileWebEndpoint endpoint = new LogFileWebEndpoint(null, this.logFile); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/logging/LogFileWebEndpointWebIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/logging/LogFileWebEndpointWebIntegrationTests.java index 3a0d2deaf2c8..b7566eeacea6 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/logging/LogFileWebEndpointWebIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/logging/LogFileWebEndpointWebIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,7 +50,7 @@ void setUp(WebTestClient client) { } @BeforeAll - static void setup(@TempDir File temp) throws IOException { + static void setup(@TempDir File temp) { tempFile = temp; } @@ -74,7 +74,7 @@ LogFileWebEndpoint logFileEndpoint() throws IOException { File logFile = new File(tempFile, "test.log"); FileCopyUtils.copy("--TEST--".getBytes(), logFile); MockEnvironment environment = new MockEnvironment(); - environment.setProperty("logging.file", logFile.getAbsolutePath()); + environment.setProperty("logging.file.name", logFile.getAbsolutePath()); return new LogFileWebEndpoint(LogFile.get(environment), null); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/logging/LoggersEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/logging/LoggersEndpointTests.java index 7ced35e133d8..5c7515e3b263 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/logging/LoggersEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/logging/LoggersEndpointTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,8 +35,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link LoggersEndpoint}. @@ -120,25 +120,25 @@ void groupNameSpecifiedShouldReturnConfiguredLevelAndMembers() { @Test void configureLogLevelShouldSetLevelOnLoggingSystem() { new LoggersEndpoint(this.loggingSystem, this.loggerGroups).configureLogLevel("ROOT", LogLevel.DEBUG); - verify(this.loggingSystem).setLogLevel("ROOT", LogLevel.DEBUG); + then(this.loggingSystem).should().setLogLevel("ROOT", LogLevel.DEBUG); } @Test void configureLogLevelWithNullSetsLevelOnLoggingSystemToNull() { new LoggersEndpoint(this.loggingSystem, this.loggerGroups).configureLogLevel("ROOT", null); - verify(this.loggingSystem).setLogLevel("ROOT", null); + then(this.loggingSystem).should().setLogLevel("ROOT", null); } @Test void configureLogLevelInLoggerGroupShouldSetLevelOnLoggingSystem() { new LoggersEndpoint(this.loggingSystem, this.loggerGroups).configureLogLevel("test", LogLevel.DEBUG); - verify(this.loggingSystem).setLogLevel("test.member", LogLevel.DEBUG); + then(this.loggingSystem).should().setLogLevel("test.member", LogLevel.DEBUG); } @Test void configureLogLevelWithNullInLoggerGroupShouldSetLevelOnLoggingSystem() { new LoggersEndpoint(this.loggingSystem, this.loggerGroups).configureLogLevel("test", null); - verify(this.loggingSystem).setLogLevel("test.member", null); + then(this.loggingSystem).should().setLogLevel("test.member", null); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/logging/LoggersEndpointWebIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/logging/LoggersEndpointWebIntegrationTests.java index 0bf55d46f2a8..254df61e2270 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/logging/LoggersEndpointWebIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/logging/LoggersEndpointWebIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ import org.mockito.Mockito; import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; +import org.springframework.boot.actuate.endpoint.ApiVersion; import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest; import org.springframework.boot.logging.LogLevel; import org.springframework.boot.logging.LoggerConfiguration; @@ -43,9 +43,8 @@ import org.springframework.test.web.reactive.server.WebTestClient; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; /** * Integration tests for {@link LoggersEndpoint} when exposed via Jersey, Spring MVC, and @@ -61,6 +60,10 @@ */ class LoggersEndpointWebIntegrationTests { + private static final String V2_JSON = ApiVersion.V2.getProducedMimeType().toString(); + + private static final String V3_JSON = ApiVersion.V3.getProducedMimeType().toString(); + private WebTestClient client; private LoggingSystem loggingSystem; @@ -120,35 +123,32 @@ void setLoggerUsingApplicationJsonShouldSetLogLevel() { this.client.post().uri("/actuator/loggers/ROOT").contentType(MediaType.APPLICATION_JSON) .bodyValue(Collections.singletonMap("configuredLevel", "debug")).exchange().expectStatus() .isNoContent(); - verify(this.loggingSystem).setLogLevel("ROOT", LogLevel.DEBUG); + then(this.loggingSystem).should().setLogLevel("ROOT", LogLevel.DEBUG); } @WebEndpointTest void setLoggerUsingActuatorV2JsonShouldSetLogLevel() { - this.client.post().uri("/actuator/loggers/ROOT") - .contentType(MediaType.parseMediaType(ActuatorMediaType.V2_JSON)) + this.client.post().uri("/actuator/loggers/ROOT").contentType(MediaType.parseMediaType(V2_JSON)) .bodyValue(Collections.singletonMap("configuredLevel", "debug")).exchange().expectStatus() .isNoContent(); - verify(this.loggingSystem).setLogLevel("ROOT", LogLevel.DEBUG); + then(this.loggingSystem).should().setLogLevel("ROOT", LogLevel.DEBUG); } @WebEndpointTest void setLoggerUsingActuatorV3JsonShouldSetLogLevel() { - this.client.post().uri("/actuator/loggers/ROOT") - .contentType(MediaType.parseMediaType(ActuatorMediaType.V3_JSON)) + this.client.post().uri("/actuator/loggers/ROOT").contentType(MediaType.parseMediaType(V3_JSON)) .bodyValue(Collections.singletonMap("configuredLevel", "debug")).exchange().expectStatus() .isNoContent(); - verify(this.loggingSystem).setLogLevel("ROOT", LogLevel.DEBUG); + then(this.loggingSystem).should().setLogLevel("ROOT", LogLevel.DEBUG); } @WebEndpointTest void setLoggerGroupUsingActuatorV2JsonShouldSetLogLevel() { - this.client.post().uri("/actuator/loggers/test") - .contentType(MediaType.parseMediaType(ActuatorMediaType.V2_JSON)) + this.client.post().uri("/actuator/loggers/test").contentType(MediaType.parseMediaType(V2_JSON)) .bodyValue(Collections.singletonMap("configuredLevel", "debug")).exchange().expectStatus() .isNoContent(); - verify(this.loggingSystem).setLogLevel("test.member1", LogLevel.DEBUG); - verify(this.loggingSystem).setLogLevel("test.member2", LogLevel.DEBUG); + then(this.loggingSystem).should().setLogLevel("test.member1", LogLevel.DEBUG); + then(this.loggingSystem).should().setLogLevel("test.member2", LogLevel.DEBUG); } @WebEndpointTest @@ -156,8 +156,8 @@ void setLoggerGroupUsingApplicationJsonShouldSetLogLevel() { this.client.post().uri("/actuator/loggers/test").contentType(MediaType.APPLICATION_JSON) .bodyValue(Collections.singletonMap("configuredLevel", "debug")).exchange().expectStatus() .isNoContent(); - verify(this.loggingSystem).setLogLevel("test.member1", LogLevel.DEBUG); - verify(this.loggingSystem).setLogLevel("test.member2", LogLevel.DEBUG); + then(this.loggingSystem).should().setLogLevel("test.member1", LogLevel.DEBUG); + then(this.loggingSystem).should().setLogLevel("test.member2", LogLevel.DEBUG); } @WebEndpointTest @@ -165,41 +165,37 @@ void setLoggerOrLoggerGroupWithWrongLogLevelResultInBadRequestResponse() { this.client.post().uri("/actuator/loggers/ROOT").contentType(MediaType.APPLICATION_JSON) .bodyValue(Collections.singletonMap("configuredLevel", "other")).exchange().expectStatus() .isBadRequest(); - verifyNoInteractions(this.loggingSystem); + then(this.loggingSystem).shouldHaveNoInteractions(); } @WebEndpointTest void setLoggerWithNullLogLevel() { - this.client.post().uri("/actuator/loggers/ROOT") - .contentType(MediaType.parseMediaType(ActuatorMediaType.V3_JSON)) + this.client.post().uri("/actuator/loggers/ROOT").contentType(MediaType.parseMediaType(V3_JSON)) .bodyValue(Collections.singletonMap("configuredLevel", null)).exchange().expectStatus().isNoContent(); - verify(this.loggingSystem).setLogLevel("ROOT", null); + then(this.loggingSystem).should().setLogLevel("ROOT", null); } @WebEndpointTest void setLoggerWithNoLogLevel() { - this.client.post().uri("/actuator/loggers/ROOT") - .contentType(MediaType.parseMediaType(ActuatorMediaType.V3_JSON)).bodyValue(Collections.emptyMap()) - .exchange().expectStatus().isNoContent(); - verify(this.loggingSystem).setLogLevel("ROOT", null); + this.client.post().uri("/actuator/loggers/ROOT").contentType(MediaType.parseMediaType(V3_JSON)) + .bodyValue(Collections.emptyMap()).exchange().expectStatus().isNoContent(); + then(this.loggingSystem).should().setLogLevel("ROOT", null); } @WebEndpointTest void setLoggerGroupWithNullLogLevel() { - this.client.post().uri("/actuator/loggers/test") - .contentType(MediaType.parseMediaType(ActuatorMediaType.V3_JSON)) + this.client.post().uri("/actuator/loggers/test").contentType(MediaType.parseMediaType(V3_JSON)) .bodyValue(Collections.singletonMap("configuredLevel", null)).exchange().expectStatus().isNoContent(); - verify(this.loggingSystem).setLogLevel("test.member1", null); - verify(this.loggingSystem).setLogLevel("test.member2", null); + then(this.loggingSystem).should().setLogLevel("test.member1", null); + then(this.loggingSystem).should().setLogLevel("test.member2", null); } @WebEndpointTest void setLoggerGroupWithNoLogLevel() { - this.client.post().uri("/actuator/loggers/test") - .contentType(MediaType.parseMediaType(ActuatorMediaType.V3_JSON)).bodyValue(Collections.emptyMap()) - .exchange().expectStatus().isNoContent(); - verify(this.loggingSystem).setLogLevel("test.member1", null); - verify(this.loggingSystem).setLogLevel("test.member2", null); + this.client.post().uri("/actuator/loggers/test").contentType(MediaType.parseMediaType(V3_JSON)) + .bodyValue(Collections.emptyMap()).exchange().expectStatus().isNoContent(); + then(this.loggingSystem).should().setLogLevel("test.member1", null); + then(this.loggingSystem).should().setLogLevel("test.member2", null); } @WebEndpointTest diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/management/HeapDumpWebEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/management/HeapDumpWebEndpointTests.java index 6a861b659248..b626414af182 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/management/HeapDumpWebEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/management/HeapDumpWebEndpointTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,7 @@ void parallelRequestProducesTooManyRequestsResponse() throws InterruptedExceptio HeapDumpWebEndpoint slowEndpoint = new HeapDumpWebEndpoint(2500) { @Override - protected HeapDumper createHeapDumper() throws HeapDumperUnavailableException { + protected HeapDumper createHeapDumper() { return (file, live) -> { dumpingLatch.countDown(); blockingLatch.await(); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/management/HeapDumpWebEndpointWebIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/management/HeapDumpWebEndpointWebIntegrationTests.java index baaf6e1a18c5..c6e58f865062 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/management/HeapDumpWebEndpointWebIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/management/HeapDumpWebEndpointWebIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -59,13 +59,13 @@ void invokeWhenNotAvailableShouldReturnServiceUnavailableStatus(WebTestClient cl } @WebEndpointTest - void getRequestShouldReturnHeapDumpInResponseBody(WebTestClient client) throws Exception { + void getRequestShouldReturnHeapDumpInResponseBody(WebTestClient client) { client.get().uri("/actuator/heapdump").exchange().expectStatus().isOk().expectHeader() .contentType(MediaType.APPLICATION_OCTET_STREAM).expectBody(String.class).isEqualTo("HEAPDUMP"); assertHeapDumpFileIsDeleted(); } - private void assertHeapDumpFileIsDeleted() throws InterruptedException { + private void assertHeapDumpFileIsDeleted() { Awaitility.waitAtMost(Duration.ofSeconds(5)).until(this.endpoint.file::exists, is(false)); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/management/ThreadDumpEndpointWebIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/management/ThreadDumpEndpointWebIntegrationTests.java index 077aa0418c38..3d4f3619403a 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/management/ThreadDumpEndpointWebIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/management/ThreadDumpEndpointWebIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,13 +33,13 @@ class ThreadDumpEndpointWebIntegrationTests { @WebEndpointTest - void getRequestWithJsonAcceptHeaderShouldProduceJsonThreadDumpResponse(WebTestClient client) throws Exception { + void getRequestWithJsonAcceptHeaderShouldProduceJsonThreadDumpResponse(WebTestClient client) { client.get().uri("/actuator/threaddump").accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk() .expectHeader().contentType(MediaType.APPLICATION_JSON); } @WebEndpointTest - void getRequestWithTextPlainAcceptHeaderShouldProduceTextPlainResponse(WebTestClient client) throws Exception { + void getRequestWithTextPlainAcceptHeaderShouldProduceTextPlainResponse(WebTestClient client) { String response = client.get().uri("/actuator/threaddump").accept(MediaType.TEXT_PLAIN).exchange() .expectStatus().isOk().expectHeader().contentType("text/plain;charset=UTF-8").expectBody(String.class) .returnResult().getResponseBody(); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/MetricsEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/MetricsEndpointTests.java index 12e3aae028bf..9413ce680452 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/MetricsEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/MetricsEndpointTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,15 +54,20 @@ void listNamesHandlesEmptyListOfMeters() { @Test void listNamesProducesListOfUniqueMeterNames() { - this.registry.counter("com.example.foo"); - this.registry.counter("com.example.bar"); - this.registry.counter("com.example.foo"); + this.registry.counter("com.example.alpha"); + this.registry.counter("com.example.charlie"); + this.registry.counter("com.example.bravo"); + this.registry.counter("com.example.delta"); + this.registry.counter("com.example.delta"); + this.registry.counter("com.example.echo"); + this.registry.counter("com.example.bravo"); MetricsEndpoint.ListNamesResponse result = this.endpoint.listNames(); - assertThat(result.getNames()).containsOnlyOnce("com.example.foo", "com.example.bar"); + assertThat(result.getNames()).containsExactly("com.example.alpha", "com.example.bravo", "com.example.charlie", + "com.example.delta", "com.example.echo"); } @Test - void listNamesRecursesOverCompositeRegistries() { + void listNamesResponseOverCompositeRegistries() { CompositeMeterRegistry composite = new CompositeMeterRegistry(); SimpleMeterRegistry reg1 = new SimpleMeterRegistry(); SimpleMeterRegistry reg2 = new SimpleMeterRegistry(); @@ -71,7 +76,7 @@ void listNamesRecursesOverCompositeRegistries() { reg1.counter("counter1").increment(); reg2.counter("counter2").increment(); MetricsEndpoint endpoint = new MetricsEndpoint(composite); - assertThat(endpoint.listNames().getNames()).containsOnly("counter1", "counter2"); + assertThat(endpoint.listNames().getNames()).containsExactly("counter1", "counter2"); } @Test diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/annotation/TimedAnnotationsTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/annotation/TimedAnnotationsTests.java new file mode 100644 index 000000000000..c563e7db8c2d --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/annotation/TimedAnnotationsTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.annotation; + +import java.lang.reflect.Method; +import java.util.Set; + +import io.micrometer.core.annotation.Timed; +import org.junit.jupiter.api.Test; + +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link TimedAnnotations}. + * + * @author Phillip Webb + */ +class TimedAnnotationsTests { + + @Test + void getWhenNoneReturnsEmptySet() { + Object bean = new None(); + Method method = ReflectionUtils.findMethod(bean.getClass(), "handle"); + Set annotations = TimedAnnotations.get(method, bean.getClass()); + assertThat(annotations).isEmpty(); + } + + @Test + void getWhenOnMethodReturnsMethodAnnotations() { + Object bean = new OnMethod(); + Method method = ReflectionUtils.findMethod(bean.getClass(), "handle"); + Set annotations = TimedAnnotations.get(method, bean.getClass()); + assertThat(annotations).extracting(Timed::value).containsOnly("y", "z"); + } + + @Test + void getWhenNonOnMethodReturnsBeanAnnotations() { + Object bean = new OnBean(); + Method method = ReflectionUtils.findMethod(bean.getClass(), "handle"); + Set annotations = TimedAnnotations.get(method, bean.getClass()); + assertThat(annotations).extracting(Timed::value).containsOnly("y", "z"); + } + + static class None { + + void handle() { + } + + } + + @Timed("x") + static class OnMethod { + + @Timed("y") + @Timed("z") + void handle() { + } + + } + + @Timed("y") + @Timed("z") + static class OnBean { + + void handle() { + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/cache/HazelcastCacheMeterBinderProviderTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/cache/HazelcastCacheMeterBinderProviderTests.java index 6bd2b383cf74..08b9a08f590e 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/cache/HazelcastCacheMeterBinderProviderTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/cache/HazelcastCacheMeterBinderProviderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ import java.util.Collections; -import com.hazelcast.core.IMap; +import com.hazelcast.map.IMap; import com.hazelcast.spring.cache.HazelcastCache; import io.micrometer.core.instrument.binder.MeterBinder; import io.micrometer.core.instrument.binder.cache.HazelcastCacheMetrics; diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/cache/RedisCacheMeterBinderProviderTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/cache/RedisCacheMeterBinderProviderTests.java new file mode 100644 index 000000000000..056560268605 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/cache/RedisCacheMeterBinderProviderTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.cache; + +import java.util.Collections; + +import io.micrometer.core.instrument.binder.MeterBinder; +import org.junit.jupiter.api.Test; + +import org.springframework.data.redis.cache.RedisCache; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link RedisCacheMeterBinderProvider}. + * + * @author Stephane Nicoll + */ +class RedisCacheMeterBinderProviderTests { + + @Test + void redisCacheProvider() { + RedisCache cache = mock(RedisCache.class); + given(cache.getName()).willReturn("test"); + MeterBinder meterBinder = new RedisCacheMeterBinderProvider().getMeterBinder(cache, Collections.emptyList()); + assertThat(meterBinder).isInstanceOf(RedisCacheMetrics.class); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/cache/RedisCacheMetricsTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/cache/RedisCacheMetricsTests.java new file mode 100644 index 000000000000..38d6af150cfa --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/cache/RedisCacheMetricsTests.java @@ -0,0 +1,139 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.cache; + +import java.util.UUID; +import java.util.function.BiConsumer; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.Test; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.ContextConsumer; +import org.springframework.boot.testsupport.testcontainers.RedisContainer; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCache; +import org.springframework.data.redis.cache.RedisCacheManager; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link RedisCacheMetrics}. + * + * @author Stephane Nicoll + */ +@Testcontainers(disabledWithoutDocker = true) +class RedisCacheMetricsTests { + + @Container + static final RedisContainer redis = new RedisContainer(); + + private static final Tags TAGS = Tags.of("app", "test").and("cache", "test"); + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class, CacheAutoConfiguration.class)) + .withUserConfiguration(CachingConfiguration.class).withPropertyValues( + "spring.redis.host=" + redis.getHost(), "spring.redis.port=" + redis.getFirstMappedPort(), + "spring.cache.type=redis", "spring.cache.redis.enable-statistics=true"); + + @Test + void cacheStatisticsAreExposed() { + this.contextRunner.run(withCacheMetrics((cache, meterRegistry) -> { + assertThat(meterRegistry.find("cache.size").tags(TAGS).functionCounter()).isNull(); + assertThat(meterRegistry.find("cache.gets").tags(TAGS.and("result", "hit")).functionCounter()).isNotNull(); + assertThat(meterRegistry.find("cache.gets").tags(TAGS.and("result", "miss")).functionCounter()).isNotNull(); + assertThat(meterRegistry.find("cache.gets").tags(TAGS.and("result", "pending")).functionCounter()) + .isNotNull(); + assertThat(meterRegistry.find("cache.evictions").tags(TAGS).functionCounter()).isNull(); + assertThat(meterRegistry.find("cache.puts").tags(TAGS).functionCounter()).isNotNull(); + assertThat(meterRegistry.find("cache.removals").tags(TAGS).functionCounter()).isNotNull(); + assertThat(meterRegistry.find("cache.lock.duration").tags(TAGS).timeGauge()).isNotNull(); + })); + } + + @Test + void cacheHitsAreExposed() { + this.contextRunner.run(withCacheMetrics((cache, meterRegistry) -> { + String key = UUID.randomUUID().toString(); + cache.put(key, "test"); + + cache.get(key); + cache.get(key); + assertThat(meterRegistry.get("cache.gets").tags(TAGS.and("result", "hit")).functionCounter().count()) + .isEqualTo(2.0d); + })); + } + + @Test + void cacheMissesAreExposed() { + this.contextRunner.run(withCacheMetrics((cache, meterRegistry) -> { + String key = UUID.randomUUID().toString(); + cache.get(key); + cache.get(key); + cache.get(key); + assertThat(meterRegistry.get("cache.gets").tags(TAGS.and("result", "miss")).functionCounter().count()) + .isEqualTo(3.0d); + })); + } + + @Test + void cacheMetricsMatchCacheStatistics() { + this.contextRunner.run((context) -> { + RedisCache cache = getTestCache(context); + RedisCacheMetrics cacheMetrics = new RedisCacheMetrics(cache, TAGS); + assertThat(cacheMetrics.hitCount()).isEqualTo(cache.getStatistics().getHits()); + assertThat(cacheMetrics.missCount()).isEqualTo(cache.getStatistics().getMisses()); + assertThat(cacheMetrics.putCount()).isEqualTo(cache.getStatistics().getPuts()); + assertThat(cacheMetrics.size()).isNull(); + assertThat(cacheMetrics.evictionCount()).isNull(); + }); + } + + private ContextConsumer withCacheMetrics( + BiConsumer stats) { + return (context) -> { + RedisCache cache = getTestCache(context); + SimpleMeterRegistry meterRegistry = new SimpleMeterRegistry(); + new RedisCacheMetrics(cache, Tags.of("app", "test")).bindTo(meterRegistry); + stats.accept(cache, meterRegistry); + }; + } + + private RedisCache getTestCache(AssertableApplicationContext context) { + assertThat(context).hasSingleBean(RedisCacheManager.class); + RedisCacheManager cacheManager = context.getBean(RedisCacheManager.class); + RedisCache cache = (RedisCache) cacheManager.getCache("test"); + assertThat(cache).isNotNull(); + return cache; + } + + @Configuration(proxyBeanMethods = false) + @EnableCaching + static class CachingConfiguration { + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/data/DefaultRepositoryTagsProviderTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/data/DefaultRepositoryTagsProviderTests.java new file mode 100644 index 000000000000..14a5efd69fbf --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/data/DefaultRepositoryTagsProviderTests.java @@ -0,0 +1,102 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.data; + +import java.io.IOException; +import java.lang.reflect.Method; + +import io.micrometer.core.instrument.Tag; +import org.junit.jupiter.api.Test; + +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocation; +import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocationResult; +import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocationResult.State; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link DefaultRepositoryTagsProvider}. + * + * @author Phillip Webb + */ +class DefaultRepositoryTagsProviderTests { + + private DefaultRepositoryTagsProvider provider = new DefaultRepositoryTagsProvider(); + + @Test + void repositoryTagsIncludesRepository() { + RepositoryMethodInvocation invocation = createInvocation(); + Iterable tags = this.provider.repositoryTags(invocation); + assertThat(tags).contains(Tag.of("repository", "ExampleRepository")); + } + + @Test + void repositoryTagsIncludesMethod() { + RepositoryMethodInvocation invocation = createInvocation(); + Iterable tags = this.provider.repositoryTags(invocation); + assertThat(tags).contains(Tag.of("method", "findById")); + } + + @Test + void repositoryTagsIncludesState() { + RepositoryMethodInvocation invocation = createInvocation(); + Iterable tags = this.provider.repositoryTags(invocation); + assertThat(tags).contains(Tag.of("state", "SUCCESS")); + } + + @Test + void repositoryTagsIncludesException() { + RepositoryMethodInvocation invocation = createInvocation(new IOException()); + Iterable tags = this.provider.repositoryTags(invocation); + assertThat(tags).contains(Tag.of("exception", "IOException")); + } + + @Test + void repositoryTagsWhenNoExceptionIncludesExceptionTagWithNone() { + RepositoryMethodInvocation invocation = createInvocation(); + Iterable tags = this.provider.repositoryTags(invocation); + assertThat(tags).contains(Tag.of("exception", "None")); + } + + private RepositoryMethodInvocation createInvocation() { + return createInvocation(null); + } + + private RepositoryMethodInvocation createInvocation(Throwable error) { + Class repositoryInterface = ExampleRepository.class; + Method method = ReflectionUtils.findMethod(repositoryInterface, "findById", long.class); + RepositoryMethodInvocationResult result = mock(RepositoryMethodInvocationResult.class); + given(result.getState()).willReturn((error != null) ? State.ERROR : State.SUCCESS); + given(result.getError()).willReturn(error); + return new RepositoryMethodInvocation(repositoryInterface, method, result, 0); + } + + interface ExampleRepository extends Repository { + + Example findById(long id); + + } + + static class Example { + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/data/MetricsRepositoryMethodInvocationListenerTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/data/MetricsRepositoryMethodInvocationListenerTests.java new file mode 100644 index 000000000000..375b1aaabf19 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/data/MetricsRepositoryMethodInvocationListenerTests.java @@ -0,0 +1,123 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.data; + +import java.lang.reflect.Method; + +import io.micrometer.core.annotation.Timed; +import io.micrometer.core.instrument.MockClock; +import io.micrometer.core.instrument.simple.SimpleConfig; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.metrics.AutoTimer; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocation; +import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocationResult; +import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocationResult.State; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link MetricsRepositoryMethodInvocationListener}. + * + * @author Phillip Webb + */ +class MetricsRepositoryMethodInvocationListenerTests { + + private static final String REQUEST_METRICS_NAME = "repository.invocations"; + + private SimpleMeterRegistry registry; + + private MetricsRepositoryMethodInvocationListener listener; + + @BeforeEach + void setup() { + MockClock clock = new MockClock(); + this.registry = new SimpleMeterRegistry(SimpleConfig.DEFAULT, clock); + this.listener = new MetricsRepositoryMethodInvocationListener(() -> this.registry, + new DefaultRepositoryTagsProvider(), REQUEST_METRICS_NAME, AutoTimer.ENABLED); + } + + @Test + void afterInvocationWhenNoTimerAnnotationsAndNoAutoTimerDoesNothing() { + this.listener = new MetricsRepositoryMethodInvocationListener(() -> this.registry, + new DefaultRepositoryTagsProvider(), REQUEST_METRICS_NAME, null); + this.listener.afterInvocation(createInvocation(NoAnnotationsRepository.class)); + assertThat(this.registry.find(REQUEST_METRICS_NAME).timers()).isEmpty(); + } + + @Test + void afterInvocationWhenTimedMethodRecordsMetrics() { + this.listener.afterInvocation(createInvocation(TimedMethodRepository.class)); + assertMetricsContainsTag("state", "SUCCESS"); + assertMetricsContainsTag("tag1", "value1"); + } + + @Test + void afterInvocationWhenTimedClassRecordsMetrics() { + this.listener.afterInvocation(createInvocation(TimedClassRepository.class)); + assertMetricsContainsTag("state", "SUCCESS"); + assertMetricsContainsTag("taga", "valuea"); + } + + @Test + void afterInvocationWhenAutoTimedRecordsMetrics() { + this.listener.afterInvocation(createInvocation(NoAnnotationsRepository.class)); + assertMetricsContainsTag("state", "SUCCESS"); + } + + private void assertMetricsContainsTag(String tagKey, String tagValue) { + assertThat(this.registry.get(REQUEST_METRICS_NAME).tag(tagKey, tagValue).timer().count()).isEqualTo(1); + } + + private RepositoryMethodInvocation createInvocation(Class repositoryInterface) { + Method method = ReflectionUtils.findMethod(repositoryInterface, "findById", long.class); + RepositoryMethodInvocationResult result = mock(RepositoryMethodInvocationResult.class); + given(result.getState()).willReturn(State.SUCCESS); + return new RepositoryMethodInvocation(repositoryInterface, method, result, 0); + } + + interface NoAnnotationsRepository extends Repository { + + Example findById(long id); + + } + + interface TimedMethodRepository extends Repository { + + @Timed(extraTags = { "tag1", "value1" }) + Example findById(long id); + + } + + @Timed(extraTags = { "taga", "valuea" }) + interface TimedClassRepository extends Repository { + + Example findById(long id); + + } + + static class Example { + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusPushGatewayManagerTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusPushGatewayManagerTests.java index 740168a1ca05..7bb3ceef2fb8 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusPushGatewayManagerTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusPushGatewayManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.springframework.boot.actuate.metrics.export.prometheus; -import java.net.UnknownHostException; import java.time.Duration; import java.util.Collections; import java.util.Map; @@ -24,12 +23,12 @@ import io.prometheus.client.CollectorRegistry; import io.prometheus.client.exporter.PushGateway; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusPushGatewayManager.PushGatewayTaskScheduler; import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusPushGatewayManager.ShutdownOperation; @@ -40,17 +39,17 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; 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; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; /** * Tests for {@link PrometheusPushGatewayManager}. * * @author Phillip Webb */ +@ExtendWith(MockitoExtension.class) class PrometheusPushGatewayManagerTests { @Mock @@ -59,6 +58,7 @@ class PrometheusPushGatewayManagerTests { @Mock private CollectorRegistry registry; + @Mock private TaskScheduler scheduler; private Duration pushRate = Duration.ofSeconds(1); @@ -71,12 +71,6 @@ class PrometheusPushGatewayManagerTests { @Mock private ScheduledFuture future; - @BeforeEach - void setup() { - MockitoAnnotations.initMocks(this); - this.scheduler = mockScheduler(TaskScheduler.class); - } - @Test void createWhenPushGatewayIsNullThrowsException() { assertThatIllegalArgumentException().isThrownBy(() -> new PrometheusPushGatewayManager(null, this.registry, @@ -115,79 +109,76 @@ void createWhenJobIsEmptyThrowsException() { void createShouldSchedulePushAsFixedRate() throws Exception { new PrometheusPushGatewayManager(this.pushGateway, this.registry, this.scheduler, this.pushRate, "job", this.groupingKey, null); - verify(this.scheduler).scheduleAtFixedRate(this.task.capture(), eq(this.pushRate)); + then(this.scheduler).should().scheduleAtFixedRate(this.task.capture(), eq(this.pushRate)); this.task.getValue().run(); - verify(this.pushGateway).pushAdd(this.registry, "job", this.groupingKey); + then(this.pushGateway).should().pushAdd(this.registry, "job", this.groupingKey); } @Test void shutdownWhenOwnsSchedulerDoesShutdownScheduler() { - PushGatewayTaskScheduler ownedScheduler = mockScheduler(PushGatewayTaskScheduler.class); + PushGatewayTaskScheduler ownedScheduler = givenScheduleAtFixedRateWillReturnFuture( + mock(PushGatewayTaskScheduler.class)); PrometheusPushGatewayManager manager = new PrometheusPushGatewayManager(this.pushGateway, this.registry, ownedScheduler, this.pushRate, "job", this.groupingKey, null); manager.shutdown(); - verify(ownedScheduler).shutdown(); + then(ownedScheduler).should().shutdown(); } @Test void shutdownWhenDoesNotOwnSchedulerDoesNotShutdownScheduler() { - ThreadPoolTaskScheduler otherScheduler = mockScheduler(ThreadPoolTaskScheduler.class); + ThreadPoolTaskScheduler otherScheduler = givenScheduleAtFixedRateWillReturnFuture( + mock(ThreadPoolTaskScheduler.class)); PrometheusPushGatewayManager manager = new PrometheusPushGatewayManager(this.pushGateway, this.registry, otherScheduler, this.pushRate, "job", this.groupingKey, null); manager.shutdown(); - verify(otherScheduler, never()).shutdown(); + then(otherScheduler).should(never()).shutdown(); } @Test void shutdownWhenShutdownOperationIsPushPerformsPushOnShutdown() throws Exception { + givenScheduleAtFixedRateWithReturnFuture(); PrometheusPushGatewayManager manager = new PrometheusPushGatewayManager(this.pushGateway, this.registry, this.scheduler, this.pushRate, "job", this.groupingKey, ShutdownOperation.PUSH); manager.shutdown(); - verify(this.future).cancel(false); - verify(this.pushGateway).pushAdd(this.registry, "job", this.groupingKey); + then(this.future).should().cancel(false); + then(this.pushGateway).should().pushAdd(this.registry, "job", this.groupingKey); } @Test void shutdownWhenShutdownOperationIsDeletePerformsDeleteOnShutdown() throws Exception { + givenScheduleAtFixedRateWithReturnFuture(); PrometheusPushGatewayManager manager = new PrometheusPushGatewayManager(this.pushGateway, this.registry, this.scheduler, this.pushRate, "job", this.groupingKey, ShutdownOperation.DELETE); manager.shutdown(); - verify(this.future).cancel(false); - verify(this.pushGateway).delete("job", this.groupingKey); + then(this.future).should().cancel(false); + then(this.pushGateway).should().delete("job", this.groupingKey); } @Test void shutdownWhenShutdownOperationIsNoneDoesNothing() { + givenScheduleAtFixedRateWithReturnFuture(); PrometheusPushGatewayManager manager = new PrometheusPushGatewayManager(this.pushGateway, this.registry, this.scheduler, this.pushRate, "job", this.groupingKey, ShutdownOperation.NONE); manager.shutdown(); - verify(this.future).cancel(false); - verifyNoInteractions(this.pushGateway); - } - - @Test - void pushWhenUnknownHostExceptionIsThrownDoesShutdown() throws Exception { - new PrometheusPushGatewayManager(this.pushGateway, this.registry, this.scheduler, this.pushRate, "job", - this.groupingKey, null); - verify(this.scheduler).scheduleAtFixedRate(this.task.capture(), eq(this.pushRate)); - willThrow(new UnknownHostException("foo")).given(this.pushGateway).pushAdd(this.registry, "job", - this.groupingKey); - this.task.getValue().run(); - verify(this.future).cancel(false); + then(this.future).should().cancel(false); + then(this.pushGateway).shouldHaveNoInteractions(); } @Test void pushDoesNotThrowException() throws Exception { new PrometheusPushGatewayManager(this.pushGateway, this.registry, this.scheduler, this.pushRate, "job", this.groupingKey, null); - verify(this.scheduler).scheduleAtFixedRate(this.task.capture(), eq(this.pushRate)); + then(this.scheduler).should().scheduleAtFixedRate(this.task.capture(), eq(this.pushRate)); willThrow(RuntimeException.class).given(this.pushGateway).pushAdd(this.registry, "job", this.groupingKey); this.task.getValue().run(); } + private void givenScheduleAtFixedRateWithReturnFuture() { + givenScheduleAtFixedRateWillReturnFuture(this.scheduler); + } + @SuppressWarnings({ "unchecked", "rawtypes" }) - private T mockScheduler(Class type) { - T scheduler = mock(type); + private T givenScheduleAtFixedRateWillReturnFuture(T scheduler) { given(scheduler.scheduleAtFixedRate(isA(Runnable.class), isA(Duration.class))) .willReturn((ScheduledFuture) this.future); return scheduler; diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpointIntegrationTests.java index 6c3e3243409b..1265d82106ff 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpointIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.boot.actuate.metrics.export.prometheus; import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.prometheus.PrometheusMeterRegistry; import io.prometheus.client.CollectorRegistry; @@ -28,17 +29,59 @@ import org.springframework.http.MediaType; import org.springframework.test.web.reactive.server.WebTestClient; +import static org.assertj.core.api.Assertions.assertThat; + /** * Tests for {@link PrometheusScrapeEndpoint}. * * @author Jon Schneider + * @author Johnny Lim */ class PrometheusScrapeEndpointIntegrationTests { @WebEndpointTest - void scrapeHasContentTypeText004(WebTestClient client) { + void scrapeHasContentTypeText004ByDefault(WebTestClient client) { + String expectedContentType = TextFormat.CONTENT_TYPE_004; + assertThat(TextFormat.chooseContentType(null)).isEqualTo(expectedContentType); client.get().uri("/actuator/prometheus").exchange().expectStatus().isOk().expectHeader() - .contentType(MediaType.parseMediaType(TextFormat.CONTENT_TYPE_004)); + .contentType(MediaType.parseMediaType(expectedContentType)).expectBody(String.class) + .value((body) -> assertThat(body).contains("counter1_total").contains("counter2_total") + .contains("counter3_total")); + } + + @WebEndpointTest + void scrapeHasContentTypeText004ByDefaultWhenClientAcceptsWildcardWithParameter(WebTestClient client) { + String expectedContentType = TextFormat.CONTENT_TYPE_004; + String accept = "*/*;q=0.8"; + assertThat(TextFormat.chooseContentType(accept)).isEqualTo(expectedContentType); + client.get().uri("/actuator/prometheus").accept(MediaType.parseMediaType(accept)).exchange().expectStatus() + .isOk().expectHeader().contentType(MediaType.parseMediaType(expectedContentType)) + .expectBody(String.class).value((body) -> assertThat(body).contains("counter1_total") + .contains("counter2_total").contains("counter3_total")); + } + + @WebEndpointTest + void scrapeCanProduceOpenMetrics100(WebTestClient client) { + MediaType openMetrics = MediaType.parseMediaType(TextFormat.CONTENT_TYPE_OPENMETRICS_100); + client.get().uri("/actuator/prometheus").accept(openMetrics).exchange().expectStatus().isOk().expectHeader() + .contentType(openMetrics).expectBody(String.class).value((body) -> assertThat(body) + .contains("counter1_total").contains("counter2_total").contains("counter3_total")); + } + + @WebEndpointTest + void scrapePrefersToProduceOpenMetrics100(WebTestClient client) { + MediaType openMetrics = MediaType.parseMediaType(TextFormat.CONTENT_TYPE_OPENMETRICS_100); + MediaType textPlain = MediaType.parseMediaType(TextFormat.CONTENT_TYPE_004); + client.get().uri("/actuator/prometheus").accept(openMetrics, textPlain).exchange().expectStatus().isOk() + .expectHeader().contentType(openMetrics); + } + + @WebEndpointTest + void scrapeWithIncludedNames(WebTestClient client) { + client.get().uri("/actuator/prometheus?includedNames=counter1_total,counter2_total").exchange().expectStatus() + .isOk().expectHeader().contentType(MediaType.parseMediaType(TextFormat.CONTENT_TYPE_004)) + .expectBody(String.class).value((body) -> assertThat(body).contains("counter1_total") + .contains("counter2_total").doesNotContain("counter3_total")); } @Configuration(proxyBeanMethods = false) @@ -56,7 +99,11 @@ CollectorRegistry collectorRegistry() { @Bean MeterRegistry registry(CollectorRegistry registry) { - return new PrometheusMeterRegistry((k) -> null, registry, Clock.SYSTEM); + PrometheusMeterRegistry meterRegistry = new PrometheusMeterRegistry((k) -> null, registry, Clock.SYSTEM); + Counter.builder("counter1").register(meterRegistry); + Counter.builder("counter2").register(meterRegistry); + Counter.builder("counter3").register(meterRegistry); + return meterRegistry; } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/http/OutcomeTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/http/OutcomeTests.java index a07d3250730e..c7dc98de9284 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/http/OutcomeTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/http/OutcomeTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ * * @author Andy Wilkinson */ -public class OutcomeTests { +class OutcomeTests { @Test void outcomeForInformationalStatusIsInformational() { diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/r2dbc/ConnectionPoolMetricsTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/r2dbc/ConnectionPoolMetricsTests.java new file mode 100644 index 000000000000..e41e18eb3f27 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/r2dbc/ConnectionPoolMetricsTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.r2dbc; + +import java.util.Collections; +import java.util.UUID; + +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.r2dbc.h2.CloseableConnectionFactory; +import io.r2dbc.h2.H2ConnectionFactory; +import io.r2dbc.h2.H2ConnectionOption; +import io.r2dbc.pool.ConnectionPool; +import io.r2dbc.pool.ConnectionPoolConfiguration; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConnectionPoolMetrics}. + * + * @author Tadaya Tsuyukubo + * @author Mark Paluch + * @author Stephane Nicoll + */ +class ConnectionPoolMetricsTests { + + private static final Tag testTag = Tag.of("test", "yes"); + + private static final Tag regionTag = Tag.of("region", "eu-2"); + + private CloseableConnectionFactory connectionFactory; + + @BeforeEach + void init() { + this.connectionFactory = H2ConnectionFactory.inMemory("db-" + UUID.randomUUID(), "sa", "", + Collections.singletonMap(H2ConnectionOption.DB_CLOSE_DELAY, "-1")); + } + + @AfterEach + void close() { + if (this.connectionFactory != null) { + StepVerifier.create(this.connectionFactory.close()).verifyComplete(); + } + } + + @Test + void connectionFactoryIsInstrumented() { + SimpleMeterRegistry registry = new SimpleMeterRegistry(); + ConnectionPool connectionPool = new ConnectionPool( + ConnectionPoolConfiguration.builder(this.connectionFactory).initialSize(3).maxSize(7).build()); + ConnectionPoolMetrics metrics = new ConnectionPoolMetrics(connectionPool, "test-pool", + Tags.of(testTag, regionTag)); + metrics.bindTo(registry); + // acquire two connections + connectionPool.create().as(StepVerifier::create).expectNextCount(1).verifyComplete(); + connectionPool.create().as(StepVerifier::create).expectNextCount(1).verifyComplete(); + assertGauge(registry, "r2dbc.pool.acquired", 2); + assertGauge(registry, "r2dbc.pool.allocated", 3); + assertGauge(registry, "r2dbc.pool.idle", 1); + assertGauge(registry, "r2dbc.pool.pending", 0); + assertGauge(registry, "r2dbc.pool.max.allocated", 7); + assertGauge(registry, "r2dbc.pool.max.pending", Integer.MAX_VALUE); + } + + private void assertGauge(SimpleMeterRegistry registry, String metric, int expectedValue) { + Gauge gauge = registry.get(metric).gauge(); + assertThat(gauge.value()).isEqualTo(expectedValue); + assertThat(gauge.getId().getTags()).containsExactlyInAnyOrder(Tag.of("name", "test-pool"), testTag, regionTag); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/client/MetricsRestTemplateCustomizerTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/client/MetricsRestTemplateCustomizerTests.java index e996d4d763bf..697f2a133a8c 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/client/MetricsRestTemplateCustomizerTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/client/MetricsRestTemplateCustomizerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,20 +16,32 @@ package org.springframework.boot.actuate.metrics.web.client; +import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.util.concurrent.atomic.AtomicBoolean; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.MockClock; import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Timer.Builder; import io.micrometer.core.instrument.simple.SimpleConfig; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.metrics.AutoTimer; +import org.springframework.boot.test.web.client.LocalHostUriTemplateHandler; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.boot.web.client.RootUriTemplateHandler; import org.springframework.http.HttpMethod; +import org.springframework.http.HttpRequest; import org.springframework.http.MediaType; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.mock.env.MockEnvironment; import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.test.web.client.match.MockRestRequestMatchers; import org.springframework.test.web.client.response.MockRestResponseCreators; @@ -107,4 +119,89 @@ void interceptRestTemplateWithUri() throws URISyntaxException { this.mockServer.verify(); } + @Test + void interceptNestedRequest() { + this.mockServer.expect(MockRestRequestMatchers.requestTo("/test/123")) + .andExpect(MockRestRequestMatchers.method(HttpMethod.GET)) + .andRespond(MockRestResponseCreators.withSuccess("OK", MediaType.APPLICATION_JSON)); + + RestTemplate nestedRestTemplate = new RestTemplate(); + MockRestServiceServer nestedMockServer = MockRestServiceServer.createServer(nestedRestTemplate); + nestedMockServer.expect(MockRestRequestMatchers.requestTo("/nestedTest/124")) + .andExpect(MockRestRequestMatchers.method(HttpMethod.GET)) + .andRespond(MockRestResponseCreators.withSuccess("OK", MediaType.APPLICATION_JSON)); + this.customizer.customize(nestedRestTemplate); + + TestInterceptor testInterceptor = new TestInterceptor(nestedRestTemplate); + this.restTemplate.getInterceptors().add(testInterceptor); + + this.restTemplate.getForObject("/test/{id}", String.class, 123); + this.registry.get("http.client.requests").tags("uri", "/test/{id}").timer(); + this.registry.get("http.client.requests").tags("uri", "/nestedTest/{nestedId}").timer(); + + this.mockServer.verify(); + nestedMockServer.verify(); + } + + @Test + void whenCustomizerAndLocalHostUriTemplateHandlerAreUsedTogetherThenRestTemplateBuilderCanBuild() { + MockEnvironment environment = new MockEnvironment(); + environment.setProperty("local.server.port", "8443"); + LocalHostUriTemplateHandler uriTemplateHandler = new LocalHostUriTemplateHandler(environment, "https"); + RestTemplate restTemplate = new RestTemplateBuilder(this.customizer).uriTemplateHandler(uriTemplateHandler) + .build(); + assertThat(restTemplate.getUriTemplateHandler()) + .asInstanceOf(InstanceOfAssertFactories.type(RootUriTemplateHandler.class)) + .extracting(RootUriTemplateHandler::getRootUri).isEqualTo("https://localhost:8443"); + } + + @Test + void whenAutoTimingIsDisabledUriTemplateHandlerDoesNotCaptureUris() { + AtomicBoolean enabled = new AtomicBoolean(false); + AutoTimer autoTimer = new AutoTimer() { + + @Override + public boolean isEnabled() { + return enabled.get(); + } + + @Override + public void apply(Builder builder) { + } + + }; + RestTemplate restTemplate = new RestTemplateBuilder(new MetricsRestTemplateCustomizer(this.registry, + new DefaultRestTemplateExchangeTagsProvider(), "http.client.requests", autoTimer)).build(); + MockRestServiceServer mockServer = MockRestServiceServer.createServer(restTemplate); + mockServer.expect(MockRestRequestMatchers.requestTo("/first/123")) + .andExpect(MockRestRequestMatchers.method(HttpMethod.GET)) + .andRespond(MockRestResponseCreators.withSuccess("OK", MediaType.APPLICATION_JSON)); + mockServer.expect(MockRestRequestMatchers.requestTo("/second/456")) + .andExpect(MockRestRequestMatchers.method(HttpMethod.GET)) + .andRespond(MockRestResponseCreators.withSuccess("OK", MediaType.APPLICATION_JSON)); + assertThat(restTemplate.getForObject("/first/{id}", String.class, 123)).isEqualTo("OK"); + assertThat(this.registry.find("http.client.requests").timer()).isNull(); + enabled.set(true); + assertThat(restTemplate.getForObject(URI.create("/second/456"), String.class)).isEqualTo("OK"); + this.registry.get("http.client.requests").tags("uri", "/second/456").timer(); + this.mockServer.verify(); + } + + private static final class TestInterceptor implements ClientHttpRequestInterceptor { + + private final RestTemplate restTemplate; + + private TestInterceptor(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + @Override + public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) + throws IOException { + this.restTemplate.getForObject("/nestedTest/{nestedId}", String.class, 124); + return execution.execute(request, body); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/DefaultWebClientExchangeTagsProviderTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/DefaultWebClientExchangeTagsProviderTests.java index 0d81f8a25fb7..5358fd357762 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/DefaultWebClientExchangeTagsProviderTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/DefaultWebClientExchangeTagsProviderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -87,4 +87,11 @@ void tagsWhenExceptionShouldReturnClientErrorStatus() { Tag.of("clientName", "example.org"), Tag.of("status", "CLIENT_ERROR"), Tag.of("outcome", "UNKNOWN")); } + @Test + void tagsWhenCancelledRequestShouldReturnClientErrorStatus() { + Iterable tags = this.tagsProvider.tags(this.request, null, null); + assertThat(tags).containsExactlyInAnyOrder(Tag.of("method", "GET"), Tag.of("uri", "/projects/{project}"), + Tag.of("clientName", "example.org"), Tag.of("status", "CLIENT_ERROR"), Tag.of("outcome", "UNKNOWN")); + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientFilterFunctionTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientFilterFunctionTests.java index ee58df4850da..b651560ec57c 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientFilterFunctionTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientFilterFunctionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,11 +24,13 @@ import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.MockClock; import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.search.MeterNotFoundException; import io.micrometer.core.instrument.simple.SimpleConfig; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; import org.springframework.boot.actuate.metrics.AutoTimer; import org.springframework.http.HttpMethod; @@ -39,6 +41,7 @@ import org.springframework.web.reactive.function.client.WebClient; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -73,7 +76,7 @@ void filterShouldRecordTimer() { ClientRequest request = ClientRequest .create(HttpMethod.GET, URI.create("https://example.com/projects/spring-boot")).build(); given(this.response.rawStatusCode()).willReturn(HttpStatus.OK.value()); - this.filterFunction.filter(request, this.exchange).block(Duration.ofSeconds(30)); + this.filterFunction.filter(request, this.exchange).block(Duration.ofSeconds(5)); assertThat(this.registry.get("http.client.requests") .tags("method", "GET", "uri", "/projects/spring-boot", "status", "200").timer().count()).isEqualTo(1); } @@ -84,7 +87,7 @@ void filterWhenUriTemplatePresentShouldRecordTimer() { .create(HttpMethod.GET, URI.create("https://example.com/projects/spring-boot")) .attribute(URI_TEMPLATE_ATTRIBUTE, "/projects/{project}").build(); given(this.response.rawStatusCode()).willReturn(HttpStatus.OK.value()); - this.filterFunction.filter(request, this.exchange).block(Duration.ofSeconds(30)); + this.filterFunction.filter(request, this.exchange).block(Duration.ofSeconds(5)); assertThat(this.registry.get("http.client.requests") .tags("method", "GET", "uri", "/projects/{project}", "status", "200").timer().count()).isEqualTo(1); } @@ -95,7 +98,7 @@ void filterWhenIoExceptionThrownShouldRecordTimer() { .create(HttpMethod.GET, URI.create("https://example.com/projects/spring-boot")).build(); ExchangeFunction errorExchange = (r) -> Mono.error(new IOException()); this.filterFunction.filter(request, errorExchange).onErrorResume(IOException.class, (t) -> Mono.empty()) - .block(Duration.ofSeconds(30)); + .block(Duration.ofSeconds(5)); assertThat(this.registry.get("http.client.requests") .tags("method", "GET", "uri", "/projects/spring-boot", "status", "IO_ERROR").timer().count()) .isEqualTo(1); @@ -107,24 +110,53 @@ void filterWhenExceptionThrownShouldRecordTimer() { .create(HttpMethod.GET, URI.create("https://example.com/projects/spring-boot")).build(); ExchangeFunction exchange = (r) -> Mono.error(new IllegalArgumentException()); this.filterFunction.filter(request, exchange).onErrorResume(IllegalArgumentException.class, (t) -> Mono.empty()) - .block(Duration.ofSeconds(30)); + .block(Duration.ofSeconds(5)); assertThat(this.registry.get("http.client.requests") .tags("method", "GET", "uri", "/projects/spring-boot", "status", "CLIENT_ERROR").timer().count()) .isEqualTo(1); } @Test - void filterWhenExceptionAndRetryShouldNotCumulateRecordTime() { + void filterWhenCancelThrownShouldRecordTimer() { + ClientRequest request = ClientRequest + .create(HttpMethod.GET, URI.create("https://example.com/projects/spring-boot")).build(); + given(this.response.rawStatusCode()).willReturn(HttpStatus.OK.value()); + Mono filter = this.filterFunction.filter(request, this.exchange); + StepVerifier.create(filter).thenCancel().verify(Duration.ofSeconds(5)); + assertThat(this.registry.get("http.client.requests") + .tags("method", "GET", "uri", "/projects/spring-boot", "status", "CLIENT_ERROR").timer().count()) + .isEqualTo(1); + assertThatThrownBy(() -> this.registry.get("http.client.requests") + .tags("method", "GET", "uri", "/projects/spring-boot", "status", "200").timer()) + .isInstanceOf(MeterNotFoundException.class); + } + + @Test + void filterWhenCancelAfterResponseThrownShouldNotRecordTimer() { + ClientRequest request = ClientRequest + .create(HttpMethod.GET, URI.create("https://example.com/projects/spring-boot")).build(); + given(this.response.rawStatusCode()).willReturn(HttpStatus.OK.value()); + Mono filter = this.filterFunction.filter(request, this.exchange); + StepVerifier.create(filter).expectNextCount(1).thenCancel().verify(Duration.ofSeconds(5)); + assertThat(this.registry.get("http.client.requests") + .tags("method", "GET", "uri", "/projects/spring-boot", "status", "200").timer().count()).isEqualTo(1); + assertThatThrownBy(() -> this.registry.get("http.client.requests") + .tags("method", "GET", "uri", "/projects/spring-boot", "status", "CLIENT_ERROR").timer()) + .isInstanceOf(MeterNotFoundException.class); + } + + @Test + void filterWhenExceptionAndRetryShouldNotAccumulateRecordTime() { ClientRequest request = ClientRequest .create(HttpMethod.GET, URI.create("https://example.com/projects/spring-boot")).build(); ExchangeFunction exchange = (r) -> Mono.error(new IllegalArgumentException()) - .delaySubscription(Duration.ofMillis(300)).cast(ClientResponse.class); + .delaySubscription(Duration.ofMillis(1000)).cast(ClientResponse.class); this.filterFunction.filter(request, exchange).retry(1) - .onErrorResume(IllegalArgumentException.class, (t) -> Mono.empty()).block(Duration.ofSeconds(30)); + .onErrorResume(IllegalArgumentException.class, (t) -> Mono.empty()).block(Duration.ofSeconds(5)); Timer timer = this.registry.get("http.client.requests") .tags("method", "GET", "uri", "/projects/spring-boot", "status", "CLIENT_ERROR").timer(); assertThat(timer.count()).isEqualTo(2); - assertThat(timer.max(TimeUnit.MILLISECONDS)).isLessThan(600); + assertThat(timer.max(TimeUnit.MILLISECONDS)).isLessThan(2000); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTagsTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTagsTests.java index 94fa8dc8c7ee..3ba0a4eed9e8 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTagsTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTagsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -78,6 +78,14 @@ void uriWhenTemplateIsMissingShouldReturnPath() { assertThat(WebClientExchangeTags.uri(this.request)).isEqualTo(Tag.of("uri", "/projects/spring-boot")); } + @Test + void uriWhenTemplateIsMissingShouldReturnPathWithQueryParams() { + this.request = ClientRequest + .create(HttpMethod.GET, URI.create("https://example.org/projects/spring-boot?section=docs")).build(); + assertThat(WebClientExchangeTags.uri(this.request)) + .isEqualTo(Tag.of("uri", "/projects/spring-boot?section=docs")); + } + @Test void clientName() { assertThat(WebClientExchangeTags.clientName(this.request)).isEqualTo(Tag.of("clientName", "example.org")); @@ -86,24 +94,29 @@ void clientName() { @Test void status() { given(this.response.rawStatusCode()).willReturn(HttpStatus.OK.value()); - assertThat(WebClientExchangeTags.status(this.response)).isEqualTo(Tag.of("status", "200")); + assertThat(WebClientExchangeTags.status(this.response, null)).isEqualTo(Tag.of("status", "200")); } @Test void statusWhenIOException() { - assertThat(WebClientExchangeTags.status(new IOException())).isEqualTo(Tag.of("status", "IO_ERROR")); + assertThat(WebClientExchangeTags.status(null, new IOException())).isEqualTo(Tag.of("status", "IO_ERROR")); } @Test void statusWhenClientException() { - assertThat(WebClientExchangeTags.status(new IllegalArgumentException())) + assertThat(WebClientExchangeTags.status(null, new IllegalArgumentException())) .isEqualTo(Tag.of("status", "CLIENT_ERROR")); } @Test void statusWhenNonStandard() { given(this.response.rawStatusCode()).willReturn(490); - assertThat(WebClientExchangeTags.status(this.response)).isEqualTo(Tag.of("status", "490")); + assertThat(WebClientExchangeTags.status(this.response, null)).isEqualTo(Tag.of("status", "490")); + } + + @Test + void statusWhenCancelled() { + assertThat(WebClientExchangeTags.status(null, null)).isEqualTo(Tag.of("status", "CLIENT_ERROR")); } @Test diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/server/DefaultWebFluxTagsProviderTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/server/DefaultWebFluxTagsProviderTests.java new file mode 100644 index 000000000000..ae2138346c40 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/server/DefaultWebFluxTagsProviderTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.web.reactive.server; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import io.micrometer.core.instrument.Tag; +import org.junit.jupiter.api.Test; + +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.web.server.ServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DefaultWebFluxTagsProvider}. + * + * @author Andy Wilkinson + */ +class DefaultWebFluxTagsProviderTests { + + @Test + void whenTagsAreProvidedThenDefaultTagsArePresent() { + ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/test")); + Map tags = asMap(new DefaultWebFluxTagsProvider().httpRequestTags(exchange, null)); + assertThat(tags).containsOnlyKeys("exception", "method", "outcome", "status", "uri"); + } + + @Test + void givenSomeContributorsWhenTagsAreProvidedThenDefaultTagsAndContributedTagsArePresent() { + ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/test")); + Map tags = asMap( + new DefaultWebFluxTagsProvider(Arrays.asList(new TestWebFluxTagsContributor("alpha"), + new TestWebFluxTagsContributor("bravo", "charlie"))).httpRequestTags(exchange, null)); + assertThat(tags).containsOnlyKeys("exception", "method", "outcome", "status", "uri", "alpha", "bravo", + "charlie"); + } + + private Map asMap(Iterable tags) { + return StreamSupport.stream(tags.spliterator(), false) + .collect(Collectors.toMap(Tag::getKey, Function.identity())); + } + + private static final class TestWebFluxTagsContributor implements WebFluxTagsContributor { + + private final List tagNames; + + private TestWebFluxTagsContributor(String... tagNames) { + this.tagNames = Arrays.asList(tagNames); + } + + @Override + public Iterable httpRequestTags(ServerWebExchange exchange, Throwable ex) { + return this.tagNames.stream().map((name) -> Tag.of(name, "value")).collect(Collectors.toList()); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/server/MetricsWebFilterTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/server/MetricsWebFilterTests.java index 33f1e141260c..c67954be72a0 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/server/MetricsWebFilterTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/server/MetricsWebFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,19 +16,28 @@ package org.springframework.boot.actuate.metrics.web.reactive.server; +import java.io.EOFException; import java.time.Duration; +import java.util.concurrent.atomic.AtomicBoolean; +import io.micrometer.core.annotation.Timed; import io.micrometer.core.instrument.MockClock; +import io.micrometer.core.instrument.Tag; import io.micrometer.core.instrument.simple.SimpleConfig; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; import org.springframework.boot.actuate.metrics.AutoTimer; +import org.springframework.boot.web.reactive.error.ErrorAttributes; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.server.ServerWebExchange; import org.springframework.web.util.pattern.PathPatternParser; import static org.assertj.core.api.Assertions.assertThat; @@ -37,11 +46,16 @@ * Tests for {@link MetricsWebFilter} * * @author Brian Clozel + * @author Madhura Bhave */ class MetricsWebFilterTests { private static final String REQUEST_METRICS_NAME = "http.server.requests"; + private static final String REQUEST_METRICS_NAME_PERCENTILE = REQUEST_METRICS_NAME + ".percentile"; + + private final FaultyWebFluxTagsProvider tagsProvider = new FaultyWebFluxTagsProvider(); + private SimpleMeterRegistry registry; private MetricsWebFilter webFilter; @@ -50,7 +64,7 @@ class MetricsWebFilterTests { void setup() { MockClock clock = new MockClock(); this.registry = new SimpleMeterRegistry(SimpleConfig.DEFAULT, clock); - this.webFilter = new MetricsWebFilter(this.registry, new DefaultWebFluxTagsProvider(), REQUEST_METRICS_NAME, + this.webFilter = new MetricsWebFilter(this.registry, this.tagsProvider, REQUEST_METRICS_NAME, AutoTimer.ENABLED); } @@ -68,7 +82,7 @@ void filterAddsTagsToRegistryForExceptions() { MockServerWebExchange exchange = createExchange("/projects/spring-boot", "/projects/{project}"); this.webFilter.filter(exchange, (serverWebExchange) -> Mono.error(new IllegalStateException("test error"))) .onErrorResume((t) -> { - exchange.getResponse().setStatusCodeValue(500); + exchange.getResponse().setRawStatusCode(500); return exchange.getResponse().setComplete(); }).block(Duration.ofSeconds(30)); assertMetricsContainsTag("uri", "/projects/{project}"); @@ -83,7 +97,7 @@ void filterAddsNonEmptyTagsToRegistryForAnonymousExceptions() { MockServerWebExchange exchange = createExchange("/projects/spring-boot", "/projects/{project}"); this.webFilter.filter(exchange, (serverWebExchange) -> Mono.error(anonymous)).onErrorResume((t) -> { - exchange.getResponse().setStatusCodeValue(500); + exchange.getResponse().setRawStatusCode(500); return exchange.getResponse().setComplete(); }).block(Duration.ofSeconds(30)); assertMetricsContainsTag("uri", "/projects/{project}"); @@ -91,17 +105,129 @@ void filterAddsNonEmptyTagsToRegistryForAnonymousExceptions() { assertMetricsContainsTag("exception", anonymous.getClass().getName()); } + @Test + void filterAddsTagsToRegistryForHandledExceptions() { + MockServerWebExchange exchange = createExchange("/projects/spring-boot", "/projects/{project}"); + this.webFilter.filter(exchange, (serverWebExchange) -> { + exchange.getAttributes().put(ErrorAttributes.ERROR_ATTRIBUTE, new IllegalStateException("test error")); + return exchange.getResponse().setComplete(); + }).block(Duration.ofSeconds(30)); + assertMetricsContainsTag("uri", "/projects/{project}"); + assertMetricsContainsTag("status", "200"); + assertMetricsContainsTag("exception", "IllegalStateException"); + } + @Test void filterAddsTagsToRegistryForExceptionsAndCommittedResponse() { MockServerWebExchange exchange = createExchange("/projects/spring-boot", "/projects/{project}"); this.webFilter.filter(exchange, (serverWebExchange) -> { - exchange.getResponse().setStatusCodeValue(500); + exchange.getResponse().setRawStatusCode(500); return exchange.getResponse().setComplete().then(Mono.error(new IllegalStateException("test error"))); }).onErrorResume((t) -> Mono.empty()).block(Duration.ofSeconds(30)); assertMetricsContainsTag("uri", "/projects/{project}"); assertMetricsContainsTag("status", "500"); } + @Test + void trailingSlashShouldNotRecordDuplicateMetrics() { + MockServerWebExchange exchange1 = createExchange("/projects/spring-boot", "/projects/{project}"); + MockServerWebExchange exchange2 = createExchange("/projects/spring-boot", "/projects/{project}/"); + this.webFilter.filter(exchange1, (serverWebExchange) -> exchange1.getResponse().setComplete()) + .block(Duration.ofSeconds(30)); + this.webFilter.filter(exchange2, (serverWebExchange) -> exchange2.getResponse().setComplete()) + .block(Duration.ofSeconds(30)); + assertThat(this.registry.get(REQUEST_METRICS_NAME).tag("uri", "/projects/{project}").timer().count()) + .isEqualTo(2); + assertThat(this.registry.get(REQUEST_METRICS_NAME).tag("status", "200").timer().count()).isEqualTo(2); + } + + @Test + void cancelledConnectionsShouldProduceMetrics() { + MockServerWebExchange exchange = createExchange("/projects/spring-boot", "/projects/{project}"); + Mono processing = this.webFilter.filter(exchange, + (serverWebExchange) -> exchange.getResponse().setComplete()); + StepVerifier.create(processing).thenCancel().verify(Duration.ofSeconds(5)); + assertMetricsContainsTag("uri", "/projects/{project}"); + assertMetricsContainsTag("status", "200"); + assertMetricsContainsTag("outcome", "UNKNOWN"); + } + + @Test + void disconnectedExceptionShouldProduceMetrics() { + MockServerWebExchange exchange = createExchange("/projects/spring-boot", "/projects/{project}"); + Mono processing = this.webFilter + .filter(exchange, (serverWebExchange) -> Mono.error(new EOFException("Disconnected"))) + .onErrorResume((t) -> { + exchange.getResponse().setRawStatusCode(500); + return exchange.getResponse().setComplete(); + }); + StepVerifier.create(processing).expectComplete().verify(Duration.ofSeconds(5)); + assertMetricsContainsTag("uri", "/projects/{project}"); + assertMetricsContainsTag("status", "500"); + assertMetricsContainsTag("outcome", "UNKNOWN"); + } + + @Test + void filterAddsStandardTags() { + MockServerWebExchange exchange = createTimedHandlerMethodExchange("timed"); + this.webFilter.filter(exchange, (serverWebExchange) -> exchange.getResponse().setComplete()) + .block(Duration.ofSeconds(30)); + assertMetricsContainsTag("uri", "/projects/{project}"); + assertMetricsContainsTag("status", "200"); + } + + @Test + void filterAddsExtraTags() { + MockServerWebExchange exchange = createTimedHandlerMethodExchange("timedExtraTags"); + this.webFilter.filter(exchange, (serverWebExchange) -> exchange.getResponse().setComplete()) + .block(Duration.ofSeconds(30)); + assertMetricsContainsTag("uri", "/projects/{project}"); + assertMetricsContainsTag("status", "200"); + assertMetricsContainsTag("tag1", "value1"); + assertMetricsContainsTag("tag2", "value2"); + } + + @Test + void filterAddsExtraTagsAndException() { + MockServerWebExchange exchange = createTimedHandlerMethodExchange("timedExtraTags"); + this.webFilter.filter(exchange, (serverWebExchange) -> Mono.error(new IllegalStateException("test error"))) + .onErrorResume((ex) -> { + exchange.getResponse().setRawStatusCode(500); + return exchange.getResponse().setComplete(); + }).block(Duration.ofSeconds(30)); + assertMetricsContainsTag("uri", "/projects/{project}"); + assertMetricsContainsTag("status", "500"); + assertMetricsContainsTag("exception", "IllegalStateException"); + assertMetricsContainsTag("tag1", "value1"); + assertMetricsContainsTag("tag2", "value2"); + } + + @Test + void filterAddsPercentileMeters() { + MockServerWebExchange exchange = createTimedHandlerMethodExchange("timedPercentiles"); + this.webFilter.filter(exchange, (serverWebExchange) -> exchange.getResponse().setComplete()) + .block(Duration.ofSeconds(30)); + assertMetricsContainsTag("uri", "/projects/{project}"); + assertMetricsContainsTag("status", "200"); + assertThat(this.registry.get(REQUEST_METRICS_NAME_PERCENTILE).tag("phi", "0.95").gauge().value()).isNotZero(); + assertThat(this.registry.get(REQUEST_METRICS_NAME_PERCENTILE).tag("phi", "0.5").gauge().value()).isNotZero(); + } + + @Test + void whenMetricsRecordingFailsThenExchangeFilteringSucceeds() { + MockServerWebExchange exchange = createExchange("/projects/spring-boot", "/projects/{project}"); + this.tagsProvider.failOnce(); + this.webFilter.filter(exchange, (serverWebExchange) -> exchange.getResponse().setComplete()) + .block(Duration.ofSeconds(30)); + } + + private MockServerWebExchange createTimedHandlerMethodExchange(String methodName) { + MockServerWebExchange exchange = createExchange("/projects/spring-boot", "/projects/{project}"); + exchange.getAttributes().put(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE, + new HandlerMethod(this, ReflectionUtils.findMethod(Handlers.class, methodName))); + return exchange; + } + private MockServerWebExchange createExchange(String path, String pathPattern) { PathPatternParser parser = new PathPatternParser(); MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get(path).build()); @@ -113,4 +239,45 @@ private void assertMetricsContainsTag(String tagKey, String tagValue) { assertThat(this.registry.get(REQUEST_METRICS_NAME).tag(tagKey, tagValue).timer().count()).isEqualTo(1); } + static class Handlers { + + @Timed + Mono timed() { + return Mono.just("test"); + } + + @Timed(extraTags = { "tag1", "value1", "tag2", "value2" }) + Mono timedExtraTags() { + return Mono.just("test"); + } + + @Timed(percentiles = { 0.5, 0.95 }) + Mono timedPercentiles() { + return Mono.just("test"); + } + + } + + class FaultyWebFluxTagsProvider extends DefaultWebFluxTagsProvider { + + private final AtomicBoolean fail = new AtomicBoolean(false); + + FaultyWebFluxTagsProvider() { + super(true); + } + + @Override + public Iterable httpRequestTags(ServerWebExchange exchange, Throwable exception) { + if (this.fail.compareAndSet(true, false)) { + throw new RuntimeException(); + } + return super.httpRequestTags(exchange, exception); + } + + void failOnce() { + this.fail.set(true); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsTests.java index b71348519189..7273b9b0f457 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,15 @@ package org.springframework.boot.actuate.metrics.web.reactive.server; +import java.io.EOFException; + import io.micrometer.core.instrument.Tag; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.web.server.MockServerWebExchange; import org.springframework.web.reactive.HandlerMapping; @@ -37,12 +40,14 @@ * * @author Brian Clozel * @author Michael McFadyen + * @author Madhura Bhave + * @author Stephane Nicoll */ class WebFluxTagsTests { private MockServerWebExchange exchange; - private PathPatternParser parser = new PathPatternParser(); + private final PathPatternParser parser = new PathPatternParser(); @BeforeEach void setup() { @@ -51,12 +56,36 @@ void setup() { @Test void uriTagValueIsBestMatchingPatternWhenAvailable() { - this.exchange.getAttributes().put(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, this.parser.parse("/spring")); + this.exchange.getAttributes().put(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, + this.parser.parse("/spring/")); + this.exchange.getResponse().setStatusCode(HttpStatus.MOVED_PERMANENTLY); + Tag tag = WebFluxTags.uri(this.exchange); + assertThat(tag.getValue()).isEqualTo("/spring/"); + } + + @Test + void uriTagValueIsRootWhenBestMatchingPatternIsEmpty() { + this.exchange.getAttributes().put(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, this.parser.parse("")); this.exchange.getResponse().setStatusCode(HttpStatus.MOVED_PERMANENTLY); Tag tag = WebFluxTags.uri(this.exchange); + assertThat(tag.getValue()).isEqualTo("root"); + } + + @Test + void uriTagValueWithBestMatchingPatternAndIgnoreTrailingSlashRemoveTrailingSlash() { + this.exchange.getAttributes().put(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, + this.parser.parse("/spring/")); + Tag tag = WebFluxTags.uri(this.exchange, true); assertThat(tag.getValue()).isEqualTo("/spring"); } + @Test + void uriTagValueWithBestMatchingPatternAndIgnoreTrailingSlashKeepSingleSlash() { + this.exchange.getAttributes().put(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, this.parser.parse("/")); + Tag tag = WebFluxTags.uri(this.exchange, true); + assertThat(tag.getValue()).isEqualTo("/"); + } + @Test void uriTagValueIsRedirectionWhenResponseStatusIs3xx() { this.exchange.getResponse().setStatusCode(HttpStatus.MOVED_PERMANENTLY); @@ -73,7 +102,7 @@ void uriTagValueIsNotFoundWhenResponseStatusIs404() { @Test void uriTagToleratesCustomResponseStatus() { - this.exchange.getResponse().setStatusCodeValue(601); + this.exchange.getResponse().setRawStatusCode(601); Tag tag = WebFluxTags.uri(this.exchange); assertThat(tag.getValue()).isEqualTo("root"); } @@ -111,58 +140,83 @@ void methodTagToleratesNonStandardHttpMethods() { } @Test - void outcomeTagIsUnknownWhenResponseStatusIsNull() { + void outcomeTagIsSuccessWhenResponseStatusIsNull() { this.exchange.getResponse().setStatusCode(null); - Tag tag = WebFluxTags.outcome(this.exchange); - assertThat(tag.getValue()).isEqualTo("UNKNOWN"); + Tag tag = WebFluxTags.outcome(this.exchange, null); + assertThat(tag.getValue()).isEqualTo("SUCCESS"); + } + + @Test + void outcomeTagIsSuccessWhenResponseStatusIsAvailableFromUnderlyingServer() { + ServerWebExchange exchange = mock(ServerWebExchange.class); + ServerHttpRequest request = mock(ServerHttpRequest.class); + ServerHttpResponse response = mock(ServerHttpResponse.class); + given(response.getStatusCode()).willReturn(HttpStatus.OK); + given(response.getRawStatusCode()).willReturn(null); + given(exchange.getRequest()).willReturn(request); + given(exchange.getResponse()).willReturn(response); + Tag tag = WebFluxTags.outcome(exchange, null); + assertThat(tag.getValue()).isEqualTo("SUCCESS"); } @Test void outcomeTagIsInformationalWhenResponseIs1xx() { this.exchange.getResponse().setStatusCode(HttpStatus.CONTINUE); - Tag tag = WebFluxTags.outcome(this.exchange); + Tag tag = WebFluxTags.outcome(this.exchange, null); assertThat(tag.getValue()).isEqualTo("INFORMATIONAL"); } @Test void outcomeTagIsSuccessWhenResponseIs2xx() { this.exchange.getResponse().setStatusCode(HttpStatus.OK); - Tag tag = WebFluxTags.outcome(this.exchange); + Tag tag = WebFluxTags.outcome(this.exchange, null); assertThat(tag.getValue()).isEqualTo("SUCCESS"); } @Test void outcomeTagIsRedirectionWhenResponseIs3xx() { this.exchange.getResponse().setStatusCode(HttpStatus.MOVED_PERMANENTLY); - Tag tag = WebFluxTags.outcome(this.exchange); + Tag tag = WebFluxTags.outcome(this.exchange, null); assertThat(tag.getValue()).isEqualTo("REDIRECTION"); } @Test void outcomeTagIsClientErrorWhenResponseIs4xx() { this.exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST); - Tag tag = WebFluxTags.outcome(this.exchange); + Tag tag = WebFluxTags.outcome(this.exchange, null); assertThat(tag.getValue()).isEqualTo("CLIENT_ERROR"); } @Test void outcomeTagIsServerErrorWhenResponseIs5xx() { this.exchange.getResponse().setStatusCode(HttpStatus.BAD_GATEWAY); - Tag tag = WebFluxTags.outcome(this.exchange); + Tag tag = WebFluxTags.outcome(this.exchange, null); assertThat(tag.getValue()).isEqualTo("SERVER_ERROR"); } @Test void outcomeTagIsClientErrorWhenResponseIsNonStandardInClientSeries() { - this.exchange.getResponse().setStatusCodeValue(490); - Tag tag = WebFluxTags.outcome(this.exchange); + this.exchange.getResponse().setRawStatusCode(490); + Tag tag = WebFluxTags.outcome(this.exchange, null); assertThat(tag.getValue()).isEqualTo("CLIENT_ERROR"); } @Test void outcomeTagIsUnknownWhenResponseStatusIsInUnknownSeries() { - this.exchange.getResponse().setStatusCodeValue(701); - Tag tag = WebFluxTags.outcome(this.exchange); + this.exchange.getResponse().setRawStatusCode(701); + Tag tag = WebFluxTags.outcome(this.exchange, null); + assertThat(tag.getValue()).isEqualTo("UNKNOWN"); + } + + @Test + void outcomeTagIsUnknownWhenExceptionIsDisconnectedClient() { + Tag tag = WebFluxTags.outcome(this.exchange, new EOFException("broken pipe")); + assertThat(tag.getValue()).isEqualTo("UNKNOWN"); + } + + @Test + void outcomeTagIsUnknownWhenExceptionIsCancelledExchange() { + Tag tag = WebFluxTags.outcome(this.exchange, new CancelledServerWebExchangeException()); assertThat(tag.getValue()).isEqualTo("UNKNOWN"); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/FaultyWebMvcTagsProvider.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/FaultyWebMvcTagsProvider.java new file mode 100644 index 000000000000..57701d271377 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/FaultyWebMvcTagsProvider.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.web.servlet; + +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import io.micrometer.core.instrument.Tag; + +/** + * {@link WebMvcTagsProvider} used for testing that can be configured to fail when getting + * tags or long task tags. + * + * @author Andy Wilkinson + */ +class FaultyWebMvcTagsProvider extends DefaultWebMvcTagsProvider { + + private final AtomicBoolean fail = new AtomicBoolean(false); + + FaultyWebMvcTagsProvider() { + super(true); + } + + @Override + public Iterable getTags(HttpServletRequest request, HttpServletResponse response, Object handler, + Throwable exception) { + if (this.fail.compareAndSet(true, false)) { + throw new RuntimeException(); + } + return super.getTags(request, response, handler, exception); + } + + @Override + public Iterable getLongRequestTags(HttpServletRequest request, Object handler) { + if (this.fail.compareAndSet(true, false)) { + throw new RuntimeException(); + } + return super.getLongRequestTags(request, handler); + } + + void failOnce() { + this.fail.set(true); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/LongTaskTimingHandlerInterceptorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/LongTaskTimingHandlerInterceptorTests.java index 1279f3131a8f..68420af8b952 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/LongTaskTimingHandlerInterceptorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/LongTaskTimingHandlerInterceptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import io.micrometer.core.annotation.Timed; @@ -76,6 +77,9 @@ class LongTaskTimingHandlerInterceptorTests { @Autowired private CyclicBarrier callableBarrier; + @Autowired + private FaultyWebMvcTagsProvider tagsProvider; + private MockMvc mvc; @BeforeEach @@ -117,6 +121,26 @@ void asyncCallableRequest() throws Exception { .isEqualTo(0); } + @Test + void whenMetricsRecordingFailsResponseIsUnaffected() throws Exception { + this.tagsProvider.failOnce(); + AtomicReference result = new AtomicReference<>(); + Thread backgroundRequest = new Thread(() -> { + try { + result.set( + this.mvc.perform(get("/api/c1/callable/10")).andExpect(request().asyncStarted()).andReturn()); + } + catch (Exception ex) { + fail("Failed to execute async request", ex); + } + }); + backgroundRequest.start(); + this.callableBarrier.await(10, TimeUnit.SECONDS); + this.callableBarrier.await(10, TimeUnit.SECONDS); + backgroundRequest.join(); + this.mvc.perform(asyncDispatch(result.get())).andExpect(status().isOk()); + } + @Configuration(proxyBeanMethods = false) @EnableWebMvc @Import(Controller1.class) @@ -138,13 +162,17 @@ CyclicBarrier callableBarrier() { } @Bean - WebMvcConfigurer handlerInterceptorConfigurer(MeterRegistry meterRegistry) { + FaultyWebMvcTagsProvider webMvcTagsProvider() { + return new FaultyWebMvcTagsProvider(); + } + + @Bean + WebMvcConfigurer handlerInterceptorConfigurer(MeterRegistry meterRegistry, WebMvcTagsProvider tagsProvider) { return new WebMvcConfigurer() { @Override public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor( - new LongTaskTimingHandlerInterceptor(meterRegistry, new DefaultWebMvcTagsProvider())); + registry.addInterceptor(new LongTaskTimingHandlerInterceptor(meterRegistry, tagsProvider)); } }; diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcMetricsFilterTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcMetricsFilterTests.java index 438ab654521b..85d6ab16e7ce 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcMetricsFilterTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcMetricsFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,6 +38,7 @@ import io.micrometer.core.annotation.Timed; import io.micrometer.core.instrument.Clock; import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.Meter.Id; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.MockClock; import io.micrometer.core.instrument.Tag; @@ -57,6 +58,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.actuate.metrics.AutoTimer; +import org.springframework.boot.web.servlet.error.ErrorAttributes; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -121,10 +123,13 @@ class WebMvcMetricsFilterTests { @Qualifier("completableFutureBarrier") private CyclicBarrier completableFutureBarrier; + @Autowired + private FaultyWebMvcTagsProvider tagsProvider; + @BeforeEach void setupMockMvc() { - this.mvc = MockMvcBuilders.webAppContextSetup(this.context) - .addFilters(this.filter, new RedirectAndNotFoundFilter()).build(); + this.mvc = MockMvcBuilders.webAppContextSetup(this.context).addFilters(this.filter, new CustomBehaviorFilter()) + .build(); } @Test @@ -161,7 +166,7 @@ void badClientRequest() throws Exception { @Test void redirectRequest() throws Exception { - this.mvc.perform(get("/api/redirect").header(RedirectAndNotFoundFilter.TEST_MISBEHAVE_HEADER, "302")) + this.mvc.perform(get("/api/redirect").header(CustomBehaviorFilter.TEST_STATUS_HEADER, "302")) .andExpect(status().is3xxRedirection()); assertThat(this.registry.get("http.server.requests").tags("uri", "REDIRECTION").tags("status", "302").timer()) .isNotNull(); @@ -169,7 +174,7 @@ void redirectRequest() throws Exception { @Test void notFoundRequest() throws Exception { - this.mvc.perform(get("/api/not/found").header(RedirectAndNotFoundFilter.TEST_MISBEHAVE_HEADER, "404")) + this.mvc.perform(get("/api/not/found").header(CustomBehaviorFilter.TEST_STATUS_HEADER, "404")) .andExpect(status().is4xxClientError()); assertThat(this.registry.get("http.server.requests").tags("uri", "NOT_FOUND").tags("status", "404").timer()) .isNotNull(); @@ -183,13 +188,29 @@ void unhandledError() { .isEqualTo(1L); } + @Test + void unhandledServletException() { + assertThatCode(() -> this.mvc + .perform(get("/api/filterError").header(CustomBehaviorFilter.TEST_SERVLET_EXCEPTION_HEADER, "throw")) + .andExpect(status().isOk())).isInstanceOf(ServletException.class); + Id meterId = this.registry.get("http.server.requests").tags("exception", "ServletException").timer().getId(); + assertThat(meterId.getTag("status")).isEqualTo("500"); + } + @Test void streamingError() throws Exception { MvcResult result = this.mvc.perform(get("/api/c1/streamingError")).andExpect(request().asyncStarted()) .andReturn(); assertThatIOException().isThrownBy(() -> this.mvc.perform(asyncDispatch(result)).andReturn()); - assertThat(this.registry.get("http.server.requests").tags("exception", "IOException").timer().count()) - .isEqualTo(1L); + Id meterId = this.registry.get("http.server.requests").tags("exception", "IOException").timer().getId(); + // Response is committed before error occurs so status is 200 (OK) + assertThat(meterId.getTag("status")).isEqualTo("200"); + } + + @Test + void whenMetricsRecordingFailsResponseIsUnaffected() throws Exception { + this.tagsProvider.failOnce(); + this.mvc.perform(get("/api/c1/10")).andExpect(status().isOk()); } @Test @@ -199,8 +220,10 @@ void anonymousError() { } catch (Throwable ignore) { } - assertThat(this.registry.get("http.server.requests").tag("uri", "/api/c1/anonymousError/{id}").timer().getId() - .getTag("exception")).endsWith("$1"); + Id meterId = this.registry.get("http.server.requests").tag("uri", "/api/c1/anonymousError/{id}").timer() + .getId(); + assertThat(meterId.getTag("exception")).endsWith("$1"); + assertThat(meterId.getTag("status")).isEqualTo("500"); } @Test @@ -264,7 +287,8 @@ void asyncCompletableFutureRequest() throws Exception { @Test void endpointThrowsError() throws Exception { this.mvc.perform(get("/api/c1/error/10")).andExpect(status().is4xxClientError()); - assertThat(this.registry.get("http.server.requests").tags("status", "422").timer().count()).isEqualTo(1L); + assertThat(this.registry.get("http.server.requests").tags("status", "422", "exception", "IllegalStateException") + .timer().count()).isEqualTo(1L); } @Test @@ -289,6 +313,14 @@ void recordHistogram() throws Exception { assertThat(this.prometheusRegistry.scrape()).contains("le=\"30.0\""); } + @Test + void trailingSlashShouldNotRecordDuplicateMetrics() throws Exception { + this.mvc.perform(get("/api/c1/simple/10")).andExpect(status().isOk()); + this.mvc.perform(get("/api/c1/simple/10/")).andExpect(status().isOk()); + assertThat(this.registry.get("http.server.requests").tags("status", "200", "uri", "/api/c1/simple/{id}").timer() + .count()).isEqualTo(2); + } + @Target({ ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Timed(percentiles = 0.95) @@ -340,8 +372,8 @@ public MeterFilterReply accept(@NonNull Meter.Id id) { } @Bean - RedirectAndNotFoundFilter redirectAndNotFoundFilter() { - return new RedirectAndNotFoundFilter(); + CustomBehaviorFilter redirectAndNotFoundFilter() { + return new CustomBehaviorFilter(); } @Bean(name = "callableBarrier") @@ -355,9 +387,14 @@ CyclicBarrier completableFutureBarrier() { } @Bean - WebMvcMetricsFilter webMetricsFilter(MeterRegistry registry, WebApplicationContext ctx) { - return new WebMvcMetricsFilter(registry, new DefaultWebMvcTagsProvider(), "http.server.requests", - AutoTimer.ENABLED); + WebMvcMetricsFilter webMetricsFilter(MeterRegistry registry, FaultyWebMvcTagsProvider tagsProvider, + WebApplicationContext ctx) { + return new WebMvcMetricsFilter(registry, tagsProvider, "http.server.requests", AutoTimer.ENABLED); + } + + @Bean + FaultyWebMvcTagsProvider faultyWebMvcTagsProvider() { + return new FaultyWebMvcTagsProvider(); } } @@ -380,6 +417,11 @@ String successfulWithExtraTags(@PathVariable Long id) { return id.toString(); } + @GetMapping("/simple/{id}") + String simpleMapping(@PathVariable Long id) { + return id.toString(); + } + @Timed @Timed(value = "my.long.request", extraTags = { "region", "test" }, longTask = true) @GetMapping("/callable/{id}") @@ -445,8 +487,10 @@ String alwaysThrowsUnhandledException(@PathVariable Long id) { } @GetMapping("/streamingError") - ResponseBodyEmitter streamingError() { + ResponseBodyEmitter streamingError() throws IOException { ResponseBodyEmitter emitter = new ResponseBodyEmitter(); + emitter.send("some data"); + emitter.send("some more data"); emitter.completeWithError(new IOException("error while writing to the response")); return emitter; } @@ -478,6 +522,8 @@ String meta(@PathVariable String id) { @ExceptionHandler(IllegalStateException.class) @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) ModelAndView defaultErrorHandler(HttpServletRequest request, Exception e) { + // this is done by ErrorAttributes implementations + request.setAttribute(ErrorAttributes.ERROR_ATTRIBUTE, e); return new ModelAndView("myerror"); } @@ -495,20 +541,24 @@ String successful(@PathVariable Long id) { } - static class RedirectAndNotFoundFilter extends OncePerRequestFilter { + static class CustomBehaviorFilter extends OncePerRequestFilter { + + static final String TEST_STATUS_HEADER = "x-test-status"; - static final String TEST_MISBEHAVE_HEADER = "x-test-misbehave-status"; + static final String TEST_SERVLET_EXCEPTION_HEADER = "x-test-servlet-exception"; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - String misbehave = request.getHeader(TEST_MISBEHAVE_HEADER); - if (misbehave != null) { - response.setStatus(Integer.parseInt(misbehave)); + String misbehaveStatus = request.getHeader(TEST_STATUS_HEADER); + if (misbehaveStatus != null) { + response.setStatus(Integer.parseInt(misbehaveStatus)); + return; } - else { - filterChain.doFilter(request, response); + if (request.getHeader(TEST_SERVLET_EXCEPTION_HEADER) != null) { + throw new ServletException(); } + filterChain.doFilter(request, response); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcMetricsIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcMetricsIntegrationTests.java index b604bd30803f..e7d4d383b53b 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcMetricsIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcMetricsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -85,7 +85,7 @@ void handledExceptionIsRecordedInMetricTag() throws Exception { } @Test - void rethrownExceptionIsRecordedInMetricTag() throws Exception { + void rethrownExceptionIsRecordedInMetricTag() { assertThatExceptionOfType(NestedServletException.class) .isThrownBy(() -> this.mvc.perform(get("/api/rethrownError")).andReturn()); assertThat(this.registry.get("http.server.requests").tags("exception", "Exception2", "status", "500").timer() diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/tomcat/TomcatMetricsBinderTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/tomcat/TomcatMetricsBinderTests.java new file mode 100644 index 000000000000..fd7d43d640e0 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/tomcat/TomcatMetricsBinderTests.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.web.tomcat; + +import io.micrometer.core.instrument.MeterRegistry; +import org.junit.jupiter.api.Test; + +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link TomcatMetricsBinder}. + * + * @author Andy Wilkinson + */ +class TomcatMetricsBinderTests { + + private final MeterRegistry meterRegistry = mock(MeterRegistry.class); + + @Test + void destroySucceedsWhenCalledBeforeApplicationHasStarted() { + new TomcatMetricsBinder(this.meterRegistry).destroy(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/mongo/MongoHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/mongo/MongoHealthIndicatorTests.java index cab01e86b1a5..0ad1de2b2c56 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/mongo/MongoHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/mongo/MongoHealthIndicatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,18 +18,16 @@ import com.mongodb.MongoException; import org.bson.Document; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.Status; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.data.mongodb.core.MongoTemplate; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link MongoHealthIndicator}. @@ -38,15 +36,6 @@ */ class MongoHealthIndicatorTests { - private AnnotationConfigApplicationContext context; - - @AfterEach - void close() { - if (this.context != null) { - this.context.close(); - } - } - @Test void mongoIsUp() { Document commandResult = mock(Document.class); @@ -57,8 +46,8 @@ void mongoIsUp() { Health health = healthIndicator.health(); assertThat(health.getStatus()).isEqualTo(Status.UP); assertThat(health.getDetails().get("version")).isEqualTo("2.6.4"); - verify(commandResult).getString("version"); - verify(mongoTemplate).executeCommand("{ buildInfo: 1 }"); + then(commandResult).should().getString("version"); + then(mongoTemplate).should().executeCommand("{ buildInfo: 1 }"); } @Test @@ -69,7 +58,7 @@ void mongoIsDown() { Health health = healthIndicator.health(); assertThat(health.getStatus()).isEqualTo(Status.DOWN); assertThat((String) health.getDetails().get("error")).contains("Connection failed"); - verify(mongoTemplate).executeCommand("{ buildInfo: 1 }"); + then(mongoTemplate).should().executeCommand("{ buildInfo: 1 }"); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jHealthIndicatorTests.java index 412bde8ec4e9..1291f710aa7f 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jHealthIndicatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,25 +16,29 @@ package org.springframework.boot.actuate.neo4j; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.neo4j.ogm.exception.CypherException; -import org.neo4j.ogm.model.Result; -import org.neo4j.ogm.session.Session; -import org.neo4j.ogm.session.SessionFactory; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Record; +import org.neo4j.driver.Result; +import org.neo4j.driver.Session; +import org.neo4j.driver.SessionConfig; +import org.neo4j.driver.Values; +import org.neo4j.driver.exceptions.ServiceUnavailableException; +import org.neo4j.driver.exceptions.SessionExpiredException; +import org.neo4j.driver.summary.ResultSummary; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.Status; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; /** * Tests for {@link Neo4jHealthIndicator}. @@ -45,42 +49,85 @@ */ class Neo4jHealthIndicatorTests { - private Session session; + @Test + void neo4jIsUp() { + ResultSummary resultSummary = ResultSummaryMock.createResultSummary("4711", "My Home", "test"); + Driver driver = mockDriver(resultSummary, "ultimate collectors edition"); + Health health = new Neo4jHealthIndicator(driver).health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertThat(health.getDetails()).containsEntry("server", "4711@My Home"); + assertThat(health.getDetails()).containsEntry("database", "test"); + assertThat(health.getDetails()).containsEntry("edition", "ultimate collectors edition"); + } - private Neo4jHealthIndicator neo4jHealthIndicator; + @Test + void neo4jIsUpWithoutDatabaseName() { + ResultSummary resultSummary = ResultSummaryMock.createResultSummary("4711", "My Home", null); + Driver driver = mockDriver(resultSummary, "some edition"); + Health health = new Neo4jHealthIndicator(driver).health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertThat(health.getDetails()).containsEntry("server", "4711@My Home"); + assertThat(health.getDetails()).doesNotContainKey("database"); + assertThat(health.getDetails()).containsEntry("edition", "some edition"); + } - @BeforeEach - void before() { - this.session = mock(Session.class); - SessionFactory sessionFactory = mock(SessionFactory.class); - given(sessionFactory.openSession()).willReturn(this.session); - this.neo4jHealthIndicator = new Neo4jHealthIndicator(sessionFactory); + @Test + void neo4jIsUpWithEmptyDatabaseName() { + ResultSummary resultSummary = ResultSummaryMock.createResultSummary("4711", "My Home", ""); + Driver driver = mockDriver(resultSummary, "some edition"); + Health health = new Neo4jHealthIndicator(driver).health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertThat(health.getDetails()).containsEntry("server", "4711@My Home"); + assertThat(health.getDetails()).doesNotContainKey("database"); + assertThat(health.getDetails()).containsEntry("edition", "some edition"); } @Test - void neo4jUp() { - Result result = mock(Result.class); - given(this.session.query(Neo4jHealthIndicator.CYPHER, Collections.emptyMap())).willReturn(result); - int nodeCount = 500; - Map expectedCypherDetails = new HashMap<>(); - expectedCypherDetails.put("nodes", nodeCount); - List> queryResults = new ArrayList<>(); - queryResults.add(expectedCypherDetails); - given(result.queryResults()).willReturn(queryResults); - Health health = this.neo4jHealthIndicator.health(); + void neo4jIsUpWithOneSessionExpiredException() { + ResultSummary resultSummary = ResultSummaryMock.createResultSummary("4711", "My Home", ""); + Session session = mock(Session.class); + Result statementResult = mockStatementResult(resultSummary, "some edition"); + AtomicInteger count = new AtomicInteger(); + given(session.run(anyString())).will((invocation) -> { + if (count.compareAndSet(0, 1)) { + throw new SessionExpiredException("Session expired"); + } + return statementResult; + }); + Driver driver = mock(Driver.class); + given(driver.session(any(SessionConfig.class))).willReturn(session); + Neo4jHealthIndicator healthIndicator = new Neo4jHealthIndicator(driver); + Health health = healthIndicator.health(); assertThat(health.getStatus()).isEqualTo(Status.UP); - Map details = health.getDetails(); - int nodeCountFromDetails = (int) details.get("nodes"); - assertThat(nodeCountFromDetails).isEqualTo(nodeCount); + assertThat(health.getDetails()).containsEntry("server", "4711@My Home"); + then(session).should(times(2)).close(); } @Test - void neo4jDown() { - CypherException cypherException = new CypherException("Neo.ClientError.Statement.SyntaxError", - "Error executing Cypher"); - given(this.session.query(Neo4jHealthIndicator.CYPHER, Collections.emptyMap())).willThrow(cypherException); - Health health = this.neo4jHealthIndicator.health(); + void neo4jIsDown() { + Driver driver = mock(Driver.class); + given(driver.session(any(SessionConfig.class))).willThrow(ServiceUnavailableException.class); + Health health = new Neo4jHealthIndicator(driver).health(); assertThat(health.getStatus()).isEqualTo(Status.DOWN); + assertThat(health.getDetails()).containsKeys("error"); + } + + private Result mockStatementResult(ResultSummary resultSummary, String edition) { + Record record = mock(Record.class); + given(record.get("edition")).willReturn(Values.value(edition)); + Result statementResult = mock(Result.class); + given(statementResult.single()).willReturn(record); + given(statementResult.consume()).willReturn(resultSummary); + return statementResult; + } + + private Driver mockDriver(ResultSummary resultSummary, String edition) { + Result statementResult = mockStatementResult(resultSummary, edition); + Session session = mock(Session.class); + given(session.run(anyString())).willReturn(statementResult); + Driver driver = mock(Driver.class); + given(driver.session(any(SessionConfig.class))).willReturn(session); + return driver; } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jReactiveHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jReactiveHealthIndicatorTests.java new file mode 100644 index 000000000000..8c66165f1246 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/Neo4jReactiveHealthIndicatorTests.java @@ -0,0 +1,116 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.neo4j; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Record; +import org.neo4j.driver.SessionConfig; +import org.neo4j.driver.Values; +import org.neo4j.driver.exceptions.ServiceUnavailableException; +import org.neo4j.driver.exceptions.SessionExpiredException; +import org.neo4j.driver.reactive.RxResult; +import org.neo4j.driver.reactive.RxSession; +import org.neo4j.driver.summary.ResultSummary; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.boot.actuate.health.Status; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; + +/** + * Tests for {@link Neo4jReactiveHealthIndicator}. + * + * @author Michael J. Simons + * @author Stephane Nicoll + */ +class Neo4jReactiveHealthIndicatorTests { + + @Test + void neo4jIsUp() { + ResultSummary resultSummary = ResultSummaryMock.createResultSummary("4711", "My Home", "test"); + Driver driver = mockDriver(resultSummary, "ultimate collectors edition"); + Neo4jReactiveHealthIndicator healthIndicator = new Neo4jReactiveHealthIndicator(driver); + healthIndicator.health().as(StepVerifier::create).consumeNextWith((health) -> { + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertThat(health.getDetails()).containsEntry("server", "4711@My Home"); + assertThat(health.getDetails()).containsEntry("edition", "ultimate collectors edition"); + }).verifyComplete(); + } + + @Test + void neo4jIsUpWithOneSessionExpiredException() { + ResultSummary resultSummary = ResultSummaryMock.createResultSummary("4711", "My Home", ""); + RxSession session = mock(RxSession.class); + RxResult statementResult = mockStatementResult(resultSummary, "some edition"); + AtomicInteger count = new AtomicInteger(); + given(session.run(anyString())).will((invocation) -> { + if (count.compareAndSet(0, 1)) { + throw new SessionExpiredException("Session expired"); + } + return statementResult; + }); + Driver driver = mock(Driver.class); + given(driver.rxSession(any(SessionConfig.class))).willReturn(session); + Neo4jReactiveHealthIndicator healthIndicator = new Neo4jReactiveHealthIndicator(driver); + healthIndicator.health().as(StepVerifier::create).consumeNextWith((health) -> { + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertThat(health.getDetails()).containsEntry("server", "4711@My Home"); + assertThat(health.getDetails()).containsEntry("edition", "some edition"); + }).verifyComplete(); + then(session).should(times(2)).close(); + } + + @Test + void neo4jIsDown() { + Driver driver = mock(Driver.class); + given(driver.rxSession(any(SessionConfig.class))).willThrow(ServiceUnavailableException.class); + Neo4jReactiveHealthIndicator healthIndicator = new Neo4jReactiveHealthIndicator(driver); + healthIndicator.health().as(StepVerifier::create).consumeNextWith((health) -> { + assertThat(health.getStatus()).isEqualTo(Status.DOWN); + assertThat(health.getDetails()).containsKeys("error"); + }).verifyComplete(); + } + + private RxResult mockStatementResult(ResultSummary resultSummary, String edition) { + Record record = mock(Record.class); + given(record.get("edition")).willReturn(Values.value(edition)); + RxResult statementResult = mock(RxResult.class); + given(statementResult.records()).willReturn(Mono.just(record)); + given(statementResult.consume()).willReturn(Mono.just(resultSummary)); + return statementResult; + } + + private Driver mockDriver(ResultSummary resultSummary, String edition) { + RxResult statementResult = mockStatementResult(resultSummary, edition); + RxSession session = mock(RxSession.class); + given(session.run(anyString())).willReturn(statementResult); + Driver driver = mock(Driver.class); + given(driver.rxSession(any(SessionConfig.class))).willReturn(session); + return driver; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/ResultSummaryMock.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/ResultSummaryMock.java new file mode 100644 index 000000000000..4bbd715a0e44 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/neo4j/ResultSummaryMock.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.neo4j; + +import org.neo4j.driver.summary.DatabaseInfo; +import org.neo4j.driver.summary.ResultSummary; +import org.neo4j.driver.summary.ServerInfo; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Test utility to mock {@link ResultSummary}. + * + * @author Stephane Nicoll + */ +final class ResultSummaryMock { + + private ResultSummaryMock() { + } + + static ResultSummary createResultSummary(String serverVersion, String serverAddress, String databaseName) { + ServerInfo serverInfo = mock(ServerInfo.class); + given(serverInfo.version()).willReturn(serverVersion); + given(serverInfo.address()).willReturn(serverAddress); + DatabaseInfo databaseInfo = mock(DatabaseInfo.class); + given(databaseInfo.name()).willReturn(databaseName); + ResultSummary resultSummary = mock(ResultSummary.class); + given(resultSummary.server()).willReturn(serverInfo); + given(resultSummary.database()).willReturn(databaseInfo); + return resultSummary; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/quartz/QuartzEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/quartz/QuartzEndpointTests.java new file mode 100644 index 000000000000..2d51fca3567c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/quartz/QuartzEndpointTests.java @@ -0,0 +1,709 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.quartz; + +import java.time.Duration; +import java.time.Instant; +import java.time.LocalTime; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TimeZone; +import java.util.stream.Stream; + +import org.assertj.core.api.InstanceOfAssertFactories; +import org.assertj.core.api.InstanceOfAssertFactory; +import org.assertj.core.api.MapAssert; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.quartz.CalendarIntervalScheduleBuilder; +import org.quartz.CalendarIntervalTrigger; +import org.quartz.CronScheduleBuilder; +import org.quartz.CronTrigger; +import org.quartz.DailyTimeIntervalScheduleBuilder; +import org.quartz.DailyTimeIntervalTrigger; +import org.quartz.DateBuilder.IntervalUnit; +import org.quartz.Job; +import org.quartz.JobBuilder; +import org.quartz.JobDetail; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.SimpleScheduleBuilder; +import org.quartz.SimpleTrigger; +import org.quartz.TimeOfDay; +import org.quartz.Trigger; +import org.quartz.Trigger.TriggerState; +import org.quartz.TriggerBuilder; +import org.quartz.TriggerKey; +import org.quartz.impl.matchers.GroupMatcher; +import org.quartz.spi.OperableTrigger; + +import org.springframework.boot.actuate.endpoint.Sanitizer; +import org.springframework.boot.actuate.quartz.QuartzEndpoint.QuartzJobDetails; +import org.springframework.boot.actuate.quartz.QuartzEndpoint.QuartzJobGroupSummary; +import org.springframework.boot.actuate.quartz.QuartzEndpoint.QuartzJobSummary; +import org.springframework.boot.actuate.quartz.QuartzEndpoint.QuartzReport; +import org.springframework.boot.actuate.quartz.QuartzEndpoint.QuartzTriggerGroupSummary; +import org.springframework.scheduling.quartz.DelegatingJob; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link QuartzEndpoint}. + * + * @author Vedran Pavic + * @author Stephane Nicoll + */ +class QuartzEndpointTests { + + private static final JobDetail jobOne = JobBuilder.newJob(Job.class).withIdentity("jobOne").build(); + + private static final JobDetail jobTwo = JobBuilder.newJob(DelegatingJob.class).withIdentity("jobTwo").build(); + + private static final JobDetail jobThree = JobBuilder.newJob(Job.class).withIdentity("jobThree", "samples").build(); + + private static final Trigger triggerOne = TriggerBuilder.newTrigger().forJob(jobOne).withIdentity("triggerOne") + .build(); + + private static final Trigger triggerTwo = TriggerBuilder.newTrigger().forJob(jobOne).withIdentity("triggerTwo") + .build(); + + private static final Trigger triggerThree = TriggerBuilder.newTrigger().forJob(jobThree) + .withIdentity("triggerThree", "samples").build(); + + private final Scheduler scheduler; + + private final QuartzEndpoint endpoint; + + QuartzEndpointTests() { + this.scheduler = mock(Scheduler.class); + this.endpoint = new QuartzEndpoint(this.scheduler); + } + + @Test + void quartzReport() throws SchedulerException { + given(this.scheduler.getJobGroupNames()).willReturn(Arrays.asList("jobSamples", "DEFAULT")); + given(this.scheduler.getTriggerGroupNames()).willReturn(Collections.singletonList("triggerSamples")); + QuartzReport quartzReport = this.endpoint.quartzReport(); + assertThat(quartzReport.getJobs().getGroups()).containsOnly("jobSamples", "DEFAULT"); + assertThat(quartzReport.getTriggers().getGroups()).containsOnly("triggerSamples"); + then(this.scheduler).should().getJobGroupNames(); + then(this.scheduler).should().getTriggerGroupNames(); + then(this.scheduler).shouldHaveNoMoreInteractions(); + } + + @Test + void quartzReportWithNoJob() throws SchedulerException { + given(this.scheduler.getJobGroupNames()).willReturn(Collections.emptyList()); + given(this.scheduler.getTriggerGroupNames()).willReturn(Arrays.asList("triggerSamples", "DEFAULT")); + QuartzReport quartzReport = this.endpoint.quartzReport(); + assertThat(quartzReport.getJobs().getGroups()).isEmpty(); + assertThat(quartzReport.getTriggers().getGroups()).containsOnly("triggerSamples", "DEFAULT"); + } + + @Test + void quartzReportWithNoTrigger() throws SchedulerException { + given(this.scheduler.getJobGroupNames()).willReturn(Collections.singletonList("jobSamples")); + given(this.scheduler.getTriggerGroupNames()).willReturn(Collections.emptyList()); + QuartzReport quartzReport = this.endpoint.quartzReport(); + assertThat(quartzReport.getJobs().getGroups()).containsOnly("jobSamples"); + assertThat(quartzReport.getTriggers().getGroups()).isEmpty(); + } + + @Test + void quartzJobGroupsWithExistingGroups() throws SchedulerException { + mockJobs(jobOne, jobTwo, jobThree); + Map jobGroups = this.endpoint.quartzJobGroups().getGroups(); + assertThat(jobGroups).containsOnlyKeys("DEFAULT", "samples"); + assertThat(jobGroups).extractingByKey("DEFAULT", nestedMap()) + .containsOnly(entry("jobs", Arrays.asList("jobOne", "jobTwo"))); + assertThat(jobGroups).extractingByKey("samples", nestedMap()) + .containsOnly(entry("jobs", Collections.singletonList("jobThree"))); + } + + @Test + void quartzJobGroupsWithNoGroup() throws SchedulerException { + given(this.scheduler.getJobGroupNames()).willReturn(Collections.emptyList()); + Map jobGroups = this.endpoint.quartzJobGroups().getGroups(); + assertThat(jobGroups).isEmpty(); + } + + @Test + void quartzTriggerGroupsWithExistingGroups() throws SchedulerException { + mockTriggers(triggerOne, triggerTwo, triggerThree); + given(this.scheduler.getPausedTriggerGroups()).willReturn(Collections.singleton("samples")); + Map triggerGroups = this.endpoint.quartzTriggerGroups().getGroups(); + assertThat(triggerGroups).containsOnlyKeys("DEFAULT", "samples"); + assertThat(triggerGroups).extractingByKey("DEFAULT", nestedMap()).containsOnly(entry("paused", false), + entry("triggers", Arrays.asList("triggerOne", "triggerTwo"))); + assertThat(triggerGroups).extractingByKey("samples", nestedMap()).containsOnly(entry("paused", true), + entry("triggers", Collections.singletonList("triggerThree"))); + } + + @Test + void quartzTriggerGroupsWithNoGroup() throws SchedulerException { + given(this.scheduler.getTriggerGroupNames()).willReturn(Collections.emptyList()); + Map triggerGroups = this.endpoint.quartzTriggerGroups().getGroups(); + assertThat(triggerGroups).isEmpty(); + } + + @Test + void quartzJobGroupSummaryWithInvalidGroup() throws SchedulerException { + given(this.scheduler.getJobGroupNames()).willReturn(Collections.singletonList("DEFAULT")); + QuartzJobGroupSummary summary = this.endpoint.quartzJobGroupSummary("unknown"); + assertThat(summary).isNull(); + } + + @Test + void quartzJobGroupSummaryWithEmptyGroup() throws SchedulerException { + given(this.scheduler.getJobGroupNames()).willReturn(Collections.singletonList("samples")); + given(this.scheduler.getJobKeys(GroupMatcher.jobGroupEquals("samples"))).willReturn(Collections.emptySet()); + QuartzJobGroupSummary summary = this.endpoint.quartzJobGroupSummary("samples"); + assertThat(summary).isNotNull(); + assertThat(summary.getGroup()).isEqualTo("samples"); + assertThat(summary.getJobs()).isEmpty(); + } + + @Test + void quartzJobGroupSummaryWithJobs() throws SchedulerException { + mockJobs(jobOne, jobTwo); + QuartzJobGroupSummary summary = this.endpoint.quartzJobGroupSummary("DEFAULT"); + assertThat(summary).isNotNull(); + assertThat(summary.getGroup()).isEqualTo("DEFAULT"); + Map jobSummaries = summary.getJobs(); + assertThat(jobSummaries).containsOnlyKeys("jobOne", "jobTwo"); + assertThat(jobSummaries.get("jobOne").getClassName()).isEqualTo(Job.class.getName()); + assertThat(jobSummaries.get("jobTwo").getClassName()).isEqualTo(DelegatingJob.class.getName()); + } + + @Test + void quartzTriggerGroupSummaryWithInvalidGroup() throws SchedulerException { + given(this.scheduler.getTriggerGroupNames()).willReturn(Collections.singletonList("DEFAULT")); + QuartzTriggerGroupSummary summary = this.endpoint.quartzTriggerGroupSummary("unknown"); + assertThat(summary).isNull(); + } + + @Test + void quartzTriggerGroupSummaryWithEmptyGroup() throws SchedulerException { + given(this.scheduler.getTriggerGroupNames()).willReturn(Collections.singletonList("samples")); + given(this.scheduler.getTriggerKeys(GroupMatcher.triggerGroupEquals("samples"))) + .willReturn(Collections.emptySet()); + QuartzTriggerGroupSummary summary = this.endpoint.quartzTriggerGroupSummary("samples"); + assertThat(summary).isNotNull(); + assertThat(summary.getGroup()).isEqualTo("samples"); + assertThat(summary.isPaused()).isFalse(); + assertThat(summary.getTriggers().getCron()).isEmpty(); + assertThat(summary.getTriggers().getSimple()).isEmpty(); + assertThat(summary.getTriggers().getDailyTimeInterval()).isEmpty(); + assertThat(summary.getTriggers().getCalendarInterval()).isEmpty(); + assertThat(summary.getTriggers().getCustom()).isEmpty(); + } + + @Test + void quartzTriggerGroupSummaryWithCronTrigger() throws SchedulerException { + CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity("3am-every-day", "samples") + .withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(3, 0)).build(); + mockTriggers(cronTrigger); + QuartzTriggerGroupSummary summary = this.endpoint.quartzTriggerGroupSummary("samples"); + assertThat(summary.getGroup()).isEqualTo("samples"); + assertThat(summary.isPaused()).isFalse(); + assertThat(summary.getTriggers().getCron()).containsOnlyKeys("3am-every-day"); + assertThat(summary.getTriggers().getSimple()).isEmpty(); + assertThat(summary.getTriggers().getDailyTimeInterval()).isEmpty(); + assertThat(summary.getTriggers().getCalendarInterval()).isEmpty(); + assertThat(summary.getTriggers().getCustom()).isEmpty(); + } + + @Test + void quartzTriggerGroupSummaryWithCronTriggerDetails() throws SchedulerException { + Date previousFireTime = Date.from(Instant.parse("2020-11-30T03:00:00Z")); + Date nextFireTime = Date.from(Instant.parse("2020-12-01T03:00:00Z")); + TimeZone timeZone = TimeZone.getTimeZone("Europe/Paris"); + CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity("3am-every-day", "samples").withPriority(3) + .withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(3, 0).inTimeZone(timeZone)).build(); + ((OperableTrigger) cronTrigger).setPreviousFireTime(previousFireTime); + ((OperableTrigger) cronTrigger).setNextFireTime(nextFireTime); + mockTriggers(cronTrigger); + QuartzTriggerGroupSummary summary = this.endpoint.quartzTriggerGroupSummary("samples"); + Map triggers = summary.getTriggers().getCron(); + assertThat(triggers).containsOnlyKeys("3am-every-day"); + assertThat(triggers).extractingByKey("3am-every-day", nestedMap()).containsOnly( + entry("previousFireTime", previousFireTime), entry("nextFireTime", nextFireTime), entry("priority", 3), + entry("expression", "0 0 3 ? * *"), entry("timeZone", timeZone)); + } + + @Test + void quartzTriggerGroupSummaryWithSimpleTrigger() throws SchedulerException { + SimpleTrigger simpleTrigger = TriggerBuilder.newTrigger().withIdentity("every-hour", "samples") + .withSchedule(SimpleScheduleBuilder.repeatHourlyForever(1)).build(); + mockTriggers(simpleTrigger); + QuartzTriggerGroupSummary summary = this.endpoint.quartzTriggerGroupSummary("samples"); + assertThat(summary.getGroup()).isEqualTo("samples"); + assertThat(summary.isPaused()).isFalse(); + assertThat(summary.getTriggers().getCron()).isEmpty(); + assertThat(summary.getTriggers().getSimple()).containsOnlyKeys("every-hour"); + assertThat(summary.getTriggers().getDailyTimeInterval()).isEmpty(); + assertThat(summary.getTriggers().getCalendarInterval()).isEmpty(); + assertThat(summary.getTriggers().getCustom()).isEmpty(); + } + + @Test + void quartzTriggerGroupSummaryWithSimpleTriggerDetails() throws SchedulerException { + Date previousFireTime = Date.from(Instant.parse("2020-11-30T03:00:00Z")); + Date nextFireTime = Date.from(Instant.parse("2020-12-01T03:00:00Z")); + SimpleTrigger simpleTrigger = TriggerBuilder.newTrigger().withIdentity("every-hour", "samples").withPriority(7) + .withSchedule(SimpleScheduleBuilder.repeatHourlyForever(1)).build(); + ((OperableTrigger) simpleTrigger).setPreviousFireTime(previousFireTime); + ((OperableTrigger) simpleTrigger).setNextFireTime(nextFireTime); + mockTriggers(simpleTrigger); + QuartzTriggerGroupSummary summary = this.endpoint.quartzTriggerGroupSummary("samples"); + Map triggers = summary.getTriggers().getSimple(); + assertThat(triggers).containsOnlyKeys("every-hour"); + assertThat(triggers).extractingByKey("every-hour", nestedMap()).containsOnly( + entry("previousFireTime", previousFireTime), entry("nextFireTime", nextFireTime), entry("priority", 7), + entry("interval", 3600000L)); + } + + @Test + void quartzTriggerGroupSummaryWithDailyIntervalTrigger() throws SchedulerException { + DailyTimeIntervalTrigger trigger = TriggerBuilder.newTrigger().withIdentity("every-hour-9am", "samples") + .withSchedule(DailyTimeIntervalScheduleBuilder.dailyTimeIntervalSchedule() + .startingDailyAt(TimeOfDay.hourAndMinuteOfDay(9, 0)).withInterval(1, IntervalUnit.HOUR)) + .build(); + mockTriggers(trigger); + QuartzTriggerGroupSummary summary = this.endpoint.quartzTriggerGroupSummary("samples"); + assertThat(summary.getGroup()).isEqualTo("samples"); + assertThat(summary.isPaused()).isFalse(); + assertThat(summary.getTriggers().getCron()).isEmpty(); + assertThat(summary.getTriggers().getSimple()).isEmpty(); + assertThat(summary.getTriggers().getDailyTimeInterval()).containsOnlyKeys("every-hour-9am"); + assertThat(summary.getTriggers().getCalendarInterval()).isEmpty(); + assertThat(summary.getTriggers().getCustom()).isEmpty(); + } + + @Test + void quartzTriggerGroupSummaryWithDailyIntervalTriggerDetails() throws SchedulerException { + Date previousFireTime = Date.from(Instant.parse("2020-11-30T03:00:00Z")); + Date nextFireTime = Date.from(Instant.parse("2020-12-01T03:00:00Z")); + DailyTimeIntervalTrigger trigger = TriggerBuilder.newTrigger().withIdentity("every-hour-tue-thu", "samples") + .withPriority(4) + .withSchedule(DailyTimeIntervalScheduleBuilder.dailyTimeIntervalSchedule() + .onDaysOfTheWeek(Calendar.TUESDAY, Calendar.THURSDAY) + .startingDailyAt(TimeOfDay.hourAndMinuteOfDay(9, 0)) + .endingDailyAt(TimeOfDay.hourAndMinuteOfDay(18, 0)).withInterval(1, IntervalUnit.HOUR)) + .build(); + ((OperableTrigger) trigger).setPreviousFireTime(previousFireTime); + ((OperableTrigger) trigger).setNextFireTime(nextFireTime); + mockTriggers(trigger); + QuartzTriggerGroupSummary summary = this.endpoint.quartzTriggerGroupSummary("samples"); + Map triggers = summary.getTriggers().getDailyTimeInterval(); + assertThat(triggers).containsOnlyKeys("every-hour-tue-thu"); + assertThat(triggers).extractingByKey("every-hour-tue-thu", nestedMap()).containsOnly( + entry("previousFireTime", previousFireTime), entry("nextFireTime", nextFireTime), entry("priority", 4), + entry("interval", 3600000L), entry("startTimeOfDay", LocalTime.of(9, 0)), + entry("endTimeOfDay", LocalTime.of(18, 0)), + entry("daysOfWeek", new LinkedHashSet<>(Arrays.asList(3, 5)))); + } + + @Test + void quartzTriggerGroupSummaryWithCalendarIntervalTrigger() throws SchedulerException { + CalendarIntervalTrigger trigger = TriggerBuilder.newTrigger().withIdentity("once-a-week", "samples") + .withSchedule(CalendarIntervalScheduleBuilder.calendarIntervalSchedule().withIntervalInWeeks(1)) + .build(); + mockTriggers(trigger); + QuartzTriggerGroupSummary summary = this.endpoint.quartzTriggerGroupSummary("samples"); + assertThat(summary.getGroup()).isEqualTo("samples"); + assertThat(summary.isPaused()).isFalse(); + assertThat(summary.getTriggers().getCron()).isEmpty(); + assertThat(summary.getTriggers().getSimple()).isEmpty(); + assertThat(summary.getTriggers().getDailyTimeInterval()).isEmpty(); + assertThat(summary.getTriggers().getCalendarInterval()).containsOnlyKeys("once-a-week"); + assertThat(summary.getTriggers().getCustom()).isEmpty(); + } + + @Test + void quartzTriggerGroupSummaryWithCalendarIntervalTriggerDetails() throws SchedulerException { + TimeZone timeZone = TimeZone.getTimeZone("Europe/Paris"); + Date previousFireTime = Date.from(Instant.parse("2020-11-30T03:00:00Z")); + Date nextFireTime = Date.from(Instant.parse("2020-12-01T03:00:00Z")); + CalendarIntervalTrigger trigger = TriggerBuilder.newTrigger().withIdentity("once-a-week", "samples") + .withPriority(8).withSchedule(CalendarIntervalScheduleBuilder.calendarIntervalSchedule() + .withIntervalInWeeks(1).inTimeZone(timeZone)) + .build(); + ((OperableTrigger) trigger).setPreviousFireTime(previousFireTime); + ((OperableTrigger) trigger).setNextFireTime(nextFireTime); + mockTriggers(trigger); + QuartzTriggerGroupSummary summary = this.endpoint.quartzTriggerGroupSummary("samples"); + Map triggers = summary.getTriggers().getCalendarInterval(); + assertThat(triggers).containsOnlyKeys("once-a-week"); + assertThat(triggers).extractingByKey("once-a-week", nestedMap()).containsOnly( + entry("previousFireTime", previousFireTime), entry("nextFireTime", nextFireTime), entry("priority", 8), + entry("interval", 604800000L), entry("timeZone", timeZone)); + } + + @Test + void quartzTriggerGroupSummaryWithCustomTrigger() throws SchedulerException { + Trigger trigger = mock(Trigger.class); + given(trigger.getKey()).willReturn(TriggerKey.triggerKey("custom", "samples")); + mockTriggers(trigger); + QuartzTriggerGroupSummary summary = this.endpoint.quartzTriggerGroupSummary("samples"); + assertThat(summary.getGroup()).isEqualTo("samples"); + assertThat(summary.isPaused()).isFalse(); + assertThat(summary.getTriggers().getCron()).isEmpty(); + assertThat(summary.getTriggers().getSimple()).isEmpty(); + assertThat(summary.getTriggers().getDailyTimeInterval()).isEmpty(); + assertThat(summary.getTriggers().getCalendarInterval()).isEmpty(); + assertThat(summary.getTriggers().getCustom()).containsOnlyKeys("custom"); + } + + @Test + void quartzTriggerGroupSummaryWithCustomTriggerDetails() throws SchedulerException { + Date previousFireTime = Date.from(Instant.parse("2020-11-30T03:00:00Z")); + Date nextFireTime = Date.from(Instant.parse("2020-12-01T03:00:00Z")); + Trigger trigger = mock(Trigger.class); + given(trigger.getKey()).willReturn(TriggerKey.triggerKey("custom", "samples")); + given(trigger.getPreviousFireTime()).willReturn(previousFireTime); + given(trigger.getNextFireTime()).willReturn(nextFireTime); + given(trigger.getPriority()).willReturn(9); + mockTriggers(trigger); + QuartzTriggerGroupSummary summary = this.endpoint.quartzTriggerGroupSummary("samples"); + Map triggers = summary.getTriggers().getCustom(); + assertThat(triggers).containsOnlyKeys("custom"); + assertThat(triggers).extractingByKey("custom", nestedMap()).containsOnly( + entry("previousFireTime", previousFireTime), entry("nextFireTime", nextFireTime), entry("priority", 9), + entry("trigger", trigger.toString())); + } + + @Test + void quartzTriggerWithCronTrigger() throws SchedulerException { + Date previousFireTime = Date.from(Instant.parse("2020-11-30T03:00:00Z")); + Date nextFireTime = Date.from(Instant.parse("2020-12-01T03:00:00Z")); + TimeZone timeZone = TimeZone.getTimeZone("Europe/Paris"); + CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity("3am-every-day", "samples").withPriority(3) + .withDescription("Sample description") + .withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(3, 0).inTimeZone(timeZone)).build(); + ((OperableTrigger) trigger).setPreviousFireTime(previousFireTime); + ((OperableTrigger) trigger).setNextFireTime(nextFireTime); + mockTriggers(trigger); + given(this.scheduler.getTriggerState(TriggerKey.triggerKey("3am-every-day", "samples"))) + .willReturn(TriggerState.NORMAL); + Map triggerDetails = this.endpoint.quartzTrigger("samples", "3am-every-day"); + assertThat(triggerDetails).contains(entry("group", "samples"), entry("name", "3am-every-day"), + entry("description", "Sample description"), entry("type", "cron"), entry("state", TriggerState.NORMAL), + entry("priority", 3)); + assertThat(triggerDetails).contains(entry("previousFireTime", previousFireTime), + entry("nextFireTime", nextFireTime)); + assertThat(triggerDetails).doesNotContainKeys("simple", "dailyTimeInterval", "calendarInterval", "custom"); + assertThat(triggerDetails).extractingByKey("cron", nestedMap()).containsOnly(entry("expression", "0 0 3 ? * *"), + entry("timeZone", timeZone)); + } + + @Test + void quartzTriggerWithSimpleTrigger() throws SchedulerException { + Date startTime = Date.from(Instant.parse("2020-01-01T09:00:00Z")); + Date previousFireTime = Date.from(Instant.parse("2020-11-30T03:00:00Z")); + Date nextFireTime = Date.from(Instant.parse("2020-12-01T03:00:00Z")); + Date endTime = Date.from(Instant.parse("2020-01-31T09:00:00Z")); + SimpleTrigger trigger = TriggerBuilder.newTrigger().withIdentity("every-hour", "samples").withPriority(20) + .withDescription("Every hour").startAt(startTime).endAt(endTime) + .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInHours(1).withRepeatCount(2000)) + .build(); + ((OperableTrigger) trigger).setPreviousFireTime(previousFireTime); + ((OperableTrigger) trigger).setNextFireTime(nextFireTime); + mockTriggers(trigger); + given(this.scheduler.getTriggerState(TriggerKey.triggerKey("every-hour", "samples"))) + .willReturn(TriggerState.COMPLETE); + Map triggerDetails = this.endpoint.quartzTrigger("samples", "every-hour"); + assertThat(triggerDetails).contains(entry("group", "samples"), entry("name", "every-hour"), + entry("description", "Every hour"), entry("type", "simple"), entry("state", TriggerState.COMPLETE), + entry("priority", 20)); + assertThat(triggerDetails).contains(entry("startTime", startTime), entry("previousFireTime", previousFireTime), + entry("nextFireTime", nextFireTime), entry("endTime", endTime)); + assertThat(triggerDetails).doesNotContainKeys("cron", "dailyTimeInterval", "calendarInterval", "custom"); + assertThat(triggerDetails).extractingByKey("simple", nestedMap()).containsOnly(entry("interval", 3600000L), + entry("repeatCount", 2000), entry("timesTriggered", 0)); + } + + @Test + void quartzTriggerWithDailyTimeIntervalTrigger() throws SchedulerException { + Date previousFireTime = Date.from(Instant.parse("2020-11-30T03:00:00Z")); + Date nextFireTime = Date.from(Instant.parse("2020-12-01T03:00:00Z")); + DailyTimeIntervalTrigger trigger = TriggerBuilder.newTrigger().withIdentity("every-hour-mon-wed", "samples") + .withDescription("Every working hour Mon Wed").withPriority(4) + .withSchedule(DailyTimeIntervalScheduleBuilder.dailyTimeIntervalSchedule() + .onDaysOfTheWeek(Calendar.MONDAY, Calendar.WEDNESDAY) + .startingDailyAt(TimeOfDay.hourAndMinuteOfDay(9, 0)) + .endingDailyAt(TimeOfDay.hourAndMinuteOfDay(18, 0)).withInterval(1, IntervalUnit.HOUR)) + .build(); + ((OperableTrigger) trigger).setPreviousFireTime(previousFireTime); + ((OperableTrigger) trigger).setNextFireTime(nextFireTime); + mockTriggers(trigger); + given(this.scheduler.getTriggerState(TriggerKey.triggerKey("every-hour-mon-wed", "samples"))) + .willReturn(TriggerState.NORMAL); + Map triggerDetails = this.endpoint.quartzTrigger("samples", "every-hour-mon-wed"); + assertThat(triggerDetails).contains(entry("group", "samples"), entry("name", "every-hour-mon-wed"), + entry("description", "Every working hour Mon Wed"), entry("type", "dailyTimeInterval"), + entry("state", TriggerState.NORMAL), entry("priority", 4)); + assertThat(triggerDetails).contains(entry("previousFireTime", previousFireTime), + entry("nextFireTime", nextFireTime)); + assertThat(triggerDetails).doesNotContainKeys("cron", "simple", "calendarInterval", "custom"); + assertThat(triggerDetails).extractingByKey("dailyTimeInterval", nestedMap()).containsOnly( + entry("interval", 3600000L), entry("startTimeOfDay", LocalTime.of(9, 0)), + entry("endTimeOfDay", LocalTime.of(18, 0)), + entry("daysOfWeek", new LinkedHashSet<>(Arrays.asList(2, 4))), entry("repeatCount", -1), + entry("timesTriggered", 0)); + } + + @Test + void quartzTriggerWithCalendarTimeIntervalTrigger() throws SchedulerException { + TimeZone timeZone = TimeZone.getTimeZone("Europe/Paris"); + Date previousFireTime = Date.from(Instant.parse("2020-11-30T03:00:00Z")); + Date nextFireTime = Date.from(Instant.parse("2020-12-01T03:00:00Z")); + CalendarIntervalTrigger trigger = TriggerBuilder.newTrigger().withIdentity("once-a-week", "samples") + .withDescription("Once a week").withPriority(8) + .withSchedule(CalendarIntervalScheduleBuilder.calendarIntervalSchedule().withIntervalInWeeks(1) + .inTimeZone(timeZone).preserveHourOfDayAcrossDaylightSavings(true)) + .build(); + ((OperableTrigger) trigger).setPreviousFireTime(previousFireTime); + ((OperableTrigger) trigger).setNextFireTime(nextFireTime); + mockTriggers(trigger); + given(this.scheduler.getTriggerState(TriggerKey.triggerKey("once-a-week", "samples"))) + .willReturn(TriggerState.BLOCKED); + Map triggerDetails = this.endpoint.quartzTrigger("samples", "once-a-week"); + assertThat(triggerDetails).contains(entry("group", "samples"), entry("name", "once-a-week"), + entry("description", "Once a week"), entry("type", "calendarInterval"), + entry("state", TriggerState.BLOCKED), entry("priority", 8)); + assertThat(triggerDetails).contains(entry("previousFireTime", previousFireTime), + entry("nextFireTime", nextFireTime)); + assertThat(triggerDetails).doesNotContainKeys("cron", "simple", "dailyTimeInterval", "custom"); + assertThat(triggerDetails).extractingByKey("calendarInterval", nestedMap()).containsOnly( + entry("interval", 604800000L), entry("timeZone", timeZone), + entry("preserveHourOfDayAcrossDaylightSavings", true), entry("skipDayIfHourDoesNotExist", false), + entry("timesTriggered", 0)); + } + + @Test + void quartzTriggerWithCustomTrigger() throws SchedulerException { + Date previousFireTime = Date.from(Instant.parse("2020-11-30T03:00:00Z")); + Date nextFireTime = Date.from(Instant.parse("2020-12-01T03:00:00Z")); + Trigger trigger = mock(Trigger.class); + given(trigger.getKey()).willReturn(TriggerKey.triggerKey("custom", "samples")); + given(trigger.getPreviousFireTime()).willReturn(previousFireTime); + given(trigger.getNextFireTime()).willReturn(nextFireTime); + given(trigger.getPriority()).willReturn(9); + mockTriggers(trigger); + given(this.scheduler.getTriggerState(TriggerKey.triggerKey("custom", "samples"))) + .willReturn(TriggerState.ERROR); + Map triggerDetails = this.endpoint.quartzTrigger("samples", "custom"); + assertThat(triggerDetails).contains(entry("group", "samples"), entry("name", "custom"), entry("type", "custom"), + entry("state", TriggerState.ERROR), entry("priority", 9)); + assertThat(triggerDetails).contains(entry("previousFireTime", previousFireTime), + entry("nextFireTime", nextFireTime)); + assertThat(triggerDetails).doesNotContainKeys("cron", "simple", "calendarInterval", "dailyTimeInterval"); + assertThat(triggerDetails).extractingByKey("custom", nestedMap()) + .containsOnly(entry("trigger", trigger.toString())); + } + + @Test + void quartzTriggerWithDataMap() throws SchedulerException { + CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity("3am-every-day", "samples") + .withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(3, 0)).usingJobData("user", "user") + .usingJobData("password", "secret").usingJobData("url", "https://user:secret@example.com").build(); + mockTriggers(trigger); + given(this.scheduler.getTriggerState(TriggerKey.triggerKey("3am-every-day", "samples"))) + .willReturn(TriggerState.NORMAL); + Map triggerDetails = this.endpoint.quartzTrigger("samples", "3am-every-day"); + assertThat(triggerDetails).extractingByKey("data", nestedMap()).containsOnly(entry("user", "user"), + entry("password", "******"), entry("url", "https://user:******@example.com")); + } + + @ParameterizedTest(name = "unit {1}") + @MethodSource("intervalUnitParameters") + void canConvertIntervalUnit(int amount, IntervalUnit unit, Duration expectedDuration) throws SchedulerException { + CalendarIntervalTrigger trigger = TriggerBuilder.newTrigger().withIdentity("trigger", "samples") + .withSchedule(CalendarIntervalScheduleBuilder.calendarIntervalSchedule().withInterval(amount, unit)) + .build(); + mockTriggers(trigger); + Map triggerDetails = this.endpoint.quartzTrigger("samples", "trigger"); + assertThat(triggerDetails).extractingByKey("calendarInterval", nestedMap()) + .contains(entry("interval", expectedDuration.toMillis())); + } + + static Stream intervalUnitParameters() { + return Stream.of(Arguments.of(3, IntervalUnit.DAY, Duration.ofDays(3)), + Arguments.of(2, IntervalUnit.HOUR, Duration.ofHours(2)), + Arguments.of(5, IntervalUnit.MINUTE, Duration.ofMinutes(5)), + Arguments.of(1, IntervalUnit.MONTH, ChronoUnit.MONTHS.getDuration()), + Arguments.of(30, IntervalUnit.SECOND, Duration.ofSeconds(30)), + Arguments.of(100, IntervalUnit.MILLISECOND, Duration.ofMillis(100)), + Arguments.of(1, IntervalUnit.WEEK, ChronoUnit.WEEKS.getDuration()), + Arguments.of(1, IntervalUnit.YEAR, ChronoUnit.YEARS.getDuration())); + } + + @Test + void quartzJobWithoutTrigger() throws SchedulerException { + JobDetail job = JobBuilder.newJob(Job.class).withIdentity("hello", "samples").withDescription("A sample job") + .storeDurably().requestRecovery(false).build(); + mockJobs(job); + QuartzJobDetails jobDetails = this.endpoint.quartzJob("samples", "hello"); + assertThat(jobDetails.getGroup()).isEqualTo("samples"); + assertThat(jobDetails.getName()).isEqualTo("hello"); + assertThat(jobDetails.getDescription()).isEqualTo("A sample job"); + assertThat(jobDetails.getClassName()).isEqualTo(Job.class.getName()); + assertThat(jobDetails.isDurable()).isTrue(); + assertThat(jobDetails.isRequestRecovery()).isFalse(); + assertThat(jobDetails.getData()).isEmpty(); + assertThat(jobDetails.getTriggers()).isEmpty(); + } + + @Test + void quartzJobWithTrigger() throws SchedulerException { + Date previousFireTime = Date.from(Instant.parse("2020-11-30T03:00:00Z")); + Date nextFireTime = Date.from(Instant.parse("2020-12-01T03:00:00Z")); + JobDetail job = JobBuilder.newJob(Job.class).withIdentity("hello", "samples").build(); + TimeZone timeZone = TimeZone.getTimeZone("Europe/Paris"); + Trigger trigger = TriggerBuilder.newTrigger().withIdentity("3am-every-day", "samples").withPriority(4) + .withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(3, 0).inTimeZone(timeZone)).build(); + ((OperableTrigger) trigger).setPreviousFireTime(previousFireTime); + ((OperableTrigger) trigger).setNextFireTime(nextFireTime); + mockJobs(job); + mockTriggers(trigger); + given(this.scheduler.getTriggersOfJob(JobKey.jobKey("hello", "samples"))) + .willAnswer((invocation) -> Collections.singletonList(trigger)); + QuartzJobDetails jobDetails = this.endpoint.quartzJob("samples", "hello"); + assertThat(jobDetails.getTriggers()).hasSize(1); + Map triggerDetails = jobDetails.getTriggers().get(0); + assertThat(triggerDetails).containsOnly(entry("group", "samples"), entry("name", "3am-every-day"), + entry("previousFireTime", previousFireTime), entry("nextFireTime", nextFireTime), entry("priority", 4)); + } + + @Test + void quartzJobOrdersTriggersAccordingToNextFireTime() throws SchedulerException { + JobDetail job = JobBuilder.newJob(Job.class).withIdentity("hello", "samples").build(); + mockJobs(job); + Date triggerOneNextFireTime = Date.from(Instant.parse("2020-12-01T03:00:00Z")); + CronTrigger triggerOne = TriggerBuilder.newTrigger().withIdentity("one", "samples").withPriority(5) + .withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(3, 0)).build(); + ((OperableTrigger) triggerOne).setNextFireTime(triggerOneNextFireTime); + Date triggerTwoNextFireTime = Date.from(Instant.parse("2020-12-01T02:00:00Z")); + CronTrigger triggerTwo = TriggerBuilder.newTrigger().withIdentity("two", "samples").withPriority(10) + .withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(2, 0)).build(); + ((OperableTrigger) triggerTwo).setNextFireTime(triggerTwoNextFireTime); + mockTriggers(triggerOne, triggerTwo); + given(this.scheduler.getTriggersOfJob(JobKey.jobKey("hello", "samples"))) + .willAnswer((invocation) -> Arrays.asList(triggerOne, triggerTwo)); + QuartzJobDetails jobDetails = this.endpoint.quartzJob("samples", "hello"); + assertThat(jobDetails.getTriggers()).hasSize(2); + assertThat(jobDetails.getTriggers().get(0)).containsEntry("name", "two"); + assertThat(jobDetails.getTriggers().get(1)).containsEntry("name", "one"); + } + + @Test + void quartzJobOrdersTriggersAccordingNextFireTimeAndPriority() throws SchedulerException { + JobDetail job = JobBuilder.newJob(Job.class).withIdentity("hello", "samples").build(); + mockJobs(job); + Date nextFireTime = Date.from(Instant.parse("2020-12-01T03:00:00Z")); + CronTrigger triggerOne = TriggerBuilder.newTrigger().withIdentity("one", "samples").withPriority(3) + .withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(3, 0)).build(); + ((OperableTrigger) triggerOne).setNextFireTime(nextFireTime); + CronTrigger triggerTwo = TriggerBuilder.newTrigger().withIdentity("two", "samples").withPriority(7) + .withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(3, 0)).build(); + ((OperableTrigger) triggerTwo).setNextFireTime(nextFireTime); + mockTriggers(triggerOne, triggerTwo); + given(this.scheduler.getTriggersOfJob(JobKey.jobKey("hello", "samples"))) + .willAnswer((invocation) -> Arrays.asList(triggerOne, triggerTwo)); + QuartzJobDetails jobDetails = this.endpoint.quartzJob("samples", "hello"); + assertThat(jobDetails.getTriggers()).hasSize(2); + assertThat(jobDetails.getTriggers().get(0)).containsEntry("name", "two"); + assertThat(jobDetails.getTriggers().get(1)).containsEntry("name", "one"); + } + + @Test + void quartzJobWithSensitiveDataMap() throws SchedulerException { + JobDetail job = JobBuilder.newJob(Job.class).withIdentity("hello", "samples").usingJobData("user", "user") + .usingJobData("password", "secret").usingJobData("url", "https://user:secret@example.com").build(); + mockJobs(job); + QuartzJobDetails jobDetails = this.endpoint.quartzJob("samples", "hello"); + assertThat(jobDetails.getData()).containsOnly(entry("user", "user"), entry("password", "******"), + entry("url", "https://user:******@example.com")); + } + + @Test + void quartzJobWithSensitiveDataMapAndCustomSanitizier() throws SchedulerException { + JobDetail job = JobBuilder.newJob(Job.class).withIdentity("hello", "samples").usingJobData("test", "value") + .usingJobData("secret", "value").build(); + mockJobs(job); + Sanitizer sanitizer = mock(Sanitizer.class); + given(sanitizer.sanitize("test", "value")).willReturn("value"); + given(sanitizer.sanitize("secret", "value")).willReturn("----"); + QuartzJobDetails jobDetails = new QuartzEndpoint(this.scheduler, sanitizer).quartzJob("samples", "hello"); + assertThat(jobDetails.getData()).containsOnly(entry("test", "value"), entry("secret", "----")); + then(sanitizer).should().sanitize("test", "value"); + then(sanitizer).should().sanitize("secret", "value"); + then(sanitizer).shouldHaveNoMoreInteractions(); + } + + private void mockJobs(JobDetail... jobs) throws SchedulerException { + MultiValueMap jobKeys = new LinkedMultiValueMap<>(); + for (JobDetail jobDetail : jobs) { + JobKey key = jobDetail.getKey(); + given(this.scheduler.getJobDetail(key)).willReturn(jobDetail); + jobKeys.add(key.getGroup(), key); + } + given(this.scheduler.getJobGroupNames()).willReturn(new ArrayList<>(jobKeys.keySet())); + for (Entry> entry : jobKeys.entrySet()) { + given(this.scheduler.getJobKeys(GroupMatcher.jobGroupEquals(entry.getKey()))) + .willReturn(new LinkedHashSet<>(entry.getValue())); + } + } + + private void mockTriggers(Trigger... triggers) throws SchedulerException { + MultiValueMap triggerKeys = new LinkedMultiValueMap<>(); + for (Trigger trigger : triggers) { + TriggerKey key = trigger.getKey(); + given(this.scheduler.getTrigger(key)).willReturn(trigger); + triggerKeys.add(key.getGroup(), key); + } + given(this.scheduler.getTriggerGroupNames()).willReturn(new ArrayList<>(triggerKeys.keySet())); + for (Entry> entry : triggerKeys.entrySet()) { + given(this.scheduler.getTriggerKeys(GroupMatcher.triggerGroupEquals(entry.getKey()))) + .willReturn(new LinkedHashSet<>(entry.getValue())); + } + } + + @SuppressWarnings("rawtypes") + private static InstanceOfAssertFactory> nestedMap() { + return InstanceOfAssertFactories.map(String.class, Object.class); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/quartz/QuartzEndpointWebIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/quartz/QuartzEndpointWebIntegrationTests.java new file mode 100644 index 000000000000..cd58eddaa342 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/quartz/QuartzEndpointWebIntegrationTests.java @@ -0,0 +1,217 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.quartz; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map.Entry; + +import net.minidev.json.JSONArray; +import org.quartz.CalendarIntervalScheduleBuilder; +import org.quartz.CalendarIntervalTrigger; +import org.quartz.CronScheduleBuilder; +import org.quartz.CronTrigger; +import org.quartz.Job; +import org.quartz.JobBuilder; +import org.quartz.JobDataMap; +import org.quartz.JobDetail; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.SimpleScheduleBuilder; +import org.quartz.SimpleTrigger; +import org.quartz.Trigger; +import org.quartz.Trigger.TriggerState; +import org.quartz.TriggerBuilder; +import org.quartz.TriggerKey; +import org.quartz.impl.matchers.GroupMatcher; + +import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.quartz.DelegatingJob; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Integration tests for {@link QuartzEndpoint} exposed by Jersey, Spring MVC, and + * WebFlux. + * + * @author Stephane Nicoll + */ +class QuartzEndpointWebIntegrationTests { + + private static final JobDetail jobOne = JobBuilder.newJob(Job.class).withIdentity("jobOne", "samples") + .usingJobData(new JobDataMap(Collections.singletonMap("name", "test"))).withDescription("A sample job") + .build(); + + private static final JobDetail jobTwo = JobBuilder.newJob(DelegatingJob.class).withIdentity("jobTwo", "samples") + .build(); + + private static final JobDetail jobThree = JobBuilder.newJob(Job.class).withIdentity("jobThree").build(); + + private static final CronTrigger triggerOne = TriggerBuilder.newTrigger().withDescription("Once a day 3AM") + .withIdentity("triggerOne").withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(3, 0)).build(); + + private static final SimpleTrigger triggerTwo = TriggerBuilder.newTrigger().withDescription("Once a day") + .withIdentity("triggerTwo", "tests").withSchedule(SimpleScheduleBuilder.repeatHourlyForever(24)).build(); + + private static final CalendarIntervalTrigger triggerThree = TriggerBuilder.newTrigger() + .withDescription("Once a week").withIdentity("triggerThree", "tests") + .withSchedule(CalendarIntervalScheduleBuilder.calendarIntervalSchedule().withIntervalInWeeks(1)).build(); + + @WebEndpointTest + void quartzReport(WebTestClient client) { + client.get().uri("/actuator/quartz").exchange().expectStatus().isOk().expectBody().jsonPath("jobs.groups") + .isEqualTo(new JSONArray().appendElement("samples").appendElement("DEFAULT")) + .jsonPath("triggers.groups").isEqualTo(new JSONArray().appendElement("DEFAULT").appendElement("tests")); + } + + @WebEndpointTest + void quartzJobNames(WebTestClient client) { + client.get().uri("/actuator/quartz/jobs").exchange().expectStatus().isOk().expectBody() + .jsonPath("groups.samples.jobs") + .isEqualTo(new JSONArray().appendElement("jobOne").appendElement("jobTwo")) + .jsonPath("groups.DEFAULT.jobs").isEqualTo(new JSONArray().appendElement("jobThree")); + } + + @WebEndpointTest + void quartzTriggerNames(WebTestClient client) { + client.get().uri("/actuator/quartz/triggers").exchange().expectStatus().isOk().expectBody() + .jsonPath("groups.DEFAULT.paused").isEqualTo(false).jsonPath("groups.DEFAULT.triggers") + .isEqualTo(new JSONArray().appendElement("triggerOne")).jsonPath("groups.tests.paused").isEqualTo(false) + .jsonPath("groups.tests.triggers") + .isEqualTo(new JSONArray().appendElement("triggerTwo").appendElement("triggerThree")); + } + + @WebEndpointTest + void quartzTriggersOrJobsAreAllowed(WebTestClient client) { + client.get().uri("/actuator/quartz/something-else").exchange().expectStatus().isBadRequest(); + } + + @WebEndpointTest + void quartzJobGroupSummary(WebTestClient client) { + client.get().uri("/actuator/quartz/jobs/samples").exchange().expectStatus().isOk().expectBody() + .jsonPath("group").isEqualTo("samples").jsonPath("jobs.jobOne.className").isEqualTo(Job.class.getName()) + .jsonPath("jobs.jobTwo.className").isEqualTo(DelegatingJob.class.getName()); + } + + @WebEndpointTest + void quartzJobGroupSummaryWithUnknownGroup(WebTestClient client) { + client.get().uri("/actuator/quartz/jobs/does-not-exist").exchange().expectStatus().isNotFound(); + } + + @WebEndpointTest + void quartzTriggerGroupSummary(WebTestClient client) { + client.get().uri("/actuator/quartz/triggers/tests").exchange().expectStatus().isOk().expectBody() + .jsonPath("group").isEqualTo("tests").jsonPath("paused").isEqualTo("false").jsonPath("triggers.cron") + .isEmpty().jsonPath("triggers.simple.triggerTwo.interval").isEqualTo(86400000) + .jsonPath("triggers.dailyTimeInterval").isEmpty() + .jsonPath("triggers.calendarInterval.triggerThree.interval").isEqualTo(604800000) + .jsonPath("triggers.custom").isEmpty(); + } + + @WebEndpointTest + void quartzTriggerGroupSummaryWithUnknownGroup(WebTestClient client) { + client.get().uri("/actuator/quartz/triggers/does-not-exist").exchange().expectStatus().isNotFound(); + } + + @WebEndpointTest + void quartzJobDetail(WebTestClient client) { + client.get().uri("/actuator/quartz/jobs/samples/jobOne").exchange().expectStatus().isOk().expectBody() + .jsonPath("group").isEqualTo("samples").jsonPath("name").isEqualTo("jobOne").jsonPath("data.name") + .isEqualTo("test"); + } + + @WebEndpointTest + void quartzJobDetailWithUnknownKey(WebTestClient client) { + client.get().uri("/actuator/quartz/jobs/samples/does-not-exist").exchange().expectStatus().isNotFound(); + } + + @WebEndpointTest + void quartzTriggerDetail(WebTestClient client) { + client.get().uri("/actuator/quartz/triggers/DEFAULT/triggerOne").exchange().expectStatus().isOk().expectBody() + .jsonPath("group").isEqualTo("DEFAULT").jsonPath("name").isEqualTo("triggerOne").jsonPath("description") + .isEqualTo("Once a day 3AM").jsonPath("state").isEqualTo("NORMAL").jsonPath("type").isEqualTo("cron") + .jsonPath("simple").doesNotExist().jsonPath("calendarInterval").doesNotExist().jsonPath("dailyInterval") + .doesNotExist().jsonPath("custom").doesNotExist().jsonPath("cron.expression").isEqualTo("0 0 3 ? * *"); + } + + @WebEndpointTest + void quartzTriggerDetailWithUnknownKey(WebTestClient client) { + client.get().uri("/actuator/quartz/triggers/tests/does-not-exist").exchange().expectStatus().isNotFound(); + } + + @Configuration(proxyBeanMethods = false) + static class TestConfiguration { + + @Bean + Scheduler scheduler() throws SchedulerException { + Scheduler scheduler = mock(Scheduler.class); + mockJobs(scheduler, jobOne, jobTwo, jobThree); + mockTriggers(scheduler, triggerOne, triggerTwo, triggerThree); + return scheduler; + } + + @Bean + QuartzEndpoint endpoint(Scheduler scheduler) { + return new QuartzEndpoint(scheduler); + } + + @Bean + QuartzEndpointWebExtension quartzEndpointWebExtension(QuartzEndpoint endpoint) { + return new QuartzEndpointWebExtension(endpoint); + } + + private void mockJobs(Scheduler scheduler, JobDetail... jobs) throws SchedulerException { + MultiValueMap jobKeys = new LinkedMultiValueMap<>(); + for (JobDetail jobDetail : jobs) { + JobKey key = jobDetail.getKey(); + given(scheduler.getJobDetail(key)).willReturn(jobDetail); + jobKeys.add(key.getGroup(), key); + } + given(scheduler.getJobGroupNames()).willReturn(new ArrayList<>(jobKeys.keySet())); + for (Entry> entry : jobKeys.entrySet()) { + given(scheduler.getJobKeys(GroupMatcher.jobGroupEquals(entry.getKey()))) + .willReturn(new LinkedHashSet<>(entry.getValue())); + } + } + + void mockTriggers(Scheduler scheduler, Trigger... triggers) throws SchedulerException { + MultiValueMap triggerKeys = new LinkedMultiValueMap<>(); + for (Trigger trigger : triggers) { + TriggerKey key = trigger.getKey(); + given(scheduler.getTrigger(key)).willReturn(trigger); + given(scheduler.getTriggerState(key)).willReturn(TriggerState.NORMAL); + triggerKeys.add(key.getGroup(), key); + } + given(scheduler.getTriggerGroupNames()).willReturn(new ArrayList<>(triggerKeys.keySet())); + for (Entry> entry : triggerKeys.entrySet()) { + given(scheduler.getTriggerKeys(GroupMatcher.triggerGroupEquals(entry.getKey()))) + .willReturn(new LinkedHashSet<>(entry.getValue())); + } + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/r2dbc/ConnectionFactoryHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/r2dbc/ConnectionFactoryHealthIndicatorTests.java new file mode 100644 index 000000000000..0622036d9c67 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/r2dbc/ConnectionFactoryHealthIndicatorTests.java @@ -0,0 +1,142 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.r2dbc; + +import java.util.Collections; +import java.util.UUID; + +import io.r2dbc.h2.CloseableConnectionFactory; +import io.r2dbc.h2.H2ConnectionFactory; +import io.r2dbc.h2.H2ConnectionOption; +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.Result; +import io.r2dbc.spi.ValidationDepth; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.boot.actuate.health.ReactiveHealthIndicator; +import org.springframework.boot.actuate.health.Status; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ConnectionFactoryHealthIndicator}. + * + * @author Mark Paluch + * @author Stephane Nicoll + */ +class ConnectionFactoryHealthIndicatorTests { + + @Test + void healthIndicatorWhenDatabaseUpWithConnectionValidation() { + CloseableConnectionFactory connectionFactory = createTestDatabase(); + try { + ConnectionFactoryHealthIndicator healthIndicator = new ConnectionFactoryHealthIndicator(connectionFactory); + healthIndicator.health().as(StepVerifier::create).assertNext((actual) -> { + assertThat(actual.getStatus()).isEqualTo(Status.UP); + assertThat(actual.getDetails()).containsOnly(entry("database", "H2"), + entry("validationQuery", "validate(REMOTE)")); + }).verifyComplete(); + } + finally { + StepVerifier.create(connectionFactory.close()).verifyComplete(); + } + } + + @Test + void healthIndicatorWhenDatabaseDownWithConnectionValidation() { + ConnectionFactory connectionFactory = mock(ConnectionFactory.class); + given(connectionFactory.getMetadata()).willReturn(() -> "mock"); + RuntimeException exception = new RuntimeException("test"); + given(connectionFactory.create()).willReturn(Mono.error(exception)); + ConnectionFactoryHealthIndicator healthIndicator = new ConnectionFactoryHealthIndicator(connectionFactory); + healthIndicator.health().as(StepVerifier::create).assertNext((actual) -> { + assertThat(actual.getStatus()).isEqualTo(Status.DOWN); + assertThat(actual.getDetails()).containsOnly(entry("database", "mock"), + entry("validationQuery", "validate(REMOTE)"), entry("error", "java.lang.RuntimeException: test")); + }).verifyComplete(); + } + + @Test + void healthIndicatorWhenConnectionValidationFails() { + ConnectionFactory connectionFactory = mock(ConnectionFactory.class); + given(connectionFactory.getMetadata()).willReturn(() -> "mock"); + Connection connection = mock(Connection.class); + given(connection.validate(ValidationDepth.REMOTE)).willReturn(Mono.just(false)); + given(connection.close()).willReturn(Mono.empty()); + given(connectionFactory.create()).willAnswer((invocation) -> Mono.just(connection)); + ConnectionFactoryHealthIndicator healthIndicator = new ConnectionFactoryHealthIndicator(connectionFactory); + healthIndicator.health().as(StepVerifier::create).assertNext((actual) -> { + assertThat(actual.getStatus()).isEqualTo(Status.DOWN); + assertThat(actual.getDetails()).containsOnly(entry("database", "mock"), + entry("validationQuery", "validate(REMOTE)")); + }).verifyComplete(); + } + + @Test + void healthIndicatorWhenDatabaseUpWithSuccessValidationQuery() { + CloseableConnectionFactory connectionFactory = createTestDatabase(); + try { + String customValidationQuery = "SELECT COUNT(*) from HEALTH_TEST"; + Mono.from(connectionFactory.create()).flatMapMany((it) -> Flux + .from(it.createStatement("CREATE TABLE HEALTH_TEST (id INTEGER IDENTITY PRIMARY KEY)").execute()) + .flatMap(Result::getRowsUpdated).thenMany(it.close())).as(StepVerifier::create).verifyComplete(); + ReactiveHealthIndicator healthIndicator = new ConnectionFactoryHealthIndicator(connectionFactory, + customValidationQuery); + healthIndicator.health().as(StepVerifier::create).assertNext((actual) -> { + assertThat(actual.getStatus()).isEqualTo(Status.UP); + assertThat(actual.getDetails()).containsOnly(entry("database", "H2"), entry("result", 0L), + entry("validationQuery", customValidationQuery)); + }).verifyComplete(); + } + finally { + StepVerifier.create(connectionFactory.close()).verifyComplete(); + } + + } + + @Test + void healthIndicatorWhenDatabaseUpWithFailureValidationQuery() { + CloseableConnectionFactory connectionFactory = createTestDatabase(); + try { + String invalidValidationQuery = "SELECT COUNT(*) from DOES_NOT_EXIST"; + ReactiveHealthIndicator healthIndicator = new ConnectionFactoryHealthIndicator(connectionFactory, + invalidValidationQuery); + healthIndicator.health().as(StepVerifier::create).assertNext((actual) -> { + assertThat(actual.getStatus()).isEqualTo(Status.DOWN); + assertThat(actual.getDetails()).contains(entry("database", "H2"), + entry("validationQuery", invalidValidationQuery)); + assertThat(actual.getDetails()).containsOnlyKeys("database", "error", "validationQuery"); + }).verifyComplete(); + } + finally { + StepVerifier.create(connectionFactory.close()).verifyComplete(); + } + } + + private CloseableConnectionFactory createTestDatabase() { + return H2ConnectionFactory.inMemory("db-" + UUID.randomUUID(), "sa", "", + Collections.singletonMap(H2ConnectionOption.DB_CLOSE_DELAY, "-1")); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/redis/RedisHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/redis/RedisHealthIndicatorTests.java index 78c47a063abb..564f68e07025 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/redis/RedisHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/redis/RedisHealthIndicatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,9 +33,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link RedisHealthIndicator}. @@ -51,7 +51,7 @@ void redisIsUp() { Properties info = new Properties(); info.put("redis_version", "2.8.9"); RedisConnection redisConnection = mock(RedisConnection.class); - given(redisConnection.info()).willReturn(info); + given(redisConnection.info("server")).willReturn(info); RedisHealthIndicator healthIndicator = createHealthIndicator(redisConnection); Health health = healthIndicator.health(); assertThat(health.getStatus()).isEqualTo(Status.UP); @@ -61,25 +61,64 @@ void redisIsUp() { @Test void redisIsDown() { RedisConnection redisConnection = mock(RedisConnection.class); - given(redisConnection.info()).willThrow(new RedisConnectionFailureException("Connection failed")); + given(redisConnection.info("server")).willThrow(new RedisConnectionFailureException("Connection failed")); RedisHealthIndicator healthIndicator = createHealthIndicator(redisConnection); Health health = healthIndicator.health(); assertThat(health.getStatus()).isEqualTo(Status.DOWN); assertThat((String) health.getDetails().get("error")).contains("Connection failed"); } + @Test + void healthWhenClusterStateIsAbsentShouldBeUp() { + RedisConnectionFactory redisConnectionFactory = createClusterConnectionFactory(null); + RedisHealthIndicator healthIndicator = new RedisHealthIndicator(redisConnectionFactory); + Health health = healthIndicator.health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertThat(health.getDetails().get("cluster_size")).isEqualTo(4L); + assertThat(health.getDetails().get("slots_up")).isEqualTo(4L); + assertThat(health.getDetails().get("slots_fail")).isEqualTo(0L); + then(redisConnectionFactory).should(atLeastOnce()).getConnection(); + } + + @Test + void healthWhenClusterStateIsOkShouldBeUp() { + RedisConnectionFactory redisConnectionFactory = createClusterConnectionFactory("ok"); + RedisHealthIndicator healthIndicator = new RedisHealthIndicator(redisConnectionFactory); + Health health = healthIndicator.health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertThat(health.getDetails().get("cluster_size")).isEqualTo(4L); + assertThat(health.getDetails().get("slots_up")).isEqualTo(4L); + assertThat(health.getDetails().get("slots_fail")).isEqualTo(0L); + then(redisConnectionFactory).should(atLeastOnce()).getConnection(); + } + + @Test + void healthWhenClusterStateIsFailShouldBeDown() { + RedisConnectionFactory redisConnectionFactory = createClusterConnectionFactory("fail"); + RedisHealthIndicator healthIndicator = new RedisHealthIndicator(redisConnectionFactory); + Health health = healthIndicator.health(); + assertThat(health.getStatus()).isEqualTo(Status.DOWN); + assertThat(health.getDetails().get("cluster_size")).isEqualTo(4L); + assertThat(health.getDetails().get("slots_up")).isEqualTo(3L); + assertThat(health.getDetails().get("slots_fail")).isEqualTo(1L); + then(redisConnectionFactory).should(atLeastOnce()).getConnection(); + } + private RedisHealthIndicator createHealthIndicator(RedisConnection redisConnection) { RedisConnectionFactory redisConnectionFactory = mock(RedisConnectionFactory.class); given(redisConnectionFactory.getConnection()).willReturn(redisConnection); return new RedisHealthIndicator(redisConnectionFactory); } - @Test - void redisClusterIsUp() { + private RedisConnectionFactory createClusterConnectionFactory(String state) { Properties clusterProperties = new Properties(); + if (state != null) { + clusterProperties.setProperty("cluster_state", state); + } clusterProperties.setProperty("cluster_size", "4"); - clusterProperties.setProperty("cluster_slots_ok", "4"); - clusterProperties.setProperty("cluster_slots_fail", "0"); + boolean failure = "fail".equals(state); + clusterProperties.setProperty("cluster_slots_ok", failure ? "3" : "4"); + clusterProperties.setProperty("cluster_slots_fail", failure ? "1" : "0"); List redisMasterNodes = Arrays.asList(new RedisClusterNode("127.0.0.1", 7001), new RedisClusterNode("127.0.0.2", 7001)); RedisClusterConnection redisConnection = mock(RedisClusterConnection.class); @@ -87,13 +126,7 @@ void redisClusterIsUp() { given(redisConnection.clusterGetClusterInfo()).willReturn(new ClusterInfo(clusterProperties)); RedisConnectionFactory redisConnectionFactory = mock(RedisConnectionFactory.class); given(redisConnectionFactory.getConnection()).willReturn(redisConnection); - RedisHealthIndicator healthIndicator = new RedisHealthIndicator(redisConnectionFactory); - Health health = healthIndicator.health(); - assertThat(health.getStatus()).isEqualTo(Status.UP); - assertThat(health.getDetails().get("cluster_size")).isEqualTo(4L); - assertThat(health.getDetails().get("slots_up")).isEqualTo(4L); - assertThat(health.getDetails().get("slots_fail")).isEqualTo(0L); - verify(redisConnectionFactory, atLeastOnce()).getConnection(); + return redisConnectionFactory; } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/redis/RedisReactiveHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/redis/RedisReactiveHealthIndicatorTests.java index 6fcce879c30b..f45b25eb922d 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/redis/RedisReactiveHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/redis/RedisReactiveHealthIndicatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,14 +26,16 @@ import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.Status; import org.springframework.data.redis.RedisConnectionFailureException; +import org.springframework.data.redis.connection.ClusterInfo; +import org.springframework.data.redis.connection.ReactiveRedisClusterConnection; import org.springframework.data.redis.connection.ReactiveRedisConnection; import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory; import org.springframework.data.redis.connection.ReactiveServerCommands; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link RedisReactiveHealthIndicator}. @@ -42,6 +44,7 @@ * @author Mark Paluch * @author Nikolay Rybak * @author Artsiom Yudovin + * @author Scott Frederick */ class RedisReactiveHealthIndicatorTests { @@ -52,7 +55,7 @@ void redisIsUp() { ReactiveRedisConnection redisConnection = mock(ReactiveRedisConnection.class); given(redisConnection.closeLater()).willReturn(Mono.empty()); ReactiveServerCommands commands = mock(ReactiveServerCommands.class); - given(commands.info()).willReturn(Mono.just(info)); + given(commands.info("server")).willReturn(Mono.just(info)); RedisReactiveHealthIndicator healthIndicator = createHealthIndicator(redisConnection, commands); Mono health = healthIndicator.health(); StepVerifier.create(health).consumeNextWith((h) -> { @@ -60,20 +63,59 @@ void redisIsUp() { assertThat(h.getDetails()).containsOnlyKeys("version"); assertThat(h.getDetails().get("version")).isEqualTo("2.8.9"); }).verifyComplete(); - verify(redisConnection).closeLater(); + then(redisConnection).should().closeLater(); + } + + @Test + void healthWhenClusterStateIsAbsentShouldBeUp() { + ReactiveRedisConnectionFactory redisConnectionFactory = createClusterConnectionFactory(null); + RedisReactiveHealthIndicator healthIndicator = new RedisReactiveHealthIndicator(redisConnectionFactory); + Mono health = healthIndicator.health(); + StepVerifier.create(health).consumeNextWith((h) -> { + assertThat(h.getStatus()).isEqualTo(Status.UP); + assertThat(h.getDetails().get("cluster_size")).isEqualTo(4L); + assertThat(h.getDetails().get("slots_up")).isEqualTo(4L); + assertThat(h.getDetails().get("slots_fail")).isEqualTo(0L); + }).verifyComplete(); + then(redisConnectionFactory.getReactiveConnection()).should().closeLater(); + } + + @Test + void healthWhenClusterStateIsOkShouldBeUp() { + ReactiveRedisConnectionFactory redisConnectionFactory = createClusterConnectionFactory("ok"); + RedisReactiveHealthIndicator healthIndicator = new RedisReactiveHealthIndicator(redisConnectionFactory); + Mono health = healthIndicator.health(); + StepVerifier.create(health).consumeNextWith((h) -> { + assertThat(h.getStatus()).isEqualTo(Status.UP); + assertThat(h.getDetails().get("cluster_size")).isEqualTo(4L); + assertThat(h.getDetails().get("slots_up")).isEqualTo(4L); + assertThat(h.getDetails().get("slots_fail")).isEqualTo(0L); + }).verifyComplete(); + } + + @Test + void healthWhenClusterStateIsFailShouldBeDown() { + ReactiveRedisConnectionFactory redisConnectionFactory = createClusterConnectionFactory("fail"); + RedisReactiveHealthIndicator healthIndicator = new RedisReactiveHealthIndicator(redisConnectionFactory); + Mono health = healthIndicator.health(); + StepVerifier.create(health).consumeNextWith((h) -> { + assertThat(h.getStatus()).isEqualTo(Status.DOWN); + assertThat(h.getDetails().get("slots_up")).isEqualTo(3L); + assertThat(h.getDetails().get("slots_fail")).isEqualTo(1L); + }).verifyComplete(); } @Test void redisCommandIsDown() { ReactiveServerCommands commands = mock(ReactiveServerCommands.class); - given(commands.info()).willReturn(Mono.error(new RedisConnectionFailureException("Connection failed"))); + given(commands.info("server")).willReturn(Mono.error(new RedisConnectionFailureException("Connection failed"))); ReactiveRedisConnection redisConnection = mock(ReactiveRedisConnection.class); given(redisConnection.closeLater()).willReturn(Mono.empty()); RedisReactiveHealthIndicator healthIndicator = createHealthIndicator(redisConnection, commands); Mono health = healthIndicator.health(); StepVerifier.create(health).consumeNextWith((h) -> assertThat(h.getStatus()).isEqualTo(Status.DOWN)) .verifyComplete(); - verify(redisConnection).closeLater(); + then(redisConnection).should().closeLater(); } @Test @@ -89,11 +131,27 @@ void redisConnectionIsDown() { private RedisReactiveHealthIndicator createHealthIndicator(ReactiveRedisConnection redisConnection, ReactiveServerCommands serverCommands) { - ReactiveRedisConnectionFactory redisConnectionFactory = mock(ReactiveRedisConnectionFactory.class); given(redisConnectionFactory.getReactiveConnection()).willReturn(redisConnection); given(redisConnection.serverCommands()).willReturn(serverCommands); return new RedisReactiveHealthIndicator(redisConnectionFactory); } + private ReactiveRedisConnectionFactory createClusterConnectionFactory(String state) { + Properties clusterProperties = new Properties(); + if (state != null) { + clusterProperties.setProperty("cluster_state", state); + } + clusterProperties.setProperty("cluster_size", "4"); + boolean failure = "fail".equals(state); + clusterProperties.setProperty("cluster_slots_ok", failure ? "3" : "4"); + clusterProperties.setProperty("cluster_slots_fail", failure ? "1" : "0"); + ReactiveRedisClusterConnection redisConnection = mock(ReactiveRedisClusterConnection.class); + given(redisConnection.closeLater()).willReturn(Mono.empty()); + given(redisConnection.clusterGetClusterInfo()).willReturn(Mono.just(new ClusterInfo(clusterProperties))); + ReactiveRedisConnectionFactory redisConnectionFactory = mock(ReactiveRedisConnectionFactory.class); + given(redisConnectionFactory.getReactiveConnection()).willReturn(redisConnection); + return redisConnectionFactory; + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/security/AuthenticationAuditListenerTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/security/AuthenticationAuditListenerTests.java index e74db119462c..77f973c65649 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/security/AuthenticationAuditListenerTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/security/AuthenticationAuditListenerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,9 +35,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; /** * Tests for {@link AuthenticationAuditListener}. @@ -65,7 +65,7 @@ void testOtherAuthenticationSuccess() { this.listener.onApplicationEvent(new InteractiveAuthenticationSuccessEvent( new UsernamePasswordAuthenticationToken("user", "password"), getClass())); // No need to audit this one (it shadows a regular AuthenticationSuccessEvent) - verify(this.publisher, never()).publishEvent(any(ApplicationEvent.class)); + then(this.publisher).should(never()).publishEvent(any(ApplicationEvent.class)); } @Test @@ -105,7 +105,7 @@ void testDetailsAreIncludedInAuditEvent() { private AuditApplicationEvent handleAuthenticationEvent(AbstractAuthenticationEvent event) { ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(AuditApplicationEvent.class); this.listener.onApplicationEvent(event); - verify(this.publisher).publishEvent(eventCaptor.capture()); + then(this.publisher).should().publishEvent(eventCaptor.capture()); return eventCaptor.getValue(); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/security/AuthorizationAuditListenerTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/security/AuthorizationAuditListenerTests.java index 3fb709256e89..cbda5c70b17d 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/security/AuthorizationAuditListenerTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/security/AuthorizationAuditListenerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,8 +33,8 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link AuthorizationAuditListener}. @@ -82,7 +82,7 @@ void testDetailsAreIncludedInAuditEvent() { private AuditApplicationEvent handleAuthorizationEvent(AbstractAuthorizationEvent event) { ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(AuditApplicationEvent.class); this.listener.onApplicationEvent(event); - verify(this.publisher).publishEvent(eventCaptor.capture()); + then(this.publisher).should().publishEvent(eventCaptor.capture()); return eventCaptor.getValue(); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointTests.java index 32a48b4abf21..5095187a5664 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,8 +28,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link SessionsEndpoint}. @@ -80,7 +80,7 @@ void getSessionWithIdNotFound() { @Test void deleteSession() { this.endpoint.deleteSession(session.getId()); - verify(this.repository).deleteById(session.getId()); + then(this.repository).should().deleteById(session.getId()); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/solr/SolrHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/solr/SolrHealthIndicatorTests.java index b920b9ac14e1..31223253ac56 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/solr/SolrHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/solr/SolrHealthIndicatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,21 +23,18 @@ import org.apache.solr.client.solrj.request.CoreAdminRequest; import org.apache.solr.client.solrj.response.SolrPingResponse; import org.apache.solr.common.util.NamedList; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.Status; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; /** * Tests for {@link SolrHealthIndicator} @@ -48,23 +45,14 @@ */ class SolrHealthIndicatorTests { - private AnnotationConfigApplicationContext context; - - @AfterEach - void close() { - if (this.context != null) { - this.context.close(); - } - } - @Test void healthWhenSolrStatusUpAndBaseUrlPointsToRootReturnsUp() throws Exception { SolrClient solrClient = mock(SolrClient.class); given(solrClient.request(any(CoreAdminRequest.class), isNull())).willReturn(mockResponse(0)); SolrHealthIndicator healthIndicator = new SolrHealthIndicator(solrClient); assertHealth(healthIndicator, Status.UP, 0, "root"); - verify(solrClient, times(1)).request(any(CoreAdminRequest.class), isNull()); - verifyNoMoreInteractions(solrClient); + then(solrClient).should().request(any(CoreAdminRequest.class), isNull()); + then(solrClient).shouldHaveNoMoreInteractions(); } @Test @@ -73,8 +61,8 @@ void healthWhenSolrStatusDownAndBaseUrlPointsToRootReturnsDown() throws Exceptio given(solrClient.request(any(CoreAdminRequest.class), isNull())).willReturn(mockResponse(400)); SolrHealthIndicator healthIndicator = new SolrHealthIndicator(solrClient); assertHealth(healthIndicator, Status.DOWN, 400, "root"); - verify(solrClient, times(1)).request(any(CoreAdminRequest.class), isNull()); - verifyNoMoreInteractions(solrClient); + then(solrClient).should().request(any(CoreAdminRequest.class), isNull()); + then(solrClient).shouldHaveNoMoreInteractions(); } @Test @@ -85,9 +73,9 @@ void healthWhenSolrStatusUpAndBaseUrlPointsToParticularCoreReturnsUp() throws Ex given(solrClient.ping()).willReturn(mockPingResponse(0)); SolrHealthIndicator healthIndicator = new SolrHealthIndicator(solrClient); assertHealth(healthIndicator, Status.UP, 0, "particular core"); - verify(solrClient, times(1)).request(any(CoreAdminRequest.class), isNull()); - verify(solrClient, times(1)).ping(); - verifyNoMoreInteractions(solrClient); + then(solrClient).should().request(any(CoreAdminRequest.class), isNull()); + then(solrClient).should().ping(); + then(solrClient).shouldHaveNoMoreInteractions(); } @Test @@ -98,9 +86,9 @@ void healthWhenSolrStatusDownAndBaseUrlPointsToParticularCoreReturnsDown() throw given(solrClient.ping()).willReturn(mockPingResponse(400)); SolrHealthIndicator healthIndicator = new SolrHealthIndicator(solrClient); assertHealth(healthIndicator, Status.DOWN, 400, "particular core"); - verify(solrClient, times(1)).request(any(CoreAdminRequest.class), isNull()); - verify(solrClient, times(1)).ping(); - verifyNoMoreInteractions(solrClient); + then(solrClient).should().request(any(CoreAdminRequest.class), isNull()); + then(solrClient).should().ping(); + then(solrClient).shouldHaveNoMoreInteractions(); } @Test @@ -112,8 +100,8 @@ void healthWhenSolrConnectionFailsReturnsDown() throws Exception { Health health = healthIndicator.health(); assertThat(health.getStatus()).isEqualTo(Status.DOWN); assertThat((String) health.getDetails().get("error")).contains("Connection failed"); - verify(solrClient, times(1)).request(any(CoreAdminRequest.class), isNull()); - verifyNoMoreInteractions(solrClient); + then(solrClient).should().request(any(CoreAdminRequest.class), isNull()); + then(solrClient).shouldHaveNoMoreInteractions(); } @Test @@ -124,12 +112,12 @@ void healthWhenMakingMultipleCallsRemembersStatusStrategy() throws Exception { given(solrClient.ping()).willReturn(mockPingResponse(0)); SolrHealthIndicator healthIndicator = new SolrHealthIndicator(solrClient); healthIndicator.health(); - verify(solrClient, times(1)).request(any(CoreAdminRequest.class), isNull()); - verify(solrClient, times(1)).ping(); - verifyNoMoreInteractions(solrClient); + then(solrClient).should().request(any(CoreAdminRequest.class), isNull()); + then(solrClient).should().ping(); + then(solrClient).shouldHaveNoMoreInteractions(); healthIndicator.health(); - verify(solrClient, times(2)).ping(); - verifyNoMoreInteractions(solrClient); + then(solrClient).should(times(2)).ping(); + then(solrClient).shouldHaveNoMoreInteractions(); } private void assertHealth(SolrHealthIndicator healthIndicator, Status expectedStatus, int expectedStatusCode, diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/startup/StartupEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/startup/StartupEndpointTests.java new file mode 100644 index 000000000000..6e9ae9e5b272 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/startup/StartupEndpointTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.startup; + +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.SpringBootVersion; +import org.springframework.boot.actuate.startup.StartupEndpoint.StartupResponse; +import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.metrics.ApplicationStartup; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link StartupEndpoint}. + * + * @author Brian Clozel + * @author Chris Bono + */ +class StartupEndpointTests { + + @Test + void startupEventsAreFound() { + BufferingApplicationStartup applicationStartup = new BufferingApplicationStartup(256); + testStartupEndpoint(applicationStartup, (startupEndpoint) -> { + StartupResponse startup = startupEndpoint.startup(); + assertThat(startup.getSpringBootVersion()).isEqualTo(SpringBootVersion.getVersion()); + assertThat(startup.getTimeline().getStartTime()) + .isEqualTo(applicationStartup.getBufferedTimeline().getStartTime()); + }); + } + + @Test + void bufferWithGetIsNotDrained() { + BufferingApplicationStartup applicationStartup = new BufferingApplicationStartup(256); + testStartupEndpoint(applicationStartup, (startupEndpoint) -> { + StartupResponse startup = startupEndpoint.startupSnapshot(); + assertThat(startup.getTimeline().getEvents()).isNotEmpty(); + assertThat(applicationStartup.getBufferedTimeline().getEvents()).isNotEmpty(); + }); + } + + @Test + void bufferWithPostIsDrained() { + BufferingApplicationStartup applicationStartup = new BufferingApplicationStartup(256); + testStartupEndpoint(applicationStartup, (startupEndpoint) -> { + StartupResponse startup = startupEndpoint.startup(); + assertThat(startup.getTimeline().getEvents()).isNotEmpty(); + assertThat(applicationStartup.getBufferedTimeline().getEvents()).isEmpty(); + }); + } + + private void testStartupEndpoint(ApplicationStartup applicationStartup, Consumer startupEndpoint) { + ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withInitializer((context) -> context.setApplicationStartup(applicationStartup)) + .withUserConfiguration(EndpointConfiguration.class); + contextRunner.run((context) -> { + assertThat(context).hasSingleBean(StartupEndpoint.class); + startupEndpoint.accept(context.getBean(StartupEndpoint.class)); + }); + } + + @Configuration(proxyBeanMethods = false) + static class EndpointConfiguration { + + @Bean + StartupEndpoint endpoint(BufferingApplicationStartup applicationStartup) { + return new StartupEndpoint(applicationStartup); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/system/DiskSpaceHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/system/DiskSpaceHealthIndicatorTests.java index 44b0b0c9fa9f..b631c492bd43 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/system/DiskSpaceHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/system/DiskSpaceHealthIndicatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,8 +20,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; @@ -37,6 +38,7 @@ * @author Mattias Severson * @author Stephane Nicoll */ +@ExtendWith(MockitoExtension.class) class DiskSpaceHealthIndicatorTests { private static final DataSize THRESHOLD = DataSize.ofKilobytes(1); @@ -50,14 +52,12 @@ class DiskSpaceHealthIndicatorTests { @BeforeEach void setUp() { - MockitoAnnotations.initMocks(this); - given(this.fileMock.exists()).willReturn(true); - given(this.fileMock.canRead()).willReturn(true); this.healthIndicator = new DiskSpaceHealthIndicator(this.fileMock, THRESHOLD); } @Test void diskSpaceIsUp() { + given(this.fileMock.exists()).willReturn(true); long freeSpace = THRESHOLD.toBytes() + 10; given(this.fileMock.getUsableSpace()).willReturn(freeSpace); given(this.fileMock.getTotalSpace()).willReturn(TOTAL_SPACE.toBytes()); @@ -66,10 +66,12 @@ void diskSpaceIsUp() { assertThat(health.getDetails().get("threshold")).isEqualTo(THRESHOLD.toBytes()); assertThat(health.getDetails().get("free")).isEqualTo(freeSpace); assertThat(health.getDetails().get("total")).isEqualTo(TOTAL_SPACE.toBytes()); + assertThat(health.getDetails().get("exists")).isEqualTo(true); } @Test void diskSpaceIsDown() { + given(this.fileMock.exists()).willReturn(true); long freeSpace = THRESHOLD.toBytes() - 10; given(this.fileMock.getUsableSpace()).willReturn(freeSpace); given(this.fileMock.getTotalSpace()).willReturn(TOTAL_SPACE.toBytes()); @@ -78,6 +80,16 @@ void diskSpaceIsDown() { assertThat(health.getDetails().get("threshold")).isEqualTo(THRESHOLD.toBytes()); assertThat(health.getDetails().get("free")).isEqualTo(freeSpace); assertThat(health.getDetails().get("total")).isEqualTo(TOTAL_SPACE.toBytes()); + assertThat(health.getDetails().get("exists")).isEqualTo(true); + } + + @Test + void whenPathDoesNotExistDiskSpaceIsDown() { + Health health = new DiskSpaceHealthIndicator(new File("does/not/exist"), THRESHOLD).health(); + assertThat(health.getStatus()).isEqualTo(Status.DOWN); + assertThat(health.getDetails().get("free")).isEqualTo(0L); + assertThat(health.getDetails().get("total")).isEqualTo(0L); + assertThat(health.getDetails().get("exists")).isEqualTo(false); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/HttpExchangeTracerTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/HttpExchangeTracerTests.java index d056f72f56fd..3b8a35fa6c8d 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/HttpExchangeTracerTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/HttpExchangeTracerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ import org.springframework.boot.actuate.trace.http.HttpTrace.Request; import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -270,6 +271,29 @@ void timeTakenCanBeIncluded() { assertThat(trace.getTimeTaken()).isNotNull(); } + @Test + void defaultIncludes() { + HttpHeaders requestHeaders = new HttpHeaders(); + requestHeaders.setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); + requestHeaders.set(HttpHeaders.COOKIE, "value"); + requestHeaders.set(HttpHeaders.AUTHORIZATION, "secret"); + HttpExchangeTracer tracer = new HttpExchangeTracer(Include.defaultIncludes()); + HttpTrace trace = tracer.receivedRequest(createRequest(requestHeaders)); + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.set(HttpHeaders.SET_COOKIE, "test=test"); + responseHeaders.setContentLength(0); + tracer.sendingResponse(trace, createResponse(responseHeaders), this::createPrincipal, () -> "sessionId"); + assertThat(trace.getTimeTaken()).isNotNull(); + assertThat(trace.getPrincipal()).isNull(); + assertThat(trace.getSession()).isNull(); + assertThat(trace.getTimestamp()).isNotNull(); + assertThat(trace.getRequest().getMethod()).isEqualTo("GET"); + assertThat(trace.getRequest().getRemoteAddress()).isNull(); + assertThat(trace.getResponse().getStatus()).isEqualTo(204); + assertThat(trace.getRequest().getHeaders()).containsOnlyKeys(HttpHeaders.ACCEPT); + assertThat(trace.getResponse().getHeaders()).containsOnlyKeys(HttpHeaders.CONTENT_LENGTH); + } + private TraceableRequest createRequest() { return createRequest(Collections.singletonMap(HttpHeaders.ACCEPT, Arrays.asList("application/json"))); } @@ -318,7 +342,7 @@ static class RequestHeadersFilterHttpExchangeTracer extends HttpExchangeTracer { @Override protected void postProcessRequestHeaders(Map> headers) { headers.remove("to-remove"); - headers.putIfAbsent("to-add", Collections.singletonList("42")); + headers.computeIfAbsent("to-add", (key) -> Collections.singletonList("42")); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/reactive/HttpTraceWebFilterIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/reactive/HttpTraceWebFilterIntegrationTests.java index 38a1f8b5a290..a46547c0460f 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/reactive/HttpTraceWebFilterIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/reactive/HttpTraceWebFilterIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,7 +51,7 @@ */ class HttpTraceWebFilterIntegrationTests { - private ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() + private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() .withUserConfiguration(Config.class); @Test diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/reactive/HttpTraceWebFilterTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/reactive/HttpTraceWebFilterTests.java index 0fa0c8e2ef89..afecd178f8ea 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/reactive/HttpTraceWebFilterTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/reactive/HttpTraceWebFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,11 +17,11 @@ package org.springframework.boot.actuate.trace.http.reactive; import java.security.Principal; -import java.time.Duration; import java.util.EnumSet; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; import org.springframework.boot.actuate.trace.http.HttpExchangeTracer; import org.springframework.boot.actuate.trace.http.HttpTrace.Session; @@ -55,16 +55,15 @@ class HttpTraceWebFilterTests { @Test void filterTracesExchange() { executeFilter(MockServerWebExchange.from(MockServerHttpRequest.get("https://api.example.com")), - (exchange) -> Mono.empty()).block(Duration.ofSeconds(30)); + (exchange) -> Mono.empty()); assertThat(this.repository.findAll()).hasSize(1); } @Test void filterCapturesSessionIdWhenSessionIsUsed() { - executeFilter(MockServerWebExchange.from(MockServerHttpRequest.get("https://api.example.com")), (exchange) -> { - exchange.getSession().block(Duration.ofSeconds(30)).getAttributes().put("a", "alpha"); - return Mono.empty(); - }).block(Duration.ofSeconds(30)); + executeFilter(MockServerWebExchange.from(MockServerHttpRequest.get("https://api.example.com")), + (exchange) -> exchange.getSession().doOnNext((session) -> session.getAttributes().put("a", "alpha")) + .then()); assertThat(this.repository.findAll()).hasSize(1); Session session = this.repository.findAll().get(0).getSession(); assertThat(session).isNotNull(); @@ -73,10 +72,8 @@ void filterCapturesSessionIdWhenSessionIsUsed() { @Test void filterDoesNotCaptureIdOfUnusedSession() { - executeFilter(MockServerWebExchange.from(MockServerHttpRequest.get("https://api.example.com")), (exchange) -> { - exchange.getSession().block(Duration.ofSeconds(30)); - return Mono.empty(); - }).block(Duration.ofSeconds(30)); + executeFilter(MockServerWebExchange.from(MockServerHttpRequest.get("https://api.example.com")), + (exchange) -> exchange.getSession().then()); assertThat(this.repository.findAll()).hasSize(1); Session session = this.repository.findAll().get(0).getSession(); assertThat(session).isNull(); @@ -89,15 +86,13 @@ void filterCapturesPrincipal() { executeFilter(new ServerWebExchangeDecorator( MockServerWebExchange.from(MockServerHttpRequest.get("https://api.example.com"))) { + @SuppressWarnings("unchecked") @Override - public Mono getPrincipal() { - return Mono.just(principal); + public Mono getPrincipal() { + return Mono.just((T) principal); } - }, (exchange) -> { - exchange.getSession().block(Duration.ofSeconds(30)).getAttributes().put("a", "alpha"); - return Mono.empty(); - }).block(Duration.ofSeconds(30)); + }, (exchange) -> exchange.getSession().doOnNext((session) -> session.getAttributes().put("a", "alpha")).then()); assertThat(this.repository.findAll()).hasSize(1); org.springframework.boot.actuate.trace.http.HttpTrace.Principal tracedPrincipal = this.repository.findAll() .get(0).getPrincipal(); @@ -105,8 +100,10 @@ public Mono getPrincipal() { assertThat(tracedPrincipal.getName()).isEqualTo("alice"); } - private Mono executeFilter(ServerWebExchange exchange, WebFilterChain chain) { - return this.filter.filter(exchange, chain).then(Mono.defer(() -> exchange.getResponse().setComplete())); + private void executeFilter(ServerWebExchange exchange, WebFilterChain chain) { + StepVerifier.create( + this.filter.filter(exchange, chain).then(Mono.defer(() -> exchange.getResponse().setComplete()))) + .verifyComplete(); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/servlet/HttpTraceFilterTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/servlet/HttpTraceFilterTests.java index 48fdcbc42c8a..bda27e8bd820 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/servlet/HttpTraceFilterTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/trace/http/servlet/HttpTraceFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -99,7 +99,7 @@ void filterCapturesPrincipal() throws ServletException, IOException { } @Test - void statusIsAssumedToBe500WhenChainFails() throws ServletException, IOException { + void statusIsAssumedToBe500WhenChainFails() { assertThatIOException().isThrownBy(() -> this.filter.doFilter(new MockHttpServletRequest(), new MockHttpServletResponse(), new MockFilterChain(new HttpServlet() { diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/mappings/MappingsEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/mappings/MappingsEndpointTests.java index b0d95504e090..91ff146d15df 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/mappings/MappingsEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/mappings/MappingsEndpointTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -56,6 +56,9 @@ import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.util.pattern.PathPatternParser; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @@ -94,6 +97,28 @@ void servletWebMappings() { }); } + @Test + void servletWebMappingsWithPathPatternParser() { + Supplier contextSupplier = prepareContextSupplier(); + new WebApplicationContextRunner(contextSupplier).withUserConfiguration(EndpointConfiguration.class, + ServletWebConfiguration.class, PathPatternParserConfiguration.class).run((context) -> { + ContextMappings contextMappings = contextMappings(context); + assertThat(contextMappings.getParentId()).isNull(); + assertThat(contextMappings.getMappings()).containsOnlyKeys("dispatcherServlets", "servletFilters", + "servlets"); + Map> dispatcherServlets = mappings( + contextMappings, "dispatcherServlets"); + assertThat(dispatcherServlets).containsOnlyKeys("dispatcherServlet"); + List handlerMappings = dispatcherServlets + .get("dispatcherServlet"); + assertThat(handlerMappings).hasSize(1); + List servlets = mappings(contextMappings, "servlets"); + assertThat(servlets).hasSize(1); + List filters = mappings(contextMappings, "servletFilters"); + assertThat(filters).hasSize(1); + }); + } + @Test void servletWebMappingsWithAdditionalDispatcherServlets() { Supplier contextSupplier = prepareContextSupplier(); @@ -260,4 +285,21 @@ private DispatcherServlet createTestDispatcherServlet(WebApplicationContext cont } + @Configuration + static class PathPatternParserConfiguration { + + @Bean + WebMvcConfigurer pathPatternParserConfigurer() { + return new WebMvcConfigurer() { + + @Override + public void configurePathMatch(PathMatchConfigurer configurer) { + configurer.setPatternParser(new PathPatternParser()); + } + + }; + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/reactive/ServerWebExchangeTraceableRequestTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/reactive/ServerWebExchangeTraceableRequestTests.java index 0c5b257ac2ed..f29c4f52c886 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/reactive/ServerWebExchangeTraceableRequestTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/reactive/ServerWebExchangeTraceableRequestTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.actuate.web.trace.reactive; import java.net.InetSocketAddress; diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/servlet/TraceableHttpServletRequestTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/servlet/TraceableHttpServletRequestTests.java index 505e2bf80613..ec68db114c21 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/servlet/TraceableHttpServletRequestTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/trace/servlet/TraceableHttpServletRequestTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.actuate.web.trace.servlet; import org.junit.jupiter.api.BeforeEach; diff --git a/spring-boot-project/spring-boot-actuator/src/test/resources/cache/test-ehcache.xml b/spring-boot-project/spring-boot-actuator/src/test/resources/cache/test-ehcache.xml deleted file mode 100644 index cdee1d0503e8..000000000000 --- a/spring-boot-project/spring-boot-actuator/src/test/resources/cache/test-ehcache.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/spring-boot-project/spring-boot-actuator/src/test/resources/cache/test-hazelcast.xml b/spring-boot-project/spring-boot-actuator/src/test/resources/cache/test-hazelcast.xml deleted file mode 100644 index bc025d7316c3..000000000000 --- a/spring-boot-project/spring-boot-actuator/src/test/resources/cache/test-hazelcast.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - diff --git a/spring-boot-project/spring-boot-actuator/src/test/resources/cache/test-infinispan.xml b/spring-boot-project/spring-boot-actuator/src/test/resources/cache/test-infinispan.xml deleted file mode 100644 index 741fb6292b7b..000000000000 --- a/spring-boot-project/spring-boot-actuator/src/test/resources/cache/test-infinispan.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/spring-boot-project/spring-boot-actuator/src/test/resources/db/changelog/db.changelog-master-backup.yaml b/spring-boot-project/spring-boot-actuator/src/test/resources/db/changelog/db.changelog-master-backup.yaml new file mode 100644 index 000000000000..a3a19b43af3a --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/resources/db/changelog/db.changelog-master-backup.yaml @@ -0,0 +1,20 @@ +databaseChangeLog: + - changeSet: + id: 1 + author: leoli + changes: + - createTable: + tableName: customerbackup + columns: + - column: + name: id + type: int + autoIncrement: true + constraints: + primaryKey: true + nullable: false + - column: + name: name + type: varchar(50) + constraints: + nullable: false diff --git a/spring-boot-project/spring-boot-actuator/src/test/resources/hazelcast-3.xml b/spring-boot-project/spring-boot-actuator/src/test/resources/hazelcast-3.xml new file mode 100644 index 000000000000..1d6e65e88927 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/resources/hazelcast-3.xml @@ -0,0 +1,13 @@ + + actuator-hazelcast-3 + + + + + + + + diff --git a/spring-boot-project/spring-boot-actuator/src/test/resources/hazelcast.xml b/spring-boot-project/spring-boot-actuator/src/test/resources/hazelcast.xml index 1a61470e6004..56298f8e7903 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/resources/hazelcast.xml +++ b/spring-boot-project/spring-boot-actuator/src/test/resources/hazelcast.xml @@ -1,7 +1,8 @@ - + + actuator-hazelcast diff --git a/spring-boot-project/spring-boot-autoconfigure/build.gradle b/spring-boot-project/spring-boot-autoconfigure/build.gradle new file mode 100644 index 000000000000..af72bd05b0aa --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/build.gradle @@ -0,0 +1,250 @@ +plugins { + id "java-library" + id "org.jetbrains.kotlin.jvm" + id "org.springframework.boot.auto-configuration" + id "org.springframework.boot.conventions" + id "org.springframework.boot.deployed" + id "org.springframework.boot.optional-dependencies" +} + +description = "Spring Boot AutoConfigure" + +dependencies { + api(project(":spring-boot-project:spring-boot")) + + optional("com.atomikos:transactions-jdbc") + optional("com.atomikos:transactions-jta") + optional("com.fasterxml.jackson.core:jackson-databind") + optional("com.fasterxml.jackson.dataformat:jackson-dataformat-cbor") + optional("com.fasterxml.jackson.dataformat:jackson-dataformat-xml") + optional("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") + optional("com.fasterxml.jackson.module:jackson-module-parameter-names") + optional("com.google.code.gson:gson") + optional("com.hazelcast:hazelcast") + optional("com.hazelcast:hazelcast-spring") + optional("com.h2database:h2") + optional("com.nimbusds:oauth2-oidc-sdk") + optional("com.oracle.database.jdbc:ojdbc8") + optional("com.oracle.database.jdbc:ucp") + optional("com.samskivert:jmustache") + optional("com.sun.mail:jakarta.mail") + optional("de.flapdoodle.embed:de.flapdoodle.embed.mongo") + optional("io.lettuce:lettuce-core") + optional("io.projectreactor.netty:reactor-netty-http") + optional("io.r2dbc:r2dbc-spi") + optional("io.r2dbc:r2dbc-pool") + optional("io.rsocket:rsocket-core") + optional("io.rsocket:rsocket-transport-netty") + optional("io.undertow:undertow-servlet") { + exclude group: "org.jboss.spec.javax.annotation", module: "jboss-annotations-api_1.3_spec" + exclude group: "org.jboss.spec.javax.servlet", module: "jboss-servlet-api_4.0_spec" + } + optional("io.undertow:undertow-websockets-jsr") { + exclude group: "org.jboss.spec.javax.annotation", module: "jboss-annotations-api_1.3_spec" + exclude group: "org.jboss.spec.javax.servlet", module: "jboss-servlet-api_4.0_spec" + exclude group: "org.jboss.spec.javax.websocket", module: "jboss-websocket-api_1.1_spec" + } + optional("jakarta.jms:jakarta.jms-api") + optional("jakarta.mail:jakarta.mail-api") + optional("jakarta.json.bind:jakarta.json.bind-api") + optional("jakarta.persistence:jakarta.persistence-api") + optional("jakarta.transaction:jakarta.transaction-api") + optional("jakarta.validation:jakarta.validation-api") + optional("jakarta.ws.rs:jakarta.ws.rs-api") + optional("javax.cache:cache-api") + optional("javax.money:money-api") + optional("net.sf.ehcache:ehcache") + optional("org.apache.activemq:activemq-broker") { + exclude group: "org.apache.geronimo.specs", module: "geronimo-j2ee-management_1.1_spec" + exclude group: "org.apache.geronimo.specs", module: "geronimo-jms_1.1_spec" + } + optional("org.apache.activemq:artemis-jms-client") { + exclude group: "commons-logging", module: "commons-logging" + exclude group: "org.apache.geronimo.specs", module: "geronimo-jms_2.0_spec" + exclude group: "org.apache.geronimo.specs", module: "geronimo-json_1.0_spec" + } + optional("org.apache.activemq:artemis-jms-server") { + exclude group: "commons-logging", module: "commons-logging" + exclude group: "org.apache.geronimo.specs", module: "geronimo-jms_2.0_spec" + exclude group: "org.apache.geronimo.specs", module: "geronimo-json_1.0_spec" + exclude group: "org.apache.geronimo.specs", module: "geronimo-jta_1.1_spec" + } + optional("org.apache.commons:commons-dbcp2") { + exclude group: "commons-logging", module: "commons-logging" + } + optional("org.apache.httpcomponents.client5:httpclient5") + optional("org.apache.kafka:kafka-streams") + optional("org.apache.solr:solr-solrj") { + exclude group: "org.slf4j", module: "jcl-over-slf4j" + } + optional("org.apache.tomcat.embed:tomcat-embed-core") + optional("org.apache.tomcat.embed:tomcat-embed-el") + optional("org.apache.tomcat.embed:tomcat-embed-websocket") + optional("org.apache.tomcat:tomcat-jdbc") + optional("org.codehaus.groovy:groovy-templates") + optional("com.github.ben-manes.caffeine:caffeine") + optional("com.github.mxab.thymeleaf.extras:thymeleaf-extras-data-attribute") + optional("com.sendgrid:sendgrid-java") { + exclude group: "commons-logging", module: "commons-logging" + } + optional("com.unboundid:unboundid-ldapsdk") + optional("com.zaxxer:HikariCP") + optional("nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect") + optional("org.aspectj:aspectjweaver") + optional("org.eclipse.jetty:jetty-webapp") { + exclude group: "javax.servlet", module: "javax.servlet-api" + } + optional("org.eclipse.jetty:jetty-reactive-httpclient") + optional("org.eclipse.jetty.websocket:javax-websocket-server-impl") { + exclude group: "javax.annotation", module: "javax.annotation-api" + exclude group: "javax.servlet", module: "javax.servlet-api" + exclude group: "javax.websocket", module: "javax.websocket-api" + exclude group: "javax.websocket", module: "javax.websocket-client-api" + exclude group: "org.eclipse.jetty", module: "jetty-jndi" + } + optional("org.ehcache:ehcache") + optional("org.elasticsearch.client:elasticsearch-rest-client") { + exclude group: "commons-logging", module: "commons-logging" + } + optional("org.elasticsearch.client:elasticsearch-rest-client-sniffer") { + exclude group: "commons-logging", module: "commons-logging" + } + optional("org.elasticsearch.client:elasticsearch-rest-high-level-client") { + exclude group: "commons-logging", module: "commons-logging" + } + optional("org.flywaydb:flyway-core") + optional("org.freemarker:freemarker") + optional("org.glassfish.jersey.core:jersey-server") + optional("org.glassfish.jersey.containers:jersey-container-servlet-core") + optional("org.glassfish.jersey.containers:jersey-container-servlet") + optional("org.glassfish.jersey.ext:jersey-spring5") + optional("org.glassfish.jersey.media:jersey-media-json-jackson") + optional("org.hibernate:hibernate-core") { + exclude group: "javax.activation", module: "javax.activation-api" + exclude group: "javax.persistence", module: "javax.persistence-api" + exclude group: "javax.xml.bind", module: "jaxb-api" + exclude group: "org.jboss.spec.javax.transaction", module: "jboss-transaction-api_1.2_spec" + } + optional("org.hibernate:hibernate-jcache") { + exclude group: "javax.activation", module: "javax.activation-api" + exclude group: "javax.persistence", module: "javax.persistence-api" + exclude group: "javax.xml.bind", module: "jaxb-api" + exclude group: "org.jboss.spec.javax.transaction", module: "jboss-transaction-api_1.2_spec" + } + optional("org.hibernate.validator:hibernate-validator") + optional("org.infinispan:infinispan-component-annotations") + optional("org.infinispan:infinispan-jcache") { + exclude group: "org.jboss.spec.javax.transaction", module: "jboss-transaction-api_1.2_spec" + } + optional("org.infinispan:infinispan-spring5-embedded") { + exclude group: "org.jboss.spec.javax.transaction", module: "jboss-transaction-api_1.2_spec" + } + optional("org.influxdb:influxdb-java") + optional("org.jooq:jooq") { + exclude group: "javax.xml.bind", module: "jaxb-api" + } + optional("org.liquibase:liquibase-core") { + exclude group: "javax.xml.bind", module: "jaxb-api" + } + optional("org.messaginghub:pooled-jms") { + exclude group: "org.apache.geronimo.specs", module: "geronimo-jms_2.0_spec" + } + optional("org.mongodb:mongodb-driver-reactivestreams") + optional("org.mongodb:mongodb-driver-sync") + optional("org.quartz-scheduler:quartz") + optional("org.springframework:spring-jdbc") + optional("org.springframework.integration:spring-integration-core") + optional("org.springframework.integration:spring-integration-jdbc") + optional("org.springframework.integration:spring-integration-jmx") + optional("org.springframework.integration:spring-integration-rsocket") + optional("org.springframework:spring-jms") + optional("org.springframework:spring-orm") + optional("org.springframework:spring-tx") + optional("org.springframework:spring-web") + optional("org.springframework:spring-websocket") + optional("org.springframework:spring-webflux") + optional("org.springframework:spring-webmvc") + optional("org.springframework.batch:spring-batch-core") + optional("org.springframework.data:spring-data-couchbase") + optional("org.springframework.data:spring-data-jpa") + optional("org.springframework.data:spring-data-rest-webmvc") + optional("org.springframework.data:spring-data-cassandra") { + exclude group: "org.slf4j", module: "jcl-over-slf4j" + } + optional("org.springframework.data:spring-data-elasticsearch") { + exclude group: "org.elasticsearch.client", module: "transport" + } + optional("org.springframework.data:spring-data-jdbc") + optional("org.springframework.data:spring-data-ldap") + optional("org.springframework.data:spring-data-mongodb") + optional("org.springframework.data:spring-data-neo4j") + optional("org.springframework.data:spring-data-r2dbc") + optional("org.springframework.data:spring-data-redis") + optional("org.springframework.hateoas:spring-hateoas") + optional("org.springframework.security:spring-security-acl") + optional("org.springframework.security:spring-security-config") + optional("org.springframework.security:spring-security-data") { + exclude group: "javax.xml.bind", module: "jaxb-api" + } + optional("org.springframework.security:spring-security-oauth2-client") + optional("org.springframework.security:spring-security-oauth2-jose") + optional("org.springframework.security:spring-security-oauth2-resource-server") + optional("org.springframework.security:spring-security-rsocket") + optional("org.springframework.security:spring-security-saml2-service-provider") + optional("org.springframework.security:spring-security-web") + optional("org.springframework.session:spring-session-core") + optional("org.springframework.session:spring-session-data-mongodb") + optional("org.springframework.session:spring-session-data-redis") + optional("org.springframework.session:spring-session-hazelcast") { + exclude group: "javax.annotation", module: "javax.annotation-api" + } + optional("org.springframework.session:spring-session-jdbc") + optional("org.springframework.amqp:spring-rabbit") + optional("org.springframework.kafka:spring-kafka") + optional("org.springframework.ws:spring-ws-core") + optional("org.thymeleaf:thymeleaf") + optional("org.thymeleaf:thymeleaf-spring5") + optional("org.thymeleaf.extras:thymeleaf-extras-java8time") + optional("org.thymeleaf.extras:thymeleaf-extras-springsecurity5") + optional("redis.clients:jedis") + + testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) + testImplementation(project(":spring-boot-project:spring-boot-test")) + testImplementation("ch.qos.logback:logback-classic") + testImplementation("commons-fileupload:commons-fileupload") + testImplementation("com.atomikos:transactions-jms") + testImplementation("com.github.h-thurow:simple-jndi") + testImplementation("com.ibm.db2:jcc") + testImplementation("com.jayway.jsonpath:json-path") + testImplementation("com.squareup.okhttp3:mockwebserver") + testImplementation("com.sun.xml.messaging.saaj:saaj-impl") + testImplementation("io.projectreactor:reactor-test") + testImplementation("io.r2dbc:r2dbc-h2") + testImplementation("jakarta.json:jakarta.json-api") + testImplementation("jakarta.xml.ws:jakarta.xml.ws-api") + testImplementation("mysql:mysql-connector-java") + testImplementation("org.apache.johnzon:johnzon-jsonb") + testImplementation("org.apache.logging.log4j:log4j-to-slf4j") + testImplementation("org.apache.tomcat.embed:tomcat-embed-jasper") + testImplementation("org.assertj:assertj-core") + testImplementation("org.awaitility:awaitility") + testImplementation("org.hsqldb:hsqldb") + testImplementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.mockito:mockito-core") + testImplementation("org.mockito:mockito-junit-jupiter") + testImplementation("org.springframework:spring-test") + testImplementation("org.springframework.kafka:spring-kafka-test") + testImplementation("org.springframework.security:spring-security-test") + testImplementation("org.testcontainers:cassandra") + testImplementation("org.testcontainers:couchbase") + testImplementation("org.testcontainers:elasticsearch") + testImplementation("org.testcontainers:junit-jupiter") + testImplementation("org.testcontainers:mongodb") + testImplementation("org.testcontainers:neo4j") + testImplementation("org.testcontainers:testcontainers") + testImplementation("org.yaml:snakeyaml") + + testRuntimeOnly("jakarta.management.j2ee:jakarta.management.j2ee-api") + testRuntimeOnly("org.jetbrains.kotlin:kotlin-reflect") +} diff --git a/spring-boot-project/spring-boot-autoconfigure/pom.xml b/spring-boot-project/spring-boot-autoconfigure/pom.xml deleted file mode 100755 index 70f9e1349ac9..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/pom.xml +++ /dev/null @@ -1,1006 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-parent - ${revision} - ../spring-boot-parent - - spring-boot-autoconfigure - Spring Boot AutoConfigure - Spring Boot AutoConfigure - - ${basedir}/../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - - org.springframework.boot - spring-boot - - - - com.atomikos - transactions-jdbc - true - - - com.atomikos - transactions-jta - true - - - com.couchbase.client - couchbase-spring-cache - true - - - com.fasterxml.jackson.core - jackson-databind - true - - - com.fasterxml.jackson.dataformat - jackson-dataformat-cbor - true - - - com.fasterxml.jackson.dataformat - jackson-dataformat-xml - true - - - com.fasterxml.jackson.datatype - jackson-datatype-joda - true - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - true - - - com.fasterxml.jackson.module - jackson-module-parameter-names - true - - - com.google.code.gson - gson - true - - - com.hazelcast - hazelcast - true - - - com.hazelcast - hazelcast-client - true - - - com.hazelcast - hazelcast-spring - true - - - com.h2database - h2 - true - - - com.samskivert - jmustache - true - - - com.sun.mail - jakarta.mail - true - - - de.flapdoodle.embed - de.flapdoodle.embed.mongo - true - - - io.lettuce - lettuce-core - true - - - io.projectreactor.netty - reactor-netty - true - - - io.rsocket - rsocket-core - true - - - io.rsocket - rsocket-transport-netty - true - - - io.searchbox - jest - true - - - jakarta.json.bind - jakarta.json.bind-api - true - - - javax.cache - cache-api - true - - - javax.money - money-api - true - - - org.apache.kafka - kafka-streams - true - - - javax.ws.rs - javax.ws.rs-api - - - - - org.flywaydb - flyway-core - true - - - org.glassfish.jersey.core - jersey-server - true - - - javax.validation - validation-api - - - - - org.glassfish.jersey.containers - jersey-container-servlet-core - true - - - org.glassfish.jersey.containers - jersey-container-servlet - true - - - org.glassfish.jersey.ext - jersey-spring5 - true - - - org.glassfish.hk2.external - bean-validator - - - org.hibernate - hibernate-validator - - - - - org.glassfish.jersey.media - jersey-media-json-jackson - true - - - org.apache.activemq - activemq-broker - true - - - geronimo-jms_1.1_spec - org.apache.geronimo.specs - - - - - org.apache.activemq - artemis-jms-client - true - - - geronimo-jms_2.0_spec - org.apache.geronimo.specs - - - - - org.apache.activemq - artemis-jms-server - true - - - geronimo-jms_2.0_spec - org.apache.geronimo.specs - - - - - org.apache.commons - commons-dbcp2 - true - - - org.apache.solr - solr-solrj - true - - - org.codehaus.woodstox - wstx-asl - - - log4j - log4j - - - - - org.apache.tomcat.embed - tomcat-embed-core - true - - - org.apache.tomcat.embed - tomcat-embed-el - true - - - org.apache.tomcat.embed - tomcat-embed-websocket - true - - - org.apache.tomcat - tomcat-jdbc - true - - - org.codehaus.btm - btm - true - - - javax.transaction - jta - - - - - org.codehaus.groovy - groovy-templates - true - - - com.sendgrid - sendgrid-java - true - - - com.unboundid - unboundid-ldapsdk - true - - - com.zaxxer - HikariCP - true - - - org.eclipse.jetty - jetty-webapp - true - - - javax.servlet - javax.servlet-api - - - - - org.eclipse.jetty - jetty-reactive-httpclient - true - - - org.eclipse.jetty.websocket - javax-websocket-server-impl - true - - - javax.annotation - javax.annotation-api - - - javax.servlet - javax.servlet-api - - - javax.websocket - javax.websocket-api - - - javax.websocket - javax.websocket-client-api - - - - - io.undertow - undertow-servlet - true - - - io.undertow - undertow-websockets-jsr - true - - - jakarta.persistence - jakarta.persistence-api - true - - - jakarta.validation - jakarta.validation-api - true - - - jakarta.ws.rs - jakarta.ws.rs-api - true - - - org.ehcache - ehcache - true - - - org.elasticsearch.client - elasticsearch-rest-client - true - - - org.elasticsearch.client - elasticsearch-rest-high-level-client - true - - - org.freemarker - freemarker - true - - - org.hibernate - hibernate-core - true - - - javax.activation - javax.activation-api - - - javax.persistence - javax.persistence-api - - - javax.xml.bind - jaxb-api - - - - - org.hibernate - hibernate-jcache - true - - - org.hibernate.validator - hibernate-validator - true - - - javax.validation - validation-api - - - - - org.infinispan - infinispan-jcache - true - - - org.infinispan - infinispan-spring5-embedded - true - - - org.jboss - jboss-transaction-spi - true - - - org.messaginghub - pooled-jms - true - - - org.apache.geronimo.specs - geronimo-jms_2.0_spec - - - - - org.mongodb - mongodb-driver-async - true - - - org.mongodb - mongodb-driver-reactivestreams - true - - - org.springframework - spring-jdbc - true - - - org.springframework.integration - spring-integration-core - true - - - org.springframework.integration - spring-integration-jdbc - true - - - org.springframework.integration - spring-integration-jmx - true - - - org.springframework - spring-jms - true - - - org.springframework - spring-orm - true - - - org.springframework - spring-tx - true - - - org.springframework - spring-web - true - - - org.springframework - spring-websocket - true - - - org.springframework - spring-webflux - true - - - org.springframework - spring-webmvc - true - - - org.springframework.batch - spring-batch-core - true - - - org.springframework.data - spring-data-couchbase - true - - - org.slf4j - jcl-over-slf4j - - - - - org.springframework.data - spring-data-jpa - true - - - jcl-over-slf4j - org.slf4j - - - - - org.springframework.data - spring-data-rest-webmvc - true - - - jcl-over-slf4j - org.slf4j - - - - - org.springframework.data - spring-data-cassandra - true - - - org.springframework.data - spring-data-jdbc - true - - - org.springframework.data - spring-data-ldap - true - - - org.springframework.data - spring-data-mongodb - true - - - jcl-over-slf4j - org.slf4j - - - - - org.springframework.data - spring-data-neo4j - true - - - jcl-over-slf4j - org.slf4j - - - - - org.springframework.data - spring-data-redis - true - - - org.springframework.data - spring-data-elasticsearch - true - - - jcl-over-slf4j - org.slf4j - - - - - org.springframework.data - spring-data-solr - true - - - jcl-over-slf4j - org.slf4j - - - - - org.springframework.hateoas - spring-hateoas - true - - - redis.clients - jedis - true - - - org.liquibase - liquibase-core - true - - - org.springframework.security - spring-security-acl - true - - - org.springframework.security - spring-security-config - true - - - org.springframework.security - spring-security-data - true - - - javax.xml.bind - jaxb-api - - - - - org.springframework.security - spring-security-oauth2-client - true - - - com.sun.mail - javax.mail - - - - - org.springframework.security - spring-security-oauth2-jose - true - - - org.springframework.security - spring-security-oauth2-resource-server - true - - - org.springframework.security - spring-security-rsocket - true - - - org.springframework.security - spring-security-saml2-service-provider - true - - - org.springframework.security - spring-security-web - true - - - org.springframework.session - spring-session-core - true - - - org.springframework.session - spring-session-data-mongodb - true - - - org.springframework.session - spring-session-data-redis - true - - - org.springframework.session - spring-session-hazelcast - true - - - javax.annotation - javax.annotation-api - - - - - org.springframework.session - spring-session-jdbc - true - - - org.springframework.amqp - spring-rabbit - true - - - org.springframework.kafka - spring-kafka - true - - - org.springframework.cloud - spring-cloud-spring-service-connector - true - - - org.springframework.ws - spring-ws-core - true - - - org.thymeleaf - thymeleaf - true - - - org.thymeleaf - thymeleaf-spring5 - true - - - nz.net.ultraq.thymeleaf - thymeleaf-layout-dialect - true - - - com.github.ben-manes.caffeine - caffeine - true - - - com.github.mxab.thymeleaf.extras - thymeleaf-extras-data-attribute - true - - - org.thymeleaf.extras - thymeleaf-extras-java8time - true - - - org.thymeleaf.extras - thymeleaf-extras-springsecurity5 - true - - - jakarta.jms - jakarta.jms-api - true - - - jakarta.mail - jakarta.mail-api - true - - - net.sf.ehcache - ehcache - true - - - org.aspectj - aspectjweaver - true - - - org.influxdb - influxdb-java - true - - - org.jooq - jooq - true - - - javax.xml.bind - jaxb-api - - - - - org.quartz-scheduler - quartz - true - - - - org.springframework.boot - spring-boot-autoconfigure-processor - true - - - org.springframework.boot - spring-boot-configuration-processor - true - - - - org.springframework.boot - spring-boot-test-support - test - - - org.springframework.boot - spring-boot-test - test - - - ch.qos.logback - logback-classic - test - - - commons-fileupload - commons-fileupload - test - - - com.atomikos - transactions-jms - test - - - com.jayway.jsonpath - json-path - test - - - com.squareup.okhttp3 - mockwebserver - test - - - org.hamcrest - hamcrest-core - - - - - com.sun.xml.messaging.saaj - saaj-impl - test - - - jakarta.json - jakarta.json-api - test - - - jakarta.xml.ws - jakarta.xml.ws-api - test - - - mysql - mysql-connector-java - test - - - org.apache.johnzon - johnzon-jsonb - test - - - org.apache.logging.log4j - log4j-to-slf4j - test - - - org.apache.tomcat.embed - tomcat-embed-jasper - test - - - org.awaitility - awaitility - test - - - org.hsqldb - hsqldb - test - - - org.neo4j - neo4j-ogm-bolt-native-types - test - - - org.neo4j - neo4j-ogm-embedded-driver - test - - - org.neo4j - neo4j-ogm-http-driver - test - - - org.springframework - spring-test - test - - - org.springframework.kafka - spring-kafka-test - test - - - org.springframework.security - spring-security-test - test - - - org.testcontainers - cassandra - test - - - org.testcontainers - elasticsearch - test - - - org.testcontainers - junit-jupiter - test - - - org.testcontainers - testcontainers - test - - - javax.annotation - javax.annotation-api - - - javax.xml.bind - jaxb-api - - - - - org.yaml - snakeyaml - test - - - - - java9+ - - [9,) - - - - org.glassfish.jaxb - jaxb-runtime - true - - - - - diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationImportSelector.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationImportSelector.java index 9ad94ff7f0df..d0e0b2bd8c58 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationImportSelector.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationImportSelector.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; import java.util.stream.Collectors; import org.apache.commons.logging.Log; @@ -88,27 +89,33 @@ public class AutoConfigurationImportSelector implements DeferredImportSelector, private ResourceLoader resourceLoader; + private ConfigurationClassFilter configurationClassFilter; + @Override public String[] selectImports(AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { return NO_IMPORTS; } - AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader - .loadMetadata(this.beanClassLoader); - AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata, - annotationMetadata); + AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata); return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations()); } + @Override + public Predicate getExclusionFilter() { + return this::shouldExclude; + } + + private boolean shouldExclude(String configurationClassName) { + return getConfigurationClassFilter().filter(Collections.singletonList(configurationClassName)).isEmpty(); + } + /** * Return the {@link AutoConfigurationEntry} based on the {@link AnnotationMetadata} * of the importing {@link Configuration @Configuration} class. - * @param autoConfigurationMetadata the auto-configuration metadata * @param annotationMetadata the annotation metadata of the configuration class * @return the auto-configurations that should be imported */ - protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, - AnnotationMetadata annotationMetadata) { + protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { return EMPTY_ENTRY; } @@ -118,7 +125,7 @@ protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMeta Set exclusions = getExclusions(annotationMetadata, attributes); checkExcludedClasses(configurations, exclusions); configurations.removeAll(exclusions); - configurations = filter(configurations, autoConfigurationMetadata); + configurations = getConfigurationClassFilter().filter(configurations); fireAutoConfigurationImportEvents(configurations, exclusions); return new AutoConfigurationEntry(configurations, exclusions); } @@ -226,60 +233,48 @@ protected Set getExclusions(AnnotationMetadata metadata, AnnotationAttri return excluded; } - private List getExcludeAutoConfigurationsProperty() { - if (getEnvironment() instanceof ConfigurableEnvironment) { - Binder binder = Binder.get(getEnvironment()); + /** + * Returns the auto-configurations excluded by the + * {@code spring.autoconfigure.exclude} property. + * @return excluded auto-configurations + * @since 2.3.2 + */ + protected List getExcludeAutoConfigurationsProperty() { + Environment environment = getEnvironment(); + if (environment == null) { + return Collections.emptyList(); + } + if (environment instanceof ConfigurableEnvironment) { + Binder binder = Binder.get(environment); return binder.bind(PROPERTY_NAME_AUTOCONFIGURE_EXCLUDE, String[].class).map(Arrays::asList) .orElse(Collections.emptyList()); } - String[] excludes = getEnvironment().getProperty(PROPERTY_NAME_AUTOCONFIGURE_EXCLUDE, String[].class); + String[] excludes = environment.getProperty(PROPERTY_NAME_AUTOCONFIGURE_EXCLUDE, String[].class); return (excludes != null) ? Arrays.asList(excludes) : Collections.emptyList(); } - private List filter(List configurations, AutoConfigurationMetadata autoConfigurationMetadata) { - long startTime = System.nanoTime(); - String[] candidates = StringUtils.toStringArray(configurations); - boolean[] skip = new boolean[candidates.length]; - boolean skipped = false; - for (AutoConfigurationImportFilter filter : getAutoConfigurationImportFilters()) { - invokeAwareMethods(filter); - boolean[] match = filter.match(candidates, autoConfigurationMetadata); - for (int i = 0; i < match.length; i++) { - if (!match[i]) { - skip[i] = true; - candidates[i] = null; - skipped = true; - } - } - } - if (!skipped) { - return configurations; - } - List result = new ArrayList<>(candidates.length); - for (int i = 0; i < candidates.length; i++) { - if (!skip[i]) { - result.add(candidates[i]); - } - } - if (logger.isTraceEnabled()) { - int numberFiltered = configurations.size() - result.size(); - logger.trace("Filtered " + numberFiltered + " auto configuration class in " - + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime) + " ms"); - } - return new ArrayList<>(result); - } - protected List getAutoConfigurationImportFilters() { return SpringFactoriesLoader.loadFactories(AutoConfigurationImportFilter.class, this.beanClassLoader); } + private ConfigurationClassFilter getConfigurationClassFilter() { + if (this.configurationClassFilter == null) { + List filters = getAutoConfigurationImportFilters(); + for (AutoConfigurationImportFilter filter : filters) { + invokeAwareMethods(filter); + } + this.configurationClassFilter = new ConfigurationClassFilter(this.beanClassLoader, filters); + } + return this.configurationClassFilter; + } + protected final List removeDuplicates(List list) { return new ArrayList<>(new LinkedHashSet<>(list)); } protected final List asList(AnnotationAttributes attributes, String name) { String[] value = attributes.getStringArray(name); - return Arrays.asList((value != null) ? value : new String[0]); + return Arrays.asList(value); } private void fireAutoConfigurationImportEvents(List configurations, Set exclusions) { @@ -356,6 +351,49 @@ public int getOrder() { return Ordered.LOWEST_PRECEDENCE - 1; } + private static class ConfigurationClassFilter { + + private final AutoConfigurationMetadata autoConfigurationMetadata; + + private final List filters; + + ConfigurationClassFilter(ClassLoader classLoader, List filters) { + this.autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(classLoader); + this.filters = filters; + } + + List filter(List configurations) { + long startTime = System.nanoTime(); + String[] candidates = StringUtils.toStringArray(configurations); + boolean skipped = false; + for (AutoConfigurationImportFilter filter : this.filters) { + boolean[] match = filter.match(candidates, this.autoConfigurationMetadata); + for (int i = 0; i < match.length; i++) { + if (!match[i]) { + candidates[i] = null; + skipped = true; + } + } + } + if (!skipped) { + return configurations; + } + List result = new ArrayList<>(candidates.length); + for (String candidate : candidates) { + if (candidate != null) { + result.add(candidate); + } + } + if (logger.isTraceEnabled()) { + int numberFiltered = configurations.size() - result.size(); + logger.trace("Filtered " + numberFiltered + " auto configuration class in " + + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime) + " ms"); + } + return result; + } + + } + private static class AutoConfigurationGroup implements DeferredImportSelector.Group, BeanClassLoaderAware, BeanFactoryAware, ResourceLoaderAware { @@ -393,7 +431,7 @@ public void process(AnnotationMetadata annotationMetadata, DeferredImportSelecto AutoConfigurationImportSelector.class.getSimpleName(), deferredImportSelector.getClass().getName())); AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector) - .getAutoConfigurationEntry(getAutoConfigurationMetadata(), annotationMetadata); + .getAutoConfigurationEntry(annotationMetadata); this.autoConfigurationEntries.add(autoConfigurationEntry); for (String importClassName : autoConfigurationEntry.getConfigurations()) { this.entries.putIfAbsent(importClassName, annotationMetadata); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationPackage.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationPackage.java index 0fbe9b9a6683..8b4ff1ac7170 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationPackage.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationPackage.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,8 +26,9 @@ import org.springframework.context.annotation.Import; /** - * Indicates that the package containing the annotated class should be registered with - * {@link AutoConfigurationPackages}. + * Registers packages with {@link AutoConfigurationPackages}. When no {@link #basePackages + * base packages} or {@link #basePackageClasses base package classes} are specified, the + * package of the annotated class is registered. * * @author Phillip Webb * @since 1.3.0 @@ -40,4 +41,25 @@ @Import(AutoConfigurationPackages.Registrar.class) public @interface AutoConfigurationPackage { + /** + * Base packages that should be registered with {@link AutoConfigurationPackages}. + *

+ * Use {@link #basePackageClasses} for a type-safe alternative to String-based package + * names. + * @return the back package names + * @since 2.3.0 + */ + String[] basePackages() default {}; + + /** + * Type-safe alternative to {@link #basePackages} for specifying the packages to be + * registered with {@link AutoConfigurationPackages}. + *

+ * Consider creating a special no-op marker class or interface in each package that + * serves no purpose other than being referenced by this attribute. + * @return the base package classes + * @since 2.3.0 + */ + Class[] basePackageClasses() default {}; + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationPackages.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationPackages.java index 520af8e4617a..6799241c38d6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationPackages.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationPackages.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import java.util.function.Supplier; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -29,11 +30,11 @@ import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.config.ConstructorArgumentValues; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.boot.context.annotation.DeterminableImports; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.type.AnnotationMetadata; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -91,27 +92,14 @@ public static List get(BeanFactory beanFactory) { */ public static void register(BeanDefinitionRegistry registry, String... packageNames) { if (registry.containsBeanDefinition(BEAN)) { - BeanDefinition beanDefinition = registry.getBeanDefinition(BEAN); - ConstructorArgumentValues constructorArguments = beanDefinition.getConstructorArgumentValues(); - constructorArguments.addIndexedArgumentValue(0, addBasePackages(constructorArguments, packageNames)); + BasePackagesBeanDefinition beanDefinition = (BasePackagesBeanDefinition) registry.getBeanDefinition(BEAN); + beanDefinition.addBasePackages(packageNames); } else { - GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); - beanDefinition.setBeanClass(BasePackages.class); - beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, packageNames); - beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - registry.registerBeanDefinition(BEAN, beanDefinition); + registry.registerBeanDefinition(BEAN, new BasePackagesBeanDefinition(packageNames)); } } - private static String[] addBasePackages(ConstructorArgumentValues constructorArguments, String[] packageNames) { - String[] existing = (String[]) constructorArguments.getIndexedArgumentValue(0, String[].class).getValue(); - Set merged = new LinkedHashSet<>(); - merged.addAll(Arrays.asList(existing)); - merged.addAll(Arrays.asList(packageNames)); - return StringUtils.toStringArray(merged); - } - /** * {@link ImportBeanDefinitionRegistrar} to store the base package from the importing * configuration. @@ -120,12 +108,12 @@ static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImp @Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { - register(registry, new PackageImport(metadata).getPackageName()); + register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0])); } @Override public Set determineImports(AnnotationMetadata metadata) { - return Collections.singleton(new PackageImport(metadata)); + return Collections.singleton(new PackageImports(metadata)); } } @@ -133,16 +121,25 @@ public Set determineImports(AnnotationMetadata metadata) { /** * Wrapper for a package import. */ - private static final class PackageImport { + private static final class PackageImports { - private final String packageName; + private final List packageNames; - PackageImport(AnnotationMetadata metadata) { - this.packageName = ClassUtils.getPackageName(metadata.getClassName()); + PackageImports(AnnotationMetadata metadata) { + AnnotationAttributes attributes = AnnotationAttributes + .fromMap(metadata.getAnnotationAttributes(AutoConfigurationPackage.class.getName(), false)); + List packageNames = new ArrayList<>(Arrays.asList(attributes.getStringArray("basePackages"))); + for (Class basePackageClass : attributes.getClassArray("basePackageClasses")) { + packageNames.add(basePackageClass.getPackage().getName()); + } + if (packageNames.isEmpty()) { + packageNames.add(ClassUtils.getPackageName(metadata.getClassName())); + } + this.packageNames = Collections.unmodifiableList(packageNames); } - String getPackageName() { - return this.packageName; + List getPackageNames() { + return this.packageNames; } @Override @@ -150,17 +147,17 @@ public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) { return false; } - return this.packageName.equals(((PackageImport) obj).packageName); + return this.packageNames.equals(((PackageImports) obj).packageNames); } @Override public int hashCode() { - return this.packageName.hashCode(); + return this.packageNames.hashCode(); } @Override public String toString() { - return "Package Import " + this.packageName; + return "Package Imports " + this.packageNames; } } @@ -207,4 +204,25 @@ List get() { } + static final class BasePackagesBeanDefinition extends GenericBeanDefinition { + + private final Set basePackages = new LinkedHashSet<>(); + + BasePackagesBeanDefinition(String... basePackages) { + setBeanClass(BasePackages.class); + setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + addBasePackages(basePackages); + } + + @Override + public Supplier getInstanceSupplier() { + return () -> new BasePackages(StringUtils.toStringArray(this.basePackages)); + } + + private void addBasePackages(String[] additionalBasePackages) { + this.basePackages.addAll(Arrays.asList(additionalBasePackages)); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationSorter.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationSorter.java index 0aea6a04cb71..6877a54ab90b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationSorter.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurationSorter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -88,8 +88,7 @@ private void doSortByAfterAnnotation(AutoConfigurationClasses classes, List processing, String current, String after) { + Assert.state(!processing.contains(after), + () -> "AutoConfigure cycle detected between " + current + " and " + after); + } + private static class AutoConfigurationClasses { private final Map classes = new HashMap<>(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigureAfter.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigureAfter.java index 3f9ab0eacb53..e3bb47f88cff 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigureAfter.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigureAfter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,9 +22,18 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; + /** * Hint for that an {@link EnableAutoConfiguration auto-configuration} should be applied * after other specified auto-configuration classes. + *

+ * As with standard {@link Configuration @Configuration} classes, the order in which + * auto-configuration classes are applied only affects the order in which their beans are + * defined. The order in which those beans are subsequently created is unaffected and is + * determined by each bean's dependencies and any {@link DependsOn @DependsOn} + * relationships. * * @author Phillip Webb * @since 1.0.0 diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigureBefore.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigureBefore.java index 34c7191abf27..5da0b3a3f95f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigureBefore.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigureBefore.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,9 +22,18 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; + /** - * Hint for that an {@link EnableAutoConfiguration auto-configuration} should be applied + * Hint that an {@link EnableAutoConfiguration auto-configuration} should be applied * before other specified auto-configuration classes. + *

+ * As with standard {@link Configuration @Configuration} classes, the order in which + * auto-configuration classes are applied only affects the order in which their beans are + * defined. The order in which those beans are subsequently created is unaffected and is + * determined by each bean's dependencies and any {@link DependsOn @DependsOn} + * relationships. * * @author Phillip Webb * @since 1.0.0 diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigureOrder.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigureOrder.java index 3c0092e070d3..2d90a2199b47 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigureOrder.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigureOrder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,8 @@ import java.lang.annotation.Target; import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; @@ -31,6 +33,12 @@ * annotation. Allows auto-configuration classes to be ordered among themselves without * affecting the order of configuration classes passed to * {@link AnnotationConfigApplicationContext#register(Class...)}. + *

+ * As with standard {@link Configuration @Configuration} classes, the order in which + * auto-configuration classes are applied only affects the order in which their beans are + * defined. The order in which those beans are subsequently created is unaffected and is + * determined by each bean's dependencies and any {@link DependsOn @DependsOn} + * relationships. * * @author Andy Wilkinson * @since 1.3.0 @@ -40,6 +48,9 @@ @Documented public @interface AutoConfigureOrder { + /** + * The default order value. + */ int DEFAULT_ORDER = 0; /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/BackgroundPreinitializer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/BackgroundPreinitializer.java index b224582db42f..d97c57d248c5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/BackgroundPreinitializer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/BackgroundPreinitializer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,12 +23,13 @@ import javax.validation.Configuration; import javax.validation.Validation; +import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; import org.springframework.boot.context.event.ApplicationFailedEvent; import org.springframework.boot.context.event.ApplicationReadyEvent; -import org.springframework.boot.context.event.ApplicationStartingEvent; import org.springframework.boot.context.event.SpringApplicationEvent; import org.springframework.boot.context.logging.LoggingApplicationListener; import org.springframework.context.ApplicationListener; +import org.springframework.core.NativeDetector; import org.springframework.core.annotation.Order; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; @@ -45,6 +46,7 @@ * @author Phillip Webb * @author Andy Wilkinson * @author Artsiom Yudovin + * @author Sebastien Deleuze * @since 1.3.0 */ @Order(LoggingApplicationListener.DEFAULT_ORDER + 1) @@ -59,14 +61,23 @@ public class BackgroundPreinitializer implements ApplicationListener 1; + } + @Override public void onApplicationEvent(SpringApplicationEvent event) { - if (!Boolean.getBoolean(IGNORE_BACKGROUNDPREINITIALIZER_PROPERTY_NAME) - && event instanceof ApplicationStartingEvent && multipleProcessors() + if (!ENABLED) { + return; + } + if (event instanceof ApplicationEnvironmentPreparedEvent && preinitializationStarted.compareAndSet(false, true)) { performPreinitialization(); } @@ -81,10 +92,6 @@ && event instanceof ApplicationStartingEvent && multipleProcessors() } } - private boolean multipleProcessors() { - return Runtime.getRuntime().availableProcessors() > 1; - } - private void performPreinitialization() { try { Thread thread = new Thread(new Runnable() { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/EnableAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/EnableAutoConfiguration.java index e25c3ab13ef2..eea76a0a4d55 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/EnableAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/EnableAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -83,6 +83,10 @@ @Import(AutoConfigurationImportSelector.class) public @interface EnableAutoConfiguration { + /** + * Environment property that can be used to override when auto-configuration is + * enabled. + */ String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration"; /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationImportSelector.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationImportSelector.java index ec52a64a8a04..bfbae2a75037 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationImportSelector.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationImportSelector.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -94,7 +94,7 @@ private Collection getConfigurationsForAnnotation(Class source, Annot } protected Collection loadFactoryNames(Class source) { - return SpringFactoriesLoader.loadFactoryNames(source, getClass().getClassLoader()); + return SpringFactoriesLoader.loadFactoryNames(source, getBeanClassLoader()); } @Override @@ -118,6 +118,7 @@ protected Set getExclusions(AnnotationMetadata metadata, AnnotationAttri } } } + exclusions.addAll(getExcludeAutoConfigurationsProperty()); return exclusions; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/SharedMetadataReaderFactoryContextInitializer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/SharedMetadataReaderFactoryContextInitializer.java index 017a4a34460e..1d4dec1b1128 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/SharedMetadataReaderFactoryContextInitializer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/SharedMetadataReaderFactoryContextInitializer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,18 @@ package org.springframework.boot.autoconfigure; +import java.util.function.Supplier; + import org.springframework.beans.BeansException; +import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; @@ -44,6 +49,7 @@ * {@link ConfigurationClassPostProcessor} and Spring Boot. * * @author Phillip Webb + * @author Dave Syer */ class SharedMetadataReaderFactoryContextInitializer implements ApplicationContextInitializer, Ordered { @@ -53,7 +59,8 @@ class SharedMetadataReaderFactoryContextInitializer @Override public void initialize(ConfigurableApplicationContext applicationContext) { - applicationContext.addBeanFactoryPostProcessor(new CachingMetadataReaderFactoryPostProcessor()); + BeanFactoryPostProcessor postProcessor = new CachingMetadataReaderFactoryPostProcessor(applicationContext); + applicationContext.addBeanFactoryPostProcessor(postProcessor); } @Override @@ -66,9 +73,15 @@ public int getOrder() { * {@link CachingMetadataReaderFactory} and configure the * {@link ConfigurationClassPostProcessor}. */ - private static class CachingMetadataReaderFactoryPostProcessor + static class CachingMetadataReaderFactoryPostProcessor implements BeanDefinitionRegistryPostProcessor, PriorityOrdered { + private final ConfigurableApplicationContext context; + + CachingMetadataReaderFactoryPostProcessor(ConfigurableApplicationContext context) { + this.context = context; + } + @Override public int getOrder() { // Must happen before the ConfigurationClassPostProcessor is created @@ -94,14 +107,66 @@ private void register(BeanDefinitionRegistry registry) { private void configureConfigurationClassPostProcessor(BeanDefinitionRegistry registry) { try { - BeanDefinition definition = registry - .getBeanDefinition(AnnotationConfigUtils.CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME); - definition.getPropertyValues().add("metadataReaderFactory", new RuntimeBeanReference(BEAN_NAME)); + configureConfigurationClassPostProcessor( + registry.getBeanDefinition(AnnotationConfigUtils.CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)); } catch (NoSuchBeanDefinitionException ex) { } } + private void configureConfigurationClassPostProcessor(BeanDefinition definition) { + if (definition instanceof AbstractBeanDefinition) { + configureConfigurationClassPostProcessor((AbstractBeanDefinition) definition); + return; + } + configureConfigurationClassPostProcessor(definition.getPropertyValues()); + } + + private void configureConfigurationClassPostProcessor(AbstractBeanDefinition definition) { + Supplier instanceSupplier = definition.getInstanceSupplier(); + if (instanceSupplier != null) { + definition.setInstanceSupplier( + new ConfigurationClassPostProcessorCustomizingSupplier(this.context, instanceSupplier)); + return; + } + configureConfigurationClassPostProcessor(definition.getPropertyValues()); + } + + private void configureConfigurationClassPostProcessor(MutablePropertyValues propertyValues) { + propertyValues.add("metadataReaderFactory", new RuntimeBeanReference(BEAN_NAME)); + } + + } + + /** + * {@link Supplier} used to customize the {@link ConfigurationClassPostProcessor} when + * it's first created. + */ + static class ConfigurationClassPostProcessorCustomizingSupplier implements Supplier { + + private final ConfigurableApplicationContext context; + + private final Supplier instanceSupplier; + + ConfigurationClassPostProcessorCustomizingSupplier(ConfigurableApplicationContext context, + Supplier instanceSupplier) { + this.context = context; + this.instanceSupplier = instanceSupplier; + } + + @Override + public Object get() { + Object instance = this.instanceSupplier.get(); + if (instance instanceof ConfigurationClassPostProcessor) { + configureConfigurationClassPostProcessor((ConfigurationClassPostProcessor) instance); + } + return instance; + } + + private void configureConfigurationClassPostProcessor(ConfigurationClassPostProcessor instance) { + instance.setMetadataReaderFactory(this.context.getBean(BEAN_NAME, MetadataReaderFactory.class)); + } + } /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/SpringBootApplication.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/SpringBootApplication.java index b341032d68c5..83e16137708a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/SpringBootApplication.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/SpringBootApplication.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,9 +23,11 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.context.TypeExcludeFilter; -import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +import org.springframework.context.annotation.AnnotationBeanNameGenerator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan.Filter; @@ -37,10 +39,9 @@ /** * Indicates a {@link Configuration configuration} class that declares one or more * {@link Bean @Bean} methods and also triggers {@link EnableAutoConfiguration - * auto-configuration}, {@link ComponentScan component scanning}, and - * {@link ConfigurationPropertiesScan configuration properties scanning}. This is a - * convenience annotation that is equivalent to declaring {@code @Configuration}, - * {@code @EnableAutoConfiguration}, {@code @ComponentScan}. + * auto-configuration} and {@link ComponentScan component scanning}. This is a convenience + * annotation that is equivalent to declaring {@code @Configuration}, + * {@code @EnableAutoConfiguration} and {@code @ComponentScan}. * * @author Phillip Webb * @author Stephane Nicoll @@ -106,6 +107,22 @@ @AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses") Class[] scanBasePackageClasses() default {}; + /** + * The {@link BeanNameGenerator} class to be used for naming detected components + * within the Spring container. + *

+ * The default value of the {@link BeanNameGenerator} interface itself indicates that + * the scanner used to process this {@code @SpringBootApplication} annotation should + * use its inherited bean name generator, e.g. the default + * {@link AnnotationBeanNameGenerator} or any custom instance supplied to the + * application context at bootstrap time. + * @return {@link BeanNameGenerator} to use + * @see SpringApplication#setBeanNameGenerator(BeanNameGenerator) + * @since 2.3.0 + */ + @AliasFor(annotation = ComponentScan.class, attribute = "nameGenerator") + Class nameGenerator() default BeanNameGenerator.class; + /** * Specify whether {@link Bean @Bean} methods should get proxied in order to enforce * bean lifecycle behavior, e.g. to return shared singleton bean instances even in diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/admin/SpringApplicationAdminJmxAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/admin/SpringApplicationAdminJmxAutoConfiguration.java index 1ed6ca48994a..80eaa31f59a5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/admin/SpringApplicationAdminJmxAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/admin/SpringApplicationAdminJmxAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/AbstractRabbitListenerContainerFactoryConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/AbstractRabbitListenerContainerFactoryConfigurer.java index 3b7f668dbe64..765f5ca279e7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/AbstractRabbitListenerContainerFactoryConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/AbstractRabbitListenerContainerFactoryConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -116,6 +116,7 @@ protected void configure(T factory, ConnectionFactory connectionFactory, factory.setIdleEventInterval(configuration.getIdleEventInterval().toMillis()); } factory.setMissingQueuesFatal(configuration.isMissingQueuesFatal()); + factory.setDeBatchingEnabled(configuration.isDeBatchingEnabled()); ListenerRetry retryConfig = configuration.getRetry(); if (retryConfig.isEnabled()) { RetryInterceptorBuilder builder = (retryConfig.isStateless()) ? RetryInterceptorBuilder.stateless() diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/ConnectionFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/ConnectionFactoryCustomizer.java new file mode 100644 index 000000000000..164b121bdc81 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/ConnectionFactoryCustomizer.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.amqp; + +import com.rabbitmq.client.ConnectionFactory; + +/** + * Callback interface that can be implemented by beans wishing to customize the + * auto-configured RabbitMQ {@link ConnectionFactory}. + * + * @author Andy Wilkinson + * @since 2.5.0 + */ +@FunctionalInterface +public interface ConnectionFactoryCustomizer { + + /** + * Customize the {@link ConnectionFactory}. + * @param factory the factory to customize + */ + void customize(ConnectionFactory factory); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfiguration.java index 8d81b0932baf..3c09a5b2fdf0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,8 @@ import java.util.stream.Collectors; import com.rabbitmq.client.Channel; +import com.rabbitmq.client.impl.CredentialsProvider; +import com.rabbitmq.client.impl.CredentialsRefreshService; import org.springframework.amqp.core.AmqpAdmin; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; @@ -42,6 +44,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.core.io.ResourceLoader; /** * {@link EnableAutoConfiguration Auto-configuration} for {@link RabbitTemplate}. @@ -95,11 +98,18 @@ protected static class RabbitConnectionFactoryCreator { @Bean public CachingConnectionFactory rabbitConnectionFactory(RabbitProperties properties, - ObjectProvider connectionNameStrategy) throws Exception { + ResourceLoader resourceLoader, ObjectProvider credentialsProvider, + ObjectProvider credentialsRefreshService, + ObjectProvider connectionNameStrategy, + ObjectProvider connectionFactoryCustomizers) throws Exception { + com.rabbitmq.client.ConnectionFactory connectionFactory = getRabbitConnectionFactoryBean(properties, + resourceLoader, credentialsProvider, credentialsRefreshService).getObject(); + connectionFactoryCustomizers.orderedStream() + .forEach((customizer) -> customizer.customize(connectionFactory)); + CachingConnectionFactory factory = new CachingConnectionFactory(connectionFactory); PropertyMapper map = PropertyMapper.get(); - CachingConnectionFactory factory = new CachingConnectionFactory( - getRabbitConnectionFactoryBean(properties).getObject()); map.from(properties::determineAddresses).to(factory::setAddresses); + map.from(properties::getAddressShuffleMode).whenNonNull().to(factory::setAddressShuffleMode); map.from(properties::isPublisherReturns).to(factory::setPublisherReturns); map.from(properties::getPublisherConfirmType).whenNonNull().to(factory::setPublisherConfirmType); RabbitProperties.Cache.Channel channel = properties.getCache().getChannel(); @@ -113,10 +123,12 @@ public CachingConnectionFactory rabbitConnectionFactory(RabbitProperties propert return factory; } - private RabbitConnectionFactoryBean getRabbitConnectionFactoryBean(RabbitProperties properties) - throws Exception { - PropertyMapper map = PropertyMapper.get(); + private RabbitConnectionFactoryBean getRabbitConnectionFactoryBean(RabbitProperties properties, + ResourceLoader resourceLoader, ObjectProvider credentialsProvider, + ObjectProvider credentialsRefreshService) { RabbitConnectionFactoryBean factory = new RabbitConnectionFactoryBean(); + factory.setResourceLoader(resourceLoader); + PropertyMapper map = PropertyMapper.get(); map.from(properties::determineHost).whenNonNull().to(factory::setHost); map.from(properties::determinePort).to(factory::setPort); map.from(properties::determineUsername).whenNonNull().to(factory::setUsername); @@ -124,6 +136,7 @@ private RabbitConnectionFactoryBean getRabbitConnectionFactoryBean(RabbitPropert map.from(properties::determineVirtualHost).whenNonNull().to(factory::setVirtualHost); map.from(properties::getRequestedHeartbeat).whenNonNull().asInt(Duration::getSeconds) .to(factory::setRequestedHeartbeat); + map.from(properties::getRequestedChannelMax).to(factory::setRequestedChannelMax); RabbitProperties.Ssl ssl = properties.getSsl(); if (ssl.determineEnabled()) { factory.setUseSSL(true); @@ -131,15 +144,21 @@ private RabbitConnectionFactoryBean getRabbitConnectionFactoryBean(RabbitPropert map.from(ssl::getKeyStoreType).to(factory::setKeyStoreType); map.from(ssl::getKeyStore).to(factory::setKeyStore); map.from(ssl::getKeyStorePassword).to(factory::setKeyStorePassphrase); + map.from(ssl::getKeyStoreAlgorithm).whenNonNull().to(factory::setKeyStoreAlgorithm); map.from(ssl::getTrustStoreType).to(factory::setTrustStoreType); map.from(ssl::getTrustStore).to(factory::setTrustStore); map.from(ssl::getTrustStorePassword).to(factory::setTrustStorePassphrase); + map.from(ssl::getTrustStoreAlgorithm).whenNonNull().to(factory::setTrustStoreAlgorithm); map.from(ssl::isValidateServerCertificate) .to((validate) -> factory.setSkipServerCertificateValidation(!validate)); map.from(ssl::getVerifyHostname).to(factory::setEnableHostnameVerification); } map.from(properties::getConnectionTimeout).whenNonNull().asInt(Duration::toMillis) .to(factory::setConnectionTimeout); + map.from(properties::getChannelRpcTimeout).whenNonNull().asInt(Duration::toMillis) + .to(factory::setChannelRpcTimeout); + map.from(credentialsProvider::getIfUnique).whenNonNull().to(factory::setCredentialsProvider); + map.from(credentialsRefreshService::getIfUnique).whenNonNull().to(factory::setCredentialsRefreshService); factory.afterPropertiesSet(); return factory; } @@ -151,36 +170,25 @@ private RabbitConnectionFactoryBean getRabbitConnectionFactoryBean(RabbitPropert protected static class RabbitTemplateConfiguration { @Bean - @ConditionalOnSingleCandidate(ConnectionFactory.class) - @ConditionalOnMissingBean(RabbitOperations.class) - public RabbitTemplate rabbitTemplate(RabbitProperties properties, + @ConditionalOnMissingBean + public RabbitTemplateConfigurer rabbitTemplateConfigurer(RabbitProperties properties, ObjectProvider messageConverter, - ObjectProvider retryTemplateCustomizers, - ConnectionFactory connectionFactory) { - PropertyMapper map = PropertyMapper.get(); - RabbitTemplate template = new RabbitTemplate(connectionFactory); - messageConverter.ifUnique(template::setMessageConverter); - template.setMandatory(determineMandatoryFlag(properties)); - RabbitProperties.Template templateProperties = properties.getTemplate(); - if (templateProperties.getRetry().isEnabled()) { - template.setRetryTemplate( - new RetryTemplateFactory(retryTemplateCustomizers.orderedStream().collect(Collectors.toList())) - .createRetryTemplate(templateProperties.getRetry(), - RabbitRetryTemplateCustomizer.Target.SENDER)); - } - map.from(templateProperties::getReceiveTimeout).whenNonNull().as(Duration::toMillis) - .to(template::setReceiveTimeout); - map.from(templateProperties::getReplyTimeout).whenNonNull().as(Duration::toMillis) - .to(template::setReplyTimeout); - map.from(templateProperties::getExchange).to(template::setExchange); - map.from(templateProperties::getRoutingKey).to(template::setRoutingKey); - map.from(templateProperties::getDefaultReceiveQueue).whenNonNull().to(template::setDefaultReceiveQueue); - return template; + ObjectProvider retryTemplateCustomizers) { + RabbitTemplateConfigurer configurer = new RabbitTemplateConfigurer(); + configurer.setMessageConverter(messageConverter.getIfUnique()); + configurer + .setRetryTemplateCustomizers(retryTemplateCustomizers.orderedStream().collect(Collectors.toList())); + configurer.setRabbitProperties(properties); + return configurer; } - private boolean determineMandatoryFlag(RabbitProperties properties) { - Boolean mandatory = properties.getTemplate().getMandatory(); - return (mandatory != null) ? mandatory : properties.isPublisherReturns(); + @Bean + @ConditionalOnSingleCandidate(ConnectionFactory.class) + @ConditionalOnMissingBean(RabbitOperations.class) + public RabbitTemplate rabbitTemplate(RabbitTemplateConfigurer configurer, ConnectionFactory connectionFactory) { + RabbitTemplate template = new RabbitTemplate(); + configurer.configure(template, connectionFactory); + return template; } @Bean diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java index e9f0a606f28d..ed96498d92bf 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,12 +20,13 @@ import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import org.springframework.amqp.core.AcknowledgeMode; +import org.springframework.amqp.rabbit.connection.AbstractConnectionFactory.AddressShuffleMode; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory.CacheMode; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory.ConfirmType; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; import org.springframework.boot.convert.DurationUnit; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -40,20 +41,26 @@ * @author Josh Thornhill * @author Gary Russell * @author Artsiom Yudovin + * @author Franjo Zilic * @since 1.0.0 */ @ConfigurationProperties(prefix = "spring.rabbitmq") public class RabbitProperties { + private static final int DEFAULT_PORT = 5672; + + private static final int DEFAULT_PORT_SECURE = 5671; + /** - * RabbitMQ host. + * RabbitMQ host. Ignored if an address is set. */ private String host = "localhost"; /** - * RabbitMQ port. + * RabbitMQ port. Ignored if an address is set. Default to 5672, or 5671 if SSL is + * enabled. */ - private int port = 5672; + private Integer port; /** * Login user to authenticate to the broker. @@ -76,10 +83,16 @@ public class RabbitProperties { private String virtualHost; /** - * Comma-separated list of addresses to which the client should connect. + * Comma-separated list of addresses to which the client should connect. When set, the + * host and port are ignored. */ private String addresses; + /** + * Mode used to shuffle configured addresses. + */ + private AddressShuffleMode addressShuffleMode = AddressShuffleMode.NONE; + /** * Requested heartbeat timeout; zero for none. If a duration suffix is not specified, * seconds will be used. @@ -87,6 +100,11 @@ public class RabbitProperties { @DurationUnit(ChronoUnit.SECONDS) private Duration requestedHeartbeat; + /** + * Number of channels per connection requested by the client. Use 0 for unlimited. + */ + private int requestedChannelMax = 2047; + /** * Whether to enable publisher returns. */ @@ -102,6 +120,11 @@ public class RabbitProperties { */ private Duration connectionTimeout; + /** + * Continuation timeout for RPC calls in channels. Set it to zero to wait forever. + */ + private Duration channelRpcTimeout = Duration.ofMinutes(10); + /** * Cache configuration. */ @@ -138,7 +161,7 @@ public void setHost(String host) { this.host = host; } - public int getPort() { + public Integer getPort() { return this.port; } @@ -151,13 +174,16 @@ public int getPort() { */ public int determinePort() { if (CollectionUtils.isEmpty(this.parsedAddresses)) { - return getPort(); + Integer port = getPort(); + if (port != null) { + return port; + } + return (Optional.ofNullable(getSsl().getEnabled()).orElse(false)) ? DEFAULT_PORT_SECURE : DEFAULT_PORT; } - Address address = this.parsedAddresses.get(0); - return address.port; + return this.parsedAddresses.get(0).port; } - public void setPort(int port) { + public void setPort(Integer port) { this.port = port; } @@ -172,7 +198,7 @@ public String getAddresses() { */ public String determineAddresses() { if (CollectionUtils.isEmpty(this.parsedAddresses)) { - return this.host + ":" + this.port; + return this.host + ":" + determinePort(); } List addressStrings = new ArrayList<>(); for (Address parsedAddress : this.parsedAddresses) { @@ -189,7 +215,7 @@ public void setAddresses(String addresses) { private List

parseAddresses(String addresses) { List
parsedAddresses = new ArrayList<>(); for (String address : StringUtils.commaDelimitedListToStringArray(addresses)) { - parsedAddresses.add(new Address(address)); + parsedAddresses.add(new Address(address, Optional.ofNullable(getSsl().getEnabled()).orElse(false))); } return parsedAddresses; } @@ -264,7 +290,15 @@ public String determineVirtualHost() { } public void setVirtualHost(String virtualHost) { - this.virtualHost = "".equals(virtualHost) ? "/" : virtualHost; + this.virtualHost = StringUtils.hasText(virtualHost) ? virtualHost : "/"; + } + + public AddressShuffleMode getAddressShuffleMode() { + return this.addressShuffleMode; + } + + public void setAddressShuffleMode(AddressShuffleMode addressShuffleMode) { + this.addressShuffleMode = addressShuffleMode; } public Duration getRequestedHeartbeat() { @@ -275,15 +309,12 @@ public void setRequestedHeartbeat(Duration requestedHeartbeat) { this.requestedHeartbeat = requestedHeartbeat; } - @DeprecatedConfigurationProperty(reason = "replaced to support additional confirm types", - replacement = "spring.rabbitmq.publisher-confirm-type") - public boolean isPublisherConfirms() { - return ConfirmType.CORRELATED.equals(this.publisherConfirmType); + public int getRequestedChannelMax() { + return this.requestedChannelMax; } - @Deprecated - public void setPublisherConfirms(boolean publisherConfirms) { - this.publisherConfirmType = (publisherConfirms) ? ConfirmType.CORRELATED : ConfirmType.NONE; + public void setRequestedChannelMax(int requestedChannelMax) { + this.requestedChannelMax = requestedChannelMax; } public boolean isPublisherReturns() { @@ -310,6 +341,14 @@ public void setConnectionTimeout(Duration connectionTimeout) { this.connectionTimeout = connectionTimeout; } + public Duration getChannelRpcTimeout() { + return this.channelRpcTimeout; + } + + public void setChannelRpcTimeout(Duration channelRpcTimeout) { + this.channelRpcTimeout = channelRpcTimeout; + } + public Cache getCache() { return this.cache; } @@ -324,10 +363,13 @@ public Template getTemplate() { public class Ssl { + private static final String SUN_X509 = "SunX509"; + /** - * Whether to enable SSL support. + * Whether to enable SSL support. Determined automatically if an address is + * provided with the protocol (amqp:// vs. amqps://). */ - private boolean enabled; + private Boolean enabled; /** * Path to the key store that holds the SSL certificate. @@ -344,6 +386,11 @@ public class Ssl { */ private String keyStorePassword; + /** + * Key store algorithm. + */ + private String keyStoreAlgorithm = SUN_X509; + /** * Trust store that holds SSL certificates. */ @@ -359,6 +406,11 @@ public class Ssl { */ private String trustStorePassword; + /** + * Trust store algorithm. + */ + private String trustStoreAlgorithm = SUN_X509; + /** * SSL algorithm to use. By default, configured by the Rabbit client library. */ @@ -374,7 +426,7 @@ public class Ssl { */ private boolean verifyHostname = true; - public boolean isEnabled() { + public Boolean getEnabled() { return this.enabled; } @@ -383,17 +435,18 @@ public boolean isEnabled() { * enabled flag if no addresses have been set. * @return whether ssl is enabled * @see #setAddresses(String) - * @see #isEnabled() + * @see #getEnabled() () */ public boolean determineEnabled() { + boolean defaultEnabled = Optional.ofNullable(getEnabled()).orElse(false); if (CollectionUtils.isEmpty(RabbitProperties.this.parsedAddresses)) { - return isEnabled(); + return defaultEnabled; } Address address = RabbitProperties.this.parsedAddresses.get(0); - return address.secureConnection; + return address.determineSslEnabled(defaultEnabled); } - public void setEnabled(boolean enabled) { + public void setEnabled(Boolean enabled) { this.enabled = enabled; } @@ -421,6 +474,14 @@ public void setKeyStorePassword(String keyStorePassword) { this.keyStorePassword = keyStorePassword; } + public String getKeyStoreAlgorithm() { + return this.keyStoreAlgorithm; + } + + public void setKeyStoreAlgorithm(String keyStoreAlgorithm) { + this.keyStoreAlgorithm = keyStoreAlgorithm; + } + public String getTrustStore() { return this.trustStore; } @@ -445,6 +506,14 @@ public void setTrustStorePassword(String trustStorePassword) { this.trustStorePassword = trustStorePassword; } + public String getTrustStoreAlgorithm() { + return this.trustStoreAlgorithm; + } + + public void setTrustStoreAlgorithm(String trustStoreAlgorithm) { + this.trustStoreAlgorithm = trustStoreAlgorithm; + } + public String getAlgorithm() { return this.algorithm; } @@ -621,6 +690,12 @@ public abstract static class AmqpContainer { */ private Duration idleEventInterval; + /** + * Whether the container should present batched messages as discrete messages or + * call the listener with the batch. + */ + private boolean deBatchingEnabled = true; + /** * Optional properties for a retry interceptor. */ @@ -668,6 +743,14 @@ public void setIdleEventInterval(Duration idleEventInterval) { public abstract boolean isMissingQueuesFatal(); + public boolean isDeBatchingEnabled() { + return this.deBatchingEnabled; + } + + public void setDeBatchingEnabled(boolean deBatchingEnabled) { + this.deBatchingEnabled = deBatchingEnabled; + } + public ListenerRetry getRetry() { return this.retry; } @@ -702,6 +785,14 @@ public static class SimpleContainer extends AmqpContainer { */ private boolean missingQueuesFatal = true; + /** + * Whether the container creates a batch of messages based on the + * 'receive-timeout' and 'batch-size'. Coerces 'de-batching-enabled' to true to + * include the contents of a producer created batch in the batch as discrete + * records. + */ + private boolean consumerBatchEnabled; + public Integer getConcurrency() { return this.concurrency; } @@ -718,28 +809,6 @@ public void setMaxConcurrency(Integer maxConcurrency) { this.maxConcurrency = maxConcurrency; } - /** - * Return the number of messages processed in one transaction. - * @return the number of messages - * @deprecated since 2.2.0 in favor of {@link SimpleContainer#getBatchSize()} - */ - @DeprecatedConfigurationProperty(replacement = "spring.rabbitmq.listener.simple.batch-size") - @Deprecated - public Integer getTransactionSize() { - return getBatchSize(); - } - - /** - * Set the number of messages processed in one transaction. - * @param transactionSize the number of messages - * @deprecated since 2.2.0 in favor of - * {@link SimpleContainer#setBatchSize(Integer)} - */ - @Deprecated - public void setTransactionSize(Integer transactionSize) { - setBatchSize(transactionSize); - } - public Integer getBatchSize() { return this.batchSize; } @@ -757,6 +826,14 @@ public void setMissingQueuesFatal(boolean missingQueuesFatal) { this.missingQueuesFatal = missingQueuesFatal; } + public boolean isConsumerBatchEnabled() { + return this.consumerBatchEnabled; + } + + public void setConsumerBatchEnabled(boolean consumerBatchEnabled) { + this.consumerBatchEnabled = consumerBatchEnabled; + } + } /** @@ -804,12 +881,12 @@ public static class Template { private Boolean mandatory; /** - * Timeout for `receive()` operations. + * Timeout for receive() operations. */ private Duration receiveTimeout; /** - * Timeout for `sendAndReceive()` operations. + * Timeout for sendAndReceive() operations. */ private Duration replyTimeout; @@ -973,12 +1050,8 @@ private static final class Address { private static final String PREFIX_AMQP = "amqp://"; - private static final int DEFAULT_PORT = 5672; - private static final String PREFIX_AMQP_SECURE = "amqps://"; - private static final int DEFAULT_PORT_SECURE = 5671; - private String host; private int port; @@ -989,14 +1062,14 @@ private static final class Address { private String virtualHost; - private boolean secureConnection; + private Boolean secureConnection; - private Address(String input) { + private Address(String input, boolean sslEnabled) { input = input.trim(); input = trimPrefix(input); input = parseUsernameAndPassword(input); input = parseVirtualHost(input); - parseHostAndPort(input); + parseHostAndPort(input, sslEnabled); } private String trimPrefix(String input) { @@ -1005,7 +1078,8 @@ private String trimPrefix(String input) { return input.substring(PREFIX_AMQP_SECURE.length()); } if (input.startsWith(PREFIX_AMQP)) { - input = input.substring(PREFIX_AMQP.length()); + this.secureConnection = false; + return input.substring(PREFIX_AMQP.length()); } return input; } @@ -1036,18 +1110,23 @@ private String parseVirtualHost(String input) { return input; } - private void parseHostAndPort(String input) { - int portIndex = input.indexOf(':'); - if (portIndex == -1) { + private void parseHostAndPort(String input, boolean sslEnabled) { + int bracketIndex = input.lastIndexOf(']'); + int colonIndex = input.lastIndexOf(':'); + if (colonIndex == -1 || colonIndex < bracketIndex) { this.host = input; - this.port = (this.secureConnection) ? DEFAULT_PORT_SECURE : DEFAULT_PORT; + this.port = (determineSslEnabled(sslEnabled)) ? DEFAULT_PORT_SECURE : DEFAULT_PORT; } else { - this.host = input.substring(0, portIndex); - this.port = Integer.valueOf(input.substring(portIndex + 1)); + this.host = input.substring(0, colonIndex); + this.port = Integer.parseInt(input.substring(colonIndex + 1)); } } + private boolean determineSslEnabled(boolean sslEnabled) { + return (this.secureConnection != null) ? this.secureConnection : sslEnabled; + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitTemplateConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitTemplateConfigurer.java new file mode 100644 index 000000000000..f7315d864b93 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitTemplateConfigurer.java @@ -0,0 +1,102 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.amqp; + +import java.time.Duration; +import java.util.List; + +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.boot.context.properties.PropertyMapper; + +/** + * Configure {@link RabbitTemplate} with sensible defaults. + * + * @author Stephane Nicoll + * @since 2.3.0 + */ +public class RabbitTemplateConfigurer { + + private MessageConverter messageConverter; + + private List retryTemplateCustomizers; + + private RabbitProperties rabbitProperties; + + /** + * Set the {@link MessageConverter} to use or {@code null} if the out-of-the-box + * converter should be used. + * @param messageConverter the {@link MessageConverter} + */ + protected void setMessageConverter(MessageConverter messageConverter) { + this.messageConverter = messageConverter; + } + + /** + * Set the {@link RabbitRetryTemplateCustomizer} instances to use. + * @param retryTemplateCustomizers the retry template customizers + */ + protected void setRetryTemplateCustomizers(List retryTemplateCustomizers) { + this.retryTemplateCustomizers = retryTemplateCustomizers; + } + + /** + * Set the {@link RabbitProperties} to use. + * @param rabbitProperties the {@link RabbitProperties} + */ + protected void setRabbitProperties(RabbitProperties rabbitProperties) { + this.rabbitProperties = rabbitProperties; + } + + protected final RabbitProperties getRabbitProperties() { + return this.rabbitProperties; + } + + /** + * Configure the specified {@link RabbitTemplate}. The template can be further tuned + * and default settings can be overridden. + * @param template the {@link RabbitTemplate} instance to configure + * @param connectionFactory the {@link ConnectionFactory} to use + */ + public void configure(RabbitTemplate template, ConnectionFactory connectionFactory) { + PropertyMapper map = PropertyMapper.get(); + template.setConnectionFactory(connectionFactory); + if (this.messageConverter != null) { + template.setMessageConverter(this.messageConverter); + } + template.setMandatory(determineMandatoryFlag()); + RabbitProperties.Template templateProperties = this.rabbitProperties.getTemplate(); + if (templateProperties.getRetry().isEnabled()) { + template.setRetryTemplate(new RetryTemplateFactory(this.retryTemplateCustomizers) + .createRetryTemplate(templateProperties.getRetry(), RabbitRetryTemplateCustomizer.Target.SENDER)); + } + map.from(templateProperties::getReceiveTimeout).whenNonNull().as(Duration::toMillis) + .to(template::setReceiveTimeout); + map.from(templateProperties::getReplyTimeout).whenNonNull().as(Duration::toMillis) + .to(template::setReplyTimeout); + map.from(templateProperties::getExchange).to(template::setExchange); + map.from(templateProperties::getRoutingKey).to(template::setRoutingKey); + map.from(templateProperties::getDefaultReceiveQueue).whenNonNull().to(template::setDefaultReceiveQueue); + } + + private boolean determineMandatoryFlag() { + Boolean mandatory = this.rabbitProperties.getTemplate().getMandatory(); + return (mandatory != null) ? mandatory : this.rabbitProperties.isPublisherReturns(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/SimpleRabbitListenerContainerFactoryConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/SimpleRabbitListenerContainerFactoryConfigurer.java index cb9c86cc711a..cef9bffcfd21 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/SimpleRabbitListenerContainerFactoryConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/SimpleRabbitListenerContainerFactoryConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,6 +39,7 @@ public void configure(SimpleRabbitListenerContainerFactory factory, ConnectionFa map.from(config::getConcurrency).whenNonNull().to(factory::setConcurrentConsumers); map.from(config::getMaxConcurrency).whenNonNull().to(factory::setMaxConcurrentConsumers); map.from(config::getBatchSize).whenNonNull().to(factory::setBatchSize); + map.from(config::isConsumerBatchEnabled).to(factory::setConsumerBatchEnabled); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/aop/AopAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/aop/AopAutoConfiguration.java index c9833dfa41f2..3755286d5585 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/aop/AopAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/aop/AopAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,11 +19,12 @@ import org.aspectj.weaver.Advice; import org.springframework.aop.config.AopConfigUtils; -import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; @@ -51,8 +52,7 @@ static class AspectJAutoProxyingConfiguration { @Configuration(proxyBeanMethods = false) @EnableAspectJAutoProxy(proxyTargetClass = false) - @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false", - matchIfMissing = false) + @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false") static class JdkDynamicAutoProxyConfiguration { } @@ -73,12 +73,15 @@ static class CglibAutoProxyConfiguration { matchIfMissing = true) static class ClassProxyingConfiguration { - ClassProxyingConfiguration(BeanFactory beanFactory) { - if (beanFactory instanceof BeanDefinitionRegistry) { - BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory; - AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry); - AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry); - } + @Bean + static BeanFactoryPostProcessor forceAutoProxyCreatorToUseClassProxying() { + return (beanFactory) -> { + if (beanFactory instanceof BeanDefinitionRegistry) { + BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory; + AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry); + AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry); + } + }; } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/availability/ApplicationAvailabilityAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/availability/ApplicationAvailabilityAutoConfiguration.java new file mode 100644 index 000000000000..ed0e3d8a9d2c --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/availability/ApplicationAvailabilityAutoConfiguration.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.availability; + +import org.springframework.boot.availability.ApplicationAvailabilityBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration} for + * {@link ApplicationAvailabilityBean}. + * + * @author Brian Clozel + * @since 2.3.0 + */ +@Configuration(proxyBeanMethods = false) +public class ApplicationAvailabilityAutoConfiguration { + + @Bean + public ApplicationAvailabilityBean applicationAvailability() { + return new ApplicationAvailabilityBean(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/availability/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/availability/package-info.java new file mode 100644 index 000000000000..2904d14e4b12 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/availability/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for application availability features. + */ +package org.springframework.boot.autoconfigure.availability; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BasicBatchConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BasicBatchConfigurer.java index 64e88cb72efb..15f8ca3e3604 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BasicBatchConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BasicBatchConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.springframework.boot.autoconfigure.batch; -import javax.annotation.PostConstruct; import javax.sql.DataSource; import org.springframework.batch.core.configuration.annotation.BatchConfigurer; @@ -26,6 +25,7 @@ import org.springframework.batch.core.launch.support.SimpleJobLauncher; import org.springframework.batch.core.repository.JobRepository; import org.springframework.batch.core.repository.support.JobRepositoryFactoryBean; +import org.springframework.beans.factory.InitializingBean; import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizers; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.jdbc.datasource.DataSourceTransactionManager; @@ -40,7 +40,7 @@ * @author Stephane Nicoll * @since 1.0.0 */ -public class BasicBatchConfigurer implements BatchConfigurer { +public class BasicBatchConfigurer implements BatchConfigurer, InitializingBean { private final BatchProperties properties; @@ -90,7 +90,11 @@ public JobExplorer getJobExplorer() throws Exception { return this.jobExplorer; } - @PostConstruct + @Override + public void afterPropertiesSet() { + initialize(); + } + public void initialize() { try { this.transactionManager = buildTransactionManager(); @@ -107,7 +111,7 @@ protected JobExplorer createJobExplorer() throws Exception { PropertyMapper map = PropertyMapper.get(); JobExplorerFactoryBean factory = new JobExplorerFactoryBean(); factory.setDataSource(this.dataSource); - map.from(this.properties::getTablePrefix).whenHasText().to(factory::setTablePrefix); + map.from(this.properties.getJdbc()::getTablePrefix).whenHasText().to(factory::setTablePrefix); factory.afterPropertiesSet(); return factory.getObject(); } @@ -124,7 +128,7 @@ protected JobRepository createJobRepository() throws Exception { PropertyMapper map = PropertyMapper.get(); map.from(this.dataSource).to(factory::setDataSource); map.from(this::determineIsolationLevel).whenNonNull().to(factory::setIsolationLevelForCreate); - map.from(this.properties::getTablePrefix).whenHasText().to(factory::setTablePrefix); + map.from(this.properties.getJdbc()::getTablePrefix).whenHasText().to(factory::setTablePrefix); map.from(this::getTransactionManager).to(factory::setTransactionManager); factory.afterPropertiesSet(); return factory.getObject(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfiguration.java index 92deb3dc8075..ce670528d4c8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,6 +35,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.sql.init.dependency.DatabaseInitializationDependencyConfigurer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -64,15 +65,15 @@ @AutoConfigureAfter(HibernateJpaAutoConfiguration.class) @ConditionalOnBean(JobLauncher.class) @EnableConfigurationProperties(BatchProperties.class) -@Import(BatchConfigurerConfiguration.class) +@Import({ BatchConfigurerConfiguration.class, DatabaseInitializationDependencyConfigurer.class }) public class BatchAutoConfiguration { @Bean @ConditionalOnMissingBean @ConditionalOnProperty(prefix = "spring.batch.job", name = "enabled", havingValue = "true", matchIfMissing = true) - public JobLauncherCommandLineRunner jobLauncherCommandLineRunner(JobLauncher jobLauncher, JobExplorer jobExplorer, + public JobLauncherApplicationRunner jobLauncherApplicationRunner(JobLauncher jobLauncher, JobExplorer jobExplorer, JobRepository jobRepository, BatchProperties properties) { - JobLauncherCommandLineRunner runner = new JobLauncherCommandLineRunner(jobLauncher, jobExplorer, jobRepository); + JobLauncherApplicationRunner runner = new JobLauncherApplicationRunner(jobLauncher, jobExplorer, jobRepository); String jobNames = properties.getJob().getNames(); if (StringUtils.hasText(jobNames)) { runner.setJobNames(jobNames); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchDataSourceInitializer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchDataSourceInitializer.java index d673bcb4c52c..007d825b3cb0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchDataSourceInitializer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchDataSourceInitializer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,10 +18,12 @@ import javax.sql.DataSource; +import org.springframework.boot.autoconfigure.batch.BatchProperties.Jdbc; import org.springframework.boot.jdbc.AbstractDataSourceInitializer; import org.springframework.boot.jdbc.DataSourceInitializationMode; import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * Initialize the Spring Batch schema (ignoring errors, so it should be idempotent). @@ -32,31 +34,38 @@ */ public class BatchDataSourceInitializer extends AbstractDataSourceInitializer { - private final BatchProperties properties; + private final Jdbc jdbcProperties; public BatchDataSourceInitializer(DataSource dataSource, ResourceLoader resourceLoader, BatchProperties properties) { super(dataSource, resourceLoader); Assert.notNull(properties, "BatchProperties must not be null"); - this.properties = properties; + this.jdbcProperties = properties.getJdbc(); } @Override protected DataSourceInitializationMode getMode() { - return this.properties.getInitializeSchema(); + return this.jdbcProperties.getInitializeSchema(); } @Override protected String getSchemaLocation() { - return this.properties.getSchema(); + return this.jdbcProperties.getSchema(); } @Override protected String getDatabaseName() { + String platform = this.jdbcProperties.getPlatform(); + if (StringUtils.hasText(platform)) { + return platform; + } String databaseName = super.getDatabaseName(); if ("oracle".equals(databaseName)) { return "oracle10g"; } + if ("mariadb".equals(databaseName)) { + return "mysql"; + } return databaseName; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchProperties.java index d1bdbc0cba91..2cfb4596bae9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.boot.autoconfigure.batch; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; import org.springframework.boot.jdbc.DataSourceInitializationMode; /** @@ -25,64 +26,79 @@ * @author Stephane Nicoll * @author Eddú Meléndez * @author Vedran Pavic + * @author Mukul Kumar Chaundhyan * @since 1.2.0 */ @ConfigurationProperties(prefix = "spring.batch") public class BatchProperties { - private static final String DEFAULT_SCHEMA_LOCATION = "classpath:org/springframework/" - + "batch/core/schema-@@platform@@.sql"; - - /** - * Path to the SQL file to use to initialize the database schema. - */ - private String schema = DEFAULT_SCHEMA_LOCATION; + private final Job job = new Job(); - /** - * Table prefix for all the batch meta-data tables. - */ - private String tablePrefix; + private final Jdbc jdbc = new Jdbc(); /** - * Database schema initialization mode. + * Return the datasource schema. + * @return the schema + * @deprecated since 2.5.0 for removal in 2.7.0 in favor of {@link Jdbc#getSchema()} */ - private DataSourceInitializationMode initializeSchema = DataSourceInitializationMode.EMBEDDED; - - private final Job job = new Job(); - + @Deprecated + @DeprecatedConfigurationProperty(replacement = "spring.batch.jdbc.schema") public String getSchema() { - return this.schema; + return this.jdbc.getSchema(); } + @Deprecated public void setSchema(String schema) { - this.schema = schema; + this.jdbc.setSchema(schema); } + /** + * Return the table prefix. + * @return the table prefix + * @deprecated since 2.5.0 for removal in 2.7.0 in favor of + * {@link Jdbc#getTablePrefix()} + */ + @Deprecated + @DeprecatedConfigurationProperty(replacement = "spring.batch.jdbc.table-prefix") public String getTablePrefix() { - return this.tablePrefix; + return this.jdbc.getTablePrefix(); } + @Deprecated public void setTablePrefix(String tablePrefix) { - this.tablePrefix = tablePrefix; + this.jdbc.setTablePrefix(tablePrefix); } + /** + * Return whether the schema should be initialized. + * @return the initialization mode + * @deprecated since 2.5.0 for removal in 2.7.0 in favor of + * {@link Jdbc#getInitializeSchema()} + */ + @Deprecated + @DeprecatedConfigurationProperty(replacement = "spring.batch.jdbc.initialize-schema") public DataSourceInitializationMode getInitializeSchema() { - return this.initializeSchema; + return this.jdbc.getInitializeSchema(); } + @Deprecated public void setInitializeSchema(DataSourceInitializationMode initializeSchema) { - this.initializeSchema = initializeSchema; + this.jdbc.setInitializeSchema(initializeSchema); } public Job getJob() { return this.job; } + public Jdbc getJdbc() { + return this.jdbc; + } + public static class Job { /** * Comma-separated list of job names to execute on startup (for instance, - * `job1,job2`). By default, all Jobs found in the context are executed. + * 'job1,job2'). By default, all Jobs found in the context are executed. */ private String names = ""; @@ -96,4 +112,64 @@ public void setNames(String names) { } + public static class Jdbc { + + private static final String DEFAULT_SCHEMA_LOCATION = "classpath:org/springframework/" + + "batch/core/schema-@@platform@@.sql"; + + /** + * Path to the SQL file to use to initialize the database schema. + */ + private String schema = DEFAULT_SCHEMA_LOCATION; + + /** + * Platform to use in initialization scripts if the @@platform@@ placeholder is + * used. Auto-detected by default. + */ + private String platform; + + /** + * Table prefix for all the batch meta-data tables. + */ + private String tablePrefix; + + /** + * Database schema initialization mode. + */ + private DataSourceInitializationMode initializeSchema = DataSourceInitializationMode.EMBEDDED; + + public String getSchema() { + return this.schema; + } + + public void setSchema(String schema) { + this.schema = schema; + } + + public String getPlatform() { + return this.platform; + } + + public void setPlatform(String platform) { + this.platform = platform; + } + + public String getTablePrefix() { + return this.tablePrefix; + } + + public void setTablePrefix(String tablePrefix) { + this.tablePrefix = tablePrefix; + } + + public DataSourceInitializationMode getInitializeSchema() { + return this.initializeSchema; + } + + public void setInitializeSchema(DataSourceInitializationMode initializeSchema) { + this.initializeSchema = initializeSchema; + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/JobLauncherApplicationRunner.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/JobLauncherApplicationRunner.java new file mode 100644 index 000000000000..ef18b46b6f51 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/JobLauncherApplicationRunner.java @@ -0,0 +1,248 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.batch; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Properties; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobExecutionException; +import org.springframework.batch.core.JobParameter; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.JobParametersInvalidException; +import org.springframework.batch.core.configuration.JobRegistry; +import org.springframework.batch.core.converter.DefaultJobParametersConverter; +import org.springframework.batch.core.converter.JobParametersConverter; +import org.springframework.batch.core.explore.JobExplorer; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.core.launch.JobParametersNotFoundException; +import org.springframework.batch.core.launch.NoSuchJobException; +import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException; +import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.repository.JobRestartException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.core.Ordered; +import org.springframework.core.log.LogMessage; +import org.springframework.util.Assert; +import org.springframework.util.PatternMatchUtils; +import org.springframework.util.StringUtils; + +/** + * {@link ApplicationRunner} to {@link JobLauncher launch} Spring Batch jobs. Runs all + * jobs in the surrounding context by default. Can also be used to launch a specific job + * by providing a jobName + * + * @author Dave Syer + * @author Jean-Pierre Bergamin + * @author Mahmoud Ben Hassine + * @author Stephane Nicoll + * @since 2.3.0 + */ +public class JobLauncherApplicationRunner implements ApplicationRunner, Ordered, ApplicationEventPublisherAware { + + /** + * The default order for the command line runner. + */ + public static final int DEFAULT_ORDER = 0; + + private static final Log logger = LogFactory.getLog(JobLauncherApplicationRunner.class); + + private JobParametersConverter converter = new DefaultJobParametersConverter(); + + private final JobLauncher jobLauncher; + + private final JobExplorer jobExplorer; + + private final JobRepository jobRepository; + + private JobRegistry jobRegistry; + + private String jobNames; + + private Collection jobs = Collections.emptySet(); + + private int order = DEFAULT_ORDER; + + private ApplicationEventPublisher publisher; + + /** + * Create a new {@link JobLauncherApplicationRunner}. + * @param jobLauncher to launch jobs + * @param jobExplorer to check the job repository for previous executions + * @param jobRepository to check if a job instance exists with the given parameters + * when running a job + */ + public JobLauncherApplicationRunner(JobLauncher jobLauncher, JobExplorer jobExplorer, JobRepository jobRepository) { + Assert.notNull(jobLauncher, "JobLauncher must not be null"); + Assert.notNull(jobExplorer, "JobExplorer must not be null"); + Assert.notNull(jobRepository, "JobRepository must not be null"); + this.jobLauncher = jobLauncher; + this.jobExplorer = jobExplorer; + this.jobRepository = jobRepository; + } + + public void setOrder(int order) { + this.order = order; + } + + @Override + public int getOrder() { + return this.order; + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { + this.publisher = publisher; + } + + @Autowired(required = false) + public void setJobRegistry(JobRegistry jobRegistry) { + this.jobRegistry = jobRegistry; + } + + public void setJobNames(String jobNames) { + this.jobNames = jobNames; + } + + @Autowired(required = false) + public void setJobParametersConverter(JobParametersConverter converter) { + this.converter = converter; + } + + @Autowired(required = false) + public void setJobs(Collection jobs) { + this.jobs = jobs; + } + + @Override + public void run(ApplicationArguments args) throws Exception { + String[] jobArguments = args.getNonOptionArgs().toArray(new String[0]); + run(jobArguments); + } + + public void run(String... args) throws JobExecutionException { + logger.info("Running default command line with: " + Arrays.asList(args)); + launchJobFromProperties(StringUtils.splitArrayElementsIntoProperties(args, "=")); + } + + protected void launchJobFromProperties(Properties properties) throws JobExecutionException { + JobParameters jobParameters = this.converter.getJobParameters(properties); + executeLocalJobs(jobParameters); + executeRegisteredJobs(jobParameters); + } + + private void executeLocalJobs(JobParameters jobParameters) throws JobExecutionException { + for (Job job : this.jobs) { + if (StringUtils.hasText(this.jobNames)) { + String[] jobsToRun = this.jobNames.split(","); + if (!PatternMatchUtils.simpleMatch(jobsToRun, job.getName())) { + logger.debug(LogMessage.format("Skipped job: %s", job.getName())); + continue; + } + } + execute(job, jobParameters); + } + } + + private void executeRegisteredJobs(JobParameters jobParameters) throws JobExecutionException { + if (this.jobRegistry != null && StringUtils.hasText(this.jobNames)) { + String[] jobsToRun = this.jobNames.split(","); + for (String jobName : jobsToRun) { + try { + Job job = this.jobRegistry.getJob(jobName); + if (this.jobs.contains(job)) { + continue; + } + execute(job, jobParameters); + } + catch (NoSuchJobException ex) { + logger.debug(LogMessage.format("No job found in registry for job name: %s", jobName)); + } + } + } + } + + protected void execute(Job job, JobParameters jobParameters) + throws JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException, + JobParametersInvalidException, JobParametersNotFoundException { + JobParameters parameters = getNextJobParameters(job, jobParameters); + JobExecution execution = this.jobLauncher.run(job, parameters); + if (this.publisher != null) { + this.publisher.publishEvent(new JobExecutionEvent(execution)); + } + } + + private JobParameters getNextJobParameters(Job job, JobParameters jobParameters) { + if (this.jobRepository != null && this.jobRepository.isJobInstanceExists(job.getName(), jobParameters)) { + return getNextJobParametersForExisting(job, jobParameters); + } + if (job.getJobParametersIncrementer() == null) { + return jobParameters; + } + JobParameters nextParameters = new JobParametersBuilder(jobParameters, this.jobExplorer) + .getNextJobParameters(job).toJobParameters(); + return merge(nextParameters, jobParameters); + } + + private JobParameters getNextJobParametersForExisting(Job job, JobParameters jobParameters) { + JobExecution lastExecution = this.jobRepository.getLastJobExecution(job.getName(), jobParameters); + if (isStoppedOrFailed(lastExecution) && job.isRestartable()) { + JobParameters previousIdentifyingParameters = getGetIdentifying(lastExecution.getJobParameters()); + return merge(previousIdentifyingParameters, jobParameters); + } + return jobParameters; + } + + private boolean isStoppedOrFailed(JobExecution execution) { + BatchStatus status = (execution != null) ? execution.getStatus() : null; + return (status == BatchStatus.STOPPED || status == BatchStatus.FAILED); + } + + private JobParameters getGetIdentifying(JobParameters parameters) { + HashMap nonIdentifying = new LinkedHashMap<>(parameters.getParameters().size()); + parameters.getParameters().forEach((key, value) -> { + if (value.isIdentifying()) { + nonIdentifying.put(key, value); + } + }); + return new JobParameters(nonIdentifying); + } + + private JobParameters merge(JobParameters parameters, JobParameters additionals) { + Map merged = new LinkedHashMap<>(); + merged.putAll(parameters.getParameters()); + merged.putAll(additionals.getParameters()); + return new JobParameters(merged); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/JobLauncherCommandLineRunner.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/JobLauncherCommandLineRunner.java index 27626e512f57..3435a0d0aeb5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/JobLauncherCommandLineRunner.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/JobLauncherCommandLineRunner.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,82 +16,25 @@ package org.springframework.boot.autoconfigure.batch; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Properties; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.batch.core.BatchStatus; -import org.springframework.batch.core.Job; -import org.springframework.batch.core.JobExecution; -import org.springframework.batch.core.JobExecutionException; -import org.springframework.batch.core.JobParameter; -import org.springframework.batch.core.JobParameters; -import org.springframework.batch.core.JobParametersBuilder; -import org.springframework.batch.core.JobParametersInvalidException; -import org.springframework.batch.core.configuration.JobRegistry; -import org.springframework.batch.core.converter.DefaultJobParametersConverter; -import org.springframework.batch.core.converter.JobParametersConverter; import org.springframework.batch.core.explore.JobExplorer; import org.springframework.batch.core.launch.JobLauncher; -import org.springframework.batch.core.launch.JobParametersNotFoundException; -import org.springframework.batch.core.launch.NoSuchJobException; -import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException; -import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException; import org.springframework.batch.core.repository.JobRepository; -import org.springframework.batch.core.repository.JobRestartException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.CommandLineRunner; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.context.ApplicationEventPublisherAware; -import org.springframework.core.Ordered; -import org.springframework.core.log.LogMessage; -import org.springframework.util.Assert; -import org.springframework.util.PatternMatchUtils; -import org.springframework.util.StringUtils; +import org.springframework.boot.ApplicationRunner; /** - * {@link CommandLineRunner} to {@link JobLauncher launch} Spring Batch jobs. Runs all + * {@link ApplicationRunner} to {@link JobLauncher launch} Spring Batch jobs. Runs all * jobs in the surrounding context by default. Can also be used to launch a specific job - * by providing a jobName + * by providing a jobName. * * @author Dave Syer * @author Jean-Pierre Bergamin * @author Mahmoud Ben Hassine * @since 1.0.0 + * @deprecated since 2.3.0 for removal in 2.6.0 in favor of + * {@link JobLauncherApplicationRunner} */ -public class JobLauncherCommandLineRunner implements CommandLineRunner, Ordered, ApplicationEventPublisherAware { - - /** - * The default order for the command line runner. - */ - public static final int DEFAULT_ORDER = 0; - - private static final Log logger = LogFactory.getLog(JobLauncherCommandLineRunner.class); - - private JobParametersConverter converter = new DefaultJobParametersConverter(); - - private final JobLauncher jobLauncher; - - private final JobExplorer jobExplorer; - - private final JobRepository jobRepository; - - private JobRegistry jobRegistry; - - private String jobNames; - - private Collection jobs = Collections.emptySet(); - - private int order = DEFAULT_ORDER; - - private ApplicationEventPublisher publisher; +@Deprecated +public class JobLauncherCommandLineRunner extends JobLauncherApplicationRunner { /** * Create a new {@link JobLauncherCommandLineRunner}. @@ -101,141 +44,7 @@ public class JobLauncherCommandLineRunner implements CommandLineRunner, Ordered, * when running a job */ public JobLauncherCommandLineRunner(JobLauncher jobLauncher, JobExplorer jobExplorer, JobRepository jobRepository) { - Assert.notNull(jobLauncher, "JobLauncher must not be null"); - Assert.notNull(jobExplorer, "JobExplorer must not be null"); - Assert.notNull(jobRepository, "JobRepository must not be null"); - this.jobLauncher = jobLauncher; - this.jobExplorer = jobExplorer; - this.jobRepository = jobRepository; - } - - public void setOrder(int order) { - this.order = order; - } - - @Override - public int getOrder() { - return this.order; - } - - @Override - public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { - this.publisher = publisher; - } - - @Autowired(required = false) - public void setJobRegistry(JobRegistry jobRegistry) { - this.jobRegistry = jobRegistry; - } - - public void setJobNames(String jobNames) { - this.jobNames = jobNames; - } - - @Autowired(required = false) - public void setJobParametersConverter(JobParametersConverter converter) { - this.converter = converter; - } - - @Autowired(required = false) - public void setJobs(Collection jobs) { - this.jobs = jobs; - } - - @Override - public void run(String... args) throws JobExecutionException { - logger.info("Running default command line with: " + Arrays.asList(args)); - launchJobFromProperties(StringUtils.splitArrayElementsIntoProperties(args, "=")); - } - - protected void launchJobFromProperties(Properties properties) throws JobExecutionException { - JobParameters jobParameters = this.converter.getJobParameters(properties); - executeLocalJobs(jobParameters); - executeRegisteredJobs(jobParameters); - } - - private void executeLocalJobs(JobParameters jobParameters) throws JobExecutionException { - for (Job job : this.jobs) { - if (StringUtils.hasText(this.jobNames)) { - String[] jobsToRun = this.jobNames.split(","); - if (!PatternMatchUtils.simpleMatch(jobsToRun, job.getName())) { - logger.debug(LogMessage.format("Skipped job: %s", job.getName())); - continue; - } - } - execute(job, jobParameters); - } - } - - private void executeRegisteredJobs(JobParameters jobParameters) throws JobExecutionException { - if (this.jobRegistry != null && StringUtils.hasText(this.jobNames)) { - String[] jobsToRun = this.jobNames.split(","); - for (String jobName : jobsToRun) { - try { - Job job = this.jobRegistry.getJob(jobName); - if (this.jobs.contains(job)) { - continue; - } - execute(job, jobParameters); - } - catch (NoSuchJobException ex) { - logger.debug(LogMessage.format("No job found in registry for job name: %s", jobName)); - } - } - } - } - - protected void execute(Job job, JobParameters jobParameters) - throws JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException, - JobParametersInvalidException, JobParametersNotFoundException { - JobParameters parameters = getNextJobParameters(job, jobParameters); - JobExecution execution = this.jobLauncher.run(job, parameters); - if (this.publisher != null) { - this.publisher.publishEvent(new JobExecutionEvent(execution)); - } - } - - private JobParameters getNextJobParameters(Job job, JobParameters jobParameters) { - if (this.jobRepository != null && this.jobRepository.isJobInstanceExists(job.getName(), jobParameters)) { - return getNextJobParametersForExisting(job, jobParameters); - } - if (job.getJobParametersIncrementer() == null) { - return jobParameters; - } - JobParameters nextParameters = new JobParametersBuilder(jobParameters, this.jobExplorer) - .getNextJobParameters(job).toJobParameters(); - return merge(nextParameters, jobParameters); - } - - private JobParameters getNextJobParametersForExisting(Job job, JobParameters jobParameters) { - JobExecution lastExecution = this.jobRepository.getLastJobExecution(job.getName(), jobParameters); - if (isStoppedOrFailed(lastExecution) && job.isRestartable()) { - JobParameters previousIdentifyingParameters = getGetIdentifying(lastExecution.getJobParameters()); - return merge(previousIdentifyingParameters, jobParameters); - } - return jobParameters; - } - - private boolean isStoppedOrFailed(JobExecution execution) { - BatchStatus status = (execution != null) ? execution.getStatus() : null; - return (status == BatchStatus.STOPPED || status == BatchStatus.FAILED); - } - - private JobParameters getGetIdentifying(JobParameters parameters) { - HashMap nonIdentifying = new LinkedHashMap<>(parameters.getParameters().size()); - parameters.getParameters().forEach((key, value) -> { - if (value.isIdentifying()) { - nonIdentifying.put(key, value); - } - }); - return new JobParameters(nonIdentifying); - } - - private JobParameters merge(JobParameters parameters, JobParameters additionals) { - Map merged = new LinkedHashMap<>(); - merged.putAll(parameters.getParameters()); - merged.putAll(additionals.getParameters()); - return new JobParameters(merged); + super(jobLauncher, jobExplorer, jobRepository); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/JobRepositoryDependsOnDatabaseInitializationDetector.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/JobRepositoryDependsOnDatabaseInitializationDetector.java new file mode 100644 index 000000000000..7efefb650640 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/JobRepositoryDependsOnDatabaseInitializationDetector.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.batch; + +import java.util.Collections; +import java.util.Set; + +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.boot.sql.init.dependency.AbstractBeansOfTypeDependsOnDatabaseInitializationDetector; +import org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitializationDetector; + +/** + * {@link DependsOnDatabaseInitializationDetector} for Spring Batch's + * {@link JobRepository}. + * + * @author Henning Pöttker + */ +class JobRepositoryDependsOnDatabaseInitializationDetector + extends AbstractBeansOfTypeDependsOnDatabaseInitializationDetector { + + @Override + protected Set> getDependsOnDatabaseInitializationBeanTypes() { + return Collections.singleton(JobRepository.class); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheAutoConfiguration.java index 108bac9fa999..36093f449c5f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,10 +27,10 @@ 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.couchbase.CouchbaseAutoConfiguration; -import org.springframework.boot.autoconfigure.data.jpa.EntityManagerFactoryDependsOnPostProcessor; +import org.springframework.boot.autoconfigure.data.couchbase.CouchbaseDataAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration; +import org.springframework.boot.autoconfigure.orm.jpa.EntityManagerFactoryDependsOnPostProcessor; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cache.CacheManager; @@ -61,7 +61,7 @@ @ConditionalOnBean(CacheAspectSupport.class) @ConditionalOnMissingBean(value = CacheManager.class, name = "cacheResolver") @EnableConfigurationProperties(CacheProperties.class) -@AutoConfigureAfter({ CouchbaseAutoConfiguration.class, HazelcastAutoConfiguration.class, +@AutoConfigureAfter({ CouchbaseDataAutoConfiguration.class, HazelcastAutoConfiguration.class, HibernateJpaAutoConfiguration.class, RedisAutoConfiguration.class }) @Import({ CacheConfigurationImportSelector.class, CacheManagerEntityManagerFactoryDependsOnPostProcessor.class }) public class CacheAutoConfiguration { @@ -107,7 +107,7 @@ static class CacheManagerValidator implements InitializingBean { @Override public void afterPropertiesSet() { Assert.notNull(this.cacheManager.getIfAvailable(), - () -> "No cache manager could be auto-configured, check your configuration (caching " + "type is '" + () -> "No cache manager could be auto-configured, check your configuration (caching type is '" + this.cacheProperties.getType() + "')"); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheConfigurations.java index 02283269988c..9fe6aacb0fbc 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheConfigurations.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheConfigurations.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,23 +27,24 @@ * * @author Phillip Webb * @author Eddú Meléndez + * @author Sebastien Deleuze */ final class CacheConfigurations { - private static final Map> MAPPINGS; + private static final Map MAPPINGS; static { - Map> mappings = new EnumMap<>(CacheType.class); - mappings.put(CacheType.GENERIC, GenericCacheConfiguration.class); - mappings.put(CacheType.EHCACHE, EhCacheCacheConfiguration.class); - mappings.put(CacheType.HAZELCAST, HazelcastCacheConfiguration.class); - mappings.put(CacheType.INFINISPAN, InfinispanCacheConfiguration.class); - mappings.put(CacheType.JCACHE, JCacheCacheConfiguration.class); - mappings.put(CacheType.COUCHBASE, CouchbaseCacheConfiguration.class); - mappings.put(CacheType.REDIS, RedisCacheConfiguration.class); - mappings.put(CacheType.CAFFEINE, CaffeineCacheConfiguration.class); - mappings.put(CacheType.SIMPLE, SimpleCacheConfiguration.class); - mappings.put(CacheType.NONE, NoOpCacheConfiguration.class); + Map mappings = new EnumMap<>(CacheType.class); + mappings.put(CacheType.GENERIC, GenericCacheConfiguration.class.getName()); + mappings.put(CacheType.EHCACHE, EhCacheCacheConfiguration.class.getName()); + mappings.put(CacheType.HAZELCAST, HazelcastCacheConfiguration.class.getName()); + mappings.put(CacheType.INFINISPAN, InfinispanCacheConfiguration.class.getName()); + mappings.put(CacheType.JCACHE, JCacheCacheConfiguration.class.getName()); + mappings.put(CacheType.COUCHBASE, CouchbaseCacheConfiguration.class.getName()); + mappings.put(CacheType.REDIS, RedisCacheConfiguration.class.getName()); + mappings.put(CacheType.CAFFEINE, CaffeineCacheConfiguration.class.getName()); + mappings.put(CacheType.SIMPLE, SimpleCacheConfiguration.class.getName()); + mappings.put(CacheType.NONE, NoOpCacheConfiguration.class.getName()); MAPPINGS = Collections.unmodifiableMap(mappings); } @@ -51,14 +52,14 @@ private CacheConfigurations() { } static String getConfigurationClass(CacheType cacheType) { - Class configurationClass = MAPPINGS.get(cacheType); - Assert.state(configurationClass != null, () -> "Unknown cache type " + cacheType); - return configurationClass.getName(); + String configurationClassName = MAPPINGS.get(cacheType); + Assert.state(configurationClassName != null, () -> "Unknown cache type " + cacheType); + return configurationClassName; } static CacheType getType(String configurationClassName) { - for (Map.Entry> entry : MAPPINGS.entrySet()) { - if (entry.getValue().getName().equals(configurationClassName)) { + for (Map.Entry entry : MAPPINGS.entrySet()) { + if (entry.getValue().equals(configurationClassName)) { return entry.getKey(); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheProperties.java index 4ef13b702fda..d7357245c801 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -257,6 +257,11 @@ public static class Redis { */ private boolean useKeyPrefix = true; + /** + * Whether to enable cache statistics. + */ + private boolean enableStatistics; + public Duration getTimeToLive() { return this.timeToLive; } @@ -289,6 +294,14 @@ public void setUseKeyPrefix(boolean useKeyPrefix) { this.useKeyPrefix = useKeyPrefix; } + public boolean isEnableStatistics() { + return this.enableStatistics; + } + + public void setEnableStatistics(boolean enableStatistics) { + this.enableStatistics = enableStatistics; + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CouchbaseCacheConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CouchbaseCacheConfiguration.java index 69c1742f13d5..a4ba5952d7bb 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CouchbaseCacheConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CouchbaseCacheConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,47 +16,55 @@ package org.springframework.boot.autoconfigure.cache; -import java.time.Duration; +import java.util.LinkedHashSet; import java.util.List; -import com.couchbase.client.java.Bucket; -import com.couchbase.client.spring.cache.CacheBuilder; -import com.couchbase.client.spring.cache.CouchbaseCacheManager; +import com.couchbase.client.java.Cluster; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.cache.CacheProperties.Couchbase; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; -import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.cache.CacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; -import org.springframework.util.StringUtils; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.cache.CouchbaseCacheManager; +import org.springframework.data.couchbase.cache.CouchbaseCacheManager.CouchbaseCacheManagerBuilder; +import org.springframework.util.ObjectUtils; /** * Couchbase cache configuration. * * @author Stephane Nicoll - * @since 1.4.0 */ @Configuration(proxyBeanMethods = false) -@ConditionalOnClass({ Bucket.class, CouchbaseCacheManager.class }) +@ConditionalOnClass({ Cluster.class, CouchbaseClientFactory.class, CouchbaseCacheManager.class }) @ConditionalOnMissingBean(CacheManager.class) -@ConditionalOnSingleCandidate(Bucket.class) +@ConditionalOnSingleCandidate(CouchbaseClientFactory.class) @Conditional(CacheCondition.class) -public class CouchbaseCacheConfiguration { +class CouchbaseCacheConfiguration { @Bean - public CouchbaseCacheManager cacheManager(CacheProperties cacheProperties, CacheManagerCustomizers customizers, - Bucket bucket) { + CouchbaseCacheManager cacheManager(CacheProperties cacheProperties, CacheManagerCustomizers customizers, + ObjectProvider couchbaseCacheManagerBuilderCustomizers, + CouchbaseClientFactory clientFactory) { List cacheNames = cacheProperties.getCacheNames(); - CacheBuilder builder = CacheBuilder.newInstance(bucket); + CouchbaseCacheManagerBuilder builder = CouchbaseCacheManager.builder(clientFactory); Couchbase couchbase = cacheProperties.getCouchbase(); - PropertyMapper.get().from(couchbase::getExpiration).whenNonNull().asInt(Duration::getSeconds) - .to(builder::withExpiration); - String[] names = StringUtils.toStringArray(cacheNames); - CouchbaseCacheManager cacheManager = new CouchbaseCacheManager(builder, names); + org.springframework.data.couchbase.cache.CouchbaseCacheConfiguration config = org.springframework.data.couchbase.cache.CouchbaseCacheConfiguration + .defaultCacheConfig(); + if (couchbase.getExpiration() != null) { + config = config.entryExpiry(couchbase.getExpiration()); + } + builder.cacheDefaults(config); + if (!ObjectUtils.isEmpty(cacheNames)) { + builder.initialCacheNames(new LinkedHashSet<>(cacheNames)); + } + couchbaseCacheManagerBuilderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); + CouchbaseCacheManager cacheManager = builder.build(); return customizers.customize(cacheManager); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CouchbaseCacheManagerBuilderCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CouchbaseCacheManagerBuilderCustomizer.java new file mode 100644 index 000000000000..af2d248390cc --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CouchbaseCacheManagerBuilderCustomizer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.cache; + +import org.springframework.data.couchbase.cache.CouchbaseCacheManager; +import org.springframework.data.couchbase.cache.CouchbaseCacheManager.CouchbaseCacheManagerBuilder; + +/** + * Callback interface that can be implemented by beans wishing to customize the + * {@link CouchbaseCacheManagerBuilder} before it is used to build the auto-configured + * {@link CouchbaseCacheManager}. + * + * @author Stephane Nicoll + * @since 2.3.3 + */ +@FunctionalInterface +public interface CouchbaseCacheManagerBuilderCustomizer { + + /** + * Customize the {@link CouchbaseCacheManagerBuilder}. + * @param builder the builder to customize + */ + void customize(CouchbaseCacheManagerBuilder builder); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/RedisCacheConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/RedisCacheConfiguration.java index bf3f23b94e94..5e14f7daa136 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/RedisCacheConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/RedisCacheConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -63,6 +63,9 @@ RedisCacheManager cacheManager(CacheProperties cacheProperties, CacheManagerCust if (!cacheNames.isEmpty()) { builder.initialCacheNames(new LinkedHashSet<>(cacheNames)); } + if (cacheProperties.getRedis().isEnableStatistics()) { + builder.enableStatistics(); + } redisCacheManagerBuilderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); return cacheManagerCustomizers.customize(builder.build()); } @@ -85,7 +88,7 @@ private org.springframework.data.redis.cache.RedisCacheConfiguration createConfi config = config.entryTtl(redisProperties.getTimeToLive()); } if (redisProperties.getKeyPrefix() != null) { - config = config.prefixKeysWith(redisProperties.getKeyPrefix()); + config = config.prefixCacheNameWith(redisProperties.getKeyPrefix()); } if (!redisProperties.isCacheNullValues()) { config = config.disableCachingNullValues(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfiguration.java index d5361b557674..20882e550f2c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,22 +16,44 @@ package org.springframework.boot.autoconfigure.cassandra; +import java.io.IOException; +import java.security.NoSuchAlgorithmException; import java.time.Duration; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; +import java.util.stream.Collectors; -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.PoolingOptions; -import com.datastax.driver.core.QueryOptions; -import com.datastax.driver.core.SocketOptions; +import javax.net.ssl.SSLContext; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.CqlSessionBuilder; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfigLoader; +import com.datastax.oss.driver.api.core.config.DriverOption; +import com.datastax.oss.driver.api.core.config.ProgrammaticDriverConfigLoaderBuilder; +import com.datastax.oss.driver.internal.core.config.typesafe.DefaultDriverConfigLoader; +import com.datastax.oss.driver.internal.core.config.typesafe.DefaultProgrammaticDriverConfigLoaderBuilder; +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.cassandra.CassandraProperties.Connection; +import org.springframework.boot.autoconfigure.cassandra.CassandraProperties.Controlconnection; +import org.springframework.boot.autoconfigure.cassandra.CassandraProperties.Request; +import org.springframework.boot.autoconfigure.cassandra.CassandraProperties.Throttler; +import org.springframework.boot.autoconfigure.cassandra.CassandraProperties.ThrottlerType; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.util.StringUtils; +import org.springframework.context.annotation.Lazy; +import org.springframework.context.annotation.Scope; +import org.springframework.core.io.Resource; /** * {@link EnableAutoConfiguration Auto-configuration} for Cassandra. @@ -44,64 +66,205 @@ * @since 1.3.0 */ @Configuration(proxyBeanMethods = false) -@ConditionalOnClass({ Cluster.class }) +@ConditionalOnClass({ CqlSession.class }) @EnableConfigurationProperties(CassandraProperties.class) public class CassandraAutoConfiguration { @Bean @ConditionalOnMissingBean - public Cluster cassandraCluster(CassandraProperties properties, - ObjectProvider builderCustomizers, - ObjectProvider clusterFactory) { - PropertyMapper map = PropertyMapper.get(); - Cluster.Builder builder = Cluster.builder().withClusterName(properties.getClusterName()) - .withPort(properties.getPort()); - map.from(properties::getUsername).whenNonNull() - .to((username) -> builder.withCredentials(username, properties.getPassword())); - map.from(properties::getCompression).whenNonNull().to(builder::withCompression); - QueryOptions queryOptions = getQueryOptions(properties); - map.from(queryOptions).to(builder::withQueryOptions); - SocketOptions socketOptions = getSocketOptions(properties); - map.from(socketOptions).to(builder::withSocketOptions); - map.from(properties::isSsl).whenTrue().toCall(builder::withSSL); - PoolingOptions poolingOptions = getPoolingOptions(properties); - map.from(poolingOptions).to(builder::withPoolingOptions); - map.from(properties::getContactPoints).as(StringUtils::toStringArray).to(builder::addContactPoints); - map.from(properties::isJmxEnabled).whenFalse().toCall(builder::withoutJMXReporting); + @Lazy + public CqlSession cassandraSession(CqlSessionBuilder cqlSessionBuilder) { + return cqlSessionBuilder.build(); + } + + @Bean + @ConditionalOnMissingBean + @Scope("prototype") + public CqlSessionBuilder cassandraSessionBuilder(CassandraProperties properties, + DriverConfigLoader driverConfigLoader, ObjectProvider builderCustomizers) { + CqlSessionBuilder builder = CqlSession.builder().withConfigLoader(driverConfigLoader); + configureAuthentication(properties, builder); + configureSsl(properties, builder); + builder.withKeyspace(properties.getKeyspaceName()); builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); - return clusterFactory.getIfAvailable(() -> Cluster::buildFrom).create(builder); + return builder; } - private QueryOptions getQueryOptions(CassandraProperties properties) { - PropertyMapper map = PropertyMapper.get(); - QueryOptions options = new QueryOptions(); - map.from(properties::getConsistencyLevel).whenNonNull().to(options::setConsistencyLevel); - map.from(properties::getSerialConsistencyLevel).whenNonNull().to(options::setSerialConsistencyLevel); - map.from(properties::getFetchSize).to(options::setFetchSize); - return options; + private void configureAuthentication(CassandraProperties properties, CqlSessionBuilder builder) { + if (properties.getUsername() != null) { + builder.withAuthCredentials(properties.getUsername(), properties.getPassword()); + } } - private SocketOptions getSocketOptions(CassandraProperties properties) { - PropertyMapper map = PropertyMapper.get(); - SocketOptions options = new SocketOptions(); - map.from(properties::getConnectTimeout).whenNonNull().asInt(Duration::toMillis) - .to(options::setConnectTimeoutMillis); - map.from(properties::getReadTimeout).whenNonNull().asInt(Duration::toMillis).to(options::setReadTimeoutMillis); - return options; + private void configureSsl(CassandraProperties properties, CqlSessionBuilder builder) { + if (properties.isSsl()) { + try { + builder.withSslContext(SSLContext.getDefault()); + } + catch (NoSuchAlgorithmException ex) { + throw new IllegalStateException("Could not setup SSL default context for Cassandra", ex); + } + } + } + + @Bean(destroyMethod = "") + @ConditionalOnMissingBean + public DriverConfigLoader cassandraDriverConfigLoader(CassandraProperties properties, + ObjectProvider builderCustomizers) { + ProgrammaticDriverConfigLoaderBuilder builder = new DefaultProgrammaticDriverConfigLoaderBuilder( + () -> cassandraConfiguration(properties), DefaultDriverConfigLoader.DEFAULT_ROOT_PATH); + builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); + return builder.build(); + } + + private Config cassandraConfiguration(CassandraProperties properties) { + Config config = mapConfig(properties); + Resource configFile = properties.getConfig(); + return (configFile != null) ? applyDefaultFallback(config.withFallback(loadConfig(configFile))) + : applyDefaultFallback(config); + } + + private Config applyDefaultFallback(Config config) { + ConfigFactory.invalidateCaches(); + return ConfigFactory.defaultOverrides().withFallback(config).withFallback(ConfigFactory.defaultReference()) + .resolve(); } - private PoolingOptions getPoolingOptions(CassandraProperties properties) { + private Config loadConfig(Resource config) { + try { + return ConfigFactory.parseURL(config.getURL()); + } + catch (IOException ex) { + throw new IllegalStateException("Failed to load cassandra configuration from " + config, ex); + } + } + + private Config mapConfig(CassandraProperties properties) { + CassandraDriverOptions options = new CassandraDriverOptions(); PropertyMapper map = PropertyMapper.get(); + map.from(properties.getSessionName()).whenHasText() + .to((sessionName) -> options.add(DefaultDriverOption.SESSION_NAME, sessionName)); + map.from(properties::getUsername).whenNonNull() + .to((username) -> options.add(DefaultDriverOption.AUTH_PROVIDER_USER_NAME, username) + .add(DefaultDriverOption.AUTH_PROVIDER_PASSWORD, properties.getPassword())); + map.from(properties::getCompression).whenNonNull() + .to((compression) -> options.add(DefaultDriverOption.PROTOCOL_COMPRESSION, compression)); + mapConnectionOptions(properties, options); + mapPoolingOptions(properties, options); + mapRequestOptions(properties, options); + mapControlConnectionOptions(properties, options); + map.from(mapContactPoints(properties)) + .to((contactPoints) -> options.add(DefaultDriverOption.CONTACT_POINTS, contactPoints)); + map.from(properties.getLocalDatacenter()).to( + (localDatacenter) -> options.add(DefaultDriverOption.LOAD_BALANCING_LOCAL_DATACENTER, localDatacenter)); + return options.build(); + } + + private void mapConnectionOptions(CassandraProperties properties, CassandraDriverOptions options) { + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + Connection connectionProperties = properties.getConnection(); + map.from(connectionProperties::getConnectTimeout).asInt(Duration::toMillis) + .to((connectTimeout) -> options.add(DefaultDriverOption.CONNECTION_CONNECT_TIMEOUT, connectTimeout)); + map.from(connectionProperties::getInitQueryTimeout).asInt(Duration::toMillis).to( + (initQueryTimeout) -> options.add(DefaultDriverOption.CONNECTION_INIT_QUERY_TIMEOUT, initQueryTimeout)); + } + + private void mapPoolingOptions(CassandraProperties properties, CassandraDriverOptions options) { + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); CassandraProperties.Pool poolProperties = properties.getPool(); - PoolingOptions options = new PoolingOptions(); - map.from(poolProperties::getIdleTimeout).whenNonNull().asInt(Duration::getSeconds) - .to(options::setIdleTimeoutSeconds); - map.from(poolProperties::getPoolTimeout).whenNonNull().asInt(Duration::toMillis) - .to(options::setPoolTimeoutMillis); - map.from(poolProperties::getHeartbeatInterval).whenNonNull().asInt(Duration::getSeconds) - .to(options::setHeartbeatIntervalSeconds); - map.from(poolProperties::getMaxQueueSize).to(options::setMaxQueueSize); - return options; + map.from(poolProperties::getIdleTimeout).asInt(Duration::toMillis) + .to((idleTimeout) -> options.add(DefaultDriverOption.HEARTBEAT_TIMEOUT, idleTimeout)); + map.from(poolProperties::getHeartbeatInterval).asInt(Duration::toMillis) + .to((heartBeatInterval) -> options.add(DefaultDriverOption.HEARTBEAT_INTERVAL, heartBeatInterval)); + } + + private void mapRequestOptions(CassandraProperties properties, CassandraDriverOptions options) { + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + Request requestProperties = properties.getRequest(); + map.from(requestProperties::getTimeout).asInt(Duration::toMillis) + .to(((timeout) -> options.add(DefaultDriverOption.REQUEST_TIMEOUT, timeout))); + map.from(requestProperties::getConsistency) + .to(((consistency) -> options.add(DefaultDriverOption.REQUEST_CONSISTENCY, consistency))); + map.from(requestProperties::getSerialConsistency).to( + (serialConsistency) -> options.add(DefaultDriverOption.REQUEST_SERIAL_CONSISTENCY, serialConsistency)); + map.from(requestProperties::getPageSize) + .to((pageSize) -> options.add(DefaultDriverOption.REQUEST_PAGE_SIZE, pageSize)); + Throttler throttlerProperties = requestProperties.getThrottler(); + map.from(throttlerProperties::getType).as(ThrottlerType::type) + .to((type) -> options.add(DefaultDriverOption.REQUEST_THROTTLER_CLASS, type)); + map.from(throttlerProperties::getMaxQueueSize) + .to((maxQueueSize) -> options.add(DefaultDriverOption.REQUEST_THROTTLER_MAX_QUEUE_SIZE, maxQueueSize)); + map.from(throttlerProperties::getMaxConcurrentRequests).to((maxConcurrentRequests) -> options + .add(DefaultDriverOption.REQUEST_THROTTLER_MAX_CONCURRENT_REQUESTS, maxConcurrentRequests)); + map.from(throttlerProperties::getMaxRequestsPerSecond).to((maxRequestsPerSecond) -> options + .add(DefaultDriverOption.REQUEST_THROTTLER_MAX_REQUESTS_PER_SECOND, maxRequestsPerSecond)); + map.from(throttlerProperties::getDrainInterval).asInt(Duration::toMillis).to( + (drainInterval) -> options.add(DefaultDriverOption.REQUEST_THROTTLER_DRAIN_INTERVAL, drainInterval)); + } + + private void mapControlConnectionOptions(CassandraProperties properties, CassandraDriverOptions options) { + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + Controlconnection controlProperties = properties.getControlconnection(); + map.from(controlProperties::getTimeout).asInt(Duration::toMillis) + .to((timeout) -> options.add(DefaultDriverOption.CONTROL_CONNECTION_TIMEOUT, timeout)); + } + + private List mapContactPoints(CassandraProperties properties) { + return properties.getContactPoints().stream() + .map((candidate) -> formatContactPoint(candidate, properties.getPort())).collect(Collectors.toList()); + } + + private String formatContactPoint(String candidate, int port) { + int i = candidate.lastIndexOf(':'); + if (i == -1 || !isPort(() -> candidate.substring(i + 1))) { + return String.format("%s:%s", candidate, port); + } + return candidate; + } + + private boolean isPort(Supplier value) { + try { + int i = Integer.parseInt(value.get()); + return i > 0 && i < 65535; + } + catch (Exception ex) { + return false; + } + } + + private static class CassandraDriverOptions { + + private final Map options = new LinkedHashMap<>(); + + private CassandraDriverOptions add(DriverOption option, String value) { + String key = createKeyFor(option); + this.options.put(key, value); + return this; + } + + private CassandraDriverOptions add(DriverOption option, int value) { + return add(option, String.valueOf(value)); + } + + private CassandraDriverOptions add(DriverOption option, Enum value) { + return add(option, value.name()); + } + + private CassandraDriverOptions add(DriverOption option, List values) { + for (int i = 0; i < values.size(); i++) { + this.options.put(String.format("%s.%s", createKeyFor(option), i), values.get(i)); + } + return this; + } + + private Config build() { + return ConfigFactory.parseMap(this.options, "Environment"); + } + + private static String createKeyFor(DriverOption option) { + return String.format("%s.%s", DefaultDriverConfigLoader.DEFAULT_ROOT_PATH, option.getPath()); + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraProperties.java index 47d5cdef637a..8c72e048a21b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,18 +17,14 @@ package org.springframework.boot.autoconfigure.cassandra; import java.time.Duration; -import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import com.datastax.driver.core.ConsistencyLevel; -import com.datastax.driver.core.ProtocolOptions; -import com.datastax.driver.core.ProtocolOptions.Compression; -import com.datastax.driver.core.QueryOptions; +import com.datastax.oss.driver.api.core.DefaultConsistencyLevel; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.convert.DurationUnit; +import org.springframework.core.io.Resource; /** * Configuration properties for Cassandra. @@ -42,25 +38,37 @@ @ConfigurationProperties(prefix = "spring.data.cassandra") public class CassandraProperties { + /** + * Location of the configuration file to use. + */ + private Resource config; + /** * Keyspace name to use. */ private String keyspaceName; /** - * Name of the Cassandra cluster. + * Name of the Cassandra session. + */ + private String sessionName; + + /** + * Cluster node addresses in the form 'host:port', or a simple 'host' to use the + * configured port. */ - private String clusterName; + private final List contactPoints = new ArrayList<>(Collections.singleton("127.0.0.1:9042")); /** - * Cluster node addresses. + * Port to use if a contact point does not specify one. */ - private final List contactPoints = new ArrayList<>(Collections.singleton("localhost")); + private int port = 9042; /** - * Port of the Cassandra server. + * Datacenter that is considered "local". Contact points should be from this + * datacenter. */ - private int port = ProtocolOptions.DEFAULT_PORT; + private String localDatacenter; /** * Login user of the server. @@ -78,50 +86,42 @@ public class CassandraProperties { private Compression compression = Compression.NONE; /** - * Queries consistency level. - */ - private ConsistencyLevel consistencyLevel; - - /** - * Queries serial consistency level. + * Schema action to take at startup. */ - private ConsistencyLevel serialConsistencyLevel; + private String schemaAction = "none"; /** - * Queries default fetch size. + * Enable SSL support. */ - private int fetchSize = QueryOptions.DEFAULT_FETCH_SIZE; + private boolean ssl = false; /** - * Socket option: connection time out. + * Connection configuration. */ - private Duration connectTimeout; + private final Connection connection = new Connection(); /** - * Socket option: read time out. + * Pool configuration. */ - private Duration readTimeout; + private final Pool pool = new Pool(); /** - * Schema action to take at startup. + * Request configuration. */ - private String schemaAction = "none"; + private final Request request = new Request(); /** - * Enable SSL support. + * Control connection configuration. */ - private boolean ssl = false; + private final Controlconnection controlconnection = new Controlconnection(); - /** - * Whether to enable JMX reporting. Default to false as Cassandra JMX reporting is not - * compatible with Dropwizard Metrics. - */ - private boolean jmxEnabled; + public Resource getConfig() { + return this.config; + } - /** - * Pool configuration. - */ - private final Pool pool = new Pool(); + public void setConfig(Resource config) { + this.config = config; + } public String getKeyspaceName() { return this.keyspaceName; @@ -131,12 +131,12 @@ public void setKeyspaceName(String keyspaceName) { this.keyspaceName = keyspaceName; } - public String getClusterName() { - return this.clusterName; + public String getSessionName() { + return this.sessionName; } - public void setClusterName(String clusterName) { - this.clusterName = clusterName; + public void setSessionName(String sessionName) { + this.sessionName = sessionName; } public List getContactPoints() { @@ -151,6 +151,14 @@ public void setPort(int port) { this.port = port; } + public String getLocalDatacenter() { + return this.localDatacenter; + } + + public void setLocalDatacenter(String localDatacenter) { + this.localDatacenter = localDatacenter; + } + public String getUsername() { return this.username; } @@ -175,103 +183,146 @@ public void setCompression(Compression compression) { this.compression = compression; } - public ConsistencyLevel getConsistencyLevel() { - return this.consistencyLevel; + public boolean isSsl() { + return this.ssl; } - public void setConsistencyLevel(ConsistencyLevel consistency) { - this.consistencyLevel = consistency; + public void setSsl(boolean ssl) { + this.ssl = ssl; } - public ConsistencyLevel getSerialConsistencyLevel() { - return this.serialConsistencyLevel; + public String getSchemaAction() { + return this.schemaAction; } - public void setSerialConsistencyLevel(ConsistencyLevel serialConsistency) { - this.serialConsistencyLevel = serialConsistency; + public void setSchemaAction(String schemaAction) { + this.schemaAction = schemaAction; } - public int getFetchSize() { - return this.fetchSize; + public Connection getConnection() { + return this.connection; } - public void setFetchSize(int fetchSize) { - this.fetchSize = fetchSize; + public Pool getPool() { + return this.pool; } - public Duration getConnectTimeout() { - return this.connectTimeout; + public Request getRequest() { + return this.request; } - public void setConnectTimeout(Duration connectTimeout) { - this.connectTimeout = connectTimeout; + public Controlconnection getControlconnection() { + return this.controlconnection; } - public Duration getReadTimeout() { - return this.readTimeout; - } + public static class Connection { - public void setReadTimeout(Duration readTimeout) { - this.readTimeout = readTimeout; - } + /** + * Timeout to use when establishing driver connections. + */ + private Duration connectTimeout; - public boolean isSsl() { - return this.ssl; - } + /** + * Timeout to use for internal queries that run as part of the initialization + * process, just after a connection is opened. + */ + private Duration initQueryTimeout; - public void setSsl(boolean ssl) { - this.ssl = ssl; - } + public Duration getConnectTimeout() { + return this.connectTimeout; + } - public boolean isJmxEnabled() { - return this.jmxEnabled; - } + public void setConnectTimeout(Duration connectTimeout) { + this.connectTimeout = connectTimeout; + } - public void setJmxEnabled(boolean jmxEnabled) { - this.jmxEnabled = jmxEnabled; - } + public Duration getInitQueryTimeout() { + return this.initQueryTimeout; + } - public String getSchemaAction() { - return this.schemaAction; - } + public void setInitQueryTimeout(Duration initQueryTimeout) { + this.initQueryTimeout = initQueryTimeout; + } - public void setSchemaAction(String schemaAction) { - this.schemaAction = schemaAction; } - public Pool getPool() { - return this.pool; - } + public static class Request { - /** - * Pool properties. - */ - public static class Pool { + /** + * How long the driver waits for a request to complete. + */ + private Duration timeout; /** - * Idle timeout before an idle connection is removed. If a duration suffix is not - * specified, seconds will be used. + * Queries consistency level. */ - @DurationUnit(ChronoUnit.SECONDS) - private Duration idleTimeout = Duration.ofSeconds(120); + private DefaultConsistencyLevel consistency; /** - * Pool timeout when trying to acquire a connection from a host's pool. + * Queries serial consistency level. */ - private Duration poolTimeout = Duration.ofMillis(5000); + private DefaultConsistencyLevel serialConsistency; /** - * Heartbeat interval after which a message is sent on an idle connection to make - * sure it's still alive. If a duration suffix is not specified, seconds will be - * used. + * How many rows will be retrieved simultaneously in a single network roundtrip. */ - @DurationUnit(ChronoUnit.SECONDS) - private Duration heartbeatInterval = Duration.ofSeconds(30); + private int pageSize; + + private final Throttler throttler = new Throttler(); + + public Duration getTimeout() { + return this.timeout; + } + + public void setTimeout(Duration timeout) { + this.timeout = timeout; + } + + public DefaultConsistencyLevel getConsistency() { + return this.consistency; + } + + public void setConsistency(DefaultConsistencyLevel consistency) { + this.consistency = consistency; + } + + public DefaultConsistencyLevel getSerialConsistency() { + return this.serialConsistency; + } + + public void setSerialConsistency(DefaultConsistencyLevel serialConsistency) { + this.serialConsistency = serialConsistency; + } + + public int getPageSize() { + return this.pageSize; + } + + public void setPageSize(int pageSize) { + this.pageSize = pageSize; + } + + public Throttler getThrottler() { + return this.throttler; + } + + } + + /** + * Pool properties. + */ + public static class Pool { + + /** + * Idle timeout before an idle connection is removed. + */ + private Duration idleTimeout; /** - * Maximum number of requests that get queued if no connection is available. + * Heartbeat interval after which a message is sent on an idle connection to make + * sure it's still alive. */ - private int maxQueueSize = 256; + private Duration heartbeatInterval; public Duration getIdleTimeout() { return this.idleTimeout; @@ -281,14 +332,6 @@ public void setIdleTimeout(Duration idleTimeout) { this.idleTimeout = idleTimeout; } - public Duration getPoolTimeout() { - return this.poolTimeout; - } - - public void setPoolTimeout(Duration poolTimeout) { - this.poolTimeout = poolTimeout; - } - public Duration getHeartbeatInterval() { return this.heartbeatInterval; } @@ -297,6 +340,63 @@ public void setHeartbeatInterval(Duration heartbeatInterval) { this.heartbeatInterval = heartbeatInterval; } + } + + public static class Controlconnection { + + /** + * Timeout to use for control queries. + */ + private Duration timeout = Duration.ofSeconds(5); + + public Duration getTimeout() { + return this.timeout; + } + + public void setTimeout(Duration timeout) { + this.timeout = timeout; + } + + } + + public static class Throttler { + + /** + * Request throttling type. + */ + private ThrottlerType type; + + /** + * Maximum number of requests that can be enqueued when the throttling threshold + * is exceeded. + */ + private int maxQueueSize; + + /** + * Maximum number of requests that are allowed to execute in parallel. + */ + private int maxConcurrentRequests; + + /** + * Maximum allowed request rate. + */ + private int maxRequestsPerSecond; + + /** + * How often the throttler attempts to dequeue requests. Set this high enough that + * each attempt will process multiple entries in the queue, but not delay requests + * too much. + */ + private Duration drainInterval; + + public ThrottlerType getType() { + return this.type; + } + + public void setType(ThrottlerType type) { + this.type = type; + } + public int getMaxQueueSize() { return this.maxQueueSize; } @@ -305,6 +405,81 @@ public void setMaxQueueSize(int maxQueueSize) { this.maxQueueSize = maxQueueSize; } + public int getMaxConcurrentRequests() { + return this.maxConcurrentRequests; + } + + public void setMaxConcurrentRequests(int maxConcurrentRequests) { + this.maxConcurrentRequests = maxConcurrentRequests; + } + + public int getMaxRequestsPerSecond() { + return this.maxRequestsPerSecond; + } + + public void setMaxRequestsPerSecond(int maxRequestsPerSecond) { + this.maxRequestsPerSecond = maxRequestsPerSecond; + } + + public Duration getDrainInterval() { + return this.drainInterval; + } + + public void setDrainInterval(Duration drainInterval) { + this.drainInterval = drainInterval; + } + + } + + /** + * Name of the algorithm used to compress protocol frames. + */ + public enum Compression { + + /** + * Requires 'net.jpountz.lz4:lz4'. + */ + LZ4, + + /** + * Requires org.xerial.snappy:snappy-java. + */ + SNAPPY, + + /** + * No compression. + */ + NONE; + + } + + public enum ThrottlerType { + + /** + * Limit the number of requests that can be executed in parallel. + */ + CONCURRENCY_LIMITING("ConcurrencyLimitingRequestThrottler"), + + /** + * Limits the request rate per second. + */ + RATE_LIMITING("RateLimitingRequestThrottler"), + + /** + * No request throttling. + */ + NONE("PassThroughRequestThrottler"); + + private final String type; + + ThrottlerType(String type) { + this.type = type; + } + + public String type() { + return this.type; + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/ClusterBuilderCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/ClusterBuilderCustomizer.java deleted file mode 100644 index 10ebf2df5978..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/ClusterBuilderCustomizer.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.cassandra; - -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.Cluster.Builder; - -/** - * Callback interface that can be implemented by beans wishing to customize the - * {@link Cluster} via a {@link Builder Cluster.Builder} whilst retaining default - * auto-configuration. - * - * @author Eddú Meléndez - * @since 1.5.0 - */ -@FunctionalInterface -public interface ClusterBuilderCustomizer { - - /** - * Customize the {@link Builder}. - * @param clusterBuilder the builder to customize - */ - void customize(Builder clusterBuilder); - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/ClusterFactory.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/ClusterFactory.java deleted file mode 100644 index 83dcb773ffc4..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/ClusterFactory.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.cassandra; - -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.Cluster.Initializer; - -/** - * {@code ClusterFactory} provides control over the creation of a {@link Cluster} from an - * {@link Initializer}. - * - * @author Steffen F. Qvistgaard - * @since 2.2.0 - */ -@FunctionalInterface -public interface ClusterFactory { - - /** - * Creates a {@link Cluster} from the given {@link Initializer}. - * @param initializer the {@code Initializer} - * @return the {@code Cluster} - */ - Cluster create(Initializer initializer); - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CqlSessionBuilderCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CqlSessionBuilderCustomizer.java new file mode 100644 index 000000000000..70d48f503142 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CqlSessionBuilderCustomizer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.cassandra; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.CqlSessionBuilder; + +/** + * Callback interface that can be implemented by beans wishing to customize the + * {@link CqlSession} via a {@link CqlSessionBuilder} whilst retaining default + * auto-configuration. + * + * @author Stephane Nicoll + * @since 2.3.0 + */ +@FunctionalInterface +public interface CqlSessionBuilderCustomizer { + + /** + * Customize the {@link CqlSessionBuilder}. + * @param cqlSessionBuilder the builder to customize + */ + void customize(CqlSessionBuilder cqlSessionBuilder); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/DriverConfigLoaderBuilderCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/DriverConfigLoaderBuilderCustomizer.java new file mode 100644 index 000000000000..020191e6731c --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/DriverConfigLoaderBuilderCustomizer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.cassandra; + +import com.datastax.oss.driver.api.core.config.DriverConfigLoader; +import com.datastax.oss.driver.api.core.config.ProgrammaticDriverConfigLoaderBuilder; + +/** + * Callback interface that can be implemented by beans wishing to customize the + * {@link DriverConfigLoader} via a {@link DriverConfigLoaderBuilderCustomizer} whilst + * retaining default auto-configuration. + * + * @author Stephane Nicoll + * @since 2.3.0 + */ +public interface DriverConfigLoaderBuilderCustomizer { + + /** + * Customize the {@linkplain ProgrammaticDriverConfigLoaderBuilder DriverConfigLoader + * builder}. + * @param builder the builder to customize + */ + void customize(ProgrammaticDriverConfigLoaderBuilder builder); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cloud/CloudServiceConnectorsAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cloud/CloudServiceConnectorsAutoConfiguration.java deleted file mode 100644 index e5f76ba18410..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cloud/CloudServiceConnectorsAutoConfiguration.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.cloud; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.boot.autoconfigure.AutoConfigureOrder; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.cloud.Cloud; -import org.springframework.cloud.app.ApplicationInstanceInfo; -import org.springframework.cloud.config.java.CloudScan; -import org.springframework.cloud.config.java.CloudScanConfiguration; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.Profile; -import org.springframework.core.Ordered; - -/** - * {@link EnableAutoConfiguration Auto-configuration} for Spring Cloud Service Connectors. - *

- * Activates when there is no bean of type {@link Cloud} and the "cloud" profile is - * active. - *

- * Once in effect, the auto-configuration is the equivalent of adding the - * {@link CloudScan @CloudScan} annotation in one of the configuration file. Specifically, - * it adds a bean for each service bound to the application and one for - * {@link ApplicationInstanceInfo}. - * - * @author Ramnivas Laddad - * @since 2.1.0 - */ -@Configuration(proxyBeanMethods = false) -@Profile("cloud") -@AutoConfigureOrder(CloudServiceConnectorsAutoConfiguration.ORDER) -@ConditionalOnClass(CloudScanConfiguration.class) -@ConditionalOnMissingBean(Cloud.class) -@Import(CloudScanConfiguration.class) -public class CloudServiceConnectorsAutoConfiguration { - - // Cloud configuration needs to happen early (before data, mongo etc.) - public static final int ORDER = Ordered.HIGHEST_PRECEDENCE + 20; - - private static final Log logger = LogFactory.getLog(CloudServiceConnectorsAutoConfiguration.class); - - public CloudServiceConnectorsAutoConfiguration() { - logger.warn("Support for Spring Cloud Connectors has been deprecated in favor of Java CFEnv"); - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cloud/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cloud/package-info.java deleted file mode 100644 index 2c3cdf2842ad..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cloud/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Auto-configuration for Spring Cloud Service Connectors. - */ -package org.springframework.boot.autoconfigure.cloud; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/codec/CodecProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/codec/CodecProperties.java index dcec5439194b..692b8bb817c7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/codec/CodecProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/codec/CodecProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,13 +28,27 @@ @ConfigurationProperties(prefix = "spring.codec") public class CodecProperties { + /** + * Whether to log form data at DEBUG level, and headers at TRACE level. + */ + private boolean logRequestDetails; + /** * Limit on the number of bytes that can be buffered whenever the input stream needs - * to be aggregated. By default this is not set, in which case individual codec + * to be aggregated. This applies only to the auto-configured WebFlux server and + * WebClient instances. By default this is not set, in which case individual codec * defaults apply. Most codecs are limited to 256K by default. */ private DataSize maxInMemorySize; + public boolean isLogRequestDetails() { + return this.logRequestDetails; + } + + public void setLogRequestDetails(boolean logRequestDetails) { + this.logRequestDetails = logRequestDetails; + } + public DataSize getMaxInMemorySize() { return this.maxInMemorySize; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionMessage.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionMessage.java index 400f93d77852..42c9826464a1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionMessage.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionMessage.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,7 +37,7 @@ */ public final class ConditionMessage { - private String message; + private final String message; private ConditionMessage() { this(null); @@ -290,23 +290,23 @@ public ConditionMessage notAvailable(String item) { } /** - * Indicates the reason. For example {@code reason("running Linux")} results in + * Indicates the reason. For example {@code because("running Linux")} results in * the message "running Linux". * @param reason the reason for the message * @return a built {@link ConditionMessage} */ public ConditionMessage because(String reason) { - if (StringUtils.isEmpty(reason)) { - return new ConditionMessage(ConditionMessage.this, this.condition); + if (StringUtils.hasLength(reason)) { + return new ConditionMessage(ConditionMessage.this, + StringUtils.hasLength(this.condition) ? this.condition + " " + reason : reason); } - return new ConditionMessage(ConditionMessage.this, - this.condition + (StringUtils.isEmpty(this.condition) ? "" : " ") + reason); + return new ConditionMessage(ConditionMessage.this, this.condition); } } /** - * Builder used to create a {@link ItemsBuilder} for a condition. + * Builder used to create an {@link ItemsBuilder} for a condition. */ public final class ItemsBuilder { @@ -381,7 +381,8 @@ public ConditionMessage items(Style style, Collection items) { Assert.notNull(style, "Style must not be null"); StringBuilder message = new StringBuilder(this.reason); items = style.applyTo(items); - if ((this.condition == null || items.size() <= 1) && StringUtils.hasLength(this.singular)) { + if ((this.condition == null || items == null || items.size() <= 1) + && StringUtils.hasLength(this.singular)) { message.append(" ").append(this.singular); } else if (StringUtils.hasLength(this.plural)) { @@ -400,22 +401,35 @@ else if (StringUtils.hasLength(this.plural)) { */ public enum Style { + /** + * Render with normal styling. + */ NORMAL { + @Override protected Object applyToItem(Object item) { return item; } + }, + /** + * Render with the item surrounded by quotes. + */ QUOTE { + @Override protected String applyToItem(Object item) { return (item != null) ? "'" + item + "'" : null; } + }; public Collection applyTo(Collection items) { - List result = new ArrayList<>(); + if (items == null) { + return null; + } + List result = new ArrayList<>(items.size()); for (Object item : items) { result.add(applyToItem(item)); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBean.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBean.java index 5985cce4ce0f..7d5ca163a162 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBean.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnExpression.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnExpression.java index bdef69f3b7d6..a8c4782a5b80 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnExpression.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnExpression.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,11 @@ /** * Configuration annotation for a conditional element that depends on the value of a SpEL * expression. + *

+ * Referencing a bean in the expression will cause that bean to be initialized very early + * in context refresh processing. As a result, the bean won't be eligible for + * post-processing (such as configuration properties binding) and its state may be + * incomplete. * * @author Dave Syer * @since 1.0.0 diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBean.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBean.java index e7acf5deae5c..c08e6200c7bb 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBean.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnProperty.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnProperty.java index aa7f5b8c6e79..a4b8f947b58a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnProperty.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnProperty.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -131,7 +131,7 @@ /** * Specify if the condition should match if the property is not set. Defaults to * {@code false}. - * @return if should match if the property is missing + * @return if the condition should match if the property is missing */ boolean matchIfMissing() default false; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnSingleCandidate.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnSingleCandidate.java index b077dcee2c3c..c1b79e7af14f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnSingleCandidate.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnSingleCandidate.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnWarDeployment.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnWarDeployment.java new file mode 100644 index 000000000000..7afa6cfaf766 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnWarDeployment.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.condition; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Conditional; + +/** + * {@link Conditional @Conditional} that matches when the application is a traditional WAR + * deployment. For applications with embedded servers, this condition will return false. + * + * @author Madhura Bhave + * @since 2.3.0 + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Conditional(OnWarDeploymentCondition.class) +public @interface ConditionalOnWarDeployment { + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/FilteringSpringBootCondition.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/FilteringSpringBootCondition.java index 7ea76c62aea4..f470245267c7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/FilteringSpringBootCondition.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/FilteringSpringBootCondition.java @@ -97,7 +97,7 @@ protected final List filter(Collection classNames, ClassNameFilt /** * Slightly faster variant of {@link ClassUtils#forName(String, ClassLoader)} that - * doesn't deal with primitives, arrays or innter types. + * doesn't deal with primitives, arrays or inner types. * @param className the class name to resolve * @param classLoader the class loader to use * @return a resolved class @@ -105,7 +105,7 @@ protected final List filter(Collection classNames, ClassNameFilt */ protected static Class resolve(String className, ClassLoader classLoader) throws ClassNotFoundException { if (classLoader != null) { - return classLoader.loadClass(className); + return Class.forName(className, false, classLoader); } return Class.forName(className); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/NoneNestedConditions.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/NoneNestedConditions.java index ac03c5aa66b7..43997cddb335 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/NoneNestedConditions.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/NoneNestedConditions.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ * be used to create composite conditions, for example: * *

- * static class OnNeitherJndiNorProperty extends NoneOfNestedConditions {
+ * static class OnNeitherJndiNorProperty extends NoneNestedConditions {
  *
  *    OnNeitherJndiNorProperty() {
  *        super(ConfigurationPhase.PARSE_CONFIGURATION);
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnBeanCondition.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnBeanCondition.java
index e2287e345537..ddc99827feac 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnBeanCondition.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnBeanCondition.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2019 the original author or authors.
+ * Copyright 2012-2022 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -24,12 +24,14 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 
+import org.springframework.aop.scope.ScopedProxyUtils;
 import org.springframework.beans.factory.BeanFactory;
 import org.springframework.beans.factory.HierarchicalBeanFactory;
 import org.springframework.beans.factory.ListableBeanFactory;
@@ -128,13 +130,25 @@ public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeM
 			if (!matchResult.isAllMatched()) {
 				return ConditionOutcome.noMatch(spec.message().didNotFind("any beans").atAll());
 			}
-			else if (!hasSingleAutowireCandidate(context.getBeanFactory(), matchResult.getNamesOfAllMatches(),
-					spec.getStrategy() == SearchStrategy.ALL)) {
-				return ConditionOutcome.noMatch(spec.message().didNotFind("a primary bean from beans")
-						.items(Style.QUOTE, matchResult.getNamesOfAllMatches()));
+			Set allBeans = matchResult.getNamesOfAllMatches();
+			if (allBeans.size() == 1) {
+				matchMessage = spec.message(matchMessage).found("a single bean").items(Style.QUOTE, allBeans);
+			}
+			else {
+				List primaryBeans = getPrimaryBeans(context.getBeanFactory(), allBeans,
+						spec.getStrategy() == SearchStrategy.ALL);
+				if (primaryBeans.isEmpty()) {
+					return ConditionOutcome.noMatch(
+							spec.message().didNotFind("a primary bean from beans").items(Style.QUOTE, allBeans));
+				}
+				if (primaryBeans.size() > 1) {
+					return ConditionOutcome
+							.noMatch(spec.message().found("multiple primary beans").items(Style.QUOTE, primaryBeans));
+				}
+				matchMessage = spec.message(matchMessage)
+						.found("a single primary bean '" + primaryBeans.get(0) + "' from beans")
+						.items(Style.QUOTE, allBeans);
 			}
-			matchMessage = spec.message(matchMessage).found("a primary bean from beans").items(Style.QUOTE,
-					matchResult.getNamesOfAllMatches());
 		}
 		if (metadata.isAnnotated(ConditionalOnMissingBean.class.getName())) {
 			Spec spec = new Spec<>(context, metadata, annotations,
@@ -166,7 +180,13 @@ protected final MatchResult getMatchingBeans(ConditionContext context, Spec s
 		for (String type : spec.getTypes()) {
 			Collection typeMatches = getBeanNamesForType(classLoader, considerHierarchy, beanFactory, type,
 					parameterizedContainers);
-			typeMatches.removeAll(beansIgnoredByType);
+			Iterator iterator = typeMatches.iterator();
+			while (iterator.hasNext()) {
+				String match = iterator.next();
+				if (beansIgnoredByType.contains(match) || ScopedProxyUtils.isScopedTarget(match)) {
+					iterator.remove();
+				}
+			}
 			if (typeMatches.isEmpty()) {
 				result.recordUnmatchedType(type);
 			}
@@ -333,11 +353,6 @@ private void appendMessageForMatches(StringBuilder reason, Map beanNames,
-			boolean considerHierarchy) {
-		return (beanNames.size() == 1 || getPrimaryBeans(beanFactory, beanNames, considerHierarchy).size() == 1);
-	}
-
 	private List getPrimaryBeans(ConfigurableListableBeanFactory beanFactory, Set beanNames,
 			boolean considerHierarchy) {
 		List primaryBeans = new ArrayList<>();
@@ -387,7 +402,7 @@ private static class Spec {
 
 		private final ClassLoader classLoader;
 
-		private final Class annotationType;
+		private final Class annotationType;
 
 		private final Set names;
 
@@ -581,11 +596,11 @@ Set> getParameterizedContainers() {
 		}
 
 		ConditionMessage.Builder message() {
-			return ConditionMessage.forCondition(ConditionalOnBean.class, this);
+			return ConditionMessage.forCondition(this.annotationType, this);
 		}
 
 		ConditionMessage.Builder message(ConditionMessage message) {
-			return message.andCondition(ConditionalOnBean.class, this);
+			return message.andCondition(this.annotationType, this);
 		}
 
 		@Override
@@ -733,7 +748,7 @@ Set getNamesOfAllMatches() {
 	}
 
 	/**
-	 * Exteption thrown when the bean type cannot be deduced.
+	 * Exception thrown when the bean type cannot be deduced.
 	 */
 	static final class BeanTypeDeductionException extends RuntimeException {
 
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnClassCondition.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnClassCondition.java
index 2f8f3a5e0788..d36ef76a0b9f 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnClassCondition.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnClassCondition.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2019 the original author or authors.
+ * Copyright 2012-2020 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -49,7 +49,7 @@ protected final ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses
 		// Split the work and perform half in a background thread if more than one
 		// processor is available. Using a single additional thread seems to offer the
 		// best performance. More threads make things worse.
-		if (Runtime.getRuntime().availableProcessors() > 1) {
+		if (autoConfigurationClasses.length > 1 && Runtime.getRuntime().availableProcessors() > 1) {
 			return resolveOutcomesThreaded(autoConfigurationClasses, autoConfigurationMetadata);
 		}
 		else {
@@ -164,7 +164,7 @@ public ConditionOutcome[] resolveOutcomes() {
 
 	}
 
-	private final class StandardOutcomesResolver implements OutcomesResolver {
+	private static final class StandardOutcomesResolver implements OutcomesResolver {
 
 		private final String[] autoConfigurationClasses;
 
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnResourceCondition.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnResourceCondition.java
index 41f0f83733d5..2d79a87c7777 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnResourceCondition.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnResourceCondition.java
@@ -24,7 +24,6 @@
 import org.springframework.context.annotation.ConditionContext;
 import org.springframework.core.Ordered;
 import org.springframework.core.annotation.Order;
-import org.springframework.core.io.DefaultResourceLoader;
 import org.springframework.core.io.ResourceLoader;
 import org.springframework.core.type.AnnotatedTypeMetadata;
 import org.springframework.util.Assert;
@@ -39,14 +38,11 @@
 @Order(Ordered.HIGHEST_PRECEDENCE + 20)
 class OnResourceCondition extends SpringBootCondition {
 
-	private final ResourceLoader defaultResourceLoader = new DefaultResourceLoader();
-
 	@Override
 	public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
 		MultiValueMap attributes = metadata
 				.getAllAnnotationAttributes(ConditionalOnResource.class.getName(), true);
-		ResourceLoader loader = (context.getResourceLoader() != null) ? context.getResourceLoader()
-				: this.defaultResourceLoader;
+		ResourceLoader loader = context.getResourceLoader();
 		List locations = new ArrayList<>();
 		collectValues(locations, attributes.get("resources"));
 		Assert.isTrue(!locations.isEmpty(),
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnWarDeploymentCondition.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnWarDeploymentCondition.java
new file mode 100644
index 000000000000..faa9949a1fbe
--- /dev/null
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnWarDeploymentCondition.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2012-2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.autoconfigure.condition;
+
+import javax.servlet.ServletContext;
+
+import org.springframework.context.annotation.Condition;
+import org.springframework.context.annotation.ConditionContext;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.core.type.AnnotatedTypeMetadata;
+import org.springframework.web.context.WebApplicationContext;
+
+/**
+ * {@link Condition} that checks if the application is running as a traditional war
+ * deployment.
+ *
+ * @author Madhura Bhave
+ */
+class OnWarDeploymentCondition extends SpringBootCondition {
+
+	@Override
+	public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
+		ResourceLoader resourceLoader = context.getResourceLoader();
+		if (resourceLoader instanceof WebApplicationContext) {
+			WebApplicationContext applicationContext = (WebApplicationContext) resourceLoader;
+			ServletContext servletContext = applicationContext.getServletContext();
+			if (servletContext != null) {
+				return ConditionOutcome.match("Application is deployed as a WAR file.");
+			}
+		}
+		return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnWarDeployment.class)
+				.because("the application is not deployed as a WAR file."));
+	}
+
+}
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/context/LifecycleAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/context/LifecycleAutoConfiguration.java
new file mode 100644
index 000000000000..70ade77b05bc
--- /dev/null
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/context/LifecycleAutoConfiguration.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2012-2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.autoconfigure.context;
+
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.SearchStrategy;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.support.AbstractApplicationContext;
+import org.springframework.context.support.DefaultLifecycleProcessor;
+
+/**
+ * {@link EnableAutoConfiguration Auto-configuration} relating to the application
+ * context's lifecycle.
+ *
+ * @author Andy Wilkinson
+ * @since 2.3.0
+ */
+@Configuration(proxyBeanMethods = false)
+@EnableConfigurationProperties(LifecycleProperties.class)
+public class LifecycleAutoConfiguration {
+
+	@Bean(name = AbstractApplicationContext.LIFECYCLE_PROCESSOR_BEAN_NAME)
+	@ConditionalOnMissingBean(name = AbstractApplicationContext.LIFECYCLE_PROCESSOR_BEAN_NAME,
+			search = SearchStrategy.CURRENT)
+	public DefaultLifecycleProcessor defaultLifecycleProcessor(LifecycleProperties properties) {
+		DefaultLifecycleProcessor lifecycleProcessor = new DefaultLifecycleProcessor();
+		lifecycleProcessor.setTimeoutPerShutdownPhase(properties.getTimeoutPerShutdownPhase().toMillis());
+		return lifecycleProcessor;
+	}
+
+}
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/context/LifecycleProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/context/LifecycleProperties.java
new file mode 100644
index 000000000000..49c706a8c223
--- /dev/null
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/context/LifecycleProperties.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2012-2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.autoconfigure.context;
+
+import java.time.Duration;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Configuration properties for lifecycle processing.
+ *
+ * @author Andy Wilkinson
+ * @since 2.3.0
+ */
+@ConfigurationProperties(prefix = "spring.lifecycle")
+public class LifecycleProperties {
+
+	/**
+	 * Timeout for the shutdown of any phase (group of SmartLifecycle beans with the same
+	 * 'phase' value).
+	 */
+	private Duration timeoutPerShutdownPhase = Duration.ofSeconds(30);
+
+	public Duration getTimeoutPerShutdownPhase() {
+		return this.timeoutPerShutdownPhase;
+	}
+
+	public void setTimeoutPerShutdownPhase(Duration timeoutPerShutdownPhase) {
+		this.timeoutPerShutdownPhase = timeoutPerShutdownPhase;
+	}
+
+}
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/ClusterEnvironmentBuilderCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/ClusterEnvironmentBuilderCustomizer.java
new file mode 100644
index 000000000000..aaf567ece89b
--- /dev/null
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/ClusterEnvironmentBuilderCustomizer.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2012-2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.autoconfigure.couchbase;
+
+import com.couchbase.client.java.env.ClusterEnvironment;
+
+/**
+ * Callback interface that can be implemented by beans wishing to customize the
+ * {@link ClusterEnvironment} via a
+ * {@link com.couchbase.client.java.env.ClusterEnvironment.Builder
+ * ClusterEnvironment.Builder} whilst retaining default auto-configuration.whilst
+ * retaining default auto-configuration.
+ *
+ * @author Stephane Nicoll
+ * @since 2.3.0
+ */
+@FunctionalInterface
+public interface ClusterEnvironmentBuilderCustomizer {
+
+	/**
+	 * Customize the {@link com.couchbase.client.java.env.ClusterEnvironment.Builder
+	 * ClusterEnvironment.Builder}.
+	 * @param builder the builder to customize
+	 */
+	void customize(ClusterEnvironment.Builder builder);
+
+}
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfiguration.java
index d99003a453d2..9500605a03c9 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfiguration.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfiguration.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2019 the original author or authors.
+ * Copyright 2012-2021 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,18 +16,38 @@
 
 package org.springframework.boot.autoconfigure.couchbase;
 
+import java.io.InputStream;
+import java.net.URL;
+import java.security.KeyStore;
+
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.TrustManagerFactory;
+
+import com.couchbase.client.core.env.IoConfig;
+import com.couchbase.client.core.env.SecurityConfig;
+import com.couchbase.client.core.env.TimeoutConfig;
 import com.couchbase.client.java.Cluster;
-import com.couchbase.client.java.CouchbaseBucket;
+import com.couchbase.client.java.ClusterOptions;
+import com.couchbase.client.java.codec.JacksonJsonSerializer;
+import com.couchbase.client.java.env.ClusterEnvironment;
+import com.couchbase.client.java.env.ClusterEnvironment.Builder;
+import com.couchbase.client.java.json.JsonValueModule;
+import com.fasterxml.jackson.databind.ObjectMapper;
 
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.AutoConfigureAfter;
 import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
-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.ConditionalOnSingleCandidate;
+import org.springframework.boot.autoconfigure.couchbase.CouchbaseProperties.Timeouts;
+import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.context.annotation.Conditional;
+import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Import;
+import org.springframework.core.Ordered;
+import org.springframework.util.ResourceUtils;
 
 /**
  * {@link EnableAutoConfiguration Auto-configuration} for Couchbase.
@@ -38,41 +58,100 @@
  * @since 1.4.0
  */
 @Configuration(proxyBeanMethods = false)
-@ConditionalOnClass({ CouchbaseBucket.class, Cluster.class })
-@Conditional(CouchbaseAutoConfiguration.CouchbaseCondition.class)
+@AutoConfigureAfter(JacksonAutoConfiguration.class)
+@ConditionalOnClass(Cluster.class)
+@ConditionalOnProperty("spring.couchbase.connection-string")
 @EnableConfigurationProperties(CouchbaseProperties.class)
 public class CouchbaseAutoConfiguration {
 
-	@Configuration(proxyBeanMethods = false)
-	@ConditionalOnMissingBean(value = CouchbaseConfiguration.class,
-			type = "org.springframework.data.couchbase.config.CouchbaseConfigurer")
-	@Import(CouchbaseConfiguration.class)
-	static class DefaultCouchbaseConfiguration {
+	@Bean
+	@ConditionalOnMissingBean
+	public ClusterEnvironment couchbaseClusterEnvironment(CouchbaseProperties properties,
+			ObjectProvider customizers) {
+		Builder builder = initializeEnvironmentBuilder(properties);
+		customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
+		return builder.build();
+	}
+
+	@Bean(destroyMethod = "disconnect")
+	@ConditionalOnMissingBean
+	public Cluster couchbaseCluster(CouchbaseProperties properties, ClusterEnvironment couchbaseClusterEnvironment) {
+		ClusterOptions options = ClusterOptions.clusterOptions(properties.getUsername(), properties.getPassword())
+				.environment(couchbaseClusterEnvironment);
+		return Cluster.connect(properties.getConnectionString(), options);
+	}
+
+	private ClusterEnvironment.Builder initializeEnvironmentBuilder(CouchbaseProperties properties) {
+		ClusterEnvironment.Builder builder = ClusterEnvironment.builder();
+		Timeouts timeouts = properties.getEnv().getTimeouts();
+		builder.timeoutConfig(TimeoutConfig.kvTimeout(timeouts.getKeyValue()).analyticsTimeout(timeouts.getAnalytics())
+				.kvDurableTimeout(timeouts.getKeyValueDurable()).queryTimeout(timeouts.getQuery())
+				.viewTimeout(timeouts.getView()).searchTimeout(timeouts.getSearch())
+				.managementTimeout(timeouts.getManagement()).connectTimeout(timeouts.getConnect())
+				.disconnectTimeout(timeouts.getDisconnect()));
+		CouchbaseProperties.Io io = properties.getEnv().getIo();
+		builder.ioConfig(IoConfig.maxHttpConnections(io.getMaxEndpoints()).numKvConnections(io.getMinEndpoints())
+				.idleHttpConnectionTimeout(io.getIdleHttpConnectionTimeout()));
+		if (properties.getEnv().getSsl().getEnabled()) {
+			builder.securityConfig(SecurityConfig.enableTls(true)
+					.trustManagerFactory(getTrustManagerFactory(properties.getEnv().getSsl())));
+		}
+		return builder;
+	}
+
+	private TrustManagerFactory getTrustManagerFactory(CouchbaseProperties.Ssl ssl) {
+		String resource = ssl.getKeyStore();
+		try {
+			TrustManagerFactory trustManagerFactory = TrustManagerFactory
+					.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+			KeyStore keyStore = loadKeyStore(resource, ssl.getKeyStorePassword());
+			trustManagerFactory.init(keyStore);
+			return trustManagerFactory;
+		}
+		catch (Exception ex) {
+			throw new IllegalStateException("Could not load Couchbase key store '" + resource + "'", ex);
+		}
+	}
 
+	private KeyStore loadKeyStore(String resource, String keyStorePassword) throws Exception {
+		KeyStore store = KeyStore.getInstance(KeyStore.getDefaultType());
+		URL url = ResourceUtils.getURL(resource);
+		try (InputStream stream = url.openStream()) {
+			store.load(stream, (keyStorePassword != null) ? keyStorePassword.toCharArray() : null);
+		}
+		return store;
 	}
 
-	/**
-	 * Determine if Couchbase should be configured. This happens if either the
-	 * user-configuration defines a {@code CouchbaseConfigurer} or if at least the
-	 * "bootstrapHosts" property is specified.
-	 * 

- * The reason why we check for the presence of {@code CouchbaseConfigurer} is that it - * might use {@link CouchbaseProperties} for its internal customization. - */ - static class CouchbaseCondition extends AnyNestedCondition { - - CouchbaseCondition() { - super(ConfigurationPhase.REGISTER_BEAN); + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(ObjectMapper.class) + static class JacksonConfiguration { + + @Bean + @ConditionalOnSingleCandidate(ObjectMapper.class) + ClusterEnvironmentBuilderCustomizer jacksonClusterEnvironmentBuilderCustomizer(ObjectMapper objectMapper) { + return new JacksonClusterEnvironmentBuilderCustomizer( + objectMapper.copy().registerModule(new JsonValueModule())); } - @Conditional(OnBootstrapHostsCondition.class) - static class BootstrapHostsProperty { + } + + private static final class JacksonClusterEnvironmentBuilderCustomizer + implements ClusterEnvironmentBuilderCustomizer, Ordered { + private final ObjectMapper objectMapper; + + private JacksonClusterEnvironmentBuilderCustomizer(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; } - @ConditionalOnBean(type = "org.springframework.data.couchbase.config.CouchbaseConfigurer") - static class CouchbaseConfigurerAvailable { + @Override + public void customize(Builder builder) { + builder.jsonSerializer(JacksonJsonSerializer.create(this.objectMapper)); + } + @Override + public int getOrder() { + return 0; } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseConfiguration.java deleted file mode 100644 index dca518496463..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseConfiguration.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.couchbase; - -import java.util.List; - -import com.couchbase.client.core.env.KeyValueServiceConfig; -import com.couchbase.client.core.env.QueryServiceConfig; -import com.couchbase.client.core.env.ViewServiceConfig; -import com.couchbase.client.java.Bucket; -import com.couchbase.client.java.Cluster; -import com.couchbase.client.java.CouchbaseCluster; -import com.couchbase.client.java.cluster.ClusterInfo; -import com.couchbase.client.java.env.DefaultCouchbaseEnvironment; - -import org.springframework.boot.autoconfigure.couchbase.CouchbaseProperties.Endpoints; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.DependsOn; -import org.springframework.context.annotation.Primary; - -/** - * Support class to configure Couchbase based on {@link CouchbaseProperties}. - * - * @author Stephane Nicoll - * @since 2.1.0 - */ -@Configuration -public class CouchbaseConfiguration { - - private final CouchbaseProperties properties; - - public CouchbaseConfiguration(CouchbaseProperties properties) { - this.properties = properties; - } - - @Bean - @Primary - public DefaultCouchbaseEnvironment couchbaseEnvironment() { - return initializeEnvironmentBuilder(this.properties).build(); - } - - @Bean - @Primary - public Cluster couchbaseCluster() { - CouchbaseCluster couchbaseCluster = CouchbaseCluster.create(couchbaseEnvironment(), determineBootstrapHosts()); - if (isRoleBasedAccessControlEnabled()) { - return couchbaseCluster.authenticate(this.properties.getUsername(), this.properties.getPassword()); - } - return couchbaseCluster; - } - - /** - * Determine the Couchbase nodes to bootstrap from. - * @return the Couchbase nodes to bootstrap from - */ - protected List determineBootstrapHosts() { - return this.properties.getBootstrapHosts(); - } - - @Bean - @Primary - @DependsOn("couchbaseClient") - public ClusterInfo couchbaseClusterInfo() { - return couchbaseCluster() - .clusterManager(this.properties.getBucket().getName(), this.properties.getBucket().getPassword()) - .info(); - } - - @Bean - @Primary - public Bucket couchbaseClient() { - if (isRoleBasedAccessControlEnabled()) { - return couchbaseCluster().openBucket(this.properties.getBucket().getName()); - } - return couchbaseCluster().openBucket(this.properties.getBucket().getName(), - this.properties.getBucket().getPassword()); - } - - private boolean isRoleBasedAccessControlEnabled() { - return this.properties.getUsername() != null && this.properties.getPassword() != null; - } - - /** - * Initialize an environment builder based on the specified settings. - * @param properties the couchbase properties to use - * @return the {@link DefaultCouchbaseEnvironment} builder. - */ - protected DefaultCouchbaseEnvironment.Builder initializeEnvironmentBuilder(CouchbaseProperties properties) { - CouchbaseProperties.Endpoints endpoints = properties.getEnv().getEndpoints(); - CouchbaseProperties.Timeouts timeouts = properties.getEnv().getTimeouts(); - DefaultCouchbaseEnvironment.Builder builder = DefaultCouchbaseEnvironment.builder(); - if (timeouts.getConnect() != null) { - builder = builder.connectTimeout(timeouts.getConnect().toMillis()); - } - builder = builder.keyValueServiceConfig(KeyValueServiceConfig.create(endpoints.getKeyValue())); - if (timeouts.getKeyValue() != null) { - builder = builder.kvTimeout(timeouts.getKeyValue().toMillis()); - } - if (timeouts.getQuery() != null) { - builder = builder.queryTimeout(timeouts.getQuery().toMillis()); - builder = builder.queryServiceConfig(getQueryServiceConfig(endpoints)); - builder = builder.viewServiceConfig(getViewServiceConfig(endpoints)); - } - if (timeouts.getSocketConnect() != null) { - builder = builder.socketConnectTimeout((int) timeouts.getSocketConnect().toMillis()); - } - if (timeouts.getView() != null) { - builder = builder.viewTimeout(timeouts.getView().toMillis()); - } - CouchbaseProperties.Ssl ssl = properties.getEnv().getSsl(); - if (ssl.getEnabled()) { - builder = builder.sslEnabled(true); - if (ssl.getKeyStore() != null) { - builder = builder.sslKeystoreFile(ssl.getKeyStore()); - } - if (ssl.getKeyStorePassword() != null) { - builder = builder.sslKeystorePassword(ssl.getKeyStorePassword()); - } - } - return builder; - } - - private QueryServiceConfig getQueryServiceConfig(Endpoints endpoints) { - return QueryServiceConfig.create(endpoints.getQueryservice().getMinEndpoints(), - endpoints.getQueryservice().getMaxEndpoints()); - } - - private ViewServiceConfig getViewServiceConfig(Endpoints endpoints) { - return ViewServiceConfig.create(endpoints.getViewservice().getMinEndpoints(), - endpoints.getViewservice().getMaxEndpoints()); - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseProperties.java index e7080ec5af25..4dc0e252ffbc 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package org.springframework.boot.autoconfigure.couchbase; import java.time.Duration; -import java.util.List; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.util.StringUtils; @@ -28,36 +27,36 @@ * @author Eddú Meléndez * @author Stephane Nicoll * @author Yulin Qin + * @author Brian Clozel + * @author Michael Nitschinger * @since 1.4.0 */ @ConfigurationProperties(prefix = "spring.couchbase") public class CouchbaseProperties { /** - * Couchbase nodes (host or IP address) to bootstrap from. + * Connection string used to locate the Couchbase cluster. */ - private List bootstrapHosts; + private String connectionString; /** - * Cluster username when using role based access. + * Cluster username. */ private String username; /** - * Cluster password when using role based access. + * Cluster password. */ private String password; - private final Bucket bucket = new Bucket(); - private final Env env = new Env(); - public List getBootstrapHosts() { - return this.bootstrapHosts; + public String getConnectionString() { + return this.connectionString; } - public void setBootstrapHosts(List bootstrapHosts) { - this.bootstrapHosts = bootstrapHosts; + public void setConnectionString(String connectionString) { + this.connectionString = connectionString; } public String getUsername() { @@ -76,54 +75,20 @@ public void setPassword(String password) { this.password = password; } - public Bucket getBucket() { - return this.bucket; - } - public Env getEnv() { return this.env; } - public static class Bucket { - - /** - * Name of the bucket to connect to. - */ - private String name = "default"; - - /** - * Password of the bucket. - */ - private String password = ""; - - public String getName() { - return this.name; - } - - public void setName(String name) { - this.name = name; - } - - public String getPassword() { - return this.password; - } - - public void setPassword(String password) { - this.password = password; - } - - } - public static class Env { - private final Endpoints endpoints = new Endpoints(); + private final Io io = new Io(); private final Ssl ssl = new Ssl(); private final Timeouts timeouts = new Timeouts(); - public Endpoints getEndpoints() { - return this.endpoints; + public Io getIo() { + return this.io; } public Ssl getSsl() { @@ -136,67 +101,46 @@ public Timeouts getTimeouts() { } - public static class Endpoints { + public static class Io { /** - * Number of sockets per node against the key/value service. + * Minimum number of sockets per node. */ - private int keyValue = 1; + private int minEndpoints = 1; /** - * Query (N1QL) service configuration. + * Maximum number of sockets per node. */ - private final CouchbaseService queryservice = new CouchbaseService(); + private int maxEndpoints = 12; /** - * View service configuration. + * Length of time an HTTP connection may remain idle before it is closed and + * removed from the pool. */ - private final CouchbaseService viewservice = new CouchbaseService(); + private Duration idleHttpConnectionTimeout = Duration.ofMillis(4500); - public int getKeyValue() { - return this.keyValue; + public int getMinEndpoints() { + return this.minEndpoints; } - public void setKeyValue(int keyValue) { - this.keyValue = keyValue; + public void setMinEndpoints(int minEndpoints) { + this.minEndpoints = minEndpoints; } - public CouchbaseService getQueryservice() { - return this.queryservice; + public int getMaxEndpoints() { + return this.maxEndpoints; } - public CouchbaseService getViewservice() { - return this.viewservice; + public void setMaxEndpoints(int maxEndpoints) { + this.maxEndpoints = maxEndpoints; } - public static class CouchbaseService { - - /** - * Minimum number of sockets per node. - */ - private int minEndpoints = 1; - - /** - * Maximum number of sockets per node. - */ - private int maxEndpoints = 1; - - public int getMinEndpoints() { - return this.minEndpoints; - } - - public void setMinEndpoints(int minEndpoints) { - this.minEndpoints = minEndpoints; - } - - public int getMaxEndpoints() { - return this.maxEndpoints; - } - - public void setMaxEndpoints(int maxEndpoints) { - this.maxEndpoints = maxEndpoints; - } + public Duration getIdleHttpConnectionTimeout() { + return this.idleHttpConnectionTimeout; + } + public void setIdleHttpConnectionTimeout(Duration idleHttpConnectionTimeout) { + this.idleHttpConnectionTimeout = idleHttpConnectionTimeout; } } @@ -248,29 +192,49 @@ public void setKeyStorePassword(String keyStorePassword) { public static class Timeouts { /** - * Bucket connections timeouts. + * Bucket connect timeout. + */ + private Duration connect = Duration.ofSeconds(10); + + /** + * Bucket disconnect timeout. */ - private Duration connect = Duration.ofMillis(5000); + private Duration disconnect = Duration.ofSeconds(10); /** - * Blocking operations performed on a specific key timeout. + * Timeout for operations on a specific key-value. */ private Duration keyValue = Duration.ofMillis(2500); /** - * N1QL query operations timeout. + * Timeout for operations on a specific key-value with a durability level. */ - private Duration query = Duration.ofMillis(7500); + private Duration keyValueDurable = Duration.ofSeconds(10); /** - * Socket connect connections timeout. + * N1QL query operations timeout. */ - private Duration socketConnect = Duration.ofMillis(1000); + private Duration query = Duration.ofSeconds(75); /** * Regular and geospatial view operations timeout. */ - private Duration view = Duration.ofMillis(7500); + private Duration view = Duration.ofSeconds(75); + + /** + * Timeout for the search service. + */ + private Duration search = Duration.ofSeconds(75); + + /** + * Timeout for the analytics service. + */ + private Duration analytics = Duration.ofSeconds(75); + + /** + * Timeout for the management operations. + */ + private Duration management = Duration.ofSeconds(75); public Duration getConnect() { return this.connect; @@ -280,6 +244,14 @@ public void setConnect(Duration connect) { this.connect = connect; } + public Duration getDisconnect() { + return this.disconnect; + } + + public void setDisconnect(Duration disconnect) { + this.disconnect = disconnect; + } + public Duration getKeyValue() { return this.keyValue; } @@ -288,20 +260,20 @@ public void setKeyValue(Duration keyValue) { this.keyValue = keyValue; } - public Duration getQuery() { - return this.query; + public Duration getKeyValueDurable() { + return this.keyValueDurable; } - public void setQuery(Duration query) { - this.query = query; + public void setKeyValueDurable(Duration keyValueDurable) { + this.keyValueDurable = keyValueDurable; } - public Duration getSocketConnect() { - return this.socketConnect; + public Duration getQuery() { + return this.query; } - public void setSocketConnect(Duration socketConnect) { - this.socketConnect = socketConnect; + public void setQuery(Duration query) { + this.query = query; } public Duration getView() { @@ -312,6 +284,30 @@ public void setView(Duration view) { this.view = view; } + public Duration getSearch() { + return this.search; + } + + public void setSearch(Duration search) { + this.search = search; + } + + public Duration getAnalytics() { + return this.analytics; + } + + public void setAnalytics(Duration analytics) { + this.analytics = analytics; + } + + public Duration getManagement() { + return this.management; + } + + public void setManagement(Duration management) { + this.management = management; + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/OnBootstrapHostsCondition.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/OnBootstrapHostsCondition.java deleted file mode 100644 index 98898ede88e9..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/OnBootstrapHostsCondition.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.couchbase; - -import org.springframework.boot.autoconfigure.condition.ConditionMessage; -import org.springframework.boot.autoconfigure.condition.OnPropertyListCondition; - -/** - * Condition to determine if {@code spring.couchbase.bootstrap-hosts} is specified. - * - * @author Stephane Nicoll - * @author Madhura Bhave - * @author Eneias Silva - */ -class OnBootstrapHostsCondition extends OnPropertyListCondition { - - OnBootstrapHostsCondition() { - super("spring.couchbase.bootstrap-hosts", () -> ConditionMessage.forCondition("Couchbase Bootstrap Hosts")); - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/AbstractRepositoryConfigurationSourceSupport.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/AbstractRepositoryConfigurationSourceSupport.java index 412c926580e5..c46621f56b15 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/AbstractRepositoryConfigurationSourceSupport.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/AbstractRepositoryConfigurationSourceSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -62,6 +62,11 @@ public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, B delegate.registerRepositoriesIn(registry, getRepositoryConfigurationExtension()); } + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + registerBeanDefinitions(importingClassMetadata, registry, null); + } + private AnnotationRepositoryConfigurationSource getConfigurationSource(BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) { AnnotationMetadata metadata = AnnotationMetadata.introspect(getConfiguration()); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraDataAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraDataAutoConfiguration.java index 309da94409c2..80d1a4da5750 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraDataAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraDataAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,28 +19,25 @@ import java.util.Collections; import java.util.List; -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.Session; +import com.datastax.oss.driver.api.core.CqlSession; import org.springframework.beans.factory.BeanFactory; import org.springframework.boot.autoconfigure.AutoConfigurationPackages; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; -import org.springframework.boot.autoconfigure.cassandra.CassandraProperties; 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.domain.EntityScanPackages; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; +import org.springframework.data.cassandra.SessionFactory; import org.springframework.data.cassandra.config.CassandraEntityClassScanner; -import org.springframework.data.cassandra.config.CassandraSessionFactoryBean; import org.springframework.data.cassandra.config.SchemaAction; +import org.springframework.data.cassandra.config.SessionFactoryFactoryBean; import org.springframework.data.cassandra.core.CassandraAdminOperations; import org.springframework.data.cassandra.core.CassandraOperations; import org.springframework.data.cassandra.core.CassandraTemplate; @@ -60,19 +57,15 @@ * @since 1.3.0 */ @Configuration(proxyBeanMethods = false) -@ConditionalOnClass({ Cluster.class, CassandraAdminOperations.class }) -@ConditionalOnBean(Cluster.class) -@EnableConfigurationProperties(CassandraProperties.class) +@ConditionalOnClass({ CqlSession.class, CassandraAdminOperations.class }) +@ConditionalOnBean(CqlSession.class) @AutoConfigureAfter(CassandraAutoConfiguration.class) public class CassandraDataAutoConfiguration { - private final CassandraProperties properties; + private final CqlSession session; - private final Cluster cluster; - - public CassandraDataAutoConfiguration(CassandraProperties properties, Cluster cluster) { - this.properties = properties; - this.cluster = cluster; + public CassandraDataAutoConfiguration(CqlSession session) { + this.session = session; } @Bean @@ -87,32 +80,27 @@ public CassandraMappingContext cassandraMapping(BeanFactory beanFactory, Cassand if (!packages.isEmpty()) { context.setInitialEntitySet(CassandraEntityClassScanner.scan(packages)); } - PropertyMapper.get().from(this.properties::getKeyspaceName).whenHasText().as(this::createSimpleUserTypeResolver) - .to(context::setUserTypeResolver); - context.setCustomConversions(conversions); + context.setSimpleTypeHolder(conversions.getSimpleTypeHolder()); return context; } - private SimpleUserTypeResolver createSimpleUserTypeResolver(String keyspaceName) { - return new SimpleUserTypeResolver(this.cluster, keyspaceName); - } - @Bean @ConditionalOnMissingBean public CassandraConverter cassandraConverter(CassandraMappingContext mapping, CassandraCustomConversions conversions) { MappingCassandraConverter converter = new MappingCassandraConverter(mapping); + converter.setCodecRegistry(this.session.getContext().getCodecRegistry()); converter.setCustomConversions(conversions); + converter.setUserTypeResolver(new SimpleUserTypeResolver(this.session)); return converter; } @Bean - @ConditionalOnMissingBean(Session.class) - public CassandraSessionFactoryBean cassandraSession(Environment environment, CassandraConverter converter) { - CassandraSessionFactoryBean session = new CassandraSessionFactoryBean(); - session.setCluster(this.cluster); + @ConditionalOnMissingBean(SessionFactory.class) + public SessionFactoryFactoryBean cassandraSessionFactory(Environment environment, CassandraConverter converter) { + SessionFactoryFactoryBean session = new SessionFactoryFactoryBean(); + session.setSession(this.session); session.setConverter(converter); - session.setKeyspaceName(this.properties.getKeyspaceName()); Binder binder = Binder.get(environment); binder.bind("spring.data.cassandra.schema-action", SchemaAction.class).ifBound(session::setSchemaAction); return session; @@ -120,8 +108,8 @@ public CassandraSessionFactoryBean cassandraSession(Environment environment, Cas @Bean @ConditionalOnMissingBean(CassandraOperations.class) - public CassandraTemplate cassandraTemplate(Session session, CassandraConverter converter) { - return new CassandraTemplate(session, converter); + public CassandraTemplate cassandraTemplate(SessionFactory sessionFactory, CassandraConverter converter) { + return new CassandraTemplate(sessionFactory, converter); } @Bean diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraReactiveDataAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraReactiveDataAutoConfiguration.java index 2f0cd835894c..f5c866fe3942 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraReactiveDataAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraReactiveDataAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,7 @@ package org.springframework.boot.autoconfigure.data.cassandra; -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.Session; +import com.datastax.oss.driver.api.core.CqlSession; import reactor.core.publisher.Flux; import org.springframework.boot.autoconfigure.AutoConfigureAfter; @@ -44,18 +43,19 @@ * @since 2.0.0 */ @Configuration(proxyBeanMethods = false) -@ConditionalOnClass({ Cluster.class, ReactiveCassandraTemplate.class, Flux.class }) -@ConditionalOnBean(Session.class) +@ConditionalOnClass({ CqlSession.class, ReactiveCassandraTemplate.class, Flux.class }) +@ConditionalOnBean(CqlSession.class) @AutoConfigureAfter(CassandraDataAutoConfiguration.class) public class CassandraReactiveDataAutoConfiguration { @Bean @ConditionalOnMissingBean - public ReactiveSession reactiveCassandraSession(Session session) { + public ReactiveSession reactiveCassandraSession(CqlSession session) { return new DefaultBridgedReactiveSession(session); } @Bean + @ConditionalOnMissingBean public ReactiveSessionFactory reactiveCassandraSessionFactory(ReactiveSession reactiveCassandraSession) { return new DefaultReactiveSessionFactory(reactiveCassandraSession); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraRepositoriesAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraRepositoriesAutoConfiguration.java index 0f717f77ddd5..9c357505d8d3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraRepositoriesAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraRepositoriesAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ package org.springframework.boot.autoconfigure.data.cassandra; -import com.datastax.driver.core.Session; +import com.datastax.oss.driver.api.core.CqlSession; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -34,11 +34,11 @@ * Repositories. * * @author Eddú Meléndez - * @see EnableCassandraRepositories * @since 1.3.0 + * @see EnableCassandraRepositories */ @Configuration(proxyBeanMethods = false) -@ConditionalOnClass({ Session.class, CassandraRepository.class }) +@ConditionalOnClass({ CqlSession.class, CassandraRepository.class }) @ConditionalOnRepositoryType(store = "cassandra", type = RepositoryType.IMPERATIVE) @ConditionalOnMissingBean(CassandraRepositoryFactoryBean.class) @Import(CassandraRepositoriesRegistrar.class) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseClientFactoryConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseClientFactoryConfiguration.java new file mode 100644 index 000000000000..f6b7aceb8bb0 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseClientFactoryConfiguration.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.couchbase; + +import com.couchbase.client.java.Cluster; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.SimpleCouchbaseClientFactory; + +/** + * Configuration for a {@link CouchbaseClientFactory}. + * + * @author Stephane Nicoll + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnSingleCandidate(Cluster.class) +@ConditionalOnProperty("spring.data.couchbase.bucket-name") +class CouchbaseClientFactoryConfiguration { + + @Bean + @ConditionalOnMissingBean + CouchbaseClientFactory couchbaseClientFactory(Cluster cluster, CouchbaseDataProperties properties) { + return new SimpleCouchbaseClientFactory(cluster, properties.getBucketName(), properties.getScopeName()); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseClientFactoryDependentConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseClientFactoryDependentConfiguration.java new file mode 100644 index 000000000000..a886437946e4 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseClientFactoryDependentConfiguration.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.couchbase; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.config.BeanNames; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter; +import org.springframework.data.couchbase.repository.config.RepositoryOperationsMapping; + +/** + * Configuration for Couchbase-related beans that depend on a + * {@link CouchbaseClientFactory}. + * + * @author Stephane Nicoll + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnSingleCandidate(CouchbaseClientFactory.class) +class CouchbaseClientFactoryDependentConfiguration { + + @Bean(name = BeanNames.COUCHBASE_TEMPLATE) + @ConditionalOnMissingBean(name = BeanNames.COUCHBASE_TEMPLATE) + CouchbaseTemplate couchbaseTemplate(CouchbaseClientFactory couchbaseClientFactory, + MappingCouchbaseConverter mappingCouchbaseConverter) { + return new CouchbaseTemplate(couchbaseClientFactory, mappingCouchbaseConverter); + } + + @Bean(name = BeanNames.COUCHBASE_OPERATIONS_MAPPING) + @ConditionalOnMissingBean(name = BeanNames.COUCHBASE_OPERATIONS_MAPPING) + RepositoryOperationsMapping couchbaseRepositoryOperationsMapping(CouchbaseTemplate couchbaseTemplate) { + return new RepositoryOperationsMapping(couchbaseTemplate); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseConfigurerAdapterConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseConfigurerAdapterConfiguration.java deleted file mode 100644 index 643ec451c725..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseConfigurerAdapterConfiguration.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.data.couchbase; - -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.couchbase.CouchbaseConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.couchbase.config.CouchbaseConfigurer; - -/** - * Adapt the core Couchbase configuration to an expected {@link CouchbaseConfigurer} if - * necessary. - * - * @author Stephane Nicoll - */ -@Configuration(proxyBeanMethods = false) -@ConditionalOnClass(CouchbaseConfigurer.class) -@ConditionalOnBean(CouchbaseConfiguration.class) -class CouchbaseConfigurerAdapterConfiguration { - - private final CouchbaseConfiguration configuration; - - CouchbaseConfigurerAdapterConfiguration(CouchbaseConfiguration configuration) { - this.configuration = configuration; - } - - @Bean - @ConditionalOnMissingBean - CouchbaseConfigurer springBootCouchbaseConfigurer() throws Exception { - return new SpringBootCouchbaseConfigurer(this.configuration.couchbaseEnvironment(), - this.configuration.couchbaseCluster(), this.configuration.couchbaseClusterInfo(), - this.configuration.couchbaseClient()); - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseDataAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseDataAutoConfiguration.java index 940417d0d5ad..48a4f76c47c2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseDataAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseDataAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,7 +44,8 @@ @ConditionalOnClass({ Bucket.class, CouchbaseRepository.class }) @AutoConfigureAfter({ CouchbaseAutoConfiguration.class, ValidationAutoConfiguration.class }) @EnableConfigurationProperties(CouchbaseDataProperties.class) -@Import({ CouchbaseConfigurerAdapterConfiguration.class, SpringBootCouchbaseDataConfiguration.class }) +@Import({ CouchbaseDataConfiguration.class, CouchbaseClientFactoryConfiguration.class, + CouchbaseClientFactoryDependentConfiguration.class }) public class CouchbaseDataAutoConfiguration { @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseDataConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseDataConfiguration.java new file mode 100644 index 000000000000..b9f55e4f46f0 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseDataConfiguration.java @@ -0,0 +1,85 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.couchbase; + +import java.util.Collections; + +import org.springframework.beans.BeanUtils; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.domain.EntityScanner; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.annotation.Persistent; +import org.springframework.data.couchbase.config.BeanNames; +import org.springframework.data.couchbase.core.convert.CouchbaseCustomConversions; +import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter; +import org.springframework.data.couchbase.core.convert.translation.JacksonTranslationService; +import org.springframework.data.couchbase.core.convert.translation.TranslationService; +import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext; +import org.springframework.data.couchbase.core.mapping.Document; +import org.springframework.data.mapping.model.FieldNamingStrategy; + +/** + * Configuration for Spring Data's couchbase support. + * + * @author Stephane Nicoll + */ +@Configuration(proxyBeanMethods = false) +class CouchbaseDataConfiguration { + + @Bean + @ConditionalOnMissingBean + MappingCouchbaseConverter couchbaseMappingConverter(CouchbaseDataProperties properties, + CouchbaseMappingContext couchbaseMappingContext, CouchbaseCustomConversions couchbaseCustomConversions) { + MappingCouchbaseConverter converter = new MappingCouchbaseConverter(couchbaseMappingContext, + properties.getTypeKey()); + converter.setCustomConversions(couchbaseCustomConversions); + return converter; + } + + @Bean + @ConditionalOnMissingBean + TranslationService couchbaseTranslationService() { + return new JacksonTranslationService(); + } + + @Bean(name = BeanNames.COUCHBASE_MAPPING_CONTEXT) + @ConditionalOnMissingBean(name = BeanNames.COUCHBASE_MAPPING_CONTEXT) + CouchbaseMappingContext couchbaseMappingContext(CouchbaseDataProperties properties, + ApplicationContext applicationContext, CouchbaseCustomConversions couchbaseCustomConversions) + throws Exception { + CouchbaseMappingContext mappingContext = new CouchbaseMappingContext(); + mappingContext + .setInitialEntitySet(new EntityScanner(applicationContext).scan(Document.class, Persistent.class)); + mappingContext.setSimpleTypeHolder(couchbaseCustomConversions.getSimpleTypeHolder()); + Class fieldNamingStrategy = properties.getFieldNamingStrategy(); + if (fieldNamingStrategy != null) { + mappingContext + .setFieldNamingStrategy((FieldNamingStrategy) BeanUtils.instantiateClass(fieldNamingStrategy)); + } + mappingContext.setAutoIndexCreation(properties.isAutoIndex()); + return mappingContext; + } + + @Bean(name = BeanNames.COUCHBASE_CUSTOM_CONVERSIONS) + @ConditionalOnMissingBean(name = BeanNames.COUCHBASE_CUSTOM_CONVERSIONS) + CouchbaseCustomConversions couchbaseCustomConversions() { + return new CouchbaseCustomConversions(Collections.emptyList()); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseDataProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseDataProperties.java index 044357082893..1230e41af722 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseDataProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseDataProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package org.springframework.boot.autoconfigure.data.couchbase; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.data.couchbase.core.query.Consistency; /** * Configuration properties for Spring Data Couchbase. @@ -35,9 +34,25 @@ public class CouchbaseDataProperties { private boolean autoIndex; /** - * Consistency to apply by default on generated queries. + * Name of the bucket to connect to. */ - private Consistency consistency = Consistency.READ_YOUR_OWN_WRITES; + private String bucketName; + + /** + * Name of the scope used for all collection access. + */ + private String scopeName; + + /** + * Fully qualified name of the FieldNamingStrategy to use. + */ + private Class fieldNamingStrategy; + + /** + * Name of the field that stores the type information for complex types when using + * "MappingCouchbaseConverter". + */ + private String typeKey = "_class"; public boolean isAutoIndex() { return this.autoIndex; @@ -47,12 +62,36 @@ public void setAutoIndex(boolean autoIndex) { this.autoIndex = autoIndex; } - public Consistency getConsistency() { - return this.consistency; + public String getBucketName() { + return this.bucketName; + } + + public void setBucketName(String bucketName) { + this.bucketName = bucketName; + } + + public String getScopeName() { + return this.scopeName; + } + + public void setScopeName(String scopeName) { + this.scopeName = scopeName; + } + + public Class getFieldNamingStrategy() { + return this.fieldNamingStrategy; + } + + public void setFieldNamingStrategy(Class fieldNamingStrategy) { + this.fieldNamingStrategy = fieldNamingStrategy; + } + + public String getTypeKey() { + return this.typeKey; } - public void setConsistency(Consistency consistency) { - this.consistency = consistency; + public void setTypeKey(String typeKey) { + this.typeKey = typeKey; } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseReactiveDataAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseReactiveDataAutoConfiguration.java index cf29d0fdbe41..104a9419647c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseReactiveDataAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseReactiveDataAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ package org.springframework.boot.autoconfigure.data.couchbase; -import com.couchbase.client.java.Bucket; +import com.couchbase.client.java.Cluster; import reactor.core.publisher.Flux; import org.springframework.boot.autoconfigure.AutoConfigureAfter; @@ -34,9 +34,9 @@ * @since 2.0.0 */ @Configuration(proxyBeanMethods = false) -@ConditionalOnClass({ Bucket.class, ReactiveCouchbaseRepository.class, Flux.class }) +@ConditionalOnClass({ Cluster.class, ReactiveCouchbaseRepository.class, Flux.class }) @AutoConfigureAfter(CouchbaseDataAutoConfiguration.class) -@Import(SpringBootCouchbaseReactiveDataConfiguration.class) +@Import(CouchbaseReactiveDataConfiguration.class) public class CouchbaseReactiveDataAutoConfiguration { } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseReactiveDataConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseReactiveDataConfiguration.java new file mode 100644 index 000000000000..8450cef5f3d9 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseReactiveDataConfiguration.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.couchbase; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.config.BeanNames; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter; +import org.springframework.data.couchbase.repository.config.ReactiveRepositoryOperationsMapping; + +/** + * Configuration for Spring Data's couchbase reactive support. + * + * @author Stephane Nicoll + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnSingleCandidate(CouchbaseClientFactory.class) +class CouchbaseReactiveDataConfiguration { + + @Bean(name = BeanNames.REACTIVE_COUCHBASE_TEMPLATE) + @ConditionalOnMissingBean(name = BeanNames.REACTIVE_COUCHBASE_TEMPLATE) + ReactiveCouchbaseTemplate reactiveCouchbaseTemplate(CouchbaseClientFactory couchbaseClientFactory, + MappingCouchbaseConverter mappingCouchbaseConverter) { + return new ReactiveCouchbaseTemplate(couchbaseClientFactory, mappingCouchbaseConverter); + } + + @Bean(name = BeanNames.REACTIVE_COUCHBASE_OPERATIONS_MAPPING) + @ConditionalOnMissingBean(name = BeanNames.REACTIVE_COUCHBASE_OPERATIONS_MAPPING) + ReactiveRepositoryOperationsMapping reactiveCouchbaseRepositoryOperationsMapping( + ReactiveCouchbaseTemplate reactiveCouchbaseTemplate) { + return new ReactiveRepositoryOperationsMapping(reactiveCouchbaseTemplate); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseReactiveRepositoriesAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseReactiveRepositoriesAutoConfiguration.java index a9a52957c19d..c120b9b6b841 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseReactiveRepositoriesAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseReactiveRepositoriesAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ package org.springframework.boot.autoconfigure.data.couchbase; -import com.couchbase.client.java.Bucket; +import com.couchbase.client.java.Cluster; import reactor.core.publisher.Flux; import org.springframework.boot.autoconfigure.AutoConfigureAfter; @@ -40,7 +40,7 @@ * @since 2.0.0 */ @Configuration(proxyBeanMethods = false) -@ConditionalOnClass({ Bucket.class, ReactiveCouchbaseRepository.class, Flux.class }) +@ConditionalOnClass({ Cluster.class, ReactiveCouchbaseRepository.class, Flux.class }) @ConditionalOnRepositoryType(store = "couchbase", type = RepositoryType.REACTIVE) @ConditionalOnBean(ReactiveRepositoryOperationsMapping.class) @ConditionalOnMissingBean(ReactiveCouchbaseRepositoryFactoryBean.class) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/SpringBootCouchbaseConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/SpringBootCouchbaseConfigurer.java deleted file mode 100644 index c481162de190..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/SpringBootCouchbaseConfigurer.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.data.couchbase; - -import com.couchbase.client.java.Bucket; -import com.couchbase.client.java.Cluster; -import com.couchbase.client.java.cluster.ClusterInfo; -import com.couchbase.client.java.env.CouchbaseEnvironment; - -import org.springframework.data.couchbase.config.CouchbaseConfigurer; - -/** - * A simple {@link CouchbaseConfigurer} implementation. - * - * @author Stephane Nicoll - * @since 1.4.0 - */ -public class SpringBootCouchbaseConfigurer implements CouchbaseConfigurer { - - private final CouchbaseEnvironment env; - - private final Cluster cluster; - - private final ClusterInfo clusterInfo; - - private final Bucket bucket; - - public SpringBootCouchbaseConfigurer(CouchbaseEnvironment env, Cluster cluster, ClusterInfo clusterInfo, - Bucket bucket) { - this.env = env; - this.cluster = cluster; - this.clusterInfo = clusterInfo; - this.bucket = bucket; - } - - @Override - public CouchbaseEnvironment couchbaseEnvironment() throws Exception { - return this.env; - } - - @Override - public Cluster couchbaseCluster() throws Exception { - return this.cluster; - } - - @Override - public ClusterInfo couchbaseClusterInfo() throws Exception { - return this.clusterInfo; - } - - @Override - public Bucket couchbaseClient() throws Exception { - return this.bucket; - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/SpringBootCouchbaseDataConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/SpringBootCouchbaseDataConfiguration.java deleted file mode 100644 index 1c1b1294255b..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/SpringBootCouchbaseDataConfiguration.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.data.couchbase; - -import java.util.Set; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.domain.EntityScanner; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.annotation.Persistent; -import org.springframework.data.convert.CustomConversions; -import org.springframework.data.couchbase.config.AbstractCouchbaseDataConfiguration; -import org.springframework.data.couchbase.config.BeanNames; -import org.springframework.data.couchbase.config.CouchbaseConfigurer; -import org.springframework.data.couchbase.core.CouchbaseTemplate; -import org.springframework.data.couchbase.core.mapping.Document; -import org.springframework.data.couchbase.core.query.Consistency; -import org.springframework.data.couchbase.repository.support.IndexManager; - -/** - * Configure Spring Data's couchbase support. - * - * @author Stephane Nicoll - */ -@Configuration -@ConditionalOnMissingBean(AbstractCouchbaseDataConfiguration.class) -@ConditionalOnBean(CouchbaseConfigurer.class) -class SpringBootCouchbaseDataConfiguration extends AbstractCouchbaseDataConfiguration { - - private final ApplicationContext applicationContext; - - private final CouchbaseDataProperties properties; - - private final CouchbaseConfigurer couchbaseConfigurer; - - SpringBootCouchbaseDataConfiguration(ApplicationContext applicationContext, CouchbaseDataProperties properties, - ObjectProvider couchbaseConfigurer) { - this.applicationContext = applicationContext; - this.properties = properties; - this.couchbaseConfigurer = couchbaseConfigurer.getIfAvailable(); - } - - @Override - protected CouchbaseConfigurer couchbaseConfigurer() { - return this.couchbaseConfigurer; - } - - @Override - protected Consistency getDefaultConsistency() { - return this.properties.getConsistency(); - } - - @Override - protected Set> getInitialEntitySet() throws ClassNotFoundException { - return new EntityScanner(this.applicationContext).scan(Document.class, Persistent.class); - } - - @Override - @ConditionalOnMissingBean(name = BeanNames.COUCHBASE_TEMPLATE) - @Bean(name = BeanNames.COUCHBASE_TEMPLATE) - public CouchbaseTemplate couchbaseTemplate() throws Exception { - return super.couchbaseTemplate(); - } - - @Override - @ConditionalOnMissingBean(name = BeanNames.COUCHBASE_CUSTOM_CONVERSIONS) - @Bean(name = BeanNames.COUCHBASE_CUSTOM_CONVERSIONS) - public CustomConversions customConversions() { - return super.customConversions(); - } - - @Override - @ConditionalOnMissingBean(name = BeanNames.COUCHBASE_INDEX_MANAGER) - @Bean(name = BeanNames.COUCHBASE_INDEX_MANAGER) - public IndexManager indexManager() { - if (this.properties.isAutoIndex()) { - return new IndexManager(true, true, true); - } - return new IndexManager(false, false, false); - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/SpringBootCouchbaseReactiveDataConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/SpringBootCouchbaseReactiveDataConfiguration.java deleted file mode 100644 index c763d3a3f22a..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/SpringBootCouchbaseReactiveDataConfiguration.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.data.couchbase; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.couchbase.config.AbstractReactiveCouchbaseDataConfiguration; -import org.springframework.data.couchbase.config.BeanNames; -import org.springframework.data.couchbase.config.CouchbaseConfigurer; -import org.springframework.data.couchbase.core.RxJavaCouchbaseTemplate; -import org.springframework.data.couchbase.core.query.Consistency; -import org.springframework.data.couchbase.repository.config.ReactiveRepositoryOperationsMapping; - -/** - * Configure Spring Data's reactive couchbase support. - * - * @author Alex Derkach - */ -@Configuration -@ConditionalOnMissingBean(AbstractReactiveCouchbaseDataConfiguration.class) -@ConditionalOnBean(CouchbaseConfigurer.class) -class SpringBootCouchbaseReactiveDataConfiguration extends AbstractReactiveCouchbaseDataConfiguration { - - private final CouchbaseDataProperties properties; - - private final CouchbaseConfigurer couchbaseConfigurer; - - SpringBootCouchbaseReactiveDataConfiguration(CouchbaseDataProperties properties, - CouchbaseConfigurer couchbaseConfigurer) { - this.properties = properties; - this.couchbaseConfigurer = couchbaseConfigurer; - } - - @Override - protected CouchbaseConfigurer couchbaseConfigurer() { - return this.couchbaseConfigurer; - } - - @Override - protected Consistency getDefaultConsistency() { - return this.properties.getConsistency(); - } - - @Override - @ConditionalOnMissingBean(name = BeanNames.RXJAVA1_COUCHBASE_TEMPLATE) - @Bean(name = BeanNames.RXJAVA1_COUCHBASE_TEMPLATE) - public RxJavaCouchbaseTemplate reactiveCouchbaseTemplate() throws Exception { - return super.reactiveCouchbaseTemplate(); - } - - @Override - @ConditionalOnMissingBean(name = BeanNames.REACTIVE_COUCHBASE_OPERATIONS_MAPPING) - @Bean(name = BeanNames.REACTIVE_COUCHBASE_OPERATIONS_MAPPING) - public ReactiveRepositoryOperationsMapping reactiveRepositoryOperationsMapping( - RxJavaCouchbaseTemplate reactiveCouchbaseTemplate) throws Exception { - return super.reactiveRepositoryOperationsMapping(reactiveCouchbaseTemplate); - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchAutoConfiguration.java deleted file mode 100644 index 74736f9f1c40..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchAutoConfiguration.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.data.elasticsearch; - -import java.util.Properties; - -import org.elasticsearch.client.Client; -import org.elasticsearch.client.transport.TransportClient; - -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.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.elasticsearch.client.TransportClientFactoryBean; - -/** - * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration - * Auto-configuration} for Elasticsearch. - * - * @author Artur Konczak - * @author Mohsin Husen - * @author Andy Wilkinson - * @since 1.1.0 - */ -@Configuration(proxyBeanMethods = false) -@ConditionalOnClass({ Client.class, TransportClientFactoryBean.class }) -@ConditionalOnProperty(prefix = "spring.data.elasticsearch", name = "cluster-nodes", matchIfMissing = false) -@EnableConfigurationProperties(ElasticsearchProperties.class) -public class ElasticsearchAutoConfiguration { - - private final ElasticsearchProperties properties; - - public ElasticsearchAutoConfiguration(ElasticsearchProperties properties) { - this.properties = properties; - } - - @Bean - @ConditionalOnMissingBean - public TransportClient elasticsearchClient() throws Exception { - TransportClientFactoryBean factory = new TransportClientFactoryBean(); - factory.setClusterNodes(this.properties.getClusterNodes()); - factory.setProperties(createProperties()); - factory.afterPropertiesSet(); - return factory.getObject(); - } - - private Properties createProperties() { - Properties properties = new Properties(); - properties.put("cluster.name", this.properties.getClusterName()); - properties.putAll(this.properties.getProperties()); - return properties; - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchDataAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchDataAutoConfiguration.java index 018da3540829..09d36949c872 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchDataAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchDataAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,33 +19,29 @@ import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.elasticsearch.rest.RestClientAutoConfiguration; +import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import org.springframework.data.elasticsearch.core.ElasticsearchTemplate; +import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate; import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories; import org.springframework.data.elasticsearch.repository.config.EnableReactiveElasticsearchRepositories; /** * {@link EnableAutoConfiguration Auto-configuration} for Spring Data's Elasticsearch * support. - *

- * Registers an {@link ElasticsearchTemplate} if no other bean of the same type and the - * same name {@code "elasticsearchTemplate"} is configured. * * @author Brian Clozel * @author Artur Konczak * @author Mohsin Husen + * @since 1.1.0 * @see EnableElasticsearchRepositories * @see EnableReactiveElasticsearchRepositories - * @since 1.1.0 */ @Configuration(proxyBeanMethods = false) -@ConditionalOnClass({ ElasticsearchTemplate.class }) -@AutoConfigureAfter({ ElasticsearchAutoConfiguration.class, RestClientAutoConfiguration.class, - ReactiveRestClientAutoConfiguration.class }) +@ConditionalOnClass({ ElasticsearchRestTemplate.class }) +@AutoConfigureAfter({ ElasticsearchRestClientAutoConfiguration.class, + ReactiveElasticsearchRestClientAutoConfiguration.class }) @Import({ ElasticsearchDataConfiguration.BaseConfiguration.class, - ElasticsearchDataConfiguration.TransportClientConfiguration.class, ElasticsearchDataConfiguration.RestClientConfiguration.class, ElasticsearchDataConfiguration.ReactiveRestClientConfiguration.class }) public class ElasticsearchDataAutoConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchDataConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchDataConfiguration.java index 91f2e8411187..4a8ee1b3ffed 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchDataConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchDataConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,27 +16,26 @@ package org.springframework.boot.autoconfigure.data.elasticsearch; +import java.util.Collections; + import org.elasticsearch.action.support.IndicesOptions; -import org.elasticsearch.action.support.WriteRequest; -import org.elasticsearch.client.Client; import org.elasticsearch.client.RestHighLevelClient; 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.domain.EntityScanner; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient; -import org.springframework.data.elasticsearch.core.DefaultEntityMapper; -import org.springframework.data.elasticsearch.core.DefaultResultMapper; import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate; -import org.springframework.data.elasticsearch.core.ElasticsearchTemplate; -import org.springframework.data.elasticsearch.core.EntityMapper; import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; import org.springframework.data.elasticsearch.core.ReactiveElasticsearchTemplate; -import org.springframework.data.elasticsearch.core.ResultsMapper; import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; +import org.springframework.data.elasticsearch.core.convert.ElasticsearchCustomConversions; import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter; import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; import org.springframework.web.reactive.function.client.WebClient; @@ -48,6 +47,8 @@ * their order of execution. * * @author Brian Clozel + * @author Scott Frederick + * @author Stephane Nicoll */ abstract class ElasticsearchDataConfiguration { @@ -56,26 +57,27 @@ static class BaseConfiguration { @Bean @ConditionalOnMissingBean - ElasticsearchConverter elasticsearchConverter(SimpleElasticsearchMappingContext mappingContext) { - return new MappingElasticsearchConverter(mappingContext); + ElasticsearchCustomConversions elasticsearchCustomConversions() { + return new ElasticsearchCustomConversions(Collections.emptyList()); } @Bean @ConditionalOnMissingBean - SimpleElasticsearchMappingContext mappingContext() { - return new SimpleElasticsearchMappingContext(); + SimpleElasticsearchMappingContext mappingContext(ApplicationContext applicationContext, + ElasticsearchCustomConversions elasticsearchCustomConversions) throws ClassNotFoundException { + SimpleElasticsearchMappingContext mappingContext = new SimpleElasticsearchMappingContext(); + mappingContext.setInitialEntitySet(new EntityScanner(applicationContext).scan(Document.class)); + mappingContext.setSimpleTypeHolder(elasticsearchCustomConversions.getSimpleTypeHolder()); + return mappingContext; } @Bean @ConditionalOnMissingBean - EntityMapper entityMapper(SimpleElasticsearchMappingContext mappingContext) { - return new DefaultEntityMapper(mappingContext); - } - - @Bean - @ConditionalOnMissingBean - ResultsMapper resultsMapper(SimpleElasticsearchMappingContext mappingContext, EntityMapper entityMapper) { - return new DefaultResultMapper(mappingContext, entityMapper); + ElasticsearchConverter elasticsearchConverter(SimpleElasticsearchMappingContext mappingContext, + ElasticsearchCustomConversions elasticsearchCustomConversions) { + MappingElasticsearchConverter converter = new MappingElasticsearchConverter(mappingContext); + converter.setConversions(elasticsearchCustomConversions); + return converter; } } @@ -87,28 +89,8 @@ static class RestClientConfiguration { @Bean @ConditionalOnMissingBean(value = ElasticsearchOperations.class, name = "elasticsearchTemplate") @ConditionalOnBean(RestHighLevelClient.class) - ElasticsearchRestTemplate elasticsearchTemplate(RestHighLevelClient client, ElasticsearchConverter converter, - ResultsMapper resultsMapper) { - return new ElasticsearchRestTemplate(client, converter, resultsMapper); - } - - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(Client.class) - static class TransportClientConfiguration { - - @Bean - @ConditionalOnMissingBean(value = ElasticsearchOperations.class, name = "elasticsearchTemplate") - @ConditionalOnBean(Client.class) - ElasticsearchTemplate elasticsearchTemplate(Client client, ElasticsearchConverter converter, - ResultsMapper resultsMapper) { - try { - return new ElasticsearchTemplate(client, converter, resultsMapper); - } - catch (Exception ex) { - throw new IllegalStateException(ex); - } + ElasticsearchRestTemplate elasticsearchTemplate(RestHighLevelClient client, ElasticsearchConverter converter) { + return new ElasticsearchRestTemplate(client, converter); } } @@ -121,11 +103,9 @@ static class ReactiveRestClientConfiguration { @ConditionalOnMissingBean(value = ReactiveElasticsearchOperations.class, name = "reactiveElasticsearchTemplate") @ConditionalOnBean(ReactiveElasticsearchClient.class) ReactiveElasticsearchTemplate reactiveElasticsearchTemplate(ReactiveElasticsearchClient client, - ElasticsearchConverter converter, ResultsMapper resultsMapper) { - ReactiveElasticsearchTemplate template = new ReactiveElasticsearchTemplate(client, converter, - resultsMapper); + ElasticsearchConverter converter) { + ReactiveElasticsearchTemplate template = new ReactiveElasticsearchTemplate(client, converter); template.setIndicesOptions(IndicesOptions.strictExpandOpenAndForbidClosed()); - template.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); return template; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchProperties.java deleted file mode 100644 index 562ee0d6b182..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchProperties.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.data.elasticsearch; - -import java.util.HashMap; -import java.util.Map; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * Configuration properties for Elasticsearch. - * - * @author Artur Konczak - * @author Mohsin Husen - * @since 1.1.0 - */ -@ConfigurationProperties(prefix = "spring.data.elasticsearch") -public class ElasticsearchProperties { - - /** - * Elasticsearch cluster name. - */ - private String clusterName = "elasticsearch"; - - /** - * Comma-separated list of cluster node addresses. - */ - private String clusterNodes; - - /** - * Additional properties used to configure the client. - */ - private Map properties = new HashMap<>(); - - public String getClusterName() { - return this.clusterName; - } - - public void setClusterName(String clusterName) { - this.clusterName = clusterName; - } - - public String getClusterNodes() { - return this.clusterNodes; - } - - public void setClusterNodes(String clusterNodes) { - this.clusterNodes = clusterNodes; - } - - public Map getProperties() { - return this.properties; - } - - public void setProperties(Map properties) { - this.properties = properties; - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchRepositoriesAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchRepositoriesAutoConfiguration.java index 709bc2e7d791..c7f16934a7dc 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchRepositoriesAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchRepositoriesAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,8 +34,8 @@ * * @author Artur Konczak * @author Mohsin Husen - * @see EnableElasticsearchRepositories * @since 1.1.0 + * @see EnableElasticsearchRepositories */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ Client.class, ElasticsearchRepository.class }) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRepositoriesAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRepositoriesAutoConfiguration.java index 657b833214ca..3823d2de1e16 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRepositoriesAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRepositoriesAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,8 +32,8 @@ * Reactive Repositories. * * @author Brian Clozel - * @see EnableReactiveElasticsearchRepositories * @since 2.2.0 + * @see EnableReactiveElasticsearchRepositories */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ ReactiveElasticsearchClient.class, ReactiveElasticsearchRepository.class }) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientAutoConfiguration.java new file mode 100644 index 000000000000..b690d5d4ba0e --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientAutoConfiguration.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.elasticsearch; + +import reactor.netty.http.client.HttpClient; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.elasticsearch.client.ClientConfiguration; +import org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient; +import org.springframework.data.elasticsearch.client.reactive.ReactiveRestClients; +import org.springframework.util.unit.DataSize; +import org.springframework.web.reactive.function.client.ExchangeStrategies; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Elasticsearch Reactive REST + * clients. + * + * @author Brian Clozel + * @since 2.2.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass({ ReactiveRestClients.class, WebClient.class, HttpClient.class }) +@EnableConfigurationProperties(ReactiveElasticsearchRestClientProperties.class) +public class ReactiveElasticsearchRestClientAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public ClientConfiguration clientConfiguration(ReactiveElasticsearchRestClientProperties properties) { + ClientConfiguration.MaybeSecureClientConfigurationBuilder builder = ClientConfiguration.builder() + .connectedTo(properties.getEndpoints().toArray(new String[0])); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(properties.isUseSsl()).whenTrue().toCall(builder::usingSsl); + map.from(properties.getUsername()).whenHasText() + .to((username) -> builder.withBasicAuth(username, properties.getPassword())); + map.from(properties.getConnectionTimeout()).to(builder::withConnectTimeout); + map.from(properties.getSocketTimeout()).to(builder::withSocketTimeout); + configureExchangeStrategies(map, builder, properties); + return builder.build(); + } + + private void configureExchangeStrategies(PropertyMapper map, + ClientConfiguration.TerminalClientConfigurationBuilder builder, + ReactiveElasticsearchRestClientProperties properties) { + map.from(properties.getMaxInMemorySize()).asInt(DataSize::toBytes).to((maxInMemorySize) -> { + builder.withWebClientConfigurer((webClient) -> { + ExchangeStrategies exchangeStrategies = ExchangeStrategies.builder() + .codecs((configurer) -> configurer.defaultCodecs().maxInMemorySize(maxInMemorySize)).build(); + return webClient.mutate().exchangeStrategies(exchangeStrategies).build(); + }); + }); + } + + @Bean + @ConditionalOnMissingBean + public ReactiveElasticsearchClient reactiveElasticsearchClient(ClientConfiguration clientConfiguration) { + return ReactiveRestClients.create(clientConfiguration); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientProperties.java new file mode 100644 index 000000000000..a4a6de83c119 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientProperties.java @@ -0,0 +1,128 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.elasticsearch; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.util.unit.DataSize; + +/** + * Configuration properties for Elasticsearch Reactive REST clients. + * + * @author Brian Clozel + * @since 2.2.0 + */ +@ConfigurationProperties(prefix = "spring.data.elasticsearch.client.reactive") +public class ReactiveElasticsearchRestClientProperties { + + /** + * Comma-separated list of the Elasticsearch endpoints to connect to. + */ + private List endpoints = new ArrayList<>(Collections.singletonList("localhost:9200")); + + /** + * Whether the client should use SSL to connect to the endpoints. + */ + private boolean useSsl = false; + + /** + * Credentials username. + */ + private String username; + + /** + * Credentials password. + */ + private String password; + + /** + * Connection timeout. + */ + private Duration connectionTimeout; + + /** + * Read and Write Socket timeout. + */ + private Duration socketTimeout; + + /** + * Limit on the number of bytes that can be buffered whenever the input stream needs + * to be aggregated. + */ + private DataSize maxInMemorySize; + + public List getEndpoints() { + return this.endpoints; + } + + public void setEndpoints(List endpoints) { + this.endpoints = endpoints; + } + + public boolean isUseSsl() { + return this.useSsl; + } + + public void setUseSsl(boolean useSsl) { + this.useSsl = useSsl; + } + + public String getUsername() { + return this.username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return this.password; + } + + public void setPassword(String password) { + this.password = password; + } + + public Duration getConnectionTimeout() { + return this.connectionTimeout; + } + + public void setConnectionTimeout(Duration connectionTimeout) { + this.connectionTimeout = connectionTimeout; + } + + public Duration getSocketTimeout() { + return this.socketTimeout; + } + + public void setSocketTimeout(Duration socketTimeout) { + this.socketTimeout = socketTimeout; + } + + public DataSize getMaxInMemorySize() { + return this.maxInMemorySize; + } + + public void setMaxInMemorySize(DataSize maxInMemorySize) { + this.maxInMemorySize = maxInMemorySize; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveRestClientAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveRestClientAutoConfiguration.java deleted file mode 100644 index 8f2ff327be63..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveRestClientAutoConfiguration.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.data.elasticsearch; - -import reactor.netty.http.client.HttpClient; - -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.context.properties.PropertyMapper; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.elasticsearch.client.ClientConfiguration; -import org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient; -import org.springframework.data.elasticsearch.client.reactive.ReactiveRestClients; -import org.springframework.http.HttpHeaders; -import org.springframework.web.reactive.function.client.WebClient; - -/** - * {@link EnableAutoConfiguration Auto-configuration} for Elasticsearch Reactive REST - * clients. - * - * @author Brian Clozel - * @since 2.2.0 - */ -@Configuration(proxyBeanMethods = false) -@ConditionalOnClass({ ReactiveRestClients.class, WebClient.class, HttpClient.class }) -@EnableConfigurationProperties(ReactiveRestClientProperties.class) -public class ReactiveRestClientAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - public ClientConfiguration clientConfiguration(ReactiveRestClientProperties properties) { - ClientConfiguration.MaybeSecureClientConfigurationBuilder builder = ClientConfiguration.builder() - .connectedTo(properties.getEndpoints().toArray(new String[0])); - if (properties.isUseSsl()) { - builder.usingSsl(); - } - configureTimeouts(builder, properties); - return builder.build(); - } - - private void configureTimeouts(ClientConfiguration.TerminalClientConfigurationBuilder builder, - ReactiveRestClientProperties properties) { - PropertyMapper map = PropertyMapper.get(); - map.from(properties.getConnectionTimeout()).whenNonNull().to(builder::withConnectTimeout); - map.from(properties.getSocketTimeout()).whenNonNull().to(builder::withSocketTimeout); - map.from(properties.getUsername()).whenHasText().to((username) -> { - HttpHeaders headers = new HttpHeaders(); - headers.setBasicAuth(username, properties.getPassword()); - builder.withDefaultHeaders(headers); - }); - } - - @Bean - @ConditionalOnMissingBean - public ReactiveElasticsearchClient reactiveElasticsearchClient(ClientConfiguration clientConfiguration) { - return ReactiveRestClients.create(clientConfiguration); - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveRestClientProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveRestClientProperties.java deleted file mode 100644 index 56d80b10c527..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveRestClientProperties.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.data.elasticsearch; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * Configuration properties for Elasticsearch Reactive REST clients. - * - * @author Brian Clozel - * @since 2.2.0 - */ -@ConfigurationProperties(prefix = "spring.data.elasticsearch.client.reactive") -public class ReactiveRestClientProperties { - - /** - * Comma-separated list of the Elasticsearch endpoints to connect to. - */ - private List endpoints = new ArrayList<>(Collections.singletonList("localhost:9200")); - - /** - * Whether the client should use SSL to connect to the endpoints. - */ - private boolean useSsl = false; - - /** - * Credentials username. - */ - private String username; - - /** - * Credentials password. - */ - private String password; - - /** - * Connection timeout. - */ - private Duration connectionTimeout; - - /** - * Read and Write Socket timeout. - */ - private Duration socketTimeout; - - public List getEndpoints() { - return this.endpoints; - } - - public void setEndpoints(List endpoints) { - this.endpoints = endpoints; - } - - public boolean isUseSsl() { - return this.useSsl; - } - - public void setUseSsl(boolean useSsl) { - this.useSsl = useSsl; - } - - public String getUsername() { - return this.username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getPassword() { - return this.password; - } - - public void setPassword(String password) { - this.password = password; - } - - public Duration getConnectionTimeout() { - return this.connectionTimeout; - } - - public void setConnectionTimeout(Duration connectionTimeout) { - this.connectionTimeout = connectionTimeout; - } - - public Duration getSocketTimeout() { - return this.socketTimeout; - } - - public void setSocketTimeout(Duration socketTimeout) { - this.socketTimeout = socketTimeout; - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/jdbc/JdbcRepositoriesAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/jdbc/JdbcRepositoriesAutoConfiguration.java index dbf2358af5ab..7f506b3338f7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/jdbc/JdbcRepositoriesAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/jdbc/JdbcRepositoriesAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,6 @@ import org.springframework.context.annotation.Import; import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration; import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories; -import org.springframework.data.jdbc.repository.config.JdbcConfiguration; import org.springframework.data.jdbc.repository.config.JdbcRepositoryConfigExtension; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.transaction.PlatformTransactionManager; @@ -45,7 +44,6 @@ * @since 2.1.0 * @see EnableJdbcRepositories */ -@SuppressWarnings("deprecation") @Configuration(proxyBeanMethods = false) @ConditionalOnBean({ NamedParameterJdbcOperations.class, PlatformTransactionManager.class }) @ConditionalOnClass({ NamedParameterJdbcOperations.class, AbstractJdbcConfiguration.class }) @@ -61,8 +59,8 @@ static class JdbcRepositoriesConfiguration { } - @Configuration - @ConditionalOnMissingBean({ AbstractJdbcConfiguration.class, JdbcConfiguration.class }) + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(AbstractJdbcConfiguration.class) static class SpringBootJdbcConfiguration extends AbstractJdbcConfiguration { } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/jpa/EntityManagerFactoryDependsOnPostProcessor.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/jpa/EntityManagerFactoryDependsOnPostProcessor.java index 19a8c159ee62..9a3c3e98595b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/jpa/EntityManagerFactoryDependsOnPostProcessor.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/jpa/EntityManagerFactoryDependsOnPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,8 +20,6 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; -import org.springframework.boot.autoconfigure.AbstractDependsOnBeanFactoryPostProcessor; -import org.springframework.orm.jpa.AbstractEntityManagerFactoryBean; /** * {@link BeanFactoryPostProcessor} that can be used to dynamically declare that all @@ -34,8 +32,12 @@ * @author Andrii Hrytsiuk * @since 1.1.0 * @see BeanDefinition#setDependsOn(String[]) + * @deprecated since 2.5.0 for removal in 2.7.0 in favor of + * {@link org.springframework.boot.autoconfigure.orm.jpa.EntityManagerFactoryDependsOnPostProcessor} */ -public class EntityManagerFactoryDependsOnPostProcessor extends AbstractDependsOnBeanFactoryPostProcessor { +@Deprecated +public class EntityManagerFactoryDependsOnPostProcessor + extends org.springframework.boot.autoconfigure.orm.jpa.EntityManagerFactoryDependsOnPostProcessor { /** * Creates a new {@code EntityManagerFactoryDependsOnPostProcessor} that will set up @@ -43,7 +45,7 @@ public class EntityManagerFactoryDependsOnPostProcessor extends AbstractDependsO * @param dependsOn names of the beans to depend upon */ public EntityManagerFactoryDependsOnPostProcessor(String... dependsOn) { - super(EntityManagerFactory.class, AbstractEntityManagerFactoryBean.class, dependsOn); + super(dependsOn); } /** @@ -53,7 +55,7 @@ public EntityManagerFactoryDependsOnPostProcessor(String... dependsOn) { * @since 2.1.8 */ public EntityManagerFactoryDependsOnPostProcessor(Class... dependsOn) { - super(EntityManagerFactory.class, AbstractEntityManagerFactoryBean.class, dependsOn); + super(dependsOn); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/jpa/JpaRepositoriesAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/jpa/JpaRepositoriesAutoConfiguration.java index f5f345bfa048..be992aeee03d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/jpa/JpaRepositoriesAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/jpa/JpaRepositoriesAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,10 +44,8 @@ * {@link EnableAutoConfiguration Auto-configuration} for Spring Data's JPA Repositories. *

* Activates when there is a bean of type {@link javax.sql.DataSource} configured in the - * context, the Spring Data JPA - * {@link org.springframework.data.jpa.repository.JpaRepository} type is on the classpath, - * and there is no other, existing - * {@link org.springframework.data.jpa.repository.JpaRepository} configured. + * context, the Spring Data JPA {@link JpaRepository} type is on the classpath, and there + * is no other, existing {@link JpaRepository} configured. *

* Once in effect, the auto-configuration is the equivalent of enabling JPA repositories * using the {@link EnableJpaRepositories @EnableJpaRepositories} annotation. @@ -56,6 +54,7 @@ * * @author Phillip Webb * @author Josh Long + * @author Scott Frederick * @since 1.0.0 * @see EnableJpaRepositories */ @@ -95,13 +94,12 @@ private static final class BootstrapExecutorCondition extends AnyNestedCondition } @ConditionalOnProperty(prefix = "spring.data.jpa.repositories", name = "bootstrap-mode", - havingValue = "deferred", matchIfMissing = false) + havingValue = "deferred") static class DeferredBootstrapMode { } - @ConditionalOnProperty(prefix = "spring.data.jpa.repositories", name = "bootstrap-mode", havingValue = "lazy", - matchIfMissing = false) + @ConditionalOnProperty(prefix = "spring.data.jpa.repositories", name = "bootstrap-mode", havingValue = "lazy") static class LazyBootstrapMode { } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/jpa/JpaRepositoriesRegistrar.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/jpa/JpaRepositoriesRegistrar.java index ddc5919ec6bf..32f6a1628d54 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/jpa/JpaRepositoriesRegistrar.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/jpa/JpaRepositoriesRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,7 @@ * * @author Phillip Webb * @author Dave Syer + * @author Scott Frederick */ class JpaRepositoriesRegistrar extends AbstractRepositoryConfigurationSourceSupport { @@ -56,7 +57,7 @@ protected RepositoryConfigurationExtension getRepositoryConfigurationExtension() @Override protected BootstrapMode getBootstrapMode() { - return (this.bootstrapMode == null) ? super.getBootstrapMode() : this.bootstrapMode; + return (this.bootstrapMode == null) ? BootstrapMode.DEFAULT : this.bootstrapMode; } @Override diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoClientDependsOnBeanFactoryPostProcessor.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoClientDependsOnBeanFactoryPostProcessor.java index 1abe21203252..86b5674ef907 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoClientDependsOnBeanFactoryPostProcessor.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoClientDependsOnBeanFactoryPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ package org.springframework.boot.autoconfigure.data.mongo; -import com.mongodb.MongoClient; +import com.mongodb.client.MongoClient; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; @@ -36,18 +36,6 @@ @Order(Ordered.LOWEST_PRECEDENCE) public class MongoClientDependsOnBeanFactoryPostProcessor extends AbstractDependsOnBeanFactoryPostProcessor { - /** - * Creates a new {@code MongoClientDependsOnBeanFactoryPostProcessor} that will set up - * dependencies upon beans with the given names. - * @param dependsOn names of the beans to depend upon - * @deprecated since 2.1.7 in favor of - * {@link #MongoClientDependsOnBeanFactoryPostProcessor(Class...)} - */ - @Deprecated - public MongoClientDependsOnBeanFactoryPostProcessor(String... dependsOn) { - super(MongoClient.class, MongoClientFactoryBean.class, dependsOn); - } - /** * Creates a new {@code MongoClientDependsOnBeanFactoryPostProcessor} that will set up * dependencies upon beans with the given types. diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoDataAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoDataAutoConfiguration.java index 358a3f33e25b..4d5cc03a7124 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoDataAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoDataAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ package org.springframework.boot.autoconfigure.data.mongo; -import com.mongodb.MongoClient; +import com.mongodb.client.MongoClient; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -48,9 +48,10 @@ * @since 1.1.0 */ @Configuration(proxyBeanMethods = false) -@ConditionalOnClass({ MongoClient.class, com.mongodb.client.MongoClient.class, MongoTemplate.class }) +@ConditionalOnClass({ MongoClient.class, MongoTemplate.class }) @EnableConfigurationProperties(MongoProperties.class) -@Import({ MongoDataConfiguration.class, MongoDbFactoryConfiguration.class, MongoDbFactoryDependentConfiguration.class }) +@Import({ MongoDataConfiguration.class, MongoDatabaseFactoryConfiguration.class, + MongoDatabaseFactoryDependentConfiguration.class }) @AutoConfigureAfter(MongoAutoConfiguration.class) public class MongoDataAutoConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoDataConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoDataConfiguration.java index 204e5a9fdec9..95debed4ef50 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoDataConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoDataConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.autoconfigure.data.mongo; import java.util.Collections; @@ -25,7 +26,6 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.annotation.Persistent; import org.springframework.data.mapping.model.FieldNamingStrategy; import org.springframework.data.mongodb.core.convert.MongoCustomConversions; import org.springframework.data.mongodb.core.mapping.Document; @@ -36,6 +36,7 @@ * * @author Madhura Bhave * @author Artsiom Yudovin + * @author Scott Fredericks */ @Configuration(proxyBeanMethods = false) class MongoDataConfiguration { @@ -47,7 +48,7 @@ MongoMappingContext mongoMappingContext(ApplicationContext applicationContext, M PropertyMapper mapper = PropertyMapper.get().alwaysApplyingWhenNonNull(); MongoMappingContext context = new MongoMappingContext(); mapper.from(properties.isAutoIndexCreation()).to(context::setAutoIndexCreation); - context.setInitialEntitySet(new EntityScanner(applicationContext).scan(Document.class, Persistent.class)); + context.setInitialEntitySet(new EntityScanner(applicationContext).scan(Document.class)); Class strategyClass = properties.getFieldNamingStrategy(); if (strategyClass != null) { context.setFieldNamingStrategy((FieldNamingStrategy) BeanUtils.instantiateClass(strategyClass)); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoDatabaseFactoryConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoDatabaseFactoryConfiguration.java new file mode 100644 index 000000000000..544bfcc17f6a --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoDatabaseFactoryConfiguration.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.mongo; + +import com.mongodb.client.MongoClient; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.boot.autoconfigure.mongo.MongoProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.MongoDatabaseFactory; +import org.springframework.data.mongodb.core.MongoDatabaseFactorySupport; +import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory; + +/** + * Configuration for a {@link MongoDatabaseFactory}. + * + * @author Andy Wilkinson + * @author Stephane Nicoll + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnMissingBean(MongoDatabaseFactory.class) +@ConditionalOnSingleCandidate(MongoClient.class) +class MongoDatabaseFactoryConfiguration { + + @Bean + MongoDatabaseFactorySupport mongoDatabaseFactory(MongoClient mongoClient, MongoProperties properties) { + return new SimpleMongoClientDatabaseFactory(mongoClient, properties.getMongoClientDatabase()); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoDatabaseFactoryDependentConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoDatabaseFactoryDependentConfiguration.java new file mode 100644 index 000000000000..9639b743b3af --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoDatabaseFactoryDependentConfiguration.java @@ -0,0 +1,131 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.mongo; + +import com.mongodb.ClientSessionOptions; +import com.mongodb.client.ClientSession; +import com.mongodb.client.MongoDatabase; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.mongo.MongoProperties; +import org.springframework.boot.autoconfigure.mongo.MongoProperties.Gridfs; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.support.PersistenceExceptionTranslator; +import org.springframework.data.mongodb.MongoDatabaseFactory; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.convert.DbRefResolver; +import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.convert.MongoConverter; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.gridfs.GridFsOperations; +import org.springframework.data.mongodb.gridfs.GridFsTemplate; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Configuration for Mongo-related beans that depend on a {@link MongoDatabaseFactory}. + * + * @author Andy Wilkinson + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnBean(MongoDatabaseFactory.class) +class MongoDatabaseFactoryDependentConfiguration { + + private final MongoProperties properties; + + MongoDatabaseFactoryDependentConfiguration(MongoProperties properties) { + this.properties = properties; + } + + @Bean + @ConditionalOnMissingBean(MongoOperations.class) + MongoTemplate mongoTemplate(MongoDatabaseFactory factory, MongoConverter converter) { + return new MongoTemplate(factory, converter); + } + + @Bean + @ConditionalOnMissingBean(MongoConverter.class) + MappingMongoConverter mappingMongoConverter(MongoDatabaseFactory factory, MongoMappingContext context, + MongoCustomConversions conversions) { + DbRefResolver dbRefResolver = new DefaultDbRefResolver(factory); + MappingMongoConverter mappingConverter = new MappingMongoConverter(dbRefResolver, context); + mappingConverter.setCustomConversions(conversions); + return mappingConverter; + } + + @Bean + @ConditionalOnMissingBean(GridFsOperations.class) + GridFsTemplate gridFsTemplate(MongoDatabaseFactory factory, MongoTemplate mongoTemplate) { + return new GridFsTemplate(new GridFsMongoDatabaseFactory(factory, this.properties), + mongoTemplate.getConverter(), this.properties.getGridfs().getBucket()); + } + + /** + * {@link MongoDatabaseFactory} decorator to respect {@link Gridfs#getDatabase()} if + * set. + */ + static class GridFsMongoDatabaseFactory implements MongoDatabaseFactory { + + private final MongoDatabaseFactory mongoDatabaseFactory; + + private final MongoProperties properties; + + GridFsMongoDatabaseFactory(MongoDatabaseFactory mongoDatabaseFactory, MongoProperties properties) { + Assert.notNull(mongoDatabaseFactory, "MongoDatabaseFactory must not be null"); + Assert.notNull(properties, "Properties must not be null"); + this.mongoDatabaseFactory = mongoDatabaseFactory; + this.properties = properties; + } + + @Override + public MongoDatabase getMongoDatabase() throws DataAccessException { + String gridFsDatabase = this.properties.getGridfs().getDatabase(); + if (StringUtils.hasText(gridFsDatabase)) { + return this.mongoDatabaseFactory.getMongoDatabase(gridFsDatabase); + } + return this.mongoDatabaseFactory.getMongoDatabase(); + } + + @Override + public MongoDatabase getMongoDatabase(String dbName) throws DataAccessException { + return this.mongoDatabaseFactory.getMongoDatabase(dbName); + } + + @Override + public PersistenceExceptionTranslator getExceptionTranslator() { + return this.mongoDatabaseFactory.getExceptionTranslator(); + } + + @Override + public ClientSession getSession(ClientSessionOptions options) { + return this.mongoDatabaseFactory.getSession(options); + } + + @Override + public MongoDatabaseFactory withSession(ClientSession session) { + return this.mongoDatabaseFactory.withSession(session); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoDbFactoryConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoDbFactoryConfiguration.java deleted file mode 100644 index e8bc2dded116..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoDbFactoryConfiguration.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.data.mongo; - -import com.mongodb.MongoClient; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.data.mongo.MongoDbFactoryConfiguration.AnyMongoClientAvailable; -import org.springframework.boot.autoconfigure.mongo.MongoProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Conditional; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.mongodb.MongoDbFactory; -import org.springframework.data.mongodb.core.MongoDbFactorySupport; -import org.springframework.data.mongodb.core.SimpleMongoClientDbFactory; -import org.springframework.data.mongodb.core.SimpleMongoDbFactory; - -/** - * Configuration for a {@link MongoDbFactory}. - * - * @author Andy Wilkinson - */ -@Configuration(proxyBeanMethods = false) -@ConditionalOnMissingBean(MongoDbFactory.class) -@Conditional(AnyMongoClientAvailable.class) -class MongoDbFactoryConfiguration { - - @Bean - MongoDbFactorySupport mongoDbFactory(ObjectProvider mongo, - ObjectProvider mongoClient, MongoProperties properties) { - MongoClient preferredClient = mongo.getIfAvailable(); - if (preferredClient != null) { - return new SimpleMongoDbFactory(preferredClient, properties.getMongoClientDatabase()); - } - com.mongodb.client.MongoClient fallbackClient = mongoClient.getIfAvailable(); - if (fallbackClient != null) { - return new SimpleMongoClientDbFactory(fallbackClient, properties.getMongoClientDatabase()); - } - throw new IllegalStateException("Expected to find at least one MongoDB client."); - } - - /** - * Check if either a {@link MongoClient com.mongodb.MongoClient} or - * {@link com.mongodb.client.MongoClient com.mongodb.client.MongoClient} bean is - * available. - */ - static class AnyMongoClientAvailable extends AnyNestedCondition { - - AnyMongoClientAvailable() { - super(ConfigurationPhase.REGISTER_BEAN); - } - - @ConditionalOnBean(MongoClient.class) - static class PreferredClientAvailable { - - } - - @ConditionalOnBean(com.mongodb.client.MongoClient.class) - static class FallbackClientAvailable { - - } - - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoDbFactoryDependentConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoDbFactoryDependentConfiguration.java deleted file mode 100644 index 9c8e7d886a96..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoDbFactoryDependentConfiguration.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.data.mongo; - -import com.mongodb.ClientSessionOptions; -import com.mongodb.DB; -import com.mongodb.client.ClientSession; -import com.mongodb.client.MongoDatabase; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.mongo.MongoProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.dao.DataAccessException; -import org.springframework.dao.support.PersistenceExceptionTranslator; -import org.springframework.data.mongodb.MongoDbFactory; -import org.springframework.data.mongodb.core.MongoOperations; -import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.data.mongodb.core.convert.DbRefResolver; -import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver; -import org.springframework.data.mongodb.core.convert.MappingMongoConverter; -import org.springframework.data.mongodb.core.convert.MongoConverter; -import org.springframework.data.mongodb.core.convert.MongoCustomConversions; -import org.springframework.data.mongodb.core.mapping.MongoMappingContext; -import org.springframework.data.mongodb.gridfs.GridFsOperations; -import org.springframework.data.mongodb.gridfs.GridFsTemplate; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; - -/** - * Configuration for Mongo-related beans that depend on a {@link MongoDbFactory}. - * - * @author Andy Wilkinson - */ -@Configuration(proxyBeanMethods = false) -@ConditionalOnBean(MongoDbFactory.class) -class MongoDbFactoryDependentConfiguration { - - private final MongoProperties properties; - - MongoDbFactoryDependentConfiguration(MongoProperties properties) { - this.properties = properties; - } - - @Bean - @ConditionalOnMissingBean(MongoOperations.class) - MongoTemplate mongoTemplate(MongoDbFactory mongoDbFactory, MongoConverter converter) { - return new MongoTemplate(mongoDbFactory, converter); - } - - @Bean - @ConditionalOnMissingBean(MongoConverter.class) - MappingMongoConverter mappingMongoConverter(MongoDbFactory factory, MongoMappingContext context, - MongoCustomConversions conversions) { - DbRefResolver dbRefResolver = new DefaultDbRefResolver(factory); - MappingMongoConverter mappingConverter = new MappingMongoConverter(dbRefResolver, context); - mappingConverter.setCustomConversions(conversions); - return mappingConverter; - } - - @Bean - @ConditionalOnMissingBean(GridFsOperations.class) - GridFsTemplate gridFsTemplate(MongoDbFactory mongoDbFactory, MongoTemplate mongoTemplate) { - return new GridFsTemplate(new GridFsMongoDbFactory(mongoDbFactory, this.properties), - mongoTemplate.getConverter()); - } - - /** - * {@link MongoDbFactory} decorator to respect - * {@link MongoProperties#getGridFsDatabase()} if set. - */ - static class GridFsMongoDbFactory implements MongoDbFactory { - - private final MongoDbFactory mongoDbFactory; - - private final MongoProperties properties; - - GridFsMongoDbFactory(MongoDbFactory mongoDbFactory, MongoProperties properties) { - Assert.notNull(mongoDbFactory, "MongoDbFactory must not be null"); - Assert.notNull(properties, "Properties must not be null"); - this.mongoDbFactory = mongoDbFactory; - this.properties = properties; - } - - @Override - public MongoDatabase getDb() throws DataAccessException { - String gridFsDatabase = this.properties.getGridFsDatabase(); - if (StringUtils.hasText(gridFsDatabase)) { - return this.mongoDbFactory.getDb(gridFsDatabase); - } - return this.mongoDbFactory.getDb(); - } - - @Override - public MongoDatabase getDb(String dbName) throws DataAccessException { - return this.mongoDbFactory.getDb(dbName); - } - - @Override - public PersistenceExceptionTranslator getExceptionTranslator() { - return this.mongoDbFactory.getExceptionTranslator(); - } - - @Override - @Deprecated - public DB getLegacyDb() { - return this.mongoDbFactory.getLegacyDb(); - } - - @Override - public ClientSession getSession(ClientSessionOptions options) { - return this.mongoDbFactory.getSession(options); - } - - @Override - public MongoDbFactory withSession(ClientSession session) { - return this.mongoDbFactory.withSession(session); - } - - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoReactiveDataAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoReactiveDataAutoConfiguration.java index a9ec8d7f5f77..c76c89c456dd 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoReactiveDataAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoReactiveDataAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,15 @@ package org.springframework.boot.autoconfigure.data.mongo; +import java.util.Optional; + +import com.mongodb.ClientSessionOptions; +import com.mongodb.reactivestreams.client.ClientSession; import com.mongodb.reactivestreams.client.MongoClient; +import com.mongodb.reactivestreams.client.MongoDatabase; +import org.bson.codecs.Codec; +import org.bson.codecs.configuration.CodecRegistry; +import reactor.core.publisher.Mono; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -24,6 +32,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.mongo.MongoProperties; +import org.springframework.boot.autoconfigure.mongo.MongoProperties.Gridfs; import org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -31,6 +40,8 @@ import org.springframework.context.annotation.Import; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory; import org.springframework.data.mongodb.core.ReactiveMongoOperations; import org.springframework.data.mongodb.core.ReactiveMongoTemplate; @@ -42,6 +53,7 @@ import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.gridfs.ReactiveGridFsOperations; import org.springframework.data.mongodb.gridfs.ReactiveGridFsTemplate; +import org.springframework.util.StringUtils; /** * {@link EnableAutoConfiguration Auto-configuration} for Spring Data's reactive mongo @@ -97,9 +109,78 @@ public DefaultDataBufferFactory dataBufferFactory() { @Bean @ConditionalOnMissingBean(ReactiveGridFsOperations.class) - public ReactiveGridFsTemplate reactiveGridFsTemplate(ReactiveMongoDatabaseFactory reactiveMongoDbFactory, - MappingMongoConverter mappingMongoConverter, DataBufferFactory dataBufferFactory) { - return new ReactiveGridFsTemplate(dataBufferFactory, reactiveMongoDbFactory, mappingMongoConverter, null); + public ReactiveGridFsTemplate reactiveGridFsTemplate(ReactiveMongoDatabaseFactory reactiveMongoDatabaseFactory, + MappingMongoConverter mappingMongoConverter, DataBufferFactory dataBufferFactory, + MongoProperties properties) { + return new ReactiveGridFsTemplate(dataBufferFactory, + new GridFsReactiveMongoDatabaseFactory(reactiveMongoDatabaseFactory, properties), mappingMongoConverter, + properties.getGridfs().getBucket()); + } + + /** + * {@link ReactiveMongoDatabaseFactory} decorator to use {@link Gridfs#getDatabase()} + * when set. + */ + static class GridFsReactiveMongoDatabaseFactory implements ReactiveMongoDatabaseFactory { + + private final ReactiveMongoDatabaseFactory delegate; + + private final MongoProperties properties; + + GridFsReactiveMongoDatabaseFactory(ReactiveMongoDatabaseFactory delegate, MongoProperties properties) { + this.delegate = delegate; + this.properties = properties; + } + + @Override + public boolean hasCodecFor(Class type) { + return this.delegate.hasCodecFor(type); + } + + @Override + public Mono getMongoDatabase() throws DataAccessException { + String gridFsDatabase = this.properties.getGridfs().getDatabase(); + if (StringUtils.hasText(gridFsDatabase)) { + return this.delegate.getMongoDatabase(gridFsDatabase); + } + return this.delegate.getMongoDatabase(); + } + + @Override + public Mono getMongoDatabase(String dbName) throws DataAccessException { + return this.delegate.getMongoDatabase(dbName); + } + + @Override + public Optional> getCodecFor(Class type) { + return this.delegate.getCodecFor(type); + } + + @Override + public PersistenceExceptionTranslator getExceptionTranslator() { + return this.delegate.getExceptionTranslator(); + } + + @Override + public CodecRegistry getCodecRegistry() { + return this.delegate.getCodecRegistry(); + } + + @Override + public Mono getSession(ClientSessionOptions options) { + return this.delegate.getSession(options); + } + + @Override + public ReactiveMongoDatabaseFactory withSession(ClientSession session) { + return this.delegate.withSession(session); + } + + @Override + public boolean isTransactionActive() { + return this.delegate.isTransactionActive(); + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoRepositoriesAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoRepositoriesAutoConfiguration.java index 527042576c8a..eb0cc5f7e434 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoRepositoriesAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/MongoRepositoriesAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ package org.springframework.boot.autoconfigure.data.mongo; -import com.mongodb.MongoClient; +import com.mongodb.client.MongoClient; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/ReactiveStreamsMongoClientDependsOnBeanFactoryPostProcessor.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/ReactiveStreamsMongoClientDependsOnBeanFactoryPostProcessor.java index fa236dca4855..0b0be21434f9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/ReactiveStreamsMongoClientDependsOnBeanFactoryPostProcessor.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/mongo/ReactiveStreamsMongoClientDependsOnBeanFactoryPostProcessor.java @@ -37,18 +37,6 @@ public class ReactiveStreamsMongoClientDependsOnBeanFactoryPostProcessor extends AbstractDependsOnBeanFactoryPostProcessor { - /** - * Creates a new {@code ReactiveStreamsMongoClientDependsOnBeanFactoryPostProcessor} - * that will set up dependencies upon beans with the given names. - * @param dependsOn names of the beans to depend upon - * @deprecated since 2.1.7 in favor of - * {@link #ReactiveStreamsMongoClientDependsOnBeanFactoryPostProcessor(Class...)} - */ - @Deprecated - public ReactiveStreamsMongoClientDependsOnBeanFactoryPostProcessor(String... dependsOn) { - super(MongoClient.class, ReactiveMongoClientFactoryBean.class, dependsOn); - } - /** * Creates a new {@code ReactiveStreamsMongoClientDependsOnBeanFactoryPostProcessor} * that will set up dependencies upon beans with the given types. diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jBookmarkManagementConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jBookmarkManagementConfiguration.java deleted file mode 100644 index 0d71fb6866a4..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jBookmarkManagementConfiguration.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.data.neo4j; - -import com.github.benmanes.caffeine.cache.Caffeine; - -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.ConditionalOnNotWebApplication; -import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; -import org.springframework.cache.caffeine.CaffeineCacheManager; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Scope; -import org.springframework.context.annotation.ScopedProxyMode; -import org.springframework.data.neo4j.bookmark.BeanFactoryBookmarkOperationAdvisor; -import org.springframework.data.neo4j.bookmark.BookmarkInterceptor; -import org.springframework.data.neo4j.bookmark.BookmarkManager; -import org.springframework.data.neo4j.bookmark.CaffeineBookmarkManager; -import org.springframework.web.context.WebApplicationContext; - -/** - * Provides a {@link BookmarkManager} for Neo4j's bookmark support based on Caffeine if - * available. Depending on the application's type (web or not) the bookmark manager will - * be bound to the application or the request, as recommend by Spring Data Neo4j. - * - * @author Michael Simons - */ -@Configuration(proxyBeanMethods = false) -@ConditionalOnClass({ Caffeine.class, CaffeineCacheManager.class }) -@ConditionalOnMissingBean(BookmarkManager.class) -@ConditionalOnBean({ BeanFactoryBookmarkOperationAdvisor.class, BookmarkInterceptor.class }) -class Neo4jBookmarkManagementConfiguration { - - private static final String BOOKMARK_MANAGER_BEAN_NAME = "bookmarkManager"; - - @Bean(BOOKMARK_MANAGER_BEAN_NAME) - @ConditionalOnWebApplication - @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.INTERFACES) - BookmarkManager requestScopedBookmarkManager() { - return new CaffeineBookmarkManager(); - } - - @Bean(BOOKMARK_MANAGER_BEAN_NAME) - @ConditionalOnNotWebApplication - BookmarkManager singletonScopedBookmarkManager() { - return new CaffeineBookmarkManager(); - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jDataAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jDataAutoConfiguration.java index 585efa568252..1d204ddcb156 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jDataAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jDataAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,34 +16,37 @@ package org.springframework.boot.autoconfigure.data.neo4j; -import java.util.List; +import java.util.Set; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.neo4j.ogm.session.SessionFactory; -import org.neo4j.ogm.session.event.EventListener; +import org.neo4j.driver.Driver; -import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.AutoConfigurationPackages; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +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.ConditionalOnWebApplication; -import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; -import org.springframework.boot.autoconfigure.domain.EntityScanPackages; +import org.springframework.boot.autoconfigure.domain.EntityScanner; +import org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration; +import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration; import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizers; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.data.neo4j.transaction.Neo4jTransactionManager; -import org.springframework.data.neo4j.web.support.OpenSessionInViewInterceptor; +import org.springframework.data.neo4j.core.DatabaseSelectionProvider; +import org.springframework.data.neo4j.core.Neo4jClient; +import org.springframework.data.neo4j.core.Neo4jOperations; +import org.springframework.data.neo4j.core.Neo4jTemplate; +import org.springframework.data.neo4j.core.convert.Neo4jConversions; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.core.schema.RelationshipProperties; +import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; +import org.springframework.data.neo4j.repository.config.Neo4jRepositoryConfigurationExtension; import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.util.StringUtils; -import org.springframework.web.servlet.config.annotation.InterceptorRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.transaction.TransactionManager; /** * {@link EnableAutoConfiguration Auto-configuration} for Spring Data Neo4j. @@ -53,85 +56,61 @@ * @author Vince Bickers * @author Stephane Nicoll * @author Kazuki Shimizu - * @author Michael Simons + * @author Michael J. Simons * @since 1.4.0 */ @Configuration(proxyBeanMethods = false) -@ConditionalOnClass({ SessionFactory.class, Neo4jTransactionManager.class, PlatformTransactionManager.class }) -@EnableConfigurationProperties(Neo4jProperties.class) -@Import(Neo4jBookmarkManagementConfiguration.class) +@ConditionalOnClass({ Driver.class, Neo4jTransactionManager.class, PlatformTransactionManager.class }) +@EnableConfigurationProperties(Neo4jDataProperties.class) +@ConditionalOnBean(Driver.class) +@AutoConfigureBefore(TransactionAutoConfiguration.class) +@AutoConfigureAfter(Neo4jAutoConfiguration.class) public class Neo4jDataAutoConfiguration { @Bean - @ConditionalOnMissingBean(PlatformTransactionManager.class) - public Neo4jTransactionManager transactionManager(SessionFactory sessionFactory, - ObjectProvider transactionManagerCustomizers) { - Neo4jTransactionManager transactionManager = new Neo4jTransactionManager(sessionFactory); - transactionManagerCustomizers.ifAvailable((customizers) -> customizers.customize(transactionManager)); - return transactionManager; + @ConditionalOnMissingBean + public Neo4jConversions neo4jConversions() { + return new Neo4jConversions(); } - @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingBean(SessionFactory.class) - static class Neo4jOgmSessionFactoryConfiguration { - - @Bean - @ConditionalOnMissingBean - org.neo4j.ogm.config.Configuration configuration(Neo4jProperties properties) { - return properties.createConfiguration(); - } - - @Bean - SessionFactory sessionFactory(org.neo4j.ogm.config.Configuration configuration, BeanFactory beanFactory, - ObjectProvider eventListeners) { - SessionFactory sessionFactory = new SessionFactory(configuration, getPackagesToScan(beanFactory)); - eventListeners.orderedStream().forEach(sessionFactory::register); - return sessionFactory; - } - - private String[] getPackagesToScan(BeanFactory beanFactory) { - List packages = EntityScanPackages.get(beanFactory).getPackageNames(); - if (packages.isEmpty() && AutoConfigurationPackages.has(beanFactory)) { - packages = AutoConfigurationPackages.get(beanFactory); - } - return StringUtils.toStringArray(packages); - } - + @Bean + @ConditionalOnMissingBean + public Neo4jMappingContext neo4jMappingContext(ApplicationContext applicationContext, + Neo4jConversions neo4jConversions) throws ClassNotFoundException { + Set> initialEntityClasses = new EntityScanner(applicationContext).scan(Node.class, + RelationshipProperties.class); + Neo4jMappingContext context = new Neo4jMappingContext(neo4jConversions); + context.setInitialEntitySet(initialEntityClasses); + return context; } - @Configuration(proxyBeanMethods = false) - @ConditionalOnWebApplication(type = Type.SERVLET) - @ConditionalOnClass({ WebMvcConfigurer.class, OpenSessionInViewInterceptor.class }) - @ConditionalOnMissingBean(OpenSessionInViewInterceptor.class) - @ConditionalOnProperty(prefix = "spring.data.neo4j", name = "open-in-view", havingValue = "true", - matchIfMissing = true) - static class Neo4jWebConfiguration { - - private static final Log logger = LogFactory.getLog(Neo4jWebConfiguration.class); - - @Bean - OpenSessionInViewInterceptor neo4jOpenSessionInViewInterceptor(Neo4jProperties properties) { - if (properties.getOpenInView() == null) { - logger.warn("spring.data.neo4j.open-in-view is enabled by default." - + "Therefore, database queries may be performed during view " - + "rendering. Explicitly configure " - + "spring.data.neo4j.open-in-view to disable this warning"); - } - return new OpenSessionInViewInterceptor(); - } - - @Bean - WebMvcConfigurer neo4jOpenSessionInViewInterceptorConfigurer(OpenSessionInViewInterceptor interceptor) { - return new WebMvcConfigurer() { + @Bean + @ConditionalOnMissingBean + public DatabaseSelectionProvider databaseSelectionProvider(Neo4jDataProperties properties) { + String database = properties.getDatabase(); + return (database != null) ? DatabaseSelectionProvider.createStaticDatabaseSelectionProvider(database) + : DatabaseSelectionProvider.getDefaultSelectionProvider(); + } - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addWebRequestInterceptor(interceptor); - } + @Bean(Neo4jRepositoryConfigurationExtension.DEFAULT_NEO4J_CLIENT_BEAN_NAME) + @ConditionalOnMissingBean + public Neo4jClient neo4jClient(Driver driver, DatabaseSelectionProvider databaseNameProvider) { + return Neo4jClient.create(driver, databaseNameProvider); + } - }; - } + @Bean(Neo4jRepositoryConfigurationExtension.DEFAULT_NEO4J_TEMPLATE_BEAN_NAME) + @ConditionalOnMissingBean(Neo4jOperations.class) + public Neo4jTemplate neo4jTemplate(Neo4jClient neo4jClient, Neo4jMappingContext neo4jMappingContext) { + return new Neo4jTemplate(neo4jClient, neo4jMappingContext); + } + @Bean(Neo4jRepositoryConfigurationExtension.DEFAULT_TRANSACTION_MANAGER_BEAN_NAME) + @ConditionalOnMissingBean(TransactionManager.class) + public Neo4jTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider, + ObjectProvider optionalCustomizers) { + Neo4jTransactionManager transactionManager = new Neo4jTransactionManager(driver, databaseNameProvider); + optionalCustomizers.ifAvailable((customizer) -> customizer.customize(transactionManager)); + return transactionManager; } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jDataProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jDataProperties.java new file mode 100644 index 000000000000..ea1c19052472 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jDataProperties.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.neo4j; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties for Spring Data Neo4j. + * + * @author Michael J. Simons + * @since 2.4.0 + */ +@ConfigurationProperties(prefix = "spring.data.neo4j") +public class Neo4jDataProperties { + + /** + * Database name to use. By default, the server decides the default database to use. + */ + private String database; + + public String getDatabase() { + return this.database; + } + + public void setDatabase(String database) { + this.database = database; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jProperties.java deleted file mode 100644 index 7eff8e1289bf..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jProperties.java +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.data.neo4j; - -import org.neo4j.ogm.config.AutoIndexMode; -import org.neo4j.ogm.config.Configuration; -import org.neo4j.ogm.config.Configuration.Builder; - -import org.springframework.beans.BeansException; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.util.ClassUtils; - -/** - * Configuration properties for Neo4j. - * - * @author Stephane Nicoll - * @author Michael Hunger - * @author Vince Bickers - * @author Aurélien Leboulanger - * @author Michael Simons - * @since 1.4.0 - */ -@ConfigurationProperties(prefix = "spring.data.neo4j") -public class Neo4jProperties implements ApplicationContextAware { - - static final String EMBEDDED_DRIVER = "org.neo4j.ogm.drivers.embedded.driver.EmbeddedDriver"; - - static final String HTTP_DRIVER = "org.neo4j.ogm.drivers.http.driver.HttpDriver"; - - static final String DEFAULT_BOLT_URI = "bolt://localhost:7687"; - - static final String BOLT_DRIVER = "org.neo4j.ogm.drivers.bolt.driver.BoltDriver"; - - /** - * URI used by the driver. Auto-detected by default. - */ - private String uri; - - /** - * Login user of the server. - */ - private String username; - - /** - * Login password of the server. - */ - private String password; - - /** - * Auto index mode. - */ - private AutoIndexMode autoIndex = AutoIndexMode.NONE; - - /** - * Register OpenSessionInViewInterceptor. Binds a Neo4j Session to the thread for the - * entire processing of the request.", - */ - private Boolean openInView; - - /** - * Whether to use Neo4j native types wherever possible. - */ - private boolean useNativeTypes = false; - - private final Embedded embedded = new Embedded(); - - private ClassLoader classLoader = Neo4jProperties.class.getClassLoader(); - - public String getUri() { - return this.uri; - } - - public void setUri(String uri) { - this.uri = uri; - } - - public String getUsername() { - return this.username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getPassword() { - return this.password; - } - - public void setPassword(String password) { - this.password = password; - } - - public AutoIndexMode getAutoIndex() { - return this.autoIndex; - } - - public void setAutoIndex(AutoIndexMode autoIndex) { - this.autoIndex = autoIndex; - } - - public Boolean getOpenInView() { - return this.openInView; - } - - public void setOpenInView(Boolean openInView) { - this.openInView = openInView; - } - - public boolean isUseNativeTypes() { - return this.useNativeTypes; - } - - public void setUseNativeTypes(boolean useNativeTypes) { - this.useNativeTypes = useNativeTypes; - } - - public Embedded getEmbedded() { - return this.embedded; - } - - @Override - public void setApplicationContext(ApplicationContext ctx) throws BeansException { - this.classLoader = ctx.getClassLoader(); - } - - /** - * Create a {@link Configuration} based on the state of this instance. - * @return a configuration - */ - public Configuration createConfiguration() { - Builder builder = new Builder(); - configure(builder); - return builder.build(); - } - - private void configure(Builder builder) { - if (this.uri != null) { - builder.uri(this.uri); - } - else { - configureUriWithDefaults(builder); - } - if (this.username != null && this.password != null) { - builder.credentials(this.username, this.password); - } - builder.autoIndex(this.getAutoIndex().getName()); - if (this.useNativeTypes) { - builder.useNativeTypes(); - } - } - - private void configureUriWithDefaults(Builder builder) { - if (!getEmbedded().isEnabled() || !ClassUtils.isPresent(EMBEDDED_DRIVER, this.classLoader)) { - builder.uri(DEFAULT_BOLT_URI); - } - } - - public static class Embedded { - - /** - * Whether to enable embedded mode if the embedded driver is available. - */ - private boolean enabled = true; - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jReactiveDataAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jReactiveDataAutoConfiguration.java new file mode 100644 index 000000000000..38dcf4e5014d --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jReactiveDataAutoConfiguration.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.neo4j; + +import org.neo4j.driver.Driver; +import reactor.core.publisher.Flux; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; +import org.springframework.data.neo4j.core.ReactiveNeo4jClient; +import org.springframework.data.neo4j.core.ReactiveNeo4jOperations; +import org.springframework.data.neo4j.core.ReactiveNeo4jTemplate; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.neo4j.repository.config.ReactiveNeo4jRepositoryConfigurationExtension; +import org.springframework.transaction.ReactiveTransactionManager; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Spring Data's reactive Neo4j + * support. + * + * @author Michael J. Simons + * @author Stephane Nicoll + * @since 2.4.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass({ Driver.class, ReactiveNeo4jTemplate.class, ReactiveTransactionManager.class, Flux.class }) +@ConditionalOnBean(Driver.class) +@AutoConfigureAfter(Neo4jDataAutoConfiguration.class) +public class Neo4jReactiveDataAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public ReactiveDatabaseSelectionProvider reactiveDatabaseSelectionProvider(Neo4jDataProperties dataProperties) { + String database = dataProperties.getDatabase(); + return (database != null) ? ReactiveDatabaseSelectionProvider.createStaticDatabaseSelectionProvider(database) + : ReactiveDatabaseSelectionProvider.getDefaultSelectionProvider(); + } + + @Bean(ReactiveNeo4jRepositoryConfigurationExtension.DEFAULT_NEO4J_CLIENT_BEAN_NAME) + @ConditionalOnMissingBean + public ReactiveNeo4jClient reactiveNeo4jClient(Driver driver, + ReactiveDatabaseSelectionProvider databaseNameProvider) { + return ReactiveNeo4jClient.create(driver, databaseNameProvider); + } + + @Bean(ReactiveNeo4jRepositoryConfigurationExtension.DEFAULT_NEO4J_TEMPLATE_BEAN_NAME) + @ConditionalOnMissingBean(ReactiveNeo4jOperations.class) + public ReactiveNeo4jTemplate reactiveNeo4jTemplate(ReactiveNeo4jClient neo4jClient, + Neo4jMappingContext neo4jMappingContext) { + return new ReactiveNeo4jTemplate(neo4jClient, neo4jMappingContext); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jReactiveRepositoriesAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jReactiveRepositoriesAutoConfiguration.java new file mode 100644 index 000000000000..27cb2ee20094 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jReactiveRepositoriesAutoConfiguration.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.neo4j; + +import org.neo4j.driver.Driver; +import reactor.core.publisher.Flux; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.data.ConditionalOnRepositoryType; +import org.springframework.boot.autoconfigure.data.RepositoryType; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; +import org.springframework.data.neo4j.repository.config.ReactiveNeo4jRepositoryConfigurationExtension; +import org.springframework.data.neo4j.repository.support.ReactiveNeo4jRepositoryFactoryBean; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Spring Data's Neo4j Reactive + * Repositories. + * + * @author Michael J. Simons + * @author Stephane Nicoll + * @since 2.4.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass({ Driver.class, ReactiveNeo4jRepository.class, Flux.class }) +@ConditionalOnMissingBean({ ReactiveNeo4jRepositoryFactoryBean.class, + ReactiveNeo4jRepositoryConfigurationExtension.class }) +@ConditionalOnRepositoryType(store = "neo4j", type = RepositoryType.REACTIVE) +@Import(Neo4jReactiveRepositoriesRegistrar.class) +@AutoConfigureAfter(Neo4jReactiveDataAutoConfiguration.class) +public class Neo4jReactiveRepositoriesAutoConfiguration { + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jReactiveRepositoriesRegistrar.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jReactiveRepositoriesRegistrar.java new file mode 100644 index 000000000000..e52749df5577 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jReactiveRepositoriesRegistrar.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.neo4j; + +import java.lang.annotation.Annotation; + +import org.springframework.boot.autoconfigure.data.AbstractRepositoryConfigurationSourceSupport; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepositories; +import org.springframework.data.neo4j.repository.config.ReactiveNeo4jRepositoryConfigurationExtension; +import org.springframework.data.repository.config.RepositoryConfigurationExtension; + +/** + * {@link ImportBeanDefinitionRegistrar} used to auto-configure Spring Data Neo4j reactive + * Repositories. + * + * @author Michael J. Simons + */ +class Neo4jReactiveRepositoriesRegistrar extends AbstractRepositoryConfigurationSourceSupport { + + @Override + protected Class getAnnotation() { + return EnableReactiveNeo4jRepositories.class; + } + + @Override + protected Class getConfiguration() { + return EnableReactiveNeo4jRepositoriesConfiguration.class; + } + + @Override + protected RepositoryConfigurationExtension getRepositoryConfigurationExtension() { + return new ReactiveNeo4jRepositoryConfigurationExtension(); + } + + @EnableReactiveNeo4jRepositories + private static class EnableReactiveNeo4jRepositoriesConfiguration { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jRepositoriesAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jRepositoriesAutoConfiguration.java index 54de8aaa2f57..76977f3e7c16 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jRepositoriesAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jRepositoriesAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,14 @@ package org.springframework.boot.autoconfigure.data.neo4j; -import org.neo4j.ogm.session.Neo4jSession; +import org.neo4j.driver.Driver; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 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.data.ConditionalOnRepositoryType; +import org.springframework.boot.autoconfigure.data.RepositoryType; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.data.neo4j.repository.Neo4jRepository; @@ -34,10 +35,10 @@ * {@link EnableAutoConfiguration Auto-configuration} for Spring Data's Neo4j * Repositories. *

- * Activates when there is no bean of type {@link Neo4jRepositoryFactoryBean} configured - * in the context, the Spring Data Neo4j {@link Neo4jRepository} type is on the classpath, - * the Neo4j client driver API is on the classpath, and there is no other configured - * {@link Neo4jRepository}. + * Activates when there is no bean of type {@link Neo4jRepositoryFactoryBean} or + * {@link Neo4jRepositoryConfigurationExtension} configured in the context, the Spring + * Data Neo4j {@link Neo4jRepository} type is on the classpath, the Neo4j client driver + * API is on the classpath, and there is no other configured {@link Neo4jRepository}. *

* Once in effect, the auto-configuration is the equivalent of enabling Neo4j repositories * using the {@link EnableNeo4jRepositories @EnableNeo4jRepositories} annotation. @@ -45,14 +46,14 @@ * @author Dave Syer * @author Oliver Gierke * @author Josh Long + * @author Michael J. Simons * @since 1.4.0 * @see EnableNeo4jRepositories */ @Configuration(proxyBeanMethods = false) -@ConditionalOnClass({ Neo4jSession.class, Neo4jRepository.class }) +@ConditionalOnClass({ Driver.class, Neo4jRepository.class }) @ConditionalOnMissingBean({ Neo4jRepositoryFactoryBean.class, Neo4jRepositoryConfigurationExtension.class }) -@ConditionalOnProperty(prefix = "spring.data.neo4j.repositories", name = "enabled", havingValue = "true", - matchIfMissing = true) +@ConditionalOnRepositoryType(store = "neo4j", type = RepositoryType.IMPERATIVE) @Import(Neo4jRepositoriesRegistrar.class) @AutoConfigureAfter(Neo4jDataAutoConfiguration.class) public class Neo4jRepositoriesAutoConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jRepositoriesRegistrar.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jRepositoriesRegistrar.java index 976581095e31..7009b5bfdb52 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jRepositoriesRegistrar.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jRepositoriesRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/package-info.java index 38b1e038a477..4a6a7a12db2d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/package-info.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/r2dbc/R2dbcDataAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/r2dbc/R2dbcDataAutoConfiguration.java new file mode 100644 index 000000000000..01ec48fd5a80 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/r2dbc/R2dbcDataAutoConfiguration.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.r2dbc; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.convert.CustomConversions; +import org.springframework.data.r2dbc.convert.MappingR2dbcConverter; +import org.springframework.data.r2dbc.convert.R2dbcConverter; +import org.springframework.data.r2dbc.convert.R2dbcCustomConversions; +import org.springframework.data.r2dbc.core.R2dbcEntityTemplate; +import org.springframework.data.r2dbc.dialect.DialectResolver; +import org.springframework.data.r2dbc.dialect.R2dbcDialect; +import org.springframework.data.r2dbc.mapping.R2dbcMappingContext; +import org.springframework.data.relational.core.mapping.NamingStrategy; +import org.springframework.r2dbc.core.DatabaseClient; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for {@link DatabaseClient}. + * + * @author Mark Paluch + * @author Oliver Drotbohm + * @since 2.3.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass({ DatabaseClient.class, R2dbcEntityTemplate.class }) +@ConditionalOnSingleCandidate(DatabaseClient.class) +@AutoConfigureAfter(R2dbcAutoConfiguration.class) +public class R2dbcDataAutoConfiguration { + + private final DatabaseClient databaseClient; + + private final R2dbcDialect dialect; + + public R2dbcDataAutoConfiguration(DatabaseClient databaseClient) { + this.databaseClient = databaseClient; + this.dialect = DialectResolver.getDialect(this.databaseClient.getConnectionFactory()); + } + + @Bean + @ConditionalOnMissingBean + public R2dbcEntityTemplate r2dbcEntityTemplate(R2dbcConverter r2dbcConverter) { + return new R2dbcEntityTemplate(this.databaseClient, this.dialect, r2dbcConverter); + } + + @Bean + @ConditionalOnMissingBean + public R2dbcMappingContext r2dbcMappingContext(ObjectProvider namingStrategy, + R2dbcCustomConversions r2dbcCustomConversions) { + R2dbcMappingContext relationalMappingContext = new R2dbcMappingContext( + namingStrategy.getIfAvailable(() -> NamingStrategy.INSTANCE)); + relationalMappingContext.setSimpleTypeHolder(r2dbcCustomConversions.getSimpleTypeHolder()); + return relationalMappingContext; + } + + @Bean + @ConditionalOnMissingBean + public MappingR2dbcConverter r2dbcConverter(R2dbcMappingContext mappingContext, + R2dbcCustomConversions r2dbcCustomConversions) { + return new MappingR2dbcConverter(mappingContext, r2dbcCustomConversions); + } + + @Bean + @ConditionalOnMissingBean + public R2dbcCustomConversions r2dbcCustomConversions() { + List converters = new ArrayList<>(this.dialect.getConverters()); + converters.addAll(R2dbcCustomConversions.STORE_CONVERTERS); + return new R2dbcCustomConversions( + CustomConversions.StoreConversions.of(this.dialect.getSimpleTypeHolder(), converters), + Collections.emptyList()); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/r2dbc/R2dbcRepositoriesAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/r2dbc/R2dbcRepositoriesAutoConfiguration.java new file mode 100644 index 000000000000..a4dbba2bbc22 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/r2dbc/R2dbcRepositoriesAutoConfiguration.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.r2dbc; + +import io.r2dbc.spi.ConnectionFactory; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +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.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.r2dbc.repository.R2dbcRepository; +import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories; +import org.springframework.data.r2dbc.repository.support.R2dbcRepositoryFactoryBean; +import org.springframework.r2dbc.core.DatabaseClient; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Spring Data R2DBC Repositories. + * + * @author Mark Paluch + * @since 2.3.0 + * @see EnableR2dbcRepositories + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass({ ConnectionFactory.class, R2dbcRepository.class }) +@ConditionalOnBean(DatabaseClient.class) +@ConditionalOnProperty(prefix = "spring.data.r2dbc.repositories", name = "enabled", havingValue = "true", + matchIfMissing = true) +@ConditionalOnMissingBean(R2dbcRepositoryFactoryBean.class) +@Import(R2dbcRepositoriesAutoConfigureRegistrar.class) +@AutoConfigureAfter(R2dbcDataAutoConfiguration.class) +public class R2dbcRepositoriesAutoConfiguration { + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/r2dbc/R2dbcRepositoriesAutoConfigureRegistrar.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/r2dbc/R2dbcRepositoriesAutoConfigureRegistrar.java new file mode 100644 index 000000000000..955ee3d317e1 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/r2dbc/R2dbcRepositoriesAutoConfigureRegistrar.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.r2dbc; + +import java.lang.annotation.Annotation; + +import org.springframework.boot.autoconfigure.data.AbstractRepositoryConfigurationSourceSupport; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories; +import org.springframework.data.r2dbc.repository.config.R2dbcRepositoryConfigurationExtension; +import org.springframework.data.repository.config.RepositoryConfigurationExtension; + +/** + * {@link ImportBeanDefinitionRegistrar} used to auto-configure Spring Data R2DBC + * Repositories. + * + * @author Mark Paluch + */ +class R2dbcRepositoriesAutoConfigureRegistrar extends AbstractRepositoryConfigurationSourceSupport { + + @Override + protected Class getAnnotation() { + return EnableR2dbcRepositories.class; + } + + @Override + protected Class getConfiguration() { + return EnableR2dbcRepositoriesConfiguration.class; + } + + @Override + protected RepositoryConfigurationExtension getRepositoryConfigurationExtension() { + return new R2dbcRepositoryConfigurationExtension(); + } + + @EnableR2dbcRepositories + private static class EnableR2dbcRepositoriesConfiguration { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/r2dbc/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/r2dbc/package-info.java new file mode 100644 index 000000000000..7dd524cb2d3e --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/r2dbc/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-Configuration for Spring Data R2DBC. + */ +package org.springframework.boot.autoconfigure.data.r2dbc; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/JedisConnectionConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/JedisConnectionConfiguration.java index ff474975008d..94e5cb0126b7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/JedisConnectionConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/JedisConnectionConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,6 @@ package org.springframework.boot.autoconfigure.data.redis; -import java.net.UnknownHostException; -import java.time.Duration; - import org.apache.commons.pool2.impl.GenericObjectPool; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPoolConfig; @@ -26,6 +23,8 @@ import org.springframework.beans.factory.ObjectProvider; 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.context.properties.PropertyMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisClusterConfiguration; @@ -45,6 +44,8 @@ */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ GenericObjectPool.class, JedisConnection.class, Jedis.class }) +@ConditionalOnMissingBean(RedisConnectionFactory.class) +@ConditionalOnProperty(name = "spring.redis.client-type", havingValue = "jedis", matchIfMissing = true) class JedisConnectionConfiguration extends RedisConnectionConfiguration { JedisConnectionConfiguration(RedisProperties properties, @@ -54,9 +55,8 @@ class JedisConnectionConfiguration extends RedisConnectionConfiguration { } @Bean - @ConditionalOnMissingBean(RedisConnectionFactory.class) JedisConnectionFactory redisConnectionFactory( - ObjectProvider builderCustomizers) throws UnknownHostException { + ObjectProvider builderCustomizers) { return createJedisConnectionFactory(builderCustomizers); } @@ -87,16 +87,11 @@ private JedisClientConfiguration getJedisClientConfiguration( } private JedisClientConfigurationBuilder applyProperties(JedisClientConfigurationBuilder builder) { - if (getProperties().isSsl()) { - builder.useSsl(); - } - if (getProperties().getTimeout() != null) { - Duration timeout = getProperties().getTimeout(); - builder.readTimeout(timeout).connectTimeout(timeout); - } - if (StringUtils.hasText(getProperties().getClientName())) { - builder.clientName(getProperties().getClientName()); - } + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(getProperties().isSsl()).whenTrue().toCall(builder::useSsl); + map.from(getProperties().getTimeout()).to(builder::readTimeout); + map.from(getProperties().getConnectTimeout()).to(builder::connectTimeout); + map.from(getProperties().getClientName()).whenHasText().to(builder::clientName); return builder; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.java index d1c7a8bd4e78..e295f97f497d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,15 @@ package org.springframework.boot.autoconfigure.data.redis; -import java.net.UnknownHostException; +import java.time.Duration; +import io.lettuce.core.ClientOptions; import io.lettuce.core.RedisClient; +import io.lettuce.core.SocketOptions; +import io.lettuce.core.TimeoutOptions; +import io.lettuce.core.cluster.ClusterClientOptions; +import io.lettuce.core.cluster.ClusterTopologyRefreshOptions; +import io.lettuce.core.cluster.ClusterTopologyRefreshOptions.Builder; import io.lettuce.core.resource.ClientResources; import io.lettuce.core.resource.DefaultClientResources; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; @@ -26,6 +32,8 @@ import org.springframework.beans.factory.ObjectProvider; 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.data.redis.RedisProperties.Lettuce.Cluster.Refresh; import org.springframework.boot.autoconfigure.data.redis.RedisProperties.Pool; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -46,6 +54,7 @@ */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(RedisClient.class) +@ConditionalOnProperty(name = "spring.redis.client-type", havingValue = "lettuce", matchIfMissing = true) class LettuceConnectionConfiguration extends RedisConnectionConfiguration { LettuceConnectionConfiguration(RedisProperties properties, @@ -64,7 +73,7 @@ DefaultClientResources lettuceClientResources() { @ConditionalOnMissingBean(RedisConnectionFactory.class) LettuceConnectionFactory redisConnectionFactory( ObjectProvider builderCustomizers, - ClientResources clientResources) throws UnknownHostException { + ClientResources clientResources) { LettuceClientConfiguration clientConfig = getLettuceClientConfiguration(builderCustomizers, clientResources, getProperties().getLettuce().getPool()); return createLettuceConnectionFactory(clientConfig); @@ -88,6 +97,7 @@ private LettuceClientConfiguration getLettuceClientConfiguration( if (StringUtils.hasText(getProperties().getUrl())) { customizeConfigurationFromUrl(builder); } + builder.clientOptions(createClientOptions()); builder.clientResources(clientResources); builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); return builder.build(); @@ -120,6 +130,32 @@ private LettuceClientConfigurationBuilder applyProperties( return builder; } + private ClientOptions createClientOptions() { + ClientOptions.Builder builder = initializeClientOptionsBuilder(); + Duration connectTimeout = getProperties().getConnectTimeout(); + if (connectTimeout != null) { + builder.socketOptions(SocketOptions.builder().connectTimeout(connectTimeout).build()); + } + return builder.timeoutOptions(TimeoutOptions.enabled()).build(); + } + + private ClientOptions.Builder initializeClientOptionsBuilder() { + if (getProperties().getCluster() != null) { + ClusterClientOptions.Builder builder = ClusterClientOptions.builder(); + Refresh refreshProperties = getProperties().getLettuce().getCluster().getRefresh(); + Builder refreshBuilder = ClusterTopologyRefreshOptions.builder() + .dynamicRefreshSources(refreshProperties.isDynamicRefreshSources()); + if (refreshProperties.getPeriod() != null) { + refreshBuilder.enablePeriodicRefresh(refreshProperties.getPeriod()); + } + if (refreshProperties.isAdaptive()) { + refreshBuilder.enableAllAdaptiveRefreshTriggers(); + } + return builder.topologyRefreshOptions(refreshBuilder.build()); + } + return ClientOptions.builder(); + } + private void customizeConfigurationFromUrl(LettuceClientConfiguration.LettuceClientConfigurationBuilder builder) { ConnectionInfo connectionInfo = parseUrl(getProperties().getUrl()); if (connectionInfo.isUseSsl()) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfiguration.java index ab7d05f323e7..c4f5b06e9b8c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,10 @@ package org.springframework.boot.autoconfigure.data.redis; -import java.net.UnknownHostException; - import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -52,8 +51,8 @@ public class RedisAutoConfiguration { @Bean @ConditionalOnMissingBean(name = "redisTemplate") - public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) - throws UnknownHostException { + @ConditionalOnSingleCandidate(RedisConnectionFactory.class) + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); return template; @@ -61,8 +60,8 @@ public RedisTemplate redisTemplate(RedisConnectionFactory redisC @Bean @ConditionalOnMissingBean - public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) - throws UnknownHostException { + @ConditionalOnSingleCandidate(RedisConnectionFactory.class) + public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { StringRedisTemplate template = new StringRedisTemplate(); template.setConnectionFactory(redisConnectionFactory); return template; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisConnectionConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisConnectionConfiguration.java index 9d94019acf9b..15bb5b24154f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisConnectionConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisConnectionConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,7 @@ * @author Mark Paluch * @author Stephane Nicoll * @author Alen Turkovic + * @author Scott Frederick */ abstract class RedisConnectionConfiguration { @@ -59,11 +60,13 @@ protected final RedisStandaloneConfiguration getStandaloneConfig() { ConnectionInfo connectionInfo = parseUrl(this.properties.getUrl()); config.setHostName(connectionInfo.getHostName()); config.setPort(connectionInfo.getPort()); + config.setUsername(connectionInfo.getUsername()); config.setPassword(RedisPassword.of(connectionInfo.getPassword())); } else { config.setHostName(this.properties.getHost()); config.setPort(this.properties.getPort()); + config.setUsername(this.properties.getUsername()); config.setPassword(RedisPassword.of(this.properties.getPassword())); } config.setDatabase(this.properties.getDatabase()); @@ -79,9 +82,13 @@ protected final RedisSentinelConfiguration getSentinelConfig() { RedisSentinelConfiguration config = new RedisSentinelConfiguration(); config.master(sentinelProperties.getMaster()); config.setSentinels(createSentinels(sentinelProperties)); + config.setUsername(this.properties.getUsername()); if (this.properties.getPassword() != null) { config.setPassword(RedisPassword.of(this.properties.getPassword())); } + if (sentinelProperties.getPassword() != null) { + config.setSentinelPassword(RedisPassword.of(sentinelProperties.getPassword())); + } config.setDatabase(this.properties.getDatabase()); return config; } @@ -104,6 +111,7 @@ protected final RedisClusterConfiguration getClusterConfiguration() { if (clusterProperties.getMaxRedirects() != null) { config.setMaxRedirects(clusterProperties.getMaxRedirects()); } + config.setUsername(this.properties.getUsername()); if (this.properties.getPassword() != null) { config.setPassword(RedisPassword.of(this.properties.getPassword())); } @@ -120,7 +128,7 @@ private List createSentinels(RedisProperties.Sentinel sentinel) { try { String[] parts = StringUtils.split(node, ":"); Assert.state(parts.length == 2, "Must be defined as 'host:port'"); - nodes.add(new RedisNode(parts[0], Integer.valueOf(parts[1]))); + nodes.add(new RedisNode(parts[0], Integer.parseInt(parts[1]))); } catch (RuntimeException ex) { throw new IllegalStateException("Invalid redis sentinel property '" + node + "'", ex); @@ -132,19 +140,28 @@ private List createSentinels(RedisProperties.Sentinel sentinel) { protected ConnectionInfo parseUrl(String url) { try { URI uri = new URI(url); - boolean useSsl = (url.startsWith("rediss://")); + String scheme = uri.getScheme(); + if (!"redis".equals(scheme) && !"rediss".equals(scheme)) { + throw new RedisUrlSyntaxException(url); + } + boolean useSsl = ("rediss".equals(scheme)); + String username = null; String password = null; if (uri.getUserInfo() != null) { - password = uri.getUserInfo(); - int index = password.indexOf(':'); + String candidate = uri.getUserInfo(); + int index = candidate.indexOf(':'); if (index >= 0) { - password = password.substring(index + 1); + username = candidate.substring(0, index); + password = candidate.substring(index + 1); + } + else { + password = candidate; } } - return new ConnectionInfo(uri, useSsl, password); + return new ConnectionInfo(uri, useSsl, username, password); } catch (URISyntaxException ex) { - throw new IllegalArgumentException("Malformed url '" + url + "'", ex); + throw new RedisUrlSyntaxException(url, ex); } } @@ -154,11 +171,14 @@ static class ConnectionInfo { private final boolean useSsl; + private final String username; + private final String password; - ConnectionInfo(URI uri, boolean useSsl, String password) { + ConnectionInfo(URI uri, boolean useSsl, String username, String password) { this.uri = uri; this.useSsl = useSsl; + this.username = username; this.password = password; } @@ -174,6 +194,10 @@ int getPort() { return this.uri.getPort(); } + String getUsername() { + return this.username; + } + String getPassword() { return this.password; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisProperties.java index b8e6cd541859..0dfb6474d84d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,6 +51,11 @@ public class RedisProperties { */ private String host = "localhost"; + /** + * Login username of the redis server. + */ + private String username; + /** * Login password of the redis server. */ @@ -67,15 +72,25 @@ public class RedisProperties { private boolean ssl; /** - * Connection timeout. + * Read timeout. */ private Duration timeout; + /** + * Connection timeout. + */ + private Duration connectTimeout; + /** * Client name to be set on connections with CLIENT SETNAME. */ private String clientName; + /** + * Type of client to use. By default, auto-detected according to the classpath. + */ + private ClientType clientType; + private Sentinel sentinel; private Cluster cluster; @@ -108,6 +123,14 @@ public void setHost(String host) { this.host = host; } + public String getUsername() { + return this.username; + } + + public void setUsername(String username) { + this.username = username; + } + public String getPassword() { return this.password; } @@ -140,6 +163,14 @@ public Duration getTimeout() { return this.timeout; } + public Duration getConnectTimeout() { + return this.connectTimeout; + } + + public void setConnectTimeout(Duration connectTimeout) { + this.connectTimeout = connectTimeout; + } + public String getClientName() { return this.clientName; } @@ -148,6 +179,14 @@ public void setClientName(String clientName) { this.clientName = clientName; } + public ClientType getClientType() { + return this.clientType; + } + + public void setClientType(ClientType clientType) { + this.clientType = clientType; + } + public Sentinel getSentinel() { return this.sentinel; } @@ -172,6 +211,23 @@ public Lettuce getLettuce() { return this.lettuce; } + /** + * Type of Redis client to use. + */ + public enum ClientType { + + /** + * Use the Lettuce redis client. + */ + LETTUCE, + + /** + * Use the Jedis redis client. + */ + JEDIS + + } + /** * Pool properties. */ @@ -301,6 +357,11 @@ public static class Sentinel { */ private List nodes; + /** + * Password for authenticating with sentinel(s). + */ + private String password; + public String getMaster() { return this.master; } @@ -317,6 +378,14 @@ public void setNodes(List nodes) { this.nodes = nodes; } + public String getPassword() { + return this.password; + } + + public void setPassword(String password) { + this.password = password; + } + } /** @@ -354,6 +423,8 @@ public static class Lettuce { */ private Pool pool; + private final Cluster cluster = new Cluster(); + public Duration getShutdownTimeout() { return this.shutdownTimeout; } @@ -370,6 +441,66 @@ public void setPool(Pool pool) { this.pool = pool; } + public Cluster getCluster() { + return this.cluster; + } + + public static class Cluster { + + private final Refresh refresh = new Refresh(); + + public Refresh getRefresh() { + return this.refresh; + } + + public static class Refresh { + + /** + * Whether to discover and query all cluster nodes for obtaining the + * cluster topology. When set to false, only the initial seed nodes are + * used as sources for topology discovery. + */ + private boolean dynamicRefreshSources = true; + + /** + * Cluster topology refresh period. + */ + private Duration period; + + /** + * Whether adaptive topology refreshing using all available refresh + * triggers should be used. + */ + private boolean adaptive; + + public boolean isDynamicRefreshSources() { + return this.dynamicRefreshSources; + } + + public void setDynamicRefreshSources(boolean dynamicRefreshSources) { + this.dynamicRefreshSources = dynamicRefreshSources; + } + + public Duration getPeriod() { + return this.period; + } + + public void setPeriod(Duration period) { + this.period = period; + } + + public boolean isAdaptive() { + return this.adaptive; + } + + public void setAdaptive(boolean adaptive) { + this.adaptive = adaptive; + } + + } + + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisRepositoriesAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisRepositoriesAutoConfiguration.java index 2faee920edf0..f106ac7f204c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisRepositoriesAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisRepositoriesAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,8 +34,8 @@ * * @author Eddú Meléndez * @author Stephane Nicoll - * @see EnableRedisRepositories * @since 1.4.0 + * @see EnableRedisRepositories */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(EnableRedisRepositories.class) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisUrlSyntaxException.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisUrlSyntaxException.java new file mode 100644 index 000000000000..d9087678fa9b --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisUrlSyntaxException.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.redis; + +/** + * Exception thrown when a Redis URL is malformed or invalid. + * + * @author Scott Frederick + */ +class RedisUrlSyntaxException extends RuntimeException { + + private final String url; + + RedisUrlSyntaxException(String url, Exception cause) { + super(buildMessage(url), cause); + this.url = url; + } + + RedisUrlSyntaxException(String url) { + super(buildMessage(url)); + this.url = url; + } + + String getUrl() { + return this.url; + } + + private static String buildMessage(String url) { + return "Invalid Redis URL '" + url + "'"; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisUrlSyntaxFailureAnalyzer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisUrlSyntaxFailureAnalyzer.java new file mode 100644 index 000000000000..4550c462d295 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisUrlSyntaxFailureAnalyzer.java @@ -0,0 +1,68 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.redis; + +import java.net.URI; +import java.net.URISyntaxException; + +import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; +import org.springframework.boot.diagnostics.FailureAnalysis; + +/** + * A {@code FailureAnalyzer} that performs analysis of failures caused by a + * {@link RedisUrlSyntaxException}. + * + * @author Scott Frederick + */ +class RedisUrlSyntaxFailureAnalyzer extends AbstractFailureAnalyzer { + + @Override + protected FailureAnalysis analyze(Throwable rootFailure, RedisUrlSyntaxException cause) { + try { + URI uri = new URI(cause.getUrl()); + if ("redis-sentinel".equals(uri.getScheme())) { + return new FailureAnalysis(getUnsupportedSchemeDescription(cause.getUrl(), uri.getScheme()), + "Use spring.redis.sentinel properties instead of spring.redis.url to configure Redis sentinel addresses.", + cause); + } + if ("redis-socket".equals(uri.getScheme())) { + return new FailureAnalysis(getUnsupportedSchemeDescription(cause.getUrl(), uri.getScheme()), + "Configure the appropriate Spring Data Redis connection beans directly instead of setting the property 'spring.redis.url'.", + cause); + } + if (!"redis".equals(uri.getScheme()) && !"rediss".equals(uri.getScheme())) { + return new FailureAnalysis(getUnsupportedSchemeDescription(cause.getUrl(), uri.getScheme()), + "Use the scheme 'redis://' for insecure or 'rediss://' for secure Redis standalone configuration.", + cause); + } + } + catch (URISyntaxException ex) { + // fall through to default description and action + } + return new FailureAnalysis(getDefaultDescription(cause.getUrl()), + "Review the value of the property 'spring.redis.url'.", cause); + } + + private String getDefaultDescription(String url) { + return "The URL '" + url + "' is not valid for configuring Spring Data Redis. "; + } + + private String getUnsupportedSchemeDescription(String url, String scheme) { + return getDefaultDescription(url) + "The scheme '" + scheme + "' is not supported."; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/rest/RepositoryRestMvcAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/rest/RepositoryRestMvcAutoConfiguration.java index 1383c41d3184..0b3d3483c724 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/rest/RepositoryRestMvcAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/rest/RepositoryRestMvcAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.boot.autoconfigure.data.rest; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -30,6 +31,7 @@ import org.springframework.context.annotation.Import; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; /** * {@link EnableAutoConfiguration Auto-configuration} for Spring Data Rest's MVC @@ -56,8 +58,9 @@ public class RepositoryRestMvcAutoConfiguration { @Bean - public SpringBootRepositoryRestConfigurer springBootRepositoryRestConfigurer() { - return new SpringBootRepositoryRestConfigurer(); + public SpringBootRepositoryRestConfigurer springBootRepositoryRestConfigurer( + ObjectProvider objectMapperBuilder, RepositoryRestProperties properties) { + return new SpringBootRepositoryRestConfigurer(objectMapperBuilder.getIfAvailable(), properties); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/rest/SpringBootRepositoryRestConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/rest/SpringBootRepositoryRestConfigurer.java index 7c6a1f30e1f0..647ae121bd4f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/rest/SpringBootRepositoryRestConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/rest/SpringBootRepositoryRestConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,11 +18,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.Order; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.web.servlet.config.annotation.CorsRegistry; /** * A {@code RepositoryRestConfigurer} that applies configuration items from the @@ -36,14 +36,18 @@ @Order(0) class SpringBootRepositoryRestConfigurer implements RepositoryRestConfigurer { - @Autowired(required = false) - private Jackson2ObjectMapperBuilder objectMapperBuilder; + private final Jackson2ObjectMapperBuilder objectMapperBuilder; - @Autowired - private RepositoryRestProperties properties; + private final RepositoryRestProperties properties; + + SpringBootRepositoryRestConfigurer(Jackson2ObjectMapperBuilder objectMapperBuilder, + RepositoryRestProperties properties) { + this.objectMapperBuilder = objectMapperBuilder; + this.properties = properties; + } @Override - public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) { + public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config, CorsRegistry cors) { this.properties.applyTo(config); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/solr/SolrRepositoriesAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/solr/SolrRepositoriesAutoConfiguration.java deleted file mode 100644 index d90b176f4f86..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/solr/SolrRepositoriesAutoConfiguration.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.data.solr; - -import org.apache.solr.client.solrj.SolrClient; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.data.solr.repository.SolrRepository; -import org.springframework.data.solr.repository.config.EnableSolrRepositories; -import org.springframework.data.solr.repository.config.SolrRepositoryConfigExtension; -import org.springframework.data.solr.repository.support.SolrRepositoryFactoryBean; - -/** - * Enables auto configuration for Spring Data Solr repositories. - *

- * Activates when there is no bean of type {@link SolrRepositoryFactoryBean} found in - * context, and both {@link SolrRepository} and {@link SolrClient} can be found on - * classpath. - *

- * If active auto configuration does the same as - * {@link EnableSolrRepositories @EnableSolrRepositories} would do. - * - * @author Christoph Strobl - * @author Oliver Gierke - * @since 1.1.0 - */ -@Configuration(proxyBeanMethods = false) -@ConditionalOnClass({ SolrClient.class, SolrRepository.class }) -@ConditionalOnMissingBean({ SolrRepositoryFactoryBean.class, SolrRepositoryConfigExtension.class }) -@ConditionalOnProperty(prefix = "spring.data.solr.repositories", name = "enabled", havingValue = "true", - matchIfMissing = true) -@Import(SolrRepositoriesRegistrar.class) -public class SolrRepositoriesAutoConfiguration { - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/solr/SolrRepositoriesRegistrar.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/solr/SolrRepositoriesRegistrar.java deleted file mode 100644 index b5f5cda2d9df..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/solr/SolrRepositoriesRegistrar.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.data.solr; - -import java.lang.annotation.Annotation; - -import org.springframework.boot.autoconfigure.data.AbstractRepositoryConfigurationSourceSupport; -import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; -import org.springframework.data.repository.config.RepositoryConfigurationExtension; -import org.springframework.data.solr.repository.config.EnableSolrRepositories; -import org.springframework.data.solr.repository.config.SolrRepositoryConfigExtension; - -/** - * {@link ImportBeanDefinitionRegistrar} used to auto-configure Spring Data Solr - * repositories. - * - * @author Christoph Strobl - */ -class SolrRepositoriesRegistrar extends AbstractRepositoryConfigurationSourceSupport { - - @Override - protected Class getAnnotation() { - return EnableSolrRepositories.class; - } - - @Override - protected Class getConfiguration() { - return EnableSolrRepositoriesConfiguration.class; - } - - @Override - protected RepositoryConfigurationExtension getRepositoryConfigurationExtension() { - return new SolrRepositoryConfigExtension(); - } - - @EnableSolrRepositories - private static class EnableSolrRepositoriesConfiguration { - - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/solr/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/solr/package-info.java deleted file mode 100644 index 9086a52e631b..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/solr/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Auto-configuration for Spring Data SOLR. - */ -package org.springframework.boot.autoconfigure.data.solr; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/diagnostics/analyzer/NoSuchBeanDefinitionFailureAnalyzer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/diagnostics/analyzer/NoSuchBeanDefinitionFailureAnalyzer.java index b3a4eb069e07..f33880417c7d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/diagnostics/analyzer/NoSuchBeanDefinitionFailureAnalyzer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/diagnostics/analyzer/NoSuchBeanDefinitionFailureAnalyzer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,6 +46,7 @@ import org.springframework.boot.diagnostics.FailureAnalysis; import org.springframework.boot.diagnostics.analyzer.AbstractInjectionFailureAnalyzer; import org.springframework.context.annotation.Bean; +import org.springframework.core.KotlinDetector; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; @@ -120,10 +121,19 @@ protected FailureAnalysis analyze(Throwable rootFailure, NoSuchBeanDefinitionExc MergedAnnotation configurationProperties = MergedAnnotations.from(declaringClass) .get(ConfigurationProperties.class); if (configurationProperties.isPresent()) { - action = String.format( - "%s%nConsider adding @%s to %s if you intended to use constructor-based " - + "configuration property binding.", - action, ConstructorBinding.class.getSimpleName(), constructor.getName()); + if (KotlinDetector.isKotlinType(declaringClass) && !KotlinDetector.isKotlinReflectPresent()) { + action = String.format( + "%s%nConsider adding a dependency on kotlin-reflect so that the constructor used for @%s can be located. Also, ensure that @%s is present on '%s' if you intended to use constructor-based " + + "configuration property binding.", + action, ConstructorBinding.class.getSimpleName(), ConstructorBinding.class.getSimpleName(), + constructor.getName()); + } + else { + action = String.format( + "%s%nConsider adding @%s to %s if you intended to use constructor-based " + + "configuration property binding.", + action, ConstructorBinding.class.getSimpleName(), constructor.getName()); + } } } return new FailureAnalysis(message.toString(), action, cause); @@ -212,7 +222,7 @@ private InjectionPoint findInjectionPoint(Throwable failure) { return unsatisfiedDependencyException.getInjectionPoint(); } - private class Source { + private static class Source { private final String className; @@ -307,7 +317,7 @@ public Iterator iterator() { } - private class AutoConfigurationResult { + private static class AutoConfigurationResult { private final MethodMetadata methodMetadata; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/domain/EntityScan.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/domain/EntityScan.java index c8fc7da791c0..5b9b47c3b5cc 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/domain/EntityScan.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/domain/EntityScan.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,12 +34,11 @@ *
  • Set the * {@link org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean#setPackagesToScan(String...) * packages scanned} for JPA entities.
  • - *
  • Set the packages used with Neo4J's {@link org.neo4j.ogm.session.SessionFactory - * SessionFactory}.
  • *
  • Set the * {@link org.springframework.data.mapping.context.AbstractMappingContext#setInitialEntitySet(java.util.Set) * initial entity set} used with Spring Data * {@link org.springframework.data.mongodb.core.mapping.MongoMappingContext MongoDB}, + * {@link org.springframework.data.neo4j.core.mapping.Neo4jMappingContext Neo4j}, * {@link org.springframework.data.cassandra.core.mapping.CassandraMappingContext * Cassandra} and * {@link org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/domain/EntityScanPackages.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/domain/EntityScanPackages.java index 2c88e5622002..185a90fd57dd 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/domain/EntityScanPackages.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/domain/EntityScanPackages.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,15 +23,16 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import java.util.function.Supplier; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.config.ConstructorArgumentValues; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.env.Environment; import org.springframework.core.type.AnnotationMetadata; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -109,35 +110,27 @@ public static void register(BeanDefinitionRegistry registry, Collection Assert.notNull(registry, "Registry must not be null"); Assert.notNull(packageNames, "PackageNames must not be null"); if (registry.containsBeanDefinition(BEAN)) { - BeanDefinition beanDefinition = registry.getBeanDefinition(BEAN); - ConstructorArgumentValues constructorArguments = beanDefinition.getConstructorArgumentValues(); - constructorArguments.addIndexedArgumentValue(0, addPackageNames(constructorArguments, packageNames)); + EntityScanPackagesBeanDefinition beanDefinition = (EntityScanPackagesBeanDefinition) registry + .getBeanDefinition(BEAN); + beanDefinition.addPackageNames(packageNames); } else { - GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); - beanDefinition.setBeanClass(EntityScanPackages.class); - beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, - StringUtils.toStringArray(packageNames)); - beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - registry.registerBeanDefinition(BEAN, beanDefinition); + registry.registerBeanDefinition(BEAN, new EntityScanPackagesBeanDefinition(packageNames)); } } - private static String[] addPackageNames(ConstructorArgumentValues constructorArguments, - Collection packageNames) { - String[] existing = (String[]) constructorArguments.getIndexedArgumentValue(0, String[].class).getValue(); - Set merged = new LinkedHashSet<>(); - merged.addAll(Arrays.asList(existing)); - merged.addAll(packageNames); - return StringUtils.toStringArray(merged); - } - /** * {@link ImportBeanDefinitionRegistrar} to store the base package from the importing * configuration. */ static class Registrar implements ImportBeanDefinitionRegistrar { + private final Environment environment; + + Registrar(Environment environment) { + this.environment = environment; + } + @Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { register(registry, getPackagesToScan(metadata)); @@ -146,20 +139,46 @@ public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionR private Set getPackagesToScan(AnnotationMetadata metadata) { AnnotationAttributes attributes = AnnotationAttributes .fromMap(metadata.getAnnotationAttributes(EntityScan.class.getName())); - String[] basePackages = attributes.getStringArray("basePackages"); - Class[] basePackageClasses = attributes.getClassArray("basePackageClasses"); - Set packagesToScan = new LinkedHashSet<>(Arrays.asList(basePackages)); - for (Class basePackageClass : basePackageClasses) { - packagesToScan.add(ClassUtils.getPackageName(basePackageClass)); + Set packagesToScan = new LinkedHashSet<>(); + for (String basePackage : attributes.getStringArray("basePackages")) { + addResolvedPackage(basePackage, packagesToScan); + } + for (Class basePackageClass : attributes.getClassArray("basePackageClasses")) { + addResolvedPackage(ClassUtils.getPackageName(basePackageClass), packagesToScan); } if (packagesToScan.isEmpty()) { String packageName = ClassUtils.getPackageName(metadata.getClassName()); - Assert.state(!StringUtils.isEmpty(packageName), "@EntityScan cannot be used with the default package"); + Assert.state(StringUtils.hasLength(packageName), "@EntityScan cannot be used with the default package"); return Collections.singleton(packageName); } return packagesToScan; } + private void addResolvedPackage(String packageName, Set packagesToScan) { + packagesToScan.add(this.environment.resolvePlaceholders(packageName)); + } + + } + + static class EntityScanPackagesBeanDefinition extends GenericBeanDefinition { + + private final Set packageNames = new LinkedHashSet<>(); + + EntityScanPackagesBeanDefinition(Collection packageNames) { + setBeanClass(EntityScanPackages.class); + setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + addPackageNames(packageNames); + } + + @Override + public Supplier getInstanceSupplier() { + return () -> new EntityScanPackages(StringUtils.toStringArray(this.packageNames)); + } + + private void addPackageNames(Collection additionalPackageNames) { + this.packageNames.addAll(additionalPackageNames); + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/domain/EntityScanner.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/domain/EntityScanner.java index 99bab3b6a6e6..4fb4efd0a9d3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/domain/EntityScanner.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/domain/EntityScanner.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -63,9 +63,8 @@ public final Set> scan(Class... annotationTypes) if (packages.isEmpty()) { return Collections.emptySet(); } - ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false); - scanner.setEnvironment(this.context.getEnvironment()); - scanner.setResourceLoader(this.context); + ClassPathScanningCandidateComponentProvider scanner = createClassPathScanningCandidateComponentProvider( + this.context); for (Class annotationType : annotationTypes) { scanner.addIncludeFilter(new AnnotationTypeFilter(annotationType)); } @@ -80,6 +79,22 @@ public final Set> scan(Class... annotationTypes) return entitySet; } + /** + * Create a {@link ClassPathScanningCandidateComponentProvider} to scan entities based + * on the specified {@link ApplicationContext}. + * @param context the {@link ApplicationContext} to use + * @return a {@link ClassPathScanningCandidateComponentProvider} suitable to scan + * entities + * @since 2.4.0 + */ + protected ClassPathScanningCandidateComponentProvider createClassPathScanningCandidateComponentProvider( + ApplicationContext context) { + ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false); + scanner.setEnvironment(context.getEnvironment()); + scanner.setResourceLoader(context); + return scanner; + } + private List getPackages() { List packages = EntityScanPackages.get(this.context).getPackageNames(); if (packages.isEmpty() && AutoConfigurationPackages.has(this.context)) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfiguration.java new file mode 100644 index 000000000000..9894fd5890c0 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfiguration.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.elasticsearch; + +import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestHighLevelClient; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientConfigurations.RestClientBuilderConfiguration; +import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientConfigurations.RestClientSnifferConfiguration; +import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientConfigurations.RestHighLevelClientConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Elasticsearch REST clients. + * + * @author Brian Clozel + * @author Stephane Nicoll + * @since 2.1.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(RestHighLevelClient.class) +@ConditionalOnMissingBean(RestClient.class) +@EnableConfigurationProperties(ElasticsearchRestClientProperties.class) +@Import({ RestClientBuilderConfiguration.class, RestHighLevelClientConfiguration.class, + RestClientSnifferConfiguration.class }) +public class ElasticsearchRestClientAutoConfiguration { + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientConfigurations.java new file mode 100644 index 000000000000..33faaa3ff038 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientConfigurations.java @@ -0,0 +1,204 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.elasticsearch; + +import java.net.URI; +import java.net.URISyntaxException; +import java.time.Duration; + +import org.apache.http.HttpHost; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.Credentials; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.nio.client.HttpAsyncClientBuilder; +import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestClientBuilder; +import org.elasticsearch.client.RestHighLevelClient; +import org.elasticsearch.client.sniff.Sniffer; +import org.elasticsearch.client.sniff.SnifferBuilder; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.StringUtils; + +/** + * Elasticsearch rest client configurations. + * + * @author Stephane Nicoll + */ +class ElasticsearchRestClientConfigurations { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(RestClientBuilder.class) + static class RestClientBuilderConfiguration { + + @Bean + RestClientBuilderCustomizer defaultRestClientBuilderCustomizer(ElasticsearchRestClientProperties properties) { + return new DefaultRestClientBuilderCustomizer(properties); + } + + @Bean + RestClientBuilder elasticsearchRestClientBuilder(ElasticsearchRestClientProperties properties, + ObjectProvider builderCustomizers) { + HttpHost[] hosts = properties.getUris().stream().map(this::createHttpHost).toArray(HttpHost[]::new); + RestClientBuilder builder = RestClient.builder(hosts); + builder.setHttpClientConfigCallback((httpClientBuilder) -> { + builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(httpClientBuilder)); + return httpClientBuilder; + }); + builder.setRequestConfigCallback((requestConfigBuilder) -> { + builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(requestConfigBuilder)); + return requestConfigBuilder; + }); + builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); + return builder; + } + + private HttpHost createHttpHost(String uri) { + try { + return createHttpHost(URI.create(uri)); + } + catch (IllegalArgumentException ex) { + return HttpHost.create(uri); + } + } + + private HttpHost createHttpHost(URI uri) { + if (!StringUtils.hasLength(uri.getUserInfo())) { + return HttpHost.create(uri.toString()); + } + try { + return HttpHost.create(new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), uri.getPath(), + uri.getQuery(), uri.getFragment()).toString()); + } + catch (URISyntaxException ex) { + throw new IllegalStateException(ex); + } + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(RestHighLevelClient.class) + static class RestHighLevelClientConfiguration { + + @Bean + RestHighLevelClient elasticsearchRestHighLevelClient(RestClientBuilder restClientBuilder) { + return new RestHighLevelClient(restClientBuilder); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(Sniffer.class) + @ConditionalOnSingleCandidate(RestHighLevelClient.class) + static class RestClientSnifferConfiguration { + + @Bean + @ConditionalOnMissingBean + Sniffer elasticsearchSniffer(RestHighLevelClient client, ElasticsearchRestClientProperties properties) { + SnifferBuilder builder = Sniffer.builder(client.getLowLevelClient()); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(properties.getSniffer().getInterval()).asInt(Duration::toMillis) + .to(builder::setSniffIntervalMillis); + map.from(properties.getSniffer().getDelayAfterFailure()).asInt(Duration::toMillis) + .to(builder::setSniffAfterFailureDelayMillis); + return builder.build(); + } + + } + + static class DefaultRestClientBuilderCustomizer implements RestClientBuilderCustomizer { + + private static final PropertyMapper map = PropertyMapper.get(); + + private final ElasticsearchRestClientProperties properties; + + DefaultRestClientBuilderCustomizer(ElasticsearchRestClientProperties properties) { + this.properties = properties; + } + + @Override + public void customize(RestClientBuilder builder) { + } + + @Override + public void customize(HttpAsyncClientBuilder builder) { + builder.setDefaultCredentialsProvider(new PropertiesCredentialsProvider(this.properties)); + } + + @Override + public void customize(RequestConfig.Builder builder) { + map.from(this.properties::getConnectionTimeout).whenNonNull().asInt(Duration::toMillis) + .to(builder::setConnectTimeout); + map.from(this.properties::getReadTimeout).whenNonNull().asInt(Duration::toMillis) + .to(builder::setSocketTimeout); + } + + } + + private static class PropertiesCredentialsProvider extends BasicCredentialsProvider { + + PropertiesCredentialsProvider(ElasticsearchRestClientProperties properties) { + if (StringUtils.hasText(properties.getUsername())) { + Credentials credentials = new UsernamePasswordCredentials(properties.getUsername(), + properties.getPassword()); + setCredentials(AuthScope.ANY, credentials); + } + properties.getUris().stream().map(this::toUri).filter(this::hasUserInfo) + .forEach(this::addUserInfoCredentials); + } + + private URI toUri(String uri) { + try { + return URI.create(uri); + } + catch (IllegalArgumentException ex) { + return null; + } + } + + private boolean hasUserInfo(URI uri) { + return uri != null && StringUtils.hasLength(uri.getUserInfo()); + } + + private void addUserInfoCredentials(URI uri) { + AuthScope authScope = new AuthScope(uri.getHost(), uri.getPort()); + Credentials credentials = createUserInfoCredentials(uri.getUserInfo()); + setCredentials(authScope, credentials); + } + + private Credentials createUserInfoCredentials(String userInfo) { + int delimiter = userInfo.indexOf(":"); + if (delimiter == -1) { + return new UsernamePasswordCredentials(userInfo, null); + } + String username = userInfo.substring(0, delimiter); + String password = userInfo.substring(delimiter + 1); + return new UsernamePasswordCredentials(username, password); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientProperties.java new file mode 100644 index 000000000000..649246d42bbd --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientProperties.java @@ -0,0 +1,136 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.elasticsearch; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties for Elasticsearch REST clients. + * + * @author Brian Clozel + * @since 2.1.0 + */ +@ConfigurationProperties(prefix = "spring.elasticsearch.rest") +public class ElasticsearchRestClientProperties { + + /** + * Comma-separated list of the Elasticsearch instances to use. + */ + private List uris = new ArrayList<>(Collections.singletonList("http://localhost:9200")); + + /** + * Credentials username. + */ + private String username; + + /** + * Credentials password. + */ + private String password; + + /** + * Connection timeout. + */ + private Duration connectionTimeout = Duration.ofSeconds(1); + + /** + * Read timeout. + */ + private Duration readTimeout = Duration.ofSeconds(30); + + private final Sniffer sniffer = new Sniffer(); + + public List getUris() { + return this.uris; + } + + public void setUris(List uris) { + this.uris = uris; + } + + public String getUsername() { + return this.username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return this.password; + } + + public void setPassword(String password) { + this.password = password; + } + + public Duration getConnectionTimeout() { + return this.connectionTimeout; + } + + public void setConnectionTimeout(Duration connectionTimeout) { + this.connectionTimeout = connectionTimeout; + } + + public Duration getReadTimeout() { + return this.readTimeout; + } + + public void setReadTimeout(Duration readTimeout) { + this.readTimeout = readTimeout; + } + + public Sniffer getSniffer() { + return this.sniffer; + } + + public static class Sniffer { + + /** + * Interval between consecutive ordinary sniff executions. + */ + private Duration interval = Duration.ofMinutes(5); + + /** + * Delay of a sniff execution scheduled after a failure. + */ + private Duration delayAfterFailure = Duration.ofMinutes(1); + + public Duration getInterval() { + return this.interval; + } + + public void setInterval(Duration interval) { + this.interval = interval; + } + + public Duration getDelayAfterFailure() { + return this.delayAfterFailure; + } + + public void setDelayAfterFailure(Duration delayAfterFailure) { + this.delayAfterFailure = delayAfterFailure; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/RestClientBuilderCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/RestClientBuilderCustomizer.java new file mode 100644 index 000000000000..685929d908c7 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/RestClientBuilderCustomizer.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.elasticsearch; + +import org.apache.http.client.config.RequestConfig; +import org.apache.http.impl.nio.client.HttpAsyncClientBuilder; +import org.elasticsearch.client.RestClientBuilder; + +/** + * Callback interface that can be implemented by beans wishing to further customize the + * {@link org.elasticsearch.client.RestClient} via a {@link RestClientBuilder} whilst + * retaining default auto-configuration. + * + * @author Brian Clozel + * @author Vedran Pavic + * @since 2.1.0 + */ +@FunctionalInterface +public interface RestClientBuilderCustomizer { + + /** + * Customize the {@link RestClientBuilder}. + *

    + * Possibly overrides customizations made with the {@code "spring.elasticsearch.rest"} + * configuration properties namespace. For more targeted changes, see + * {@link #customize(HttpAsyncClientBuilder)} and + * {@link #customize(RequestConfig.Builder)}. + * @param builder the builder to customize + */ + void customize(RestClientBuilder builder); + + /** + * Customize the {@link HttpAsyncClientBuilder}. + * @param builder the builder + * @since 2.3.0 + */ + default void customize(HttpAsyncClientBuilder builder) { + } + + /** + * Customize the {@link RequestConfig.Builder}. + * @param builder the builder + * @since 2.3.0 + */ + default void customize(RequestConfig.Builder builder) { + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/jest/HttpClientConfigBuilderCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/jest/HttpClientConfigBuilderCustomizer.java deleted file mode 100644 index d7becfc262ca..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/jest/HttpClientConfigBuilderCustomizer.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.elasticsearch.jest; - -import io.searchbox.client.config.HttpClientConfig; -import io.searchbox.client.config.HttpClientConfig.Builder; - -/** - * Callback interface that can be implemented by beans wishing to further customize the - * {@link HttpClientConfig} via a {@link Builder HttpClientConfig.Builder} whilst - * retaining default auto-configuration. - * - * @author Stephane Nicoll - * @since 1.5.0 - */ -@FunctionalInterface -public interface HttpClientConfigBuilderCustomizer { - - /** - * Customize the {@link Builder}. - * @param builder the builder to customize - */ - void customize(Builder builder); - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/jest/JestAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/jest/JestAutoConfiguration.java deleted file mode 100644 index 5f72f32e46f0..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/jest/JestAutoConfiguration.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.elasticsearch.jest; - -import java.time.Duration; - -import com.google.gson.Gson; -import io.searchbox.client.JestClient; -import io.searchbox.client.JestClientFactory; -import io.searchbox.client.config.HttpClientConfig; -import org.apache.http.HttpHost; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.elasticsearch.jest.JestProperties.Proxy; -import org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.context.properties.PropertyMapper; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.util.Assert; - -/** - * {@link EnableAutoConfiguration Auto-configuration} for Jest. - * - * @author Stephane Nicoll - * @since 1.4.0 - * @deprecated since 2.2.0 in favor of other auto-configured Elasticsearch clients - */ -@Configuration(proxyBeanMethods = false) -@ConditionalOnClass(JestClient.class) -@EnableConfigurationProperties(JestProperties.class) -@AutoConfigureAfter(GsonAutoConfiguration.class) -@Deprecated -public class JestAutoConfiguration { - - @Bean(destroyMethod = "shutdownClient") - @ConditionalOnMissingBean - public JestClient jestClient(JestProperties properties, ObjectProvider gson, - ObjectProvider builderCustomizers) { - JestClientFactory factory = new JestClientFactory(); - factory.setHttpClientConfig(createHttpClientConfig(properties, gson, builderCustomizers)); - return factory.getObject(); - } - - protected HttpClientConfig createHttpClientConfig(JestProperties properties, ObjectProvider gson, - ObjectProvider builderCustomizers) { - HttpClientConfig.Builder builder = new HttpClientConfig.Builder(properties.getUris()); - PropertyMapper map = PropertyMapper.get(); - map.from(properties::getUsername).whenHasText() - .to((username) -> builder.defaultCredentials(username, properties.getPassword())); - Proxy proxy = properties.getProxy(); - map.from(proxy::getHost).whenHasText().to((host) -> { - Assert.notNull(proxy.getPort(), "Proxy port must not be null"); - builder.proxy(new HttpHost(host, proxy.getPort())); - }); - map.from(gson::getIfUnique).whenNonNull().to(builder::gson); - map.from(properties::isMultiThreaded).to(builder::multiThreaded); - map.from(properties::getConnectionTimeout).whenNonNull().asInt(Duration::toMillis).to(builder::connTimeout); - map.from(properties::getReadTimeout).whenNonNull().asInt(Duration::toMillis).to(builder::readTimeout); - builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); - return builder.build(); - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/jest/JestProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/jest/JestProperties.java deleted file mode 100644 index d2d6facf8e2b..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/jest/JestProperties.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.elasticsearch.jest; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * Configuration properties for Jest. - * - * @author Stephane Nicoll - * @since 1.4.0 - */ -@ConfigurationProperties(prefix = "spring.elasticsearch.jest") -public class JestProperties { - - /** - * Comma-separated list of the Elasticsearch instances to use. - */ - private List uris = new ArrayList<>(Collections.singletonList("http://localhost:9200")); - - /** - * Login username. - */ - private String username; - - /** - * Login password. - */ - private String password; - - /** - * Whether to enable connection requests from multiple execution threads. - */ - private boolean multiThreaded = true; - - /** - * Connection timeout. - */ - private Duration connectionTimeout = Duration.ofSeconds(3); - - /** - * Read timeout. - */ - private Duration readTimeout = Duration.ofSeconds(3); - - /** - * Proxy settings. - */ - private final Proxy proxy = new Proxy(); - - public List getUris() { - return this.uris; - } - - public void setUris(List uris) { - this.uris = uris; - } - - public String getUsername() { - return this.username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getPassword() { - return this.password; - } - - public void setPassword(String password) { - this.password = password; - } - - public boolean isMultiThreaded() { - return this.multiThreaded; - } - - public void setMultiThreaded(boolean multiThreaded) { - this.multiThreaded = multiThreaded; - } - - public Duration getConnectionTimeout() { - return this.connectionTimeout; - } - - public void setConnectionTimeout(Duration connectionTimeout) { - this.connectionTimeout = connectionTimeout; - } - - public Duration getReadTimeout() { - return this.readTimeout; - } - - public void setReadTimeout(Duration readTimeout) { - this.readTimeout = readTimeout; - } - - public Proxy getProxy() { - return this.proxy; - } - - public static class Proxy { - - /** - * Proxy host the HTTP client should use. - */ - private String host; - - /** - * Proxy port the HTTP client should use. - */ - private Integer port; - - public String getHost() { - return this.host; - } - - public void setHost(String host) { - this.host = host; - } - - public Integer getPort() { - return this.port; - } - - public void setPort(Integer port) { - this.port = port; - } - - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/jest/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/jest/package-info.java deleted file mode 100644 index 0ecba962fc4e..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/jest/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Auto-configuration for Jest. - */ -package org.springframework.boot.autoconfigure.elasticsearch.jest; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/package-info.java new file mode 100644 index 000000000000..2931a91e4f33 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for Elasticsearch client. + */ +package org.springframework.boot.autoconfigure.elasticsearch; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/rest/RestClientAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/rest/RestClientAutoConfiguration.java deleted file mode 100644 index 45dc26614728..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/rest/RestClientAutoConfiguration.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.elasticsearch.rest; - -import org.elasticsearch.client.RestClient; - -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -/** - * {@link EnableAutoConfiguration Auto-configuration} for Elasticsearch REST clients. - * - * @author Brian Clozel - * @author Stephane Nicoll - * @since 2.1.0 - */ -@Configuration(proxyBeanMethods = false) -@ConditionalOnClass(RestClient.class) -@EnableConfigurationProperties(RestClientProperties.class) -@Import({ RestClientConfigurations.RestClientBuilderConfiguration.class, - RestClientConfigurations.RestHighLevelClientConfiguration.class, - RestClientConfigurations.RestClientFallbackConfiguration.class }) -public class RestClientAutoConfiguration { - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/rest/RestClientBuilderCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/rest/RestClientBuilderCustomizer.java deleted file mode 100644 index 8eed6849405e..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/rest/RestClientBuilderCustomizer.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.elasticsearch.rest; - -import org.elasticsearch.client.RestClientBuilder; - -/** - * Callback interface that can be implemented by beans wishing to further customize the - * {@link org.elasticsearch.client.RestClient} via a {@link RestClientBuilder} whilst - * retaining default auto-configuration. - * - * @author Brian Clozel - * @since 2.1.0 - */ -@FunctionalInterface -public interface RestClientBuilderCustomizer { - - /** - * Customize the {@link RestClientBuilder}. - * @param builder the builder to customize - */ - void customize(RestClientBuilder builder); - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/rest/RestClientConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/rest/RestClientConfigurations.java deleted file mode 100644 index e84caea1c53d..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/rest/RestClientConfigurations.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.elasticsearch.rest; - -import java.time.Duration; - -import org.apache.http.HttpHost; -import org.apache.http.auth.AuthScope; -import org.apache.http.auth.Credentials; -import org.apache.http.auth.UsernamePasswordCredentials; -import org.apache.http.client.CredentialsProvider; -import org.apache.http.impl.client.BasicCredentialsProvider; -import org.elasticsearch.client.RestClient; -import org.elasticsearch.client.RestClientBuilder; -import org.elasticsearch.client.RestHighLevelClient; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.context.properties.PropertyMapper; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * Elasticsearch rest client infrastructure configurations. - * - * @author Brian Clozel - * @author Stephane Nicoll - */ -class RestClientConfigurations { - - @Configuration(proxyBeanMethods = false) - static class RestClientBuilderConfiguration { - - @Bean - @ConditionalOnMissingBean - RestClientBuilder elasticsearchRestClientBuilder(RestClientProperties properties, - ObjectProvider builderCustomizers) { - HttpHost[] hosts = properties.getUris().stream().map(HttpHost::create).toArray(HttpHost[]::new); - RestClientBuilder builder = RestClient.builder(hosts); - PropertyMapper map = PropertyMapper.get(); - map.from(properties::getUsername).whenHasText().to((username) -> { - CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); - Credentials credentials = new UsernamePasswordCredentials(properties.getUsername(), - properties.getPassword()); - credentialsProvider.setCredentials(AuthScope.ANY, credentials); - builder.setHttpClientConfigCallback( - (httpClientBuilder) -> httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider)); - }); - builder.setRequestConfigCallback((requestConfigBuilder) -> { - map.from(properties::getConnectionTimeout).whenNonNull().asInt(Duration::toMillis) - .to(requestConfigBuilder::setConnectTimeout); - map.from(properties::getReadTimeout).whenNonNull().asInt(Duration::toMillis) - .to(requestConfigBuilder::setSocketTimeout); - return requestConfigBuilder; - }); - builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); - return builder; - } - - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(RestHighLevelClient.class) - static class RestHighLevelClientConfiguration { - - @Bean - @ConditionalOnMissingBean - RestHighLevelClient elasticsearchRestHighLevelClient(RestClientBuilder restClientBuilder) { - return new RestHighLevelClient(restClientBuilder); - } - - @Bean - @ConditionalOnMissingBean - RestClient elasticsearchRestClient(RestClientBuilder builder, - ObjectProvider restHighLevelClient) { - RestHighLevelClient client = restHighLevelClient.getIfUnique(); - if (client != null) { - return client.getLowLevelClient(); - } - return builder.build(); - } - - } - - @Configuration(proxyBeanMethods = false) - static class RestClientFallbackConfiguration { - - @Bean - @ConditionalOnMissingBean - RestClient elasticsearchRestClient(RestClientBuilder builder) { - return builder.build(); - } - - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/rest/RestClientProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/rest/RestClientProperties.java deleted file mode 100644 index 7711e7cfc087..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/rest/RestClientProperties.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.elasticsearch.rest; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * Configuration properties for Elasticsearch REST clients. - * - * @author Brian Clozel - * @since 2.1.0 - */ -@ConfigurationProperties(prefix = "spring.elasticsearch.rest") -public class RestClientProperties { - - /** - * Comma-separated list of the Elasticsearch instances to use. - */ - private List uris = new ArrayList<>(Collections.singletonList("http://localhost:9200")); - - /** - * Credentials username. - */ - private String username; - - /** - * Credentials password. - */ - private String password; - - /** - * Connection timeout. - */ - private Duration connectionTimeout = Duration.ofSeconds(1); - - /** - * Read timeout. - */ - private Duration readTimeout = Duration.ofSeconds(30); - - public List getUris() { - return this.uris; - } - - public void setUris(List uris) { - this.uris = uris; - } - - public String getUsername() { - return this.username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getPassword() { - return this.password; - } - - public void setPassword(String password) { - this.password = password; - } - - public Duration getConnectionTimeout() { - return this.connectionTimeout; - } - - public void setConnectionTimeout(Duration connectionTimeout) { - this.connectionTimeout = connectionTimeout; - } - - public Duration getReadTimeout() { - return this.readTimeout; - } - - public void setReadTimeout(Duration readTimeout) { - this.readTimeout = readTimeout; - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/rest/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/rest/package-info.java deleted file mode 100644 index 4dc49d07ef94..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/rest/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Auto-configuration for Elasticsearch REST clients. - */ -package org.springframework.boot.autoconfigure.elasticsearch.rest; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java index c0375d822880..4a342d0eb825 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,15 +16,15 @@ package org.springframework.boot.autoconfigure.flyway; +import java.sql.DatabaseMetaData; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; -import java.util.function.Supplier; import java.util.stream.Collectors; -import javax.persistence.EntityManagerFactory; import javax.sql.DataSource; import org.flywaydb.core.Flyway; @@ -41,21 +41,16 @@ 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.data.jpa.EntityManagerFactoryDependsOnPostProcessor; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.FlywayDataSourceCondition; -import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.FlywayEntityManagerFactoryDependsOnPostProcessor; -import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.FlywayJdbcOperationsDependsOnPostProcessor; -import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.FlywayNamedParameterJdbcOperationsDependencyConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; -import org.springframework.boot.autoconfigure.jdbc.JdbcOperationsDependsOnPostProcessor; import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.NamedParameterJdbcOperationsDependsOnPostProcessor; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.boot.context.properties.ConfigurationPropertiesBinding; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.boot.jdbc.DatabaseDriver; +import org.springframework.boot.sql.init.dependency.DatabaseInitializationDependencyConfigurer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; @@ -63,12 +58,10 @@ import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.GenericConverter; import org.springframework.core.io.ResourceLoader; -import org.springframework.jdbc.core.JdbcOperations; -import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; +import org.springframework.jdbc.datasource.SimpleDriverDataSource; import org.springframework.jdbc.support.JdbcUtils; import org.springframework.jdbc.support.MetaDataAccessException; -import org.springframework.orm.jpa.AbstractEntityManagerFactoryBean; -import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -86,6 +79,7 @@ * @author Dan Zheng * @author András Deák * @author Semyon Danilov + * @author Chris Bono * @since 1.1.0 */ @Configuration(proxyBeanMethods = false) @@ -94,8 +88,7 @@ @ConditionalOnProperty(prefix = "spring.flyway", name = "enabled", matchIfMissing = true) @AutoConfigureAfter({ DataSourceAutoConfiguration.class, JdbcTemplateAutoConfiguration.class, HibernateJpaAutoConfiguration.class }) -@Import({ FlywayEntityManagerFactoryDependsOnPostProcessor.class, FlywayJdbcOperationsDependsOnPostProcessor.class, - FlywayNamedParameterJdbcOperationsDependencyConfiguration.class }) +@Import(DatabaseInitializationDependencyConfigurer.class) public class FlywayAutoConfiguration { @Bean @@ -110,23 +103,19 @@ public FlywaySchemaManagementProvider flywayDefaultDdlModeProvider(ObjectProvide } @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(JdbcUtils.class) @ConditionalOnMissingBean(Flyway.class) - @EnableConfigurationProperties({ DataSourceProperties.class, FlywayProperties.class }) - @Import({ FlywayMigrationInitializerEntityManagerFactoryDependsOnPostProcessor.class, - FlywayMigrationInitializerJdbcOperationsDependsOnPostProcessor.class, - FlywayMigrationInitializerNamedParameterJdbcOperationsDependsOnPostProcessor.class }) + @EnableConfigurationProperties(FlywayProperties.class) public static class FlywayConfiguration { @Bean - public Flyway flyway(FlywayProperties properties, DataSourceProperties dataSourceProperties, - ResourceLoader resourceLoader, ObjectProvider dataSource, - @FlywayDataSource ObjectProvider flywayDataSource, + public Flyway flyway(FlywayProperties properties, ResourceLoader resourceLoader, + ObjectProvider dataSource, @FlywayDataSource ObjectProvider flywayDataSource, ObjectProvider fluentConfigurationCustomizers, ObjectProvider javaMigrations, ObjectProvider callbacks) { FluentConfiguration configuration = new FluentConfiguration(resourceLoader.getClassLoader()); - DataSource dataSourceToMigrate = configureDataSource(configuration, properties, dataSourceProperties, - flywayDataSource.getIfAvailable(), dataSource.getIfAvailable()); - checkLocationExists(dataSourceToMigrate, properties, resourceLoader); + configureDataSource(configuration, properties, flywayDataSource.getIfAvailable(), dataSource.getIfUnique()); + checkLocationExists(configuration.getDataSource(), properties, resourceLoader); configureProperties(configuration, properties); List orderedCallbacks = callbacks.orderedStream().collect(Collectors.toList()); configureCallbacks(configuration, orderedCallbacks); @@ -137,27 +126,42 @@ public Flyway flyway(FlywayProperties properties, DataSourceProperties dataSourc return configuration.load(); } - private DataSource configureDataSource(FluentConfiguration configuration, FlywayProperties properties, - DataSourceProperties dataSourceProperties, DataSource flywayDataSource, DataSource dataSource) { - if (properties.isCreateDataSource()) { - String url = getProperty(properties::getUrl, dataSourceProperties::determineUrl); - String user = getProperty(properties::getUser, dataSourceProperties::determineUsername); - String password = getProperty(properties::getPassword, dataSourceProperties::determinePassword); - configuration.dataSource(url, user, password); - if (!CollectionUtils.isEmpty(properties.getInitSqls())) { - String initSql = StringUtils.collectionToDelimitedString(properties.getInitSqls(), "\n"); - configuration.initSql(initSql); - } + private void configureDataSource(FluentConfiguration configuration, FlywayProperties properties, + DataSource flywayDataSource, DataSource dataSource) { + DataSource migrationDataSource = getMigrationDataSource(properties, flywayDataSource, dataSource); + configuration.dataSource(migrationDataSource); + } + + private DataSource getMigrationDataSource(FlywayProperties properties, DataSource flywayDataSource, + DataSource dataSource) { + if (flywayDataSource != null) { + return flywayDataSource; + } + if (properties.getUrl() != null) { + DataSourceBuilder builder = DataSourceBuilder.create().type(SimpleDriverDataSource.class); + builder.url(properties.getUrl()); + applyCommonBuilderProperties(properties, builder); + return builder.build(); } - else if (flywayDataSource != null) { - configuration.dataSource(flywayDataSource); + if (properties.getUser() != null && dataSource != null) { + DataSourceBuilder builder = DataSourceBuilder.derivedFrom(dataSource) + .type(SimpleDriverDataSource.class); + applyCommonBuilderProperties(properties, builder); + return builder.build(); } - else { - configuration.dataSource(dataSource); + Assert.state(dataSource != null, "Flyway migration DataSource missing"); + return dataSource; + } + + private void applyCommonBuilderProperties(FlywayProperties properties, DataSourceBuilder builder) { + builder.username(properties.getUser()); + builder.password(properties.getPassword()); + if (StringUtils.hasText(properties.getDriverClassName())) { + builder.driverClassName(properties.getDriverClassName()); } - return configuration.getDataSource(); } + @SuppressWarnings("deprecation") private void checkLocationExists(DataSource dataSource, FlywayProperties properties, ResourceLoader resourceLoader) { if (properties.isCheckLocation()) { @@ -175,10 +179,16 @@ private void configureProperties(FluentConfiguration configuration, FlywayProper map.from(locations).to(configuration::locations); map.from(properties.getEncoding()).to(configuration::encoding); map.from(properties.getConnectRetries()).to(configuration::connectRetries); + // No method reference for compatibility with Flyway 6.x + map.from(properties.getLockRetryCount()) + .to((lockRetryCount) -> configuration.lockRetryCount(lockRetryCount)); + // No method reference for compatibility with Flyway 5.x + map.from(properties.getDefaultSchema()).to((schema) -> configuration.defaultSchema(schema)); map.from(properties.getSchemas()).as(StringUtils::toStringArray).to(configuration::schemas); + configureCreateSchemas(configuration, properties.isCreateSchemas()); map.from(properties.getTable()).to(configuration::table); // No method reference for compatibility with Flyway 5.x - map.from(properties.getTablespace()).whenNonNull().to((tablespace) -> configuration.tablespace(tablespace)); + map.from(properties.getTablespace()).to((tablespace) -> configuration.tablespace(tablespace)); map.from(properties.getBaselineDescription()).to(configuration::baselineDescription); map.from(properties.getBaselineVersion()).to(configuration::baselineVersion); map.from(properties.getInstalledBy()).to(configuration::installedBy); @@ -204,18 +214,64 @@ private void configureProperties(FluentConfiguration configuration, FlywayProper map.from(properties.isOutOfOrder()).to(configuration::outOfOrder); map.from(properties.isSkipDefaultCallbacks()).to(configuration::skipDefaultCallbacks); map.from(properties.isSkipDefaultResolvers()).to(configuration::skipDefaultResolvers); + configureValidateMigrationNaming(configuration, properties.isValidateMigrationNaming()); map.from(properties.isValidateOnMigrate()).to(configuration::validateOnMigrate); + map.from(properties.getInitSqls()).whenNot(CollectionUtils::isEmpty) + .as((initSqls) -> StringUtils.collectionToDelimitedString(initSqls, "\n")) + .to(configuration::initSql); // Pro properties - map.from(properties.getBatch()).whenNonNull().to(configuration::batch); - map.from(properties.getDryRunOutput()).whenNonNull().to(configuration::dryRunOutput); - map.from(properties.getErrorOverrides()).whenNonNull().to(configuration::errorOverrides); - map.from(properties.getLicenseKey()).whenNonNull().to(configuration::licenseKey); - map.from(properties.getOracleSqlplus()).whenNonNull().to(configuration::oracleSqlplus); + map.from(properties.getBatch()).to(configuration::batch); + map.from(properties.getDryRunOutput()).to(configuration::dryRunOutput); + map.from(properties.getErrorOverrides()).to(configuration::errorOverrides); + map.from(properties.getLicenseKey()).to(configuration::licenseKey); + map.from(properties.getOracleSqlplus()).to(configuration::oracleSqlplus); // No method reference for compatibility with Flyway 5.x - map.from(properties.getOracleSqlplusWarn()).whenNonNull() + map.from(properties.getOracleSqlplusWarn()) .to((oracleSqlplusWarn) -> configuration.oracleSqlplusWarn(oracleSqlplusWarn)); - map.from(properties.getStream()).whenNonNull().to(configuration::stream); - map.from(properties.getUndoSqlMigrationPrefix()).whenNonNull().to(configuration::undoSqlMigrationPrefix); + map.from(properties.getStream()).to(configuration::stream); + map.from(properties.getUndoSqlMigrationPrefix()).to(configuration::undoSqlMigrationPrefix); + // No method reference for compatibility with Flyway 6.x + map.from(properties.getCherryPick()).to((cherryPick) -> configuration.cherryPick(cherryPick)); + // No method reference for compatibility with Flyway 6.x + map.from(properties.getJdbcProperties()).whenNot(Map::isEmpty) + .to((jdbcProperties) -> configuration.jdbcProperties(jdbcProperties)); + // No method reference for compatibility with Flyway 6.x + map.from(properties.getOracleKerberosCacheFile()) + .to((cacheFile) -> configuration.oracleKerberosCacheFile(cacheFile)); + // No method reference for compatibility with Flyway 6.x + map.from(properties.getOracleKerberosConfigFile()) + .to((configFile) -> configuration.oracleKerberosConfigFile(configFile)); + // No method reference for compatibility with Flyway 6.x + map.from(properties.getOutputQueryResults()) + .to((outputQueryResults) -> configuration.outputQueryResults(outputQueryResults)); + // No method reference for compatibility with Flyway 6.x + map.from(properties.getSkipExecutingMigrations()) + .to((skipExecutingMigrations) -> configuration.skipExecutingMigrations(skipExecutingMigrations)); + // Teams secrets management properties (all non-method reference for + // compatibility with Flyway 6.x) + map.from(properties.getVaultUrl()).to((vaultUrl) -> configuration.vaultUrl(vaultUrl)); + map.from(properties.getVaultToken()).to((vaultToken) -> configuration.vaultToken(vaultToken)); + map.from(properties.getVaultSecrets()).whenNot(List::isEmpty) + .to((vaultSecrets) -> configuration.vaultSecrets(vaultSecrets.toArray(new String[0]))); + } + + private void configureCreateSchemas(FluentConfiguration configuration, boolean createSchemas) { + try { + configuration.createSchemas(createSchemas); + } + catch (NoSuchMethodError ex) { + // Flyway < 6.5 + } + } + + private void configureValidateMigrationNaming(FluentConfiguration configuration, + boolean validateMigrationNaming) { + try { + configuration.validateMigrationNaming(validateMigrationNaming); + } + catch (NoSuchMethodError ex) { + // Flyway < 6.2 + } } private void configureCallbacks(FluentConfiguration configuration, List callbacks) { @@ -241,11 +297,6 @@ private void configureJavaMigrations(FluentConfiguration flyway, List property, Supplier defaultValue) { - String value = property.get(); - return (value != null) ? value : defaultValue.get(); - } - private boolean hasAtLeastOneLocation(ResourceLoader resourceLoader, Collection locations) { for (String location : locations) { if (resourceLoader.getResource(normalizePrefix(location)).exists()) { @@ -268,94 +319,6 @@ public FlywayMigrationInitializer flywayInitializer(Flyway flyway, } - /** - * Post processor to ensure that {@link EntityManagerFactory} beans depend on any - * {@link FlywayMigrationInitializer} beans. - */ - @ConditionalOnClass(LocalContainerEntityManagerFactoryBean.class) - @ConditionalOnBean(AbstractEntityManagerFactoryBean.class) - static class FlywayMigrationInitializerEntityManagerFactoryDependsOnPostProcessor - extends EntityManagerFactoryDependsOnPostProcessor { - - FlywayMigrationInitializerEntityManagerFactoryDependsOnPostProcessor() { - super(FlywayMigrationInitializer.class); - } - - } - - /** - * Post processor to ensure that {@link JdbcOperations} beans depend on any - * {@link FlywayMigrationInitializer} beans. - */ - @ConditionalOnClass(JdbcOperations.class) - @ConditionalOnBean(JdbcOperations.class) - static class FlywayMigrationInitializerJdbcOperationsDependsOnPostProcessor - extends JdbcOperationsDependsOnPostProcessor { - - FlywayMigrationInitializerJdbcOperationsDependsOnPostProcessor() { - super(FlywayMigrationInitializer.class); - } - - } - - /** - * Post processor to ensure that {@link NamedParameterJdbcOperations} beans depend on - * any {@link FlywayMigrationInitializer} beans. - */ - @ConditionalOnClass(NamedParameterJdbcOperations.class) - @ConditionalOnBean(NamedParameterJdbcOperations.class) - static class FlywayMigrationInitializerNamedParameterJdbcOperationsDependsOnPostProcessor - extends NamedParameterJdbcOperationsDependsOnPostProcessor { - - FlywayMigrationInitializerNamedParameterJdbcOperationsDependsOnPostProcessor() { - super(FlywayMigrationInitializer.class); - } - - } - - /** - * Post processor to ensure that {@link EntityManagerFactory} beans depend on any - * {@link Flyway} beans. - */ - @ConditionalOnClass(LocalContainerEntityManagerFactoryBean.class) - @ConditionalOnBean(AbstractEntityManagerFactoryBean.class) - static class FlywayEntityManagerFactoryDependsOnPostProcessor extends EntityManagerFactoryDependsOnPostProcessor { - - FlywayEntityManagerFactoryDependsOnPostProcessor() { - super(Flyway.class); - } - - } - - /** - * Post processor to ensure that {@link JdbcOperations} beans depend on any - * {@link Flyway} beans. - */ - @ConditionalOnClass(JdbcOperations.class) - @ConditionalOnBean(JdbcOperations.class) - static class FlywayJdbcOperationsDependsOnPostProcessor extends JdbcOperationsDependsOnPostProcessor { - - FlywayJdbcOperationsDependsOnPostProcessor() { - super(Flyway.class); - } - - } - - /** - * Post processor to ensure that {@link NamedParameterJdbcOperations} beans depend on - * any {@link Flyway} beans. - */ - @ConditionalOnClass(NamedParameterJdbcOperations.class) - @ConditionalOnBean(NamedParameterJdbcOperations.class) - protected static class FlywayNamedParameterJdbcOperationsDependencyConfiguration - extends NamedParameterJdbcOperationsDependsOnPostProcessor { - - public FlywayNamedParameterJdbcOperationsDependencyConfiguration() { - super(Flyway.class); - } - - } - private static class LocationResolver { private static final String VENDOR_PLACEHOLDER = "{vendor}"; @@ -385,7 +348,7 @@ private List replaceVendorLocations(List locations, DatabaseDriv private DatabaseDriver getDatabaseDriver() { try { - String url = JdbcUtils.extractDatabaseMetaData(this.dataSource, "getURL"); + String url = JdbcUtils.extractDatabaseMetaData(this.dataSource, DatabaseMetaData::getURL); return DatabaseDriver.fromJdbcUrl(url); } catch (MetaDataAccessException ex) { @@ -443,7 +406,7 @@ private static final class DataSourceBeanCondition { } - @ConditionalOnProperty(prefix = "spring.flyway", name = "url", matchIfMissing = false) + @ConditionalOnProperty(prefix = "spring.flyway", name = "url") private static final class FlywayUrlCondition { } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayMigrationInitializer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayMigrationInitializer.java index cca907e41916..6f864abb5ed3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayMigrationInitializer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayMigrationInitializer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -62,7 +62,13 @@ public void afterPropertiesSet() throws Exception { this.migrationStrategy.migrate(this.flyway); } else { - this.flyway.migrate(); + try { + this.flyway.migrate(); + } + catch (NoSuchMethodError ex) { + // Flyway < 7.0 + this.flyway.getClass().getMethod("migrate").invoke(this.flyway); + } } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayMigrationInitializerDatabaseInitializerDetector.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayMigrationInitializerDatabaseInitializerDetector.java new file mode 100644 index 000000000000..73690134dc8f --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayMigrationInitializerDatabaseInitializerDetector.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.flyway; + +import java.util.Collections; +import java.util.Set; + +import org.springframework.boot.sql.init.dependency.AbstractBeansOfTypeDatabaseInitializerDetector; +import org.springframework.boot.sql.init.dependency.DatabaseInitializerDetector; + +/** + * A {@link DatabaseInitializerDetector} for {@link FlywayMigrationInitializer}. + * + * @author Andy Wilkinson + */ +class FlywayMigrationInitializerDatabaseInitializerDetector extends AbstractBeansOfTypeDatabaseInitializerDetector { + + @Override + protected Set> getDatabaseInitializerBeanTypes() { + return Collections.singleton(FlywayMigrationInitializer.class); + } + + @Override + public int getOrder() { + return 1; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayMigrationScriptMissingException.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayMigrationScriptMissingException.java index 4ac622585c77..c61a268889f3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayMigrationScriptMissingException.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayMigrationScriptMissingException.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,9 @@ * @author Anand Shastri * @author Stephane Nicoll * @since 2.2.0 + * @deprecated since 2.5.0 for removal in 2.7.0 as location checking is deprecated */ +@Deprecated public class FlywayMigrationScriptMissingException extends RuntimeException { private final List locations; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayMigrationScriptMissingFailureAnalyzer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayMigrationScriptMissingFailureAnalyzer.java index ebf282e15365..d66f2c7b66f9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayMigrationScriptMissingFailureAnalyzer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayMigrationScriptMissingFailureAnalyzer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,9 @@ * * @author Anand Shastri * @author Stephane Nicoll + * @deprecated since 2.5.0 for removal in 2.7.0 as location checking is deprecated */ +@Deprecated class FlywayMigrationScriptMissingFailureAnalyzer extends AbstractFailureAnalyzer { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayProperties.java index ac9e130629c1..b5e9ae7af828 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import java.util.Map; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; /** * Configuration properties for Flyway database migrations. @@ -33,6 +34,7 @@ * @author Dave Syer * @author Eddú Meléndez * @author Stephane Nicoll + * @author Chris Bono * @since 1.1.0 */ @ConfigurationProperties(prefix = "spring.flyway") @@ -44,8 +46,10 @@ public class FlywayProperties { private boolean enabled = true; /** - * Whether to check that migration scripts location exists. + * Whether to check that migration scripts location exists. Should be set to false + * when using a wildcard location or a remote-hosted location such as S3 or GCS. */ + @Deprecated private boolean checkLocation = true; /** @@ -64,11 +68,27 @@ public class FlywayProperties { */ private int connectRetries; + /** + * Maximum number of retries when trying to obtain a lock. + */ + private Integer lockRetryCount; + + /** + * Default schema name managed by Flyway (case-sensitive). + */ + private String defaultSchema; + /** * Scheme names managed by Flyway (case-sensitive). */ private List schemas = new ArrayList<>(); + /** + * Whether Flyway should attempt to create the schemas specified in the schemas + * property. + */ + private boolean createSchemas = true; + /** * Name of the schema history table that will be used by Flyway. */ @@ -141,12 +161,6 @@ public class FlywayProperties { */ private String target; - /** - * JDBC url of the database to migrate. If not set, the primary configured data source - * is used. - */ - private String url; - /** * Login user of the database to migrate. */ @@ -157,6 +171,17 @@ public class FlywayProperties { */ private String password; + /** + * Fully qualified name of the JDBC driver. Auto-detected based on the URL by default. + */ + private String driverClassName; + + /** + * JDBC url of the database to migrate. If not set, the primary configured data source + * is used. + */ + private String url; + /** * SQL statements to execute to initialize a connection immediately after obtaining * it. @@ -225,57 +250,111 @@ public class FlywayProperties { */ private boolean skipDefaultResolvers; + /** + * Whether to validate migrations and callbacks whose scripts do not obey the correct + * naming convention. + */ + private boolean validateMigrationNaming = false; + /** * Whether to automatically call validate when performing a migration. */ private boolean validateOnMigrate = true; /** - * Whether to batch SQL statements when executing them. Requires Flyway Pro or Flyway - * Enterprise. + * Whether to batch SQL statements when executing them. Requires Flyway Teams. */ private Boolean batch; /** * File to which the SQL statements of a migration dry run should be output. Requires - * Flyway Pro or Flyway Enterprise. + * Flyway Teams. */ private File dryRunOutput; /** * Rules for the built-in error handling to override specific SQL states and error - * codes. Requires Flyway Pro or Flyway Enterprise. + * codes. Requires Flyway Teams. */ private String[] errorOverrides; /** - * Licence key for Flyway Pro or Flyway Enterprise. + * Licence key for Flyway Teams. */ private String licenseKey; /** - * Whether to enable support for Oracle SQL*Plus commands. Requires Flyway Pro or - * Flyway Enterprise. + * Whether to enable support for Oracle SQL*Plus commands. Requires Flyway Teams. */ private Boolean oracleSqlplus; /** * Whether to issue a warning rather than an error when a not-yet-supported Oracle - * SQL*Plus statement is encountered. Requires Flyway Pro or Flyway Enterprise. + * SQL*Plus statement is encountered. Requires Flyway Teams. */ private Boolean oracleSqlplusWarn; /** - * Whether to stream SQL migrations when executing them. Requires Flyway Pro or Flyway - * Enterprise. + * Whether to stream SQL migrations when executing them. Requires Flyway Teams. */ private Boolean stream; /** - * File name prefix for undo SQL migrations. Requires Flyway Pro or Flyway Enterprise. + * File name prefix for undo SQL migrations. Requires Flyway Teams. */ private String undoSqlMigrationPrefix; + /** + * Migrations that Flyway should consider when migrating or undoing. When empty all + * available migrations are considered. Requires Flyway Teams. + */ + private String[] cherryPick; + + /** + * Properties to pass to the JDBC driver. Requires Flyway Teams. + */ + private Map jdbcProperties = new HashMap<>(); + + /** + * Path of the Oracle Kerberos cache file. Requires Flyway Teams. + */ + private String oracleKerberosCacheFile; + + /** + * Path of the Oracle Kerberos config file. Requires Flyway Teams. + */ + private String oracleKerberosConfigFile; + + /** + * Whether Flyway should output a table with the results of queries when executing + * migrations. Requires Flyway Teams. + */ + private Boolean outputQueryResults; + + /** + * Whether Flyway should skip executing the contents of the migrations and only update + * the schema history table. Requires Flyway teams. + */ + private Boolean skipExecutingMigrations; + + /** + * REST API URL of the Vault server. Requires Flyway teams. + */ + private String vaultUrl; + + /** + * Vault token required to access secrets. Requires Flyway teams. + */ + private String vaultToken; + + /** + * Comma-separated list of paths to secrets that contain Flyway configurations. This + * must start with the name of the engine followed by '/data/' and end with the name + * of the secret. The resulting form is '{engine}/data/{path}/{to}/{secret_name}'. + * Requires Flyway teams. + */ + private List vaultSecrets; + public boolean isEnabled() { return this.enabled; } @@ -284,10 +363,14 @@ public void setEnabled(boolean enabled) { this.enabled = enabled; } + @Deprecated + @DeprecatedConfigurationProperty( + reason = "Locations can no longer be checked accurately due to changes in Flyway's location support.") public boolean isCheckLocation() { return this.checkLocation; } + @Deprecated public void setCheckLocation(boolean checkLocation) { this.checkLocation = checkLocation; } @@ -316,6 +399,22 @@ public void setConnectRetries(int connectRetries) { this.connectRetries = connectRetries; } + public Integer getLockRetryCount() { + return this.lockRetryCount; + } + + public void setLockRetryCount(Integer lockRetryCount) { + this.lockRetryCount = lockRetryCount; + } + + public String getDefaultSchema() { + return this.defaultSchema; + } + + public void setDefaultSchema(String defaultSchema) { + this.defaultSchema = defaultSchema; + } + public List getSchemas() { return this.schemas; } @@ -324,6 +423,14 @@ public void setSchemas(List schemas) { this.schemas = schemas; } + public boolean isCreateSchemas() { + return this.createSchemas; + } + + public void setCreateSchemas(boolean createSchemas) { + this.createSchemas = createSchemas; + } + public String getTable() { return this.table; } @@ -436,18 +543,17 @@ public void setTarget(String target) { this.target = target; } + /** + * Return if a new datasource is being created. + * @return {@code true} if a new datasource is created + * @deprecated since 2.5.0 for removal in 2.7.0 in favor of directly checking user and + * url. + */ + @Deprecated public boolean isCreateDataSource() { return this.url != null || this.user != null; } - public String getUrl() { - return this.url; - } - - public void setUrl(String url) { - this.url = url; - } - public String getUser() { return this.user; } @@ -464,6 +570,22 @@ public void setPassword(String password) { this.password = password; } + public String getDriverClassName() { + return this.driverClassName; + } + + public void setDriverClassName(String driverClassName) { + this.driverClassName = driverClassName; + } + + public String getUrl() { + return this.url; + } + + public void setUrl(String url) { + this.url = url; + } + public List getInitSqls() { return this.initSqls; } @@ -568,6 +690,14 @@ public void setSkipDefaultResolvers(boolean skipDefaultResolvers) { this.skipDefaultResolvers = skipDefaultResolvers; } + public boolean isValidateMigrationNaming() { + return this.validateMigrationNaming; + } + + public void setValidateMigrationNaming(boolean validateMigrationNaming) { + this.validateMigrationNaming = validateMigrationNaming; + } + public boolean isValidateOnMigrate() { return this.validateOnMigrate; } @@ -640,4 +770,76 @@ public void setUndoSqlMigrationPrefix(String undoSqlMigrationPrefix) { this.undoSqlMigrationPrefix = undoSqlMigrationPrefix; } + public String[] getCherryPick() { + return this.cherryPick; + } + + public void setCherryPick(String[] cherryPick) { + this.cherryPick = cherryPick; + } + + public Map getJdbcProperties() { + return this.jdbcProperties; + } + + public void setJdbcProperties(Map jdbcProperties) { + this.jdbcProperties = jdbcProperties; + } + + public String getOracleKerberosCacheFile() { + return this.oracleKerberosCacheFile; + } + + public void setOracleKerberosCacheFile(String oracleKerberosCacheFile) { + this.oracleKerberosCacheFile = oracleKerberosCacheFile; + } + + public String getOracleKerberosConfigFile() { + return this.oracleKerberosConfigFile; + } + + public void setOracleKerberosConfigFile(String oracleKerberosConfigFile) { + this.oracleKerberosConfigFile = oracleKerberosConfigFile; + } + + public Boolean getOutputQueryResults() { + return this.outputQueryResults; + } + + public void setOutputQueryResults(Boolean outputQueryResults) { + this.outputQueryResults = outputQueryResults; + } + + public Boolean getSkipExecutingMigrations() { + return this.skipExecutingMigrations; + } + + public void setSkipExecutingMigrations(Boolean skipExecutingMigrations) { + this.skipExecutingMigrations = skipExecutingMigrations; + } + + public String getVaultUrl() { + return this.vaultUrl; + } + + public void setVaultUrl(String vaultUrl) { + this.vaultUrl = vaultUrl; + } + + public String getVaultToken() { + return this.vaultToken; + } + + public void setVaultToken(String vaultToken) { + this.vaultToken = vaultToken; + } + + public List getVaultSecrets() { + return this.vaultSecrets; + } + + public void setVaultSecrets(List vaultSecrets) { + this.vaultSecrets = vaultSecrets; + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerAutoConfiguration.java index 4ddd5c8a8bb1..ff3980883a6e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,8 +19,6 @@ import java.util.ArrayList; import java.util.List; -import javax.annotation.PostConstruct; - import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -57,9 +55,9 @@ public class FreeMarkerAutoConfiguration { public FreeMarkerAutoConfiguration(ApplicationContext applicationContext, FreeMarkerProperties properties) { this.applicationContext = applicationContext; this.properties = properties; + checkTemplateLocationExists(); } - @PostConstruct public void checkTemplateLocationExists() { if (logger.isWarnEnabled() && this.properties.isCheckTemplateLocation()) { List locations = getLocations(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerProperties.java index be5e7eb62a8f..6e6087d57fd5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,10 +49,12 @@ public class FreeMarkerProperties extends AbstractTemplateViewResolverProperties private String[] templateLoaderPath = new String[] { DEFAULT_TEMPLATE_LOADER_PATH }; /** - * Whether to prefer file system access for template loading. File system access - * enables hot detection of template changes. + * Whether to prefer file system access for template loading to enable hot detection + * of template changes. When a template path is detected as a directory, templates are + * loaded from the directory only and other matching classpath locations will not be + * considered. */ - private boolean preferFileSystemAccess = true; + private boolean preferFileSystemAccess; public FreeMarkerProperties() { super(DEFAULT_PREFIX, DEFAULT_SUFFIX); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/groovy/template/GroovyTemplateAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/groovy/template/GroovyTemplateAutoConfiguration.java index 8385bfc99d92..ec50ca39997f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/groovy/template/GroovyTemplateAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/groovy/template/GroovyTemplateAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ import java.security.CodeSource; import java.security.ProtectionDomain; -import javax.annotation.PostConstruct; import javax.servlet.Servlet; import groovy.text.markup.MarkupTemplateEngine; @@ -74,13 +73,12 @@ public static class GroovyMarkupConfiguration { private final GroovyTemplateProperties properties; - public GroovyMarkupConfiguration(ApplicationContext applicationContext, GroovyTemplateProperties properties, - ObjectProvider templateEngine) { + public GroovyMarkupConfiguration(ApplicationContext applicationContext, GroovyTemplateProperties properties) { this.applicationContext = applicationContext; this.properties = properties; + checkTemplateLocationExists(); } - @PostConstruct public void checkTemplateLocationExists() { if (this.properties.isCheckTemplateLocation() && !isUsingGroovyAllJar()) { TemplateLocation location = new TemplateLocation(this.properties.getResourceLoaderPath()); @@ -96,8 +94,8 @@ public void checkTemplateLocationExists() { /** * MarkupTemplateEngine could be loaded from groovy-templates or groovy-all. * Unfortunately it's quite common for people to use groovy-all and not actually - * need templating support. This method check attempts to check the source jar so - * that we can skip the {@code /template} folder check for such cases. + * need templating support. This method attempts to check the source jar so that + * we can skip the {@code /template} directory check for such cases. * @return true if the groovy-all jar is used */ private boolean isUsingGroovyAllJar() { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfiguration.java index c857d486fcd8..2a34342119bd 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package org.springframework.boot.autoconfigure.h2; import java.sql.Connection; -import java.sql.SQLException; import javax.sql.DataSource; @@ -32,6 +31,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; +import org.springframework.boot.autoconfigure.h2.H2ConsoleProperties.Settings; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.servlet.ServletRegistrationBean; @@ -49,7 +49,7 @@ @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) @ConditionalOnClass(WebServlet.class) -@ConditionalOnProperty(prefix = "spring.h2.console", name = "enabled", havingValue = "true", matchIfMissing = false) +@ConditionalOnProperty(prefix = "spring.h2.console", name = "enabled", havingValue = "true") @AutoConfigureAfter(DataSourceAutoConfiguration.class) @EnableConfigurationProperties(H2ConsoleProperties.class) public class H2ConsoleAutoConfiguration { @@ -62,23 +62,29 @@ public ServletRegistrationBean h2Console(H2ConsoleProperties propert String path = properties.getPath(); String urlMapping = path + (path.endsWith("/") ? "*" : "/*"); ServletRegistrationBean registration = new ServletRegistrationBean<>(new WebServlet(), urlMapping); - H2ConsoleProperties.Settings settings = properties.getSettings(); - if (settings.isTrace()) { - registration.addInitParameter("trace", ""); - } - if (settings.isWebAllowOthers()) { - registration.addInitParameter("webAllowOthers", ""); - } + configureH2ConsoleSettings(registration, properties.getSettings()); dataSource.ifAvailable((available) -> { try (Connection connection = available.getConnection()) { logger.info("H2 console available at '" + path + "'. Database available at '" + connection.getMetaData().getURL() + "'"); } - catch (SQLException ex) { + catch (Exception ex) { // Continue } }); return registration; } + private void configureH2ConsoleSettings(ServletRegistrationBean registration, Settings settings) { + if (settings.isTrace()) { + registration.addInitParameter("trace", ""); + } + if (settings.isWebAllowOthers()) { + registration.addInitParameter("webAllowOthers", ""); + } + if (settings.getWebAdminPassword() != null) { + registration.addInitParameter("webAdminPassword", settings.getWebAdminPassword()); + } + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/h2/H2ConsoleProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/h2/H2ConsoleProperties.java index 5118a0627743..d17ccfabbb60 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/h2/H2ConsoleProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/h2/H2ConsoleProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -77,6 +77,11 @@ public static class Settings { */ private boolean webAllowOthers = false; + /** + * Password to access preferences and tools of H2 Console. + */ + private String webAdminPassword; + public boolean isTrace() { return this.trace; } @@ -93,6 +98,14 @@ public void setWebAllowOthers(boolean webAllowOthers) { this.webAllowOthers = webAllowOthers; } + public String getWebAdminPassword() { + return this.webAdminPassword; + } + + public void setWebAdminPassword(String webAdminPassword) { + this.webAdminPassword = webAdminPassword; + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HypermediaAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HypermediaAutoConfiguration.java index 27f1bc188d0a..aa562cc55dac 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HypermediaAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HypermediaAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,20 +22,24 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 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.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; import org.springframework.hateoas.EntityModel; import org.springframework.hateoas.client.LinkDiscoverers; import org.springframework.hateoas.config.EnableHypermediaSupport; import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; +import org.springframework.hateoas.mediatype.hal.HalConfiguration; +import org.springframework.http.MediaType; import org.springframework.plugin.core.Plugin; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; /** * {@link EnableAutoConfiguration Auto-configuration} for Spring HATEOAS's @@ -47,14 +51,22 @@ * @since 1.1.0 */ @Configuration(proxyBeanMethods = false) -@ConditionalOnClass({ EntityModel.class, RequestMapping.class, Plugin.class }) +@ConditionalOnClass({ EntityModel.class, RequestMapping.class, RequestMappingHandlerAdapter.class, Plugin.class }) @ConditionalOnWebApplication @AutoConfigureAfter({ WebMvcAutoConfiguration.class, JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, RepositoryRestMvcAutoConfiguration.class }) @EnableConfigurationProperties(HateoasProperties.class) -@Import(HypermediaHttpMessageConverterConfiguration.class) public class HypermediaAutoConfiguration { + @Bean + @ConditionalOnMissingBean + @ConditionalOnClass(name = "com.fasterxml.jackson.databind.ObjectMapper") + @ConditionalOnProperty(prefix = "spring.hateoas", name = "use-hal-as-default-json-media-type", + matchIfMissing = true) + HalConfiguration applicationJsonHalConfiguration() { + return new HalConfiguration().withMediaType(MediaType.APPLICATION_JSON); + } + @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(LinkDiscoverers.class) @ConditionalOnClass(ObjectMapper.class) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HypermediaHttpMessageConverterConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HypermediaHttpMessageConverterConfiguration.java index a26d0f2c9d98..95eb71908619 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HypermediaHttpMessageConverterConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HypermediaHttpMessageConverterConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,16 +20,16 @@ import java.util.Collection; import java.util.List; -import javax.annotation.PostConstruct; - import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.hateoas.mediatype.hal.HalConfiguration; import org.springframework.hateoas.server.mvc.TypeConstrainedMappingJackson2HttpMessageConverter; import org.springframework.http.MediaType; import org.springframework.http.converter.AbstractHttpMessageConverter; @@ -42,7 +42,10 @@ * * @author Andy Wilkinson * @since 1.3.0 + * @deprecated since 2.5.0 for removal in 2.7.0 in favor of a {@link HalConfiguration} + * bean */ +@Deprecated @Configuration(proxyBeanMethods = false) public class HypermediaHttpMessageConverterConfiguration { @@ -60,12 +63,13 @@ public static HalMessageConverterSupportedMediaTypesCustomizer halMessageConvert * {@code Jackson2ModuleRegisteringBeanPostProcessor} has registered the converter and * it is unordered. */ - private static class HalMessageConverterSupportedMediaTypesCustomizer implements BeanFactoryAware { + private static class HalMessageConverterSupportedMediaTypesCustomizer + implements BeanFactoryAware, InitializingBean { private volatile BeanFactory beanFactory; - @PostConstruct - void configureHttpMessageConverters() { + @Override + public void afterPropertiesSet() { if (this.beanFactory instanceof ListableBeanFactory) { configureHttpMessageConverters(((ListableBeanFactory) this.beanFactory) .getBeansOfType(RequestMappingHandlerAdapter.class).values()); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastAutoConfiguration.java index 353a39fa9cb0..8f6f5148cafc 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,13 +19,22 @@ import com.hazelcast.core.HazelcastInstance; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionMessage; +import org.springframework.boot.autoconfigure.condition.ConditionMessage.Builder; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration.HazelcastDataGridCondition; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.core.io.Resource; +import org.springframework.core.type.AnnotatedTypeMetadata; /** - * {@link EnableAutoConfiguration Auto-configuration} for Hazelcast. Creates a + * {@link EnableAutoConfiguration Auto-configuration} for Hazelcast IMDG. Creates a * {@link HazelcastInstance} based on explicit configuration or when a default * configuration file is found in the environment. * @@ -35,9 +44,26 @@ * @see HazelcastConfigResourceCondition */ @Configuration(proxyBeanMethods = false) +@Conditional(HazelcastDataGridCondition.class) @ConditionalOnClass(HazelcastInstance.class) @EnableConfigurationProperties(HazelcastProperties.class) @Import({ HazelcastClientConfiguration.class, HazelcastServerConfiguration.class }) public class HazelcastAutoConfiguration { + static class HazelcastDataGridCondition extends SpringBootCondition { + + private static final String HAZELCAST_JET_CONFIG_FILE = "classpath:/hazelcast-jet-default.yaml"; + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { + Builder message = ConditionMessage.forCondition(HazelcastDataGridCondition.class.getSimpleName()); + Resource resource = context.getResourceLoader().getResource(HAZELCAST_JET_CONFIG_FILE); + if (resource.exists()) { + return ConditionOutcome.noMatch(message.because("Found Hazelcast Jet on the classpath")); + } + return ConditionOutcome.match(message.because("Hazelcast Jet not found on the classpath")); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastClientConfigAvailableCondition.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastClientConfigAvailableCondition.java new file mode 100644 index 000000000000..a08a40da1926 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastClientConfigAvailableCondition.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.hazelcast; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +import com.hazelcast.client.config.ClientConfigRecognizer; +import com.hazelcast.config.ConfigStream; + +import org.springframework.boot.autoconfigure.condition.ConditionMessage.Builder; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.io.Resource; +import org.springframework.core.type.AnnotatedTypeMetadata; + +/** + * {@link HazelcastConfigResourceCondition} that checks if the + * {@code spring.hazelcast.config} configuration key is defined. + * + * @author Stephane Nicoll + */ +class HazelcastClientConfigAvailableCondition extends HazelcastConfigResourceCondition { + + HazelcastClientConfigAvailableCondition() { + super(HazelcastClientConfiguration.CONFIG_SYSTEM_PROPERTY, "file:./hazelcast-client.xml", + "classpath:/hazelcast-client.xml", "file:./hazelcast-client.yaml", "classpath:/hazelcast-client.yaml"); + } + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { + if (context.getEnvironment().containsProperty(HAZELCAST_CONFIG_PROPERTY)) { + ConditionOutcome configValidationOutcome = Hazelcast4ClientValidation.clientConfigOutcome(context, + HAZELCAST_CONFIG_PROPERTY, startConditionMessage()); + return (configValidationOutcome != null) ? configValidationOutcome : ConditionOutcome + .match(startConditionMessage().foundExactly("property " + HAZELCAST_CONFIG_PROPERTY)); + } + return getResourceOutcome(context, metadata); + } + + static class Hazelcast4ClientValidation { + + static ConditionOutcome clientConfigOutcome(ConditionContext context, String propertyName, Builder builder) { + String resourcePath = context.getEnvironment().getProperty(propertyName); + Resource resource = context.getResourceLoader().getResource(resourcePath); + if (!resource.exists()) { + return ConditionOutcome.noMatch(builder.because("Hazelcast configuration does not exist")); + } + try (InputStream in = resource.getInputStream()) { + boolean clientConfig = new ClientConfigRecognizer().isRecognized(new ConfigStream(in)); + return new ConditionOutcome(clientConfig, existingConfigurationOutcome(resource, clientConfig)); + } + catch (Throwable ex) { // Hazelcast 4 specific API + return null; + } + } + + private static String existingConfigurationOutcome(Resource resource, boolean client) throws IOException { + URL location = resource.getURL(); + return client ? "Hazelcast client configuration detected at '" + location + "'" + : "Hazelcast server configuration detected at '" + location + "'"; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastClientConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastClientConfiguration.java index d3ed1a3df479..897085d6df37 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastClientConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastClientConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,9 +17,12 @@ package org.springframework.boot.autoconfigure.hazelcast; import java.io.IOException; +import java.net.URL; import com.hazelcast.client.HazelcastClient; import com.hazelcast.client.config.ClientConfig; +import com.hazelcast.client.config.XmlClientConfigBuilder; +import com.hazelcast.client.config.YamlClientConfigBuilder; import com.hazelcast.core.HazelcastInstance; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -29,6 +32,8 @@ import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.util.StringUtils; /** * Configuration for Hazelcast client. @@ -43,18 +48,34 @@ class HazelcastClientConfiguration { static final String CONFIG_SYSTEM_PROPERTY = "hazelcast.client.config"; + private static HazelcastInstance getHazelcastInstance(ClientConfig config) { + if (StringUtils.hasText(config.getInstanceName())) { + return HazelcastClient.getOrCreateHazelcastClient(config); + } + return HazelcastClient.newHazelcastClient(config); + } + @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(ClientConfig.class) - @Conditional(ConfigAvailableCondition.class) + @Conditional(HazelcastClientConfigAvailableCondition.class) static class HazelcastClientConfigFileConfiguration { @Bean - HazelcastInstance hazelcastInstance(HazelcastProperties properties) throws IOException { - Resource config = properties.resolveConfigLocation(); - if (config != null) { - return new HazelcastClientFactory(config).getHazelcastInstance(); + HazelcastInstance hazelcastInstance(HazelcastProperties properties, ResourceLoader resourceLoader) + throws IOException { + Resource configLocation = properties.resolveConfigLocation(); + ClientConfig config = (configLocation != null) ? loadClientConfig(configLocation) : ClientConfig.load(); + config.setClassLoader(resourceLoader.getClassLoader()); + return getHazelcastInstance(config); + } + + private ClientConfig loadClientConfig(Resource configLocation) throws IOException { + URL configUrl = configLocation.getURL(); + String configFileName = configUrl.getPath(); + if (configFileName.endsWith(".yaml")) { + return new YamlClientConfigBuilder(configUrl).build(); } - return HazelcastClient.newHazelcastClient(); + return new XmlClientConfigBuilder(configUrl).build(); } } @@ -65,20 +86,7 @@ static class HazelcastClientConfigConfiguration { @Bean HazelcastInstance hazelcastInstance(ClientConfig config) { - return new HazelcastClientFactory(config).getHazelcastInstance(); - } - - } - - /** - * {@link HazelcastConfigResourceCondition} that checks if the - * {@code spring.hazelcast.config} configuration key is defined. - */ - static class ConfigAvailableCondition extends HazelcastConfigResourceCondition { - - ConfigAvailableCondition() { - super(CONFIG_SYSTEM_PROPERTY, "file:./hazelcast-client.xml", "classpath:/hazelcast-client.xml", - "file:./hazelcast-client.yaml", "classpath:/hazelcast-client.yaml"); + return getHazelcastInstance(config); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastClientFactory.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastClientFactory.java index 7bf5e0d305da..248699817d3e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastClientFactory.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastClientFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,9 @@ * * @author Vedran Pavic * @since 2.0.0 + * @deprecated since 2.4.3 for removal in 2.6 in favor of using the Hazelcast API directly */ +@Deprecated public class HazelcastClientFactory { private final ClientConfig clientConfig; @@ -72,7 +74,7 @@ private ClientConfig getClientConfig(Resource clientConfigLocation) throws IOExc */ public HazelcastInstance getHazelcastInstance() { if (StringUtils.hasText(this.clientConfig.getInstanceName())) { - return HazelcastClient.getHazelcastClientByName(this.clientConfig.getInstanceName()); + return HazelcastClient.getOrCreateHazelcastClient(this.clientConfig); } return HazelcastClient.newHazelcastClient(this.clientConfig); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastConfigResourceCondition.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastConfigResourceCondition.java index f49a89d7c062..359810d59124 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastConfigResourceCondition.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastConfigResourceCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,10 +35,12 @@ */ public abstract class HazelcastConfigResourceCondition extends ResourceCondition { + protected static final String HAZELCAST_CONFIG_PROPERTY = "spring.hazelcast.config"; + private final String configSystemProperty; protected HazelcastConfigResourceCondition(String configSystemProperty, String... resourceLocations) { - super("Hazelcast", "spring.hazelcast.config", resourceLocations); + super("Hazelcast", HAZELCAST_CONFIG_PROPERTY, resourceLocations); Assert.notNull(configSystemProperty, "ConfigSystemProperty must not be null"); this.configSystemProperty = configSystemProperty; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastInstanceFactory.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastInstanceFactory.java index be66d2dffb4a..40fa78ac3b4d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastInstanceFactory.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastInstanceFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,9 @@ * @author Stephane Nicoll * @author Phillip Webb * @since 1.3.0 + * @deprecated since 2.4.3 for removal in 2.6 in favor of using the Hazelcast API directly */ +@Deprecated public class HazelcastInstanceFactory { private final Config config; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastJpaDependencyAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastJpaDependencyAutoConfiguration.java index 38f84112a01c..46930efba997 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastJpaDependencyAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastJpaDependencyAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,8 +24,8 @@ import org.springframework.boot.autoconfigure.condition.AllNestedConditions; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.data.jpa.EntityManagerFactoryDependsOnPostProcessor; import org.springframework.boot.autoconfigure.hazelcast.HazelcastJpaDependencyAutoConfiguration.HazelcastInstanceEntityManagerFactoryDependsOnPostProcessor; +import org.springframework.boot.autoconfigure.orm.jpa.EntityManagerFactoryDependsOnPostProcessor; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastServerConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastServerConfiguration.java index 0a552bb19d90..63fe94612e71 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastServerConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastServerConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,11 @@ package org.springframework.boot.autoconfigure.hazelcast; import java.io.IOException; +import java.net.URL; import com.hazelcast.config.Config; +import com.hazelcast.config.XmlConfigBuilder; +import com.hazelcast.config.YamlConfigBuilder; import com.hazelcast.core.Hazelcast; import com.hazelcast.core.HazelcastInstance; @@ -28,6 +31,9 @@ import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.util.ResourceUtils; +import org.springframework.util.StringUtils; /** * Configuration for Hazelcast server. @@ -41,18 +47,45 @@ class HazelcastServerConfiguration { static final String CONFIG_SYSTEM_PROPERTY = "hazelcast.config"; + private static HazelcastInstance getHazelcastInstance(Config config) { + if (StringUtils.hasText(config.getInstanceName())) { + return Hazelcast.getOrCreateHazelcastInstance(config); + } + return Hazelcast.newHazelcastInstance(config); + } + @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(Config.class) @Conditional(ConfigAvailableCondition.class) static class HazelcastServerConfigFileConfiguration { @Bean - HazelcastInstance hazelcastInstance(HazelcastProperties properties) throws IOException { - Resource config = properties.resolveConfigLocation(); - if (config != null) { - return new HazelcastInstanceFactory(config).getHazelcastInstance(); + HazelcastInstance hazelcastInstance(HazelcastProperties properties, ResourceLoader resourceLoader) + throws IOException { + Resource configLocation = properties.resolveConfigLocation(); + Config config = (configLocation != null) ? loadConfig(configLocation) : Config.load(); + config.setClassLoader(resourceLoader.getClassLoader()); + return getHazelcastInstance(config); + } + + private Config loadConfig(Resource configLocation) throws IOException { + URL configUrl = configLocation.getURL(); + Config config = loadConfig(configUrl); + if (ResourceUtils.isFileURL(configUrl)) { + config.setConfigurationFile(configLocation.getFile()); + } + else { + config.setConfigurationUrl(configUrl); + } + return config; + } + + private static Config loadConfig(URL configUrl) throws IOException { + String configFileName = configUrl.getPath(); + if (configFileName.endsWith(".yaml")) { + return new YamlConfigBuilder(configUrl).build(); } - return Hazelcast.newHazelcastInstance(); + return new XmlConfigBuilder(configUrl).build(); } } @@ -63,7 +96,7 @@ static class HazelcastServerConfigConfiguration { @Bean HazelcastInstance hazelcastInstance(Config config) { - return new HazelcastInstanceFactory(config).getHazelcastInstance(); + return getHazelcastInstance(config); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/http/HttpMessageConverters.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/http/HttpMessageConverters.java index b6bd8be33843..2ca518f80005 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/http/HttpMessageConverters.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/http/HttpMessageConverters.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,21 +16,20 @@ package org.springframework.boot.autoconfigure.http; -import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Map; -import org.springframework.http.converter.FormHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; import org.springframework.http.converter.xml.AbstractXmlHttpMessageConverter; import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; import org.springframework.util.ClassUtils; -import org.springframework.util.ReflectionUtils; import org.springframework.web.client.RestTemplate; import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; @@ -44,8 +43,8 @@ * needed, otherwise default converters will be used. *

    * NOTE: The default converters used are the same as standard Spring MVC (see - * {@link WebMvcConfigurationSupport#getMessageConverters} with some slight re-ordering to - * put XML converters at the back of the list. + * {@link WebMvcConfigurationSupport}) with some slight re-ordering to put XML converters + * at the back of the list. * * @author Dave Syer * @author Phillip Webb @@ -66,6 +65,15 @@ public class HttpMessageConverters implements Iterable> NON_REPLACING_CONVERTERS = Collections.unmodifiableList(nonReplacingConverters); } + private static final Map, Class> EQUIVALENT_CONVERTERS; + + static { + Map, Class> equivalentConverters = new HashMap<>(); + putIfExists(equivalentConverters, "org.springframework.http.converter.json.MappingJackson2HttpMessageConverter", + "org.springframework.http.converter.json.GsonHttpMessageConverter"); + EQUIVALENT_CONVERTERS = Collections.unmodifiableMap(equivalentConverters); + } + private final List> converters; /** @@ -135,24 +143,22 @@ private boolean isReplacement(HttpMessageConverter defaultConverter, HttpMess return false; } } - return ClassUtils.isAssignableValue(defaultConverter.getClass(), candidate); + Class converterClass = defaultConverter.getClass(); + if (ClassUtils.isAssignableValue(converterClass, candidate)) { + return true; + } + Class equivalentClass = EQUIVALENT_CONVERTERS.get(converterClass); + return equivalentClass != null && ClassUtils.isAssignableValue(equivalentClass, candidate); } private void configurePartConverters(AllEncompassingFormHttpMessageConverter formConverter, Collection> converters) { - List> partConverters = extractPartConverters(formConverter); + List> partConverters = formConverter.getPartConverters(); List> combinedConverters = getCombinedConverters(converters, partConverters); combinedConverters = postProcessPartConverters(combinedConverters); formConverter.setPartConverters(combinedConverters); } - @SuppressWarnings("unchecked") - private List> extractPartConverters(FormHttpMessageConverter formConverter) { - Field field = ReflectionUtils.findField(FormHttpMessageConverter.class, "partConverters"); - ReflectionUtils.makeAccessible(field); - return (List>) ReflectionUtils.getField(field, formConverter); - } - /** * Method that can be used to post-process the {@link HttpMessageConverter} list * before it is used. @@ -230,4 +236,13 @@ private static void addClassIfExists(List> list, String className) { } } + private static void putIfExists(Map, Class> map, String keyClassName, String valueClassName) { + try { + map.put(Class.forName(keyClassName), Class.forName(valueClassName)); + } + catch (ClassNotFoundException | NoClassDefFoundError ex) { + // Ignore + } + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/http/HttpMessageConvertersAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/http/HttpMessageConvertersAutoConfiguration.java index cf98f5719a83..255f69751ca1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/http/HttpMessageConvertersAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/http/HttpMessageConvertersAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,11 +30,13 @@ import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration.NotReactiveWebApplicationCondition; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration; -import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.web.servlet.server.Encoding; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.core.env.Environment; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; @@ -60,7 +62,7 @@ JsonbHttpMessageConvertersConfiguration.class }) public class HttpMessageConvertersAutoConfiguration { - static final String PREFERRED_MAPPER_PROPERTY = "spring.http.converters.preferred-json-mapper"; + static final String PREFERRED_MAPPER_PROPERTY = "spring.mvc.converters.preferred-json-mapper"; @Bean @ConditionalOnMissingBean @@ -70,14 +72,13 @@ public HttpMessageConverters messageConverters(ObjectProvider mapping; - - public Charset getCharset() { - return this.charset; - } - - public void setCharset(Charset charset) { - this.charset = charset; - } - - public boolean isForce() { - return Boolean.TRUE.equals(this.force); - } - - public void setForce(boolean force) { - this.force = force; - } - - public boolean isForceRequest() { - return Boolean.TRUE.equals(this.forceRequest); - } - - public void setForceRequest(boolean forceRequest) { - this.forceRequest = forceRequest; - } - - public boolean isForceResponse() { - return Boolean.TRUE.equals(this.forceResponse); - } - - public void setForceResponse(boolean forceResponse) { - this.forceResponse = forceResponse; - } - - public Map getMapping() { - return this.mapping; - } - - public void setMapping(Map mapping) { - this.mapping = mapping; - } - - public boolean shouldForce(Type type) { - Boolean force = (type != Type.REQUEST) ? this.forceResponse : this.forceRequest; - if (force == null) { - force = this.force; - } - if (force == null) { - force = (type == Type.REQUEST); - } - return force; - } - - public enum Type { - - REQUEST, RESPONSE - - } - - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/http/codec/CodecsAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/http/codec/CodecsAutoConfiguration.java index 9fbc6be3b835..7a9f65839348 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/http/codec/CodecsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/http/codec/CodecsAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,6 @@ import org.springframework.boot.autoconfigure.codec.CodecProperties; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.http.HttpProperties; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.PropertyMapper; @@ -49,7 +48,7 @@ @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ CodecConfigurer.class, WebClient.class }) @AutoConfigureAfter(JacksonAutoConfiguration.class) -@EnableConfigurationProperties({ HttpProperties.class, CodecProperties.class }) +@EnableConfigurationProperties(CodecProperties.class) public class CodecsAutoConfiguration { private static final MimeType[] EMPTY_MIME_TYPES = {}; @@ -76,11 +75,11 @@ static class DefaultCodecsConfiguration { @Bean @Order(0) - CodecCustomizer defaultCodecCustomizer(HttpProperties httpProperties, CodecProperties codecProperties) { + CodecCustomizer defaultCodecCustomizer(CodecProperties codecProperties) { return (configurer) -> { PropertyMapper map = PropertyMapper.get(); CodecConfigurer.DefaultCodecs defaultCodecs = configurer.defaultCodecs(); - defaultCodecs.enableLoggingRequestDetails(httpProperties.isLogRequestDetails()); + defaultCodecs.enableLoggingRequestDetails(codecProperties.isLogRequestDetails()); map.from(codecProperties.getMaxInMemorySize()).whenNonNull().asInt(DataSize::toBytes) .to(defaultCodecs::maxInMemorySize); }; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbAutoConfiguration.java index e64f047e9559..e1cf2ec7871c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,10 +45,12 @@ public class InfluxDbAutoConfiguration { @Bean @ConditionalOnMissingBean @ConditionalOnProperty("spring.influx.url") - public InfluxDB influxDb(InfluxDbProperties properties, - ObjectProvider builder) { - return new InfluxDBImpl(properties.getUrl(), properties.getUser(), properties.getPassword(), + public InfluxDB influxDb(InfluxDbProperties properties, ObjectProvider builder, + ObjectProvider customizers) { + InfluxDB influxDb = new InfluxDBImpl(properties.getUrl(), properties.getUser(), properties.getPassword(), determineBuilder(builder.getIfAvailable())); + customizers.orderedStream().forEach((customizer) -> customizer.customize(influxDb)); + return influxDb; } private static OkHttpClient.Builder determineBuilder(InfluxDbOkHttpClientBuilderProvider builder) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbCustomizer.java new file mode 100644 index 000000000000..9e46dd17fa3e --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbCustomizer.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.influx; + +import org.influxdb.InfluxDB; + +/** + * Callback interface that can be implemented by beans wishing to further customize + * {@code InfluxDB} whilst retaining default auto-configuration. + * + * @author Eddú Meléndez + * @since 2.5.0 + */ +@FunctionalInterface +public interface InfluxDbCustomizer { + + /** + * Customize the {@link InfluxDB}. + * @param influxDb the InfluxDB instance to customize + */ + void customize(InfluxDB influxDb); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbProperties.java index 2cb481b790f7..d8a4c07d5b68 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationAutoConfiguration.java index 7b9b31ace538..456e9b40a808 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,8 +19,12 @@ import javax.management.MBeanServer; import javax.sql.DataSource; +import io.rsocket.transport.netty.server.TcpServerTransport; + import org.springframework.beans.factory.BeanFactory; import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +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; @@ -29,8 +33,13 @@ import org.springframework.boot.autoconfigure.condition.SearchStrategy; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration; +import org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration; +import org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.task.TaskSchedulerBuilder; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.core.env.Environment; @@ -38,10 +47,20 @@ import org.springframework.integration.config.EnableIntegration; import org.springframework.integration.config.EnableIntegrationManagement; import org.springframework.integration.config.IntegrationManagementConfigurer; +import org.springframework.integration.context.IntegrationContextUtils; import org.springframework.integration.gateway.GatewayProxyFactoryBean; import org.springframework.integration.jdbc.store.JdbcMessageStore; import org.springframework.integration.jmx.config.EnableIntegrationMBeanExport; import org.springframework.integration.monitor.IntegrationMBeanExporter; +import org.springframework.integration.rsocket.ClientRSocketConnector; +import org.springframework.integration.rsocket.IntegrationRSocketEndpoint; +import org.springframework.integration.rsocket.ServerRSocketConnector; +import org.springframework.integration.rsocket.ServerRSocketMessageHandler; +import org.springframework.integration.rsocket.outbound.RSocketOutboundGateway; +import org.springframework.messaging.rsocket.RSocketRequester; +import org.springframework.messaging.rsocket.RSocketStrategies; +import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.util.StringUtils; /** @@ -58,9 +77,33 @@ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(EnableIntegration.class) @EnableConfigurationProperties(IntegrationProperties.class) -@AutoConfigureAfter({ DataSourceAutoConfiguration.class, JmxAutoConfiguration.class }) +@AutoConfigureAfter({ DataSourceAutoConfiguration.class, JmxAutoConfiguration.class, + TaskSchedulingAutoConfiguration.class }) public class IntegrationAutoConfiguration { + @Bean(name = IntegrationContextUtils.INTEGRATION_GLOBAL_PROPERTIES_BEAN_NAME) + @ConditionalOnMissingBean(name = IntegrationContextUtils.INTEGRATION_GLOBAL_PROPERTIES_BEAN_NAME) + public static org.springframework.integration.context.IntegrationProperties integrationGlobalProperties( + IntegrationProperties properties) { + org.springframework.integration.context.IntegrationProperties integrationProperties = new org.springframework.integration.context.IntegrationProperties(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(properties.getChannel().isAutoCreate()).to(integrationProperties::setChannelsAutoCreate); + map.from(properties.getChannel().getMaxUnicastSubscribers()) + .to(integrationProperties::setChannelsMaxUnicastSubscribers); + map.from(properties.getChannel().getMaxBroadcastSubscribers()) + .to(integrationProperties::setChannelsMaxBroadcastSubscribers); + map.from(properties.getError().isRequireSubscribers()) + .to(integrationProperties::setErrorChannelRequireSubscribers); + map.from(properties.getError().isIgnoreFailures()).to(integrationProperties::setErrorChannelIgnoreFailures); + map.from(properties.getEndpoint().isThrowExceptionOnLateReply()) + .to(integrationProperties::setMessagingTemplateThrowExceptionOnLateReply); + map.from(properties.getEndpoint().getReadOnlyHeaders()).as(StringUtils::toStringArray) + .to(integrationProperties::setReadOnlyHeaders); + map.from(properties.getEndpoint().getNoAutoStartup()).as(StringUtils::toStringArray) + .to(integrationProperties::setNoAutoStartupEndpoints); + return integrationProperties; + } + /** * Basic Spring Integration configuration. */ @@ -70,6 +113,22 @@ protected static class IntegrationConfiguration { } + /** + * Expose a standard {@link ThreadPoolTaskScheduler} if the user has not enabled task + * scheduling explicitly. + */ + @Configuration(proxyBeanMethods = false) + @ConditionalOnBean(TaskSchedulerBuilder.class) + @ConditionalOnMissingBean(name = IntegrationContextUtils.TASK_SCHEDULER_BEAN_NAME) + protected static class IntegrationTaskSchedulerConfiguration { + + @Bean(name = IntegrationContextUtils.TASK_SCHEDULER_BEAN_NAME) + public ThreadPoolTaskScheduler taskScheduler(TaskSchedulerBuilder builder) { + return builder.build(); + } + + } + /** * Spring Integration JMX configuration. */ @@ -104,7 +163,7 @@ public IntegrationMBeanExporter integrationMbeanExporter(BeanFactory beanFactory protected static class IntegrationManagementConfiguration { @Configuration(proxyBeanMethods = false) - @EnableIntegrationManagement(defaultCountsEnabled = "true") + @EnableIntegrationManagement protected static class EnableIntegrationManagementConfiguration { } @@ -138,4 +197,100 @@ public IntegrationDataSourceInitializer integrationDataSourceInitializer(DataSou } + /** + * Integration RSocket configuration. + */ + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass({ IntegrationRSocketEndpoint.class, RSocketRequester.class, io.rsocket.RSocket.class }) + @Conditional(IntegrationRSocketConfiguration.AnyRSocketChannelAdapterAvailable.class) + protected static class IntegrationRSocketConfiguration { + + /** + * Check if either an {@link IntegrationRSocketEndpoint} or + * {@link RSocketOutboundGateway} bean is available. + */ + static class AnyRSocketChannelAdapterAvailable extends AnyNestedCondition { + + AnyRSocketChannelAdapterAvailable() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnBean(IntegrationRSocketEndpoint.class) + static class IntegrationRSocketEndpointAvailable { + + } + + @ConditionalOnBean(RSocketOutboundGateway.class) + static class RSocketOutboundGatewayAvailable { + + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(TcpServerTransport.class) + @AutoConfigureBefore(RSocketMessagingAutoConfiguration.class) + protected static class IntegrationRSocketServerConfiguration { + + @Bean + @ConditionalOnMissingBean(ServerRSocketMessageHandler.class) + public RSocketMessageHandler serverRSocketMessageHandler(RSocketStrategies rSocketStrategies, + IntegrationProperties integrationProperties) { + + RSocketMessageHandler messageHandler = new ServerRSocketMessageHandler( + integrationProperties.getRsocket().getServer().isMessageMappingEnabled()); + messageHandler.setRSocketStrategies(rSocketStrategies); + return messageHandler; + } + + @Bean + @ConditionalOnMissingBean + public ServerRSocketConnector serverRSocketConnector(ServerRSocketMessageHandler messageHandler) { + return new ServerRSocketConnector(messageHandler); + } + + } + + @Configuration(proxyBeanMethods = false) + protected static class IntegrationRSocketClientConfiguration { + + @Bean + @ConditionalOnMissingBean + @Conditional(RemoteRSocketServerAddressConfigured.class) + public ClientRSocketConnector clientRSocketConnector(IntegrationProperties integrationProperties, + RSocketStrategies rSocketStrategies) { + + IntegrationProperties.RSocket.Client client = integrationProperties.getRsocket().getClient(); + ClientRSocketConnector clientRSocketConnector = (client.getUri() != null) + ? new ClientRSocketConnector(client.getUri()) + : new ClientRSocketConnector(client.getHost(), client.getPort()); + clientRSocketConnector.setRSocketStrategies(rSocketStrategies); + return clientRSocketConnector; + } + + /** + * Check if a remote address is configured for the RSocket Integration client. + */ + static class RemoteRSocketServerAddressConfigured extends AnyNestedCondition { + + RemoteRSocketServerAddressConfigured() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnProperty(prefix = "spring.integration.rsocket.client", name = "uri") + static class WebSocketAddressConfigured { + + } + + @ConditionalOnProperty(prefix = "spring.integration.rsocket.client", name = { "host", "port" }) + static class TcpAddressConfigured { + + } + + } + + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationDataSourceInitializer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationDataSourceInitializer.java index b11f56bfb869..e3299e848c21 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationDataSourceInitializer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationDataSourceInitializer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import org.springframework.boot.jdbc.DataSourceInitializationMode; import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * Initializer for Spring Integration schema. @@ -50,4 +51,13 @@ protected String getSchemaLocation() { return this.properties.getSchema(); } + @Override + protected String getDatabaseName() { + String platform = this.properties.getPlatform(); + if (StringUtils.hasText(platform)) { + return platform; + } + return super.getDatabaseName(); + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationProperties.java index b27c1d9c1b31..0ec22ba33966 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,10 @@ package org.springframework.boot.autoconfigure.integration; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; + import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceInitializationMode; @@ -24,17 +28,164 @@ * * @author Vedran Pavic * @author Stephane Nicoll + * @author Artem Bilan * @since 2.0.0 */ @ConfigurationProperties(prefix = "spring.integration") public class IntegrationProperties { + private final Channel channel = new Channel(); + + private final Endpoint endpoint = new Endpoint(); + + private final Error error = new Error(); + private final Jdbc jdbc = new Jdbc(); + private final RSocket rsocket = new RSocket(); + + public Channel getChannel() { + return this.channel; + } + + public Endpoint getEndpoint() { + return this.endpoint; + } + + public Error getError() { + return this.error; + } + public Jdbc getJdbc() { return this.jdbc; } + public RSocket getRsocket() { + return this.rsocket; + } + + public static class Channel { + + /** + * Whether to create input channels if necessary. + */ + private boolean autoCreate = true; + + /** + * Default number of subscribers allowed on, for example, a 'DirectChannel'. + */ + private int maxUnicastSubscribers = Integer.MAX_VALUE; + + /** + * Default number of subscribers allowed on, for example, a + * 'PublishSubscribeChannel'. + */ + private int maxBroadcastSubscribers = Integer.MAX_VALUE; + + public void setAutoCreate(boolean autoCreate) { + this.autoCreate = autoCreate; + } + + public boolean isAutoCreate() { + return this.autoCreate; + } + + public void setMaxUnicastSubscribers(int maxUnicastSubscribers) { + this.maxUnicastSubscribers = maxUnicastSubscribers; + } + + public int getMaxUnicastSubscribers() { + return this.maxUnicastSubscribers; + } + + public void setMaxBroadcastSubscribers(int maxBroadcastSubscribers) { + this.maxBroadcastSubscribers = maxBroadcastSubscribers; + } + + public int getMaxBroadcastSubscribers() { + return this.maxBroadcastSubscribers; + } + + } + + public static class Endpoint { + + /** + * Whether to throw an exception when a reply is not expected anymore by a + * gateway. + */ + private boolean throwExceptionOnLateReply = false; + + /** + * A comma-separated list of message header names that should not be populated + * into Message instances during a header copying operation. + */ + private List readOnlyHeaders = new ArrayList<>(); + + /** + * A comma-separated list of endpoint bean names patterns that should not be + * started automatically during application startup. + */ + private List noAutoStartup = new ArrayList<>(); + + public void setThrowExceptionOnLateReply(boolean throwExceptionOnLateReply) { + this.throwExceptionOnLateReply = throwExceptionOnLateReply; + } + + public boolean isThrowExceptionOnLateReply() { + return this.throwExceptionOnLateReply; + } + + public List getReadOnlyHeaders() { + return this.readOnlyHeaders; + } + + public void setReadOnlyHeaders(List readOnlyHeaders) { + this.readOnlyHeaders = readOnlyHeaders; + } + + public List getNoAutoStartup() { + return this.noAutoStartup; + } + + public void setNoAutoStartup(List noAutoStartup) { + this.noAutoStartup = noAutoStartup; + } + + } + + public static class Error { + + /** + * Whether to not silently ignore messages on the global 'errorChannel' when they + * are no subscribers. + */ + private boolean requireSubscribers = true; + + /** + * Whether to ignore failures for one or more of the handlers of the global + * 'errorChannel'. + */ + private boolean ignoreFailures = true; + + public boolean isRequireSubscribers() { + return this.requireSubscribers; + } + + public void setRequireSubscribers(boolean requireSubscribers) { + this.requireSubscribers = requireSubscribers; + } + + public boolean isIgnoreFailures() { + return this.ignoreFailures; + } + + public void setIgnoreFailures(boolean ignoreFailures) { + this.ignoreFailures = ignoreFailures; + } + + } + public static class Jdbc { private static final String DEFAULT_SCHEMA_LOCATION = "classpath:org/springframework/" @@ -45,6 +196,12 @@ public static class Jdbc { */ private String schema = DEFAULT_SCHEMA_LOCATION; + /** + * Platform to use in initialization scripts if the @@platform@@ placeholder is + * used. Auto-detected by default. + */ + private String platform; + /** * Database schema initialization mode. */ @@ -58,6 +215,14 @@ public void setSchema(String schema) { this.schema = schema; } + public String getPlatform() { + return this.platform; + } + + public void setPlatform(String platform) { + this.platform = platform; + } + public DataSourceInitializationMode getInitializeSchema() { return this.initializeSchema; } @@ -68,4 +233,80 @@ public void setInitializeSchema(DataSourceInitializationMode initializeSchema) { } + public static class RSocket { + + private final Client client = new Client(); + + private final Server server = new Server(); + + public Client getClient() { + return this.client; + } + + public Server getServer() { + return this.server; + } + + public static class Client { + + /** + * TCP RSocket server host to connect to. + */ + private String host; + + /** + * TCP RSocket server port to connect to. + */ + private Integer port; + + /** + * WebSocket RSocket server uri to connect to. + */ + private URI uri; + + public void setHost(String host) { + this.host = host; + } + + public String getHost() { + return this.host; + } + + public void setPort(Integer port) { + this.port = port; + } + + public Integer getPort() { + return this.port; + } + + public void setUri(URI uri) { + this.uri = uri; + } + + public URI getUri() { + return this.uri; + } + + } + + public static class Server { + + /** + * Whether to handle message mapping for RSocket via Spring Integration. + */ + private boolean messageMappingEnabled; + + public boolean isMessageMappingEnabled() { + return this.messageMappingEnabled; + } + + public void setMessageMappingEnabled(boolean messageMappingEnabled) { + this.messageMappingEnabled = messageMappingEnabled; + } + + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationPropertiesEnvironmentPostProcessor.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationPropertiesEnvironmentPostProcessor.java new file mode 100644 index 000000000000..3ed91d7a501c --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationPropertiesEnvironmentPostProcessor.java @@ -0,0 +1,113 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.integration; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.boot.env.OriginTrackedMapPropertySource; +import org.springframework.boot.env.PropertiesPropertySourceLoader; +import org.springframework.boot.origin.Origin; +import org.springframework.boot.origin.OriginLookup; +import org.springframework.core.Ordered; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.PropertySource; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.integration.context.IntegrationProperties; + +/** + * An {@link EnvironmentPostProcessor} that maps the configuration of + * {@code META-INF/spring.integration.properties} in the environment. + * + * @author Artem Bilan + * @author Stephane Nicoll + */ +class IntegrationPropertiesEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered { + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + Resource resource = new ClassPathResource("META-INF/spring.integration.properties"); + if (resource.exists()) { + registerIntegrationPropertiesPropertySource(environment, resource); + } + } + + protected void registerIntegrationPropertiesPropertySource(ConfigurableEnvironment environment, Resource resource) { + PropertiesPropertySourceLoader loader = new PropertiesPropertySourceLoader(); + try { + OriginTrackedMapPropertySource propertyFileSource = (OriginTrackedMapPropertySource) loader + .load("META-INF/spring.integration.properties", resource).get(0); + environment.getPropertySources().addLast(new IntegrationPropertiesPropertySource(propertyFileSource)); + } + catch (IOException ex) { + throw new IllegalStateException("Failed to load integration properties from " + resource, ex); + } + } + + private static final class IntegrationPropertiesPropertySource extends PropertySource> + implements OriginLookup { + + private static final String PREFIX = "spring.integration."; + + private static final Map KEYS_MAPPING; + + static { + Map mappings = new HashMap<>(); + mappings.put(PREFIX + "channel.auto-create", IntegrationProperties.CHANNELS_AUTOCREATE); + mappings.put(PREFIX + "channel.max-unicast-subscribers", + IntegrationProperties.CHANNELS_MAX_UNICAST_SUBSCRIBERS); + mappings.put(PREFIX + "channel.max-broadcast-subscribers", + IntegrationProperties.CHANNELS_MAX_BROADCAST_SUBSCRIBERS); + mappings.put(PREFIX + "error.require-subscribers", IntegrationProperties.ERROR_CHANNEL_REQUIRE_SUBSCRIBERS); + mappings.put(PREFIX + "error.ignore-failures", IntegrationProperties.ERROR_CHANNEL_IGNORE_FAILURES); + mappings.put(PREFIX + "endpoint.throw-exception-on-late-reply", + IntegrationProperties.THROW_EXCEPTION_ON_LATE_REPLY); + mappings.put(PREFIX + "endpoint.read-only-headers", IntegrationProperties.READ_ONLY_HEADERS); + mappings.put(PREFIX + "endpoint.no-auto-startup", IntegrationProperties.ENDPOINTS_NO_AUTO_STARTUP); + KEYS_MAPPING = Collections.unmodifiableMap(mappings); + } + + private final OriginTrackedMapPropertySource delegate; + + IntegrationPropertiesPropertySource(OriginTrackedMapPropertySource delegate) { + super("META-INF/spring.integration.properties", delegate.getSource()); + this.delegate = delegate; + } + + @Override + public Object getProperty(String name) { + return this.delegate.getProperty(KEYS_MAPPING.get(name)); + } + + @Override + public Origin getOrigin(String key) { + return this.delegate.getOrigin(KEYS_MAPPING.get(key)); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java index 79caab4a3eb7..915eb534c9f8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,14 +34,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.datatype.joda.cfg.JacksonJodaDateFormat; -import com.fasterxml.jackson.datatype.joda.ser.DateTimeSerializer; import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.joda.time.DateTime; -import org.joda.time.format.DateTimeFormat; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.BeanFactoryUtils; @@ -88,6 +81,7 @@ public class JacksonAutoConfiguration { static { Map featureDefaults = new HashMap<>(); featureDefaults.put(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + featureDefaults.put(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false); FEATURE_DEFAULTS = Collections.unmodifiableMap(featureDefaults); } @@ -109,49 +103,6 @@ ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) { } - @Deprecated - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass({ Jackson2ObjectMapperBuilder.class, DateTime.class, DateTimeSerializer.class, - JacksonJodaDateFormat.class }) - static class JodaDateTimeJacksonConfiguration { - - private static final Log logger = LogFactory.getLog(JodaDateTimeJacksonConfiguration.class); - - @Bean - SimpleModule jodaDateTimeSerializationModule(JacksonProperties jacksonProperties) { - logger.warn("Auto-configuration of Jackson's Joda-Time integration is deprecated in favor of using " - + "java.time (JSR-310)."); - SimpleModule module = new SimpleModule(); - JacksonJodaDateFormat jacksonJodaFormat = getJacksonJodaDateFormat(jacksonProperties); - if (jacksonJodaFormat != null) { - module.addSerializer(DateTime.class, new DateTimeSerializer(jacksonJodaFormat, 0)); - } - return module; - } - - private JacksonJodaDateFormat getJacksonJodaDateFormat(JacksonProperties jacksonProperties) { - if (jacksonProperties.getJodaDateTimeFormat() != null) { - return new JacksonJodaDateFormat( - DateTimeFormat.forPattern(jacksonProperties.getJodaDateTimeFormat()).withZoneUTC()); - } - if (jacksonProperties.getDateFormat() != null) { - try { - return new JacksonJodaDateFormat( - DateTimeFormat.forPattern(jacksonProperties.getDateFormat()).withZoneUTC()); - } - catch (IllegalArgumentException ex) { - if (logger.isWarnEnabled()) { - logger.warn("spring.jackson.date-format could not be used to " - + "configure formatting of Joda's DateTime. You may want " - + "to configure spring.jackson.joda-date-time-format as well."); - } - } - } - return null; - } - - } - @Configuration(proxyBeanMethods = false) @ConditionalOnClass(ParameterNamesModule.class) static class ParameterNamesModuleConfiguration { @@ -219,7 +170,6 @@ public int getOrder() { @Override public void customize(Jackson2ObjectMapperBuilder builder) { - if (this.jacksonProperties.getDefaultPropertyInclusion() != null) { builder.serializationInclusion(this.jacksonProperties.getDefaultPropertyInclusion()); } @@ -306,10 +256,8 @@ private void configurePropertyNamingStrategyClass(Jackson2ObjectMapperBuilder bu private void configurePropertyNamingStrategyField(Jackson2ObjectMapperBuilder builder, String fieldName) { // Find the field (this way we automatically support new constants // that may be added by Jackson in the future) - Field field = ReflectionUtils.findField(PropertyNamingStrategy.class, fieldName, - PropertyNamingStrategy.class); - Assert.notNull(field, () -> "Constant named '" + fieldName + "' not found on " - + PropertyNamingStrategy.class.getName()); + Field field = findPropertyNamingStrategyField(fieldName); + Assert.notNull(field, () -> "Constant named '" + fieldName + "' not found"); try { builder.propertyNamingStrategy((PropertyNamingStrategy) field.get(null)); } @@ -318,6 +266,17 @@ private void configurePropertyNamingStrategyField(Jackson2ObjectMapperBuilder bu } } + private Field findPropertyNamingStrategyField(String fieldName) { + try { + return ReflectionUtils.findField(com.fasterxml.jackson.databind.PropertyNamingStrategies.class, + fieldName, PropertyNamingStrategy.class); + } + catch (NoClassDefFoundError ex) { // Fallback pre Jackson 2.12 + return ReflectionUtils.findField(PropertyNamingStrategy.class, fieldName, + PropertyNamingStrategy.class); + } + } + private void configureModules(Jackson2ObjectMapperBuilder builder) { Collection moduleBeans = getBeans(this.applicationContext, Module.class); builder.modulesToInstall(moduleBeans.toArray(new Module[0])); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java index c19f9c3efbf4..17d55fbfadfe 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,6 @@ import com.fasterxml.jackson.databind.SerializationFeature; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; /** * Configuration properties to configure Jackson. @@ -46,19 +45,13 @@ public class JacksonProperties { /** * Date format string or a fully-qualified date format class name. For instance, - * `yyyy-MM-dd HH:mm:ss`. + * 'yyyy-MM-dd HH:mm:ss'. */ private String dateFormat; /** - * Joda date time format string. If not configured, "date-format" is used as a - * fallback if it is configured with a format string. - */ - private String jodaDateTimeFormat; - - /** - * One of the constants on Jackson's PropertyNamingStrategy. Can also be a - * fully-qualified class name of a PropertyNamingStrategy subclass. + * One of the constants on Jackson's PropertyNamingStrategies. Can also be a + * fully-qualified class name of a PropertyNamingStrategy implementation. */ private String propertyNamingStrategy; @@ -118,18 +111,6 @@ public void setDateFormat(String dateFormat) { this.dateFormat = dateFormat; } - @Deprecated - @DeprecatedConfigurationProperty(replacement = "dateFormat", - reason = "Auto-configuration for Jackson's Joda-Time integration is " - + "deprecated in favor of its Java 8 Time integration") - public String getJodaDateTimeFormat() { - return this.jodaDateTimeFormat; - } - - public void setJodaDateTimeFormat(String jodaDataTimeFormat) { - this.jodaDateTimeFormat = jodaDataTimeFormat; - } - public String getPropertyNamingStrategy() { return this.propertyNamingStrategy; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java index 3ecdab3d2416..6dbdece9c28a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,8 +36,10 @@ import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.core.env.Environment; import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.util.StringUtils; /** * {@link EnableAutoConfiguration Auto-configuration} for {@link DataSource}. @@ -48,10 +50,14 @@ * @author Kazuki Shimizu * @since 1.0.0 */ +@SuppressWarnings("deprecation") @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }) +@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory") @EnableConfigurationProperties(DataSourceProperties.class) -@Import({ DataSourcePoolMetadataProvidersConfiguration.class, DataSourceInitializationConfiguration.class }) +@Import({ DataSourcePoolMetadataProvidersConfiguration.class, + DataSourceInitializationConfiguration.InitializationSpecificCredentialsDataSourceInitializationConfiguration.class, + DataSourceInitializationConfiguration.SharedCredentialsDataSourceInitializationConfiguration.class }) public class DataSourceAutoConfiguration { @Configuration(proxyBeanMethods = false) @@ -66,8 +72,8 @@ protected static class EmbeddedDatabaseConfiguration { @Conditional(PooledDataSourceCondition.class) @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) @Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class, - DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.Generic.class, - DataSourceJmxConfiguration.class }) + DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class, + DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class }) protected static class PooledDataSourceConfiguration { } @@ -117,11 +123,16 @@ public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeM */ static class EmbeddedDatabaseCondition extends SpringBootCondition { + private static final String DATASOURCE_URL_PROPERTY = "spring.datasource.url"; + private final SpringBootCondition pooledCondition = new PooledDataSourceCondition(); @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { ConditionMessage.Builder message = ConditionMessage.forCondition("EmbeddedDataSource"); + if (hasDataSourceUrlProperty(context)) { + return ConditionOutcome.noMatch(message.because(DATASOURCE_URL_PROPERTY + " is set")); + } if (anyMatches(context, metadata, this.pooledCondition)) { return ConditionOutcome.noMatch(message.foundExactly("supported pooled data source")); } @@ -132,6 +143,19 @@ public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeM return ConditionOutcome.match(message.found("embedded database").items(type)); } + private boolean hasDataSourceUrlProperty(ConditionContext context) { + Environment environment = context.getEnvironment(); + if (environment.containsProperty(DATASOURCE_URL_PROPERTY)) { + try { + return StringUtils.hasText(environment.getProperty(DATASOURCE_URL_PROPERTY)); + } + catch (IllegalArgumentException ex) { + // Ignore unresolvable placeholder errors + } + } + return false; + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java index d94e9d3ec1a7..6dc95e5820d9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,13 @@ package org.springframework.boot.autoconfigure.jdbc; +import java.sql.SQLException; + import javax.sql.DataSource; import com.zaxxer.hikari.HikariDataSource; +import oracle.jdbc.OracleConnection; +import oracle.ucp.jdbc.PoolDataSourceImpl; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -35,6 +39,7 @@ * @author Dave Syer * @author Phillip Webb * @author Stephane Nicoll + * @author Fabio Grassi */ abstract class DataSourceConfiguration { @@ -109,6 +114,29 @@ org.apache.commons.dbcp2.BasicDataSource dataSource(DataSourceProperties propert } + /** + * Oracle UCP DataSource configuration. + */ + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass({ PoolDataSourceImpl.class, OracleConnection.class }) + @ConditionalOnMissingBean(DataSource.class) + @ConditionalOnProperty(name = "spring.datasource.type", havingValue = "oracle.ucp.jdbc.PoolDataSource", + matchIfMissing = true) + static class OracleUcp { + + @Bean + @ConfigurationProperties(prefix = "spring.datasource.oracleucp") + PoolDataSourceImpl dataSource(DataSourceProperties properties) throws SQLException { + PoolDataSourceImpl dataSource = createDataSource(properties, PoolDataSourceImpl.class); + dataSource.setValidateConnectionOnBorrow(true); + if (StringUtils.hasText(properties.getName())) { + dataSource.setConnectionPoolName(properties.getName()); + } + return dataSource; + } + + } + /** * Generic DataSource configuration. */ diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializationConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializationConfiguration.java index fd6e68c6d1a6..73059859fc06 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializationConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializationConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,44 +16,183 @@ package org.springframework.boot.autoconfigure.jdbc; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.beans.factory.support.GenericBeanDefinition; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; -import org.springframework.core.type.AnnotationMetadata; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; +import org.springframework.boot.autoconfigure.condition.ConditionMessage; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.boot.autoconfigure.jdbc.DataSourceInitializationConfiguration.InitializationSpecificCredentialsDataSourceInitializationConfiguration.DifferentCredentialsCondition; +import org.springframework.boot.autoconfigure.jdbc.DataSourceInitializationConfiguration.SharedCredentialsDataSourceInitializationConfiguration.DataSourceInitializationCondition; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.boot.jdbc.DataSourceInitializationMode; +import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer; +import org.springframework.boot.sql.init.DatabaseInitializationMode; +import org.springframework.boot.sql.init.DatabaseInitializationSettings; +import org.springframework.boot.sql.init.dependency.DatabaseInitializationDependencyConfigurer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.DependsOn; +import org.springframework.core.env.Environment; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.jdbc.datasource.SimpleDriverDataSource; +import org.springframework.util.StringUtils; /** - * Configures DataSource initialization. + * Configuration for {@link DataSource} initialization using a + * {@link DataSourceScriptDatabaseInitializer} with DDL and DML scripts. * - * @author Stephane Nicoll + * @author Andy Wilkinson */ -@Configuration(proxyBeanMethods = false) -@Import({ DataSourceInitializerInvoker.class, DataSourceInitializationConfiguration.Registrar.class }) +@Deprecated class DataSourceInitializationConfiguration { - /** - * {@link ImportBeanDefinitionRegistrar} to register the - * {@link DataSourceInitializerPostProcessor} without causing early bean instantiation - * issues. - */ - static class Registrar implements ImportBeanDefinitionRegistrar { - - private static final String BEAN_NAME = "dataSourceInitializerPostProcessor"; - - @Override - public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, - BeanDefinitionRegistry registry) { - if (!registry.containsBeanDefinition(BEAN_NAME)) { - GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); - beanDefinition.setBeanClass(DataSourceInitializerPostProcessor.class); - beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - // We don't need this one to be post processed otherwise it can cause a - // cascade of bean instantiation that we would rather avoid. - beanDefinition.setSynthetic(true); - registry.registerBeanDefinition(BEAN_NAME, beanDefinition); + private static DataSource determineDataSource(Supplier dataSource, String username, String password) { + if (StringUtils.hasText(username) && StringUtils.hasText(password)) { + return DataSourceBuilder.derivedFrom(dataSource.get()).type(SimpleDriverDataSource.class).username(username) + .password(password).build(); + } + return dataSource.get(); + } + + private static List scriptLocations(List locations, String fallback, String platform) { + if (locations != null) { + return locations; + } + List fallbackLocations = new ArrayList<>(); + fallbackLocations.add("optional:classpath*:" + fallback + "-" + platform + ".sql"); + fallbackLocations.add("optional:classpath*:" + fallback + ".sql"); + return fallbackLocations; + } + + private static DatabaseInitializationMode mapMode(DataSourceInitializationMode mode) { + switch (mode) { + case ALWAYS: + return DatabaseInitializationMode.ALWAYS; + case EMBEDDED: + return DatabaseInitializationMode.EMBEDDED; + case NEVER: + return DatabaseInitializationMode.NEVER; + default: + throw new IllegalStateException("Unexpected initialization mode '" + mode + "'"); + } + } + + // Fully-qualified to work around javac bug in JDK 1.8 + @org.springframework.context.annotation.Configuration(proxyBeanMethods = false) + @org.springframework.context.annotation.Conditional(DifferentCredentialsCondition.class) + @org.springframework.context.annotation.Import(DatabaseInitializationDependencyConfigurer.class) + @ConditionalOnSingleCandidate(DataSource.class) + @ConditionalOnMissingBean(DataSourceScriptDatabaseInitializer.class) + static class InitializationSpecificCredentialsDataSourceInitializationConfiguration { + + @Bean + DataSourceScriptDatabaseInitializer ddlOnlyScriptDataSourceInitializer(ObjectProvider dataSource, + DataSourceProperties properties) { + DatabaseInitializationSettings settings = new DatabaseInitializationSettings(); + settings.setSchemaLocations(scriptLocations(properties.getSchema(), "schema", properties.getPlatform())); + settings.setContinueOnError(properties.isContinueOnError()); + settings.setSeparator(properties.getSeparator()); + settings.setEncoding(properties.getSqlScriptEncoding()); + settings.setMode(mapMode(properties.getInitializationMode())); + DataSource initializationDataSource = determineDataSource(dataSource::getObject, + properties.getSchemaUsername(), properties.getSchemaPassword()); + return new DataSourceScriptDatabaseInitializer(initializationDataSource, settings); + } + + @Bean + @DependsOn("ddlOnlyScriptDataSourceInitializer") + DataSourceScriptDatabaseInitializer dmlOnlyScriptDataSourceInitializer(ObjectProvider dataSource, + DataSourceProperties properties) { + DatabaseInitializationSettings settings = new DatabaseInitializationSettings(); + settings.setDataLocations(scriptLocations(properties.getData(), "data", properties.getPlatform())); + settings.setContinueOnError(properties.isContinueOnError()); + settings.setSeparator(properties.getSeparator()); + settings.setEncoding(properties.getSqlScriptEncoding()); + settings.setMode(mapMode(properties.getInitializationMode())); + DataSource initializationDataSource = determineDataSource(dataSource::getObject, + properties.getDataUsername(), properties.getDataPassword()); + return new DataSourceScriptDatabaseInitializer(initializationDataSource, settings); + } + + static class DifferentCredentialsCondition extends AnyNestedCondition { + + DifferentCredentialsCondition() { + super(ConfigurationPhase.PARSE_CONFIGURATION); } + + @ConditionalOnProperty(prefix = "spring.datasource", name = "schema-username") + static class SchemaCredentials { + + } + + @ConditionalOnProperty(prefix = "spring.datasource", name = "data-username") + static class DataCredentials { + + } + + } + + } + + // Fully-qualified to work around javac bug in JDK 1.8 + @org.springframework.context.annotation.Configuration(proxyBeanMethods = false) + @org.springframework.context.annotation.Import(DatabaseInitializationDependencyConfigurer.class) + @org.springframework.context.annotation.Conditional(DataSourceInitializationCondition.class) + @ConditionalOnSingleCandidate(DataSource.class) + @ConditionalOnMissingBean(DataSourceScriptDatabaseInitializer.class) + static class SharedCredentialsDataSourceInitializationConfiguration { + + @Bean + DataSourceScriptDatabaseInitializer scriptDataSourceInitializer(DataSource dataSource, + DataSourceProperties properties) { + DatabaseInitializationSettings settings = new DatabaseInitializationSettings(); + settings.setSchemaLocations(scriptLocations(properties.getSchema(), "schema", properties.getPlatform())); + settings.setDataLocations(scriptLocations(properties.getData(), "data", properties.getPlatform())); + settings.setContinueOnError(properties.isContinueOnError()); + settings.setSeparator(properties.getSeparator()); + settings.setEncoding(properties.getSqlScriptEncoding()); + settings.setMode(mapMode(properties.getInitializationMode())); + return new DataSourceScriptDatabaseInitializer(dataSource, settings); + } + + static class DataSourceInitializationCondition extends SpringBootCondition { + + private static final Set INITIALIZATION_PROPERTIES = Collections + .unmodifiableSet(new HashSet<>(Arrays.asList("spring.datasource.initialization-mode", + "spring.datasource.platform", "spring.datasource.schema", "spring.datasource.schema[0]", + "spring.datasource.schema-username", "spring.datasource.schema-password", + "spring.datasource.data", "spring.datasource.data[0]", "spring.datasource.data-username", + "spring.datasource.data-password", "spring.datasource.continue-on-error", + "spring.datasource.separator", "spring.datasource.sql-script-encoding"))); + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { + ConditionMessage.Builder message = ConditionMessage.forCondition("DataSource Initialization"); + Environment environment = context.getEnvironment(); + Set configuredProperties = INITIALIZATION_PROPERTIES.stream() + .filter(environment::containsProperty).collect(Collectors.toSet()); + if (configuredProperties.isEmpty()) { + return ConditionOutcome + .noMatch(message.didNotFind("configured properties").items(INITIALIZATION_PROPERTIES)); + } + return ConditionOutcome.match( + message.found("configured property", "configured properties").items(configuredProperties)); + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializer.java deleted file mode 100644 index 5b862d402435..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializer.java +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.jdbc; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import javax.sql.DataSource; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; -import org.springframework.boot.jdbc.DataSourceBuilder; -import org.springframework.boot.jdbc.DataSourceInitializationMode; -import org.springframework.boot.jdbc.EmbeddedDatabaseConnection; -import org.springframework.core.io.DefaultResourceLoader; -import org.springframework.core.io.Resource; -import org.springframework.core.io.ResourceLoader; -import org.springframework.jdbc.config.SortedResourcesFactoryBean; -import org.springframework.jdbc.datasource.init.DatabasePopulatorUtils; -import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; -import org.springframework.util.StringUtils; - -/** - * Initialize a {@link DataSource} based on a matching {@link DataSourceProperties} - * config. - * - * @author Dave Syer - * @author Phillip Webb - * @author Eddú Meléndez - * @author Stephane Nicoll - * @author Kazuki Shimizu - */ -class DataSourceInitializer { - - private static final Log logger = LogFactory.getLog(DataSourceInitializer.class); - - private final DataSource dataSource; - - private final DataSourceProperties properties; - - private final ResourceLoader resourceLoader; - - /** - * Create a new instance with the {@link DataSource} to initialize and its matching - * {@link DataSourceProperties configuration}. - * @param dataSource the datasource to initialize - * @param properties the matching configuration - * @param resourceLoader the resource loader to use (can be null) - */ - DataSourceInitializer(DataSource dataSource, DataSourceProperties properties, ResourceLoader resourceLoader) { - this.dataSource = dataSource; - this.properties = properties; - this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader(); - } - - /** - * Create a new instance with the {@link DataSource} to initialize and its matching - * {@link DataSourceProperties configuration}. - * @param dataSource the datasource to initialize - * @param properties the matching configuration - */ - DataSourceInitializer(DataSource dataSource, DataSourceProperties properties) { - this(dataSource, properties, null); - } - - DataSource getDataSource() { - return this.dataSource; - } - - /** - * Create the schema if necessary. - * @return {@code true} if the schema was created - * @see DataSourceProperties#getSchema() - */ - boolean createSchema() { - List scripts = getScripts("spring.datasource.schema", this.properties.getSchema(), "schema"); - if (!scripts.isEmpty()) { - if (!isEnabled()) { - logger.debug("Initialization disabled (not running DDL scripts)"); - return false; - } - String username = this.properties.getSchemaUsername(); - String password = this.properties.getSchemaPassword(); - runScripts(scripts, username, password); - } - return !scripts.isEmpty(); - } - - /** - * Initialize the schema if necessary. - * @see DataSourceProperties#getData() - */ - void initSchema() { - List scripts = getScripts("spring.datasource.data", this.properties.getData(), "data"); - if (!scripts.isEmpty()) { - if (!isEnabled()) { - logger.debug("Initialization disabled (not running data scripts)"); - return; - } - String username = this.properties.getDataUsername(); - String password = this.properties.getDataPassword(); - runScripts(scripts, username, password); - } - } - - private boolean isEnabled() { - DataSourceInitializationMode mode = this.properties.getInitializationMode(); - if (mode == DataSourceInitializationMode.NEVER) { - return false; - } - if (mode == DataSourceInitializationMode.EMBEDDED && !isEmbedded()) { - return false; - } - return true; - } - - private boolean isEmbedded() { - try { - return EmbeddedDatabaseConnection.isEmbedded(this.dataSource); - } - catch (Exception ex) { - logger.debug("Could not determine if datasource is embedded", ex); - return false; - } - } - - private List getScripts(String propertyName, List resources, String fallback) { - if (resources != null) { - return getResources(propertyName, resources, true); - } - String platform = this.properties.getPlatform(); - List fallbackResources = new ArrayList<>(); - fallbackResources.add("classpath*:" + fallback + "-" + platform + ".sql"); - fallbackResources.add("classpath*:" + fallback + ".sql"); - return getResources(propertyName, fallbackResources, false); - } - - private List getResources(String propertyName, List locations, boolean validate) { - List resources = new ArrayList<>(); - for (String location : locations) { - for (Resource resource : doGetResources(location)) { - if (resource.exists()) { - resources.add(resource); - } - else if (validate) { - throw new InvalidConfigurationPropertyValueException(propertyName, resource, - "The specified resource does not exist."); - } - } - } - return resources; - } - - private Resource[] doGetResources(String location) { - try { - SortedResourcesFactoryBean factory = new SortedResourcesFactoryBean(this.resourceLoader, - Collections.singletonList(location)); - factory.afterPropertiesSet(); - return factory.getObject(); - } - catch (Exception ex) { - throw new IllegalStateException("Unable to load resources from " + location, ex); - } - } - - private void runScripts(List resources, String username, String password) { - if (resources.isEmpty()) { - return; - } - ResourceDatabasePopulator populator = new ResourceDatabasePopulator(); - populator.setContinueOnError(this.properties.isContinueOnError()); - populator.setSeparator(this.properties.getSeparator()); - if (this.properties.getSqlScriptEncoding() != null) { - populator.setSqlScriptEncoding(this.properties.getSqlScriptEncoding().name()); - } - for (Resource resource : resources) { - populator.addScript(resource); - } - DataSource dataSource = this.dataSource; - if (StringUtils.hasText(username) && StringUtils.hasText(password)) { - dataSource = DataSourceBuilder.create(this.properties.getClassLoader()) - .driverClassName(this.properties.determineDriverClassName()).url(this.properties.determineUrl()) - .username(username).password(password).build(); - } - DatabasePopulatorUtils.execute(populator, dataSource); - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializerInvoker.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializerInvoker.java deleted file mode 100644 index a6ad430c2115..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializerInvoker.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.jdbc; - -import javax.sql.DataSource; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationListener; -import org.springframework.core.log.LogMessage; - -/** - * Bean to handle {@link DataSource} initialization by running {@literal schema-*.sql} on - * {@link InitializingBean#afterPropertiesSet()} and {@literal data-*.sql} SQL scripts on - * a {@link DataSourceSchemaCreatedEvent}. - * - * @author Stephane Nicoll - * @see DataSourceAutoConfiguration - */ -class DataSourceInitializerInvoker implements ApplicationListener, InitializingBean { - - private static final Log logger = LogFactory.getLog(DataSourceInitializerInvoker.class); - - private final ObjectProvider dataSource; - - private final DataSourceProperties properties; - - private final ApplicationContext applicationContext; - - private DataSourceInitializer dataSourceInitializer; - - private boolean initialized; - - DataSourceInitializerInvoker(ObjectProvider dataSource, DataSourceProperties properties, - ApplicationContext applicationContext) { - this.dataSource = dataSource; - this.properties = properties; - this.applicationContext = applicationContext; - } - - @Override - public void afterPropertiesSet() { - DataSourceInitializer initializer = getDataSourceInitializer(); - if (initializer != null) { - boolean schemaCreated = this.dataSourceInitializer.createSchema(); - if (schemaCreated) { - initialize(initializer); - } - } - } - - private void initialize(DataSourceInitializer initializer) { - try { - this.applicationContext.publishEvent(new DataSourceSchemaCreatedEvent(initializer.getDataSource())); - // The listener might not be registered yet, so don't rely on it. - if (!this.initialized) { - this.dataSourceInitializer.initSchema(); - this.initialized = true; - } - } - catch (IllegalStateException ex) { - logger.warn(LogMessage.format("Could not send event to complete DataSource initialization (%s)", - ex.getMessage())); - } - } - - @Override - public void onApplicationEvent(DataSourceSchemaCreatedEvent event) { - // NOTE the event can happen more than once and - // the event datasource is not used here - DataSourceInitializer initializer = getDataSourceInitializer(); - if (!this.initialized && initializer != null) { - initializer.initSchema(); - this.initialized = true; - } - } - - private DataSourceInitializer getDataSourceInitializer() { - if (this.dataSourceInitializer == null) { - DataSource ds = this.dataSource.getIfUnique(); - if (ds != null) { - this.dataSourceInitializer = new DataSourceInitializer(ds, this.properties, this.applicationContext); - } - } - return this.dataSourceInitializer; - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializerPostProcessor.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializerPostProcessor.java deleted file mode 100644 index d0e27542e594..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializerPostProcessor.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.jdbc; - -import javax.sql.DataSource; - -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.core.Ordered; - -/** - * {@link BeanPostProcessor} used to ensure that {@link DataSourceInitializer} is - * initialized as soon as a {@link DataSource} is. - * - * @author Dave Syer - */ -class DataSourceInitializerPostProcessor implements BeanPostProcessor, Ordered { - - @Override - public int getOrder() { - return Ordered.HIGHEST_PRECEDENCE + 1; - } - - @Autowired - private BeanFactory beanFactory; - - @Override - public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { - return bean; - } - - @Override - public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { - if (bean instanceof DataSource) { - // force initialization of this bean as soon as we see a DataSource - this.beanFactory.getBean(DataSourceInitializerInvoker.class); - } - return bean; - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceJmxConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceJmxConfiguration.java index 96351d9ec6d6..e07b2f1f2f99 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceJmxConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceJmxConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,13 +18,14 @@ import java.sql.SQLException; -import javax.annotation.PostConstruct; import javax.sql.DataSource; +import com.zaxxer.hikari.HikariConfigMXBean; import com.zaxxer.hikari.HikariDataSource; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.tomcat.jdbc.pool.DataSourceProxy; +import org.apache.tomcat.jdbc.pool.PoolConfiguration; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -59,11 +60,12 @@ static class Hikari { Hikari(DataSource dataSource, ObjectProvider mBeanExporter) { this.dataSource = dataSource; this.mBeanExporter = mBeanExporter; + validateMBeans(); } - @PostConstruct - void validateMBeans() { - HikariDataSource hikariDataSource = DataSourceUnwrapper.unwrap(this.dataSource, HikariDataSource.class); + private void validateMBeans() { + HikariDataSource hikariDataSource = DataSourceUnwrapper.unwrap(this.dataSource, HikariConfigMXBean.class, + HikariDataSource.class); if (hikariDataSource != null && hikariDataSource.isRegisterMbeans()) { this.mBeanExporter.ifUnique((exporter) -> exporter.addExcludedBean("dataSource")); } @@ -80,7 +82,8 @@ static class TomcatDataSourceJmxConfiguration { @Bean @ConditionalOnMissingBean(name = "dataSourceMBean") Object dataSourceMBean(DataSource dataSource) { - DataSourceProxy dataSourceProxy = DataSourceUnwrapper.unwrap(dataSource, DataSourceProxy.class); + DataSourceProxy dataSourceProxy = DataSourceUnwrapper.unwrap(dataSource, PoolConfiguration.class, + DataSourceProxy.class); if (dataSourceProxy != null) { try { return dataSourceProxy.createPool().getJmxPool(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java index c0ab1f5b8c55..84eeca6ad925 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.InitializingBean; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.boot.jdbc.DataSourceInitializationMode; import org.springframework.boot.jdbc.DatabaseDriver; @@ -44,6 +45,7 @@ * @author Stephane Nicoll * @author Benedikt Ritter * @author Eddú Meléndez + * @author Scott Frederick * @since 1.1.0 */ @ConfigurationProperties(prefix = "spring.datasource") @@ -52,14 +54,15 @@ public class DataSourceProperties implements BeanClassLoaderAware, InitializingB private ClassLoader classLoader; /** - * Name of the datasource. Default to "testdb" when using an embedded database. + * Whether to generate a random datasource name. */ - private String name; + private boolean generateUniqueName = true; /** - * Whether to generate a random datasource name. + * Datasource name to use if "generate-unique-name" is false. Defaults to "testdb" + * when using an embedded database, otherwise null. */ - private boolean generateUniqueName; + private String name; /** * Fully qualified name of the connection pool implementation to use. By default, it @@ -88,20 +91,23 @@ public class DataSourceProperties implements BeanClassLoaderAware, InitializingB private String password; /** - * JNDI location of the datasource. Class, url, username & password are ignored when + * JNDI location of the datasource. Class, url, username and password are ignored when * set. */ private String jndiName; /** - * Initialize the datasource with available DDL and DML scripts. + * Mode to apply when determining if DataSource initialization should be performed + * using the available DDL and DML scripts. */ + @Deprecated private DataSourceInitializationMode initializationMode = DataSourceInitializationMode.EMBEDDED; /** * Platform to use in the DDL or DML scripts (such as schema-${platform}.sql or * data-${platform}.sql). */ + @Deprecated private String platform = "all"; /** @@ -112,44 +118,56 @@ public class DataSourceProperties implements BeanClassLoaderAware, InitializingB /** * Username of the database to execute DDL scripts (if different). */ + @Deprecated private String schemaUsername; /** * Password of the database to execute DDL scripts (if different). */ + @Deprecated private String schemaPassword; /** * Data (DML) script resource references. */ + @Deprecated private List data; /** * Username of the database to execute DML scripts (if different). */ + @Deprecated private String dataUsername; /** * Password of the database to execute DML scripts (if different). */ + @Deprecated private String dataPassword; /** * Whether to stop if an error occurs while initializing the database. */ + @Deprecated private boolean continueOnError = false; /** * Statement separator in SQL initialization scripts. */ + @Deprecated private String separator = ";"; /** * SQL scripts encoding. */ + @Deprecated private Charset sqlScriptEncoding; - private EmbeddedDatabaseConnection embeddedDatabaseConnection = EmbeddedDatabaseConnection.NONE; + /** + * Connection details for an embedded database. Defaults to the most suitable embedded + * database that is available on the classpath. + */ + private EmbeddedDatabaseConnection embeddedDatabaseConnection; private Xa xa = new Xa(); @@ -162,7 +180,9 @@ public void setBeanClassLoader(ClassLoader classLoader) { @Override public void afterPropertiesSet() throws Exception { - this.embeddedDatabaseConnection = EmbeddedDatabaseConnection.get(this.classLoader); + if (this.embeddedDatabaseConnection == null) { + this.embeddedDatabaseConnection = EmbeddedDatabaseConnection.get(this.classLoader); + } } /** @@ -175,14 +195,6 @@ public DataSourceBuilder initializeDataSourceBuilder() { .url(determineUrl()).username(determineUsername()).password(determinePassword()); } - public String getName() { - return this.name; - } - - public void setName(String name) { - this.name = name; - } - public boolean isGenerateUniqueName() { return this.generateUniqueName; } @@ -191,6 +203,14 @@ public void setGenerateUniqueName(boolean generateUniqueName) { this.generateUniqueName = generateUniqueName; } + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + public Class getType() { return this.type; } @@ -324,7 +344,7 @@ public String determineUsername() { if (StringUtils.hasText(this.username)) { return this.username; } - if (EmbeddedDatabaseConnection.isEmbedded(determineDriverClassName())) { + if (EmbeddedDatabaseConnection.isEmbedded(determineDriverClassName(), determineUrl())) { return "sa"; } return null; @@ -352,7 +372,7 @@ public String determinePassword() { if (StringUtils.hasText(this.password)) { return this.password; } - if (EmbeddedDatabaseConnection.isEmbedded(determineDriverClassName())) { + if (EmbeddedDatabaseConnection.isEmbedded(determineDriverClassName(), determineUrl())) { return ""; } return null; @@ -372,94 +392,135 @@ public void setJndiName(String jndiName) { this.jndiName = jndiName; } + @Deprecated + @DeprecatedConfigurationProperty(replacement = "spring.sql.init.mode") public DataSourceInitializationMode getInitializationMode() { return this.initializationMode; } + @Deprecated public void setInitializationMode(DataSourceInitializationMode initializationMode) { this.initializationMode = initializationMode; } + @Deprecated + @DeprecatedConfigurationProperty(replacement = "spring.sql.init.platform") public String getPlatform() { return this.platform; } + @Deprecated public void setPlatform(String platform) { this.platform = platform; } + @Deprecated + @DeprecatedConfigurationProperty(replacement = "spring.sql.init.schema-locations") public List getSchema() { return this.schema; } + @Deprecated public void setSchema(List schema) { this.schema = schema; } + @Deprecated + @DeprecatedConfigurationProperty(replacement = "spring.sql.init.username") public String getSchemaUsername() { return this.schemaUsername; } + @Deprecated public void setSchemaUsername(String schemaUsername) { this.schemaUsername = schemaUsername; } + @Deprecated + @DeprecatedConfigurationProperty(replacement = "spring.sql.init.password") public String getSchemaPassword() { return this.schemaPassword; } + @Deprecated public void setSchemaPassword(String schemaPassword) { this.schemaPassword = schemaPassword; } + @Deprecated + @DeprecatedConfigurationProperty(replacement = "spring.sql.init.data-locations") public List getData() { return this.data; } + @Deprecated public void setData(List data) { this.data = data; } + @Deprecated + @DeprecatedConfigurationProperty(replacement = "spring.sql.init.username") public String getDataUsername() { return this.dataUsername; } + @Deprecated public void setDataUsername(String dataUsername) { this.dataUsername = dataUsername; } + @Deprecated + @DeprecatedConfigurationProperty(replacement = "spring.sql.init.password") public String getDataPassword() { return this.dataPassword; } + @Deprecated public void setDataPassword(String dataPassword) { this.dataPassword = dataPassword; } + @Deprecated + @DeprecatedConfigurationProperty(replacement = "spring.sql.init.continue-on-error") public boolean isContinueOnError() { return this.continueOnError; } + @Deprecated public void setContinueOnError(boolean continueOnError) { this.continueOnError = continueOnError; } + @Deprecated + @DeprecatedConfigurationProperty(replacement = "spring.sql.init.separator") public String getSeparator() { return this.separator; } + @Deprecated public void setSeparator(String separator) { this.separator = separator; } + @Deprecated + @DeprecatedConfigurationProperty(replacement = "spring.sql.init.encoding") public Charset getSqlScriptEncoding() { return this.sqlScriptEncoding; } + @Deprecated public void setSqlScriptEncoding(Charset sqlScriptEncoding) { this.sqlScriptEncoding = sqlScriptEncoding; } + public EmbeddedDatabaseConnection getEmbeddedDatabaseConnection() { + return this.embeddedDatabaseConnection; + } + + public void setEmbeddedDatabaseConnection(EmbeddedDatabaseConnection embeddedDatabaseConnection) { + this.embeddedDatabaseConnection = embeddedDatabaseConnection; + } + public ClassLoader getClassLoader() { return this.classLoader; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceSchemaCreatedEvent.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceSchemaCreatedEvent.java index 8da579a3e5a0..c7970ba079e6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceSchemaCreatedEvent.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceSchemaCreatedEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,8 +28,11 @@ * @author Dave Syer * @author Stephane Nicoll * @since 2.0.0 + * @deprecated since 2.5.0 for removal in 2.7.0 with no replacement as the event is no + * longer published */ @SuppressWarnings("serial") +@Deprecated public class DataSourceSchemaCreatedEvent extends ApplicationEvent { /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceTransactionManagerAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceTransactionManagerAutoConfiguration.java index 1664c622f63d..4fa98b3b7bd0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceTransactionManagerAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceTransactionManagerAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,13 +29,14 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; +import org.springframework.core.env.Environment; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DataSourceTransactionManager; -import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.jdbc.support.JdbcTransactionManager; +import org.springframework.transaction.TransactionManager; /** - * {@link EnableAutoConfiguration Auto-configuration} for - * {@link DataSourceTransactionManager}. + * {@link EnableAutoConfiguration Auto-configuration} for {@link JdbcTransactionManager}. * * @author Dave Syer * @author Stephane Nicoll @@ -44,24 +45,29 @@ * @since 1.0.0 */ @Configuration(proxyBeanMethods = false) -@ConditionalOnClass({ JdbcTemplate.class, PlatformTransactionManager.class }) +@ConditionalOnClass({ JdbcTemplate.class, TransactionManager.class }) @AutoConfigureOrder(Ordered.LOWEST_PRECEDENCE) @EnableConfigurationProperties(DataSourceProperties.class) public class DataSourceTransactionManagerAutoConfiguration { @Configuration(proxyBeanMethods = false) @ConditionalOnSingleCandidate(DataSource.class) - static class DataSourceTransactionManagerConfiguration { + static class JdbcTransactionManagerConfiguration { @Bean - @ConditionalOnMissingBean(PlatformTransactionManager.class) - DataSourceTransactionManager transactionManager(DataSource dataSource, + @ConditionalOnMissingBean(TransactionManager.class) + DataSourceTransactionManager transactionManager(Environment environment, DataSource dataSource, ObjectProvider transactionManagerCustomizers) { - DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(dataSource); + DataSourceTransactionManager transactionManager = createTransactionManager(environment, dataSource); transactionManagerCustomizers.ifAvailable((customizers) -> customizers.customize(transactionManager)); return transactionManager; } + private DataSourceTransactionManager createTransactionManager(Environment environment, DataSource dataSource) { + return environment.getProperty("spring.dao.exceptiontranslation.enabled", Boolean.class, Boolean.TRUE) + ? new JdbcTransactionManager(dataSource) : new DataSourceTransactionManager(dataSource); + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcOperationsDependsOnPostProcessor.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcOperationsDependsOnPostProcessor.java index ef375dec4db9..176c2962f7c6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcOperationsDependsOnPostProcessor.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcOperationsDependsOnPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.boot.autoconfigure.AbstractDependsOnBeanFactoryPostProcessor; +import org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitializationDetector; import org.springframework.jdbc.core.JdbcOperations; /** @@ -32,7 +33,10 @@ * @author Andrii Hrytsiuk * @since 2.0.4 * @see BeanDefinition#setDependsOn(String[]) + * @deprecated since 2.5.0 for removal in 2.7.0 in favor of + * {@link DependsOnDatabaseInitializationDetector} */ +@Deprecated public class JdbcOperationsDependsOnPostProcessor extends AbstractDependsOnBeanFactoryPostProcessor { /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcTemplateAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcTemplateAutoConfiguration.java index 2fdb9af54256..0d9b3838af5e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcTemplateAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcTemplateAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.sql.init.dependency.DatabaseInitializationDependencyConfigurer; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.jdbc.core.JdbcTemplate; @@ -43,7 +44,8 @@ @ConditionalOnSingleCandidate(DataSource.class) @AutoConfigureAfter(DataSourceAutoConfiguration.class) @EnableConfigurationProperties(JdbcProperties.class) -@Import({ JdbcTemplateConfiguration.class, NamedParameterJdbcTemplateConfiguration.class }) +@Import({ DatabaseInitializationDependencyConfigurer.class, JdbcTemplateConfiguration.class, + NamedParameterJdbcTemplateConfiguration.class }) public class JdbcTemplateAutoConfiguration { } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/NamedParameterJdbcOperationsDependsOnPostProcessor.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/NamedParameterJdbcOperationsDependsOnPostProcessor.java index d7174a61a20a..c0afaed6df6b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/NamedParameterJdbcOperationsDependsOnPostProcessor.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/NamedParameterJdbcOperationsDependsOnPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.boot.autoconfigure.AbstractDependsOnBeanFactoryPostProcessor; +import org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitializationDetector; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; /** @@ -30,7 +31,10 @@ * @author Andrii Hrytsiuk * @since 2.1.4 * @see BeanDefinition#setDependsOn(String[]) + * @deprecated since 2.5.0 for removal in 2.7.0 in favor of + * {@link DependsOnDatabaseInitializationDetector} */ +@Deprecated public class NamedParameterJdbcOperationsDependsOnPostProcessor extends AbstractDependsOnBeanFactoryPostProcessor { /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfiguration.java index ccc56ca16f56..da1282b7a963 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,9 @@ package org.springframework.boot.autoconfigure.jdbc; +import java.util.HashMap; +import java.util.Map; + import javax.sql.DataSource; import javax.sql.XADataSource; import javax.transaction.TransactionManager; @@ -28,6 +31,7 @@ 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.jdbc.DataSourceProperties.DataSourceBeanCreationException; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; @@ -102,11 +106,17 @@ private void bindXaProperties(XADataSource target, DataSourceProperties dataSour } private ConfigurationPropertySource getBinderSource(DataSourceProperties dataSourceProperties) { - MapConfigurationPropertySource source = new MapConfigurationPropertySource(); - source.put("user", dataSourceProperties.determineUsername()); - source.put("password", dataSourceProperties.determinePassword()); - source.put("url", dataSourceProperties.determineUrl()); - source.putAll(dataSourceProperties.getXa().getProperties()); + Map properties = new HashMap<>(); + properties.putAll(dataSourceProperties.getXa().getProperties()); + properties.computeIfAbsent("user", (key) -> dataSourceProperties.determineUsername()); + properties.computeIfAbsent("password", (key) -> dataSourceProperties.determinePassword()); + try { + properties.computeIfAbsent("url", (key) -> dataSourceProperties.determineUrl()); + } + catch (DataSourceBeanCreationException ex) { + // Continue as not all XA DataSource's require a URL + } + MapConfigurationPropertySource source = new MapConfigurationPropertySource(properties); ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases(); aliases.addAliases("user", "username"); return source.withAliases(aliases); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/metadata/DataSourcePoolMetadataProvidersConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/metadata/DataSourcePoolMetadataProvidersConfiguration.java index d34a183bfb4c..a250d84c7fc0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/metadata/DataSourcePoolMetadataProvidersConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/metadata/DataSourcePoolMetadataProvidersConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,20 @@ package org.springframework.boot.autoconfigure.jdbc.metadata; +import com.zaxxer.hikari.HikariConfigMXBean; import com.zaxxer.hikari.HikariDataSource; +import oracle.jdbc.OracleConnection; +import oracle.ucp.jdbc.PoolDataSource; import org.apache.commons.dbcp2.BasicDataSource; +import org.apache.commons.dbcp2.BasicDataSourceMXBean; +import org.apache.tomcat.jdbc.pool.jmx.ConnectionPoolMBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.jdbc.DataSourceUnwrapper; import org.springframework.boot.jdbc.metadata.CommonsDbcp2DataSourcePoolMetadata; import org.springframework.boot.jdbc.metadata.DataSourcePoolMetadataProvider; import org.springframework.boot.jdbc.metadata.HikariDataSourcePoolMetadata; +import org.springframework.boot.jdbc.metadata.OracleUcpDataSourcePoolMetadata; import org.springframework.boot.jdbc.metadata.TomcatDataSourcePoolMetadata; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -33,6 +39,7 @@ * sources. * * @author Stephane Nicoll + * @author Fabio Grassi * @since 1.2.0 */ @Configuration(proxyBeanMethods = false) @@ -46,7 +53,7 @@ static class TomcatDataSourcePoolMetadataProviderConfiguration { DataSourcePoolMetadataProvider tomcatPoolDataSourceMetadataProvider() { return (dataSource) -> { org.apache.tomcat.jdbc.pool.DataSource tomcatDataSource = DataSourceUnwrapper.unwrap(dataSource, - org.apache.tomcat.jdbc.pool.DataSource.class); + ConnectionPoolMBean.class, org.apache.tomcat.jdbc.pool.DataSource.class); if (tomcatDataSource != null) { return new TomcatDataSourcePoolMetadata(tomcatDataSource); } @@ -63,7 +70,8 @@ static class HikariPoolDataSourceMetadataProviderConfiguration { @Bean DataSourcePoolMetadataProvider hikariPoolDataSourceMetadataProvider() { return (dataSource) -> { - HikariDataSource hikariDataSource = DataSourceUnwrapper.unwrap(dataSource, HikariDataSource.class); + HikariDataSource hikariDataSource = DataSourceUnwrapper.unwrap(dataSource, HikariConfigMXBean.class, + HikariDataSource.class); if (hikariDataSource != null) { return new HikariDataSourcePoolMetadata(hikariDataSource); } @@ -80,7 +88,8 @@ static class CommonsDbcp2PoolDataSourceMetadataProviderConfiguration { @Bean DataSourcePoolMetadataProvider commonsDbcp2PoolDataSourceMetadataProvider() { return (dataSource) -> { - BasicDataSource dbcpDataSource = DataSourceUnwrapper.unwrap(dataSource, BasicDataSource.class); + BasicDataSource dbcpDataSource = DataSourceUnwrapper.unwrap(dataSource, BasicDataSourceMXBean.class, + BasicDataSource.class); if (dbcpDataSource != null) { return new CommonsDbcp2DataSourcePoolMetadata(dbcpDataSource); } @@ -90,4 +99,21 @@ DataSourcePoolMetadataProvider commonsDbcp2PoolDataSourceMetadataProvider() { } + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass({ PoolDataSource.class, OracleConnection.class }) + static class OracleUcpPoolDataSourceMetadataProviderConfiguration { + + @Bean + DataSourcePoolMetadataProvider oracleUcpPoolDataSourceMetadataProvider() { + return (dataSource) -> { + PoolDataSource ucpDataSource = DataSourceUnwrapper.unwrap(dataSource, PoolDataSource.class); + if (ucpDataSource != null) { + return new OracleUcpDataSourcePoolMetadata(ucpDataSource); + } + return null; + }; + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfiguration.java index 9011c0f2ff5b..8ce16c5d2031 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ import java.util.Collections; import java.util.EnumSet; -import javax.annotation.PostConstruct; import javax.servlet.DispatcherType; import javax.servlet.ServletContext; import javax.servlet.ServletException; @@ -95,22 +94,11 @@ public class JerseyAutoConfiguration implements ServletContextAware { private final ResourceConfig config; - private final ObjectProvider customizers; - public JerseyAutoConfiguration(JerseyProperties jersey, ResourceConfig config, ObjectProvider customizers) { this.jersey = jersey; this.config = config; - this.customizers = customizers; - } - - @PostConstruct - public void path() { - customize(); - } - - private void customize() { - this.customizers.orderedStream().forEach((customizer) -> customizer.customize(this.config)); + customizers.orderedStream().forEach((customizer) -> customizer.customize(this.config)); } @Bean diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java index ccb1f730b027..d8ed7372333e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.time.Duration; import javax.jms.ConnectionFactory; +import javax.jms.ExceptionListener; import org.springframework.jms.config.DefaultJmsListenerContainerFactory; import org.springframework.jms.support.converter.MessageConverter; @@ -30,6 +31,7 @@ * Configure {@link DefaultJmsListenerContainerFactory} with sensible defaults. * * @author Stephane Nicoll + * @author Eddú Meléndez * @since 1.3.3 */ public final class DefaultJmsListenerContainerFactoryConfigurer { @@ -38,6 +40,8 @@ public final class DefaultJmsListenerContainerFactoryConfigurer { private MessageConverter messageConverter; + private ExceptionListener exceptionListener; + private JtaTransactionManager transactionManager; private JmsProperties jmsProperties; @@ -60,6 +64,15 @@ void setMessageConverter(MessageConverter messageConverter) { this.messageConverter = messageConverter; } + /** + * Set the {@link ExceptionListener} to use or {@code null} if no exception listener + * should be associated by default. + * @param exceptionListener the {@link ExceptionListener} + */ + void setExceptionListener(ExceptionListener exceptionListener) { + this.exceptionListener = exceptionListener; + } + /** * Set the {@link JtaTransactionManager} to use or {@code null} if the JTA support * should not be used. @@ -100,6 +113,9 @@ public void configure(DefaultJmsListenerContainerFactory factory, ConnectionFact if (this.messageConverter != null) { factory.setMessageConverter(this.messageConverter); } + if (this.exceptionListener != null) { + factory.setExceptionListener(this.exceptionListener); + } JmsProperties.Listener listener = this.jmsProperties.getListener(); factory.setAutoStartup(listener.isAutoStartup()); if (listener.getAcknowledgeMode() != null) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAnnotationDrivenConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAnnotationDrivenConfiguration.java index 271a3a9da4e2..432cba4bff51 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAnnotationDrivenConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAnnotationDrivenConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.boot.autoconfigure.jms; import javax.jms.ConnectionFactory; +import javax.jms.ExceptionListener; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -38,6 +39,7 @@ * * @author Phillip Webb * @author Stephane Nicoll + * @author Eddú Meléndez */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(EnableJms.class) @@ -49,14 +51,17 @@ class JmsAnnotationDrivenConfiguration { private final ObjectProvider messageConverter; + private final ObjectProvider exceptionListener; + private final JmsProperties properties; JmsAnnotationDrivenConfiguration(ObjectProvider destinationResolver, ObjectProvider transactionManager, ObjectProvider messageConverter, - JmsProperties properties) { + ObjectProvider exceptionListener, JmsProperties properties) { this.destinationResolver = destinationResolver; this.transactionManager = transactionManager; this.messageConverter = messageConverter; + this.exceptionListener = exceptionListener; this.properties = properties; } @@ -67,6 +72,7 @@ DefaultJmsListenerContainerFactoryConfigurer jmsListenerContainerFactoryConfigur configurer.setDestinationResolver(this.destinationResolver.getIfUnique()); configurer.setTransactionManager(this.transactionManager.getIfUnique()); configurer.setMessageConverter(this.messageConverter.getIfUnique()); + configurer.setExceptionListener(this.exceptionListener.getIfUnique()); configurer.setJmsProperties(this.properties); return configurer; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JndiConnectionFactoryAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JndiConnectionFactoryAutoConfiguration.java index 73733af06cc9..b3075bd43382 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JndiConnectionFactoryAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JndiConnectionFactoryAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,7 +55,7 @@ public class JndiConnectionFactoryAutoConfiguration { private static final String[] JNDI_LOCATIONS = { "java:/JmsXA", "java:/XAConnectionFactory" }; @Bean - public ConnectionFactory connectionFactory(JmsProperties properties) throws NamingException { + public ConnectionFactory jmsConnectionFactory(JmsProperties properties) throws NamingException { JndiLocatorDelegate jndiLocatorDelegate = JndiLocatorDelegate.createDefaultResourceRefLocator(); if (StringUtils.hasLength(properties.getJndiName())) { return jndiLocatorDelegate.lookup(properties.getJndiName(), ConnectionFactory.class); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQConnectionFactoryConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQConnectionFactoryConfiguration.java index 3288d1c233cb..e53aa4b65fb8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQConnectionFactoryConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQConnectionFactoryConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -73,8 +73,7 @@ private static ActiveMQConnectionFactory createJmsConnectionFactory(ActiveMQProp static class CachingConnectionFactoryConfiguration { @Bean - CachingConnectionFactory cachingJmsConnectionFactory(JmsProperties jmsProperties, - ActiveMQProperties properties, + CachingConnectionFactory jmsConnectionFactory(JmsProperties jmsProperties, ActiveMQProperties properties, ObjectProvider factoryCustomizers) { JmsProperties.Cache cacheProperties = jmsProperties.getCache(); CachingConnectionFactory connectionFactory = new CachingConnectionFactory( @@ -95,7 +94,7 @@ static class PooledConnectionFactoryConfiguration { @Bean(destroyMethod = "stop") @ConditionalOnProperty(prefix = "spring.activemq.pool", name = "enabled", havingValue = "true") - JmsPoolConnectionFactory pooledJmsConnectionFactory(ActiveMQProperties properties, + JmsPoolConnectionFactory jmsConnectionFactory(ActiveMQProperties properties, ObjectProvider factoryCustomizers) { ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactoryFactory(properties, factoryCustomizers.orderedStream().collect(Collectors.toList())) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConfigurationCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConfigurationCustomizer.java index d603c1920f4e..59aee98817e8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConfigurationCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConfigurationCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,12 +17,12 @@ package org.springframework.boot.autoconfigure.jms.artemis; import org.apache.activemq.artemis.core.config.Configuration; -import org.apache.activemq.artemis.jms.server.embedded.EmbeddedJMS; +import org.apache.activemq.artemis.core.server.embedded.EmbeddedActiveMQ; /** * Callback interface that can be implemented by beans wishing to customize the Artemis * JMS server {@link Configuration} before it is used by an auto-configured - * {@link EmbeddedJMS} instance. + * {@link EmbeddedActiveMQ} instance. * * @author Eddú Meléndez * @author Phillip Webb diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionFactoryConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionFactoryConfiguration.java index 7c441fa984d4..33ca773b33f6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionFactoryConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionFactoryConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -58,7 +58,7 @@ static class SimpleConnectionFactoryConfiguration { this.beanFactory = beanFactory; } - @Bean + @Bean(name = "jmsConnectionFactory") @ConditionalOnProperty(prefix = "spring.jms.cache", name = "enabled", havingValue = "true", matchIfMissing = true) CachingConnectionFactory cachingJmsConnectionFactory(JmsProperties jmsProperties) { @@ -70,7 +70,7 @@ CachingConnectionFactory cachingJmsConnectionFactory(JmsProperties jmsProperties return connectionFactory; } - @Bean + @Bean(name = "jmsConnectionFactory") @ConditionalOnProperty(prefix = "spring.jms.cache", name = "enabled", havingValue = "false") ActiveMQConnectionFactory jmsConnectionFactory() { return createConnectionFactory(); @@ -85,13 +85,11 @@ private ActiveMQConnectionFactory createConnectionFactory() { @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ JmsPoolConnectionFactory.class, PooledObject.class }) + @ConditionalOnProperty(prefix = "spring.artemis.pool", name = "enabled", havingValue = "true") static class PooledConnectionFactoryConfiguration { @Bean(destroyMethod = "stop") - @ConditionalOnProperty(prefix = "spring.artemis.pool", name = "enabled", havingValue = "true", - matchIfMissing = false) - JmsPoolConnectionFactory pooledJmsConnectionFactory(ListableBeanFactory beanFactory, - ArtemisProperties properties) { + JmsPoolConnectionFactory jmsConnectionFactory(ListableBeanFactory beanFactory, ArtemisProperties properties) { ActiveMQConnectionFactory connectionFactory = new ArtemisConnectionFactoryFactory(beanFactory, properties) .createConnectionFactory(ActiveMQConnectionFactory.class); return new JmsPoolConnectionFactoryFactory(properties.getPool()) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionFactoryFactory.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionFactoryFactory.java index fd0a76c1fa3b..62de6fb0ed45 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionFactoryFactory.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionFactoryFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,9 +40,12 @@ * @author Eddú Meléndez * @author Phillip Webb * @author Stephane Nicoll + * @author Justin Bertram */ class ArtemisConnectionFactoryFactory { + private static final String DEFAULT_BROKER_URL = "tcp://localhost:61616"; + static final String[] EMBEDDED_JMS_CLASSES = { "org.apache.activemq.artemis.jms.server.embedded.EmbeddedJMS", "org.apache.activemq.artemis.core.server.embedded.EmbeddedActiveMQ" }; @@ -68,10 +71,10 @@ T createConnectionFactory(Class factory } private void startEmbeddedJms() { - for (int i = 0; i < EMBEDDED_JMS_CLASSES.length; i++) { - if (ClassUtils.isPresent(EMBEDDED_JMS_CLASSES[i], null)) { + for (String embeddedJmsClass : EMBEDDED_JMS_CLASSES) { + if (ClassUtils.isPresent(embeddedJmsClass, null)) { try { - this.beanFactory.getBeansOfType(Class.forName(EMBEDDED_JMS_CLASSES[i])); + this.beanFactory.getBeansOfType(Class.forName(embeddedJmsClass)); } catch (Exception ex) { // Ignore @@ -103,8 +106,8 @@ private ArtemisMode deduceMode() { } private boolean isEmbeddedJmsClassPresent() { - for (int i = 0; i < EMBEDDED_JMS_CLASSES.length; i++) { - if (ClassUtils.isPresent(EMBEDDED_JMS_CLASSES[i], null)) { + for (String embeddedJmsClass : EMBEDDED_JMS_CLASSES) { + if (ClassUtils.isPresent(embeddedJmsClass, null)) { return true; } } @@ -127,13 +130,7 @@ private T createEmbeddedConnectionFactory( private T createNativeConnectionFactory(Class factoryClass) throws Exception { - Map params = new HashMap<>(); - params.put(TransportConstants.HOST_PROP_NAME, this.properties.getHost()); - params.put(TransportConstants.PORT_PROP_NAME, this.properties.getPort()); - TransportConfiguration transportConfiguration = new TransportConfiguration( - NettyConnectorFactory.class.getName(), params); - Constructor constructor = factoryClass.getConstructor(boolean.class, TransportConfiguration[].class); - T connectionFactory = constructor.newInstance(false, new TransportConfiguration[] { transportConfiguration }); + T connectionFactory = newNativeConnectionFactory(factoryClass); String user = this.properties.getUser(); if (StringUtils.hasText(user)) { connectionFactory.setUser(user); @@ -142,4 +139,23 @@ private T createNativeConnectionFactory(Cl return connectionFactory; } + @SuppressWarnings("deprecation") + private T newNativeConnectionFactory(Class factoryClass) throws Exception { + // Fallback if the broker url is not set + if (!StringUtils.hasText(this.properties.getBrokerUrl()) && StringUtils.hasText(this.properties.getHost())) { + Map params = new HashMap<>(); + params.put(TransportConstants.HOST_PROP_NAME, this.properties.getHost()); + params.put(TransportConstants.PORT_PROP_NAME, this.properties.getPort()); + TransportConfiguration transportConfiguration = new TransportConfiguration( + NettyConnectorFactory.class.getName(), params); + Constructor constructor = factoryClass.getConstructor(boolean.class, TransportConfiguration[].class); + return constructor.newInstance(false, new TransportConfiguration[] { transportConfiguration }); + } + String brokerUrl = StringUtils.hasText(this.properties.getBrokerUrl()) ? this.properties.getBrokerUrl() + : DEFAULT_BROKER_URL; + Constructor constructor = factoryClass.getConstructor(String.class); + return constructor.newInstance(brokerUrl); + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisEmbeddedConfigurationFactory.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisEmbeddedConfigurationFactory.java index 3df21be4eee2..6d47dcfd5863 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisEmbeddedConfigurationFactory.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisEmbeddedConfigurationFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,12 +18,12 @@ import java.io.File; +import org.apache.activemq.artemis.api.core.QueueConfiguration; import org.apache.activemq.artemis.api.core.RoutingType; import org.apache.activemq.artemis.api.core.SimpleString; import org.apache.activemq.artemis.api.core.TransportConfiguration; import org.apache.activemq.artemis.core.config.Configuration; import org.apache.activemq.artemis.core.config.CoreAddressConfiguration; -import org.apache.activemq.artemis.core.config.CoreQueueConfiguration; import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl; import org.apache.activemq.artemis.core.remoting.impl.invm.InVMAcceptorFactory; import org.apache.activemq.artemis.core.server.JournalType; @@ -77,7 +77,7 @@ Configuration createConfiguration() { private CoreAddressConfiguration createAddressConfiguration(String name) { return new CoreAddressConfiguration().setName(name).addRoutingType(RoutingType.ANYCAST).addQueueConfiguration( - new CoreQueueConfiguration().setName(name).setRoutingType(RoutingType.ANYCAST).setAddress(name)); + new QueueConfiguration(name).setRoutingType(RoutingType.ANYCAST).setAddress(name)); } private String getDataDir() { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisEmbeddedServerConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisEmbeddedServerConfiguration.java index 300b2639057a..336b87c695ca 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisEmbeddedServerConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisEmbeddedServerConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,9 +19,9 @@ import java.util.List; import java.util.stream.Collectors; +import org.apache.activemq.artemis.api.core.QueueConfiguration; import org.apache.activemq.artemis.api.core.RoutingType; import org.apache.activemq.artemis.core.config.CoreAddressConfiguration; -import org.apache.activemq.artemis.core.config.CoreQueueConfiguration; import org.apache.activemq.artemis.core.server.embedded.EmbeddedActiveMQ; import org.apache.activemq.artemis.jms.server.config.JMSConfiguration; import org.apache.activemq.artemis.jms.server.config.JMSQueueConfiguration; @@ -71,7 +71,7 @@ EmbeddedActiveMQ embeddedActiveMq(org.apache.activemq.artemis.core.config.Config String queueName = queueConfiguration.getName(); configuration.addAddressConfiguration( new CoreAddressConfiguration().setName(queueName).addRoutingType(RoutingType.ANYCAST) - .addQueueConfiguration(new CoreQueueConfiguration().setAddress(queueName).setName(queueName) + .addQueueConfiguration(new QueueConfiguration(queueName).setAddress(queueName) .setFilterString(queueConfiguration.getSelector()) .setDurable(queueConfiguration.isDurable()).setRoutingType(RoutingType.ANYCAST))); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisProperties.java index 1b227f60a7bc..2ec338156450 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import org.springframework.boot.autoconfigure.jms.JmsPoolConnectionFactoryProperties; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; import org.springframework.boot.context.properties.NestedConfigurationProperty; /** @@ -32,6 +33,7 @@ * * @author Eddú Meléndez * @author Stephane Nicoll + * @author Justin Bertram * @since 1.3.0 */ @ConfigurationProperties(prefix = "spring.artemis") @@ -42,10 +44,15 @@ public class ArtemisProperties { */ private ArtemisMode mode; + /** + * Artemis broker port. + */ + private String brokerUrl; + /** * Artemis broker host. */ - private String host = "localhost"; + private String host; /** * Artemis broker port. @@ -75,18 +82,42 @@ public void setMode(ArtemisMode mode) { this.mode = mode; } + public String getBrokerUrl() { + return this.brokerUrl; + } + + public void setBrokerUrl(String brokerUrl) { + this.brokerUrl = brokerUrl; + } + + /** + * Return the host of the broker. + * @return the host + * @deprecated since 2.5.0 for removal in 2.7.0 in favor of broker url + */ + @Deprecated + @DeprecatedConfigurationProperty(replacement = "spring.artemis.broker-url") public String getHost() { return this.host; } + @Deprecated public void setHost(String host) { this.host = host; } + /** + * Return the port of the broker. + * @return the port + * @deprecated since 2.5.0 for removal in 2.7.0 in favor of broker url + */ + @Deprecated + @DeprecatedConfigurationProperty(replacement = "spring.artemis.broker-url") public int getPort() { return this.port; } + @Deprecated public void setPort(int port) { this.port = port; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jmx/ParentAwareNamingStrategy.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jmx/ParentAwareNamingStrategy.java index 492ca6e119f8..288552fe1b96 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jmx/ParentAwareNamingStrategy.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jmx/ParentAwareNamingStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import org.springframework.context.ApplicationContextAware; import org.springframework.jmx.export.metadata.JmxAttributeSource; import org.springframework.jmx.export.naming.MetadataNamingStrategy; +import org.springframework.jmx.support.JmxUtils; import org.springframework.jmx.support.ObjectNameManager; import org.springframework.util.ObjectUtils; @@ -48,28 +49,28 @@ public ParentAwareNamingStrategy(JmxAttributeSource attributeSource) { /** * Set if unique runtime object names should be ensured. - * @param ensureUniqueRuntimeObjectNames {@code true} if unique names should ensured. + * @param ensureUniqueRuntimeObjectNames {@code true} if unique names should be + * ensured. */ public void setEnsureUniqueRuntimeObjectNames(boolean ensureUniqueRuntimeObjectNames) { this.ensureUniqueRuntimeObjectNames = ensureUniqueRuntimeObjectNames; } + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + @Override public ObjectName getObjectName(Object managedBean, String beanKey) throws MalformedObjectNameException { ObjectName name = super.getObjectName(managedBean, beanKey); - Hashtable properties = new Hashtable<>(name.getKeyPropertyList()); if (this.ensureUniqueRuntimeObjectNames) { - properties.put("identity", ObjectUtils.getIdentityHexString(managedBean)); + return JmxUtils.appendIdentityToObjectName(name, managedBean); } - else if (parentContextContainsSameBean(this.applicationContext, beanKey)) { - properties.put("context", ObjectUtils.getIdentityHexString(this.applicationContext)); + if (parentContextContainsSameBean(this.applicationContext, beanKey)) { + return appendToObjectName(name, "context", ObjectUtils.getIdentityHexString(this.applicationContext)); } - return ObjectNameManager.getInstance(name.getDomain(), properties); - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.applicationContext = applicationContext; + return name; } private boolean parentContextContainsSameBean(ApplicationContext context, String beanKey) { @@ -85,4 +86,11 @@ private boolean parentContextContainsSameBean(ApplicationContext context, String } } + private ObjectName appendToObjectName(ObjectName name, String key, String value) + throws MalformedObjectNameException { + Hashtable keyProperties = name.getKeyPropertyList(); + keyProperties.put(key, value); + return ObjectNameManager.getInstance(name.getDomain(), keyProperties); + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/DefaultConfigurationCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/DefaultConfigurationCustomizer.java new file mode 100644 index 000000000000..54e074db1431 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/DefaultConfigurationCustomizer.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jooq; + +import org.jooq.impl.DefaultConfiguration; + +/** + * Callback interface that can be implemented by beans wishing to customize the + * {@link DefaultConfiguration} whilst retaining default auto-configuration. + * + * @author Stephane Nicoll + * @since 2.5.0 + */ +@FunctionalInterface +public interface DefaultConfigurationCustomizer { + + /** + * Customize the {@link DefaultConfiguration jOOQ Configuration}. + * @param configuration the configuration to customize + */ + void customize(DefaultConfiguration configuration); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/DslContextDependsOnPostProcessor.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/DslContextDependsOnPostProcessor.java new file mode 100644 index 000000000000..3cb72988701e --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/DslContextDependsOnPostProcessor.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jooq; + +import org.jooq.DSLContext; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.boot.autoconfigure.AbstractDependsOnBeanFactoryPostProcessor; +import org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitializationDetector; + +/** + * {@link BeanFactoryPostProcessor} that can be used to dynamically declare that all + * {@link DSLContext} beans should "depend on" one or more specific beans. + * + * @author Eddú Meléndez + * @since 2.3.9 + * @see BeanDefinition#setDependsOn(String[]) + * @deprecated since 2.5.0 for removal in 2.7.0 in favor of + * {@link DependsOnDatabaseInitializationDetector} + */ +@Deprecated +public class DslContextDependsOnPostProcessor extends AbstractDependsOnBeanFactoryPostProcessor { + + /** + * Creates a new {@code DslContextDependsOnPostProcessor} that will set up + * dependencies upon beans with the given names. + * @param dependsOn names of the beans to depend upon + */ + public DslContextDependsOnPostProcessor(String... dependsOn) { + super(DSLContext.class, dependsOn); + } + + /** + * Creates a new {@code DslContextDependsOnPostProcessor} that will set up + * dependencies upon beans with the given types. + * @param dependsOn types of the beans to depend upon + */ + public DslContextDependsOnPostProcessor(Class... dependsOn) { + super(DSLContext.class, dependsOn); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfiguration.java index da2f9265c018..91e4bc5de0ed 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,6 +45,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy; import org.springframework.transaction.PlatformTransactionManager; @@ -94,28 +95,58 @@ public DefaultDSLContext dslContext(org.jooq.Configuration configuration) { @Bean @ConditionalOnMissingBean(org.jooq.Configuration.class) public DefaultConfiguration jooqConfiguration(JooqProperties properties, ConnectionProvider connectionProvider, - DataSource dataSource, ObjectProvider transactionProvider, + DataSource dataSource, ObjectProvider executeListenerProviders, + ObjectProvider configurationCustomizers) { + DefaultConfiguration configuration = new DefaultConfiguration(); + configuration.set(properties.determineSqlDialect(dataSource)); + configuration.set(connectionProvider); + configuration.set(executeListenerProviders.orderedStream().toArray(ExecuteListenerProvider[]::new)); + configurationCustomizers.orderedStream().forEach((customizer) -> customizer.customize(configuration)); + return configuration; + } + + @Bean + @Deprecated + public DefaultConfigurationCustomizer jooqProvidersDefaultConfigurationCustomizer( + ObjectProvider transactionProvider, ObjectProvider recordMapperProvider, ObjectProvider recordUnmapperProvider, ObjectProvider settings, ObjectProvider recordListenerProviders, - ObjectProvider executeListenerProviders, ObjectProvider visitListenerProviders, ObjectProvider transactionListenerProviders, ObjectProvider executorProvider) { - DefaultConfiguration configuration = new DefaultConfiguration(); - configuration.set(properties.determineSqlDialect(dataSource)); - configuration.set(connectionProvider); - transactionProvider.ifAvailable(configuration::set); - recordMapperProvider.ifAvailable(configuration::set); - recordUnmapperProvider.ifAvailable(configuration::set); - settings.ifAvailable(configuration::set); - executorProvider.ifAvailable(configuration::set); - configuration.set(recordListenerProviders.orderedStream().toArray(RecordListenerProvider[]::new)); - configuration.set(executeListenerProviders.orderedStream().toArray(ExecuteListenerProvider[]::new)); - configuration.set(visitListenerProviders.orderedStream().toArray(VisitListenerProvider[]::new)); - configuration.setTransactionListenerProvider( - transactionListenerProviders.orderedStream().toArray(TransactionListenerProvider[]::new)); - return configuration; + return new OrderedDefaultConfigurationCustomizer((configuration) -> { + transactionProvider.ifAvailable(configuration::set); + recordMapperProvider.ifAvailable(configuration::set); + recordUnmapperProvider.ifAvailable(configuration::set); + settings.ifAvailable(configuration::set); + executorProvider.ifAvailable(configuration::set); + configuration.set(recordListenerProviders.orderedStream().toArray(RecordListenerProvider[]::new)); + configuration.set(visitListenerProviders.orderedStream().toArray(VisitListenerProvider[]::new)); + configuration.setTransactionListenerProvider( + transactionListenerProviders.orderedStream().toArray(TransactionListenerProvider[]::new)); + }); + } + + } + + private static class OrderedDefaultConfigurationCustomizer implements DefaultConfigurationCustomizer, Ordered { + + private final DefaultConfigurationCustomizer delegate; + + OrderedDefaultConfigurationCustomizer(DefaultConfigurationCustomizer delegate) { + this.delegate = delegate; + } + + @Override + public void customize(DefaultConfiguration configuration) { + this.delegate.customize(configuration); + + } + + @Override + public int getOrder() { + return 0; } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslator.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslator.java index 075934c882bb..5a5c4743cd71 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslator.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -80,10 +80,12 @@ private SQLExceptionTranslator getTranslator(ExecuteContext context) { private void handle(ExecuteContext context, SQLExceptionTranslator translator, SQLException exception) { DataAccessException translated = translate(context, translator, exception); if (exception.getNextException() == null) { - context.exception(translated); + if (translated != null) { + context.exception(translated); + } } else { - logger.error("Execution of SQL statement failed.", translated); + logger.error("Execution of SQL statement failed.", (translated != null) ? translated : exception); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/NoDslContextBeanFailureAnalyzer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/NoDslContextBeanFailureAnalyzer.java new file mode 100644 index 000000000000..480d1ba5d900 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/NoDslContextBeanFailureAnalyzer.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jooq; + +import org.jooq.DSLContext; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; +import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; +import org.springframework.boot.diagnostics.FailureAnalysis; +import org.springframework.core.Ordered; + +class NoDslContextBeanFailureAnalyzer extends AbstractFailureAnalyzer + implements Ordered, BeanFactoryAware { + + private BeanFactory beanFactory; + + @Override + protected FailureAnalysis analyze(Throwable rootFailure, NoSuchBeanDefinitionException cause) { + if (DSLContext.class.equals(cause.getBeanType()) && hasR2dbcAutoConfiguration()) { + return new FailureAnalysis( + "jOOQ has not been auto-configured as R2DBC has been auto-configured in favor of JDBC and jOOQ " + + "auto-configuration does not yet support R2DBC. ", + "To use jOOQ with JDBC, exclude R2dbcAutoConfiguration. To use jOOQ with R2DBC, define your own " + + "jOOQ configuration.", + cause); + } + return null; + } + + private boolean hasR2dbcAutoConfiguration() { + try { + this.beanFactory.getBean(R2dbcAutoConfiguration.class); + return true; + } + catch (Exception ex) { + return false; + } + } + + @Override + public int getOrder() { + return 0; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/SqlDialectLookup.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/SqlDialectLookup.java index 5c03f0bde952..d3c81f88fee0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/SqlDialectLookup.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/SqlDialectLookup.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.boot.autoconfigure.jooq; +import java.sql.DatabaseMetaData; + import javax.sql.DataSource; import org.apache.commons.logging.Log; @@ -49,7 +51,7 @@ static SQLDialect getDialect(DataSource dataSource) { return SQLDialect.DEFAULT; } try { - String url = JdbcUtils.extractDatabaseMetaData(dataSource, "getURL"); + String url = JdbcUtils.extractDatabaseMetaData(dataSource, DatabaseMetaData::getURL); SQLDialect sqlDialect = JDBCUtils.dialect(url); if (sqlDialect != null) { return sqlDialect; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryConfigurer.java index 28ca5b491414..47b12837bd19 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ import org.springframework.kafka.listener.ContainerProperties; import org.springframework.kafka.listener.ErrorHandler; import org.springframework.kafka.listener.RecordInterceptor; +import org.springframework.kafka.listener.adapter.RecordFilterStrategy; import org.springframework.kafka.support.converter.MessageConverter; import org.springframework.kafka.transaction.KafkaAwareTransactionManager; @@ -45,6 +46,8 @@ public class ConcurrentKafkaListenerContainerFactoryConfigurer { private MessageConverter messageConverter; + private RecordFilterStrategy recordFilterStrategy; + private KafkaTemplate replyTemplate; private KafkaAwareTransactionManager transactionManager; @@ -75,6 +78,14 @@ void setMessageConverter(MessageConverter messageConverter) { this.messageConverter = messageConverter; } + /** + * Set the {@link RecordFilterStrategy} to use to filter incoming records. + * @param recordFilterStrategy the record filter strategy + */ + void setRecordFilterStrategy(RecordFilterStrategy recordFilterStrategy) { + this.recordFilterStrategy = recordFilterStrategy; + } + /** * Set the {@link KafkaTemplate} to use to send replies. * @param replyTemplate the reply template @@ -151,6 +162,7 @@ private void configureListenerFactory(ConcurrentKafkaListenerContainerFactory consumerFactory); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/DefaultKafkaProducerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/DefaultKafkaProducerFactoryCustomizer.java new file mode 100644 index 000000000000..7865947b1eb9 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/DefaultKafkaProducerFactoryCustomizer.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.kafka; + +import org.springframework.kafka.core.DefaultKafkaProducerFactory; + +/** + * Callback interface for customizing {@code DefaultKafkaProducerFactory} beans. + * + * @author Stephane Nicoll + * @since 2.3.0 + */ +@FunctionalInterface +public interface DefaultKafkaProducerFactoryCustomizer { + + /** + * Customize the {@link DefaultKafkaProducerFactory}. + * @param producerFactory the producer factory to customize + */ + void customize(DefaultKafkaProducerFactory producerFactory); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaAnnotationDrivenConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaAnnotationDrivenConfiguration.java index 1fb2e2643d40..e11a6b13865d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaAnnotationDrivenConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaAnnotationDrivenConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,12 +26,14 @@ import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; import org.springframework.kafka.config.KafkaListenerConfigUtils; import org.springframework.kafka.core.ConsumerFactory; +import org.springframework.kafka.core.DefaultKafkaConsumerFactory; import org.springframework.kafka.core.KafkaTemplate; import org.springframework.kafka.listener.AfterRollbackProcessor; import org.springframework.kafka.listener.BatchErrorHandler; import org.springframework.kafka.listener.ConsumerAwareRebalanceListener; import org.springframework.kafka.listener.ErrorHandler; import org.springframework.kafka.listener.RecordInterceptor; +import org.springframework.kafka.listener.adapter.RecordFilterStrategy; import org.springframework.kafka.support.converter.BatchMessageConverter; import org.springframework.kafka.support.converter.BatchMessagingMessageConverter; import org.springframework.kafka.support.converter.MessageConverter; @@ -52,6 +54,8 @@ class KafkaAnnotationDrivenConfiguration { private final RecordMessageConverter messageConverter; + private final RecordFilterStrategy recordFilterStrategy; + private final BatchMessageConverter batchMessageConverter; private final KafkaTemplate kafkaTemplate; @@ -70,6 +74,7 @@ class KafkaAnnotationDrivenConfiguration { KafkaAnnotationDrivenConfiguration(KafkaProperties properties, ObjectProvider messageConverter, + ObjectProvider> recordFilterStrategy, ObjectProvider batchMessageConverter, ObjectProvider> kafkaTemplate, ObjectProvider> kafkaTransactionManager, @@ -79,6 +84,7 @@ class KafkaAnnotationDrivenConfiguration { ObjectProvider> recordInterceptor) { this.properties = properties; this.messageConverter = messageConverter.getIfUnique(); + this.recordFilterStrategy = recordFilterStrategy.getIfUnique(); this.batchMessageConverter = batchMessageConverter .getIfUnique(() -> new BatchMessagingMessageConverter(this.messageConverter)); this.kafkaTemplate = kafkaTemplate.getIfUnique(); @@ -98,6 +104,7 @@ ConcurrentKafkaListenerContainerFactoryConfigurer kafkaListenerContainerFactoryC MessageConverter messageConverterToUse = (this.properties.getListener().getType().equals(Type.BATCH)) ? this.batchMessageConverter : this.messageConverter; configurer.setMessageConverter(messageConverterToUse); + configurer.setRecordFilterStrategy(this.recordFilterStrategy); configurer.setReplyTemplate(this.kafkaTemplate); configurer.setTransactionManager(this.transactionManager); configurer.setRebalanceListener(this.rebalanceListener); @@ -112,9 +119,10 @@ ConcurrentKafkaListenerContainerFactoryConfigurer kafkaListenerContainerFactoryC @ConditionalOnMissingBean(name = "kafkaListenerContainerFactory") ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory( ConcurrentKafkaListenerContainerFactoryConfigurer configurer, - ConsumerFactory kafkaConsumerFactory) { + ObjectProvider> kafkaConsumerFactory) { ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>(); - configurer.configure(factory, kafkaConsumerFactory); + configurer.configure(factory, kafkaConsumerFactory + .getIfAvailable(() -> new DefaultKafkaConsumerFactory<>(this.properties.buildConsumerProperties()))); return factory; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfiguration.java index d4a806cc2581..6a6e0183293f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -81,19 +81,25 @@ public ProducerListener kafkaProducerListener() { @Bean @ConditionalOnMissingBean(ConsumerFactory.class) - public ConsumerFactory kafkaConsumerFactory() { - return new DefaultKafkaConsumerFactory<>(this.properties.buildConsumerProperties()); + public ConsumerFactory kafkaConsumerFactory( + ObjectProvider customizers) { + DefaultKafkaConsumerFactory factory = new DefaultKafkaConsumerFactory<>( + this.properties.buildConsumerProperties()); + customizers.orderedStream().forEach((customizer) -> customizer.customize(factory)); + return factory; } @Bean @ConditionalOnMissingBean(ProducerFactory.class) - public ProducerFactory kafkaProducerFactory() { + public ProducerFactory kafkaProducerFactory( + ObjectProvider customizers) { DefaultKafkaProducerFactory factory = new DefaultKafkaProducerFactory<>( this.properties.buildProducerProperties()); String transactionIdPrefix = this.properties.getProducer().getTransactionIdPrefix(); if (transactionIdPrefix != null) { factory.setTransactionIdPrefix(transactionIdPrefix); } + customizers.orderedStream().forEach((customizer) -> customizer.customize(factory)); return factory; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaProperties.java index 7f9b95c6d43a..e76dd85de209 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,6 @@ import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.producer.ProducerConfig; import org.apache.kafka.common.config.SslConfigs; -import org.apache.kafka.common.requests.IsolationLevel; import org.apache.kafka.common.serialization.StringDeserializer; import org.apache.kafka.common.serialization.StringSerializer; @@ -91,6 +90,8 @@ public class KafkaProperties { private final Template template = new Template(); + private final Security security = new Security(); + public List getBootstrapServers() { return this.bootstrapServers; } @@ -143,6 +144,10 @@ public Template getTemplate() { return this.template; } + public Security getSecurity() { + return this.security; + } + private Map buildCommonProperties() { Map properties = new HashMap<>(); if (this.bootstrapServers != null) { @@ -152,6 +157,7 @@ private Map buildCommonProperties() { properties.put(CommonClientConfigs.CLIENT_ID_CONFIG, this.clientId); } properties.putAll(this.ssl.buildProperties()); + properties.putAll(this.security.buildProperties()); if (!CollectionUtils.isEmpty(this.properties)) { properties.putAll(this.properties); } @@ -217,6 +223,8 @@ public static class Consumer { private final Ssl ssl = new Ssl(); + private final Security security = new Security(); + /** * Frequency with which the consumer offsets are auto-committed to Kafka if * 'enable.auto.commit' is set to true. @@ -297,6 +305,10 @@ public Ssl getSsl() { return this.ssl; } + public Security getSecurity() { + return this.security; + } + public Duration getAutoCommitInterval() { return this.autoCommitInterval; } @@ -426,7 +438,7 @@ public Map buildProperties() { map.from(this::getKeyDeserializer).to(properties.in(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG)); map.from(this::getValueDeserializer).to(properties.in(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG)); map.from(this::getMaxPollRecords).to(properties.in(ConsumerConfig.MAX_POLL_RECORDS_CONFIG)); - return properties.with(this.ssl, this.properties); + return properties.with(this.ssl, this.security, this.properties); } } @@ -435,6 +447,8 @@ public static class Producer { private final Ssl ssl = new Ssl(); + private final Security security = new Security(); + /** * Number of acknowledgments the producer requires the leader to have received * before considering a request complete. @@ -498,6 +512,10 @@ public Ssl getSsl() { return this.ssl; } + public Security getSecurity() { + return this.security; + } + public String getAcks() { return this.acks; } @@ -595,7 +613,7 @@ public Map buildProperties() { map.from(this::getKeySerializer).to(properties.in(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG)); map.from(this::getRetries).to(properties.in(ProducerConfig.RETRIES_CONFIG)); map.from(this::getValueSerializer).to(properties.in(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG)); - return properties.with(this.ssl, this.properties); + return properties.with(this.ssl, this.security, this.properties); } } @@ -604,6 +622,8 @@ public static class Admin { private final Ssl ssl = new Ssl(); + private final Security security = new Security(); + /** * ID to pass to the server when making requests. Used for server-side logging. */ @@ -623,6 +643,10 @@ public Ssl getSsl() { return this.ssl; } + public Security getSecurity() { + return this.security; + } + public String getClientId() { return this.clientId; } @@ -647,7 +671,7 @@ public Map buildProperties() { Properties properties = new Properties(); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); map.from(this::getClientId).to(properties.in(ProducerConfig.CLIENT_ID_CONFIG)); - return properties.with(this.ssl, this.properties); + return properties.with(this.ssl, this.security, this.properties); } } @@ -659,6 +683,10 @@ public static class Streams { private final Ssl ssl = new Ssl(); + private final Security security = new Security(); + + private final Cleanup cleanup = new Cleanup(); + /** * Kafka streams application.id property; default spring.application.name. */ @@ -705,6 +733,14 @@ public Ssl getSsl() { return this.ssl; } + public Security getSecurity() { + return this.security; + } + + public Cleanup getCleanup() { + return this.cleanup; + } + public String getApplicationId() { return this.applicationId; } @@ -775,7 +811,7 @@ public Map buildProperties() { map.from(this::getClientId).to(properties.in(CommonClientConfigs.CLIENT_ID_CONFIG)); map.from(this::getReplicationFactor).to(properties.in("replication.factor")); map.from(this::getStateDir).to(properties.in("state.dir")); - return properties.with(this.ssl, this.properties); + return properties.with(this.ssl, this.security, this.properties); } } @@ -855,6 +891,11 @@ public enum Type { */ private Duration ackTime; + /** + * Sleep interval between Consumer.poll(Duration) calls. + */ + private Duration idleBetweenPolls = Duration.ZERO; + /** * Time between publishing idle consumer events (no data received). */ @@ -872,11 +913,17 @@ public enum Type { */ private Boolean logContainerConfig; + /** + * Whether to suppress the entire record from being written to the log when + * retries are being attempted. + */ + private boolean onlyLogRecordMetadata = true; + /** * Whether the container should fail to start if at least one of the configured * topics are not present on the broker. */ - private boolean missingTopicsFatal = true; + private boolean missingTopicsFatal = false; public Type getType() { return this.type; @@ -942,6 +989,14 @@ public void setAckTime(Duration ackTime) { this.ackTime = ackTime; } + public Duration getIdleBetweenPolls() { + return this.idleBetweenPolls; + } + + public void setIdleBetweenPolls(Duration idleBetweenPolls) { + this.idleBetweenPolls = idleBetweenPolls; + } + public Duration getIdleEventInterval() { return this.idleEventInterval; } @@ -966,6 +1021,14 @@ public void setLogContainerConfig(Boolean logContainerConfig) { this.logContainerConfig = logContainerConfig; } + public boolean isOnlyLogRecordMetadata() { + return this.onlyLogRecordMetadata; + } + + public void setOnlyLogRecordMetadata(boolean onlyLogRecordMetadata) { + this.onlyLogRecordMetadata = onlyLogRecordMetadata; + } + public boolean isMissingTopicsFatal() { return this.missingTopicsFatal; } @@ -1167,6 +1230,85 @@ public void setOptions(Map options) { } + public static class Security { + + /** + * Security protocol used to communicate with brokers. + */ + private String protocol; + + public String getProtocol() { + return this.protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + public Map buildProperties() { + Properties properties = new Properties(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(this::getProtocol).to(properties.in(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG)); + return properties; + } + + } + + public static class Cleanup { + + /** + * Cleanup the application’s local state directory on startup. + */ + private boolean onStartup = false; + + /** + * Cleanup the application’s local state directory on shutdown. + */ + private boolean onShutdown = false; + + public boolean isOnStartup() { + return this.onStartup; + } + + public void setOnStartup(boolean onStartup) { + this.onStartup = onStartup; + } + + public boolean isOnShutdown() { + return this.onShutdown; + } + + public void setOnShutdown(boolean onShutdown) { + this.onShutdown = onShutdown; + } + + } + + public enum IsolationLevel { + + /** + * Read everything including aborted transactions. + */ + READ_UNCOMMITTED((byte) 0), + + /** + * Read records from committed transactions, in addition to records not part of + * transactions. + */ + READ_COMMITTED((byte) 1); + + private final byte id; + + IsolationLevel(byte id) { + this.id = id; + } + + public byte id() { + return this.id; + } + + } + @SuppressWarnings("serial") private static class Properties extends HashMap { @@ -1174,8 +1316,9 @@ java.util.function.Consumer in(String key) { return (value) -> put(key, value); } - Properties with(Ssl ssl, Map properties) { + Properties with(Ssl ssl, Security security, Map properties) { putAll(ssl.buildProperties()); + putAll(security.buildProperties()); putAll(properties); return this; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaStreamsAnnotationDrivenConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaStreamsAnnotationDrivenConfiguration.java index 06f22c7e0b62..feec421f8404 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaStreamsAnnotationDrivenConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaStreamsAnnotationDrivenConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import org.apache.kafka.streams.StreamsConfig; import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -33,12 +34,14 @@ import org.springframework.kafka.annotation.KafkaStreamsDefaultConfiguration; import org.springframework.kafka.config.KafkaStreamsConfiguration; import org.springframework.kafka.config.StreamsBuilderFactoryBean; +import org.springframework.kafka.core.CleanupConfig; /** * Configuration for Kafka Streams annotation-driven support. * * @author Gary Russell * @author Stephane Nicoll + * @author Eddú Meléndez */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(StreamsBuilder.class) @@ -68,7 +71,9 @@ KafkaStreamsConfiguration defaultKafkaStreamsConfig(Environment environment) { @Bean KafkaStreamsFactoryBeanConfigurer kafkaStreamsFactoryBeanConfigurer( - @Qualifier(KafkaStreamsDefaultConfiguration.DEFAULT_STREAMS_BUILDER_BEAN_NAME) StreamsBuilderFactoryBean factoryBean) { + @Qualifier(KafkaStreamsDefaultConfiguration.DEFAULT_STREAMS_BUILDER_BEAN_NAME) StreamsBuilderFactoryBean factoryBean, + ObjectProvider customizers) { + customizers.orderedStream().forEach((customizer) -> customizer.customize(factoryBean)); return new KafkaStreamsFactoryBeanConfigurer(this.properties, factoryBean); } @@ -87,6 +92,9 @@ static class KafkaStreamsFactoryBeanConfigurer implements InitializingBean { @Override public void afterPropertiesSet() { this.factoryBean.setAutoStartup(this.properties.getStreams().isAutoStartup()); + KafkaProperties.Cleanup cleanup = this.properties.getStreams().getCleanup(); + CleanupConfig cleanupConfig = new CleanupConfig(cleanup.isOnStartup(), cleanup.isOnShutdown()); + this.factoryBean.setCleanupConfig(cleanupConfig); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/StreamsBuilderFactoryBeanCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/StreamsBuilderFactoryBeanCustomizer.java new file mode 100644 index 000000000000..789eb846d736 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/StreamsBuilderFactoryBeanCustomizer.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.kafka; + +import org.springframework.kafka.config.StreamsBuilderFactoryBean; + +/** + * Callback interface for customizing {@code StreamsBuilderFactoryBean} beans. + * + * @author Eddú Meléndez + * @since 2.3.2 + */ +@FunctionalInterface +public interface StreamsBuilderFactoryBeanCustomizer { + + /** + * Customize the {@link StreamsBuilderFactoryBean}. + * @param factoryBean the factory bean to customize + */ + void customize(StreamsBuilderFactoryBean factoryBean); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/LdapAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/LdapAutoConfiguration.java index afb14cf81cc2..a4707e6364e6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/LdapAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/LdapAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,9 +18,11 @@ import java.util.Collections; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.ldap.LdapProperties.Template; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.context.annotation.Bean; @@ -29,6 +31,7 @@ import org.springframework.ldap.core.ContextSource; import org.springframework.ldap.core.LdapOperations; import org.springframework.ldap.core.LdapTemplate; +import org.springframework.ldap.core.support.DirContextAuthenticationStrategy; import org.springframework.ldap.core.support.LdapContextSource; /** @@ -45,8 +48,10 @@ public class LdapAutoConfiguration { @Bean @ConditionalOnMissingBean - public LdapContextSource ldapContextSource(LdapProperties properties, Environment environment) { + public LdapContextSource ldapContextSource(LdapProperties properties, Environment environment, + ObjectProvider dirContextAuthenticationStrategy) { LdapContextSource source = new LdapContextSource(); + dirContextAuthenticationStrategy.ifUnique(source::setAuthenticationStrategy); PropertyMapper propertyMapper = PropertyMapper.get().alwaysApplyingWhenNonNull(); propertyMapper.from(properties.getUsername()).to(source::setUserDn); propertyMapper.from(properties.getPassword()).to(source::setPassword); @@ -60,8 +65,16 @@ public LdapContextSource ldapContextSource(LdapProperties properties, Environmen @Bean @ConditionalOnMissingBean(LdapOperations.class) - public LdapTemplate ldapTemplate(ContextSource contextSource) { - return new LdapTemplate(contextSource); + public LdapTemplate ldapTemplate(LdapProperties properties, ContextSource contextSource) { + Template template = properties.getTemplate(); + PropertyMapper propertyMapper = PropertyMapper.get().alwaysApplyingWhenNonNull(); + LdapTemplate ldapTemplate = new LdapTemplate(contextSource); + propertyMapper.from(template.isIgnorePartialResultException()) + .to(ldapTemplate::setIgnorePartialResultException); + propertyMapper.from(template.isIgnoreNameNotFoundException()).to(ldapTemplate::setIgnoreNameNotFoundException); + propertyMapper.from(template.isIgnoreSizeLimitExceededException()) + .to(ldapTemplate::setIgnoreSizeLimitExceededException); + return ldapTemplate; } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/LdapProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/LdapProperties.java index db02437662cc..239e97f32b5b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/LdapProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/LdapProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.core.env.Environment; +import org.springframework.ldap.core.LdapTemplate; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -56,15 +57,18 @@ public class LdapProperties { private String password; /** - * Whether read-only operations should use an anonymous environment. + * Whether read-only operations should use an anonymous environment. Disabled by + * default unless a username is set. */ - private boolean anonymousReadOnly; + private Boolean anonymousReadOnly; /** * LDAP specification settings. */ private final Map baseEnvironment = new HashMap<>(); + private final Template template = new Template(); + public String[] getUrls() { return this.urls; } @@ -97,11 +101,11 @@ public void setPassword(String password) { this.password = password; } - public boolean getAnonymousReadOnly() { + public Boolean getAnonymousReadOnly() { return this.anonymousReadOnly; } - public void setAnonymousReadOnly(boolean anonymousReadOnly) { + public void setAnonymousReadOnly(Boolean anonymousReadOnly) { this.anonymousReadOnly = anonymousReadOnly; } @@ -109,6 +113,10 @@ public Map getBaseEnvironment() { return this.baseEnvironment; } + public Template getTemplate() { + return this.template; + } + public String[] determineUrls(Environment environment) { if (ObjectUtils.isEmpty(this.urls)) { return new String[] { "ldap://localhost:" + determinePort(environment) }; @@ -120,9 +128,58 @@ private int determinePort(Environment environment) { Assert.notNull(environment, "Environment must not be null"); String localPort = environment.getProperty("local.ldap.port"); if (localPort != null) { - return Integer.valueOf(localPort); + return Integer.parseInt(localPort); } return DEFAULT_PORT; } + /** + * {@link LdapTemplate settings}. + */ + public static class Template { + + /** + * Whether PartialResultException should be ignored in searches via the + * LdapTemplate. + */ + private boolean ignorePartialResultException = false; + + /** + * Whether NameNotFoundException should be ignored in searches via the + * LdapTemplate. + */ + private boolean ignoreNameNotFoundException = false; + + /** + * Whether SizeLimitExceededException should be ignored in searches via the + * LdapTemplate. + */ + private boolean ignoreSizeLimitExceededException = true; + + public boolean isIgnorePartialResultException() { + return this.ignorePartialResultException; + } + + public void setIgnorePartialResultException(boolean ignorePartialResultException) { + this.ignorePartialResultException = ignorePartialResultException; + } + + public boolean isIgnoreNameNotFoundException() { + return this.ignoreNameNotFoundException; + } + + public void setIgnoreNameNotFoundException(boolean ignoreNameNotFoundException) { + this.ignoreNameNotFoundException = ignoreNameNotFoundException; + } + + public boolean isIgnoreSizeLimitExceededException() { + return this.ignoreSizeLimitExceededException; + } + + public void setIgnoreSizeLimitExceededException(Boolean ignoreSizeLimitExceededException) { + this.ignoreSizeLimitExceededException = ignoreSizeLimitExceededException; + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/embedded/EmbeddedLdapAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/embedded/EmbeddedLdapAutoConfiguration.java index 18634cae039e..8abb907c1e67 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/embedded/EmbeddedLdapAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/embedded/EmbeddedLdapAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,6 @@ import org.springframework.boot.autoconfigure.condition.SpringBootCondition; import org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration; import org.springframework.boot.autoconfigure.ldap.LdapProperties; -import org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapProperties.Credential; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; @@ -58,6 +57,7 @@ import org.springframework.core.env.PropertySource; import org.springframework.core.io.Resource; import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.ldap.core.ContextSource; import org.springframework.ldap.core.support.LdapContextSource; import org.springframework.util.StringUtils; @@ -86,24 +86,11 @@ public EmbeddedLdapAutoConfiguration(EmbeddedLdapProperties embeddedProperties) this.embeddedProperties = embeddedProperties; } - @Bean - @DependsOn("directoryServer") - @ConditionalOnMissingBean - public LdapContextSource ldapContextSource(Environment environment, LdapProperties properties) { - LdapContextSource source = new LdapContextSource(); - if (hasCredentials(this.embeddedProperties.getCredential())) { - source.setUserDn(this.embeddedProperties.getCredential().getUsername()); - source.setPassword(this.embeddedProperties.getCredential().getPassword()); - } - source.setUrls(properties.determineUrls(environment)); - return source; - } - @Bean public InMemoryDirectoryServer directoryServer(ApplicationContext applicationContext) throws LDAPException { String[] baseDn = StringUtils.toStringArray(this.embeddedProperties.getBaseDn()); InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(baseDn); - if (hasCredentials(this.embeddedProperties.getCredential())) { + if (this.embeddedProperties.getCredential().isAvailable()) { config.addAdditionalBindCredentials(this.embeddedProperties.getCredential().getUsername(), this.embeddedProperties.getCredential().getPassword()); } @@ -140,10 +127,6 @@ private void setSchema(InMemoryDirectoryServerConfig config, Resource resource) } } - private boolean hasCredentials(Credential credential) { - return StringUtils.hasText(credential.getUsername()) && StringUtils.hasText(credential.getPassword()); - } - private void importLdif(ApplicationContext applicationContext) throws LDAPException { String location = this.embeddedProperties.getLdif(); if (StringUtils.hasText(location)) { @@ -210,4 +193,24 @@ public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeM } + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(ContextSource.class) + static class EmbeddedLdapContextConfiguration { + + @Bean + @DependsOn("directoryServer") + @ConditionalOnMissingBean + LdapContextSource ldapContextSource(Environment environment, LdapProperties properties, + EmbeddedLdapProperties embeddedProperties) { + LdapContextSource source = new LdapContextSource(); + if (embeddedProperties.getCredential().isAvailable()) { + source.setUserDn(embeddedProperties.getCredential().getUsername()); + source.setPassword(embeddedProperties.getCredential().getPassword()); + } + source.setUrls(properties.determineUrls(environment)); + return source; + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/embedded/EmbeddedLdapProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/embedded/EmbeddedLdapProperties.java index fe4b8dfca608..ef563541a640 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/embedded/EmbeddedLdapProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/embedded/EmbeddedLdapProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.convert.Delimiter; import org.springframework.core.io.Resource; +import org.springframework.util.StringUtils; /** * Configuration properties for Embedded LDAP. @@ -123,6 +124,10 @@ public void setPassword(String password) { this.password = password; } + boolean isAvailable() { + return StringUtils.hasText(this.username) && StringUtils.hasText(this.password); + } + } public static class Validation { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguration.java index 2d165d52fee6..1984d44fcb3a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,6 @@ package org.springframework.boot.autoconfigure.liquibase; -import java.util.function.Supplier; - -import javax.persistence.EntityManagerFactory; import javax.sql.DataSource; import liquibase.change.DatabaseChange; @@ -32,26 +29,20 @@ 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.data.jpa.EntityManagerFactoryDependsOnPostProcessor; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; -import org.springframework.boot.autoconfigure.jdbc.JdbcOperationsDependsOnPostProcessor; -import org.springframework.boot.autoconfigure.jdbc.NamedParameterJdbcOperationsDependsOnPostProcessor; import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration.LiquibaseDataSourceCondition; -import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration.LiquibaseEntityManagerFactoryDependsOnPostProcessor; -import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration.LiquibaseJdbcOperationsDependsOnPostProcessor; -import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration.LiquibaseNamedParameterJdbcOperationsDependsOnPostProcessor; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.boot.sql.init.dependency.DatabaseInitializationDependencyConfigurer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import org.springframework.jdbc.core.JdbcOperations; -import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; -import org.springframework.orm.jpa.AbstractEntityManagerFactoryBean; -import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.jdbc.core.ConnectionCallback; +import org.springframework.jdbc.datasource.SimpleDriverDataSource; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * {@link EnableAutoConfiguration Auto-configuration} for Liquibase. @@ -64,6 +55,8 @@ * @author Dominic Gunn * @author Dan Zheng * @author András Deák + * @author Ferenc Gratzer + * @author Evgeniy Cheban * @since 1.1.0 */ @Configuration(proxyBeanMethods = false) @@ -71,9 +64,7 @@ @ConditionalOnProperty(prefix = "spring.liquibase", name = "enabled", matchIfMissing = true) @Conditional(LiquibaseDataSourceCondition.class) @AutoConfigureAfter({ DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class }) -@Import({ LiquibaseEntityManagerFactoryDependsOnPostProcessor.class, - LiquibaseJdbcOperationsDependsOnPostProcessor.class, - LiquibaseNamedParameterJdbcOperationsDependsOnPostProcessor.class }) +@Import(DatabaseInitializationDependencyConfigurer.class) public class LiquibaseAutoConfiguration { @Bean @@ -83,8 +74,9 @@ public LiquibaseSchemaManagementProvider liquibaseDefaultDdlModeProvider( } @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(ConnectionCallback.class) @ConditionalOnMissingBean(SpringLiquibase.class) - @EnableConfigurationProperties({ DataSourceProperties.class, LiquibaseProperties.class }) + @EnableConfigurationProperties(LiquibaseProperties.class) public static class LiquibaseConfiguration { private final LiquibaseProperties properties; @@ -94,12 +86,12 @@ public LiquibaseConfiguration(LiquibaseProperties properties) { } @Bean - public SpringLiquibase liquibase(DataSourceProperties dataSourceProperties, - ObjectProvider dataSource, + public SpringLiquibase liquibase(ObjectProvider dataSource, @LiquibaseDataSource ObjectProvider liquibaseDataSource) { SpringLiquibase liquibase = createSpringLiquibase(liquibaseDataSource.getIfAvailable(), - dataSource.getIfUnique(), dataSourceProperties); + dataSource.getIfUnique()); liquibase.setChangeLog(this.properties.getChangeLog()); + liquibase.setClearCheckSums(this.properties.isClearChecksums()); liquibase.setContexts(this.properties.getContexts()); liquibase.setDefaultSchema(this.properties.getDefaultSchema()); liquibase.setLiquibaseSchema(this.properties.getLiquibaseSchema()); @@ -112,86 +104,47 @@ public SpringLiquibase liquibase(DataSourceProperties dataSourceProperties, liquibase.setChangeLogParameters(this.properties.getParameters()); liquibase.setRollbackFile(this.properties.getRollbackFile()); liquibase.setTestRollbackOnUpdate(this.properties.isTestRollbackOnUpdate()); + liquibase.setTag(this.properties.getTag()); return liquibase; } - private SpringLiquibase createSpringLiquibase(DataSource liquibaseDatasource, DataSource dataSource, - DataSourceProperties dataSourceProperties) { - DataSource liquibaseDataSource = getDataSource(liquibaseDatasource, dataSource); - if (liquibaseDataSource != null) { - SpringLiquibase liquibase = new SpringLiquibase(); - liquibase.setDataSource(liquibaseDataSource); - return liquibase; - } - SpringLiquibase liquibase = new DataSourceClosingSpringLiquibase(); - liquibase.setDataSource(createNewDataSource(dataSourceProperties)); + private SpringLiquibase createSpringLiquibase(DataSource liquibaseDataSource, DataSource dataSource) { + LiquibaseProperties properties = this.properties; + DataSource migrationDataSource = getMigrationDataSource(liquibaseDataSource, dataSource, properties); + SpringLiquibase liquibase = (migrationDataSource == liquibaseDataSource + || migrationDataSource == dataSource) ? new SpringLiquibase() + : new DataSourceClosingSpringLiquibase(); + liquibase.setDataSource(migrationDataSource); return liquibase; } - private DataSource getDataSource(DataSource liquibaseDataSource, DataSource dataSource) { + private DataSource getMigrationDataSource(DataSource liquibaseDataSource, DataSource dataSource, + LiquibaseProperties properties) { if (liquibaseDataSource != null) { return liquibaseDataSource; } - if (this.properties.getUrl() == null && this.properties.getUser() == null) { - return dataSource; + if (properties.getUrl() != null) { + DataSourceBuilder builder = DataSourceBuilder.create().type(SimpleDriverDataSource.class); + builder.url(properties.getUrl()); + applyCommonBuilderProperties(properties, builder); + return builder.build(); } - return null; - } - - private DataSource createNewDataSource(DataSourceProperties dataSourceProperties) { - String url = getProperty(this.properties::getUrl, dataSourceProperties::determineUrl); - String user = getProperty(this.properties::getUser, dataSourceProperties::determineUsername); - String password = getProperty(this.properties::getPassword, dataSourceProperties::determinePassword); - return DataSourceBuilder.create().url(url).username(user).password(password).build(); - } - - private String getProperty(Supplier property, Supplier defaultValue) { - String value = property.get(); - return (value != null) ? value : defaultValue.get(); - } - - } - - /** - * Post processor to ensure that {@link EntityManagerFactory} beans depend on the - * liquibase bean. - */ - @ConditionalOnClass(LocalContainerEntityManagerFactoryBean.class) - @ConditionalOnBean(AbstractEntityManagerFactoryBean.class) - static class LiquibaseEntityManagerFactoryDependsOnPostProcessor - extends EntityManagerFactoryDependsOnPostProcessor { - - LiquibaseEntityManagerFactoryDependsOnPostProcessor() { - super(SpringLiquibase.class); - } - - } - - /** - * Additional configuration to ensure that {@link JdbcOperations} beans depend on the - * liquibase bean. - */ - @ConditionalOnClass(JdbcOperations.class) - @ConditionalOnBean(JdbcOperations.class) - static class LiquibaseJdbcOperationsDependsOnPostProcessor extends JdbcOperationsDependsOnPostProcessor { - - LiquibaseJdbcOperationsDependsOnPostProcessor() { - super(SpringLiquibase.class); + if (properties.getUser() != null && dataSource != null) { + DataSourceBuilder builder = DataSourceBuilder.derivedFrom(dataSource) + .type(SimpleDriverDataSource.class); + applyCommonBuilderProperties(properties, builder); + return builder.build(); + } + Assert.state(dataSource != null, "Liquibase migration DataSource missing"); + return dataSource; } - } - - /** - * Post processor to ensure that {@link NamedParameterJdbcOperations} beans depend on - * the liquibase bean. - */ - @ConditionalOnClass(NamedParameterJdbcOperations.class) - @ConditionalOnBean(NamedParameterJdbcOperations.class) - static class LiquibaseNamedParameterJdbcOperationsDependsOnPostProcessor - extends NamedParameterJdbcOperationsDependsOnPostProcessor { - - LiquibaseNamedParameterJdbcOperationsDependsOnPostProcessor() { - super(SpringLiquibase.class); + private void applyCommonBuilderProperties(LiquibaseProperties properties, DataSourceBuilder builder) { + builder.username(properties.getUser()); + builder.password(properties.getPassword()); + if (StringUtils.hasText(properties.getDriverClassName())) { + builder.driverClassName(properties.getDriverClassName()); + } } } @@ -207,7 +160,7 @@ private static final class DataSourceBeanCondition { } - @ConditionalOnProperty(prefix = "spring.liquibase", name = "url", matchIfMissing = false) + @ConditionalOnProperty(prefix = "spring.liquibase", name = "url") private static final class LiquibaseUrlCondition { } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseProperties.java index 2fe7fdf4e932..7dea8fc68667 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,9 @@ * Configuration properties to configure {@link SpringLiquibase}. * * @author Marcel Overdijk + * @author Eddú Meléndez + * @author Ferenc Gratzer + * @author Evgeniy Cheban * @since 1.1.0 */ @ConfigurationProperties(prefix = "spring.liquibase", ignoreUnknownFields = false) @@ -38,6 +41,12 @@ public class LiquibaseProperties { */ private String changeLog = "classpath:/db/changelog/db.changelog-master.yaml"; + /** + * Whether to clear all checksums in the current changelog, so they will be + * recalculated upon the next update. + */ + private boolean clearChecksums; + /** * Comma-separated list of runtime contexts to use. */ @@ -88,6 +97,11 @@ public class LiquibaseProperties { */ private String password; + /** + * Fully qualified name of the JDBC driver. Auto-detected based on the URL by default. + */ + private String driverClassName; + /** * JDBC URL of the database to migrate. If not set, the primary configured data source * is used. @@ -114,6 +128,13 @@ public class LiquibaseProperties { */ private boolean testRollbackOnUpdate; + /** + * Tag name to use when applying database changes. Can also be used with + * "rollbackFile" to generate a rollback script for all existing changes associated + * with that tag. + */ + private String tag; + public String getChangeLog() { return this.changeLog; } @@ -179,6 +200,14 @@ public void setDropFirst(boolean dropFirst) { this.dropFirst = dropFirst; } + public boolean isClearChecksums() { + return this.clearChecksums; + } + + public void setClearChecksums(boolean clearChecksums) { + this.clearChecksums = clearChecksums; + } + public boolean isEnabled() { return this.enabled; } @@ -203,6 +232,14 @@ public void setPassword(String password) { this.password = password; } + public String getDriverClassName() { + return this.driverClassName; + } + + public void setDriverClassName(String driverClassName) { + this.driverClassName = driverClassName; + } + public String getUrl() { return this.url; } @@ -243,4 +280,12 @@ public void setTestRollbackOnUpdate(boolean testRollbackOnUpdate) { this.testRollbackOnUpdate = testRollbackOnUpdate; } + public String getTag() { + return this.tag; + } + + public void setTag(String tag) { + this.tag = tag; + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/logging/ConditionEvaluationReportLoggingListener.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/logging/ConditionEvaluationReportLoggingListener.java index ea72312dfd66..00328e129a07 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/logging/ConditionEvaluationReportLoggingListener.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/logging/ConditionEvaluationReportLoggingListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -113,7 +113,7 @@ public void logAutoConfigurationReport(boolean isCrashReport) { this.report = ConditionEvaluationReport.get(this.applicationContext.getBeanFactory()); } if (!this.report.getConditionAndOutcomesBySource().isEmpty()) { - if (this.getLogLevelForReport().equals(LogLevel.INFO)) { + if (getLogLevelForReport().equals(LogLevel.INFO)) { if (this.logger.isInfoEnabled()) { this.logger.info(new ConditionEvaluationReportMessage(this.report)); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mail/MailProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mail/MailProperties.java index bf69098a51cc..2c5e40402fba 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mail/MailProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mail/MailProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,7 +37,7 @@ public class MailProperties { private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; /** - * SMTP server host. For instance, `smtp.example.com`. + * SMTP server host. For instance, 'smtp.example.com'. */ private String host; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mail/MailSenderValidatorAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mail/MailSenderValidatorAutoConfiguration.java index ff262e4fbca9..e72104819fa0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mail/MailSenderValidatorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mail/MailSenderValidatorAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.springframework.boot.autoconfigure.mail; -import javax.annotation.PostConstruct; import javax.mail.MessagingException; import org.springframework.boot.autoconfigure.AutoConfigureAfter; @@ -44,9 +43,9 @@ public class MailSenderValidatorAutoConfiguration { public MailSenderValidatorAutoConfiguration(JavaMailSenderImpl mailSender) { this.mailSender = mailSender; + validateConnection(); } - @PostConstruct public void validateConnection() { try { this.mailSender.testConnection(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoAutoConfiguration.java index 7e4d882df45b..9b1f2acfe5f6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,10 @@ package org.springframework.boot.autoconfigure.mongo; -import com.mongodb.MongoClient; -import com.mongodb.MongoClientOptions; +import java.util.stream.Collectors; + +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -36,19 +38,38 @@ * @author Phillip Webb * @author Mark Paluch * @author Stephane Nicoll + * @author Scott Frederick * @since 1.0.0 */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(MongoClient.class) @EnableConfigurationProperties(MongoProperties.class) -@ConditionalOnMissingBean(type = "org.springframework.data.mongodb.MongoDbFactory") +@ConditionalOnMissingBean(type = "org.springframework.data.mongodb.MongoDatabaseFactory") public class MongoAutoConfiguration { @Bean - @ConditionalOnMissingBean(type = { "com.mongodb.MongoClient", "com.mongodb.client.MongoClient" }) - public MongoClient mongo(MongoProperties properties, ObjectProvider options, - Environment environment) { - return new MongoClientFactory(properties, environment).createMongoClient(options.getIfAvailable()); + @ConditionalOnMissingBean(MongoClient.class) + public MongoClient mongo(ObjectProvider builderCustomizers, + MongoClientSettings settings) { + return new MongoClientFactory(builderCustomizers.orderedStream().collect(Collectors.toList())) + .createMongoClient(settings); + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(MongoClientSettings.class) + static class MongoClientSettingsConfiguration { + + @Bean + MongoClientSettings mongoClientSettings() { + return MongoClientSettings.builder().build(); + } + + @Bean + MongoPropertiesClientSettingsBuilderCustomizer mongoPropertiesCustomizer(MongoProperties properties, + Environment environment) { + return new MongoPropertiesClientSettingsBuilderCustomizer(properties, environment); + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoClientFactory.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoClientFactory.java index 0717a51d8490..d0c31bfd4a79 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoClientFactory.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoClientFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,20 +16,13 @@ package org.springframework.boot.autoconfigure.mongo; -import java.util.Collections; import java.util.List; -import com.mongodb.MongoClient; -import com.mongodb.MongoClientOptions; -import com.mongodb.MongoClientOptions.Builder; -import com.mongodb.MongoClientURI; -import com.mongodb.MongoCredential; -import com.mongodb.ServerAddress; - -import org.springframework.core.env.Environment; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; /** - * A factory for a blocking {@link MongoClient} that applies {@link MongoProperties}. + * A factory for a blocking {@link MongoClient}. * * @author Dave Syer * @author Phillip Webb @@ -39,102 +32,17 @@ * @author Stephane Nicoll * @author Nasko Vasilev * @author Mark Paluch + * @author Scott Frederick * @since 2.0.0 */ -public class MongoClientFactory { - - private final MongoProperties properties; - - private final Environment environment; - - public MongoClientFactory(MongoProperties properties, Environment environment) { - this.properties = properties; - this.environment = environment; - } +public class MongoClientFactory extends MongoClientFactorySupport { /** - * Creates a {@link MongoClient} using the given {@code options}. If the environment - * contains a {@code local.mongo.port} property, it is used to configure a client to - * an embedded MongoDB instance. - * @param options the options - * @return the Mongo client + * Construct a factory for creating a blocking {@link MongoClient}. + * @param builderCustomizers a list of configuration settings customizers */ - public MongoClient createMongoClient(MongoClientOptions options) { - Integer embeddedPort = getEmbeddedPort(); - if (embeddedPort != null) { - return createEmbeddedMongoClient(options, embeddedPort); - } - return createNetworkMongoClient(options); - } - - private Integer getEmbeddedPort() { - if (this.environment != null) { - String localPort = this.environment.getProperty("local.mongo.port"); - if (localPort != null) { - return Integer.valueOf(localPort); - } - } - return null; - } - - private MongoClient createEmbeddedMongoClient(MongoClientOptions options, int port) { - if (options == null) { - options = MongoClientOptions.builder().build(); - } - String host = (this.properties.getHost() != null) ? this.properties.getHost() : "localhost"; - return new MongoClient(Collections.singletonList(new ServerAddress(host, port)), options); - } - - private MongoClient createNetworkMongoClient(MongoClientOptions options) { - MongoProperties properties = this.properties; - if (properties.getUri() != null) { - return createMongoClient(properties.getUri(), options); - } - if (hasCustomAddress() || hasCustomCredentials()) { - if (options == null) { - options = MongoClientOptions.builder().build(); - } - MongoCredential credentials = getCredentials(properties); - String host = getValue(properties.getHost(), "localhost"); - int port = getValue(properties.getPort(), MongoProperties.DEFAULT_PORT); - List seeds = Collections.singletonList(new ServerAddress(host, port)); - return (credentials != null) ? new MongoClient(seeds, credentials, options) - : new MongoClient(seeds, options); - } - return createMongoClient(MongoProperties.DEFAULT_URI, options); - } - - private MongoClient createMongoClient(String uri, MongoClientOptions options) { - return new MongoClient(new MongoClientURI(uri, builder(options))); - } - - private T getValue(T value, T fallback) { - return (value != null) ? value : fallback; - } - - private boolean hasCustomAddress() { - return this.properties.getHost() != null || this.properties.getPort() != null; - } - - private MongoCredential getCredentials(MongoProperties properties) { - if (!hasCustomCredentials()) { - return null; - } - String username = properties.getUsername(); - String database = getValue(properties.getAuthenticationDatabase(), properties.getMongoClientDatabase()); - char[] password = properties.getPassword(); - return MongoCredential.createCredential(username, database, password); - } - - private boolean hasCustomCredentials() { - return this.properties.getUsername() != null && this.properties.getPassword() != null; - } - - private Builder builder(MongoClientOptions options) { - if (options != null) { - return MongoClientOptions.builder(options); - } - return MongoClientOptions.builder(); + public MongoClientFactory(List builderCustomizers) { + super(builderCustomizers, MongoClients::create); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoClientFactorySupport.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoClientFactorySupport.java new file mode 100644 index 000000000000..be841adec8cc --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoClientFactorySupport.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.mongo; + +import java.util.Collections; +import java.util.List; +import java.util.function.BiFunction; + +import com.mongodb.MongoClientSettings; +import com.mongodb.MongoClientSettings.Builder; +import com.mongodb.MongoDriverInformation; + +/** + * Base class for setup that is common to MongoDB client factories. + * + * @param the mongo client type + * @author Christoph Strobl + * @author Scott Frederick + * @since 2.3.0 + */ +public abstract class MongoClientFactorySupport { + + private final List builderCustomizers; + + private final BiFunction clientCreator; + + protected MongoClientFactorySupport(List builderCustomizers, + BiFunction clientCreator) { + this.builderCustomizers = (builderCustomizers != null) ? builderCustomizers : Collections.emptyList(); + this.clientCreator = clientCreator; + } + + public T createMongoClient(MongoClientSettings settings) { + Builder targetSettings = MongoClientSettings.builder(settings); + customize(targetSettings); + return this.clientCreator.apply(targetSettings.build(), driverInformation()); + } + + private void customize(Builder builder) { + for (MongoClientSettingsBuilderCustomizer customizer : this.builderCustomizers) { + customizer.customize(builder); + } + } + + private MongoDriverInformation driverInformation() { + return MongoDriverInformation.builder(MongoDriverInformation.builder().build()).driverName("spring-boot") + .build(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoProperties.java index a19b605927e4..7b9d3cc43e99 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,11 @@ package org.springframework.boot.autoconfigure.mongo; -import com.mongodb.MongoClientURI; +import com.mongodb.ConnectionString; +import org.bson.UuidRepresentation; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; /** * Configuration properties for Mongo. @@ -58,7 +60,8 @@ public class MongoProperties { private Integer port = null; /** - * Mongo database URI. Cannot be set with host, port and credentials. + * Mongo database URI. Cannot be set with host, port, credentials and replica set + * name. */ private String uri; @@ -72,15 +75,11 @@ public class MongoProperties { */ private String authenticationDatabase; - /** - * GridFS database name. - */ - private String gridFsDatabase; + private final Gridfs gridfs = new Gridfs(); /** * Login user of the mongo server. Cannot be set with URI. */ - private String username; /** @@ -88,11 +87,21 @@ public class MongoProperties { */ private char[] password; + /** + * Required replica set name for the cluster. Cannot be set with URI. + */ + private String replicaSetName; + /** * Fully qualified name of the FieldNamingStrategy to use. */ private Class fieldNamingStrategy; + /** + * Representation to use when converting a UUID to a BSON binary value. + */ + private UuidRepresentation uuidRepresentation = UuidRepresentation.JAVA_LEGACY; + /** * Whether to enable auto-index creation. */ @@ -138,6 +147,14 @@ public void setPassword(char[] password) { this.password = password; } + public String getReplicaSetName() { + return this.replicaSetName; + } + + public void setReplicaSetName(String replicaSetName) { + this.replicaSetName = replicaSetName; + } + public Class getFieldNamingStrategy() { return this.fieldNamingStrategy; } @@ -146,6 +163,14 @@ public void setFieldNamingStrategy(Class fieldNamingStrategy) { this.fieldNamingStrategy = fieldNamingStrategy; } + public UuidRepresentation getUuidRepresentation() { + return this.uuidRepresentation; + } + + public void setUuidRepresentation(UuidRepresentation uuidRepresentation) { + this.uuidRepresentation = uuidRepresentation; + } + public String getUri() { return this.uri; } @@ -166,19 +191,32 @@ public void setPort(Integer port) { this.port = port; } + public Gridfs getGridfs() { + return this.gridfs; + } + + /** + * Return the GridFS database name. + * @return the GridFS database name + * @deprecated since 2.4.0 for removal in 2.6.0 in favor of + * {@link Gridfs#getDatabase()} + */ + @DeprecatedConfigurationProperty(replacement = "spring.data.mongodb.gridfs.database") + @Deprecated public String getGridFsDatabase() { - return this.gridFsDatabase; + return this.gridfs.getDatabase(); } + @Deprecated public void setGridFsDatabase(String gridFsDatabase) { - this.gridFsDatabase = gridFsDatabase; + this.gridfs.setDatabase(gridFsDatabase); } public String getMongoClientDatabase() { if (this.database != null) { return this.database; } - return new MongoClientURI(determineUri()).getDatabase(); + return new ConnectionString(determineUri()).getDatabase(); } public Boolean isAutoIndexCreation() { @@ -189,4 +227,34 @@ public void setAutoIndexCreation(Boolean autoIndexCreation) { this.autoIndexCreation = autoIndexCreation; } + public static class Gridfs { + + /** + * GridFS database name. + */ + private String database; + + /** + * GridFS bucket name. + */ + private String bucket; + + public String getDatabase() { + return this.database; + } + + public void setDatabase(String database) { + this.database = database; + } + + public String getBucket() { + return this.bucket; + } + + public void setBucket(String bucket) { + this.bucket = bucket; + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoPropertiesClientSettingsBuilderCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoPropertiesClientSettingsBuilderCustomizer.java new file mode 100644 index 000000000000..2326e00af35a --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoPropertiesClientSettingsBuilderCustomizer.java @@ -0,0 +1,143 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.mongo; + +import java.util.Collections; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.MongoCredential; +import com.mongodb.ServerAddress; + +import org.springframework.core.Ordered; +import org.springframework.core.env.Environment; +import org.springframework.util.Assert; + +/** + * A {@link MongoClientSettingsBuilderCustomizer} that applies properties from a + * {@link MongoProperties} to a {@link MongoClientSettings}. + * + * @author Scott Frederick + * @since 2.4.0 + */ +public class MongoPropertiesClientSettingsBuilderCustomizer implements MongoClientSettingsBuilderCustomizer, Ordered { + + private final MongoProperties properties; + + private final Environment environment; + + private int order = 0; + + public MongoPropertiesClientSettingsBuilderCustomizer(MongoProperties properties, Environment environment) { + this.properties = properties; + this.environment = environment; + } + + @Override + public void customize(MongoClientSettings.Builder settingsBuilder) { + validateConfiguration(); + applyUuidRepresentation(settingsBuilder); + applyHostAndPort(settingsBuilder); + applyCredentials(settingsBuilder); + applyReplicaSet(settingsBuilder); + } + + private void validateConfiguration() { + if (hasCustomAddress() || hasCustomCredentials() || hasReplicaSet()) { + Assert.state(this.properties.getUri() == null, + "Invalid mongo configuration, either uri or host/port/credentials/replicaSet must be specified"); + } + } + + private void applyUuidRepresentation(MongoClientSettings.Builder settingsBuilder) { + settingsBuilder.uuidRepresentation(this.properties.getUuidRepresentation()); + } + + private void applyHostAndPort(MongoClientSettings.Builder settings) { + if (getEmbeddedPort() != null) { + settings.applyConnectionString(new ConnectionString("mongodb://localhost:" + getEmbeddedPort())); + return; + } + + if (hasCustomAddress()) { + String host = getOrDefault(this.properties.getHost(), "localhost"); + int port = getOrDefault(this.properties.getPort(), MongoProperties.DEFAULT_PORT); + ServerAddress serverAddress = new ServerAddress(host, port); + settings.applyToClusterSettings((cluster) -> cluster.hosts(Collections.singletonList(serverAddress))); + return; + } + + settings.applyConnectionString(new ConnectionString(this.properties.determineUri())); + } + + private void applyCredentials(MongoClientSettings.Builder builder) { + if (hasCustomCredentials()) { + String database = (this.properties.getAuthenticationDatabase() != null) + ? this.properties.getAuthenticationDatabase() : this.properties.getMongoClientDatabase(); + builder.credential((MongoCredential.createCredential(this.properties.getUsername(), database, + this.properties.getPassword()))); + } + } + + private void applyReplicaSet(MongoClientSettings.Builder builder) { + if (hasReplicaSet()) { + builder.applyToClusterSettings( + (cluster) -> cluster.requiredReplicaSetName(this.properties.getReplicaSetName())); + } + } + + private V getOrDefault(V value, V defaultValue) { + return (value != null) ? value : defaultValue; + } + + private Integer getEmbeddedPort() { + if (this.environment != null) { + String localPort = this.environment.getProperty("local.mongo.port"); + if (localPort != null) { + return Integer.valueOf(localPort); + } + } + return null; + } + + private boolean hasCustomCredentials() { + return this.properties.getUsername() != null && this.properties.getPassword() != null; + } + + private boolean hasCustomAddress() { + return this.properties.getHost() != null || this.properties.getPort() != null; + } + + private boolean hasReplicaSet() { + return this.properties.getReplicaSetName() != null; + } + + @Override + public int getOrder() { + return this.order; + } + + /** + * Set the order value of this object. + * @param order the new order value + * @see #getOrder() + */ + public void setOrder(int order) { + this.order = order; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoReactiveAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoReactiveAutoConfiguration.java index 0e87bd1a7cff..c2b8fdaf8529 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoReactiveAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoReactiveAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,6 +44,7 @@ * * @author Mark Paluch * @author Stephane Nicoll + * @author Scott Frederick * @since 2.0.0 */ @Configuration(proxyBeanMethods = false) @@ -53,12 +54,28 @@ public class MongoReactiveAutoConfiguration { @Bean @ConditionalOnMissingBean - public MongoClient reactiveStreamsMongoClient(MongoProperties properties, Environment environment, - ObjectProvider builderCustomizers, - ObjectProvider settings) { - ReactiveMongoClientFactory factory = new ReactiveMongoClientFactory(properties, environment, + public MongoClient reactiveStreamsMongoClient( + ObjectProvider builderCustomizers, MongoClientSettings settings) { + ReactiveMongoClientFactory factory = new ReactiveMongoClientFactory( builderCustomizers.orderedStream().collect(Collectors.toList())); - return factory.createMongoClient(settings.getIfAvailable()); + return factory.createMongoClient(settings); + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(MongoClientSettings.class) + static class MongoClientSettingsConfiguration { + + @Bean + MongoClientSettings mongoClientSettings() { + return MongoClientSettings.builder().build(); + } + + @Bean + MongoPropertiesClientSettingsBuilderCustomizer mongoPropertiesCustomizer(MongoProperties properties, + Environment environment) { + return new MongoPropertiesClientSettingsBuilderCustomizer(properties, environment); + } + } @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/ReactiveMongoClientFactory.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/ReactiveMongoClientFactory.java index 24aa30f94249..2a026db7604d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/ReactiveMongoClientFactory.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/ReactiveMongoClientFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,136 +16,27 @@ package org.springframework.boot.autoconfigure.mongo; -import java.util.Collections; import java.util.List; -import com.mongodb.ConnectionString; -import com.mongodb.MongoClientSettings; -import com.mongodb.MongoClientSettings.Builder; -import com.mongodb.MongoCredential; -import com.mongodb.ServerAddress; import com.mongodb.reactivestreams.client.MongoClient; import com.mongodb.reactivestreams.client.MongoClients; -import org.springframework.core.env.Environment; -import org.springframework.util.Assert; - /** - * A factory for a reactive {@link MongoClient} that applies {@link MongoProperties}. + * A factory for a reactive {@link MongoClient}. * * @author Mark Paluch * @author Stephane Nicoll + * @author Scott Frederick * @since 2.0.0 */ -public class ReactiveMongoClientFactory { - - private final MongoProperties properties; - - private final Environment environment; - - private final List builderCustomizers; - - public ReactiveMongoClientFactory(MongoProperties properties, Environment environment, - List builderCustomizers) { - this.properties = properties; - this.environment = environment; - this.builderCustomizers = (builderCustomizers != null) ? builderCustomizers : Collections.emptyList(); - } +public class ReactiveMongoClientFactory extends MongoClientFactorySupport { /** - * Creates a {@link MongoClient} using the given {@code settings}. If the environment - * contains a {@code local.mongo.port} property, it is used to configure a client to - * an embedded MongoDB instance. - * @param settings the settings - * @return the Mongo client + * Construct a factory for creating a {@link MongoClient}. + * @param builderCustomizers a list of configuration settings customizers */ - public MongoClient createMongoClient(MongoClientSettings settings) { - Integer embeddedPort = getEmbeddedPort(); - if (embeddedPort != null) { - return createEmbeddedMongoClient(settings, embeddedPort); - } - return createNetworkMongoClient(settings); - } - - private Integer getEmbeddedPort() { - if (this.environment != null) { - String localPort = this.environment.getProperty("local.mongo.port"); - if (localPort != null) { - return Integer.valueOf(localPort); - } - } - return null; - } - - private MongoClient createEmbeddedMongoClient(MongoClientSettings settings, int port) { - Builder builder = builder(settings); - String host = (this.properties.getHost() != null) ? this.properties.getHost() : "localhost"; - builder.applyToClusterSettings( - (cluster) -> cluster.hosts(Collections.singletonList(new ServerAddress(host, port)))); - return createMongoClient(builder); - } - - private MongoClient createNetworkMongoClient(MongoClientSettings settings) { - if (hasCustomAddress() || hasCustomCredentials()) { - return createCredentialNetworkMongoClient(settings); - } - ConnectionString connectionString = new ConnectionString(this.properties.determineUri()); - return createMongoClient(createBuilder(settings, connectionString)); - } - - private MongoClient createCredentialNetworkMongoClient(MongoClientSettings settings) { - Assert.state(this.properties.getUri() == null, - "Invalid mongo configuration, either uri or host/port/credentials must be specified"); - Builder builder = builder(settings); - if (hasCustomCredentials()) { - applyCredentials(builder); - } - String host = getOrDefault(this.properties.getHost(), "localhost"); - int port = getOrDefault(this.properties.getPort(), MongoProperties.DEFAULT_PORT); - ServerAddress serverAddress = new ServerAddress(host, port); - builder.applyToClusterSettings((cluster) -> cluster.hosts(Collections.singletonList(serverAddress))); - return createMongoClient(builder); - } - - private void applyCredentials(Builder builder) { - String database = (this.properties.getAuthenticationDatabase() != null) - ? this.properties.getAuthenticationDatabase() : this.properties.getMongoClientDatabase(); - builder.credential((MongoCredential.createCredential(this.properties.getUsername(), database, - this.properties.getPassword()))); - } - - private T getOrDefault(T value, T defaultValue) { - return (value != null) ? value : defaultValue; - } - - private MongoClient createMongoClient(Builder builder) { - customize(builder); - return MongoClients.create(builder.build()); - } - - private Builder createBuilder(MongoClientSettings settings, ConnectionString connection) { - return builder(settings).applyConnectionString(connection); - } - - private void customize(MongoClientSettings.Builder builder) { - for (MongoClientSettingsBuilderCustomizer customizer : this.builderCustomizers) { - customizer.customize(builder); - } - } - - private boolean hasCustomAddress() { - return this.properties.getHost() != null || this.properties.getPort() != null; - } - - private boolean hasCustomCredentials() { - return this.properties.getUsername() != null && this.properties.getPassword() != null; - } - - private Builder builder(MongoClientSettings settings) { - if (settings == null) { - return MongoClientSettings.builder(); - } - return MongoClientSettings.builder(settings); + public ReactiveMongoClientFactory(List builderCustomizers) { + super(builderCustomizers, MongoClients::create); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/embedded/DownloadConfigBuilderCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/embedded/DownloadConfigBuilderCustomizer.java index bf623c15767d..1689d0bbab38 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/embedded/DownloadConfigBuilderCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/embedded/DownloadConfigBuilderCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,13 @@ package org.springframework.boot.autoconfigure.mongo.embedded; -import de.flapdoodle.embed.mongo.config.DownloadConfigBuilder; -import de.flapdoodle.embed.process.config.store.IDownloadConfig; +import de.flapdoodle.embed.process.config.store.DownloadConfig; +import de.flapdoodle.embed.process.config.store.ImmutableDownloadConfig; /** * Callback interface that can be implemented by beans wishing to customize the - * {@link IDownloadConfig} via a {@link DownloadConfigBuilder} whilst retaining default - * auto-configuration. + * {@link DownloadConfig} via an {@link ImmutableDownloadConfig.Builder} whilst retaining + * default auto-configuration. * * @author Michael Gmeiner * @since 2.2.0 @@ -31,9 +31,10 @@ public interface DownloadConfigBuilderCustomizer { /** - * Customize the {@link DownloadConfigBuilder}. - * @param downloadConfigBuilder the {@link DownloadConfigBuilder} to customize + * Customize the {@link ImmutableDownloadConfig.Builder}. + * @param downloadConfigBuilder the {@link ImmutableDownloadConfig.Builder} to + * customize */ - void customize(DownloadConfigBuilder downloadConfigBuilder); + void customize(ImmutableDownloadConfig.Builder downloadConfigBuilder); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/embedded/EmbeddedMongoAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/embedded/EmbeddedMongoAutoConfiguration.java index b0fb3f57f265..4217375ab784 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/embedded/EmbeddedMongoAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/embedded/EmbeddedMongoAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,30 +23,29 @@ import java.util.Map; import java.util.stream.Stream; -import com.mongodb.MongoClient; +import com.mongodb.MongoClientSettings; import de.flapdoodle.embed.mongo.Command; import de.flapdoodle.embed.mongo.MongodExecutable; import de.flapdoodle.embed.mongo.MongodStarter; -import de.flapdoodle.embed.mongo.config.DownloadConfigBuilder; -import de.flapdoodle.embed.mongo.config.ExtractedArtifactStoreBuilder; -import de.flapdoodle.embed.mongo.config.IMongodConfig; -import de.flapdoodle.embed.mongo.config.MongodConfigBuilder; +import de.flapdoodle.embed.mongo.config.Defaults; +import de.flapdoodle.embed.mongo.config.ImmutableMongodConfig; +import de.flapdoodle.embed.mongo.config.MongodConfig; import de.flapdoodle.embed.mongo.config.Net; -import de.flapdoodle.embed.mongo.config.RuntimeConfigBuilder; import de.flapdoodle.embed.mongo.config.Storage; import de.flapdoodle.embed.mongo.distribution.Feature; import de.flapdoodle.embed.mongo.distribution.IFeatureAwareVersion; import de.flapdoodle.embed.mongo.distribution.Version; import de.flapdoodle.embed.mongo.distribution.Versions; -import de.flapdoodle.embed.process.config.IRuntimeConfig; +import de.flapdoodle.embed.process.config.RuntimeConfig; import de.flapdoodle.embed.process.config.io.ProcessOutput; -import de.flapdoodle.embed.process.config.store.IDownloadConfig; -import de.flapdoodle.embed.process.distribution.GenericVersion; +import de.flapdoodle.embed.process.config.store.DownloadConfig; +import de.flapdoodle.embed.process.config.store.ImmutableDownloadConfig; +import de.flapdoodle.embed.process.distribution.Version.GenericVersion; import de.flapdoodle.embed.process.io.Processors; import de.flapdoodle.embed.process.io.Slf4jLevel; import de.flapdoodle.embed.process.io.progress.Slf4jProgressListener; import de.flapdoodle.embed.process.runtime.Network; -import de.flapdoodle.embed.process.store.ArtifactStoreBuilder; +import de.flapdoodle.embed.process.store.ExtractedArtifactStore; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -81,12 +80,13 @@ * @author Yogesh Lonkar * @author Mark Paluch * @author Issam El-atif + * @author Paulius Dambrauskas * @since 1.3.0 */ @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties({ MongoProperties.class, EmbeddedMongoProperties.class }) @AutoConfigureBefore(MongoAutoConfiguration.class) -@ConditionalOnClass({ MongoClient.class, MongodStarter.class }) +@ConditionalOnClass({ MongoClientSettings.class, MongodStarter.class }) @Import({ EmbeddedMongoClientDependsOnBeanFactoryPostProcessor.class, EmbeddedReactiveStreamsMongoClientDependsOnBeanFactoryPostProcessor.class }) public class EmbeddedMongoAutoConfiguration { @@ -97,14 +97,14 @@ public class EmbeddedMongoAutoConfiguration { private final MongoProperties properties; - public EmbeddedMongoAutoConfiguration(MongoProperties properties, EmbeddedMongoProperties embeddedProperties) { + public EmbeddedMongoAutoConfiguration(MongoProperties properties) { this.properties = properties; } @Bean(initMethod = "start", destroyMethod = "stop") @ConditionalOnMissingBean - public MongodExecutable embeddedMongoServer(IMongodConfig mongodConfig, IRuntimeConfig runtimeConfig, - ApplicationContext context) throws IOException { + public MongodExecutable embeddedMongoServer(MongodConfig mongodConfig, RuntimeConfig runtimeConfig, + ApplicationContext context) { Integer configuredPort = this.properties.getPort(); if (configuredPort == null || configuredPort == 0) { setEmbeddedPort(context, mongodConfig.net().getPort()); @@ -113,7 +113,7 @@ public MongodExecutable embeddedMongoServer(IMongodConfig mongodConfig, IRuntime return mongodStarter.prepare(mongodConfig); } - private MongodStarter getMongodStarter(IRuntimeConfig runtimeConfig) { + private MongodStarter getMongodStarter(RuntimeConfig runtimeConfig) { if (runtimeConfig == null) { return MongodStarter.getDefaultInstance(); } @@ -122,8 +122,8 @@ private MongodStarter getMongodStarter(IRuntimeConfig runtimeConfig) { @Bean @ConditionalOnMissingBean - public IMongodConfig embeddedMongoConfiguration(EmbeddedMongoProperties embeddedProperties) throws IOException { - MongodConfigBuilder builder = new MongodConfigBuilder().version(determineVersion(embeddedProperties)); + public MongodConfig embeddedMongoConfiguration(EmbeddedMongoProperties embeddedProperties) throws IOException { + ImmutableMongodConfig.Builder builder = MongodConfig.builder().version(determineVersion(embeddedProperties)); EmbeddedMongoProperties.Storage storage = embeddedProperties.getStorage(); if (storage != null) { String databaseDir = storage.getDatabaseDir(); @@ -149,12 +149,16 @@ private IFeatureAwareVersion determineVersion(EmbeddedMongoProperties embeddedPr return version; } } - return Versions.withFeatures(new GenericVersion(embeddedProperties.getVersion())); + return Versions.withFeatures(createEmbeddedMongoVersion(embeddedProperties)); } - return Versions.withFeatures(new GenericVersion(embeddedProperties.getVersion()), + return Versions.withFeatures(createEmbeddedMongoVersion(embeddedProperties), embeddedProperties.getFeatures().toArray(new Feature[0])); } + private GenericVersion createEmbeddedMongoVersion(EmbeddedMongoProperties embeddedProperties) { + return de.flapdoodle.embed.process.distribution.Version.of(embeddedProperties.getVersion()); + } + private InetAddress getHost() throws UnknownHostException { if (this.properties.getHost() == null) { return InetAddress.getByAddress(Network.localhostIsIPv6() ? IP6_LOOPBACK_ADDRESS : IP4_LOOPBACK_ADDRESS); @@ -189,38 +193,37 @@ private Map getMongoPorts(MutablePropertySources sources) { @Configuration(proxyBeanMethods = false) @ConditionalOnClass(Logger.class) - @ConditionalOnMissingBean(IRuntimeConfig.class) + @ConditionalOnMissingBean(RuntimeConfig.class) static class RuntimeConfigConfiguration { @Bean - IRuntimeConfig embeddedMongoRuntimeConfig( + RuntimeConfig embeddedMongoRuntimeConfig( ObjectProvider downloadConfigBuilderCustomizers) { Logger logger = LoggerFactory.getLogger(getClass().getPackage().getName() + ".EmbeddedMongo"); ProcessOutput processOutput = new ProcessOutput(Processors.logTo(logger, Slf4jLevel.INFO), Processors.logTo(logger, Slf4jLevel.ERROR), Processors.named("[console>]", Processors.logTo(logger, Slf4jLevel.DEBUG))); - return new RuntimeConfigBuilder().defaultsWithLogger(Command.MongoD, logger).processOutput(processOutput) + return Defaults.runtimeConfigFor(Command.MongoD, logger).processOutput(processOutput) .artifactStore(getArtifactStore(logger, downloadConfigBuilderCustomizers.orderedStream())) - .daemonProcess(false).build(); + .isDaemonProcess(false).build(); } - private ArtifactStoreBuilder getArtifactStore(Logger logger, + private ExtractedArtifactStore getArtifactStore(Logger logger, Stream downloadConfigBuilderCustomizers) { - DownloadConfigBuilder downloadConfigBuilder = new DownloadConfigBuilder() - .defaultsForCommand(Command.MongoD); + ImmutableDownloadConfig.Builder downloadConfigBuilder = Defaults.downloadConfigFor(Command.MongoD); downloadConfigBuilder.progressListener(new Slf4jProgressListener(logger)); downloadConfigBuilderCustomizers.forEach((customizer) -> customizer.customize(downloadConfigBuilder)); - IDownloadConfig downloadConfig = downloadConfigBuilder.build(); - return new ExtractedArtifactStoreBuilder().defaults(Command.MongoD).download(downloadConfig); + DownloadConfig downloadConfig = downloadConfigBuilder.build(); + return Defaults.extractedArtifactStoreFor(Command.MongoD).withDownloadConfig(downloadConfig); } } /** - * Post processor to ensure that {@link MongoClient} beans depend on any - * {@link MongodExecutable} beans. + * Post processor to ensure that {@link com.mongodb.client.MongoClient} beans depend + * on any {@link MongodExecutable} beans. */ - @ConditionalOnClass({ MongoClient.class, MongoClientFactoryBean.class }) + @ConditionalOnClass({ com.mongodb.client.MongoClient.class, MongoClientFactoryBean.class }) static class EmbeddedMongoClientDependsOnBeanFactoryPostProcessor extends MongoClientDependsOnBeanFactoryPostProcessor { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/MustacheAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/MustacheAutoConfiguration.java index 227b41280bf5..0d8949ae6917 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/MustacheAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/MustacheAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ package org.springframework.boot.autoconfigure.mustache; -import javax.annotation.PostConstruct; - import com.samskivert.mustache.Mustache; import com.samskivert.mustache.Mustache.Collector; import com.samskivert.mustache.Mustache.TemplateLoader; @@ -57,9 +55,9 @@ public class MustacheAutoConfiguration { public MustacheAutoConfiguration(MustacheProperties mustache, ApplicationContext applicationContext) { this.mustache = mustache; this.applicationContext = applicationContext; + checkTemplateLocationExists(); } - @PostConstruct public void checkTemplateLocationExists() { if (this.mustache.isCheckTemplateLocation()) { TemplateLocation location = new TemplateLocation(this.mustache.getPrefix()); @@ -77,6 +75,7 @@ public Mustache.Compiler mustacheCompiler(TemplateLoader mustacheTemplateLoader, return Mustache.compiler().withLoader(mustacheTemplateLoader).withCollector(collector(environment)); } + @Deprecated private Collector collector(Environment environment) { MustacheEnvironmentCollector collector = new MustacheEnvironmentCollector(); collector.setEnvironment(environment); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/MustacheEnvironmentCollector.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/MustacheEnvironmentCollector.java index 0a58f4341ded..f4d192413b41 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/MustacheEnvironmentCollector.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/MustacheEnvironmentCollector.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,13 @@ package org.springframework.boot.autoconfigure.mustache; +import java.util.concurrent.atomic.AtomicBoolean; + import com.samskivert.mustache.DefaultCollector; import com.samskivert.mustache.Mustache.Collector; import com.samskivert.mustache.Mustache.VariableFetcher; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.context.EnvironmentAware; import org.springframework.core.env.ConfigurableEnvironment; @@ -30,7 +34,10 @@ * @author Dave Syer * @author Madhura Bhave * @since 1.2.2 + * @deprecated since 2.5.0 for removal in 2.7.0 in favor of direct addition of values from + * the Environment to the model */ +@Deprecated public class MustacheEnvironmentCollector extends DefaultCollector implements EnvironmentAware { private ConfigurableEnvironment environment; @@ -56,9 +63,18 @@ public VariableFetcher createFetcher(Object ctx, String name) { private class PropertyVariableFetcher implements VariableFetcher { + private final Log log = LogFactory.getLog(PropertyVariableFetcher.class); + + private final AtomicBoolean logDeprecationWarning = new AtomicBoolean(); + @Override public Object get(Object ctx, String name) { - return MustacheEnvironmentCollector.this.environment.getProperty(name); + String property = MustacheEnvironmentCollector.this.environment.getProperty(name); + if (property != null && this.logDeprecationWarning.compareAndSet(false, true)) { + this.log.warn("Mustache variable resolution relied upon deprecated support for falling back to the " + + "Spring Environment. Please add values from the Environment directly to the model instead."); + } + return property; } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/MustacheReactiveWebConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/MustacheReactiveWebConfiguration.java index 4aa926fafa3d..729fb82b55b1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/MustacheReactiveWebConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/MustacheReactiveWebConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import com.samskivert.mustache.Mustache.Compiler; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.web.reactive.result.view.MustacheViewResolver; @@ -32,6 +33,7 @@ class MustacheReactiveWebConfiguration { @Bean @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = "spring.mustache", name = "enabled", matchIfMissing = true) MustacheViewResolver mustacheViewResolver(Compiler mustacheCompiler, MustacheProperties mustache) { MustacheViewResolver resolver = new MustacheViewResolver(mustacheCompiler); resolver.setPrefix(mustache.getPrefix()); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/MustacheResourceTemplateLoader.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/MustacheResourceTemplateLoader.java index 9c2702cccb71..3f119ce4bd09 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/MustacheResourceTemplateLoader.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/MustacheResourceTemplateLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,7 +47,7 @@ public class MustacheResourceTemplateLoader implements TemplateLoader, ResourceL private String charSet = "UTF-8"; - private ResourceLoader resourceLoader = new DefaultResourceLoader(); + private ResourceLoader resourceLoader = new DefaultResourceLoader(null); public MustacheResourceTemplateLoader() { } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/MustacheServletWebConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/MustacheServletWebConfiguration.java index 884a019ecd1f..03852a80e807 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/MustacheServletWebConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mustache/MustacheServletWebConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,9 @@ import com.samskivert.mustache.Mustache.Compiler; +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.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.web.servlet.view.MustacheViewResolver; @@ -28,10 +30,12 @@ @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) +@ConditionalOnClass(MustacheViewResolver.class) class MustacheServletWebConfiguration { @Bean @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = "spring.mustache", name = "enabled", matchIfMissing = true) MustacheViewResolver mustacheViewResolver(Compiler mustacheCompiler, MustacheProperties mustache) { MustacheViewResolver resolver = new MustacheViewResolver(mustacheCompiler); mustache.applyToMvcViewResolver(resolver); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/ConfigBuilderCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/ConfigBuilderCustomizer.java new file mode 100644 index 000000000000..10885b17f25f --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/ConfigBuilderCustomizer.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.neo4j; + +import org.neo4j.driver.Config; +import org.neo4j.driver.Config.ConfigBuilder; + +/** + * Callback interface that can be implemented by beans wishing to customize the + * {@link Config} via a {@link ConfigBuilder} whilst retaining default auto-configuration. + * + * @author Stephane Nicoll + * @since 2.4.0 + */ +@FunctionalInterface +public interface ConfigBuilderCustomizer { + + /** + * Customize the {@link ConfigBuilder}. + * @param configBuilder the {@link ConfigBuilder} to customize + */ + void customize(ConfigBuilder configBuilder); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfiguration.java new file mode 100644 index 000000000000..473d6d0c8443 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfiguration.java @@ -0,0 +1,207 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.neo4j; + +import java.io.File; +import java.net.URI; +import java.time.Duration; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.neo4j.driver.AuthToken; +import org.neo4j.driver.AuthTokens; +import org.neo4j.driver.Config; +import org.neo4j.driver.Config.TrustStrategy; +import org.neo4j.driver.Driver; +import org.neo4j.driver.GraphDatabase; +import org.neo4j.driver.internal.Scheme; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.neo4j.Neo4jProperties.Pool; +import org.springframework.boot.autoconfigure.neo4j.Neo4jProperties.Security; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.util.StringUtils; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Neo4j. + * + * @author Michael J. Simons + * @author Stephane Nicoll + * @since 2.4.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(Driver.class) +@EnableConfigurationProperties(Neo4jProperties.class) +public class Neo4jAutoConfiguration { + + private static final URI DEFAULT_SERVER_URI = URI.create("bolt://localhost:7687"); + + @Bean + @ConditionalOnMissingBean + public Driver neo4jDriver(Neo4jProperties properties, Environment environment, + ObjectProvider configBuilderCustomizers) { + AuthToken authToken = mapAuthToken(properties.getAuthentication(), environment); + Config config = mapDriverConfig(properties, + configBuilderCustomizers.orderedStream().collect(Collectors.toList())); + URI serverUri = determineServerUri(properties, environment); + return GraphDatabase.driver(serverUri, authToken, config); + } + + URI determineServerUri(Neo4jProperties properties, Environment environment) { + return getOrFallback(properties.getUri(), () -> { + URI deprecatedProperty = environment.getProperty("spring.data.neo4j.uri", URI.class); + return (deprecatedProperty != null) ? deprecatedProperty : DEFAULT_SERVER_URI; + }); + } + + AuthToken mapAuthToken(Neo4jProperties.Authentication authentication, Environment environment) { + String username = getOrFallback(authentication.getUsername(), + () -> environment.getProperty("spring.data.neo4j.username", String.class)); + String password = getOrFallback(authentication.getPassword(), + () -> environment.getProperty("spring.data.neo4j.password", String.class)); + String kerberosTicket = authentication.getKerberosTicket(); + String realm = authentication.getRealm(); + + boolean hasUsername = StringUtils.hasText(username); + boolean hasPassword = StringUtils.hasText(password); + boolean hasKerberosTicket = StringUtils.hasText(kerberosTicket); + + if (hasUsername && hasKerberosTicket) { + throw new IllegalStateException(String.format( + "Cannot specify both username ('%s') and kerberos ticket ('%s')", username, kerberosTicket)); + } + if (hasUsername && hasPassword) { + return AuthTokens.basic(username, password, realm); + } + if (hasKerberosTicket) { + return AuthTokens.kerberos(kerberosTicket); + } + return AuthTokens.none(); + } + + private T getOrFallback(T value, Supplier fallback) { + if (value != null) { + return value; + } + return fallback.get(); + } + + Config mapDriverConfig(Neo4jProperties properties, List customizers) { + Config.ConfigBuilder builder = Config.builder(); + configurePoolSettings(builder, properties.getPool()); + URI uri = properties.getUri(); + String scheme = (uri != null) ? uri.getScheme() : "bolt"; + configureDriverSettings(builder, properties, isSimpleScheme(scheme)); + builder.withLogging(new Neo4jSpringJclLogging()); + customizers.forEach((customizer) -> customizer.customize(builder)); + return builder.build(); + } + + private boolean isSimpleScheme(String scheme) { + String lowerCaseScheme = scheme.toLowerCase(Locale.ENGLISH); + try { + Scheme.validateScheme(lowerCaseScheme); + } + catch (IllegalArgumentException ex) { + throw new IllegalArgumentException(String.format("'%s' is not a supported scheme.", scheme)); + } + return lowerCaseScheme.equals("bolt") || lowerCaseScheme.equals("neo4j"); + } + + private void configurePoolSettings(Config.ConfigBuilder builder, Pool pool) { + if (pool.isLogLeakedSessions()) { + builder.withLeakedSessionsLogging(); + } + builder.withMaxConnectionPoolSize(pool.getMaxConnectionPoolSize()); + Duration idleTimeBeforeConnectionTest = pool.getIdleTimeBeforeConnectionTest(); + if (idleTimeBeforeConnectionTest != null) { + builder.withConnectionLivenessCheckTimeout(idleTimeBeforeConnectionTest.toMillis(), TimeUnit.MILLISECONDS); + } + builder.withMaxConnectionLifetime(pool.getMaxConnectionLifetime().toMillis(), TimeUnit.MILLISECONDS); + builder.withConnectionAcquisitionTimeout(pool.getConnectionAcquisitionTimeout().toMillis(), + TimeUnit.MILLISECONDS); + if (pool.isMetricsEnabled()) { + builder.withDriverMetrics(); + } + else { + builder.withoutDriverMetrics(); + } + } + + private void configureDriverSettings(Config.ConfigBuilder builder, Neo4jProperties properties, + boolean withEncryptionAndTrustSettings) { + if (withEncryptionAndTrustSettings) { + applyEncryptionAndTrustSettings(builder, properties.getSecurity()); + } + builder.withConnectionTimeout(properties.getConnectionTimeout().toMillis(), TimeUnit.MILLISECONDS); + builder.withMaxTransactionRetryTime(properties.getMaxTransactionRetryTime().toMillis(), TimeUnit.MILLISECONDS); + } + + private void applyEncryptionAndTrustSettings(Config.ConfigBuilder builder, + Neo4jProperties.Security securityProperties) { + if (securityProperties.isEncrypted()) { + builder.withEncryption(); + } + else { + builder.withoutEncryption(); + } + builder.withTrustStrategy(mapTrustStrategy(securityProperties)); + } + + private Config.TrustStrategy mapTrustStrategy(Neo4jProperties.Security securityProperties) { + String propertyName = "spring.neo4j.security.trust-strategy"; + Security.TrustStrategy strategy = securityProperties.getTrustStrategy(); + TrustStrategy trustStrategy = createTrustStrategy(securityProperties, propertyName, strategy); + if (securityProperties.isHostnameVerificationEnabled()) { + trustStrategy.withHostnameVerification(); + } + else { + trustStrategy.withoutHostnameVerification(); + } + return trustStrategy; + } + + private TrustStrategy createTrustStrategy(Neo4jProperties.Security securityProperties, String propertyName, + Security.TrustStrategy strategy) { + switch (strategy) { + case TRUST_ALL_CERTIFICATES: + return TrustStrategy.trustAllCertificates(); + case TRUST_SYSTEM_CA_SIGNED_CERTIFICATES: + return TrustStrategy.trustSystemCertificates(); + case TRUST_CUSTOM_CA_SIGNED_CERTIFICATES: + File certFile = securityProperties.getCertFile(); + if (certFile == null || !certFile.isFile()) { + throw new InvalidConfigurationPropertyValueException(propertyName, strategy.name(), + "Configured trust strategy requires a certificate file."); + } + return TrustStrategy.trustCustomCertificateSignedBy(certFile); + default: + throw new InvalidConfigurationPropertyValueException(propertyName, strategy.name(), "Unknown strategy."); + } + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jProperties.java new file mode 100644 index 000000000000..5b856a52e975 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jProperties.java @@ -0,0 +1,309 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.neo4j; + +import java.io.File; +import java.net.URI; +import java.time.Duration; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties for Neo4j. + * + * @author Michael J. Simons + * @author Stephane Nicoll + * @since 2.4.0 + */ +@ConfigurationProperties(prefix = "spring.neo4j") +public class Neo4jProperties { + + /** + * URI used by the driver. + */ + private URI uri; + + /** + * Timeout for borrowing connections from the pool. + */ + private Duration connectionTimeout = Duration.ofSeconds(30); + + /** + * Maximum time transactions are allowed to retry. + */ + private Duration maxTransactionRetryTime = Duration.ofSeconds(30); + + private final Authentication authentication = new Authentication(); + + private final Pool pool = new Pool(); + + private final Security security = new Security(); + + public URI getUri() { + return this.uri; + } + + public void setUri(URI uri) { + this.uri = uri; + } + + public Duration getConnectionTimeout() { + return this.connectionTimeout; + } + + public void setConnectionTimeout(Duration connectionTimeout) { + this.connectionTimeout = connectionTimeout; + } + + public Duration getMaxTransactionRetryTime() { + return this.maxTransactionRetryTime; + } + + public void setMaxTransactionRetryTime(Duration maxTransactionRetryTime) { + this.maxTransactionRetryTime = maxTransactionRetryTime; + } + + public Authentication getAuthentication() { + return this.authentication; + } + + public Pool getPool() { + return this.pool; + } + + public Security getSecurity() { + return this.security; + } + + public static class Authentication { + + /** + * Login user of the server. + */ + private String username; + + /** + * Login password of the server. + */ + private String password; + + /** + * Realm to connect to. + */ + private String realm; + + /** + * Kerberos ticket for connecting to the database. Mutual exclusive with a given + * username. + */ + private String kerberosTicket; + + public String getUsername() { + return this.username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return this.password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getRealm() { + return this.realm; + } + + public void setRealm(String realm) { + this.realm = realm; + } + + public String getKerberosTicket() { + return this.kerberosTicket; + } + + public void setKerberosTicket(String kerberosTicket) { + this.kerberosTicket = kerberosTicket; + } + + } + + public static class Pool { + + /** + * Whether to enable metrics. + */ + private boolean metricsEnabled = false; + + /** + * Whether to log leaked sessions. + */ + private boolean logLeakedSessions = false; + + /** + * Maximum amount of connections in the connection pool towards a single database. + */ + private int maxConnectionPoolSize = 100; + + /** + * Pooled connections that have been idle in the pool for longer than this + * threshold will be tested before they are used again. + */ + private Duration idleTimeBeforeConnectionTest; + + /** + * Pooled connections older than this threshold will be closed and removed from + * the pool. + */ + private Duration maxConnectionLifetime = Duration.ofHours(1); + + /** + * Acquisition of new connections will be attempted for at most configured + * timeout. + */ + private Duration connectionAcquisitionTimeout = Duration.ofSeconds(60); + + public boolean isLogLeakedSessions() { + return this.logLeakedSessions; + } + + public void setLogLeakedSessions(boolean logLeakedSessions) { + this.logLeakedSessions = logLeakedSessions; + } + + public int getMaxConnectionPoolSize() { + return this.maxConnectionPoolSize; + } + + public void setMaxConnectionPoolSize(int maxConnectionPoolSize) { + this.maxConnectionPoolSize = maxConnectionPoolSize; + } + + public Duration getIdleTimeBeforeConnectionTest() { + return this.idleTimeBeforeConnectionTest; + } + + public void setIdleTimeBeforeConnectionTest(Duration idleTimeBeforeConnectionTest) { + this.idleTimeBeforeConnectionTest = idleTimeBeforeConnectionTest; + } + + public Duration getMaxConnectionLifetime() { + return this.maxConnectionLifetime; + } + + public void setMaxConnectionLifetime(Duration maxConnectionLifetime) { + this.maxConnectionLifetime = maxConnectionLifetime; + } + + public Duration getConnectionAcquisitionTimeout() { + return this.connectionAcquisitionTimeout; + } + + public void setConnectionAcquisitionTimeout(Duration connectionAcquisitionTimeout) { + this.connectionAcquisitionTimeout = connectionAcquisitionTimeout; + } + + public boolean isMetricsEnabled() { + return this.metricsEnabled; + } + + public void setMetricsEnabled(boolean metricsEnabled) { + this.metricsEnabled = metricsEnabled; + } + + } + + public static class Security { + + /** + * Whether the driver should use encrypted traffic. + */ + private boolean encrypted = false; + + /** + * Trust strategy to use. + */ + private TrustStrategy trustStrategy = TrustStrategy.TRUST_SYSTEM_CA_SIGNED_CERTIFICATES; + + /** + * Path to the file that holds the trusted certificates. + */ + private File certFile; + + /** + * Whether hostname verification is required. + */ + private boolean hostnameVerificationEnabled = true; + + public boolean isEncrypted() { + return this.encrypted; + } + + public void setEncrypted(boolean encrypted) { + this.encrypted = encrypted; + } + + public TrustStrategy getTrustStrategy() { + return this.trustStrategy; + } + + public void setTrustStrategy(TrustStrategy trustStrategy) { + this.trustStrategy = trustStrategy; + } + + public File getCertFile() { + return this.certFile; + } + + public void setCertFile(File certFile) { + this.certFile = certFile; + } + + public boolean isHostnameVerificationEnabled() { + return this.hostnameVerificationEnabled; + } + + public void setHostnameVerificationEnabled(boolean hostnameVerificationEnabled) { + this.hostnameVerificationEnabled = hostnameVerificationEnabled; + } + + public enum TrustStrategy { + + /** + * Trust all certificates. + */ + TRUST_ALL_CERTIFICATES, + + /** + * Trust certificates that are signed by a trusted certificate. + */ + TRUST_CUSTOM_CA_SIGNED_CERTIFICATES, + + /** + * Trust certificates that can be verified through the local system store. + */ + TRUST_SYSTEM_CA_SIGNED_CERTIFICATES + + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jSpringJclLogging.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jSpringJclLogging.java new file mode 100644 index 000000000000..c1a53370745d --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jSpringJclLogging.java @@ -0,0 +1,109 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.neo4j; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.neo4j.driver.Logger; +import org.neo4j.driver.Logging; + +/** + * Shim to use Spring JCL implementation, delegating all the hard work of deciding the + * underlying system to Spring and Spring Boot. + * + * @author Michael J. Simons + */ +class Neo4jSpringJclLogging implements Logging { + + /** + * This prefix gets added to the log names the driver requests to add some namespace + * around it in a bigger application scenario. + */ + private static final String AUTOMATIC_PREFIX = "org.neo4j.driver."; + + @Override + public Logger getLog(String name) { + String requestedLog = name; + if (!requestedLog.startsWith(AUTOMATIC_PREFIX)) { + requestedLog = AUTOMATIC_PREFIX + name; + } + Log springJclLog = LogFactory.getLog(requestedLog); + return new SpringJclLogger(springJclLog); + } + + private static final class SpringJclLogger implements Logger { + + private final Log delegate; + + SpringJclLogger(Log delegate) { + this.delegate = delegate; + } + + @Override + public void error(String message, Throwable cause) { + this.delegate.error(message, cause); + } + + @Override + public void info(String format, Object... params) { + this.delegate.info(String.format(format, params)); + } + + @Override + public void warn(String format, Object... params) { + this.delegate.warn(String.format(format, params)); + } + + @Override + public void warn(String message, Throwable cause) { + this.delegate.warn(message, cause); + } + + @Override + public void debug(String format, Object... params) { + if (isDebugEnabled()) { + this.delegate.debug(String.format(format, params)); + } + } + + @Override + public void debug(String message, Throwable throwable) { + if (isDebugEnabled()) { + this.delegate.debug(message, throwable); + } + } + + @Override + public void trace(String format, Object... params) { + if (isTraceEnabled()) { + this.delegate.trace(String.format(format, params)); + } + } + + @Override + public boolean isTraceEnabled() { + return this.delegate.isTraceEnabled(); + } + + @Override + public boolean isDebugEnabled() { + return this.delegate.isDebugEnabled(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/package-info.java new file mode 100644 index 000000000000..d47e98efc150 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for Neo4j. + */ +package org.springframework.boot.autoconfigure.neo4j; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/netty/NettyAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/netty/NettyAutoConfiguration.java new file mode 100644 index 000000000000..c4c82989025b --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/netty/NettyAutoConfiguration.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.netty; + +import io.netty.util.NettyRuntime; +import io.netty.util.ResourceLeakDetector; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Netty. + * + * @author Brian Clozel + * @since 2.5.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(NettyRuntime.class) +@EnableConfigurationProperties(NettyProperties.class) +public class NettyAutoConfiguration { + + public NettyAutoConfiguration(NettyProperties properties) { + if (properties.getLeakDetection() != null) { + NettyProperties.LeakDetection leakDetection = properties.getLeakDetection(); + ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.valueOf(leakDetection.name())); + } + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/netty/NettyProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/netty/NettyProperties.java new file mode 100644 index 000000000000..1f381468a303 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/netty/NettyProperties.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.netty; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * {@link ConfigurationProperties @ConfigurationProperties} for the Netty engine. + *

    + * These properties apply globally to the Netty library, used as a client or a server. + * + * @author Brian Clozel + * @since 2.5.0 + */ +@ConfigurationProperties(prefix = "spring.netty") +public class NettyProperties { + + /** + * Level of leak detection for reference-counted buffers. + */ + private LeakDetection leakDetection = LeakDetection.SIMPLE; + + public LeakDetection getLeakDetection() { + return this.leakDetection; + } + + public void setLeakDetection(LeakDetection leakDetection) { + this.leakDetection = leakDetection; + } + + public enum LeakDetection { + + /** + * Disable leak detection completely. + */ + DISABLED, + + /** + * Detect leaks for 1% of buffers. + */ + SIMPLE, + + /** + * Detect leaks for 1% of buffers and track where they were accessed. + */ + ADVANCED, + + /** + * Detect leaks for 100% of buffers and track where they were accessed. + */ + PARANOID + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/netty/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/netty/package-info.java new file mode 100644 index 000000000000..0784c137ecef --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/netty/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for the Netty library. + */ +package org.springframework.boot.autoconfigure.netty; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/DataSourceInitializedPublisher.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/DataSourceInitializedPublisher.java deleted file mode 100644 index 9896a82af337..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/DataSourceInitializedPublisher.java +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.orm.jpa; - -import java.util.Map; -import java.util.function.Supplier; - -import javax.persistence.EntityManager; -import javax.persistence.EntityManagerFactory; -import javax.persistence.spi.PersistenceProvider; -import javax.persistence.spi.PersistenceUnitInfo; -import javax.sql.DataSource; - -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.beans.factory.support.GenericBeanDefinition; -import org.springframework.boot.autoconfigure.jdbc.DataSourceSchemaCreatedEvent; -import org.springframework.boot.jdbc.EmbeddedDatabaseConnection; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; -import org.springframework.core.task.AsyncTaskExecutor; -import org.springframework.core.type.AnnotationMetadata; -import org.springframework.orm.jpa.JpaDialect; -import org.springframework.orm.jpa.JpaVendorAdapter; -import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; - -/** - * {@link BeanPostProcessor} used to fire {@link DataSourceSchemaCreatedEvent}s. Should - * only be registered via the inner {@link Registrar} class. - * - * @author Dave Syer - */ -class DataSourceInitializedPublisher implements BeanPostProcessor { - - @Autowired - private ApplicationContext applicationContext; - - private DataSource dataSource; - - private JpaProperties jpaProperties; - - private HibernateProperties hibernateProperties; - - private DataSourceSchemaCreatedPublisher schemaCreatedPublisher; - - @Override - public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { - if (bean instanceof LocalContainerEntityManagerFactoryBean) { - LocalContainerEntityManagerFactoryBean factory = (LocalContainerEntityManagerFactoryBean) bean; - if (factory.getBootstrapExecutor() != null && factory.getJpaVendorAdapter() != null) { - this.schemaCreatedPublisher = new DataSourceSchemaCreatedPublisher(factory); - factory.setJpaVendorAdapter(this.schemaCreatedPublisher); - } - } - return bean; - } - - @Override - public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { - if (bean instanceof DataSource) { - // Normally this will be the right DataSource - this.dataSource = (DataSource) bean; - } - if (bean instanceof JpaProperties) { - this.jpaProperties = (JpaProperties) bean; - } - if (bean instanceof HibernateProperties) { - this.hibernateProperties = (HibernateProperties) bean; - } - if (bean instanceof LocalContainerEntityManagerFactoryBean && this.schemaCreatedPublisher == null) { - LocalContainerEntityManagerFactoryBean factoryBean = (LocalContainerEntityManagerFactoryBean) bean; - EntityManagerFactory entityManagerFactory = factoryBean.getNativeEntityManagerFactory(); - publishEventIfRequired(factoryBean, entityManagerFactory); - } - return bean; - } - - private void publishEventIfRequired(LocalContainerEntityManagerFactoryBean factoryBean, - EntityManagerFactory entityManagerFactory) { - DataSource dataSource = findDataSource(factoryBean, entityManagerFactory); - if (dataSource != null && isInitializingDatabase(dataSource)) { - this.applicationContext.publishEvent(new DataSourceSchemaCreatedEvent(dataSource)); - } - } - - private DataSource findDataSource(LocalContainerEntityManagerFactoryBean factoryBean, - EntityManagerFactory entityManagerFactory) { - Object dataSource = entityManagerFactory.getProperties().get("javax.persistence.nonJtaDataSource"); - if (dataSource == null) { - dataSource = factoryBean.getPersistenceUnitInfo().getNonJtaDataSource(); - } - return (dataSource instanceof DataSource) ? (DataSource) dataSource : this.dataSource; - } - - private boolean isInitializingDatabase(DataSource dataSource) { - if (this.jpaProperties == null || this.hibernateProperties == null) { - return true; // better safe than sorry - } - Supplier defaultDdlAuto = () -> (EmbeddedDatabaseConnection.isEmbedded(dataSource) ? "create-drop" - : "none"); - Map hibernate = this.hibernateProperties.determineHibernateProperties( - this.jpaProperties.getProperties(), new HibernateSettings().ddlAuto(defaultDdlAuto)); - return hibernate.containsKey("hibernate.hbm2ddl.auto"); - } - - /** - * {@link ImportBeanDefinitionRegistrar} to register the - * {@link DataSourceInitializedPublisher} without causing early bean instantiation - * issues. - */ - static class Registrar implements ImportBeanDefinitionRegistrar { - - private static final String BEAN_NAME = "dataSourceInitializedPublisher"; - - @Override - public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, - BeanDefinitionRegistry registry) { - if (!registry.containsBeanDefinition(BEAN_NAME)) { - GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); - beanDefinition.setBeanClass(DataSourceInitializedPublisher.class); - beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - // We don't need this one to be post processed otherwise it can cause a - // cascade of bean instantiation that we would rather avoid. - beanDefinition.setSynthetic(true); - registry.registerBeanDefinition(BEAN_NAME, beanDefinition); - } - } - - } - - final class DataSourceSchemaCreatedPublisher implements JpaVendorAdapter { - - private final LocalContainerEntityManagerFactoryBean factoryBean; - - private final JpaVendorAdapter delegate; - - private DataSourceSchemaCreatedPublisher(LocalContainerEntityManagerFactoryBean factoryBean) { - this.factoryBean = factoryBean; - this.delegate = factoryBean.getJpaVendorAdapter(); - } - - @Override - public PersistenceProvider getPersistenceProvider() { - return this.delegate.getPersistenceProvider(); - } - - @Override - public String getPersistenceProviderRootPackage() { - return this.delegate.getPersistenceProviderRootPackage(); - } - - @Override - public Map getJpaPropertyMap(PersistenceUnitInfo pui) { - return this.delegate.getJpaPropertyMap(pui); - } - - @Override - public Map getJpaPropertyMap() { - return this.delegate.getJpaPropertyMap(); - } - - @Override - public JpaDialect getJpaDialect() { - return this.delegate.getJpaDialect(); - } - - @Override - public Class getEntityManagerFactoryInterface() { - return this.delegate.getEntityManagerFactoryInterface(); - } - - @Override - public Class getEntityManagerInterface() { - return this.delegate.getEntityManagerInterface(); - } - - @Override - public void postProcessEntityManagerFactory(EntityManagerFactory entityManagerFactory) { - this.delegate.postProcessEntityManagerFactory(entityManagerFactory); - AsyncTaskExecutor bootstrapExecutor = this.factoryBean.getBootstrapExecutor(); - if (bootstrapExecutor != null) { - bootstrapExecutor.execute(() -> DataSourceInitializedPublisher.this - .publishEventIfRequired(this.factoryBean, entityManagerFactory)); - } - } - - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/DatabaseLookup.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/DatabaseLookup.java deleted file mode 100644 index 6cc0743eeaac..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/DatabaseLookup.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.orm.jpa; - -import java.util.Collections; -import java.util.EnumMap; -import java.util.Map; - -import javax.sql.DataSource; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.boot.jdbc.DatabaseDriver; -import org.springframework.jdbc.support.JdbcUtils; -import org.springframework.jdbc.support.MetaDataAccessException; -import org.springframework.orm.jpa.vendor.Database; - -/** - * Utility to lookup well known {@link Database Databases} from a {@link DataSource}. - * - * @author Eddú Meléndez - * @author Phillip Webb - */ -final class DatabaseLookup { - - private static final Log logger = LogFactory.getLog(DatabaseLookup.class); - - private static final Map LOOKUP; - - static { - Map map = new EnumMap<>(DatabaseDriver.class); - map.put(DatabaseDriver.DERBY, Database.DERBY); - map.put(DatabaseDriver.H2, Database.H2); - map.put(DatabaseDriver.HSQLDB, Database.HSQL); - map.put(DatabaseDriver.MYSQL, Database.MYSQL); - map.put(DatabaseDriver.ORACLE, Database.ORACLE); - map.put(DatabaseDriver.POSTGRESQL, Database.POSTGRESQL); - map.put(DatabaseDriver.SQLSERVER, Database.SQL_SERVER); - map.put(DatabaseDriver.DB2, Database.DB2); - map.put(DatabaseDriver.INFORMIX, Database.INFORMIX); - map.put(DatabaseDriver.HANA, Database.HANA); - LOOKUP = Collections.unmodifiableMap(map); - } - - private DatabaseLookup() { - } - - /** - * Return the most suitable {@link Database} for the given {@link DataSource}. - * @param dataSource the source {@link DataSource} - * @return the most suitable {@link Database} - */ - static Database getDatabase(DataSource dataSource) { - if (dataSource == null) { - return Database.DEFAULT; - } - try { - String url = JdbcUtils.extractDatabaseMetaData(dataSource, "getURL"); - DatabaseDriver driver = DatabaseDriver.fromJdbcUrl(url); - Database database = LOOKUP.get(driver); - if (database != null) { - return database; - } - } - catch (MetaDataAccessException ex) { - logger.warn("Unable to determine jdbc url from datasource", ex); - } - return Database.DEFAULT; - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/EntityManagerFactoryDependsOnPostProcessor.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/EntityManagerFactoryDependsOnPostProcessor.java new file mode 100644 index 000000000000..c09faf7649b1 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/EntityManagerFactoryDependsOnPostProcessor.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.orm.jpa; + +import javax.persistence.EntityManagerFactory; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.boot.autoconfigure.AbstractDependsOnBeanFactoryPostProcessor; +import org.springframework.orm.jpa.AbstractEntityManagerFactoryBean; + +/** + * {@link BeanFactoryPostProcessor} that can be used to dynamically declare that all + * {@link EntityManagerFactory} beans should "depend on" one or more specific beans. + * + * @author Marcel Overdijk + * @author Dave Syer + * @author Phillip Webb + * @author Andy Wilkinson + * @author Andrii Hrytsiuk + * @since 2.5.0 + * @see BeanDefinition#setDependsOn(String[]) + */ +public class EntityManagerFactoryDependsOnPostProcessor extends AbstractDependsOnBeanFactoryPostProcessor { + + /** + * Creates a new {@code EntityManagerFactoryDependsOnPostProcessor} that will set up + * dependencies upon beans with the given names. + * @param dependsOn names of the beans to depend upon + */ + public EntityManagerFactoryDependsOnPostProcessor(String... dependsOn) { + super(EntityManagerFactory.class, AbstractEntityManagerFactoryBean.class, dependsOn); + } + + /** + * Creates a new {@code EntityManagerFactoryDependsOnPostProcessor} that will set up + * dependencies upon beans with the given types. + * @param dependsOn types of the beans to depend upon + */ + public EntityManagerFactoryDependsOnPostProcessor(Class... dependsOn) { + super(EntityManagerFactory.class, AbstractEntityManagerFactoryBean.class, dependsOn); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.java index ab4f1c6bff5f..d6532819e1b8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.java index 0e9d9938318a..01a8fa29fdf5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -183,8 +183,7 @@ private void configureSpringJtaPlatform(Map vendorProperties, // containers (e.g. JBoss EAP 6) wrap it in the superclass LinkageError if (!isUsingJndi()) { throw new IllegalStateException( - "Unable to set Hibernate JTA platform, are you using the correct " + "version of Hibernate?", - ex); + "Unable to set Hibernate JTA platform, are you using the correct version of Hibernate?", ex); } // Assume that Hibernate will use JNDI if (logger.isDebugEnabled()) { @@ -205,7 +204,7 @@ private boolean isUsingJndi() { private Object getNoJtaPlatformManager() { for (String candidate : NO_JTA_PLATFORM_CLASSES) { try { - return Class.forName(candidate).newInstance(); + return Class.forName(candidate).getDeclaredConstructor().newInstance(); } catch (Exception ex) { // Continue searching diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateProperties.java index 208c9baa492d..d967e2a18159 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,6 +35,7 @@ * Configuration properties for Hibernate. * * @author Stephane Nicoll + * @author Chris Bono * @since 2.1.0 * @see JpaProperties */ @@ -133,7 +134,13 @@ private String determineDdlAuto(Map existing, Supplier d if (ddlAuto != null) { return ddlAuto; } - return (this.ddlAuto != null) ? this.ddlAuto : defaultDdlAuto.get(); + if (this.ddlAuto != null) { + return this.ddlAuto; + } + if (existing.get(AvailableSettings.HBM2DDL_DATABASE_ACTION) != null) { + return null; + } + return defaultDdlAuto.get(); } public static class Naming { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration.java index 4f9400ea31e2..8526773988cf 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,7 +44,6 @@ import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Primary; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.JpaVendorAdapter; @@ -54,6 +53,7 @@ import org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor; import org.springframework.orm.jpa.vendor.AbstractJpaVendorAdapter; import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionManager; import org.springframework.transaction.jta.JtaTransactionManager; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -73,7 +73,6 @@ */ @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(JpaProperties.class) -@Import(DataSourceInitializedPublisher.Registrar.class) public abstract class JpaBaseConfiguration implements BeanFactoryAware { private final DataSource dataSource; @@ -92,7 +91,7 @@ protected JpaBaseConfiguration(DataSource dataSource, JpaProperties properties, } @Bean - @ConditionalOnMissingBean + @ConditionalOnMissingBean(TransactionManager.class) public PlatformTransactionManager transactionManager( ObjectProvider transactionManagerCustomizers) { JpaTransactionManager transactionManager = new JpaTransactionManager(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaProperties.java index 909496791f11..9a208af28e7e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,8 +21,6 @@ import java.util.List; import java.util.Map; -import javax.sql.DataSource; - import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.orm.jpa.vendor.Database; @@ -129,20 +127,4 @@ public void setOpenInView(Boolean openInView) { this.openInView = openInView; } - /** - * Determine the {@link Database} to use based on this configuration and the primary - * {@link DataSource}. - * @param dataSource the auto-configured data source - * @return {@code Database} - * @deprecated since 2.2.0 in favor of letting the JPA container detect the database - * to use. - */ - @Deprecated - public Database determineDatabase(DataSource dataSource) { - if (this.database != null) { - return this.database; - } - return DatabaseLookup.getDatabase(dataSource); - } - } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzAutoConfiguration.java index 64d437534bfa..96ab8804711f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,30 +21,28 @@ import javax.sql.DataSource; -import liquibase.integration.spring.SpringLiquibase; import org.quartz.Calendar; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.Trigger; import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.AbstractDependsOnBeanFactoryPostProcessor; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -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.ConditionalOnSingleCandidate; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration; -import org.springframework.boot.autoconfigure.flyway.FlywayMigrationInitializer; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.sql.init.dependency.DatabaseInitializationDependencyConfigurer; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import org.springframework.core.annotation.Order; import org.springframework.core.io.ResourceLoader; import org.springframework.scheduling.quartz.SchedulerFactoryBean; @@ -100,17 +98,20 @@ private Properties asProperties(Map source) { @Configuration(proxyBeanMethods = false) @ConditionalOnSingleCandidate(DataSource.class) @ConditionalOnProperty(prefix = "spring.quartz", name = "job-store-type", havingValue = "jdbc") + @Import(DatabaseInitializationDependencyConfigurer.class) protected static class JdbcStoreTypeConfiguration { @Bean @Order(0) public SchedulerFactoryBeanCustomizer dataSourceCustomizer(QuartzProperties properties, DataSource dataSource, @QuartzDataSource ObjectProvider quartzDataSource, - ObjectProvider transactionManager) { + ObjectProvider transactionManager, + @QuartzTransactionManager ObjectProvider quartzTransactionManager) { return (schedulerFactoryBean) -> { DataSource dataSourceToUse = getDataSource(dataSource, quartzDataSource); schedulerFactoryBean.setDataSource(dataSourceToUse); - PlatformTransactionManager txManager = transactionManager.getIfUnique(); + PlatformTransactionManager txManager = getTransactionManager(transactionManager, + quartzTransactionManager); if (txManager != null) { schedulerFactoryBean.setTransactionManager(txManager); } @@ -122,6 +123,14 @@ private DataSource getDataSource(DataSource dataSource, ObjectProvider transactionManager, + ObjectProvider quartzTransactionManager) { + PlatformTransactionManager transactionManagerIfAvailable = quartzTransactionManager.getIfAvailable(); + return (transactionManagerIfAvailable != null) ? transactionManagerIfAvailable + : transactionManager.getIfUnique(); + } + @Bean @ConditionalOnMissingBean public QuartzDataSourceInitializer quartzDataSourceInitializer(DataSource dataSource, @@ -131,51 +140,6 @@ public QuartzDataSourceInitializer quartzDataSourceInitializer(DataSource dataSo return new QuartzDataSourceInitializer(dataSourceToUse, resourceLoader, properties); } - /** - * Additional configuration to ensure that {@link SchedulerFactoryBean} and - * {@link Scheduler} beans depend on any beans that perform data source - * initialization. - */ - @Configuration(proxyBeanMethods = false) - static class QuartzSchedulerDependencyConfiguration { - - @Bean - static SchedulerDependsOnBeanFactoryPostProcessor quartzSchedulerDataSourceInitializerDependsOnBeanFactoryPostProcessor() { - return new SchedulerDependsOnBeanFactoryPostProcessor(QuartzDataSourceInitializer.class); - } - - @Bean - @ConditionalOnBean(FlywayMigrationInitializer.class) - static SchedulerDependsOnBeanFactoryPostProcessor quartzSchedulerFlywayDependsOnBeanFactoryPostProcessor() { - return new SchedulerDependsOnBeanFactoryPostProcessor(FlywayMigrationInitializer.class); - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(SpringLiquibase.class) - static class LiquibaseQuartzSchedulerDependencyConfiguration { - - @Bean - @ConditionalOnBean(SpringLiquibase.class) - static SchedulerDependsOnBeanFactoryPostProcessor quartzSchedulerLiquibaseDependsOnBeanFactoryPostProcessor() { - return new SchedulerDependsOnBeanFactoryPostProcessor(SpringLiquibase.class); - } - - } - - } - - } - - /** - * {@link AbstractDependsOnBeanFactoryPostProcessor} for Quartz {@link Scheduler} and - * {@link SchedulerFactoryBean}. - */ - private static class SchedulerDependsOnBeanFactoryPostProcessor extends AbstractDependsOnBeanFactoryPostProcessor { - - SchedulerDependsOnBeanFactoryPostProcessor(Class... dependencyTypes) { - super(Scheduler.class, SchedulerFactoryBean.class, dependencyTypes); - } - } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzDataSource.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzDataSource.java index e703bd49e4d4..615b48e1a0b7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzDataSource.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzDataSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ * {@code @Primary}. * * @author Madhura Bhave + * @see QuartzDataSource * @since 2.0.2 */ @Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE }) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzDataSourceInitializer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzDataSourceInitializer.java index 5f94f84d622e..94757233780b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzDataSourceInitializer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzDataSourceInitializer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import org.springframework.core.io.ResourceLoader; import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * Initialize the Quartz Scheduler schema. @@ -58,11 +59,15 @@ protected String getSchemaLocation() { @Override protected String getDatabaseName() { + String platform = this.properties.getJdbc().getPlatform(); + if (StringUtils.hasText(platform)) { + return platform; + } String databaseName = super.getDatabaseName(); if ("db2".equals(databaseName)) { return "db2_v95"; } - if ("mysql".equals(databaseName)) { + if ("mysql".equals(databaseName) || "mariadb".equals(databaseName)) { return "mysql_innodb"; } if ("postgresql".equals(databaseName)) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzProperties.java index 3509ddbb3fe3..7c0337e33c6f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -141,6 +141,12 @@ public static class Jdbc { */ private String schema = DEFAULT_SCHEMA_LOCATION; + /** + * Platform to use in initialization scripts if the @@platform@@ placeholder is + * used. Auto-detected by default. + */ + private String platform; + /** * Database schema initialization mode. */ @@ -159,6 +165,14 @@ public void setSchema(String schema) { this.schema = schema; } + public String getPlatform() { + return this.platform; + } + + public void setPlatform(String platform) { + this.platform = platform; + } + public DataSourceInitializationMode getInitializeSchema() { return this.initializeSchema; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzTransactionManager.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzTransactionManager.java new file mode 100644 index 000000000000..9e56a89dcc2c --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/QuartzTransactionManager.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.quartz; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.beans.factory.annotation.Qualifier; + +/** + * Qualifier annotation for a TransactionManager to be injected into Quartz + * auto-configuration. Can be used on a secondary transaction manager, if there is another + * one marked as {@code @Primary}. + * + * @author Andy Wilkinson + * @see QuartzDataSource + * @since 2.2.11 + */ +@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Qualifier +public @interface QuartzTransactionManager { + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/SchedulerDependsOnDatabaseInitializationDetector.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/SchedulerDependsOnDatabaseInitializationDetector.java new file mode 100644 index 000000000000..9bdc02cc18d3 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/quartz/SchedulerDependsOnDatabaseInitializationDetector.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.quartz; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import org.quartz.Scheduler; + +import org.springframework.boot.sql.init.dependency.AbstractBeansOfTypeDependsOnDatabaseInitializationDetector; +import org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitializationDetector; +import org.springframework.scheduling.quartz.SchedulerFactoryBean; + +/** + * A {@link DependsOnDatabaseInitializationDetector} for Quartz {@link Scheduler} and + * {@link SchedulerFactoryBean}. + * + * @author Andy Wilkinson + */ +class SchedulerDependsOnDatabaseInitializationDetector + extends AbstractBeansOfTypeDependsOnDatabaseInitializationDetector { + + @Override + protected Set> getDependsOnDatabaseInitializationBeanTypes() { + return new HashSet<>(Arrays.asList(Scheduler.class, SchedulerFactoryBean.class)); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBeanCreationFailureAnalyzer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBeanCreationFailureAnalyzer.java new file mode 100644 index 000000000000..264aa3f3154b --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBeanCreationFailureAnalyzer.java @@ -0,0 +1,94 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.r2dbc; + +import org.springframework.boot.autoconfigure.r2dbc.ConnectionFactoryOptionsInitializer.ConnectionFactoryBeanCreationException; +import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; +import org.springframework.boot.diagnostics.FailureAnalysis; +import org.springframework.boot.r2dbc.EmbeddedDatabaseConnection; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.env.Environment; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * An {@link AbstractFailureAnalyzer} for failures caused by a + * {@link ConnectionFactoryBeanCreationException}. + * + * @author Mark Paluch + */ +class ConnectionFactoryBeanCreationFailureAnalyzer + extends AbstractFailureAnalyzer implements EnvironmentAware { + + private Environment environment; + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + + @Override + protected FailureAnalysis analyze(Throwable rootFailure, ConnectionFactoryBeanCreationException cause) { + return getFailureAnalysis(cause); + } + + private FailureAnalysis getFailureAnalysis(ConnectionFactoryBeanCreationException cause) { + String description = getDescription(cause); + String action = getAction(cause); + return new FailureAnalysis(description, action, cause); + } + + private String getDescription(ConnectionFactoryBeanCreationException cause) { + StringBuilder description = new StringBuilder(); + description.append("Failed to configure a ConnectionFactory: "); + if (!StringUtils.hasText(cause.getProperties().getUrl())) { + description.append("'url' attribute is not specified and "); + } + description.append(String.format("no embedded database could be configured.%n")); + description.append(String.format("%nReason: %s%n", cause.getMessage())); + return description.toString(); + } + + private String getAction(ConnectionFactoryBeanCreationException cause) { + StringBuilder action = new StringBuilder(); + action.append(String.format("Consider the following:%n")); + if (EmbeddedDatabaseConnection.NONE == cause.getEmbeddedDatabaseConnection()) { + action.append(String.format("\tIf you want an embedded database (H2), please put it on the classpath.%n")); + } + else { + action.append(String.format("\tReview the configuration of %s%n.", cause.getEmbeddedDatabaseConnection())); + } + action.append("\tIf you have database settings to be loaded from a particular " + + "profile you may need to activate it").append(getActiveProfiles()); + return action.toString(); + } + + private String getActiveProfiles() { + StringBuilder message = new StringBuilder(); + String[] profiles = this.environment.getActiveProfiles(); + if (ObjectUtils.isEmpty(profiles)) { + message.append(" (no profiles are currently active)."); + } + else { + message.append(" (the profiles "); + message.append(StringUtils.arrayToCommaDelimitedString(profiles)); + message.append(" are currently active)."); + } + return message.toString(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBuilder.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBuilder.java new file mode 100644 index 000000000000..20497b6c6720 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBuilder.java @@ -0,0 +1,143 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.r2dbc; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +import io.r2dbc.spi.ConnectionFactories; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.ConnectionFactoryOptions; +import io.r2dbc.spi.ConnectionFactoryOptions.Builder; + +/** + * Builder for {@link ConnectionFactory}. + * + * @author Mark Paluch + * @author Tadaya Tsuyukubo + * @author Stephane Nicoll + * @since 2.3.0 + * @deprecated since 2.5.0 for removal in 2.7.0 in favor of + * {@link org.springframework.boot.r2dbc.ConnectionFactoryBuilder} + */ +@Deprecated +public final class ConnectionFactoryBuilder { + + private final ConnectionFactoryOptions.Builder optionsBuilder; + + private ConnectionFactoryBuilder(ConnectionFactoryOptions.Builder optionsBuilder) { + this.optionsBuilder = optionsBuilder; + } + + /** + * Initialize a new {@link ConnectionFactoryBuilder} based on the specified + * {@link R2dbcProperties}. If no url is specified, the + * {@link EmbeddedDatabaseConnection} supplier is invoked to determine if an embedded + * database can be configured instead. + * @param properties the properties to use to initialize the builder + * @param embeddedDatabaseConnection a supplier for an + * {@link EmbeddedDatabaseConnection} + * @return a new builder initialized with the settings defined in + * {@link R2dbcProperties} + */ + public static ConnectionFactoryBuilder of(R2dbcProperties properties, + Supplier embeddedDatabaseConnection) { + return new ConnectionFactoryBuilder( + new ConnectionFactoryOptionsInitializer().initialize(properties, adapt(embeddedDatabaseConnection))); + } + + /** + * Configure additional options. + * @param options a {@link Consumer} to customize the options + * @return this for method chaining + */ + public ConnectionFactoryBuilder configure(Consumer options) { + options.accept(this.optionsBuilder); + return this; + } + + /** + * Configure the {@linkplain ConnectionFactoryOptions#USER username}. + * @param username the connection factory username + * @return this for method chaining + */ + public ConnectionFactoryBuilder username(String username) { + return configure((options) -> options.option(ConnectionFactoryOptions.USER, username)); + } + + /** + * Configure the {@linkplain ConnectionFactoryOptions#PASSWORD password}. + * @param password the connection factory password + * @return this for method chaining + */ + public ConnectionFactoryBuilder password(CharSequence password) { + return configure((options) -> options.option(ConnectionFactoryOptions.PASSWORD, password)); + } + + /** + * Configure the {@linkplain ConnectionFactoryOptions#HOST host name}. + * @param host the connection factory hostname + * @return this for method chaining + */ + public ConnectionFactoryBuilder hostname(String host) { + return configure((options) -> options.option(ConnectionFactoryOptions.HOST, host)); + } + + /** + * Configure the {@linkplain ConnectionFactoryOptions#PORT port}. + * @param port the connection factory port + * @return this for method chaining + */ + public ConnectionFactoryBuilder port(int port) { + return configure((options) -> options.option(ConnectionFactoryOptions.PORT, port)); + } + + /** + * Configure the {@linkplain ConnectionFactoryOptions#DATABASE database}. + * @param database the connection factory database + * @return this for method chaining + */ + public ConnectionFactoryBuilder database(String database) { + return configure((options) -> options.option(ConnectionFactoryOptions.DATABASE, database)); + } + + /** + * Build a {@link ConnectionFactory} based on the state of this builder. + * @return a connection factory + */ + public ConnectionFactory build() { + return ConnectionFactories.get(buildOptions()); + } + + /** + * Build a {@link ConnectionFactoryOptions} based on the state of this builder. + * @return the options + */ + public ConnectionFactoryOptions buildOptions() { + return this.optionsBuilder.build(); + } + + private static Supplier adapt( + Supplier embeddedDatabaseConnection) { + return () -> { + EmbeddedDatabaseConnection connection = embeddedDatabaseConnection.get(); + return (connection != null) + ? org.springframework.boot.r2dbc.EmbeddedDatabaseConnection.valueOf(connection.name()) : null; + }; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryConfigurations.java new file mode 100644 index 000000000000..599b947c1f50 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryConfigurations.java @@ -0,0 +1,132 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.r2dbc; + +import java.util.List; +import java.util.stream.Collectors; + +import io.r2dbc.pool.ConnectionPool; +import io.r2dbc.pool.ConnectionPoolConfiguration; +import io.r2dbc.spi.ConnectionFactory; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +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.SpringBootCondition; +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.r2dbc.EmbeddedDatabaseConnection; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.util.StringUtils; + +/** + * Actual {@link ConnectionFactory} configurations. + * + * @author Mark Paluch + * @author Stephane Nicoll + * @author Rodolpho S. Couto + */ +abstract class ConnectionFactoryConfigurations { + + protected static ConnectionFactory createConnectionFactory(R2dbcProperties properties, ClassLoader classLoader, + List optionsCustomizers) { + return org.springframework.boot.r2dbc.ConnectionFactoryBuilder + .withOptions(new ConnectionFactoryOptionsInitializer().initialize(properties, + () -> EmbeddedDatabaseConnection.get(classLoader))) + .configure((options) -> { + for (ConnectionFactoryOptionsBuilderCustomizer optionsCustomizer : optionsCustomizers) { + optionsCustomizer.customize(options); + } + }).build(); + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(ConnectionPool.class) + @Conditional(PooledConnectionFactoryCondition.class) + @ConditionalOnMissingBean(ConnectionFactory.class) + static class Pool { + + @Bean(destroyMethod = "dispose") + ConnectionPool connectionFactory(R2dbcProperties properties, ResourceLoader resourceLoader, + ObjectProvider customizers) { + ConnectionFactory connectionFactory = createConnectionFactory(properties, resourceLoader.getClassLoader(), + customizers.orderedStream().collect(Collectors.toList())); + R2dbcProperties.Pool pool = properties.getPool(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + ConnectionPoolConfiguration.Builder builder = ConnectionPoolConfiguration.builder(connectionFactory); + map.from(pool.getMaxIdleTime()).to(builder::maxIdleTime); + map.from(pool.getMaxLifeTime()).to(builder::maxLifeTime); + map.from(pool.getMaxAcquireTime()).to(builder::maxAcquireTime); + map.from(pool.getMaxCreateConnectionTime()).to(builder::maxCreateConnectionTime); + map.from(pool.getInitialSize()).to(builder::initialSize); + map.from(pool.getMaxSize()).to(builder::maxSize); + map.from(pool.getValidationQuery()).whenHasText().to(builder::validationQuery); + map.from(pool.getValidationDepth()).to(builder::validationDepth); + return new ConnectionPool(builder.build()); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnProperty(prefix = "spring.r2dbc.pool", value = "enabled", havingValue = "false", + matchIfMissing = true) + @ConditionalOnMissingBean(ConnectionFactory.class) + static class Generic { + + @Bean + ConnectionFactory connectionFactory(R2dbcProperties properties, ResourceLoader resourceLoader, + ObjectProvider customizers) { + return createConnectionFactory(properties, resourceLoader.getClassLoader(), + customizers.orderedStream().collect(Collectors.toList())); + } + + } + + /** + * {@link Condition} that checks that a {@link ConnectionPool} is requested. The + * condition matches if pooling was opt-in via configuration and the r2dbc url does + * not contain pooling-related options. + */ + static class PooledConnectionFactoryCondition extends SpringBootCondition { + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { + boolean poolEnabled = context.getEnvironment().getProperty("spring.r2dbc.pool.enabled", Boolean.class, + true); + if (poolEnabled) { + // Make sure the URL does not have pool options + String url = context.getEnvironment().getProperty("spring.r2dbc.url"); + boolean pooledUrl = StringUtils.hasText(url) && url.contains(":pool:"); + if (pooledUrl) { + return ConditionOutcome.noMatch("R2DBC Connection URL contains pooling-related options"); + } + return ConditionOutcome + .match("Pooling is enabled and R2DBC Connection URL does not contain pooling-related options"); + } + return ConditionOutcome.noMatch("Pooling is disabled"); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryDependentConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryDependentConfiguration.java new file mode 100644 index 000000000000..0b5c01858a0b --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryDependentConfiguration.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.r2dbc; + +import io.r2dbc.spi.ConnectionFactory; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.r2dbc.core.DatabaseClient; + +/** + * Configuration of the R2DBC infrastructure based on a {@link ConnectionFactory}. + * + * @author Stephane Nicoll + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(DatabaseClient.class) +@ConditionalOnSingleCandidate(ConnectionFactory.class) +class ConnectionFactoryDependentConfiguration { + + @Bean + @ConditionalOnMissingBean + DatabaseClient r2dbcDatabaseClient(ConnectionFactory connectionFactory) { + return DatabaseClient.builder().connectionFactory(connectionFactory).build(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryOptionsBuilderCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryOptionsBuilderCustomizer.java new file mode 100644 index 000000000000..20fd830ed62b --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryOptionsBuilderCustomizer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.r2dbc; + +import io.r2dbc.spi.ConnectionFactoryOptions; +import io.r2dbc.spi.ConnectionFactoryOptions.Builder; + +/** + * Callback interface that can be implemented by beans wishing to customize the + * {@link ConnectionFactoryOptions} via a {@link Builder} whilst retaining default + * auto-configuration.whilst retaining default auto-configuration. + * + * @author Mark Paluch + * @since 2.3.0 + */ +@FunctionalInterface +public interface ConnectionFactoryOptionsBuilderCustomizer { + + /** + * Customize the {@link Builder}. + * @param builder the builder to customize + */ + void customize(Builder builder); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryOptionsInitializer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryOptionsInitializer.java new file mode 100644 index 000000000000..f5e10f4c7342 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryOptionsInitializer.java @@ -0,0 +1,155 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.r2dbc; + +import java.util.function.Predicate; +import java.util.function.Supplier; + +import io.r2dbc.spi.ConnectionFactoryOptions; +import io.r2dbc.spi.ConnectionFactoryOptions.Builder; +import io.r2dbc.spi.Option; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.boot.r2dbc.EmbeddedDatabaseConnection; +import org.springframework.util.StringUtils; + +/** + * Initialize a {@link ConnectionFactoryOptions.Builder} based on {@link R2dbcProperties}. + * + * @author Stephane Nicoll + */ +class ConnectionFactoryOptionsInitializer { + + /** + * Initialize a {@link io.r2dbc.spi.ConnectionFactoryOptions.Builder + * ConnectionFactoryOptions.Builder} using the specified properties. + * @param properties the properties to use to initialize the builder + * @param embeddedDatabaseConnection the embedded connection to use as a fallback + * @return an initialized builder + * @throws ConnectionFactoryBeanCreationException if no suitable connection could be + * determined + */ + ConnectionFactoryOptions.Builder initialize(R2dbcProperties properties, + Supplier embeddedDatabaseConnection) { + if (StringUtils.hasText(properties.getUrl())) { + return initializeRegularOptions(properties); + } + EmbeddedDatabaseConnection embeddedConnection = embeddedDatabaseConnection.get(); + if (embeddedConnection != EmbeddedDatabaseConnection.NONE) { + return initializeEmbeddedOptions(properties, embeddedConnection); + } + throw connectionFactoryBeanCreationException("Failed to determine a suitable R2DBC Connection URL", properties, + embeddedConnection); + } + + private ConnectionFactoryOptions.Builder initializeRegularOptions(R2dbcProperties properties) { + ConnectionFactoryOptions urlOptions = ConnectionFactoryOptions.parse(properties.getUrl()); + Builder optionsBuilder = urlOptions.mutate(); + configureIf(optionsBuilder, urlOptions, ConnectionFactoryOptions.USER, properties::getUsername, + StringUtils::hasText); + configureIf(optionsBuilder, urlOptions, ConnectionFactoryOptions.PASSWORD, properties::getPassword, + StringUtils::hasText); + configureIf(optionsBuilder, urlOptions, ConnectionFactoryOptions.DATABASE, + () -> determineDatabaseName(properties), StringUtils::hasText); + if (properties.getProperties() != null) { + properties.getProperties().forEach((key, value) -> optionsBuilder.option(Option.valueOf(key), value)); + } + return optionsBuilder; + } + + private Builder initializeEmbeddedOptions(R2dbcProperties properties, + EmbeddedDatabaseConnection embeddedDatabaseConnection) { + String url = embeddedDatabaseConnection.getUrl(determineEmbeddedDatabaseName(properties)); + if (url == null) { + throw connectionFactoryBeanCreationException("Failed to determine a suitable R2DBC Connection URL", + properties, embeddedDatabaseConnection); + } + Builder builder = ConnectionFactoryOptions.parse(url).mutate(); + String username = determineEmbeddedUsername(properties); + if (StringUtils.hasText(username)) { + builder.option(ConnectionFactoryOptions.USER, username); + } + if (StringUtils.hasText(properties.getPassword())) { + builder.option(ConnectionFactoryOptions.PASSWORD, properties.getPassword()); + } + return builder; + } + + private String determineDatabaseName(R2dbcProperties properties) { + if (properties.isGenerateUniqueName()) { + return properties.determineUniqueName(); + } + if (StringUtils.hasLength(properties.getName())) { + return properties.getName(); + } + return null; + } + + private String determineEmbeddedDatabaseName(R2dbcProperties properties) { + String databaseName = determineDatabaseName(properties); + return (databaseName != null) ? databaseName : "testdb"; + } + + private String determineEmbeddedUsername(R2dbcProperties properties) { + String username = ifHasText(properties.getUsername()); + return (username != null) ? username : "sa"; + } + + private void configureIf(Builder optionsBuilder, ConnectionFactoryOptions originalOptions, + Option option, Supplier valueSupplier, Predicate setIf) { + if (originalOptions.hasOption(option)) { + return; + } + T value = valueSupplier.get(); + if (setIf.test(value)) { + optionsBuilder.option(option, value); + } + } + + private ConnectionFactoryBeanCreationException connectionFactoryBeanCreationException(String message, + R2dbcProperties properties, EmbeddedDatabaseConnection embeddedDatabaseConnection) { + return new ConnectionFactoryBeanCreationException(message, properties, embeddedDatabaseConnection); + } + + private String ifHasText(String candidate) { + return (StringUtils.hasText(candidate)) ? candidate : null; + } + + static class ConnectionFactoryBeanCreationException extends BeanCreationException { + + private final R2dbcProperties properties; + + private final EmbeddedDatabaseConnection embeddedDatabaseConnection; + + ConnectionFactoryBeanCreationException(String message, R2dbcProperties properties, + EmbeddedDatabaseConnection embeddedDatabaseConnection) { + super(message); + this.properties = properties; + this.embeddedDatabaseConnection = embeddedDatabaseConnection; + } + + EmbeddedDatabaseConnection getEmbeddedDatabaseConnection() { + return this.embeddedDatabaseConnection; + } + + R2dbcProperties getProperties() { + return this.properties; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/EmbeddedDatabaseConnection.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/EmbeddedDatabaseConnection.java new file mode 100644 index 000000000000..46426a0f1484 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/EmbeddedDatabaseConnection.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.r2dbc; + +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Connection details for embedded databases compatible with r2dbc. + * + * @author Mark Paluch + * @author Stephane Nicoll + * @since 2.3.0 + * @deprecated since 2.5.0 for removal in 2.7.0 in favor of + * {@link org.springframework.boot.r2dbc.EmbeddedDatabaseConnection} + */ +@Deprecated +public enum EmbeddedDatabaseConnection { + + /** + * No Connection. + */ + NONE(null, null, null), + + /** + * H2 Database Connection. + */ + H2("H2", "io.r2dbc.h2.H2ConnectionFactoryProvider", + "r2dbc:h2:mem:///%s?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"); + + private final String type; + + private final String driverClassName; + + private final String url; + + EmbeddedDatabaseConnection(String type, String driverClassName, String url) { + this.type = type; + this.driverClassName = driverClassName; + this.url = url; + } + + /** + * Returns the driver class name. + * @return the driver class name + */ + public String getDriverClassName() { + return this.driverClassName; + } + + /** + * Returns the embedded database type name for the connection. + * @return the database type + */ + public String getType() { + return this.type; + } + + /** + * Returns the R2DBC URL for the connection using the specified {@code databaseName}. + * @param databaseName the name of the database + * @return the connection URL + */ + public String getUrl(String databaseName) { + Assert.hasText(databaseName, "DatabaseName must not be empty"); + return (this.url != null) ? String.format(this.url, databaseName) : null; + } + + /** + * Returns the most suitable {@link EmbeddedDatabaseConnection} for the given class + * loader. + * @param classLoader the class loader used to check for classes + * @return an {@link EmbeddedDatabaseConnection} or {@link #NONE}. + */ + public static EmbeddedDatabaseConnection get(ClassLoader classLoader) { + for (EmbeddedDatabaseConnection candidate : EmbeddedDatabaseConnection.values()) { + if (candidate != NONE && ClassUtils.isPresent(candidate.getDriverClassName(), classLoader)) { + return candidate; + } + } + return NONE; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcAutoConfiguration.java new file mode 100644 index 000000000000..11f50c9621ac --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcAutoConfiguration.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.r2dbc; + +import io.r2dbc.spi.ConnectionFactory; + +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for R2DBC. + * + * @author Mark Paluch + * @author Stephane Nicoll + * @since 2.3.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(ConnectionFactory.class) +@AutoConfigureBefore(DataSourceAutoConfiguration.class) +@EnableConfigurationProperties(R2dbcProperties.class) +@Import({ ConnectionFactoryConfigurations.Pool.class, ConnectionFactoryConfigurations.Generic.class, + ConnectionFactoryDependentConfiguration.class }) +public class R2dbcAutoConfiguration { + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcProperties.java new file mode 100644 index 000000000000..e93add6bb408 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcProperties.java @@ -0,0 +1,247 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.r2dbc; + +import java.time.Duration; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; + +import io.r2dbc.spi.ValidationDepth; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties for R2DBC. + * + * @author Mark Paluch + * @author Andreas Killaitis + * @author Stephane Nicoll + * @author Rodolpho S. Couto + * @since 2.3.0 + */ +@ConfigurationProperties(prefix = "spring.r2dbc") +public class R2dbcProperties { + + /** + * Database name. Set if no name is specified in the url. Default to "testdb" when + * using an embedded database. + */ + private String name; + + /** + * Whether to generate a random database name. Ignore any configured name when + * enabled. + */ + private boolean generateUniqueName; + + /** + * R2DBC URL of the database. database name, username, password and pooling options + * specified in the url take precedence over individual options. + */ + private String url; + + /** + * Login username of the database. Set if no username is specified in the url. + */ + private String username; + + /** + * Login password of the database. Set if no password is specified in the url. + */ + private String password; + + /** + * Additional R2DBC options. + */ + private final Map properties = new LinkedHashMap<>(); + + private final Pool pool = new Pool(); + + private String uniqueName; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isGenerateUniqueName() { + return this.generateUniqueName; + } + + public void setGenerateUniqueName(boolean generateUniqueName) { + this.generateUniqueName = generateUniqueName; + } + + public String getUrl() { + return this.url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getUsername() { + return this.username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return this.password; + } + + public void setPassword(String password) { + this.password = password; + } + + public Map getProperties() { + return this.properties; + } + + public Pool getPool() { + return this.pool; + } + + /** + * Provide a unique name specific to this instance. Calling this method several times + * return the same unique name. + * @return a unique name for this instance + */ + public String determineUniqueName() { + if (this.uniqueName == null) { + this.uniqueName = UUID.randomUUID().toString(); + } + return this.uniqueName; + } + + public static class Pool { + + /** + * Maximum amount of time that a connection is allowed to sit idle in the pool. + */ + private Duration maxIdleTime = Duration.ofMinutes(30); + + /** + * Maximum lifetime of a connection in the pool. By default, connections have an + * infinite lifetime. + */ + private Duration maxLifeTime; + + /** + * Maximum time to acquire a connection from the pool. By default, wait + * indefinitely. + */ + private Duration maxAcquireTime; + + /** + * Maximum time to wait to create a new connection. By default, wait indefinitely. + */ + private Duration maxCreateConnectionTime; + + /** + * Initial connection pool size. + */ + private int initialSize = 10; + + /** + * Maximal connection pool size. + */ + private int maxSize = 10; + + /** + * Validation query. + */ + private String validationQuery; + + /** + * Validation depth. + */ + private ValidationDepth validationDepth = ValidationDepth.LOCAL; + + public Duration getMaxIdleTime() { + return this.maxIdleTime; + } + + public void setMaxIdleTime(Duration maxIdleTime) { + this.maxIdleTime = maxIdleTime; + } + + public Duration getMaxLifeTime() { + return this.maxLifeTime; + } + + public void setMaxLifeTime(Duration maxLifeTime) { + this.maxLifeTime = maxLifeTime; + } + + public Duration getMaxAcquireTime() { + return this.maxAcquireTime; + } + + public void setMaxAcquireTime(Duration maxAcquireTime) { + this.maxAcquireTime = maxAcquireTime; + } + + public Duration getMaxCreateConnectionTime() { + return this.maxCreateConnectionTime; + } + + public void setMaxCreateConnectionTime(Duration maxCreateConnectionTime) { + this.maxCreateConnectionTime = maxCreateConnectionTime; + } + + public int getInitialSize() { + return this.initialSize; + } + + public void setInitialSize(int initialSize) { + this.initialSize = initialSize; + } + + public int getMaxSize() { + return this.maxSize; + } + + public void setMaxSize(int maxSize) { + this.maxSize = maxSize; + } + + public String getValidationQuery() { + return this.validationQuery; + } + + public void setValidationQuery(String validationQuery) { + this.validationQuery = validationQuery; + } + + public ValidationDepth getValidationDepth() { + return this.validationDepth; + } + + public void setValidationDepth(ValidationDepth validationDepth) { + this.validationDepth = validationDepth; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcTransactionManagerAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcTransactionManagerAutoConfiguration.java new file mode 100644 index 000000000000..3b963f2bd0ed --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcTransactionManagerAutoConfiguration.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.r2dbc; + +import io.r2dbc.spi.ConnectionFactory; + +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.AutoConfigureOrder; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.r2dbc.connection.R2dbcTransactionManager; +import org.springframework.transaction.ReactiveTransactionManager; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for {@link R2dbcTransactionManager}. + * + * @author Mark Paluch + * @since 2.3.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass({ R2dbcTransactionManager.class, ReactiveTransactionManager.class }) +@ConditionalOnSingleCandidate(ConnectionFactory.class) +@AutoConfigureOrder(Ordered.LOWEST_PRECEDENCE) +@AutoConfigureBefore(TransactionAutoConfiguration.class) +public class R2dbcTransactionManagerAutoConfiguration { + + @Bean + @ConditionalOnMissingBean(ReactiveTransactionManager.class) + public R2dbcTransactionManager connectionFactoryTransactionManager(ConnectionFactory connectionFactory) { + return new R2dbcTransactionManager(connectionFactory); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/package-info.java new file mode 100644 index 000000000000..e36e5c3a3ade --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-Configuration for R2DBC. + */ +package org.springframework.boot.autoconfigure.r2dbc; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketMessageHandlerCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketMessageHandlerCustomizer.java new file mode 100644 index 000000000000..c1f7e2f1d62a --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketMessageHandlerCustomizer.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.rsocket; + +import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler; + +/** + * Callback interface that can be used to customize a {@link RSocketMessageHandler}. + * + * @author Aarti Gupta + * @author Madhura Bhave + * @since 2.3.0 + */ +@FunctionalInterface +public interface RSocketMessageHandlerCustomizer { + + /** + * Customize the {@link RSocketMessageHandler}. + * @param messageHandler the message handler to customize + */ + void customize(RSocketMessageHandler messageHandler); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketMessagingAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketMessagingAutoConfiguration.java index 09a3898180bf..d8520580a711 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketMessagingAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketMessagingAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,9 @@ package org.springframework.boot.autoconfigure.rsocket; -import io.rsocket.RSocketFactory; import io.rsocket.transport.netty.server.TcpServerTransport; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -37,15 +37,17 @@ * @since 2.2.0 */ @Configuration(proxyBeanMethods = false) -@ConditionalOnClass({ RSocketRequester.class, RSocketFactory.class, TcpServerTransport.class }) +@ConditionalOnClass({ RSocketRequester.class, io.rsocket.RSocket.class, TcpServerTransport.class }) @AutoConfigureAfter(RSocketStrategiesAutoConfiguration.class) public class RSocketMessagingAutoConfiguration { @Bean @ConditionalOnMissingBean - public RSocketMessageHandler messageHandler(RSocketStrategies rSocketStrategies) { + public RSocketMessageHandler messageHandler(RSocketStrategies rSocketStrategies, + ObjectProvider customizers) { RSocketMessageHandler messageHandler = new RSocketMessageHandler(); messageHandler.setRSocketStrategies(rSocketStrategies); + customizers.orderedStream().forEach((customizer) -> customizer.customize(messageHandler)); return messageHandler; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketProperties.java index 232eb1db4272..f1921f23885c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,17 +19,22 @@ import java.net.InetAddress; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; import org.springframework.boot.rsocket.server.RSocketServer; +import org.springframework.boot.web.server.Ssl; +import org.springframework.util.unit.DataSize; /** * {@link ConfigurationProperties properties} for RSocket support. * * @author Brian Clozel + * @author Chris Bono * @since 2.2.0 */ @ConfigurationProperties("spring.rsocket") public class RSocketProperties { + @NestedConfigurationProperty private final Server server = new Server(); public Server getServer() { @@ -59,6 +64,15 @@ public static class Server { */ private String mappingPath; + /** + * Maximum transmission unit. Frames larger than the specified value are + * fragmented. + */ + private DataSize fragmentSize; + + @NestedConfigurationProperty + private Ssl ssl; + public Integer getPort() { return this.port; } @@ -91,6 +105,22 @@ public void setMappingPath(String mappingPath) { this.mappingPath = mappingPath; } + public DataSize getFragmentSize() { + return this.fragmentSize; + } + + public void setFragmentSize(DataSize fragmentSize) { + this.fragmentSize = fragmentSize; + } + + public Ssl getSsl() { + return this.ssl; + } + + public void setSsl(Ssl ssl) { + this.ssl = ssl; + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketRequesterAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketRequesterAutoConfiguration.java index 64366ebea4a0..9028ea132019 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketRequesterAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketRequesterAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.springframework.boot.autoconfigure.rsocket; -import io.rsocket.RSocketFactory; import io.rsocket.transport.netty.server.TcpServerTransport; import reactor.netty.http.server.HttpServer; @@ -41,7 +40,7 @@ * @since 2.2.0 */ @Configuration(proxyBeanMethods = false) -@ConditionalOnClass({ RSocketRequester.class, RSocketFactory.class, HttpServer.class, TcpServerTransport.class }) +@ConditionalOnClass({ RSocketRequester.class, io.rsocket.RSocket.class, HttpServer.class, TcpServerTransport.class }) @AutoConfigureAfter(RSocketStrategiesAutoConfiguration.class) public class RSocketRequesterAutoConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfiguration.java index 8856f145213f..1dd595f7639d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ import java.util.stream.Collectors; -import io.rsocket.RSocketFactory; +import io.rsocket.core.RSocketServer; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.transport.netty.server.TcpServerTransport; import reactor.netty.http.server.HttpServer; @@ -36,8 +36,8 @@ import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.rsocket.context.RSocketServerBootstrap; import org.springframework.boot.rsocket.netty.NettyRSocketServerFactory; +import org.springframework.boot.rsocket.server.RSocketServerCustomizer; import org.springframework.boot.rsocket.server.RSocketServerFactory; -import org.springframework.boot.rsocket.server.ServerRSocketFactoryProcessor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; @@ -57,7 +57,7 @@ * @since 2.2.0 */ @Configuration(proxyBeanMethods = false) -@ConditionalOnClass({ RSocketFactory.class, RSocketStrategies.class, HttpServer.class, TcpServerTransport.class }) +@ConditionalOnClass({ RSocketServer.class, RSocketStrategies.class, HttpServer.class, TcpServerTransport.class }) @ConditionalOnBean(RSocketMessageHandler.class) @AutoConfigureAfter(RSocketStrategiesAutoConfiguration.class) @EnableConfigurationProperties(RSocketProperties.class) @@ -65,21 +65,22 @@ public class RSocketServerAutoConfiguration { @Conditional(OnRSocketWebServerCondition.class) @Configuration(proxyBeanMethods = false) - static class WebFluxServerAutoConfiguration { + static class WebFluxServerConfiguration { @Bean @ConditionalOnMissingBean RSocketWebSocketNettyRouteProvider rSocketWebsocketRouteProvider(RSocketProperties properties, - RSocketMessageHandler messageHandler, ObjectProvider processors) { + RSocketMessageHandler messageHandler, ObjectProvider customizers) { return new RSocketWebSocketNettyRouteProvider(properties.getServer().getMappingPath(), - messageHandler.responder(), processors.orderedStream()); + messageHandler.responder(), customizers.orderedStream()); } } @ConditionalOnProperty(prefix = "spring.rsocket.server", name = "port") + @ConditionalOnClass(ReactorResourceFactory.class) @Configuration(proxyBeanMethods = false) - static class EmbeddedServerAutoConfiguration { + static class EmbeddedServerConfiguration { @Bean @ConditionalOnMissingBean @@ -90,14 +91,16 @@ ReactorResourceFactory reactorResourceFactory() { @Bean @ConditionalOnMissingBean RSocketServerFactory rSocketServerFactory(RSocketProperties properties, ReactorResourceFactory resourceFactory, - ObjectProvider processors) { + ObjectProvider customizers) { NettyRSocketServerFactory factory = new NettyRSocketServerFactory(); factory.setResourceFactory(resourceFactory); factory.setTransport(properties.getServer().getTransport()); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); map.from(properties.getServer().getAddress()).to(factory::setAddress); map.from(properties.getServer().getPort()).to(factory::setPort); - factory.setSocketFactoryProcessors(processors.orderedStream().collect(Collectors.toList())); + map.from(properties.getServer().getFragmentSize()).to(factory::setFragmentSize); + map.from(properties.getServer().getSsl()).to(factory::setSsl); + factory.setRSocketServerCustomizers(customizers.orderedStream().collect(Collectors.toList())); return factory; } @@ -109,13 +112,12 @@ RSocketServerBootstrap rSocketServerBootstrap(RSocketServerFactory rSocketServer } @Bean - ServerRSocketFactoryProcessor frameDecoderServerFactoryCustomizer(RSocketMessageHandler rSocketMessageHandler) { - return (serverRSocketFactory) -> { + RSocketServerCustomizer frameDecoderRSocketServerCustomizer(RSocketMessageHandler rSocketMessageHandler) { + return (server) -> { if (rSocketMessageHandler.getRSocketStrategies() .dataBufferFactory() instanceof NettyDataBufferFactory) { - return serverRSocketFactory.frameDecoder(PayloadDecoder.ZERO_COPY); + server.payloadDecoder(PayloadDecoder.ZERO_COPY); } - return serverRSocketFactory; }; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketStrategiesAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketStrategiesAutoConfiguration.java index b458d8739dc1..e59466b08283 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketStrategiesAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketStrategiesAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.cbor.CBORFactory; import io.netty.buffer.PooledByteBufAllocator; -import io.rsocket.RSocketFactory; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfigureAfter; @@ -49,7 +48,7 @@ * @since 2.2.0 */ @Configuration(proxyBeanMethods = false) -@ConditionalOnClass({ RSocketFactory.class, RSocketStrategies.class, PooledByteBufAllocator.class }) +@ConditionalOnClass({ io.rsocket.RSocket.class, RSocketStrategies.class, PooledByteBufAllocator.class }) @AutoConfigureAfter(JacksonAutoConfiguration.class) public class RSocketStrategiesAutoConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketWebSocketNettyRouteProvider.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketWebSocketNettyRouteProvider.java index a15a82cc0995..d34935ea67de 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketWebSocketNettyRouteProvider.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketWebSocketNettyRouteProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,13 +20,13 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import io.rsocket.RSocketFactory; import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketServer; import io.rsocket.transport.ServerTransport; import io.rsocket.transport.netty.server.WebsocketRouteTransport; import reactor.netty.http.server.HttpServerRoutes; -import org.springframework.boot.rsocket.server.ServerRSocketFactoryProcessor; +import org.springframework.boot.rsocket.server.RSocketServerCustomizer; import org.springframework.boot.web.embedded.netty.NettyRouteProvider; /** @@ -40,23 +40,21 @@ class RSocketWebSocketNettyRouteProvider implements NettyRouteProvider { private final SocketAcceptor socketAcceptor; - private final List processors; + private final List customizers; RSocketWebSocketNettyRouteProvider(String mappingPath, SocketAcceptor socketAcceptor, - Stream processors) { + Stream customizers) { this.mappingPath = mappingPath; this.socketAcceptor = socketAcceptor; - this.processors = processors.collect(Collectors.toList()); + this.customizers = customizers.collect(Collectors.toList()); } @Override public HttpServerRoutes apply(HttpServerRoutes httpServerRoutes) { - RSocketFactory.ServerRSocketFactory server = RSocketFactory.receive(); - for (ServerRSocketFactoryProcessor processor : this.processors) { - server = processor.process(server); - } - ServerTransport.ConnectionAcceptor acceptor = server.acceptor(this.socketAcceptor).toConnectionAcceptor(); - return httpServerRoutes.ws(this.mappingPath, WebsocketRouteTransport.newHandler(acceptor)); + RSocketServer server = RSocketServer.create(this.socketAcceptor); + this.customizers.forEach((customizer) -> customizer.customize(server)); + ServerTransport.ConnectionAcceptor connectionAcceptor = server.asConnectionAcceptor(); + return httpServerRoutes.ws(this.mappingPath, WebsocketRouteTransport.newHandler(connectionAcceptor)); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/ConditionalOnDefaultWebSecurity.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/ConditionalOnDefaultWebSecurity.java new file mode 100644 index 000000000000..d23354151d08 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/ConditionalOnDefaultWebSecurity.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Conditional; + +/** + * {@link Conditional @Conditional} that only matches when web security is available and + * the user has not defined their own configuration. + * + * @author Phillip Webb + * @since 2.4.0 + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Conditional(DefaultWebSecurityCondition.class) +public @interface ConditionalOnDefaultWebSecurity { + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/DefaultWebSecurityCondition.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/DefaultWebSecurityCondition.java new file mode 100644 index 000000000000..1b8f12d68528 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/DefaultWebSecurityCondition.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security; + +import org.springframework.boot.autoconfigure.condition.AllNestedConditions; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Condition; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.web.SecurityFilterChain; + +/** + * {@link Condition} for + * {@link ConditionalOnDefaultWebSecurity @ConditionalOnDefaultWebSecurity}. + * + * @author Phillip Webb + */ +class DefaultWebSecurityCondition extends AllNestedConditions { + + DefaultWebSecurityCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnClass({ SecurityFilterChain.class, HttpSecurity.class }) + static class Classes { + + } + + @ConditionalOnMissingBean({ WebSecurityConfigurerAdapter.class, SecurityFilterChain.class }) + static class Beans { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SecurityProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SecurityProperties.java index f705de67e377..aa2b859c5a81 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SecurityProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SecurityProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ public class SecurityProperties { /** - * Order applied to the WebSecurityConfigurerAdapter that is used to configure basic + * Order applied to the SecurityFilterChain that is used to configure basic * authentication for application endpoints. If you want to add your own * authentication for all or some of those endpoints the best thing to do is to add * your own WebSecurityConfigurerAdapter with lower order. @@ -49,7 +49,7 @@ public class SecurityProperties { public static final int BASIC_AUTH_ORDER = Ordered.LOWEST_PRECEDENCE - 5; /** - * Order applied to the WebSecurityConfigurer that ignores standard static resource + * Order applied to the WebSecurityCustomizer that ignores standard static resource * paths. */ public static final int IGNORED_ORDER = Ordered.HIGHEST_PRECEDENCE; @@ -63,7 +63,7 @@ public class SecurityProperties { private final Filter filter = new Filter(); - private User user = new User(); + private final User user = new User(); public User getUser() { return this.user; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/StaticResourceLocation.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/StaticResourceLocation.java index 19c1cd220009..ab61a0d2c7bb 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/StaticResourceLocation.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/StaticResourceLocation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,7 +50,7 @@ public enum StaticResourceLocation { /** * The {@code "favicon.ico"} resource. */ - FAVICON("/**/favicon.ico"); + FAVICON("/favicon.*", "/*/icon-*"); private final String[] patterns; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ClientsConfiguredCondition.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ClientsConfiguredCondition.java index b5f60c223e48..c6246d4c24b1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ClientsConfiguredCondition.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ClientsConfiguredCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.autoconfigure.security.oauth2.client; import java.util.Collections; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientProperties.java index 8b16e6b2fd88..22effd124982 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,8 +20,7 @@ import java.util.Map; import java.util.Set; -import javax.annotation.PostConstruct; - +import org.springframework.beans.factory.InitializingBean; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.util.StringUtils; @@ -35,7 +34,7 @@ * @since 2.0.0 */ @ConfigurationProperties(prefix = "spring.security.oauth2.client") -public class OAuth2ClientProperties { +public class OAuth2ClientProperties implements InitializingBean { /** * OAuth provider details. @@ -55,9 +54,13 @@ public Map getRegistration() { return this.registration; } - @PostConstruct + @Override + public void afterPropertiesSet() { + validate(); + } + public void validate() { - this.getRegistration().values().forEach(this::validateRegistration); + getRegistration().values().forEach(this::validateRegistration); } private void validateRegistration(Registration registration) { @@ -105,7 +108,8 @@ public static class Registration { private String redirectUri; /** - * Authorization scopes. May be left blank when using a pre-defined provider. + * Authorization scopes. When left blank the provider's default scopes, if any, + * will be used. */ private Set scope; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapter.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapter.java index 481257a44a93..d1b977767597 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapter.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -67,7 +67,7 @@ private static ClientRegistration getClientRegistration(String registrationId, .to(builder::clientAuthenticationMethod); map.from(properties::getAuthorizationGrantType).as(AuthorizationGrantType::new) .to(builder::authorizationGrantType); - map.from(properties::getRedirectUri).to(builder::redirectUriTemplate); + map.from(properties::getRedirectUri).to(builder::redirectUri); map.from(properties::getScope).as(StringUtils::toStringArray).to(builder::scope); map.from(properties::getClientName).to(builder::clientName); return builder.build(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientAutoConfiguration.java index ddb5590e3eea..288c9a1d62fd 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.autoconfigure.security.oauth2.client.reactive; import reactor.core.publisher.Flux; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientConfigurations.java index 3509e66325bb..4f4621f1fea0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientConfigurations.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientConfigurations.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,7 +45,7 @@ */ class ReactiveOAuth2ClientConfigurations { - @Configuration + @Configuration(proxyBeanMethods = false) @Conditional(ClientsConfiguredCondition.class) @ConditionalOnMissingBean(ReactiveClientRegistrationRepository.class) static class ReactiveClientRegistrationRepositoryConfiguration { @@ -59,7 +59,7 @@ InMemoryReactiveClientRegistrationRepository clientRegistrationRepository(OAuth2 } - @Configuration + @Configuration(proxyBeanMethods = false) @ConditionalOnBean(ReactiveClientRegistrationRepository.class) static class ReactiveOAuth2ClientConfiguration { @@ -77,7 +77,7 @@ ServerOAuth2AuthorizedClientRepository authorizedClientRepository( return new AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository(authorizedClientService); } - @Configuration + @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) static class SecurityWebFilterChainConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/servlet/OAuth2WebSecurityConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/servlet/OAuth2WebSecurityConfiguration.java index 41eda3bd4cf3..bee12ac2c3a1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/servlet/OAuth2WebSecurityConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/servlet/OAuth2WebSecurityConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,19 +18,20 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.security.ConditionalOnDefaultWebSecurity; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.web.SecurityFilterChain; /** - * {@link WebSecurityConfigurerAdapter} to add OAuth client support. + * {@link SecurityFilterChain} to add OAuth client support. * * @author Madhura Bhave * @author Phillip Webb @@ -52,14 +53,15 @@ OAuth2AuthorizedClientRepository authorizedClientRepository(OAuth2AuthorizedClie } @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class) - static class OAuth2WebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { + @ConditionalOnDefaultWebSecurity + static class OAuth2SecurityFilterChainConfiguration { - @Override - protected void configure(HttpSecurity http) throws Exception { + @Bean + SecurityFilterChain oauth2SecurityFilterChain(HttpSecurity http) throws Exception { http.authorizeRequests((requests) -> requests.anyRequest().authenticated()); http.oauth2Login(Customizer.withDefaults()); http.oauth2Client(); + return http.build(); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/IssuerUriCondition.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/IssuerUriCondition.java index fa8be7d4dd0f..d2e5c10763fc 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/IssuerUriCondition.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/IssuerUriCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.autoconfigure.security.oauth2.resource; import org.springframework.boot.autoconfigure.condition.ConditionMessage; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerProperties.java index fe6eff975d75..acf7fbc23566 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,14 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.autoconfigure.security.oauth2.resource; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; -import javax.annotation.PostConstruct; - import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; import org.springframework.core.io.Resource; @@ -49,26 +48,6 @@ public Opaquetoken getOpaquetoken() { return this.opaqueToken; } - @PostConstruct - public void validate() { - if (this.getOpaquetoken().getIntrospectionUri() != null) { - if (this.getJwt().getJwkSetUri() != null) { - handleError("jwt.jwk-set-uri"); - } - if (this.getJwt().getIssuerUri() != null) { - handleError("jwt.issuer-uri"); - } - if (this.getJwt().getPublicKeyLocation() != null) { - handleError("jwt.public-key-location"); - } - } - } - - private void handleError(String property) { - throw new IllegalStateException( - "Only one of " + property + " and opaquetoken.introspection-uri should be configured."); - } - public static class Jwt { /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfiguration.java index 74d8e0b52751..743251cefb19 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.autoconfigure.security.oauth2.resource.reactive; import org.springframework.boot.autoconfigure.AutoConfigureBefore; @@ -26,9 +27,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; -import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; -import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; -import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector; /** * {@link EnableAutoConfiguration Auto-configuration} for Reactive OAuth2 resource server @@ -42,22 +40,8 @@ @EnableConfigurationProperties(OAuth2ResourceServerProperties.class) @ConditionalOnClass({ EnableWebFluxSecurity.class }) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) +@Import({ ReactiveOAuth2ResourceServerConfiguration.JwtConfiguration.class, + ReactiveOAuth2ResourceServerConfiguration.OpaqueTokenConfiguration.class }) public class ReactiveOAuth2ResourceServerAutoConfiguration { - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass({ BearerTokenAuthenticationToken.class, ReactiveJwtDecoder.class }) - @Import({ ReactiveOAuth2ResourceServerJwkConfiguration.JwtConfiguration.class, - ReactiveOAuth2ResourceServerJwkConfiguration.WebSecurityConfiguration.class }) - static class JwtConfiguration { - - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass({ BearerTokenAuthenticationToken.class, ReactiveOpaqueTokenIntrospector.class }) - @Import({ ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.OpaqueTokenIntrospectionClientConfiguration.class, - ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.WebSecurityConfiguration.class }) - static class OpaqueTokenConfiguration { - - } - } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerConfiguration.java new file mode 100644 index 000000000000..2911e9afd5e8 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerConfiguration.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security.oauth2.resource.reactive; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; +import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; +import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector; + +/** + * Configuration classes for OAuth2 Resource Server These should be {@code @Import} in a + * regular auto-configuration class to guarantee their order of execution. + * + * @author Madhura Bhave + */ +class ReactiveOAuth2ResourceServerConfiguration { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass({ BearerTokenAuthenticationToken.class, ReactiveJwtDecoder.class }) + @Import({ ReactiveOAuth2ResourceServerJwkConfiguration.JwtConfiguration.class, + ReactiveOAuth2ResourceServerJwkConfiguration.WebSecurityConfiguration.class }) + static class JwtConfiguration { + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass({ BearerTokenAuthenticationToken.class, ReactiveOpaqueTokenIntrospector.class }) + @Import({ ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.OpaqueTokenIntrospectionClientConfiguration.class, + ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.WebSecurityConfiguration.class }) + static class OpaqueTokenConfiguration { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java index f02413356006..03c7a34bc31f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.autoconfigure.security.oauth2.resource.reactive; import java.security.KeyFactory; @@ -31,6 +32,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity.OAuth2ResourceServerSpec; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import org.springframework.security.oauth2.jwt.JwtValidators; import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; @@ -45,6 +47,7 @@ * @author Madhura Bhave * @author Artsiom Yudovin * @author HaiTao Zhang + * @author Anastasiia Losieva */ @Configuration(proxyBeanMethods = false) class ReactiveOAuth2ResourceServerJwkConfiguration { @@ -62,8 +65,9 @@ static class JwtConfiguration { @Bean @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri") ReactiveJwtDecoder jwtDecoder() { - NimbusReactiveJwtDecoder nimbusReactiveJwtDecoder = new NimbusReactiveJwtDecoder( - this.properties.getJwkSetUri()); + NimbusReactiveJwtDecoder nimbusReactiveJwtDecoder = NimbusReactiveJwtDecoder + .withJwkSetUri(this.properties.getJwkSetUri()) + .jwsAlgorithm(SignatureAlgorithm.from(this.properties.getJwsAlgorithm())).build(); String issuerUri = this.properties.getIssuerUri(); if (issuerUri != null) { nimbusReactiveJwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuerUri)); @@ -76,7 +80,8 @@ ReactiveJwtDecoder jwtDecoder() { NimbusReactiveJwtDecoder jwtDecoderByPublicKeyValue() throws Exception { RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA") .generatePublic(new X509EncodedKeySpec(getKeySpec(this.properties.readPublicKey()))); - return NimbusReactiveJwtDecoder.withPublicKey(publicKey).build(); + return NimbusReactiveJwtDecoder.withPublicKey(publicKey) + .signatureAlgorithm(SignatureAlgorithm.from(this.properties.getJwsAlgorithm())).build(); } private byte[] getKeySpec(String keyValue) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfiguration.java index eb36410c0db2..e43a973020e0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.autoconfigure.security.oauth2.resource.servlet; import org.springframework.boot.autoconfigure.AutoConfigureBefore; @@ -25,10 +26,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; -import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; -import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; /** * {@link EnableAutoConfiguration Auto-configuration} for OAuth2 resource server support. @@ -39,24 +37,10 @@ @Configuration(proxyBeanMethods = false) @AutoConfigureBefore({ SecurityAutoConfiguration.class, UserDetailsServiceAutoConfiguration.class }) @EnableConfigurationProperties(OAuth2ResourceServerProperties.class) +@ConditionalOnClass(BearerTokenAuthenticationToken.class) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) - +@Import({ Oauth2ResourceServerConfiguration.JwtConfiguration.class, + Oauth2ResourceServerConfiguration.OpaqueTokenConfiguration.class }) public class OAuth2ResourceServerAutoConfiguration { - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass({ JwtAuthenticationToken.class, JwtDecoder.class }) - @Import({ OAuth2ResourceServerJwtConfiguration.JwtDecoderConfiguration.class, - OAuth2ResourceServerJwtConfiguration.OAuth2WebSecurityConfigurerAdapter.class }) - static class JwtConfiguration { - - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass({ BearerTokenAuthenticationToken.class, OpaqueTokenIntrospector.class }) - @Import({ OAuth2ResourceServerOpaqueTokenConfiguration.OpaqueTokenIntrospectionClientConfiguration.class, - OAuth2ResourceServerOpaqueTokenConfiguration.OAuth2WebSecurityConfigurerAdapter.class }) - static class OpaqueTokenConfiguration { - - } - } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java index 98c27f2fde04..1ea3676faab0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.autoconfigure.security.oauth2.resource.servlet; import java.security.KeyFactory; @@ -23,6 +24,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.security.ConditionalOnDefaultWebSecurity; import org.springframework.boot.autoconfigure.security.oauth2.resource.IssuerUriCondition; import org.springframework.boot.autoconfigure.security.oauth2.resource.KeyValueCondition; import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; @@ -30,25 +32,24 @@ import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtDecoders; import org.springframework.security.oauth2.jwt.JwtValidators; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.web.SecurityFilterChain; /** * Configures a {@link JwtDecoder} when a JWK Set URI, OpenID Connect Issuer URI or Public - * Key configuration is available. Also configures a {@link WebSecurityConfigurerAdapter} - * if a {@link JwtDecoder} bean is found. + * Key configuration is available. Also configures a {@link SecurityFilterChain} if a + * {@link JwtDecoder} bean is found. * * @author Madhura Bhave * @author Artsiom Yudovin * @author HaiTao Zhang */ @Configuration(proxyBeanMethods = false) - class OAuth2ResourceServerJwtConfiguration { @Configuration(proxyBeanMethods = false) @@ -78,7 +79,8 @@ JwtDecoder jwtDecoderByJwkKeySetUri() { JwtDecoder jwtDecoderByPublicKeyValue() throws Exception { RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA") .generatePublic(new X509EncodedKeySpec(getKeySpec(this.properties.readPublicKey()))); - return NimbusJwtDecoder.withPublicKey(publicKey).build(); + return NimbusJwtDecoder.withPublicKey(publicKey) + .signatureAlgorithm(SignatureAlgorithm.from(this.properties.getJwsAlgorithm())).build(); } private byte[] getKeySpec(String keyValue) { @@ -95,21 +97,15 @@ JwtDecoder jwtDecoderByIssuerUri() { } @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class) - static class OAuth2WebSecurityConfigurerAdapter { + @ConditionalOnDefaultWebSecurity + static class OAuth2SecurityFilterChainConfiguration { @Bean @ConditionalOnBean(JwtDecoder.class) - WebSecurityConfigurerAdapter jwtDecoderWebSecurityConfigurerAdapter() { - return new WebSecurityConfigurerAdapter() { - - @Override - protected void configure(HttpSecurity http) throws Exception { - http.authorizeRequests((requests) -> requests.anyRequest().authenticated()); - http.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt); - } - - }; + SecurityFilterChain jwtSecurityFilterChain(HttpSecurity http) throws Exception { + http.authorizeRequests((requests) -> requests.anyRequest().authenticated()); + http.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt); + return http.build(); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerOpaqueTokenConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerOpaqueTokenConfiguration.java index ab1c206c6101..d5be309d311c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerOpaqueTokenConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerOpaqueTokenConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,23 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.autoconfigure.security.oauth2.resource.servlet; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.security.ConditionalOnDefaultWebSecurity; import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; import org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector; import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; +import org.springframework.security.web.SecurityFilterChain; /** * Configures a {@link OpaqueTokenIntrospector} when a token introspection endpoint is - * available. Also configures a {@link WebSecurityConfigurerAdapter} if a + * available. Also configures a {@link SecurityFilterChain} if a * {@link OpaqueTokenIntrospector} bean is found. * * @author Madhura Bhave @@ -52,21 +54,15 @@ NimbusOpaqueTokenIntrospector opaqueTokenIntrospector(OAuth2ResourceServerProper } @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class) - static class OAuth2WebSecurityConfigurerAdapter { + @ConditionalOnDefaultWebSecurity + static class OAuth2SecurityFilterChainConfiguration { @Bean @ConditionalOnBean(OpaqueTokenIntrospector.class) - WebSecurityConfigurerAdapter opaqueTokenWebSecurityConfigurerAdapter() { - return new WebSecurityConfigurerAdapter() { - - @Override - protected void configure(HttpSecurity http) throws Exception { - http.authorizeRequests((requests) -> requests.anyRequest().authenticated()); - http.oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken); - } - - }; + SecurityFilterChain opaqueTokenSecurityFilterChain(HttpSecurity http) throws Exception { + http.authorizeRequests((requests) -> requests.anyRequest().authenticated()); + http.oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken); + return http.build(); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/Oauth2ResourceServerConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/Oauth2ResourceServerConfiguration.java new file mode 100644 index 000000000000..36c522e39a7b --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/Oauth2ResourceServerConfiguration.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security.oauth2.resource.servlet; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.security.oauth2.jwt.JwtDecoder; + +/** + * Configuration classes for OAuth2 Resource Server These should be {@code @Import} in a + * regular auto-configuration class to guarantee their order of execution. + * + * @author Madhura Bhave + */ +class Oauth2ResourceServerConfiguration { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(JwtDecoder.class) + @Import({ OAuth2ResourceServerJwtConfiguration.JwtDecoderConfiguration.class, + OAuth2ResourceServerJwtConfiguration.OAuth2SecurityFilterChainConfiguration.class }) + static class JwtConfiguration { + + } + + @Configuration(proxyBeanMethods = false) + @Import({ OAuth2ResourceServerOpaqueTokenConfiguration.OpaqueTokenIntrospectionClientConfiguration.class, + OAuth2ResourceServerOpaqueTokenConfiguration.OAuth2SecurityFilterChainConfiguration.class }) + static class OpaqueTokenConfiguration { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/StaticResourceRequest.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/StaticResourceRequest.java index 04f9baa1123f..a1609fa9520c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/StaticResourceRequest.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/StaticResourceRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/rsocket/RSocketSecurityAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/rsocket/RSocketSecurityAutoConfiguration.java index f95790be6bf3..69235ed362c5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/rsocket/RSocketSecurityAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/rsocket/RSocketSecurityAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.rsocket.server.ServerRSocketFactoryProcessor; +import org.springframework.boot.rsocket.server.RSocketServerCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.rsocket.EnableRSocketSecurity; @@ -29,6 +29,7 @@ * server. * * @author Madhura Bhave + * @author Brian Clozel * @since 2.2.0 */ @Configuration(proxyBeanMethods = false) @@ -37,8 +38,8 @@ public class RSocketSecurityAutoConfiguration { @Bean - ServerRSocketFactoryProcessor springSecurityRSocketSecurity(SecuritySocketAcceptorInterceptor interceptor) { - return (factory) -> factory.addSocketAcceptorPlugin(interceptor); + RSocketServerCustomizer springSecurityRSocketSecurity(SecuritySocketAcceptorInterceptor interceptor) { + return (server) -> server.interceptors((registry) -> registry.forSocketAcceptor(interceptor)); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/RegistrationConfiguredCondition.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/RegistrationConfiguredCondition.java index 616767bb9dc7..2cc774ed1a59 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/RegistrationConfiguredCondition.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/RegistrationConfiguredCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.autoconfigure.security.saml2; import java.util.Collections; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/Saml2LoginConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/Saml2LoginConfiguration.java index b270cfbed771..8d8e6770d7cb 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/Saml2LoginConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/Saml2LoginConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,31 +17,28 @@ package org.springframework.boot.autoconfigure.security.saml2; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.security.ConditionalOnDefaultWebSecurity; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; +import org.springframework.security.web.SecurityFilterChain; /** - * {@link WebSecurityConfigurerAdapter} configuration for Spring Security's relying party - * SAML support. + * {@link SecurityFilterChain} configuration for Spring Security's relying party SAML + * support. * * @author Madhura Bhave */ @Configuration(proxyBeanMethods = false) +@ConditionalOnDefaultWebSecurity @ConditionalOnBean(RelyingPartyRegistrationRepository.class) class Saml2LoginConfiguration { - @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class) - static class Saml2LoginConfigurerAdapter extends WebSecurityConfigurerAdapter { - - @Override - protected void configure(HttpSecurity http) throws Exception { - http.authorizeRequests((requests) -> requests.anyRequest().authenticated()).saml2Login(); - } - + @Bean + SecurityFilterChain samlSecurityFilterChain(HttpSecurity http) throws Exception { + http.authorizeRequests((requests) -> requests.anyRequest().authenticated()).saml2Login(); + return http.build(); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyProperties.java index 6a445e2ec944..cd85b0ede8a1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.core.io.Resource; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; /** * SAML2 relying party properties. @@ -37,7 +38,7 @@ public class Saml2RelyingPartyProperties { /** * SAML2 relying party registrations. */ - private Map registration = new LinkedHashMap<>(); + private final Map registration = new LinkedHashMap<>(); public Map getRegistration() { return this.registration; @@ -48,26 +49,86 @@ public Map getRegistration() { */ public static class Registration { + /** + * Relying party's entity ID. The value may contain a number of placeholders. They + * are "baseUrl", "registrationId", "baseScheme", "baseHost", and "basePort". + */ + private String entityId = "{baseUrl}/saml2/service-provider-metadata/{registrationId}"; + + /** + * Assertion Consumer Service. + */ + private final Acs acs = new Acs(); + private final Signing signing = new Signing(); + private final Decryption decryption = new Decryption(); + /** * Remote SAML Identity Provider. */ - private Identityprovider identityprovider = new Identityprovider(); + private final Identityprovider identityprovider = new Identityprovider(); + + public String getEntityId() { + return this.entityId; + } + + public void setEntityId(String entityId) { + this.entityId = entityId; + } + + public Acs getAcs() { + return this.acs; + } public Signing getSigning() { return this.signing; } - Identityprovider getIdentityprovider() { + public Decryption getDecryption() { + return this.decryption; + } + + public Identityprovider getIdentityprovider() { return this.identityprovider; } + public static class Acs { + + /** + * Assertion Consumer Service location template. Can generate its location + * based on possible variables of "baseUrl", "registrationId", "baseScheme", + * "baseHost", and "basePort". + */ + private String location = "{baseUrl}/login/saml2/sso/{registrationId}"; + + /** + * Assertion Consumer Service binding. + */ + private Saml2MessageBinding binding = Saml2MessageBinding.POST; + + public String getLocation() { + return this.location; + } + + public void setLocation(String location) { + this.location = location; + } + + public Saml2MessageBinding getBinding() { + return this.binding; + } + + public void setBinding(Saml2MessageBinding binding) { + this.binding = binding; + } + + } + public static class Signing { /** - * Credentials used for signing and decrypting the SAML authentication - * request. + * Credentials used for signing the SAML authentication request. */ private List credentials = new ArrayList<>(); @@ -75,10 +136,14 @@ public List getCredentials() { return this.credentials; } + public void setCredentials(List credentials) { + this.credentials = credentials; + } + public static class Credential { /** - * Private key used for signing or decrypting. + * Private key used for signing. */ private Resource privateKeyLocation; @@ -109,6 +174,53 @@ public void setCertificateLocation(Resource certificate) { } + public static class Decryption { + + /** + * Credentials used for decrypting the SAML authentication request. + */ + private List credentials = new ArrayList<>(); + + public List getCredentials() { + return this.credentials; + } + + public void setCredentials(List credentials) { + this.credentials = credentials; + } + + public static class Credential { + + /** + * Private key used for decrypting. + */ + private Resource privateKeyLocation; + + /** + * Relying Party X509Certificate shared with the identity provider. + */ + private Resource certificateLocation; + + public Resource getPrivateKeyLocation() { + return this.privateKeyLocation; + } + + public void setPrivateKeyLocation(Resource privateKey) { + this.privateKeyLocation = privateKey; + } + + public Resource getCertificateLocation() { + return this.certificateLocation; + } + + public void setCertificateLocation(Resource certificate) { + this.certificateLocation = certificate; + } + + } + + } + /** * Represents a remote Identity Provider. */ @@ -120,11 +232,13 @@ public static class Identityprovider { private String entityId; /** - * Remote endpoint to send authentication requests to. + * URI to the metadata endpoint for discovery-based configuration. */ - private String ssoUrl; + private String metadataUri; - private Verification verification = new Verification(); + private final Singlesignon singlesignon = new Singlesignon(); + + private final Verification verification = new Verification(); public String getEntityId() { return this.entityId; @@ -134,18 +248,71 @@ public void setEntityId(String entityId) { this.entityId = entityId; } - public String getSsoUrl() { - return this.ssoUrl; + public String getMetadataUri() { + return this.metadataUri; + } + + public void setMetadataUri(String metadataUri) { + this.metadataUri = metadataUri; } - public void setSsoUrl(String ssoUrl) { - this.ssoUrl = ssoUrl; + public Singlesignon getSinglesignon() { + return this.singlesignon; } public Verification getVerification() { return this.verification; } + /** + * Single sign on details for an Identity Provider. + */ + public static class Singlesignon { + + /** + * Remote endpoint to send authentication requests to. + */ + private String url; + + /** + * Whether to redirect or post authentication requests. + */ + private Saml2MessageBinding binding; + + /** + * Whether to sign authentication requests. + */ + private boolean signRequest = true; + + public String getUrl() { + return this.url; + } + + public void setUrl(String url) { + this.url = url; + } + + public Saml2MessageBinding getBinding() { + return this.binding; + } + + public void setBinding(Saml2MessageBinding binding) { + this.binding = binding; + } + + public boolean isSignRequest() { + return this.signRequest; + } + + public void setSignRequest(boolean signRequest) { + this.signRequest = signRequest; + } + + } + + /** + * Verification details for an Identity Provider. + */ public static class Verification { /** @@ -157,6 +324,10 @@ public List getCredentials() { return this.credentials; } + public void setCredentials(List credentials) { + this.credentials = credentials; + } + public static class Credential { /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyRegistrationConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyRegistrationConfiguration.java index a514c2ad36c0..f795bae223c5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyRegistrationConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyRegistrationConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,27 +20,32 @@ import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.security.interfaces.RSAPrivateKey; -import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import java.util.stream.Collectors; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyProperties.Decryption; import org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyProperties.Identityprovider.Verification; import org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyProperties.Registration; import org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyProperties.Registration.Signing; +import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.Resource; import org.springframework.security.converter.RsaKeyConverters; -import org.springframework.security.saml2.credentials.Saml2X509Credential; -import org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType; import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration.AssertingPartyDetails; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration.Builder; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; -import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrations; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * {@link Configuration @Configuration} used to map {@link Saml2RelyingPartyProperties} to @@ -66,39 +71,69 @@ private RelyingPartyRegistration asRegistration(Map.Entry } private RelyingPartyRegistration asRegistration(String id, Registration properties) { - RelyingPartyRegistration.Builder builder = RelyingPartyRegistration.withRegistrationId(id); - builder.assertionConsumerServiceUrlTemplate( - "{baseUrl}" + Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI); - builder.idpWebSsoUrl(properties.getIdentityprovider().getSsoUrl()); - builder.remoteIdpEntityId(properties.getIdentityprovider().getEntityId()); - builder.credentials((credentials) -> credentials.addAll(asCredentials(properties))); - return builder.build(); + boolean usingMetadata = StringUtils.hasText(properties.getIdentityprovider().getMetadataUri()); + Builder builder = (usingMetadata) ? RelyingPartyRegistrations + .fromMetadataLocation(properties.getIdentityprovider().getMetadataUri()).registrationId(id) + : RelyingPartyRegistration.withRegistrationId(id); + builder.assertionConsumerServiceLocation(properties.getAcs().getLocation()); + builder.assertionConsumerServiceBinding(properties.getAcs().getBinding()); + builder.assertingPartyDetails(mapIdentityProvider(properties, usingMetadata)); + builder.signingX509Credentials((credentials) -> properties.getSigning().getCredentials().stream() + .map(this::asSigningCredential).forEach(credentials::add)); + builder.decryptionX509Credentials((credentials) -> properties.getDecryption().getCredentials().stream() + .map(this::asDecryptionCredential).forEach(credentials::add)); + builder.assertingPartyDetails((details) -> details + .verificationX509Credentials((credentials) -> properties.getIdentityprovider().getVerification() + .getCredentials().stream().map(this::asVerificationCredential).forEach(credentials::add))); + builder.entityId(properties.getEntityId()); + RelyingPartyRegistration registration = builder.build(); + boolean signRequest = registration.getAssertingPartyDetails().getWantAuthnRequestsSigned(); + validateSigningCredentials(properties, signRequest); + return registration; } - private List asCredentials(Registration properties) { - List credentials = new ArrayList<>(); - properties.getSigning().getCredentials().stream().map(this::asSigningCredential).forEach(credentials::add); - properties.getIdentityprovider().getVerification().getCredentials().stream().map(this::asVerificationCredential) - .forEach(credentials::add); - return credentials; + private Consumer mapIdentityProvider(Registration properties, + boolean usingMetadata) { + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + Saml2RelyingPartyProperties.Identityprovider identityprovider = properties.getIdentityprovider(); + return (details) -> { + map.from(identityprovider::getEntityId).to(details::entityId); + map.from(identityprovider.getSinglesignon()::getBinding).whenNonNull() + .to(details::singleSignOnServiceBinding); + map.from(identityprovider.getSinglesignon()::getUrl).to(details::singleSignOnServiceLocation); + map.from(identityprovider.getSinglesignon()::isSignRequest).when((signRequest) -> !usingMetadata) + .to(details::wantAuthnRequestsSigned); + }; + } + + private void validateSigningCredentials(Registration properties, boolean signRequest) { + if (signRequest) { + Assert.state(!properties.getSigning().getCredentials().isEmpty(), + "Signing credentials must not be empty when authentication requests require signing."); + } } private Saml2X509Credential asSigningCredential(Signing.Credential properties) { RSAPrivateKey privateKey = readPrivateKey(properties.getPrivateKeyLocation()); X509Certificate certificate = readCertificate(properties.getCertificateLocation()); - return new Saml2X509Credential(privateKey, certificate, Saml2X509CredentialType.SIGNING, - Saml2X509CredentialType.DECRYPTION); + return new Saml2X509Credential(privateKey, certificate, Saml2X509CredentialType.SIGNING); + } + + private Saml2X509Credential asDecryptionCredential(Decryption.Credential properties) { + RSAPrivateKey privateKey = readPrivateKey(properties.getPrivateKeyLocation()); + X509Certificate certificate = readCertificate(properties.getCertificateLocation()); + return new Saml2X509Credential(privateKey, certificate, Saml2X509CredentialType.DECRYPTION); } private Saml2X509Credential asVerificationCredential(Verification.Credential properties) { X509Certificate certificate = readCertificate(properties.getCertificateLocation()); - return new Saml2X509Credential(certificate, Saml2X509CredentialType.ENCRYPTION, - Saml2X509CredentialType.VERIFICATION); + return new Saml2X509Credential(certificate, Saml2X509Credential.Saml2X509CredentialType.ENCRYPTION, + Saml2X509Credential.Saml2X509CredentialType.VERIFICATION); } private RSAPrivateKey readPrivateKey(Resource location) { Assert.state(location != null, "No private key location specified"); - Assert.state(location.exists(), "Private key location '" + location + "' does not exist"); + Assert.state(location.exists(), () -> "Private key location '" + location + "' does not exist"); try (InputStream inputStream = location.getInputStream()) { return RsaKeyConverters.pkcs8().convert(inputStream); } @@ -109,7 +144,7 @@ private RSAPrivateKey readPrivateKey(Resource location) { private X509Certificate readCertificate(Resource location) { Assert.state(location != null, "No certificate location specified"); - Assert.state(location.exists(), "Certificate location '" + location + "' does not exist"); + Assert.state(location.exists(), () -> "Certificate location '" + location + "' does not exist"); try (InputStream inputStream = location.getInputStream()) { return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(inputStream); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/JerseyRequestMatcherProvider.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/JerseyRequestMatcherProvider.java deleted file mode 100644 index c25210b6cc00..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/JerseyRequestMatcherProvider.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.boot.autoconfigure.security.servlet; - -import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import org.springframework.security.web.util.matcher.RequestMatcher; - -/** - * {@link RequestMatcherProvider} that provides an {@link AntPathRequestMatcher} that can - * be used for Jersey applications. - * - * @author Madhura Bhave - * @since 2.0.7 - * @deprecated since 2.1.8 in favor of {@link AntPathRequestMatcher} - */ -@Deprecated -public class JerseyRequestMatcherProvider implements RequestMatcherProvider { - - private final JerseyApplicationPath jerseyApplicationPath; - - public JerseyRequestMatcherProvider(JerseyApplicationPath jerseyApplicationPath) { - this.jerseyApplicationPath = jerseyApplicationPath; - } - - @Override - public RequestMatcher getRequestMatcher(String pattern) { - return new AntPathRequestMatcher(this.jerseyApplicationPath.getRelativePath(pattern)); - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/RequestMatcherProvider.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/RequestMatcherProvider.java index 003aee23fe0e..39c20aeb1534 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/RequestMatcherProvider.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/RequestMatcherProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.autoconfigure.security.servlet; import org.springframework.security.web.util.matcher.RequestMatcher; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/SecurityFilterAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/SecurityFilterAutoConfiguration.java index 1c61d414576f..1e8228b9ba03 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/SecurityFilterAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/SecurityFilterAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -73,7 +73,7 @@ private EnumSet getDispatcherTypes(SecurityProperties securityPr } return securityProperties.getFilter().getDispatcherTypes().stream() .map((type) -> DispatcherType.valueOf(type.name())) - .collect(Collectors.collectingAndThen(Collectors.toSet(), EnumSet::copyOf)); + .collect(Collectors.toCollection(() -> EnumSet.noneOf(DispatcherType.class))); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/SpringBootWebSecurityConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/SpringBootWebSecurityConfiguration.java index 1432bac73adb..a127a94113d3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/SpringBootWebSecurityConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/SpringBootWebSecurityConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,35 +16,37 @@ package org.springframework.boot.autoconfigure.security.servlet; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; +import org.springframework.boot.autoconfigure.security.ConditionalOnDefaultWebSecurity; import org.springframework.boot.autoconfigure.security.SecurityProperties; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.web.SecurityFilterChain; /** * The default configuration for web security. It relies on Spring Security's * content-negotiation strategy to determine what sort of authentication to use. If the - * user specifies their own {@link WebSecurityConfigurerAdapter}, this will back-off - * completely and the users should specify all the bits that they want to configure as - * part of the custom security configuration. + * user specifies their own {@link WebSecurityConfigurerAdapter} or + * {@link SecurityFilterChain} bean, this will back-off completely and the users should + * specify all the bits that they want to configure as part of the custom security + * configuration. * * @author Madhura Bhave - * @since 2.0.0 */ @Configuration(proxyBeanMethods = false) -@ConditionalOnClass(WebSecurityConfigurerAdapter.class) -@ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class) +@ConditionalOnDefaultWebSecurity @ConditionalOnWebApplication(type = Type.SERVLET) -public class SpringBootWebSecurityConfiguration { +class SpringBootWebSecurityConfiguration { - @Configuration(proxyBeanMethods = false) + @Bean @Order(SecurityProperties.BASIC_AUTH_ORDER) - static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter { - + SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { + http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic(); + return http.build(); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfiguration.java index 91d2d128210d..683da7f82780 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -58,7 +58,8 @@ @ConditionalOnMissingBean( value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class }, type = { "org.springframework.security.oauth2.jwt.JwtDecoder", - "org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector" }) + "org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector", + "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository" }) public class UserDetailsServiceAutoConfiguration { private static final String NOOP_PASSWORD_PREFIX = "{noop}"; @@ -68,8 +69,6 @@ public class UserDetailsServiceAutoConfiguration { private static final Log logger = LogFactory.getLog(UserDetailsServiceAutoConfiguration.class); @Bean - @ConditionalOnMissingBean( - type = "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository") @Lazy public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties, ObjectProvider passwordEncoder) { @@ -83,7 +82,11 @@ public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder encoder) { String password = user.getPassword(); if (user.isPasswordGenerated()) { - logger.info(String.format("%n%nUsing generated security password: %s%n", user.getPassword())); + logger.warn(String.format( + "%n%nUsing generated security password: %s%n%nThis generated password is for development use only. " + + "Your security configuration must be updated before running your application in " + + "production.%n", + user.getPassword())); } if (encoder != null || PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()) { return password; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/WebSecurityEnablerConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/WebSecurityEnablerConfiguration.java index ae826ca9f132..c78de091844b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/WebSecurityEnablerConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/WebSecurityEnablerConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,31 +16,28 @@ package org.springframework.boot.autoconfigure.security.servlet; -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.ConditionalOnWebApplication; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.BeanIds; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; /** - * If there is a bean of type WebSecurityConfigurerAdapter, this adds the - * {@link EnableWebSecurity @EnableWebSecurity} annotation. This will make sure that the - * annotation is present with default security auto-configuration and also if the user - * adds custom security and forgets to add the annotation. If - * {@link EnableWebSecurity @EnableWebSecurity} has already been added or if a bean with - * name {@value BeanIds#SPRING_SECURITY_FILTER_CHAIN} has been configured by the user, - * this will back-off. + * Adds the{@link EnableWebSecurity @EnableWebSecurity} annotation if Spring Security is + * on the classpath. This will make sure that the annotation is present with default + * security auto-configuration and also if the user adds custom security and forgets to + * add the annotation. If {@link EnableWebSecurity @EnableWebSecurity} has already been + * added or if a bean with name {@value BeanIds#SPRING_SECURITY_FILTER_CHAIN} has been + * configured by the user, this will back-off. * * @author Madhura Bhave - * @since 2.0.0 */ @Configuration(proxyBeanMethods = false) -@ConditionalOnBean(WebSecurityConfigurerAdapter.class) @ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN) +@ConditionalOnClass(EnableWebSecurity.class) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) @EnableWebSecurity -public class WebSecurityEnablerConfiguration { +class WebSecurityEnablerConfiguration { } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/AbstractSessionCondition.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/AbstractSessionCondition.java index ac072ced3a13..52cc4945198b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/AbstractSessionCondition.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/AbstractSessionCondition.java @@ -58,7 +58,8 @@ public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeM return binder.bind("spring.session.store-type", StoreType.class) .map((t) -> new ConditionOutcome(t == required, message.found("spring.session.store-type property").items(t))) - .orElse(ConditionOutcome.noMatch(message.didNotFind("spring.session.store-type property").atAll())); + .orElseGet(() -> ConditionOutcome + .noMatch(message.didNotFind("spring.session.store-type property").atAll())); } catch (BindException ex) { return ConditionOutcome.noMatch(message.found("invalid spring.session.store-type property").atAll()); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/DefaultCookieSerializerCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/DefaultCookieSerializerCustomizer.java new file mode 100644 index 000000000000..197d81cc9e8d --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/DefaultCookieSerializerCustomizer.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.session; + +import org.springframework.session.web.http.DefaultCookieSerializer; + +/** + * Callback interface that can be implemented by beans wishing to customize the + * {@link DefaultCookieSerializer} configuration. + * + * @author Vedran Pavic + * @since 2.3.0 + */ +@FunctionalInterface +public interface DefaultCookieSerializerCustomizer { + + /** + * Customize the cookie serializer. + * @param cookieSerializer the {@code DefaultCookieSerializer} to customize + */ + void customize(DefaultCookieSerializer cookieSerializer); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/HazelcastSessionConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/HazelcastSessionConfiguration.java index ea0493aca7ba..15fbb27b0d40 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/HazelcastSessionConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/HazelcastSessionConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ 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.web.ServerProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; @@ -47,13 +48,14 @@ @EnableConfigurationProperties(HazelcastSessionProperties.class) class HazelcastSessionConfiguration { - @Configuration + @Configuration(proxyBeanMethods = false) public static class SpringBootHazelcastHttpSessionConfiguration extends HazelcastHttpSessionConfiguration { @Autowired public void customize(SessionProperties sessionProperties, - HazelcastSessionProperties hazelcastSessionProperties) { - Duration timeout = sessionProperties.getTimeout(); + HazelcastSessionProperties hazelcastSessionProperties, ServerProperties serverProperties) { + Duration timeout = sessionProperties + .determineTimeout(() -> serverProperties.getServlet().getSession().getTimeout()); if (timeout != null) { setMaxInactiveIntervalInSeconds((int) timeout.getSeconds()); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/JdbcIndexedSessionRepositoryDependsOnDatabaseInitializationDetector.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/JdbcIndexedSessionRepositoryDependsOnDatabaseInitializationDetector.java new file mode 100644 index 000000000000..c227a5849db1 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/JdbcIndexedSessionRepositoryDependsOnDatabaseInitializationDetector.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.session; + +import java.util.Collections; +import java.util.Set; + +import org.springframework.boot.sql.init.dependency.AbstractBeansOfTypeDependsOnDatabaseInitializationDetector; +import org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitializationDetector; +import org.springframework.session.jdbc.JdbcIndexedSessionRepository; + +/** + * + * {@link DependsOnDatabaseInitializationDetector} for + * {@link JdbcIndexedSessionRepository}. + * + * @author Andy Wilkinson + */ +class JdbcIndexedSessionRepositoryDependsOnDatabaseInitializationDetector + extends AbstractBeansOfTypeDependsOnDatabaseInitializationDetector { + + @Override + protected Set> getDependsOnDatabaseInitializationBeanTypes() { + return Collections.singleton(JdbcIndexedSessionRepository.class); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/JdbcSessionConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/JdbcSessionConfiguration.java index 855d57ca0d0d..934b85e76c9f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/JdbcSessionConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/JdbcSessionConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,18 +20,23 @@ import javax.sql.DataSource; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; 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.web.ServerProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.sql.init.dependency.DatabaseInitializationDependencyConfigurer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import org.springframework.core.io.ResourceLoader; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.session.SessionRepository; import org.springframework.session.jdbc.JdbcIndexedSessionRepository; +import org.springframework.session.jdbc.config.annotation.SpringSessionDataSource; import org.springframework.session.jdbc.config.annotation.web.http.JdbcHttpSessionConfiguration; /** @@ -47,21 +52,26 @@ @ConditionalOnBean(DataSource.class) @Conditional(ServletSessionCondition.class) @EnableConfigurationProperties(JdbcSessionProperties.class) +@Import(DatabaseInitializationDependencyConfigurer.class) class JdbcSessionConfiguration { @Bean @ConditionalOnMissingBean - JdbcSessionDataSourceInitializer jdbcSessionDataSourceInitializer(DataSource dataSource, - ResourceLoader resourceLoader, JdbcSessionProperties properties) { - return new JdbcSessionDataSourceInitializer(dataSource, resourceLoader, properties); + JdbcSessionDataSourceInitializer jdbcSessionDataSourceInitializer( + @SpringSessionDataSource ObjectProvider sessionDataSource, + ObjectProvider dataSource, ResourceLoader resourceLoader, JdbcSessionProperties properties) { + return new JdbcSessionDataSourceInitializer(sessionDataSource.getIfAvailable(dataSource::getObject), + resourceLoader, properties); } - @Configuration + @Configuration(proxyBeanMethods = false) static class SpringBootJdbcHttpSessionConfiguration extends JdbcHttpSessionConfiguration { @Autowired - void customize(SessionProperties sessionProperties, JdbcSessionProperties jdbcSessionProperties) { - Duration timeout = sessionProperties.getTimeout(); + void customize(SessionProperties sessionProperties, JdbcSessionProperties jdbcSessionProperties, + ServerProperties serverProperties) { + Duration timeout = sessionProperties + .determineTimeout(() -> serverProperties.getServlet().getSession().getTimeout()); if (timeout != null) { setMaxInactiveIntervalInSeconds((int) timeout.getSeconds()); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/JdbcSessionDataSourceInitializer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/JdbcSessionDataSourceInitializer.java index 4dd4c9f2d8d0..ea36a2865f1b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/JdbcSessionDataSourceInitializer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/JdbcSessionDataSourceInitializer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import org.springframework.boot.jdbc.DataSourceInitializationMode; import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * Initializer for Spring Session schema. @@ -50,4 +51,13 @@ protected String getSchemaLocation() { return this.properties.getSchema(); } + @Override + protected String getDatabaseName() { + String platform = this.properties.getPlatform(); + if (StringUtils.hasText(platform)) { + return platform; + } + return super.getDatabaseName(); + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/JdbcSessionProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/JdbcSessionProperties.java index 4a0dee835a65..215b3881eca7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/JdbcSessionProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/JdbcSessionProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,6 +42,12 @@ public class JdbcSessionProperties { */ private String schema = DEFAULT_SCHEMA_LOCATION; + /** + * Platform to use in initialization scripts if the @@platform@@ placeholder is used. + * Auto-detected by default. + */ + private String platform; + /** * Name of the database table used to store sessions. */ @@ -77,6 +83,14 @@ public void setSchema(String schema) { this.schema = schema; } + public String getPlatform() { + return this.platform; + } + + public void setPlatform(String platform) { + this.platform = platform; + } + public String getTableName() { return this.tableName; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/MongoReactiveSessionConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/MongoReactiveSessionConfiguration.java index 8522edadff07..73ecbf0119f1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/MongoReactiveSessionConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/MongoReactiveSessionConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,7 +43,7 @@ @EnableConfigurationProperties(MongoSessionProperties.class) class MongoReactiveSessionConfiguration { - @Configuration + @Configuration(proxyBeanMethods = false) static class SpringBootReactiveMongoWebSessionConfiguration extends ReactiveMongoWebSessionConfiguration { @Autowired diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/MongoSessionConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/MongoSessionConfiguration.java index 2d2419cfac4f..2118bdbdc666 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/MongoSessionConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/MongoSessionConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ 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.web.ServerProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; @@ -44,12 +45,14 @@ @EnableConfigurationProperties(MongoSessionProperties.class) class MongoSessionConfiguration { - @Configuration + @Configuration(proxyBeanMethods = false) public static class SpringBootMongoHttpSessionConfiguration extends MongoHttpSessionConfiguration { @Autowired - public void customize(SessionProperties sessionProperties, MongoSessionProperties mongoSessionProperties) { - Duration timeout = sessionProperties.getTimeout(); + public void customize(SessionProperties sessionProperties, MongoSessionProperties mongoSessionProperties, + ServerProperties serverProperties) { + Duration timeout = sessionProperties + .determineTimeout(() -> serverProperties.getServlet().getSession().getTimeout()); if (timeout != null) { setMaxInactiveIntervalInSeconds((int) timeout.getSeconds()); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/NonUniqueSessionRepositoryFailureAnalyzer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/NonUniqueSessionRepositoryFailureAnalyzer.java index d0c31438b643..3cbb89f57b29 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/NonUniqueSessionRepositoryFailureAnalyzer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/NonUniqueSessionRepositoryFailureAnalyzer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,7 +37,7 @@ protected FailureAnalysis analyze(Throwable rootFailure, NonUniqueSessionReposit StringBuilder action = new StringBuilder(); action.append(String.format("Consider any of the following:%n")); action.append( - String.format(" - Define the `spring.session.store-type` property to the store you want to use%n")); + String.format(" - Define the 'spring.session.store-type' property to the store you want to use%n")); action.append(String.format(" - Review your classpath and remove the unwanted store implementation(s)%n")); return new FailureAnalysis(message.toString(), action.toString(), cause); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/RedisReactiveSessionConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/RedisReactiveSessionConfiguration.java index 31be3bd28e8c..15edb73b3b88 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/RedisReactiveSessionConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/RedisReactiveSessionConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,7 +43,7 @@ @EnableConfigurationProperties(RedisSessionProperties.class) class RedisReactiveSessionConfiguration { - @Configuration + @Configuration(proxyBeanMethods = false) static class SpringBootRedisWebSessionConfiguration extends RedisWebSessionConfiguration { @Autowired diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/RedisSessionConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/RedisSessionConfiguration.java index 9f73dbc73a03..e31c794c8587 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/RedisSessionConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/RedisSessionConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ 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.web.ServerProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; @@ -64,12 +65,14 @@ ConfigureRedisAction configureRedisAction(RedisSessionProperties redisSessionPro "Unsupported redis configure action '" + redisSessionProperties.getConfigureAction() + "'."); } - @Configuration + @Configuration(proxyBeanMethods = false) public static class SpringBootRedisHttpSessionConfiguration extends RedisHttpSessionConfiguration { @Autowired - public void customize(SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties) { - Duration timeout = sessionProperties.getTimeout(); + public void customize(SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties, + ServerProperties serverProperties) { + Duration timeout = sessionProperties + .determineTimeout(() -> serverProperties.getServlet().getSession().getTimeout()); if (timeout != null) { setMaxInactiveIntervalInSeconds((int) timeout.getSeconds()); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionAutoConfiguration.java index ea9e253b42fd..8824b65cefe8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,8 +21,7 @@ import java.util.List; import java.util.Locale; -import javax.annotation.PostConstruct; - +import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.WebApplicationType; import org.springframework.boot.autoconfigure.AutoConfigureAfter; @@ -43,6 +42,7 @@ import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration; +import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.web.servlet.server.Session.Cookie; @@ -53,6 +53,7 @@ import org.springframework.context.annotation.Import; import org.springframework.context.annotation.ImportSelector; import org.springframework.core.type.AnnotationMetadata; +import org.springframework.security.web.authentication.RememberMeServices; import org.springframework.session.ReactiveSessionRepository; import org.springframework.session.Session; import org.springframework.session.SessionRepository; @@ -61,7 +62,6 @@ import org.springframework.session.web.http.CookieSerializer; import org.springframework.session.web.http.DefaultCookieSerializer; import org.springframework.session.web.http.HttpSessionIdResolver; -import org.springframework.util.ClassUtils; /** * {@link EnableAutoConfiguration Auto-configuration} for Spring Session. @@ -80,11 +80,9 @@ @AutoConfigureAfter({ DataSourceAutoConfiguration.class, HazelcastAutoConfiguration.class, JdbcTemplateAutoConfiguration.class, MongoDataAutoConfiguration.class, MongoReactiveDataAutoConfiguration.class, RedisAutoConfiguration.class, RedisReactiveAutoConfiguration.class }) -@AutoConfigureBefore(HttpHandlerAutoConfiguration.class) +@AutoConfigureBefore({ HttpHandlerAutoConfiguration.class, WebFluxAutoConfiguration.class }) public class SessionAutoConfiguration { - private static final String REMEMBER_ME_SERVICES_CLASS = "org.springframework.security.web.authentication.RememberMeServices"; - @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) @Import({ ServletSessionRepositoryValidator.class, SessionRepositoryFilterConfiguration.class }) @@ -92,7 +90,8 @@ static class ServletSessionConfiguration { @Bean @Conditional(DefaultCookieSerializerCondition.class) - DefaultCookieSerializer cookieSerializer(ServerProperties serverProperties) { + DefaultCookieSerializer cookieSerializer(ServerProperties serverProperties, + ObjectProvider cookieSerializerCustomizers) { Cookie cookie = serverProperties.getServlet().getSession().getCookie(); DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer(); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); @@ -102,12 +101,22 @@ DefaultCookieSerializer cookieSerializer(ServerProperties serverProperties) { map.from(cookie::getHttpOnly).to(cookieSerializer::setUseHttpOnlyCookie); map.from(cookie::getSecure).to(cookieSerializer::setUseSecureCookie); map.from(cookie::getMaxAge).to((maxAge) -> cookieSerializer.setCookieMaxAge((int) maxAge.getSeconds())); - if (ClassUtils.isPresent(REMEMBER_ME_SERVICES_CLASS, getClass().getClassLoader())) { - new RememberMeServicesCookieSerializerCustomizer().apply(cookieSerializer); - } + cookieSerializerCustomizers.orderedStream().forEach((customizer) -> customizer.customize(cookieSerializer)); return cookieSerializer; } + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(RememberMeServices.class) + static class RememberMeServicesConfiguration { + + @Bean + DefaultCookieSerializerCustomizer rememberMeServicesCookieSerializerCustomizer() { + return (cookieSerializer) -> cookieSerializer + .setRememberMeRequestAttribute(SpringSessionRememberMeServices.REMEMBER_ME_LOGIN_ATTR); + } + + } + @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(SessionRepository.class) @Import({ ServletSessionRepositoryImplementationValidator.class, @@ -133,18 +142,6 @@ static class ReactiveSessionRepositoryConfiguration { } - /** - * Customization for {@link SpringSessionRememberMeServices} that is only instantiated - * when Spring Security is on the classpath. - */ - static class RememberMeServicesCookieSerializerCustomizer { - - void apply(DefaultCookieSerializer cookieSerializer) { - cookieSerializer.setRememberMeRequestAttribute(SpringSessionRememberMeServices.REMEMBER_ME_LOGIN_ATTR); - } - - } - /** * Condition to trigger the creation of a {@link DefaultCookieSerializer}. This kicks * in if either no {@link HttpSessionIdResolver} and {@link CookieSerializer} beans @@ -226,10 +223,10 @@ abstract static class AbstractSessionRepositoryImplementationValidator { this.classLoader = applicationContext.getClassLoader(); this.sessionProperties = sessionProperties; this.candidates = candidates; + checkAvailableImplementations(); } - @PostConstruct - void checkAvailableImplementations() { + private void checkAvailableImplementations() { List> availableCandidates = new ArrayList<>(); for (String candidate : this.candidates) { addCandidateIfAvailable(availableCandidates, candidate); @@ -242,7 +239,7 @@ void checkAvailableImplementations() { private void addCandidateIfAvailable(List> candidates, String type) { try { - Class candidate = this.classLoader.loadClass(type); + Class candidate = Class.forName(type, false, this.classLoader); if (candidate != null) { candidates.add(candidate); } @@ -291,7 +288,7 @@ static class ReactiveSessionRepositoryImplementationValidator /** * Base class for validating that a (reactive) session repository bean exists. */ - abstract static class AbstractSessionRepositoryValidator { + abstract static class AbstractSessionRepositoryValidator implements InitializingBean { private final SessionProperties sessionProperties; @@ -303,8 +300,8 @@ protected AbstractSessionRepositoryValidator(SessionProperties sessionProperties this.sessionRepositoryProvider = sessionRepositoryProvider; } - @PostConstruct - void checkSessionRepository() { + @Override + public void afterPropertiesSet() { StoreType storeType = this.sessionProperties.getStoreType(); if (storeType != StoreType.NONE && this.sessionRepositoryProvider.getIfAvailable() == null && storeType != null) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionProperties.java index d35986f4ae8b..2e9f4d3378dd 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,16 +21,11 @@ import java.util.Arrays; import java.util.HashSet; import java.util.Set; +import java.util.function.Supplier; -import javax.annotation.PostConstruct; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.convert.DurationUnit; import org.springframework.boot.web.servlet.DispatcherType; -import org.springframework.boot.web.servlet.server.Session; import org.springframework.session.web.http.SessionRepositoryFilter; /** @@ -57,20 +52,6 @@ public class SessionProperties { private Servlet servlet = new Servlet(); - private ServerProperties serverProperties; - - @Autowired - void setServerProperties(ObjectProvider serverProperties) { - this.serverProperties = serverProperties.getIfUnique(); - } - - @PostConstruct - public void checkSessionTimeout() { - if (this.timeout == null && this.serverProperties != null) { - this.timeout = this.serverProperties.getServlet().getSession().getTimeout(); - } - } - public StoreType getStoreType() { return this.storeType; } @@ -79,11 +60,6 @@ public void setStoreType(StoreType storeType) { this.storeType = storeType; } - /** - * Return the session timeout. - * @return the session timeout - * @see Session#getTimeout() - */ public Duration getTimeout() { return this.timeout; } @@ -100,6 +76,17 @@ public void setServlet(Servlet servlet) { this.servlet = servlet; } + /** + * Determine the session timeout. If no timeout is configured, the + * {@code fallbackTimeout} is used. + * @param fallbackTimeout a fallback timeout value if the timeout isn't configured + * @return the session timeout + * @since 2.4.0 + */ + public Duration determineTimeout(Supplier fallbackTimeout) { + return (this.timeout != null) ? this.timeout : fallbackTimeout.get(); + } + /** * Servlet-related properties. */ diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionRepositoryFilterConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionRepositoryFilterConfiguration.java index a5726ebe750d..973a81d411c5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionRepositoryFilterConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionRepositoryFilterConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -53,7 +53,7 @@ private EnumSet getDispatcherTypes(SessionProperties sessionProp return null; } return servletProperties.getFilterDispatcherTypes().stream().map((type) -> DispatcherType.valueOf(type.name())) - .collect(Collectors.collectingAndThen(Collectors.toSet(), EnumSet::copyOf)); + .collect(Collectors.toCollection(() -> EnumSet.noneOf(DispatcherType.class))); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/sql/init/DataSourceInitializationConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/sql/init/DataSourceInitializationConfiguration.java new file mode 100644 index 000000000000..b591991cb5dc --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/sql/init/DataSourceInitializationConfiguration.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.sql.init; + +import javax.sql.DataSource; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer; +import org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer; +import org.springframework.boot.sql.init.DatabaseInitializationSettings; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.datasource.SimpleDriverDataSource; +import org.springframework.jdbc.datasource.init.DatabasePopulator; +import org.springframework.util.StringUtils; + +@Configuration(proxyBeanMethods = false) +@ConditionalOnMissingBean(AbstractScriptDatabaseInitializer.class) +@ConditionalOnSingleCandidate(DataSource.class) +@ConditionalOnClass(DatabasePopulator.class) +class DataSourceInitializationConfiguration { + + @Bean + DataSourceScriptDatabaseInitializer dataSourceScriptDatabaseInitializer(DataSource dataSource, + SqlInitializationProperties initializationProperties) { + DatabaseInitializationSettings settings = SettingsCreator.createFrom(initializationProperties); + return new DataSourceScriptDatabaseInitializer(determineDataSource(dataSource, + initializationProperties.getUsername(), initializationProperties.getPassword()), settings); + } + + private static DataSource determineDataSource(DataSource dataSource, String username, String password) { + if (StringUtils.hasText(username) && StringUtils.hasText(password)) { + return DataSourceBuilder.derivedFrom(dataSource).username(username).password(password) + .type(SimpleDriverDataSource.class).build(); + } + return dataSource; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/sql/init/R2dbcInitializationConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/sql/init/R2dbcInitializationConfiguration.java new file mode 100644 index 000000000000..cff498adaf02 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/sql/init/R2dbcInitializationConfiguration.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.sql.init; + +import io.r2dbc.spi.ConnectionFactory; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.boot.r2dbc.ConnectionFactoryBuilder; +import org.springframework.boot.r2dbc.init.R2dbcScriptDatabaseInitializer; +import org.springframework.boot.sql.init.DatabaseInitializationSettings; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.r2dbc.connection.init.DatabasePopulator; +import org.springframework.util.StringUtils; + +/** + * Configuration for initializing an SQL database accessed via an R2DBC + * {@link ConnectionFactory}. + * + * @author Andy Wilkinson + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass({ ConnectionFactory.class, DatabasePopulator.class }) +@ConditionalOnSingleCandidate(ConnectionFactory.class) +class R2dbcInitializationConfiguration { + + @Bean + R2dbcScriptDatabaseInitializer r2dbcScriptDatabaseInitializer(ConnectionFactory connectionFactory, + SqlInitializationProperties properties) { + DatabaseInitializationSettings settings = SettingsCreator.createFrom(properties); + return new R2dbcScriptDatabaseInitializer( + determineConnectionFactory(connectionFactory, properties.getUsername(), properties.getPassword()), + settings); + } + + private static ConnectionFactory determineConnectionFactory(ConnectionFactory connectionFactory, String username, + String password) { + if (StringUtils.hasText(username) && StringUtils.hasText(password)) { + return ConnectionFactoryBuilder.derivedFrom(connectionFactory).username(username).password(password) + .build(); + } + return connectionFactory; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/sql/init/SettingsCreator.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/sql/init/SettingsCreator.java new file mode 100644 index 000000000000..c4e276de4dfb --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/sql/init/SettingsCreator.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.sql.init; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.boot.sql.init.DatabaseInitializationSettings; + +/** + * Helpers class for creating {@link DatabaseInitializationSettings} from + * {@link SqlInitializationProperties}. + * + * @author Andy Wilkinson + */ +final class SettingsCreator { + + private SettingsCreator() { + + } + + static DatabaseInitializationSettings createFrom(SqlInitializationProperties properties) { + DatabaseInitializationSettings settings = new DatabaseInitializationSettings(); + settings.setSchemaLocations( + scriptLocations(properties.getSchemaLocations(), "schema", properties.getPlatform())); + settings.setDataLocations(scriptLocations(properties.getDataLocations(), "data", properties.getPlatform())); + settings.setContinueOnError(properties.isContinueOnError()); + settings.setSeparator(properties.getSeparator()); + settings.setEncoding(properties.getEncoding()); + settings.setMode(properties.getMode()); + return settings; + } + + private static List scriptLocations(List locations, String fallback, String platform) { + if (locations != null) { + return locations; + } + List fallbackLocations = new ArrayList<>(); + fallbackLocations.add("optional:classpath*:" + fallback + "-" + platform + ".sql"); + fallbackLocations.add("optional:classpath*:" + fallback + ".sql"); + return fallbackLocations; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/sql/init/SqlInitializationAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/sql/init/SqlInitializationAutoConfiguration.java new file mode 100644 index 000000000000..6ec161671a51 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/sql/init/SqlInitializationAutoConfiguration.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.sql.init; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.NoneNestedConditions; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; +import org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration.SqlInitializationModeCondition; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer; +import org.springframework.boot.sql.init.dependency.DatabaseInitializationDependencyConfigurer; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for initializing an SQL database. + * + * @author Andy Wilkinson + * @since 2.5.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnMissingBean(AbstractScriptDatabaseInitializer.class) +@AutoConfigureAfter({ R2dbcAutoConfiguration.class, DataSourceAutoConfiguration.class }) +@EnableConfigurationProperties(SqlInitializationProperties.class) +@Import({ DatabaseInitializationDependencyConfigurer.class, R2dbcInitializationConfiguration.class, + DataSourceInitializationConfiguration.class }) +@ConditionalOnProperty(prefix = "spring.sql.init", name = "enabled", matchIfMissing = true) +@Conditional(SqlInitializationModeCondition.class) +public class SqlInitializationAutoConfiguration { + + static class SqlInitializationModeCondition extends NoneNestedConditions { + + SqlInitializationModeCondition() { + super(ConfigurationPhase.PARSE_CONFIGURATION); + } + + @ConditionalOnProperty(prefix = "spring.sql.init", name = "mode", havingValue = "never") + static class ModeIsNever { + + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/sql/init/SqlInitializationProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/sql/init/SqlInitializationProperties.java new file mode 100644 index 000000000000..6d11f61816da --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/sql/init/SqlInitializationProperties.java @@ -0,0 +1,155 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.sql.init; + +import java.nio.charset.Charset; +import java.util.List; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.sql.init.DatabaseInitializationMode; + +/** + * {@link ConfigurationProperties Configuration properties} for initializing an SQL + * database. + * + * @author Andy Wilkinson + * @since 2.5.0 + */ +@ConfigurationProperties("spring.sql.init") +public class SqlInitializationProperties { + + /** + * Locations of the schema (DDL) scripts to apply to the database. + */ + private List schemaLocations; + + /** + * Locations of the data (DML) scripts to apply to the database. + */ + private List dataLocations; + + /** + * Platform to use in the default schema or data script locations, + * schema-${platform}.sql and data-${platform}.sql. + */ + private String platform = "all"; + + /** + * Username of the database to use when applying initialization scripts (if + * different). + */ + private String username; + + /** + * Password of the database to use when applying initialization scripts (if + * different). + */ + private String password; + + /** + * Whether initialization should continue when an error occurs. + */ + private boolean continueOnError = false; + + /** + * Statement separator in the schema and data scripts. + */ + private String separator = ";"; + + /** + * Encoding of the schema and data scripts. + */ + private Charset encoding; + + /** + * Mode to apply when determining whether initialization should be performed. + */ + private DatabaseInitializationMode mode = DatabaseInitializationMode.EMBEDDED; + + public List getSchemaLocations() { + return this.schemaLocations; + } + + public void setSchemaLocations(List schemaLocations) { + this.schemaLocations = schemaLocations; + } + + public List getDataLocations() { + return this.dataLocations; + } + + public void setDataLocations(List dataLocations) { + this.dataLocations = dataLocations; + } + + public String getPlatform() { + return this.platform; + } + + public void setPlatform(String platform) { + this.platform = platform; + } + + public String getUsername() { + return this.username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return this.password; + } + + public void setPassword(String password) { + this.password = password; + } + + public boolean isContinueOnError() { + return this.continueOnError; + } + + public void setContinueOnError(boolean continueOnError) { + this.continueOnError = continueOnError; + } + + public String getSeparator() { + return this.separator; + } + + public void setSeparator(String separator) { + this.separator = separator; + } + + public Charset getEncoding() { + return this.encoding; + } + + public void setEncoding(Charset encoding) { + this.encoding = encoding; + } + + public DatabaseInitializationMode getMode() { + return this.mode; + } + + public void setMode(DatabaseInitializationMode mode) { + this.mode = mode; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/sql/init/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/sql/init/package-info.java new file mode 100644 index 000000000000..cfbdf3cd78fa --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/sql/init/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for basic script-based initialization of an SQL database. + */ +package org.springframework.boot.autoconfigure.sql.init; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/ScheduledBeanLazyInitializationExcludeFilter.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/ScheduledBeanLazyInitializationExcludeFilter.java new file mode 100644 index 000000000000..644a4bcb2baa --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/ScheduledBeanLazyInitializationExcludeFilter.java @@ -0,0 +1,78 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.task; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledExecutorService; + +import org.springframework.aop.framework.AopInfrastructureBean; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.boot.LazyInitializationExcludeFilter; +import org.springframework.core.MethodIntrospector; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.scheduling.annotation.Schedules; +import org.springframework.util.ClassUtils; + +/** + * A {@link LazyInitializationExcludeFilter} that detects bean methods annotated with + * {@link Scheduled} or {@link Schedules}. + * + * @author Stephane Nicoll + */ +class ScheduledBeanLazyInitializationExcludeFilter implements LazyInitializationExcludeFilter { + + private final Set> nonAnnotatedClasses = Collections.newSetFromMap(new ConcurrentHashMap<>(64)); + + ScheduledBeanLazyInitializationExcludeFilter() { + // Ignore AOP infrastructure such as scoped proxies. + this.nonAnnotatedClasses.add(AopInfrastructureBean.class); + this.nonAnnotatedClasses.add(TaskScheduler.class); + this.nonAnnotatedClasses.add(ScheduledExecutorService.class); + } + + @Override + public boolean isExcluded(String beanName, BeanDefinition beanDefinition, Class beanType) { + return hasScheduledTask(beanType); + } + + private boolean hasScheduledTask(Class type) { + Class targetType = ClassUtils.getUserClass(type); + if (!this.nonAnnotatedClasses.contains(targetType) + && AnnotationUtils.isCandidateClass(targetType, Arrays.asList(Scheduled.class, Schedules.class))) { + Map> annotatedMethods = MethodIntrospector.selectMethods(targetType, + (MethodIntrospector.MetadataLookup>) (method) -> { + Set scheduledAnnotations = AnnotatedElementUtils + .getMergedRepeatableAnnotations(method, Scheduled.class, Schedules.class); + return (!scheduledAnnotations.isEmpty() ? scheduledAnnotations : null); + }); + if (annotatedMethods.isEmpty()) { + this.nonAnnotatedClasses.add(targetType); + } + return !annotatedMethods.isEmpty(); + } + return false; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfiguration.java index 4282aac7c5b1..00e4e5dadd66 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.util.concurrent.ScheduledExecutorService; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.LazyInitializationExcludeFilter; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -54,6 +55,11 @@ public ThreadPoolTaskScheduler taskScheduler(TaskSchedulerBuilder builder) { return builder.build(); } + @Bean + public static LazyInitializationExcludeFilter scheduledBeanLazyInitializationExcludeFilter() { + return new ScheduledBeanLazyInitializationExcludeFilter(); + } + @Bean @ConditionalOnMissingBean public TaskSchedulerBuilder taskSchedulerBuilder(TaskSchedulingProperties properties, diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/template/AbstractTemplateViewResolverProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/template/AbstractTemplateViewResolverProperties.java index d152a393f6b8..b9422a539d44 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/template/AbstractTemplateViewResolverProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/template/AbstractTemplateViewResolverProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -152,7 +152,7 @@ public void setExposeSpringMacroHelpers(boolean exposeSpringMacroHelpers) { */ public void applyToMvcViewResolver(Object viewResolver) { Assert.isInstanceOf(AbstractTemplateViewResolver.class, viewResolver, - "ViewResolver is not an instance of AbstractTemplateViewResolver :" + viewResolver); + () -> "ViewResolver is not an instance of AbstractTemplateViewResolver :" + viewResolver); AbstractTemplateViewResolver resolver = (AbstractTemplateViewResolver) viewResolver; resolver.setPrefix(getPrefix()); resolver.setSuffix(getSuffix()); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/template/AbstractViewResolverProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/template/AbstractViewResolverProperties.java index 0846796b84fa..c64f92d0ec4d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/template/AbstractViewResolverProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/template/AbstractViewResolverProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,7 +61,7 @@ public abstract class AbstractViewResolverProperties { private Charset charset = DEFAULT_CHARSET; /** - * White list of view names that can be resolved. + * View names that can be resolved. */ private String[] viewNames; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thymeleaf/TemplateEngineConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thymeleaf/TemplateEngineConfigurations.java new file mode 100644 index 000000000000..bdf3c0e06cfb --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thymeleaf/TemplateEngineConfigurations.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.thymeleaf; + +import org.thymeleaf.ITemplateEngine; +import org.thymeleaf.dialect.IDialect; +import org.thymeleaf.spring5.ISpringTemplateEngine; +import org.thymeleaf.spring5.ISpringWebFluxTemplateEngine; +import org.thymeleaf.spring5.SpringTemplateEngine; +import org.thymeleaf.spring5.SpringWebFluxTemplateEngine; +import org.thymeleaf.templateresolver.ITemplateResolver; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Configuration classes for Thymeleaf's {@link ITemplateEngine}. Imported by + * {@link ThymeleafAutoConfiguration}. + * + * @author Andy Wilkinson + */ +class TemplateEngineConfigurations { + + @Configuration(proxyBeanMethods = false) + static class DefaultTemplateEngineConfiguration { + + @Bean + @ConditionalOnMissingBean(ISpringTemplateEngine.class) + SpringTemplateEngine templateEngine(ThymeleafProperties properties, + ObjectProvider templateResolvers, ObjectProvider dialects) { + SpringTemplateEngine engine = new SpringTemplateEngine(); + engine.setEnableSpringELCompiler(properties.isEnableSpringElCompiler()); + engine.setRenderHiddenMarkersBeforeCheckboxes(properties.isRenderHiddenMarkersBeforeCheckboxes()); + templateResolvers.orderedStream().forEach(engine::addTemplateResolver); + dialects.orderedStream().forEach(engine::addDialect); + return engine; + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnWebApplication(type = Type.REACTIVE) + @ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true) + static class ReactiveTemplateEngineConfiguration { + + @Bean + @ConditionalOnMissingBean(ISpringWebFluxTemplateEngine.class) + SpringWebFluxTemplateEngine templateEngine(ThymeleafProperties properties, + ObjectProvider templateResolvers, ObjectProvider dialects) { + SpringWebFluxTemplateEngine engine = new SpringWebFluxTemplateEngine(); + engine.setEnableSpringELCompiler(properties.isEnableSpringElCompiler()); + engine.setRenderHiddenMarkersBeforeCheckboxes(properties.isRenderHiddenMarkersBeforeCheckboxes()); + templateResolvers.orderedStream().forEach(engine::addTemplateResolver); + dialects.orderedStream().forEach(engine::addDialect); + return engine; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thymeleaf/ThymeleafAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thymeleaf/ThymeleafAutoConfiguration.java index b1ab53974b38..4e54aebca332 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thymeleaf/ThymeleafAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thymeleaf/ThymeleafAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,27 +18,21 @@ import java.util.LinkedHashMap; -import javax.annotation.PostConstruct; import javax.servlet.DispatcherType; import com.github.mxab.thymeleaf.extras.dataattribute.dialect.DataAttributeDialect; import nz.net.ultraq.thymeleaf.LayoutDialect; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.thymeleaf.dialect.IDialect; import org.thymeleaf.extras.java8time.dialect.Java8TimeDialect; import org.thymeleaf.extras.springsecurity5.dialect.SpringSecurityDialect; -import org.thymeleaf.spring5.ISpringTemplateEngine; import org.thymeleaf.spring5.ISpringWebFluxTemplateEngine; import org.thymeleaf.spring5.SpringTemplateEngine; -import org.thymeleaf.spring5.SpringWebFluxTemplateEngine; import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver; import org.thymeleaf.spring5.view.ThymeleafViewResolver; import org.thymeleaf.spring5.view.reactive.ThymeleafReactiveViewResolver; import org.thymeleaf.templatemode.TemplateMode; -import org.thymeleaf.templateresolver.ITemplateResolver; -import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -58,7 +52,9 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import org.springframework.core.Ordered; +import org.springframework.security.web.server.csrf.CsrfToken; import org.springframework.util.MimeType; import org.springframework.util.unit.DataSize; import org.springframework.web.servlet.resource.ResourceUrlEncodingFilter; @@ -80,6 +76,8 @@ @EnableConfigurationProperties(ThymeleafProperties.class) @ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class }) @AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class }) +@Import({ TemplateEngineConfigurations.ReactiveTemplateEngineConfiguration.class, + TemplateEngineConfigurations.DefaultTemplateEngineConfiguration.class }) public class ThymeleafAutoConfiguration { @Configuration(proxyBeanMethods = false) @@ -95,10 +93,10 @@ static class DefaultTemplateResolverConfiguration { DefaultTemplateResolverConfiguration(ThymeleafProperties properties, ApplicationContext applicationContext) { this.properties = properties; this.applicationContext = applicationContext; + checkTemplateLocationExists(); } - @PostConstruct - void checkTemplateLocationExists() { + private void checkTemplateLocationExists() { boolean checkTemplateLocation = this.properties.isCheckTemplateLocation(); if (checkTemplateLocation) { TemplateLocation location = new TemplateLocation(this.properties.getPrefix()); @@ -130,23 +128,6 @@ SpringResourceTemplateResolver defaultTemplateResolver() { } - @Configuration(proxyBeanMethods = false) - protected static class ThymeleafDefaultConfiguration { - - @Bean - @ConditionalOnMissingBean(ISpringTemplateEngine.class) - SpringTemplateEngine templateEngine(ThymeleafProperties properties, - ObjectProvider templateResolvers, ObjectProvider dialects) { - SpringTemplateEngine engine = new SpringTemplateEngine(); - engine.setEnableSpringELCompiler(properties.isEnableSpringElCompiler()); - engine.setRenderHiddenMarkersBeforeCheckboxes(properties.isRenderHiddenMarkersBeforeCheckboxes()); - templateResolvers.orderedStream().forEach(engine::addTemplateResolver); - dialects.orderedStream().forEach(engine::addDialect); - return engine; - } - - } - @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) @ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true) @@ -199,25 +180,6 @@ private String appendCharset(MimeType type, String charset) { } - @Configuration(proxyBeanMethods = false) - @ConditionalOnWebApplication(type = Type.REACTIVE) - @ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true) - static class ThymeleafReactiveConfiguration { - - @Bean - @ConditionalOnMissingBean(ISpringWebFluxTemplateEngine.class) - SpringWebFluxTemplateEngine templateEngine(ThymeleafProperties properties, - ObjectProvider templateResolvers, ObjectProvider dialects) { - SpringWebFluxTemplateEngine engine = new SpringWebFluxTemplateEngine(); - engine.setEnableSpringELCompiler(properties.isEnableSpringElCompiler()); - engine.setRenderHiddenMarkersBeforeCheckboxes(properties.isRenderHiddenMarkersBeforeCheckboxes()); - templateResolvers.orderedStream().forEach(engine::addTemplateResolver); - dialects.orderedStream().forEach(engine::addDialect); - return engine; - } - - } - @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.REACTIVE) @ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true) @@ -280,7 +242,7 @@ DataAttributeDialect dialect() { } @Configuration(proxyBeanMethods = false) - @ConditionalOnClass({ SpringSecurityDialect.class }) + @ConditionalOnClass({ SpringSecurityDialect.class, CsrfToken.class }) static class ThymeleafSecurityDialectConfiguration { @Bean diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionAutoConfiguration.java index a38b22e87e4e..483cc1253bc2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -88,8 +88,7 @@ public static class EnableTransactionManagementConfiguration { @Configuration(proxyBeanMethods = false) @EnableTransactionManagement(proxyTargetClass = false) - @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false", - matchIfMissing = false) + @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false") public static class JdkDynamicAutoProxyConfiguration { } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/AtomikosJtaConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/AtomikosJtaConfiguration.java index abd3cd7084bb..393494c0a232 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/AtomikosJtaConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/AtomikosJtaConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,6 @@ import org.springframework.boot.system.ApplicationHome; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.jta.JtaTransactionManager; import org.springframework.util.StringUtils; @@ -57,7 +56,7 @@ @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties({ AtomikosProperties.class, JtaProperties.class }) @ConditionalOnClass({ JtaTransactionManager.class, UserTransactionManager.class }) -@ConditionalOnMissingBean(PlatformTransactionManager.class) +@ConditionalOnMissingBean(org.springframework.transaction.TransactionManager.class) class AtomikosJtaConfiguration { @Bean(initMethod = "init", destroyMethod = "shutdownWait") diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/BitronixJtaConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/BitronixJtaConfiguration.java deleted file mode 100644 index 1edf664e77bf..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/BitronixJtaConfiguration.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.transaction.jta; - -import java.io.File; - -import javax.jms.Message; -import javax.transaction.TransactionManager; -import javax.transaction.UserTransaction; - -import bitronix.tm.BitronixTransactionManager; -import bitronix.tm.TransactionManagerServices; -import bitronix.tm.jndi.BitronixContext; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizers; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.jdbc.XADataSourceWrapper; -import org.springframework.boot.jms.XAConnectionFactoryWrapper; -import org.springframework.boot.jta.bitronix.BitronixDependentBeanFactoryPostProcessor; -import org.springframework.boot.jta.bitronix.BitronixXAConnectionFactoryWrapper; -import org.springframework.boot.jta.bitronix.BitronixXADataSourceWrapper; -import org.springframework.boot.system.ApplicationHome; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.jta.JtaTransactionManager; -import org.springframework.util.StringUtils; - -/** - * JTA Configuration for Bitronix. - * - * @author Josh Long - * @author Phillip Webb - * @author Andy Wilkinson - * @author Kazuki Shimizu - */ -@Configuration(proxyBeanMethods = false) -@EnableConfigurationProperties(JtaProperties.class) -@ConditionalOnClass({ JtaTransactionManager.class, BitronixContext.class }) -@ConditionalOnMissingBean(PlatformTransactionManager.class) -class BitronixJtaConfiguration { - - @Bean - @ConditionalOnMissingBean - @ConfigurationProperties(prefix = "spring.jta.bitronix.properties") - bitronix.tm.Configuration bitronixConfiguration(JtaProperties jtaProperties) { - bitronix.tm.Configuration config = TransactionManagerServices.getConfiguration(); - if (StringUtils.hasText(jtaProperties.getTransactionManagerId())) { - config.setServerId(jtaProperties.getTransactionManagerId()); - } - File logBaseDir = getLogBaseDir(jtaProperties); - config.setLogPart1Filename(new File(logBaseDir, "part1.btm").getAbsolutePath()); - config.setLogPart2Filename(new File(logBaseDir, "part2.btm").getAbsolutePath()); - config.setDisableJmx(true); - return config; - } - - private File getLogBaseDir(JtaProperties jtaProperties) { - if (StringUtils.hasLength(jtaProperties.getLogDir())) { - return new File(jtaProperties.getLogDir()); - } - File home = new ApplicationHome().getDir(); - return new File(home, "transaction-logs"); - } - - @Bean - @ConditionalOnMissingBean(TransactionManager.class) - BitronixTransactionManager bitronixTransactionManager(bitronix.tm.Configuration configuration) { - // Inject configuration to force ordering - return TransactionManagerServices.getTransactionManager(); - } - - @Bean - @ConditionalOnMissingBean(XADataSourceWrapper.class) - BitronixXADataSourceWrapper xaDataSourceWrapper() { - return new BitronixXADataSourceWrapper(); - } - - @Bean - @ConditionalOnMissingBean - static BitronixDependentBeanFactoryPostProcessor bitronixDependentBeanFactoryPostProcessor() { - return new BitronixDependentBeanFactoryPostProcessor(); - } - - @Bean - JtaTransactionManager transactionManager(UserTransaction userTransaction, TransactionManager transactionManager, - ObjectProvider transactionManagerCustomizers) { - JtaTransactionManager jtaTransactionManager = new JtaTransactionManager(userTransaction, transactionManager); - transactionManagerCustomizers.ifAvailable((customizers) -> customizers.customize(jtaTransactionManager)); - return jtaTransactionManager; - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(Message.class) - static class BitronixJtaJmsConfiguration { - - @Bean - @ConditionalOnMissingBean(XAConnectionFactoryWrapper.class) - BitronixXAConnectionFactoryWrapper xaConnectionFactoryWrapper() { - return new BitronixXAConnectionFactoryWrapper(); - } - - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/JndiJtaConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/JndiJtaConfiguration.java index 2bee60e2b57e..cc9f6325a8cd 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/JndiJtaConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/JndiJtaConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,6 @@ import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizers; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.config.JtaTransactionManagerFactoryBean; import org.springframework.transaction.jta.JtaTransactionManager; @@ -38,7 +37,7 @@ @ConditionalOnClass(JtaTransactionManager.class) @ConditionalOnJndi({ JtaTransactionManager.DEFAULT_USER_TRANSACTION_NAME, "java:comp/TransactionManager", "java:appserver/TransactionManager", "java:pm/TransactionManager", "java:/TransactionManager" }) -@ConditionalOnMissingBean(PlatformTransactionManager.class) +@ConditionalOnMissingBean(org.springframework.transaction.TransactionManager.class) class JndiJtaConfiguration { @Bean diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/JtaAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/JtaAutoConfiguration.java index 2fb24cec7c9b..f4d62b49318b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/JtaAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/JtaAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,7 +40,7 @@ @ConditionalOnProperty(prefix = "spring.jta", value = "enabled", matchIfMissing = true) @AutoConfigureBefore({ XADataSourceAutoConfiguration.class, ActiveMQAutoConfiguration.class, ArtemisAutoConfiguration.class, HibernateJpaAutoConfiguration.class }) -@Import({ JndiJtaConfiguration.class, BitronixJtaConfiguration.class, AtomikosJtaConfiguration.class }) +@Import({ JndiJtaConfiguration.class, AtomikosJtaConfiguration.class }) public class JtaAutoConfiguration { } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/PrimaryDefaultValidatorPostProcessor.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/PrimaryDefaultValidatorPostProcessor.java index ff32b479ed13..7fbafabf2094 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/PrimaryDefaultValidatorPostProcessor.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/PrimaryDefaultValidatorPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionRegistry; @@ -37,6 +36,8 @@ * as primary. * * @author Stephane Nicoll + * @author Matej Nedic + * @author Andy Wilkinson */ class PrimaryDefaultValidatorPostProcessor implements ImportBeanDefinitionRegistrar, BeanFactoryAware { @@ -58,7 +59,7 @@ public void setBeanFactory(BeanFactory beanFactory) throws BeansException { public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { BeanDefinition definition = getAutoConfiguredValidator(registry); if (definition != null) { - definition.setPrimary(!hasPrimarySpringValidator(registry)); + definition.setPrimary(!hasPrimarySpringValidator()); } } @@ -77,12 +78,11 @@ private boolean isTypeMatch(String name, Class type) { return this.beanFactory != null && this.beanFactory.isTypeMatch(name, type); } - private boolean hasPrimarySpringValidator(BeanDefinitionRegistry registry) { - String[] validatorBeans = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, Validator.class, - false, false); + private boolean hasPrimarySpringValidator() { + String[] validatorBeans = this.beanFactory.getBeanNamesForType(Validator.class, false, false); for (String validatorBean : validatorBeans) { - BeanDefinition definition = registry.getBeanDefinition(validatorBean); - if (definition != null && definition.isPrimary()) { + BeanDefinition definition = this.beanFactory.getBeanDefinition(validatorBean); + if (definition.isPrimary()) { return true; } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfiguration.java index 57d48be9189c..04775648a618 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,12 +19,16 @@ import javax.validation.Validator; import javax.validation.executable.ExecutableValidator; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnResource; +import org.springframework.boot.autoconfigure.condition.SearchStrategy; import org.springframework.boot.validation.MessageInterpolatorFactory; +import org.springframework.boot.validation.beanvalidation.FilteredMethodValidationPostProcessor; +import org.springframework.boot.validation.beanvalidation.MethodValidationExcludeFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -59,10 +63,11 @@ public static LocalValidatorFactoryBean defaultValidator() { } @Bean - @ConditionalOnMissingBean + @ConditionalOnMissingBean(search = SearchStrategy.CURRENT) public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment, - @Lazy Validator validator) { - MethodValidationPostProcessor processor = new MethodValidationPostProcessor(); + @Lazy Validator validator, ObjectProvider excludeFilters) { + FilteredMethodValidationPostProcessor processor = new FilteredMethodValidationPostProcessor( + excludeFilters.orderedStream()); boolean proxyTargetClass = environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true); processor.setProxyTargetClass(proxyTargetClass); processor.setValidator(validator); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ConditionalOnEnabledResourceChain.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ConditionalOnEnabledResourceChain.java index 0d5f1472ddd4..f2637f533488 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ConditionalOnEnabledResourceChain.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ConditionalOnEnabledResourceChain.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,8 +26,9 @@ /** * {@link Conditional @Conditional} that checks whether or not the Spring resource - * handling chain is enabled. Matches if {@link ResourceProperties.Chain#getEnabled()} is - * {@code true} or if {@code webjars-locator-core} is on the classpath. + * handling chain is enabled. Matches if + * {@link WebProperties.Resources.Chain#getEnabled()} is {@code true} or if + * {@code webjars-locator-core} is on the classpath. * * @author Stephane Nicoll * @since 1.3.0 diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ErrorProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ErrorProperties.java index d38a134f290a..9900a86157b9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ErrorProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ErrorProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ * @author Michael Stummvoll * @author Stephane Nicoll * @author Vedran Pavic + * @author Scott Frederick * @since 1.3.0 */ public class ErrorProperties { @@ -40,9 +41,19 @@ public class ErrorProperties { private boolean includeException; /** - * When to include a "stacktrace" attribute. + * When to include the "trace" attribute. */ - private IncludeStacktrace includeStacktrace = IncludeStacktrace.NEVER; + private IncludeAttribute includeStacktrace = IncludeAttribute.NEVER; + + /** + * When to include "message" attribute. + */ + private IncludeAttribute includeMessage = IncludeAttribute.NEVER; + + /** + * When to include "errors" attribute. + */ + private IncludeAttribute includeBindingErrors = IncludeAttribute.NEVER; private final Whitelabel whitelabel = new Whitelabel(); @@ -62,14 +73,30 @@ public void setIncludeException(boolean includeException) { this.includeException = includeException; } - public IncludeStacktrace getIncludeStacktrace() { + public IncludeAttribute getIncludeStacktrace() { return this.includeStacktrace; } - public void setIncludeStacktrace(IncludeStacktrace includeStacktrace) { + public void setIncludeStacktrace(IncludeAttribute includeStacktrace) { this.includeStacktrace = includeStacktrace; } + public IncludeAttribute getIncludeMessage() { + return this.includeMessage; + } + + public void setIncludeMessage(IncludeAttribute includeMessage) { + this.includeMessage = includeMessage; + } + + public IncludeAttribute getIncludeBindingErrors() { + return this.includeBindingErrors; + } + + public void setIncludeBindingErrors(IncludeAttribute includeBindingErrors) { + this.includeBindingErrors = includeBindingErrors; + } + public Whitelabel getWhitelabel() { return this.whitelabel; } @@ -90,9 +117,31 @@ public enum IncludeStacktrace { ALWAYS, /** - * Add stacktrace information when the "trace" request parameter is "true". + * Add stacktrace attribute when the appropriate request parameter is not "false". + */ + ON_PARAM + + } + + /** + * Include error attributes options. + */ + public enum IncludeAttribute { + + /** + * Never add error attribute. + */ + NEVER, + + /** + * Always add error attribute. + */ + ALWAYS, + + /** + * Add error attribute when the appropriate request parameter is not "false". */ - ON_TRACE_PARAM + ON_PARAM } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/OnEnabledResourceChainCondition.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/OnEnabledResourceChainCondition.java index 5c82245e76c5..1d03c210e71e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/OnEnabledResourceChainCondition.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/OnEnabledResourceChainCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,9 +19,13 @@ import org.springframework.boot.autoconfigure.condition.ConditionMessage; import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.boot.autoconfigure.web.WebProperties.Resources.Chain; +import org.springframework.boot.context.properties.bind.BindResult; +import org.springframework.boot.context.properties.bind.Binder; import org.springframework.context.annotation.Condition; import org.springframework.context.annotation.ConditionContext; import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.util.ClassUtils; @@ -41,10 +45,11 @@ class OnEnabledResourceChainCondition extends SpringBootCondition { @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { ConfigurableEnvironment environment = (ConfigurableEnvironment) context.getEnvironment(); - boolean fixed = getEnabledProperty(environment, "strategy.fixed.", false); - boolean content = getEnabledProperty(environment, "strategy.content.", false); - Boolean chain = getEnabledProperty(environment, "", null); - Boolean match = ResourceProperties.Chain.getEnabled(fixed, content, chain); + String prefix = determineResourcePropertiesPrefix(environment); + boolean fixed = getEnabledProperty(environment, prefix, "strategy.fixed.", false); + boolean content = getEnabledProperty(environment, prefix, "strategy.content.", false); + Boolean chain = getEnabledProperty(environment, prefix, "", null); + Boolean match = Chain.getEnabled(fixed, content, chain); ConditionMessage.Builder message = ConditionMessage.forCondition(ConditionalOnEnabledResourceChain.class); if (match == null) { if (ClassUtils.isPresent(WEBJAR_ASSET_LOCATOR, getClass().getClassLoader())) { @@ -58,8 +63,19 @@ public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeM return ConditionOutcome.noMatch(message.because("disabled")); } - private Boolean getEnabledProperty(ConfigurableEnvironment environment, String key, Boolean defaultValue) { - String name = "spring.resources.chain." + key + "enabled"; + @SuppressWarnings("deprecation") + private String determineResourcePropertiesPrefix(Environment environment) { + BindResult result = Binder.get(environment) + .bind("spring.resources", org.springframework.boot.autoconfigure.web.ResourceProperties.class); + if (result.isBound() && result.get().hasBeenCustomized()) { + return "spring.resources.chain."; + } + return "spring.web.resources.chain."; + } + + private Boolean getEnabledProperty(ConfigurableEnvironment environment, String prefix, String key, + Boolean defaultValue) { + String name = prefix + key + "enabled"; return environment.getProperty(name, Boolean.class, defaultValue); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ResourceProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ResourceProperties.java index 9b8ba0362bce..7bf90836fb18 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ResourceProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ResourceProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,13 +17,10 @@ package org.springframework.boot.autoconfigure.web; import java.time.Duration; -import java.time.temporal.ChronoUnit; -import java.util.concurrent.TimeUnit; +import org.springframework.boot.autoconfigure.web.WebProperties.Resources; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.PropertyMapper; -import org.springframework.boot.convert.DurationUnit; -import org.springframework.http.CacheControl; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; /** * Properties used to configure resource handling. @@ -34,135 +31,81 @@ * @author Venil Noronha * @author Kristine Jetzke * @since 1.1.0 + * @deprecated since 2.4.0 for removal in 2.6.0 in favor of + * {@link WebProperties.Resources} accessed through {@link WebProperties} and + * {@link WebProperties#getResources() getResources()} */ +@Deprecated @ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false) -public class ResourceProperties { - - private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/", - "classpath:/resources/", "classpath:/static/", "classpath:/public/" }; - - /** - * Locations of static resources. Defaults to classpath:[/META-INF/resources/, - * /resources/, /static/, /public/]. - */ - private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS; - - /** - * Whether to enable default resource handling. - */ - private boolean addMappings = true; +public class ResourceProperties extends Resources { private final Chain chain = new Chain(); private final Cache cache = new Cache(); + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.static-locations") public String[] getStaticLocations() { - return this.staticLocations; - } - - public void setStaticLocations(String[] staticLocations) { - this.staticLocations = appendSlashIfNecessary(staticLocations); - } - - private String[] appendSlashIfNecessary(String[] staticLocations) { - String[] normalized = new String[staticLocations.length]; - for (int i = 0; i < staticLocations.length; i++) { - String location = staticLocations[i]; - normalized[i] = location.endsWith("/") ? location : location + "/"; - } - return normalized; + return super.getStaticLocations(); } + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.add-mappings") public boolean isAddMappings() { - return this.addMappings; - } - - public void setAddMappings(boolean addMappings) { - this.addMappings = addMappings; + return super.isAddMappings(); } + @Override public Chain getChain() { return this.chain; } + @Override public Cache getCache() { return this.cache; } - /** - * Configuration for the Spring Resource Handling chain. - */ - public static class Chain { - - /** - * Whether to enable the Spring Resource Handling chain. By default, disabled - * unless at least one strategy has been enabled. - */ - private Boolean enabled; + @Deprecated + public static class Chain extends Resources.Chain { - /** - * Whether to enable caching in the Resource chain. - */ - private boolean cache = true; + private final org.springframework.boot.autoconfigure.web.ResourceProperties.Strategy strategy = new org.springframework.boot.autoconfigure.web.ResourceProperties.Strategy(); /** * Whether to enable HTML5 application cache manifest rewriting. */ private boolean htmlApplicationCache = false; - /** - * Whether to enable resolution of already compressed resources (gzip, brotli). - * Checks for a resource name with the '.gz' or '.br' file extensions. - */ - private boolean compressed = false; - - private final Strategy strategy = new Strategy(); - - /** - * Return whether the resource chain is enabled. Return {@code null} if no - * specific settings are present. - * @return whether the resource chain is enabled or {@code null} if no specified - * settings are present. - */ + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.chain.enabled") public Boolean getEnabled() { - return getEnabled(getStrategy().getFixed().isEnabled(), getStrategy().getContent().isEnabled(), - this.enabled); - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; + return super.getEnabled(); } + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.chain.cache") public boolean isCache() { - return this.cache; - } - - public void setCache(boolean cache) { - this.cache = cache; - } - - public Strategy getStrategy() { - return this.strategy; + return super.isCache(); } + @DeprecatedConfigurationProperty(reason = "The appcache manifest feature is being removed from browsers.") public boolean isHtmlApplicationCache() { return this.htmlApplicationCache; } public void setHtmlApplicationCache(boolean htmlApplicationCache) { this.htmlApplicationCache = htmlApplicationCache; + this.customized = true; } + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.chain.compressed") public boolean isCompressed() { - return this.compressed; + return super.isCompressed(); } - public void setCompressed(boolean compressed) { - this.compressed = compressed; - } - - static Boolean getEnabled(boolean fixedEnabled, boolean contentEnabled, Boolean chainEnabled) { - return (fixedEnabled || contentEnabled) ? Boolean.TRUE : chainEnabled; + @Override + public org.springframework.boot.autoconfigure.web.ResourceProperties.Strategy getStrategy() { + return this.strategy; } } @@ -170,17 +113,20 @@ static Boolean getEnabled(boolean fixedEnabled, boolean contentEnabled, Boolean /** * Strategies for extracting and embedding a resource version in its URL path. */ - public static class Strategy { + @Deprecated + public static class Strategy extends Resources.Chain.Strategy { - private final Fixed fixed = new Fixed(); + private final org.springframework.boot.autoconfigure.web.ResourceProperties.Fixed fixed = new org.springframework.boot.autoconfigure.web.ResourceProperties.Fixed(); - private final Content content = new Content(); + private final org.springframework.boot.autoconfigure.web.ResourceProperties.Content content = new org.springframework.boot.autoconfigure.web.ResourceProperties.Content(); - public Fixed getFixed() { + @Override + public org.springframework.boot.autoconfigure.web.ResourceProperties.Fixed getFixed() { return this.fixed; } - public Content getContent() { + @Override + public org.springframework.boot.autoconfigure.web.ResourceProperties.Content getContent() { return this.content; } @@ -189,32 +135,19 @@ public Content getContent() { /** * Version Strategy based on content hashing. */ - public static class Content { - - /** - * Whether to enable the content Version Strategy. - */ - private boolean enabled; - - /** - * Comma-separated list of patterns to apply to the content Version Strategy. - */ - private String[] paths = new String[] { "/**" }; + @Deprecated + public static class Content extends Resources.Chain.Strategy.Content { + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.chain.strategy.content.enabled") public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; + return super.isEnabled(); } + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.chain.strategy.content.paths") public String[] getPaths() { - return this.paths; - } - - public void setPaths(String[] paths) { - this.paths = paths; + return super.getPaths(); } } @@ -222,45 +155,25 @@ public void setPaths(String[] paths) { /** * Version Strategy based on a fixed version string. */ - public static class Fixed { - - /** - * Whether to enable the fixed Version Strategy. - */ - private boolean enabled; - - /** - * Comma-separated list of patterns to apply to the fixed Version Strategy. - */ - private String[] paths = new String[] { "/**" }; - - /** - * Version string to use for the fixed Version Strategy. - */ - private String version; + @Deprecated + public static class Fixed extends Resources.Chain.Strategy.Fixed { + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.chain.strategy.fixed.enabled") public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; + return super.isEnabled(); } + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.chain.strategy.fixed.paths") public String[] getPaths() { - return this.paths; - } - - public void setPaths(String[] paths) { - this.paths = paths; + return super.getPaths(); } + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.chain.strategy.fixed.version") public String getVersion() { - return this.version; - } - - public void setVersion(String version) { - this.version = version; + return super.getVersion(); } } @@ -268,227 +181,99 @@ public void setVersion(String version) { /** * Cache configuration. */ - public static class Cache { - - /** - * Cache period for the resources served by the resource handler. If a duration - * suffix is not specified, seconds will be used. Can be overridden by the - * 'spring.resources.cache.cachecontrol' properties. - */ - @DurationUnit(ChronoUnit.SECONDS) - private Duration period; + @Deprecated + public static class Cache extends Resources.Cache { - /** - * Cache control HTTP headers, only allows valid directive combinations. Overrides - * the 'spring.resources.cache.period' property. - */ private final Cachecontrol cachecontrol = new Cachecontrol(); + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.cache.period") public Duration getPeriod() { - return this.period; - } - - public void setPeriod(Duration period) { - this.period = period; + return super.getPeriod(); } + @Override public Cachecontrol getCachecontrol() { return this.cachecontrol; } + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.cache.use-last-modified") + public boolean isUseLastModified() { + return super.isUseLastModified(); + } + /** * Cache Control HTTP header configuration. */ - public static class Cachecontrol { - - /** - * Maximum time the response should be cached, in seconds if no duration - * suffix is not specified. - */ - @DurationUnit(ChronoUnit.SECONDS) - private Duration maxAge; - - /** - * Indicate that the cached response can be reused only if re-validated with - * the server. - */ - private Boolean noCache; - - /** - * Indicate to not cache the response in any case. - */ - private Boolean noStore; - - /** - * Indicate that once it has become stale, a cache must not use the response - * without re-validating it with the server. - */ - private Boolean mustRevalidate; - - /** - * Indicate intermediaries (caches and others) that they should not transform - * the response content. - */ - private Boolean noTransform; - - /** - * Indicate that any cache may store the response. - */ - private Boolean cachePublic; - - /** - * Indicate that the response message is intended for a single user and must - * not be stored by a shared cache. - */ - private Boolean cachePrivate; - - /** - * Same meaning as the "must-revalidate" directive, except that it does not - * apply to private caches. - */ - private Boolean proxyRevalidate; - - /** - * Maximum time the response can be served after it becomes stale, in seconds - * if no duration suffix is not specified. - */ - @DurationUnit(ChronoUnit.SECONDS) - private Duration staleWhileRevalidate; - - /** - * Maximum time the response may be used when errors are encountered, in - * seconds if no duration suffix is not specified. - */ - @DurationUnit(ChronoUnit.SECONDS) - private Duration staleIfError; - - /** - * Maximum time the response should be cached by shared caches, in seconds if - * no duration suffix is not specified. - */ - @DurationUnit(ChronoUnit.SECONDS) - private Duration sMaxAge; + @Deprecated + public static class Cachecontrol extends Resources.Cache.Cachecontrol { + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.cache.cachecontrol.max-age") public Duration getMaxAge() { - return this.maxAge; - } - - public void setMaxAge(Duration maxAge) { - this.maxAge = maxAge; + return super.getMaxAge(); } + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.cache.cachecontrol.no-cache") public Boolean getNoCache() { - return this.noCache; - } - - public void setNoCache(Boolean noCache) { - this.noCache = noCache; + return super.getNoCache(); } + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.cache.cachecontrol.no-store") public Boolean getNoStore() { - return this.noStore; - } - - public void setNoStore(Boolean noStore) { - this.noStore = noStore; + return super.getNoStore(); } + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.cache.cachecontrol.must-revalidate") public Boolean getMustRevalidate() { - return this.mustRevalidate; - } - - public void setMustRevalidate(Boolean mustRevalidate) { - this.mustRevalidate = mustRevalidate; + return super.getMustRevalidate(); } + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.cache.cachecontrol.no-transform") public Boolean getNoTransform() { - return this.noTransform; - } - - public void setNoTransform(Boolean noTransform) { - this.noTransform = noTransform; + return super.getNoTransform(); } + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.cache.cachecontrol.cache-public") public Boolean getCachePublic() { - return this.cachePublic; - } - - public void setCachePublic(Boolean cachePublic) { - this.cachePublic = cachePublic; + return super.getCachePublic(); } + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.cache.cachecontrol.cache-private") public Boolean getCachePrivate() { - return this.cachePrivate; - } - - public void setCachePrivate(Boolean cachePrivate) { - this.cachePrivate = cachePrivate; + return super.getCachePrivate(); } + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.cache.cachecontrol.proxy-revalidate") public Boolean getProxyRevalidate() { - return this.proxyRevalidate; - } - - public void setProxyRevalidate(Boolean proxyRevalidate) { - this.proxyRevalidate = proxyRevalidate; + return super.getProxyRevalidate(); } + @Override + @DeprecatedConfigurationProperty( + replacement = "spring.web.resources.cache.cachecontrol.stale-while-revalidate") public Duration getStaleWhileRevalidate() { - return this.staleWhileRevalidate; - } - - public void setStaleWhileRevalidate(Duration staleWhileRevalidate) { - this.staleWhileRevalidate = staleWhileRevalidate; + return super.getStaleWhileRevalidate(); } + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.cache.cachecontrol.stale-if-error") public Duration getStaleIfError() { - return this.staleIfError; - } - - public void setStaleIfError(Duration staleIfError) { - this.staleIfError = staleIfError; + return super.getStaleIfError(); } + @Override + @DeprecatedConfigurationProperty(replacement = "spring.web.resources.cache.cachecontrol.s-max-age") public Duration getSMaxAge() { - return this.sMaxAge; - } - - public void setSMaxAge(Duration sMaxAge) { - this.sMaxAge = sMaxAge; - } - - public CacheControl toHttpCacheControl() { - PropertyMapper map = PropertyMapper.get(); - CacheControl control = createCacheControl(); - map.from(this::getMustRevalidate).whenTrue().toCall(control::mustRevalidate); - map.from(this::getNoTransform).whenTrue().toCall(control::noTransform); - map.from(this::getCachePublic).whenTrue().toCall(control::cachePublic); - map.from(this::getCachePrivate).whenTrue().toCall(control::cachePrivate); - map.from(this::getProxyRevalidate).whenTrue().toCall(control::proxyRevalidate); - map.from(this::getStaleWhileRevalidate).whenNonNull() - .to((duration) -> control.staleWhileRevalidate(duration.getSeconds(), TimeUnit.SECONDS)); - map.from(this::getStaleIfError).whenNonNull() - .to((duration) -> control.staleIfError(duration.getSeconds(), TimeUnit.SECONDS)); - map.from(this::getSMaxAge).whenNonNull() - .to((duration) -> control.sMaxAge(duration.getSeconds(), TimeUnit.SECONDS)); - // check if cacheControl remained untouched - if (control.getHeaderValue() == null) { - return null; - } - return control; - } - - private CacheControl createCacheControl() { - if (Boolean.TRUE.equals(this.noStore)) { - return CacheControl.noStore(); - } - if (Boolean.TRUE.equals(this.noCache)) { - return CacheControl.noCache(); - } - if (this.maxAge != null) { - return CacheControl.maxAge(this.maxAge.getSeconds(), TimeUnit.SECONDS); - } - return CacheControl.empty(); + return super.getSMaxAge(); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java index 1eaae3ac6310..1b5456129d6d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,12 +31,13 @@ import io.undertow.UndertowOptions; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; import org.springframework.boot.context.properties.NestedConfigurationProperty; import org.springframework.boot.convert.DurationUnit; import org.springframework.boot.web.server.Compression; import org.springframework.boot.web.server.Http2; +import org.springframework.boot.web.server.Shutdown; import org.springframework.boot.web.server.Ssl; +import org.springframework.boot.web.servlet.server.Encoding; import org.springframework.boot.web.servlet.server.Jsp; import org.springframework.boot.web.servlet.server.Session; import org.springframework.util.StringUtils; @@ -63,6 +64,9 @@ * @author Rafiullah Hamedy * @author Dirk Deyne * @author HaiTao Zhang + * @author Victor Mandujano + * @author Chris Bono + * @author Parviz Rozikov * @since 1.0.0 */ @ConfigurationProperties(prefix = "server", ignoreUnknownFields = true) @@ -84,7 +88,7 @@ public class ServerProperties { /** * Strategy for handling X-Forwarded-* headers. */ - private ForwardHeadersStrategy forwardHeadersStrategy = ForwardHeadersStrategy.NONE; + private ForwardHeadersStrategy forwardHeadersStrategy; /** * Value to use for the Server response header (if empty, no header is sent). @@ -97,11 +101,9 @@ public class ServerProperties { private DataSize maxHttpHeaderSize = DataSize.ofKilobytes(8); /** - * Time that connectors wait for another HTTP request before closing the connection. - * When not set, the connector's container-specific default is used. Use a value of -1 - * to indicate no (that is, an infinite) timeout. + * Type of shutdown that the server will support. */ - private Duration connectionTimeout; + private Shutdown shutdown = Shutdown.IMMEDIATE; @NestedConfigurationProperty private Ssl ssl; @@ -138,17 +140,6 @@ public void setAddress(InetAddress address) { this.address = address; } - @DeprecatedConfigurationProperty(reason = "replaced to support additional strategies", - replacement = "server.forward-headers-strategy") - public Boolean isUseForwardHeaders() { - return ForwardHeadersStrategy.NATIVE.equals(this.forwardHeadersStrategy); - } - - public void setUseForwardHeaders(Boolean useForwardHeaders) { - this.forwardHeadersStrategy = Boolean.TRUE.equals(useForwardHeaders) ? ForwardHeadersStrategy.NATIVE - : ForwardHeadersStrategy.NONE; - } - public String getServerHeader() { return this.serverHeader; } @@ -165,16 +156,12 @@ public void setMaxHttpHeaderSize(DataSize maxHttpHeaderSize) { this.maxHttpHeaderSize = maxHttpHeaderSize; } - @Deprecated - @DeprecatedConfigurationProperty( - reason = "Each server behaves differently. Use server specific properties instead.") - public Duration getConnectionTimeout() { - return this.connectionTimeout; + public Shutdown getShutdown() { + return this.shutdown; } - @Deprecated - public void setConnectionTimeout(Duration connectionTimeout) { - this.connectionTimeout = connectionTimeout; + public void setShutdown(Shutdown shutdown) { + this.shutdown = shutdown; } public ErrorProperties getError() { @@ -245,6 +232,14 @@ public static class Servlet { */ private String applicationDisplayName = "application"; + /** + * Whether to register the default Servlet with the container. + */ + private boolean registerDefaultServlet = false; + + @NestedConfigurationProperty + private final Encoding encoding = new Encoding(); + @NestedConfigurationProperty private final Jsp jsp = new Jsp(); @@ -275,10 +270,22 @@ public void setApplicationDisplayName(String displayName) { this.applicationDisplayName = displayName; } + public boolean isRegisterDefaultServlet() { + return this.registerDefaultServlet; + } + + public void setRegisterDefaultServlet(boolean registerDefaultServlet) { + this.registerDefaultServlet = registerDefaultServlet; + } + public Map getContextParameters() { return this.contextParameters; } + public Encoding getEncoding() { + return this.encoding; + } + public Jsp getJsp() { return this.jsp; } @@ -300,41 +307,9 @@ public static class Tomcat { private final Accesslog accesslog = new Accesslog(); /** - * Regular expression that matches proxies that are to be trusted. - */ - private String internalProxies = "10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" // 10/8 - + "192\\.168\\.\\d{1,3}\\.\\d{1,3}|" // 192.168/16 - + "169\\.254\\.\\d{1,3}\\.\\d{1,3}|" // 169.254/16 - + "127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" // 127/8 - + "172\\.1[6-9]{1}\\.\\d{1,3}\\.\\d{1,3}|" // 172.16/12 - + "172\\.2[0-9]{1}\\.\\d{1,3}\\.\\d{1,3}|172\\.3[0-1]{1}\\.\\d{1,3}\\.\\d{1,3}|" // - + "0:0:0:0:0:0:0:1|::1"; - - /** - * Header that holds the incoming protocol, usually named "X-Forwarded-Proto". - */ - private String protocolHeader; - - /** - * Value of the protocol header indicating whether the incoming request uses SSL. + * Thread related configuration. */ - private String protocolHeaderHttpsValue = "https"; - - /** - * Name of the HTTP header used to override the original port value. - */ - private String portHeader = "X-Forwarded-Port"; - - /** - * Name of the HTTP header from which the remote IP is extracted. For instance, - * `X-FORWARDED-FOR`. - */ - private String remoteIpHeader; - - /** - * Name of the HTTP header from which the remote host is extracted. - */ - private String hostHeader = "X-Forwarded-Host"; + private final Threads threads = new Threads(); /** * Tomcat base directory. If not specified, a temporary directory is used. @@ -348,16 +323,6 @@ public static class Tomcat { @DurationUnit(ChronoUnit.SECONDS) private Duration backgroundProcessorDelay = Duration.ofSeconds(10); - /** - * Maximum amount of worker threads. - */ - private int maxThreads = 200; - - /** - * Minimum amount of worker threads. - */ - private int minSpareThreads = 10; - /** * Maximum size of the form content in any HTTP post request. */ @@ -370,7 +335,8 @@ public static class Tomcat { /** * Whether requests to the context root should be redirected by appending a / to - * the path. + * the path. When using SSL terminated at a proxy, this property should be set to + * false. */ private Boolean redirectContextRoot = true; @@ -378,7 +344,7 @@ public static class Tomcat { * Whether HTTP 1.1 and later location headers generated by a call to sendRedirect * will use relative or absolute redirects. */ - private Boolean useRelativeRedirects; + private boolean useRelativeRedirects; /** * Character encoding to use to decode the URI. @@ -390,7 +356,7 @@ public static class Tomcat { * given time. Once the limit has been reached, the operating system may still * accept connections based on the "acceptCount" property. */ - private int maxConnections = 10000; + private int maxConnections = 8192; /** * Maximum queue length for incoming connection requests when all possible request @@ -405,6 +371,19 @@ public static class Tomcat { */ private int processorCache = 200; + /** + * Time to wait for another HTTP request before the connection is closed. When not + * set the connectionTimeout is used. When set to -1 there will be no timeout. + */ + private Duration keepAliveTimeout; + + /** + * Maximum number of HTTP requests that can be pipelined before the connection is + * closed. When set to 0 or 1, keep-alive and pipelining are disabled. When set to + * -1, an unlimited number of pipelined or keep-alive requests are allowed. + */ + private int maxKeepAliveRequests = 100; + /** * Comma-separated list of additional patterns that match jars to ignore for TLD * scanning. The special '?' and '*' characters can be used in the pattern to @@ -440,32 +419,10 @@ public static class Tomcat { */ private final Mbeanregistry mbeanregistry = new Mbeanregistry(); - public int getMaxThreads() { - return this.maxThreads; - } - - public void setMaxThreads(int maxThreads) { - this.maxThreads = maxThreads; - } - - public int getMinSpareThreads() { - return this.minSpareThreads; - } - - public void setMinSpareThreads(int minSpareThreads) { - this.minSpareThreads = minSpareThreads; - } - - @Deprecated - @DeprecatedConfigurationProperty(replacement = "server.tomcat.max-http-form-post-size") - public DataSize getMaxHttpPostSize() { - return this.maxHttpFormPostSize; - } - - @Deprecated - public void setMaxHttpPostSize(DataSize maxHttpPostSize) { - this.maxHttpFormPostSize = maxHttpPostSize; - } + /** + * Remote Ip Valve configuration. + */ + private final Remoteip remoteip = new Remoteip(); public DataSize getMaxHttpFormPostSize() { return this.maxHttpFormPostSize; @@ -479,6 +436,10 @@ public Accesslog getAccesslog() { return this.accesslog; } + public Threads getThreads() { + return this.threads; + } + public Duration getBackgroundProcessorDelay() { return this.backgroundProcessorDelay; } @@ -495,38 +456,6 @@ public void setBasedir(File basedir) { this.basedir = basedir; } - public String getInternalProxies() { - return this.internalProxies; - } - - public void setInternalProxies(String internalProxies) { - this.internalProxies = internalProxies; - } - - public String getProtocolHeader() { - return this.protocolHeader; - } - - public void setProtocolHeader(String protocolHeader) { - this.protocolHeader = protocolHeader; - } - - public String getProtocolHeaderHttpsValue() { - return this.protocolHeaderHttpsValue; - } - - public void setProtocolHeaderHttpsValue(String protocolHeaderHttpsValue) { - this.protocolHeaderHttpsValue = protocolHeaderHttpsValue; - } - - public String getPortHeader() { - return this.portHeader; - } - - public void setPortHeader(String portHeader) { - this.portHeader = portHeader; - } - public Boolean getRedirectContextRoot() { return this.redirectContextRoot; } @@ -535,30 +464,14 @@ public void setRedirectContextRoot(Boolean redirectContextRoot) { this.redirectContextRoot = redirectContextRoot; } - public Boolean getUseRelativeRedirects() { + public boolean isUseRelativeRedirects() { return this.useRelativeRedirects; } - public void setUseRelativeRedirects(Boolean useRelativeRedirects) { + public void setUseRelativeRedirects(boolean useRelativeRedirects) { this.useRelativeRedirects = useRelativeRedirects; } - public String getRemoteIpHeader() { - return this.remoteIpHeader; - } - - public void setRemoteIpHeader(String remoteIpHeader) { - this.remoteIpHeader = remoteIpHeader; - } - - public String getHostHeader() { - return this.hostHeader; - } - - public void setHostHeader(String hostHeader) { - this.hostHeader = hostHeader; - } - public Charset getUriEncoding() { return this.uriEncoding; } @@ -599,6 +512,22 @@ public void setProcessorCache(int processorCache) { this.processorCache = processorCache; } + public Duration getKeepAliveTimeout() { + return this.keepAliveTimeout; + } + + public void setKeepAliveTimeout(Duration keepAliveTimeout) { + this.keepAliveTimeout = keepAliveTimeout; + } + + public int getMaxKeepAliveRequests() { + return this.maxKeepAliveRequests; + } + + public void setMaxKeepAliveRequests(int maxKeepAliveRequests) { + this.maxKeepAliveRequests = maxKeepAliveRequests; + } + public List getAdditionalTldSkipPatterns() { return this.additionalTldSkipPatterns; } @@ -639,6 +568,10 @@ public Mbeanregistry getMbeanregistry() { return this.mbeanregistry; } + public Remoteip getRemoteip() { + return this.remoteip; + } + /** * Tomcat access log properties. */ @@ -875,6 +808,39 @@ public void setBuffered(boolean buffered) { } + /** + * Tomcat thread properties. + */ + public static class Threads { + + /** + * Maximum amount of worker threads. + */ + private int max = 200; + + /** + * Minimum amount of worker threads. + */ + private int minSpare = 10; + + public int getMax() { + return this.max; + } + + public void setMax(int max) { + this.max = max; + } + + public int getMinSpare() { + return this.minSpare; + } + + public void setMinSpare(int minSpare) { + this.minSpare = minSpare; + } + + } + /** * Tomcat static resource properties. */ @@ -925,6 +891,96 @@ public void setEnabled(boolean enabled) { } + public static class Remoteip { + + /** + * Regular expression that matches proxies that are to be trusted. + */ + private String internalProxies = "10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" // 10/8 + + "192\\.168\\.\\d{1,3}\\.\\d{1,3}|" // 192.168/16 + + "169\\.254\\.\\d{1,3}\\.\\d{1,3}|" // 169.254/16 + + "127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" // 127/8 + + "172\\.1[6-9]{1}\\.\\d{1,3}\\.\\d{1,3}|" // 172.16/12 + + "172\\.2[0-9]{1}\\.\\d{1,3}\\.\\d{1,3}|172\\.3[0-1]{1}\\.\\d{1,3}\\.\\d{1,3}|" // + + "0:0:0:0:0:0:0:1|::1"; + + /** + * Header that holds the incoming protocol, usually named "X-Forwarded-Proto". + */ + private String protocolHeader; + + /** + * Value of the protocol header indicating whether the incoming request uses + * SSL. + */ + private String protocolHeaderHttpsValue = "https"; + + /** + * Name of the HTTP header from which the remote host is extracted. + */ + private String hostHeader = "X-Forwarded-Host"; + + /** + * Name of the HTTP header used to override the original port value. + */ + private String portHeader = "X-Forwarded-Port"; + + /** + * Name of the HTTP header from which the remote IP is extracted. For + * instance, 'X-FORWARDED-FOR'. + */ + private String remoteIpHeader; + + public String getInternalProxies() { + return this.internalProxies; + } + + public void setInternalProxies(String internalProxies) { + this.internalProxies = internalProxies; + } + + public String getProtocolHeader() { + return this.protocolHeader; + } + + public void setProtocolHeader(String protocolHeader) { + this.protocolHeader = protocolHeader; + } + + public String getProtocolHeaderHttpsValue() { + return this.protocolHeaderHttpsValue; + } + + public String getHostHeader() { + return this.hostHeader; + } + + public void setHostHeader(String hostHeader) { + this.hostHeader = hostHeader; + } + + public void setProtocolHeaderHttpsValue(String protocolHeaderHttpsValue) { + this.protocolHeaderHttpsValue = protocolHeaderHttpsValue; + } + + public String getPortHeader() { + return this.portHeader; + } + + public void setPortHeader(String portHeader) { + this.portHeader = portHeader; + } + + public String getRemoteIpHeader() { + return this.remoteIpHeader; + } + + public void setRemoteIpHeader(String remoteIpHeader) { + this.remoteIpHeader = remoteIpHeader; + } + + } + } /** @@ -938,36 +994,14 @@ public static class Jetty { private final Accesslog accesslog = new Accesslog(); /** - * Maximum size of the form content in any HTTP post request. - */ - private DataSize maxHttpFormPostSize = DataSize.ofBytes(200000); - - /** - * Number of acceptor threads to use. When the value is -1, the default, the - * number of acceptors is derived from the operating environment. + * Thread related configuration. */ - private Integer acceptors = -1; + private final Threads threads = new Threads(); /** - * Number of selector threads to use. When the value is -1, the default, the - * number of selectors is derived from the operating environment. - */ - private Integer selectors = -1; - - /** - * Maximum number of threads. - */ - private Integer maxThreads = 200; - - /** - * Minimum number of threads. - */ - private Integer minThreads = 8; - - /** - * Maximum thread idle time. + * Maximum size of the form content in any HTTP post request. */ - private Duration threadIdleTimeout = Duration.ofMillis(60000); + private DataSize maxHttpFormPostSize = DataSize.ofBytes(200000); /** * Time that the connection can be idle before it is closed. @@ -978,15 +1012,8 @@ public Accesslog getAccesslog() { return this.accesslog; } - @Deprecated - @DeprecatedConfigurationProperty(replacement = "server.jetty.max-http-form-post-size") - public DataSize getMaxHttpPostSize() { - return this.maxHttpFormPostSize; - } - - @Deprecated - public void setMaxHttpPostSize(DataSize maxHttpPostSize) { - this.maxHttpFormPostSize = maxHttpPostSize; + public Threads getThreads() { + return this.threads; } public DataSize getMaxHttpFormPostSize() { @@ -997,46 +1024,6 @@ public void setMaxHttpFormPostSize(DataSize maxHttpFormPostSize) { this.maxHttpFormPostSize = maxHttpFormPostSize; } - public Integer getAcceptors() { - return this.acceptors; - } - - public void setAcceptors(Integer acceptors) { - this.acceptors = acceptors; - } - - public Integer getSelectors() { - return this.selectors; - } - - public void setSelectors(Integer selectors) { - this.selectors = selectors; - } - - public void setMinThreads(Integer minThreads) { - this.minThreads = minThreads; - } - - public Integer getMinThreads() { - return this.minThreads; - } - - public void setMaxThreads(Integer maxThreads) { - this.maxThreads = maxThreads; - } - - public Integer getMaxThreads() { - return this.maxThreads; - } - - public void setThreadIdleTimeout(Duration threadIdleTimeout) { - this.threadIdleTimeout = threadIdleTimeout; - } - - public Duration getThreadIdleTimeout() { - return this.threadIdleTimeout; - } - public Duration getConnectionIdleTimeout() { return this.connectionIdleTimeout; } @@ -1175,6 +1162,94 @@ public enum FORMAT { } + /** + * Jetty thread properties. + */ + public static class Threads { + + /** + * Number of acceptor threads to use. When the value is -1, the default, the + * number of acceptors is derived from the operating environment. + */ + private Integer acceptors = -1; + + /** + * Number of selector threads to use. When the value is -1, the default, the + * number of selectors is derived from the operating environment. + */ + private Integer selectors = -1; + + /** + * Maximum number of threads. + */ + private Integer max = 200; + + /** + * Minimum number of threads. + */ + private Integer min = 8; + + /** + * Maximum capacity of the thread pool's backing queue. A default is computed + * based on the threading configuration. + */ + private Integer maxQueueCapacity; + + /** + * Maximum thread idle time. + */ + private Duration idleTimeout = Duration.ofMillis(60000); + + public Integer getAcceptors() { + return this.acceptors; + } + + public void setAcceptors(Integer acceptors) { + this.acceptors = acceptors; + } + + public Integer getSelectors() { + return this.selectors; + } + + public void setSelectors(Integer selectors) { + this.selectors = selectors; + } + + public void setMin(Integer min) { + this.min = min; + } + + public Integer getMin() { + return this.min; + } + + public void setMax(Integer max) { + this.max = max; + } + + public Integer getMax() { + return this.max; + } + + public Integer getMaxQueueCapacity() { + return this.maxQueueCapacity; + } + + public void setMaxQueueCapacity(Integer maxQueueCapacity) { + this.maxQueueCapacity = maxQueueCapacity; + } + + public void setIdleTimeout(Duration idleTimeout) { + this.idleTimeout = idleTimeout; + } + + public Duration getIdleTimeout() { + return this.idleTimeout; + } + + } + } /** @@ -1187,6 +1262,31 @@ public static class Netty { */ private Duration connectionTimeout; + /** + * Maximum content length of an H2C upgrade request. + */ + private DataSize h2cMaxContentLength = DataSize.ofBytes(0); + + /** + * Initial buffer size for HTTP request decoding. + */ + private DataSize initialBufferSize = DataSize.ofBytes(128); + + /** + * Maximum chunk size that can be decoded for an HTTP request. + */ + private DataSize maxChunkSize = DataSize.ofKilobytes(8); + + /** + * Maximum length that can be decoded for an HTTP request's initial line. + */ + private DataSize maxInitialLineLength = DataSize.ofKilobytes(4); + + /** + * Whether to validate headers when decoding requests. + */ + private boolean validateHeaders = true; + public Duration getConnectionTimeout() { return this.connectionTimeout; } @@ -1195,6 +1295,46 @@ public void setConnectionTimeout(Duration connectionTimeout) { this.connectionTimeout = connectionTimeout; } + public DataSize getH2cMaxContentLength() { + return this.h2cMaxContentLength; + } + + public void setH2cMaxContentLength(DataSize h2cMaxContentLength) { + this.h2cMaxContentLength = h2cMaxContentLength; + } + + public DataSize getInitialBufferSize() { + return this.initialBufferSize; + } + + public void setInitialBufferSize(DataSize initialBufferSize) { + this.initialBufferSize = initialBufferSize; + } + + public DataSize getMaxChunkSize() { + return this.maxChunkSize; + } + + public void setMaxChunkSize(DataSize maxChunkSize) { + this.maxChunkSize = maxChunkSize; + } + + public DataSize getMaxInitialLineLength() { + return this.maxInitialLineLength; + } + + public void setMaxInitialLineLength(DataSize maxInitialLineLength) { + this.maxInitialLineLength = maxInitialLineLength; + } + + public boolean isValidateHeaders() { + return this.validateHeaders; + } + + public void setValidateHeaders(boolean validateHeaders) { + this.validateHeaders = validateHeaders; + } + } /** @@ -1214,17 +1354,6 @@ public static class Undertow { */ private DataSize bufferSize; - /** - * Number of I/O threads to create for the worker. The default is derived from the - * number of available processors. - */ - private Integer ioThreads; - - /** - * Number of worker threads. The default is 8 times the number of I/O threads. - */ - private Integer workerThreads; - /** * Whether to allocate buffers outside the Java heap. The default is derived from * the maximum amount of memory that is available to the JVM. @@ -1285,8 +1414,18 @@ public static class Undertow { */ private Duration noRequestTimeout; + /** + * Whether to preserve the path of a request when it is forwarded. + */ + private boolean preservePathOnForward = false; + private final Accesslog accesslog = new Accesslog(); + /** + * Thread related configuration. + */ + private final Threads threads = new Threads(); + private final Options options = new Options(); public DataSize getMaxHttpPostSize() { @@ -1305,22 +1444,6 @@ public void setBufferSize(DataSize bufferSize) { this.bufferSize = bufferSize; } - public Integer getIoThreads() { - return this.ioThreads; - } - - public void setIoThreads(Integer ioThreads) { - this.ioThreads = ioThreads; - } - - public Integer getWorkerThreads() { - return this.workerThreads; - } - - public void setWorkerThreads(Integer workerThreads) { - this.workerThreads = workerThreads; - } - public Boolean getDirectBuffers() { return this.directBuffers; } @@ -1401,10 +1524,22 @@ public void setNoRequestTimeout(Duration noRequestTimeout) { this.noRequestTimeout = noRequestTimeout; } + public boolean isPreservePathOnForward() { + return this.preservePathOnForward; + } + + public void setPreservePathOnForward(boolean preservePathOnForward) { + this.preservePathOnForward = preservePathOnForward; + } + public Accesslog getAccesslog() { return this.accesslog; } + public Threads getThreads() { + return this.threads; + } + public Options getOptions() { return this.options; } @@ -1494,6 +1629,40 @@ public void setRotate(boolean rotate) { } + /** + * Undertow thread properties. + */ + public static class Threads { + + /** + * Number of I/O threads to create for the worker. The default is derived from + * the number of available processors. + */ + private Integer io; + + /** + * Number of worker threads. The default is 8 times the number of I/O threads. + */ + private Integer worker; + + public Integer getIo() { + return this.io; + } + + public void setIo(Integer io) { + this.io = io; + } + + public Integer getWorker() { + return this.worker; + } + + public void setWorker(Integer worker) { + this.worker = worker; + } + + } + public static class Options { private Map socket = new LinkedHashMap<>(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebProperties.java new file mode 100644 index 000000000000..2127c227ff02 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebProperties.java @@ -0,0 +1,613 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.web; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.convert.DurationUnit; +import org.springframework.http.CacheControl; + +/** + * {@link ConfigurationProperties Configuration properties} for general web concerns. + * + * @author Andy Wilkinson + * @since 2.4.0 + */ +@ConfigurationProperties("spring.web") +public class WebProperties { + + /** + * Locale to use. By default, this locale is overridden by the "Accept-Language" + * header. + */ + private Locale locale; + + /** + * Define how the locale should be resolved. + */ + private LocaleResolver localeResolver = LocaleResolver.ACCEPT_HEADER; + + private final Resources resources = new Resources(); + + public Locale getLocale() { + return this.locale; + } + + public void setLocale(Locale locale) { + this.locale = locale; + } + + public LocaleResolver getLocaleResolver() { + return this.localeResolver; + } + + public void setLocaleResolver(LocaleResolver localeResolver) { + this.localeResolver = localeResolver; + } + + public Resources getResources() { + return this.resources; + } + + public enum LocaleResolver { + + /** + * Always use the configured locale. + */ + FIXED, + + /** + * Use the "Accept-Language" header or the configured locale if the header is not + * set. + */ + ACCEPT_HEADER + + } + + public static class Resources { + + private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/", + "classpath:/resources/", "classpath:/static/", "classpath:/public/" }; + + /** + * Locations of static resources. Defaults to classpath:[/META-INF/resources/, + * /resources/, /static/, /public/]. + */ + private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS; + + /** + * Whether to enable default resource handling. + */ + private boolean addMappings = true; + + private boolean customized = false; + + private final Chain chain = new Chain(); + + private final Cache cache = new Cache(); + + public String[] getStaticLocations() { + return this.staticLocations; + } + + public void setStaticLocations(String[] staticLocations) { + this.staticLocations = appendSlashIfNecessary(staticLocations); + this.customized = true; + } + + private String[] appendSlashIfNecessary(String[] staticLocations) { + String[] normalized = new String[staticLocations.length]; + for (int i = 0; i < staticLocations.length; i++) { + String location = staticLocations[i]; + normalized[i] = location.endsWith("/") ? location : location + "/"; + } + return normalized; + } + + public boolean isAddMappings() { + return this.addMappings; + } + + public void setAddMappings(boolean addMappings) { + this.customized = true; + this.addMappings = addMappings; + } + + public Chain getChain() { + return this.chain; + } + + public Cache getCache() { + return this.cache; + } + + public boolean hasBeenCustomized() { + return this.customized || getChain().hasBeenCustomized() || getCache().hasBeenCustomized(); + } + + /** + * Configuration for the Spring Resource Handling chain. + */ + public static class Chain { + + boolean customized = false; + + /** + * Whether to enable the Spring Resource Handling chain. By default, disabled + * unless at least one strategy has been enabled. + */ + private Boolean enabled; + + /** + * Whether to enable caching in the Resource chain. + */ + private boolean cache = true; + + /** + * Whether to enable resolution of already compressed resources (gzip, + * brotli). Checks for a resource name with the '.gz' or '.br' file + * extensions. + */ + private boolean compressed = false; + + private final Strategy strategy = new Strategy(); + + /** + * Return whether the resource chain is enabled. Return {@code null} if no + * specific settings are present. + * @return whether the resource chain is enabled or {@code null} if no + * specified settings are present. + */ + public Boolean getEnabled() { + return getEnabled(getStrategy().getFixed().isEnabled(), getStrategy().getContent().isEnabled(), + this.enabled); + } + + private boolean hasBeenCustomized() { + return this.customized || getStrategy().hasBeenCustomized(); + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + this.customized = true; + } + + public boolean isCache() { + return this.cache; + } + + public void setCache(boolean cache) { + this.cache = cache; + this.customized = true; + } + + public Strategy getStrategy() { + return this.strategy; + } + + public boolean isCompressed() { + return this.compressed; + } + + public void setCompressed(boolean compressed) { + this.compressed = compressed; + this.customized = true; + } + + static Boolean getEnabled(boolean fixedEnabled, boolean contentEnabled, Boolean chainEnabled) { + return (fixedEnabled || contentEnabled) ? Boolean.TRUE : chainEnabled; + } + + /** + * Strategies for extracting and embedding a resource version in its URL path. + */ + public static class Strategy { + + private final Fixed fixed = new Fixed(); + + private final Content content = new Content(); + + public Fixed getFixed() { + return this.fixed; + } + + public Content getContent() { + return this.content; + } + + private boolean hasBeenCustomized() { + return getFixed().hasBeenCustomized() || getContent().hasBeenCustomized(); + } + + /** + * Version Strategy based on content hashing. + */ + public static class Content { + + private boolean customized = false; + + /** + * Whether to enable the content Version Strategy. + */ + private boolean enabled; + + /** + * Comma-separated list of patterns to apply to the content Version + * Strategy. + */ + private String[] paths = new String[] { "/**" }; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.customized = true; + this.enabled = enabled; + } + + public String[] getPaths() { + return this.paths; + } + + public void setPaths(String[] paths) { + this.customized = true; + this.paths = paths; + } + + private boolean hasBeenCustomized() { + return this.customized; + } + + } + + /** + * Version Strategy based on a fixed version string. + */ + public static class Fixed { + + private boolean customized = false; + + /** + * Whether to enable the fixed Version Strategy. + */ + private boolean enabled; + + /** + * Comma-separated list of patterns to apply to the fixed Version + * Strategy. + */ + private String[] paths = new String[] { "/**" }; + + /** + * Version string to use for the fixed Version Strategy. + */ + private String version; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.customized = true; + this.enabled = enabled; + } + + public String[] getPaths() { + return this.paths; + } + + public void setPaths(String[] paths) { + this.customized = true; + this.paths = paths; + } + + public String getVersion() { + return this.version; + } + + public void setVersion(String version) { + this.customized = true; + this.version = version; + } + + private boolean hasBeenCustomized() { + return this.customized; + } + + } + + } + + } + + /** + * Cache configuration. + */ + public static class Cache { + + private boolean customized = false; + + /** + * Cache period for the resources served by the resource handler. If a + * duration suffix is not specified, seconds will be used. Can be overridden + * by the 'spring.web.resources.cache.cachecontrol' properties. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration period; + + /** + * Cache control HTTP headers, only allows valid directive combinations. + * Overrides the 'spring.web.resources.cache.period' property. + */ + private final Cachecontrol cachecontrol = new Cachecontrol(); + + /** + * Whether we should use the "lastModified" metadata of the files in HTTP + * caching headers. + */ + private boolean useLastModified = true; + + public Duration getPeriod() { + return this.period; + } + + public void setPeriod(Duration period) { + this.customized = true; + this.period = period; + } + + public Cachecontrol getCachecontrol() { + return this.cachecontrol; + } + + public boolean isUseLastModified() { + return this.useLastModified; + } + + public void setUseLastModified(boolean useLastModified) { + this.useLastModified = useLastModified; + } + + private boolean hasBeenCustomized() { + return this.customized || getCachecontrol().hasBeenCustomized(); + } + + /** + * Cache Control HTTP header configuration. + */ + public static class Cachecontrol { + + private boolean customized = false; + + /** + * Maximum time the response should be cached, in seconds if no duration + * suffix is not specified. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration maxAge; + + /** + * Indicate that the cached response can be reused only if re-validated + * with the server. + */ + private Boolean noCache; + + /** + * Indicate to not cache the response in any case. + */ + private Boolean noStore; + + /** + * Indicate that once it has become stale, a cache must not use the + * response without re-validating it with the server. + */ + private Boolean mustRevalidate; + + /** + * Indicate intermediaries (caches and others) that they should not + * transform the response content. + */ + private Boolean noTransform; + + /** + * Indicate that any cache may store the response. + */ + private Boolean cachePublic; + + /** + * Indicate that the response message is intended for a single user and + * must not be stored by a shared cache. + */ + private Boolean cachePrivate; + + /** + * Same meaning as the "must-revalidate" directive, except that it does + * not apply to private caches. + */ + private Boolean proxyRevalidate; + + /** + * Maximum time the response can be served after it becomes stale, in + * seconds if no duration suffix is not specified. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration staleWhileRevalidate; + + /** + * Maximum time the response may be used when errors are encountered, in + * seconds if no duration suffix is not specified. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration staleIfError; + + /** + * Maximum time the response should be cached by shared caches, in seconds + * if no duration suffix is not specified. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration sMaxAge; + + public Duration getMaxAge() { + return this.maxAge; + } + + public void setMaxAge(Duration maxAge) { + this.customized = true; + this.maxAge = maxAge; + } + + public Boolean getNoCache() { + return this.noCache; + } + + public void setNoCache(Boolean noCache) { + this.customized = true; + this.noCache = noCache; + } + + public Boolean getNoStore() { + return this.noStore; + } + + public void setNoStore(Boolean noStore) { + this.customized = true; + this.noStore = noStore; + } + + public Boolean getMustRevalidate() { + return this.mustRevalidate; + } + + public void setMustRevalidate(Boolean mustRevalidate) { + this.customized = true; + this.mustRevalidate = mustRevalidate; + } + + public Boolean getNoTransform() { + return this.noTransform; + } + + public void setNoTransform(Boolean noTransform) { + this.customized = true; + this.noTransform = noTransform; + } + + public Boolean getCachePublic() { + return this.cachePublic; + } + + public void setCachePublic(Boolean cachePublic) { + this.customized = true; + this.cachePublic = cachePublic; + } + + public Boolean getCachePrivate() { + return this.cachePrivate; + } + + public void setCachePrivate(Boolean cachePrivate) { + this.customized = true; + this.cachePrivate = cachePrivate; + } + + public Boolean getProxyRevalidate() { + return this.proxyRevalidate; + } + + public void setProxyRevalidate(Boolean proxyRevalidate) { + this.customized = true; + this.proxyRevalidate = proxyRevalidate; + } + + public Duration getStaleWhileRevalidate() { + return this.staleWhileRevalidate; + } + + public void setStaleWhileRevalidate(Duration staleWhileRevalidate) { + this.customized = true; + this.staleWhileRevalidate = staleWhileRevalidate; + } + + public Duration getStaleIfError() { + return this.staleIfError; + } + + public void setStaleIfError(Duration staleIfError) { + this.customized = true; + this.staleIfError = staleIfError; + } + + public Duration getSMaxAge() { + return this.sMaxAge; + } + + public void setSMaxAge(Duration sMaxAge) { + this.customized = true; + this.sMaxAge = sMaxAge; + } + + public CacheControl toHttpCacheControl() { + PropertyMapper map = PropertyMapper.get(); + CacheControl control = createCacheControl(); + map.from(this::getMustRevalidate).whenTrue().toCall(control::mustRevalidate); + map.from(this::getNoTransform).whenTrue().toCall(control::noTransform); + map.from(this::getCachePublic).whenTrue().toCall(control::cachePublic); + map.from(this::getCachePrivate).whenTrue().toCall(control::cachePrivate); + map.from(this::getProxyRevalidate).whenTrue().toCall(control::proxyRevalidate); + map.from(this::getStaleWhileRevalidate).whenNonNull() + .to((duration) -> control.staleWhileRevalidate(duration.getSeconds(), TimeUnit.SECONDS)); + map.from(this::getStaleIfError).whenNonNull() + .to((duration) -> control.staleIfError(duration.getSeconds(), TimeUnit.SECONDS)); + map.from(this::getSMaxAge).whenNonNull() + .to((duration) -> control.sMaxAge(duration.getSeconds(), TimeUnit.SECONDS)); + // check if cacheControl remained untouched + if (control.getHeaderValue() == null) { + return null; + } + return control; + } + + private CacheControl createCacheControl() { + if (Boolean.TRUE.equals(this.noStore)) { + return CacheControl.noStore(); + } + if (Boolean.TRUE.equals(this.noCache)) { + return CacheControl.noCache(); + } + if (this.maxAge != null) { + return CacheControl.maxAge(this.maxAge.getSeconds(), TimeUnit.SECONDS); + } + return CacheControl.empty(); + } + + private boolean hasBeenCustomized() { + return this.customized; + } + + } + + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfiguration.java index bd8536c3f034..970f600c27d3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,6 @@ package org.springframework.boot.autoconfigure.web.client; -import java.util.Collection; -import java.util.List; -import java.util.function.BiFunction; import java.util.stream.Collectors; import org.springframework.beans.factory.ObjectProvider; @@ -38,6 +35,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; import org.springframework.web.client.RestTemplate; /** @@ -54,27 +52,26 @@ public class RestTemplateAutoConfiguration { @Bean + @Lazy @ConditionalOnMissingBean - public RestTemplateBuilder restTemplateBuilder(ObjectProvider messageConverters, + public RestTemplateBuilderConfigurer restTemplateBuilderConfigurer( + ObjectProvider messageConverters, ObjectProvider restTemplateCustomizers, ObjectProvider> restTemplateRequestCustomizers) { - RestTemplateBuilder builder = new RestTemplateBuilder(); - HttpMessageConverters converters = messageConverters.getIfUnique(); - if (converters != null) { - builder = builder.messageConverters(converters.getConverters()); - } - builder = addCustomizers(builder, restTemplateCustomizers, RestTemplateBuilder::customizers); - builder = addCustomizers(builder, restTemplateRequestCustomizers, RestTemplateBuilder::requestCustomizers); - return builder; + RestTemplateBuilderConfigurer configurer = new RestTemplateBuilderConfigurer(); + configurer.setHttpMessageConverters(messageConverters.getIfUnique()); + configurer.setRestTemplateCustomizers(restTemplateCustomizers.orderedStream().collect(Collectors.toList())); + configurer.setRestTemplateRequestCustomizers( + restTemplateRequestCustomizers.orderedStream().collect(Collectors.toList())); + return configurer; } - private RestTemplateBuilder addCustomizers(RestTemplateBuilder builder, ObjectProvider objectProvider, - BiFunction, RestTemplateBuilder> method) { - List customizers = objectProvider.orderedStream().collect(Collectors.toList()); - if (!customizers.isEmpty()) { - return method.apply(builder, customizers); - } - return builder; + @Bean + @Lazy + @ConditionalOnMissingBean + public RestTemplateBuilder restTemplateBuilder(RestTemplateBuilderConfigurer restTemplateBuilderConfigurer) { + RestTemplateBuilder builder = new RestTemplateBuilder(); + return restTemplateBuilderConfigurer.configure(builder); } static class NotReactiveWebApplicationCondition extends NoneNestedConditions { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestTemplateBuilderConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestTemplateBuilderConfigurer.java new file mode 100644 index 000000000000..55e3351b85ff --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestTemplateBuilderConfigurer.java @@ -0,0 +1,78 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.web.client; + +import java.util.Collection; +import java.util.List; +import java.util.function.BiFunction; + +import org.springframework.boot.autoconfigure.http.HttpMessageConverters; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.boot.web.client.RestTemplateCustomizer; +import org.springframework.boot.web.client.RestTemplateRequestCustomizer; +import org.springframework.util.ObjectUtils; + +/** + * Configure {@link RestTemplateBuilder} with sensible defaults. + * + * @author Stephane Nicoll + * @since 2.4.0 + */ +public final class RestTemplateBuilderConfigurer { + + private HttpMessageConverters httpMessageConverters; + + private List restTemplateCustomizers; + + private List> restTemplateRequestCustomizers; + + void setHttpMessageConverters(HttpMessageConverters httpMessageConverters) { + this.httpMessageConverters = httpMessageConverters; + } + + void setRestTemplateCustomizers(List restTemplateCustomizers) { + this.restTemplateCustomizers = restTemplateCustomizers; + } + + void setRestTemplateRequestCustomizers(List> restTemplateRequestCustomizers) { + this.restTemplateRequestCustomizers = restTemplateRequestCustomizers; + } + + /** + * Configure the specified {@link RestTemplateBuilder}. The builder can be further + * tuned and default settings can be overridden. + * @param builder the {@link RestTemplateBuilder} instance to configure + * @return the configured builder + */ + public RestTemplateBuilder configure(RestTemplateBuilder builder) { + if (this.httpMessageConverters != null) { + builder = builder.messageConverters(this.httpMessageConverters.getConverters()); + } + builder = addCustomizers(builder, this.restTemplateCustomizers, RestTemplateBuilder::customizers); + builder = addCustomizers(builder, this.restTemplateRequestCustomizers, RestTemplateBuilder::requestCustomizers); + return builder; + } + + private RestTemplateBuilder addCustomizers(RestTemplateBuilder builder, List customizers, + BiFunction, RestTemplateBuilder> method) { + if (!ObjectUtils.isEmpty(customizers)) { + return method.apply(builder, customizers); + } + return builder; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizer.java index e0047352802b..d5d0f8d253f9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,8 @@ import java.time.Duration; import java.util.Arrays; -import java.util.function.Consumer; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.SynchronousQueue; import org.eclipse.jetty.server.AbstractConnector; import org.eclipse.jetty.server.ConnectionFactory; @@ -30,6 +31,7 @@ import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.HandlerCollection; import org.eclipse.jetty.server.handler.HandlerWrapper; +import org.eclipse.jetty.util.BlockingArrayQueue; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.util.thread.ThreadPool; @@ -75,22 +77,16 @@ public void customize(ConfigurableJettyWebServerFactory factory) { ServerProperties properties = this.serverProperties; ServerProperties.Jetty jettyProperties = properties.getJetty(); factory.setUseForwardHeaders(getOrDeduceUseForwardHeaders()); + ServerProperties.Jetty.Threads threadProperties = jettyProperties.getThreads(); + factory.setThreadPool(determineThreadPool(jettyProperties.getThreads())); PropertyMapper propertyMapper = PropertyMapper.get(); - propertyMapper.from(jettyProperties::getAcceptors).whenNonNull().to(factory::setAcceptors); - propertyMapper.from(jettyProperties::getSelectors).whenNonNull().to(factory::setSelectors); + propertyMapper.from(threadProperties::getAcceptors).whenNonNull().to(factory::setAcceptors); + propertyMapper.from(threadProperties::getSelectors).whenNonNull().to(factory::setSelectors); propertyMapper.from(properties::getMaxHttpHeaderSize).whenNonNull().asInt(DataSize::toBytes) .when(this::isPositive).to((maxHttpHeaderSize) -> factory .addServerCustomizers(new MaxHttpHeaderSizeCustomizer(maxHttpHeaderSize))); propertyMapper.from(jettyProperties::getMaxHttpFormPostSize).asInt(DataSize::toBytes).when(this::isPositive) .to((maxHttpFormPostSize) -> customizeMaxHttpFormPostSize(factory, maxHttpFormPostSize)); - propertyMapper.from(jettyProperties::getMaxThreads).when(this::isPositive) - .to((maxThreads) -> customizeThreadPool(factory, (threadPool) -> threadPool.setMaxThreads(maxThreads))); - propertyMapper.from(jettyProperties::getMinThreads).when(this::isPositive) - .to((minThreads) -> customizeThreadPool(factory, (threadPool) -> threadPool.setMinThreads(minThreads))); - propertyMapper.from(jettyProperties::getThreadIdleTimeout).whenNonNull().asInt(Duration::toMillis).to( - (idleTimeout) -> customizeThreadPool(factory, (threadPool) -> threadPool.setIdleTimeout(idleTimeout))); - propertyMapper.from(properties::getConnectionTimeout).whenNonNull() - .to((connectionTimeout) -> customizeIdleTimeout(factory, connectionTimeout)); propertyMapper.from(jettyProperties::getConnectionIdleTimeout).whenNonNull() .to((idleTimeout) -> customizeIdleTimeout(factory, idleTimeout)); propertyMapper.from(jettyProperties::getAccesslog).when(ServerProperties.Jetty.Accesslog::isEnabled) @@ -102,7 +98,7 @@ private boolean isPositive(Integer value) { } private boolean getOrDeduceUseForwardHeaders() { - if (this.serverProperties.getForwardHeadersStrategy().equals(ServerProperties.ForwardHeadersStrategy.NONE)) { + if (this.serverProperties.getForwardHeadersStrategy() == null) { CloudPlatform platform = CloudPlatform.getActive(this.environment); return platform != null && platform.isUsingForwardHeaders(); } @@ -144,13 +140,25 @@ else if (handler instanceof HandlerCollection) { }); } - private void customizeThreadPool(ConfigurableJettyWebServerFactory factory, Consumer customizer) { - factory.addServerCustomizers((connector) -> { - ThreadPool threadPool = connector.getThreadPool(); - if (threadPool instanceof QueuedThreadPool) { - customizer.accept((QueuedThreadPool) threadPool); - } - }); + private ThreadPool determineThreadPool(ServerProperties.Jetty.Threads properties) { + BlockingQueue queue = determineBlockingQueue(properties.getMaxQueueCapacity()); + int maxThreadCount = (properties.getMax() > 0) ? properties.getMax() : 200; + int minThreadCount = (properties.getMin() > 0) ? properties.getMin() : 8; + int threadIdleTimeout = (properties.getIdleTimeout() != null) ? (int) properties.getIdleTimeout().toMillis() + : 60000; + return new QueuedThreadPool(maxThreadCount, minThreadCount, threadIdleTimeout, queue); + } + + private BlockingQueue determineBlockingQueue(Integer maxQueueCapacity) { + if (maxQueueCapacity == null) { + return null; + } + if (maxQueueCapacity == 0) { + return new SynchronousQueue<>(); + } + else { + return new BlockingArrayQueue<>(maxQueueCapacity); + } } private void customizeAccessLog(ConfigurableJettyWebServerFactory factory, diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizer.java index 607f2ce1bc23..2e807bd6f979 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,6 @@ import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.core.Ordered; import org.springframework.core.env.Environment; -import org.springframework.util.unit.DataSize; /** * Customization for Netty-specific features. @@ -58,39 +57,45 @@ public int getOrder() { public void customize(NettyReactiveWebServerFactory factory) { factory.setUseForwardHeaders(getOrDeduceUseForwardHeaders()); PropertyMapper propertyMapper = PropertyMapper.get().alwaysApplyingWhenNonNull(); - propertyMapper.from(this.serverProperties::getMaxHttpHeaderSize) - .to((maxHttpRequestHeaderSize) -> customizeMaxHttpHeaderSize(factory, maxHttpRequestHeaderSize)); - propertyMapper.from(this.serverProperties::getConnectionTimeout) - .to((connectionTimeout) -> customizeGenericConnectionTimeout(factory, connectionTimeout)); ServerProperties.Netty nettyProperties = this.serverProperties.getNetty(); propertyMapper.from(nettyProperties::getConnectionTimeout).whenNonNull() .to((connectionTimeout) -> customizeConnectionTimeout(factory, connectionTimeout)); + customizeRequestDecoder(factory, propertyMapper); } private boolean getOrDeduceUseForwardHeaders() { - if (this.serverProperties.getForwardHeadersStrategy().equals(ServerProperties.ForwardHeadersStrategy.NONE)) { + if (this.serverProperties.getForwardHeadersStrategy() == null) { CloudPlatform platform = CloudPlatform.getActive(this.environment); return platform != null && platform.isUsingForwardHeaders(); } return this.serverProperties.getForwardHeadersStrategy().equals(ServerProperties.ForwardHeadersStrategy.NATIVE); } - private void customizeMaxHttpHeaderSize(NettyReactiveWebServerFactory factory, DataSize maxHttpHeaderSize) { - factory.addServerCustomizers((httpServer) -> httpServer.httpRequestDecoder( - (httpRequestDecoderSpec) -> httpRequestDecoderSpec.maxHeaderSize((int) maxHttpHeaderSize.toBytes()))); - } - - private void customizeGenericConnectionTimeout(NettyReactiveWebServerFactory factory, Duration connectionTimeout) { - if (!connectionTimeout.isZero()) { - long timeoutMillis = connectionTimeout.isNegative() ? 0 : connectionTimeout.toMillis(); - factory.addServerCustomizers((httpServer) -> httpServer.tcpConfiguration((tcpServer) -> tcpServer - .selectorOption(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) timeoutMillis))); - } + private void customizeConnectionTimeout(NettyReactiveWebServerFactory factory, Duration connectionTimeout) { + factory.addServerCustomizers((httpServer) -> httpServer.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, + (int) connectionTimeout.toMillis())); } - private void customizeConnectionTimeout(NettyReactiveWebServerFactory factory, Duration connectionTimeout) { - factory.addServerCustomizers((httpServer) -> httpServer.tcpConfiguration((tcpServer) -> tcpServer - .selectorOption(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) connectionTimeout.toMillis()))); + private void customizeRequestDecoder(NettyReactiveWebServerFactory factory, PropertyMapper propertyMapper) { + factory.addServerCustomizers((httpServer) -> httpServer.httpRequestDecoder((httpRequestDecoderSpec) -> { + propertyMapper.from(this.serverProperties.getMaxHttpHeaderSize()).whenNonNull() + .to((maxHttpRequestHeader) -> httpRequestDecoderSpec + .maxHeaderSize((int) maxHttpRequestHeader.toBytes())); + ServerProperties.Netty nettyProperties = this.serverProperties.getNetty(); + propertyMapper.from(nettyProperties.getMaxChunkSize()).whenNonNull() + .to((maxChunkSize) -> httpRequestDecoderSpec.maxChunkSize((int) maxChunkSize.toBytes())); + propertyMapper.from(nettyProperties.getMaxInitialLineLength()).whenNonNull() + .to((maxInitialLineLength) -> httpRequestDecoderSpec + .maxInitialLineLength((int) maxInitialLineLength.toBytes())); + propertyMapper.from(nettyProperties.getH2cMaxContentLength()).whenNonNull() + .to((h2cMaxContentLength) -> httpRequestDecoderSpec + .h2cMaxContentLength((int) h2cMaxContentLength.toBytes())); + propertyMapper.from(nettyProperties.getInitialBufferSize()).whenNonNull().to( + (initialBufferSize) -> httpRequestDecoderSpec.initialBufferSize((int) initialBufferSize.toBytes())); + propertyMapper.from(nettyProperties.isValidateHeaders()).whenNonNull() + .to(httpRequestDecoderSpec::validateHeaders); + return httpRequestDecoderSpec; + })); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java index 052a13ba12ee..9502816d9448 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,13 +26,15 @@ import org.apache.catalina.valves.RemoteIpValve; import org.apache.coyote.AbstractProtocol; import org.apache.coyote.ProtocolHandler; +import org.apache.coyote.UpgradeProtocol; import org.apache.coyote.http11.AbstractHttp11Protocol; +import org.apache.coyote.http2.Http2Protocol; import org.springframework.boot.autoconfigure.web.ErrorProperties; -import org.springframework.boot.autoconfigure.web.ErrorProperties.IncludeStacktrace; +import org.springframework.boot.autoconfigure.web.ErrorProperties.IncludeAttribute; import org.springframework.boot.autoconfigure.web.ServerProperties; -import org.springframework.boot.autoconfigure.web.ServerProperties.Tomcat; import org.springframework.boot.autoconfigure.web.ServerProperties.Tomcat.Accesslog; +import org.springframework.boot.autoconfigure.web.ServerProperties.Tomcat.Remoteip; import org.springframework.boot.cloud.CloudPlatform; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.web.embedded.tomcat.ConfigurableTomcatWebServerFactory; @@ -55,6 +57,8 @@ * @author Andrew McGhie * @author Dirk Deyne * @author Rafiullah Hamedy + * @author Victor Mandujano + * @author Parviz Rozikov * @since 2.0.0 */ public class TomcatWebServerFactoryCustomizer @@ -83,9 +87,10 @@ public void customize(ConfigurableTomcatWebServerFactory factory) { propertyMapper.from(tomcatProperties::getBackgroundProcessorDelay).whenNonNull().as(Duration::getSeconds) .as(Long::intValue).to(factory::setBackgroundProcessorDelay); customizeRemoteIpValve(factory); - propertyMapper.from(tomcatProperties::getMaxThreads).when(this::isPositive) - .to((maxThreads) -> customizeMaxThreads(factory, tomcatProperties.getMaxThreads())); - propertyMapper.from(tomcatProperties::getMinSpareThreads).when(this::isPositive) + ServerProperties.Tomcat.Threads threadProperties = tomcatProperties.getThreads(); + propertyMapper.from(threadProperties::getMax).when(this::isPositive) + .to((maxThreads) -> customizeMaxThreads(factory, threadProperties.getMax())); + propertyMapper.from(threadProperties::getMinSpare).when(this::isPositive) .to((minSpareThreads) -> customizeMinThreads(factory, minSpareThreads)); propertyMapper.from(this.serverProperties.getMaxHttpHeaderSize()).whenNonNull().asInt(DataSize::toBytes) .when(this::isPositive) @@ -98,8 +103,6 @@ public void customize(ConfigurableTomcatWebServerFactory factory) { propertyMapper.from(tomcatProperties::getAccesslog).when(ServerProperties.Tomcat.Accesslog::isEnabled) .to((enabled) -> customizeAccessLog(factory)); propertyMapper.from(tomcatProperties::getUriEncoding).whenNonNull().to(factory::setUriEncoding); - propertyMapper.from(properties::getConnectionTimeout).whenNonNull() - .to((connectionTimeout) -> customizeConnectionTimeout(factory, connectionTimeout)); propertyMapper.from(tomcatProperties::getConnectionTimeout).whenNonNull() .to((connectionTimeout) -> customizeConnectionTimeout(factory, connectionTimeout)); propertyMapper.from(tomcatProperties::getMaxConnections).when(this::isPositive) @@ -108,6 +111,10 @@ public void customize(ConfigurableTomcatWebServerFactory factory) { .to((acceptCount) -> customizeAcceptCount(factory, acceptCount)); propertyMapper.from(tomcatProperties::getProcessorCache) .to((processorCache) -> customizeProcessorCache(factory, processorCache)); + propertyMapper.from(tomcatProperties::getKeepAliveTimeout).whenNonNull() + .to((keepAliveTimeout) -> customizeKeepAliveTimeout(factory, keepAliveTimeout)); + propertyMapper.from(tomcatProperties::getMaxKeepAliveRequests) + .to((maxKeepAliveRequests) -> customizeMaxKeepAliveRequests(factory, maxKeepAliveRequests)); propertyMapper.from(tomcatProperties::getRelaxedPathChars).as(this::joinCharacters).whenHasText() .to((relaxedChars) -> customizeRelaxedPathChars(factory, relaxedChars)); propertyMapper.from(tomcatProperties::getRelaxedQueryChars).as(this::joinCharacters).whenHasText() @@ -139,6 +146,31 @@ private void customizeProcessorCache(ConfigurableTomcatWebServerFactory factory, }); } + private void customizeKeepAliveTimeout(ConfigurableTomcatWebServerFactory factory, Duration keepAliveTimeout) { + factory.addConnectorCustomizers((connector) -> { + ProtocolHandler handler = connector.getProtocolHandler(); + for (UpgradeProtocol upgradeProtocol : handler.findUpgradeProtocols()) { + if (upgradeProtocol instanceof Http2Protocol) { + ((Http2Protocol) upgradeProtocol).setKeepAliveTimeout(keepAliveTimeout.toMillis()); + } + } + if (handler instanceof AbstractProtocol) { + AbstractProtocol protocol = (AbstractProtocol) handler; + protocol.setKeepAliveTimeout((int) keepAliveTimeout.toMillis()); + } + }); + } + + private void customizeMaxKeepAliveRequests(ConfigurableTomcatWebServerFactory factory, int maxKeepAliveRequests) { + factory.addConnectorCustomizers((connector) -> { + ProtocolHandler handler = connector.getProtocolHandler(); + if (handler instanceof AbstractHttp11Protocol) { + AbstractHttp11Protocol protocol = (AbstractHttp11Protocol) handler; + protocol.setMaxKeepAliveRequests(maxKeepAliveRequests); + } + }); + } + private void customizeMaxConnections(ConfigurableTomcatWebServerFactory factory, int maxConnections) { factory.addConnectorCustomizers((connector) -> { ProtocolHandler handler = connector.getProtocolHandler(); @@ -160,11 +192,11 @@ private void customizeConnectionTimeout(ConfigurableTomcatWebServerFactory facto } private void customizeRelaxedPathChars(ConfigurableTomcatWebServerFactory factory, String relaxedChars) { - factory.addConnectorCustomizers((connector) -> connector.setAttribute("relaxedPathChars", relaxedChars)); + factory.addConnectorCustomizers((connector) -> connector.setProperty("relaxedPathChars", relaxedChars)); } private void customizeRelaxedQueryChars(ConfigurableTomcatWebServerFactory factory, String relaxedChars) { - factory.addConnectorCustomizers((connector) -> connector.setAttribute("relaxedQueryChars", relaxedChars)); + factory.addConnectorCustomizers((connector) -> connector.setProperty("relaxedQueryChars", relaxedChars)); } private String joinCharacters(List content) { @@ -172,9 +204,9 @@ private String joinCharacters(List content) { } private void customizeRemoteIpValve(ConfigurableTomcatWebServerFactory factory) { - Tomcat tomcatProperties = this.serverProperties.getTomcat(); - String protocolHeader = tomcatProperties.getProtocolHeader(); - String remoteIpHeader = tomcatProperties.getRemoteIpHeader(); + Remoteip remoteIpProperties = this.serverProperties.getTomcat().getRemoteip(); + String protocolHeader = remoteIpProperties.getProtocolHeader(); + String remoteIpHeader = remoteIpProperties.getRemoteIpHeader(); // For back compatibility the valve is also enabled if protocol-header is set if (StringUtils.hasText(protocolHeader) || StringUtils.hasText(remoteIpHeader) || getOrDeduceUseForwardHeaders()) { @@ -183,19 +215,24 @@ private void customizeRemoteIpValve(ConfigurableTomcatWebServerFactory factory) if (StringUtils.hasLength(remoteIpHeader)) { valve.setRemoteIpHeader(remoteIpHeader); } - // The internal proxies default to a white list of "safe" internal IP - // addresses - valve.setInternalProxies(tomcatProperties.getInternalProxies()); - valve.setHostHeader(tomcatProperties.getHostHeader()); - valve.setPortHeader(tomcatProperties.getPortHeader()); - valve.setProtocolHeaderHttpsValue(tomcatProperties.getProtocolHeaderHttpsValue()); + // The internal proxies default to a list of "safe" internal IP addresses + valve.setInternalProxies(remoteIpProperties.getInternalProxies()); + try { + valve.setHostHeader(remoteIpProperties.getHostHeader()); + } + catch (NoSuchMethodError ex) { + // Avoid failure with war deployments to Tomcat 8.5 before 8.5.44 and + // Tomcat 9 before 9.0.23 + } + valve.setPortHeader(remoteIpProperties.getPortHeader()); + valve.setProtocolHeaderHttpsValue(remoteIpProperties.getProtocolHeaderHttpsValue()); // ... so it's safe to add this valve by default. factory.addEngineValves(valve); } } private boolean getOrDeduceUseForwardHeaders() { - if (this.serverProperties.getForwardHeadersStrategy().equals(ServerProperties.ForwardHeadersStrategy.NONE)) { + if (this.serverProperties.getForwardHeadersStrategy() == null) { CloudPlatform platform = CloudPlatform.getActive(this.environment); return platform != null && platform.isUsingForwardHeaders(); } @@ -275,21 +312,19 @@ private void customizeAccessLog(ConfigurableTomcatWebServerFactory factory) { private void customizeStaticResources(ConfigurableTomcatWebServerFactory factory) { ServerProperties.Tomcat.Resource resource = this.serverProperties.getTomcat().getResource(); - factory.addContextCustomizers((context) -> { - context.addLifecycleListener((event) -> { - if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) { - context.getResources().setCachingAllowed(resource.isAllowCaching()); - if (resource.getCacheTtl() != null) { - long ttl = resource.getCacheTtl().toMillis(); - context.getResources().setCacheTtl(ttl); - } + factory.addContextCustomizers((context) -> context.addLifecycleListener((event) -> { + if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) { + context.getResources().setCachingAllowed(resource.isAllowCaching()); + if (resource.getCacheTtl() != null) { + long ttl = resource.getCacheTtl().toMillis(); + context.getResources().setCacheTtl(ttl); } - }); - }); + } + })); } private void customizeErrorReportValve(ErrorProperties error, ConfigurableTomcatWebServerFactory factory) { - if (error.getIncludeStacktrace() == IncludeStacktrace.NEVER) { + if (error.getIncludeStacktrace() == IncludeAttribute.NEVER) { factory.addContextCustomizers((context) -> { ErrorReportValve valve = new ErrorReportValve(); valve.setShowServerInfo(false); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizer.java index c5626845782e..8f291b1a220c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import io.undertow.UndertowOptions; import org.xnio.Option; +import org.xnio.Options; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.ServerProperties.Undertow; @@ -38,6 +39,7 @@ import org.springframework.core.Ordered; import org.springframework.core.env.Environment; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.unit.DataSize; @@ -74,37 +76,37 @@ public int getOrder() { @Override public void customize(ConfigurableUndertowWebServerFactory factory) { PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - FactoryOptions options = new FactoryOptions(factory); + ServerOptions options = new ServerOptions(factory); ServerProperties properties = this.serverProperties; map.from(properties::getMaxHttpHeaderSize).asInt(DataSize::toBytes).when(this::isPositive) - .to(options.server(UndertowOptions.MAX_HEADER_SIZE)); - map.from(properties::getConnectionTimeout).asInt(Duration::toMillis) - .to(options.server(UndertowOptions.NO_REQUEST_TIMEOUT)); + .to(options.option(UndertowOptions.MAX_HEADER_SIZE)); mapUndertowProperties(factory, options); mapAccessLogProperties(factory); map.from(this::getOrDeduceUseForwardHeaders).to(factory::setUseForwardHeaders); } - private void mapUndertowProperties(ConfigurableUndertowWebServerFactory factory, FactoryOptions options) { + private void mapUndertowProperties(ConfigurableUndertowWebServerFactory factory, ServerOptions serverOptions) { PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); Undertow properties = this.serverProperties.getUndertow(); map.from(properties::getBufferSize).whenNonNull().asInt(DataSize::toBytes).to(factory::setBufferSize); - map.from(properties::getIoThreads).to(factory::setIoThreads); - map.from(properties::getWorkerThreads).to(factory::setWorkerThreads); + ServerProperties.Undertow.Threads threadProperties = properties.getThreads(); + map.from(threadProperties::getIo).to(factory::setIoThreads); + map.from(threadProperties::getWorker).to(factory::setWorkerThreads); map.from(properties::getDirectBuffers).to(factory::setUseDirectBuffers); map.from(properties::getMaxHttpPostSize).as(DataSize::toBytes).when(this::isPositive) - .to(options.server(UndertowOptions.MAX_ENTITY_SIZE)); - map.from(properties::getMaxParameters).to(options.server(UndertowOptions.MAX_PARAMETERS)); - map.from(properties::getMaxHeaders).to(options.server(UndertowOptions.MAX_HEADERS)); - map.from(properties::getMaxCookies).to(options.server(UndertowOptions.MAX_COOKIES)); - map.from(properties::isAllowEncodedSlash).to(options.server(UndertowOptions.ALLOW_ENCODED_SLASH)); - map.from(properties::isDecodeUrl).to(options.server(UndertowOptions.DECODE_URL)); - map.from(properties::getUrlCharset).as(Charset::name).to(options.server(UndertowOptions.URL_CHARSET)); - map.from(properties::isAlwaysSetKeepAlive).to(options.server(UndertowOptions.ALWAYS_SET_KEEP_ALIVE)); + .to(serverOptions.option(UndertowOptions.MAX_ENTITY_SIZE)); + map.from(properties::getMaxParameters).to(serverOptions.option(UndertowOptions.MAX_PARAMETERS)); + map.from(properties::getMaxHeaders).to(serverOptions.option(UndertowOptions.MAX_HEADERS)); + map.from(properties::getMaxCookies).to(serverOptions.option(UndertowOptions.MAX_COOKIES)); + map.from(properties::isAllowEncodedSlash).to(serverOptions.option(UndertowOptions.ALLOW_ENCODED_SLASH)); + map.from(properties::isDecodeUrl).to(serverOptions.option(UndertowOptions.DECODE_URL)); + map.from(properties::getUrlCharset).as(Charset::name).to(serverOptions.option(UndertowOptions.URL_CHARSET)); + map.from(properties::isAlwaysSetKeepAlive).to(serverOptions.option(UndertowOptions.ALWAYS_SET_KEEP_ALIVE)); map.from(properties::getNoRequestTimeout).asInt(Duration::toMillis) - .to(options.server(UndertowOptions.NO_REQUEST_TIMEOUT)); - map.from(properties.getOptions()::getServer).to(options.forEach(options::server)); - map.from(properties.getOptions()::getSocket).to(options.forEach(options::socket)); + .to(serverOptions.option(UndertowOptions.NO_REQUEST_TIMEOUT)); + map.from(properties.getOptions()::getServer).to(serverOptions.forEach(serverOptions::option)); + SocketOptions socketOptions = new SocketOptions(factory); + map.from(properties.getOptions()::getSocket).to(socketOptions.forEach(socketOptions::option)); } private boolean isPositive(Number value) { @@ -123,23 +125,24 @@ private void mapAccessLogProperties(ConfigurableUndertowWebServerFactory factory } private boolean getOrDeduceUseForwardHeaders() { - if (this.serverProperties.getForwardHeadersStrategy().equals(ServerProperties.ForwardHeadersStrategy.NONE)) { + if (this.serverProperties.getForwardHeadersStrategy() == null) { CloudPlatform platform = CloudPlatform.getActive(this.environment); return platform != null && platform.isUsingForwardHeaders(); } return this.serverProperties.getForwardHeadersStrategy().equals(ServerProperties.ForwardHeadersStrategy.NATIVE); } - /** - * {@link ConfigurableUndertowWebServerFactory} wrapper that makes it easier to apply - * {@link UndertowOptions}. - */ - private static class FactoryOptions { + private abstract static class AbstractOptions { + + private final Class source; + + private final Map> nameLookup; - private static final Map> NAME_LOOKUP; - static { + private final ConfigurableUndertowWebServerFactory factory; + + AbstractOptions(Class source, ConfigurableUndertowWebServerFactory factory) { Map> lookup = new HashMap<>(); - ReflectionUtils.doWithLocalFields(UndertowOptions.class, (field) -> { + ReflectionUtils.doWithLocalFields(source, (field) -> { int modifiers = field.getModifiers(); if (Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && Option.class.isAssignableFrom(field.getType())) { @@ -151,33 +154,24 @@ private static class FactoryOptions { } } }); - NAME_LOOKUP = Collections.unmodifiableMap(lookup); - } - - private final ConfigurableUndertowWebServerFactory factory; - - FactoryOptions(ConfigurableUndertowWebServerFactory factory) { + this.source = source; + this.nameLookup = Collections.unmodifiableMap(lookup); this.factory = factory; } - Consumer server(Option option) { - return (value) -> this.factory.addBuilderCustomizers((builder) -> builder.setServerOption(option, value)); - } - - Consumer socket(Option option) { - return (value) -> this.factory.addBuilderCustomizers((builder) -> builder.setSocketOption(option, value)); + protected ConfigurableUndertowWebServerFactory getFactory() { + return this.factory; } @SuppressWarnings("unchecked") Consumer> forEach(Function, Consumer> function) { - return (map) -> { - map.forEach((key, value) -> { - Option option = (Option) NAME_LOOKUP.get(getCanonicalName(key)); - Assert.state(option != null, "Unable to find '" + key + "' in UndertowOptions"); - T parsed = option.parseValue(value, getClass().getClassLoader()); - function.apply(option).accept(parsed); - }); - }; + return (map) -> map.forEach((key, value) -> { + Option option = (Option) this.nameLookup.get(getCanonicalName(key)); + Assert.state(option != null, + () -> "Unable to find '" + key + "' in " + ClassUtils.getShortName(this.source)); + T parsed = option.parseValue(value, getClass().getClassLoader()); + function.apply(option).accept(parsed); + }); } private static String getCanonicalName(String name) { @@ -189,4 +183,36 @@ private static String getCanonicalName(String name) { } + /** + * {@link ConfigurableUndertowWebServerFactory} wrapper that makes it easier to apply + * {@link UndertowOptions server options}. + */ + private static class ServerOptions extends AbstractOptions { + + ServerOptions(ConfigurableUndertowWebServerFactory factory) { + super(UndertowOptions.class, factory); + } + + Consumer option(Option option) { + return (value) -> getFactory().addBuilderCustomizers((builder) -> builder.setServerOption(option, value)); + } + + } + + /** + * {@link ConfigurableUndertowWebServerFactory} wrapper that makes it easier to apply + * {@link Options socket options}. + */ + private static class SocketOptions extends AbstractOptions { + + SocketOptions(ConfigurableUndertowWebServerFactory factory) { + super(Options.class, factory); + } + + Consumer option(Option option) { + return (value) -> getFactory().addBuilderCustomizers((builder) -> builder.setSocketOption(option, value)); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/format/DateTimeFormatters.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/format/DateTimeFormatters.java new file mode 100644 index 000000000000..3ed58d9e31cc --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/format/DateTimeFormatters.java @@ -0,0 +1,113 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.web.format; + +import java.time.format.DateTimeFormatter; +import java.time.format.ResolverStyle; + +import org.springframework.util.StringUtils; + +/** + * {@link DateTimeFormatter Formatters} for dates, times, and date-times. + * + * @author Andy Wilkinson + * @author Gaurav Pareek + * @since 2.3.0 + */ +public class DateTimeFormatters { + + private DateTimeFormatter dateFormatter; + + private String datePattern; + + private DateTimeFormatter timeFormatter; + + private DateTimeFormatter dateTimeFormatter; + + /** + * Configures the date format using the given {@code pattern}. + * @param pattern the pattern for formatting dates + * @return {@code this} for chained method invocation + */ + public DateTimeFormatters dateFormat(String pattern) { + if (isIso(pattern)) { + this.dateFormatter = DateTimeFormatter.ISO_LOCAL_DATE; + this.datePattern = "yyyy-MM-dd"; + } + else { + this.dateFormatter = formatter(pattern); + this.datePattern = pattern; + } + return this; + } + + /** + * Configures the time format using the given {@code pattern}. + * @param pattern the pattern for formatting times + * @return {@code this} for chained method invocation + */ + public DateTimeFormatters timeFormat(String pattern) { + this.timeFormatter = isIso(pattern) ? DateTimeFormatter.ISO_LOCAL_TIME + : (isIsoOffset(pattern) ? DateTimeFormatter.ISO_OFFSET_TIME : formatter(pattern)); + return this; + } + + /** + * Configures the date-time format using the given {@code pattern}. + * @param pattern the pattern for formatting date-times + * @return {@code this} for chained method invocation + */ + public DateTimeFormatters dateTimeFormat(String pattern) { + this.dateTimeFormatter = isIso(pattern) ? DateTimeFormatter.ISO_LOCAL_DATE_TIME + : (isIsoOffset(pattern) ? DateTimeFormatter.ISO_OFFSET_DATE_TIME : formatter(pattern)); + return this; + } + + DateTimeFormatter getDateFormatter() { + return this.dateFormatter; + } + + String getDatePattern() { + return this.datePattern; + } + + DateTimeFormatter getTimeFormatter() { + return this.timeFormatter; + } + + DateTimeFormatter getDateTimeFormatter() { + return this.dateTimeFormatter; + } + + boolean isCustomized() { + return this.dateFormatter != null || this.timeFormatter != null || this.dateTimeFormatter != null; + } + + private static DateTimeFormatter formatter(String pattern) { + return StringUtils.hasText(pattern) + ? DateTimeFormatter.ofPattern(pattern).withResolverStyle(ResolverStyle.SMART) : null; + } + + private static boolean isIso(String pattern) { + return "iso".equalsIgnoreCase(pattern); + } + + private static boolean isIsoOffset(String pattern) { + return "isooffset".equalsIgnoreCase(pattern) || "iso-offset".equalsIgnoreCase(pattern); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/format/WebConversionService.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/format/WebConversionService.java index fa1df0bfc2fc..82a229ca1e15 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/format/WebConversionService.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/format/WebConversionService.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,15 +17,11 @@ package org.springframework.boot.autoconfigure.web.format; import java.time.format.DateTimeFormatter; -import java.time.format.ResolverStyle; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.joda.time.format.DateTimeFormatterBuilder; +import java.util.function.Consumer; +import java.util.function.Supplier; import org.springframework.format.datetime.DateFormatter; import org.springframework.format.datetime.DateFormatterRegistrar; -import org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar; import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar; import org.springframework.format.number.NumberFormatAnnotationFormatterFactory; import org.springframework.format.number.money.CurrencyUnitFormatter; @@ -33,7 +29,6 @@ import org.springframework.format.number.money.MonetaryAmountFormatter; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.util.ClassUtils; -import org.springframework.util.StringUtils; /** * {@link org.springframework.format.support.FormattingConversionService} dedicated to web @@ -51,67 +46,55 @@ public class WebConversionService extends DefaultFormattingConversionService { private static final boolean JSR_354_PRESENT = ClassUtils.isPresent("javax.money.MonetaryAmount", WebConversionService.class.getClassLoader()); - @Deprecated - private static final boolean JODA_TIME_PRESENT = ClassUtils.isPresent("org.joda.time.LocalDate", - WebConversionService.class.getClassLoader()); - - private static final Log logger = LogFactory.getLog(WebConversionService.class); - - private final String dateFormat; - /** - * Create a new WebConversionService that configures formatters with the provided date - * format, or register the default ones if no custom format is provided. - * @param dateFormat the custom date format to use for date conversions + * 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. + * @param dateTimeFormatters the formatters to use for date, time, and date-time + * formatting + * @since 2.3.0 */ - public WebConversionService(String dateFormat) { + public WebConversionService(DateTimeFormatters dateTimeFormatters) { super(false); - this.dateFormat = StringUtils.hasText(dateFormat) ? dateFormat : null; - if (this.dateFormat != null) { - addFormatters(); + if (dateTimeFormatters.isCustomized()) { + addFormatters(dateTimeFormatters); } else { addDefaultFormatters(this); } } - private void addFormatters() { + private void addFormatters(DateTimeFormatters dateTimeFormatters) { addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory()); if (JSR_354_PRESENT) { addFormatter(new CurrencyUnitFormatter()); addFormatter(new MonetaryAmountFormatter()); addFormatterForFieldAnnotation(new Jsr354NumberFormatAnnotationFormatterFactory()); } - registerJsr310(); - if (JODA_TIME_PRESENT) { - registerJodaTime(); - } - registerJavaDate(); + registerJsr310(dateTimeFormatters); + registerJavaDate(dateTimeFormatters); } - private void registerJsr310() { + private void registerJsr310(DateTimeFormatters dateTimeFormatters) { DateTimeFormatterRegistrar dateTime = new DateTimeFormatterRegistrar(); - if (this.dateFormat != null) { - dateTime.setDateFormatter( - DateTimeFormatter.ofPattern(this.dateFormat).withResolverStyle(ResolverStyle.SMART)); - } + configure(dateTimeFormatters::getDateFormatter, dateTime::setDateFormatter); + configure(dateTimeFormatters::getTimeFormatter, dateTime::setTimeFormatter); + configure(dateTimeFormatters::getDateTimeFormatter, dateTime::setDateTimeFormatter); dateTime.registerFormatters(this); } - @Deprecated - private void registerJodaTime() { - logger.warn("Auto-configuration of Joda-Time formatters is deprecated in favor of using java.time (JSR-310)."); - JodaTimeFormatterRegistrar jodaTime = new JodaTimeFormatterRegistrar(); - if (this.dateFormat != null) { - jodaTime.setDateFormatter(new DateTimeFormatterBuilder().appendPattern(this.dateFormat).toFormatter()); + private void configure(Supplier supplier, Consumer consumer) { + DateTimeFormatter formatter = supplier.get(); + if (formatter != null) { + consumer.accept(formatter); } - jodaTime.registerFormatters(this); } - private void registerJavaDate() { + private void registerJavaDate(DateTimeFormatters dateTimeFormatters) { DateFormatterRegistrar dateFormatterRegistrar = new DateFormatterRegistrar(); - if (this.dateFormat != null) { - DateFormatter dateFormatter = new DateFormatter(this.dateFormat); + String datePattern = dateTimeFormatters.getDatePattern(); + if (datePattern != null) { + DateFormatter dateFormatter = new DateFormatter(datePattern); dateFormatterRegistrar.setFormatter(dateFormatter); } dateFormatterRegistrar.registerFormatters(this); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/HttpHandlerAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/HttpHandlerAutoConfiguration.java index 3004fdb91f43..8048c833b44f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/HttpHandlerAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/HttpHandlerAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,10 @@ package org.springframework.boot.autoconfigure.web.reactive; +import java.util.Collections; +import java.util.Map; + +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureOrder; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -26,7 +30,9 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; +import org.springframework.http.server.reactive.ContextPathCompositeHandler; import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.util.StringUtils; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; @@ -48,15 +54,21 @@ public class HttpHandlerAutoConfiguration { @Configuration(proxyBeanMethods = false) public static class AnnotationConfig { - private ApplicationContext applicationContext; + private final ApplicationContext applicationContext; public AnnotationConfig(ApplicationContext applicationContext) { this.applicationContext = applicationContext; } @Bean - public HttpHandler httpHandler() { - return WebHttpHandlerBuilder.applicationContext(this.applicationContext).build(); + public HttpHandler httpHandler(ObjectProvider propsProvider) { + HttpHandler httpHandler = WebHttpHandlerBuilder.applicationContext(this.applicationContext).build(); + WebFluxProperties properties = propsProvider.getIfAvailable(); + if (properties != null && StringUtils.hasText(properties.getBasePath())) { + Map handlersMap = Collections.singletonMap(properties.getBasePath(), httpHandler); + return new ContextPathCompositeHandler(handlersMap); + } + return httpHandler; } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryAutoConfiguration.java index 5eeedff51ed3..6515daa6691b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.boot.autoconfigure.web.reactive; +import java.util.function.Supplier; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; @@ -100,12 +102,14 @@ public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, return; } registerSyntheticBeanIfMissing(registry, "webServerFactoryCustomizerBeanPostProcessor", - WebServerFactoryCustomizerBeanPostProcessor.class); + WebServerFactoryCustomizerBeanPostProcessor.class, + WebServerFactoryCustomizerBeanPostProcessor::new); } - private void registerSyntheticBeanIfMissing(BeanDefinitionRegistry registry, String name, Class beanClass) { + private void registerSyntheticBeanIfMissing(BeanDefinitionRegistry registry, String name, + Class beanClass, Supplier instanceSupplier) { if (ObjectUtils.isEmpty(this.beanFactory.getBeanNamesForType(beanClass, true, false))) { - RootBeanDefinition beanDefinition = new RootBeanDefinition(beanClass); + RootBeanDefinition beanDefinition = new RootBeanDefinition(beanClass, instanceSupplier); beanDefinition.setSynthetic(true); registry.registerBeanDefinition(name, beanDefinition); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryConfiguration.java index e03963cd3d09..6ec994d77666 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.util.stream.Collectors; import io.undertow.Undertow; +import org.eclipse.jetty.servlet.ServletHolder; import reactor.netty.http.server.HttpServer; import org.springframework.beans.factory.ObjectProvider; @@ -100,7 +101,7 @@ TomcatReactiveWebServerFactory tomcatReactiveWebServerFactory( @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(ReactiveWebServerFactory.class) - @ConditionalOnClass({ org.eclipse.jetty.server.Server.class }) + @ConditionalOnClass({ org.eclipse.jetty.server.Server.class, ServletHolder.class }) static class EmbeddedJetty { @Bean diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryCustomizer.java index c597ce099e81..8d15e4db4a15 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,6 +52,7 @@ public void customize(ConfigurableReactiveWebServerFactory factory) { map.from(this.serverProperties::getSsl).to(factory::setSsl); map.from(this.serverProperties::getCompression).to(factory::setCompression); map.from(this.serverProperties::getHttp2).to(factory::setHttp2); + map.from(this.serverProperties.getShutdown()).to(factory::setShutdown); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ResourceChainResourceHandlerRegistrationCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ResourceChainResourceHandlerRegistrationCustomizer.java index 35cb76c95760..492dcd069074 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ResourceChainResourceHandlerRegistrationCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ResourceChainResourceHandlerRegistrationCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,9 @@ package org.springframework.boot.autoconfigure.web.reactive; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.web.ResourceProperties; +import org.springframework.boot.autoconfigure.web.WebProperties.Resources; import org.springframework.web.reactive.config.ResourceChainRegistration; import org.springframework.web.reactive.config.ResourceHandlerRegistration; -import org.springframework.web.reactive.resource.AppCacheManifestTransformer; import org.springframework.web.reactive.resource.EncodedResourceResolver; import org.springframework.web.reactive.resource.ResourceResolver; import org.springframework.web.reactive.resource.VersionResourceResolver; @@ -33,29 +31,35 @@ */ class ResourceChainResourceHandlerRegistrationCustomizer implements ResourceHandlerRegistrationCustomizer { - @Autowired - private ResourceProperties resourceProperties = new ResourceProperties(); + private final Resources resourceProperties; + + ResourceChainResourceHandlerRegistrationCustomizer(Resources resources) { + this.resourceProperties = resources; + } @Override public void customize(ResourceHandlerRegistration registration) { - ResourceProperties.Chain properties = this.resourceProperties.getChain(); + Resources.Chain properties = this.resourceProperties.getChain(); configureResourceChain(properties, registration.resourceChain(properties.isCache())); } - private void configureResourceChain(ResourceProperties.Chain properties, ResourceChainRegistration chain) { - ResourceProperties.Strategy strategy = properties.getStrategy(); + @SuppressWarnings("deprecation") + private void configureResourceChain(Resources.Chain properties, ResourceChainRegistration chain) { + Resources.Chain.Strategy strategy = properties.getStrategy(); if (properties.isCompressed()) { chain.addResolver(new EncodedResourceResolver()); } if (strategy.getFixed().isEnabled() || strategy.getContent().isEnabled()) { chain.addResolver(getVersionResourceResolver(strategy)); } - if (properties.isHtmlApplicationCache()) { - chain.addTransformer(new AppCacheManifestTransformer()); + if ((properties instanceof org.springframework.boot.autoconfigure.web.ResourceProperties.Chain) + && ((org.springframework.boot.autoconfigure.web.ResourceProperties.Chain) properties) + .isHtmlApplicationCache()) { + chain.addTransformer(new org.springframework.web.reactive.resource.AppCacheManifestTransformer()); } } - private ResourceResolver getVersionResourceResolver(ResourceProperties.Strategy properties) { + private ResourceResolver getVersionResourceResolver(Resources.Chain.Strategy properties) { VersionResourceResolver resolver = new VersionResourceResolver(); if (properties.getFixed().isEnabled()) { String version = properties.getFixed().getVersion(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java index 18a25fc81ef2..b1e6969ee763 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,19 +31,25 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration; +import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders; import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.autoconfigure.validation.ValidatorAdapter; import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain; -import org.springframework.boot.autoconfigure.web.ResourceProperties; +import org.springframework.boot.autoconfigure.web.WebProperties; +import org.springframework.boot.autoconfigure.web.WebProperties.Resources; +import org.springframework.boot.autoconfigure.web.format.DateTimeFormatters; import org.springframework.boot.autoconfigure.web.format.WebConversionService; +import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties.Format; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.convert.ApplicationConversionService; import org.springframework.boot.web.codec.CodecCustomizer; import org.springframework.boot.web.reactive.filter.OrderedHiddenHttpMethodFilter; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.format.FormatterRegistry; import org.springframework.format.support.FormattingConversionService; import org.springframework.http.codec.ServerCodecConfigurer; @@ -57,11 +63,21 @@ import org.springframework.web.reactive.config.ViewResolverRegistry; import org.springframework.web.reactive.config.WebFluxConfigurationSupport; import org.springframework.web.reactive.config.WebFluxConfigurer; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.reactive.function.server.support.RouterFunctionMapping; import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.reactive.result.view.ViewResolver; +import org.springframework.web.server.adapter.WebHttpHandlerBuilder; +import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver; +import org.springframework.web.server.i18n.FixedLocaleContextResolver; +import org.springframework.web.server.i18n.LocaleContextResolver; +import org.springframework.web.server.session.CookieWebSessionIdResolver; +import org.springframework.web.server.session.DefaultWebSessionManager; +import org.springframework.web.server.session.WebSessionManager; /** * {@link EnableAutoConfiguration Auto-configuration} for {@link EnableWebFlux WebFlux}. @@ -86,19 +102,47 @@ public class WebFluxAutoConfiguration { @Bean @ConditionalOnMissingBean(HiddenHttpMethodFilter.class) - @ConditionalOnProperty(prefix = "spring.webflux.hiddenmethod.filter", name = "enabled", matchIfMissing = false) + @ConditionalOnProperty(prefix = "spring.webflux.hiddenmethod.filter", name = "enabled") public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() { return new OrderedHiddenHttpMethodFilter(); } @Configuration(proxyBeanMethods = false) - @EnableConfigurationProperties({ ResourceProperties.class, WebFluxProperties.class }) + public static class WelcomePageConfiguration { + + @Bean + @SuppressWarnings("deprecation") + public RouterFunctionMapping welcomePageRouterFunctionMapping(ApplicationContext applicationContext, + WebFluxProperties webFluxProperties, + org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties, + WebProperties webProperties) { + String[] staticLocations = resourceProperties.hasBeenCustomized() ? resourceProperties.getStaticLocations() + : webProperties.getResources().getStaticLocations(); + WelcomePageRouterFunctionFactory factory = new WelcomePageRouterFunctionFactory( + new TemplateAvailabilityProviders(applicationContext), applicationContext, staticLocations, + webFluxProperties.getStaticPathPattern()); + RouterFunction routerFunction = factory.createRouterFunction(); + if (routerFunction != null) { + RouterFunctionMapping routerFunctionMapping = new RouterFunctionMapping(routerFunction); + routerFunctionMapping.setOrder(1); + return routerFunctionMapping; + } + return null; + } + + } + + @SuppressWarnings("deprecation") + @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties({ org.springframework.boot.autoconfigure.web.ResourceProperties.class, + WebProperties.class, WebFluxProperties.class }) @Import({ EnableWebFluxConfiguration.class }) + @Order(0) public static class WebFluxConfig implements WebFluxConfigurer { private static final Log logger = LogFactory.getLog(WebFluxConfig.class); - private final ResourceProperties resourceProperties; + private final Resources resourceProperties; private final WebFluxProperties webFluxProperties; @@ -112,12 +156,14 @@ public static class WebFluxConfig implements WebFluxConfigurer { private final ObjectProvider viewResolvers; - public WebFluxConfig(ResourceProperties resourceProperties, WebFluxProperties webFluxProperties, - ListableBeanFactory beanFactory, ObjectProvider resolvers, + public WebFluxConfig(org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties, + WebProperties webProperties, WebFluxProperties webFluxProperties, ListableBeanFactory beanFactory, + ObjectProvider resolvers, ObjectProvider codecCustomizers, ObjectProvider resourceHandlerRegistrationCustomizer, ObjectProvider viewResolvers) { - this.resourceProperties = resourceProperties; + this.resourceProperties = resourceProperties.hasBeenCustomized() ? resourceProperties + : webProperties.getResources(); this.webFluxProperties = webFluxProperties; this.beanFactory = beanFactory; this.argumentResolvers = resolvers; @@ -159,11 +205,13 @@ public void addResourceHandlers(ResourceHandlerRegistry registry) { private void configureResourceCaching(ResourceHandlerRegistration registration) { Duration cachePeriod = this.resourceProperties.getCache().getPeriod(); - ResourceProperties.Cache.Cachecontrol cacheControl = this.resourceProperties.getCache().getCachecontrol(); + WebProperties.Resources.Cache.Cachecontrol cacheControl = this.resourceProperties.getCache() + .getCachecontrol(); if (cachePeriod != null && cacheControl.getMaxAge() == null) { cacheControl.setMaxAge(cachePeriod); } registration.setCacheControl(cacheControl.toHttpCacheControl()); + registration.setUseLastModified(this.resourceProperties.getCache().isUseLastModified()); } @Override @@ -188,22 +236,28 @@ private void customizeResourceHandlerRegistration(ResourceHandlerRegistration re * Configuration equivalent to {@code @EnableWebFlux}. */ @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties(WebProperties.class) public static class EnableWebFluxConfiguration extends DelegatingWebFluxConfiguration { private final WebFluxProperties webFluxProperties; + private final WebProperties webProperties; + private final WebFluxRegistrations webFluxRegistrations; - public EnableWebFluxConfiguration(WebFluxProperties webFluxProperties, + public EnableWebFluxConfiguration(WebFluxProperties webFluxProperties, WebProperties webProperties, ObjectProvider webFluxRegistrations) { this.webFluxProperties = webFluxProperties; + this.webProperties = webProperties; this.webFluxRegistrations = webFluxRegistrations.getIfUnique(); } @Bean @Override public FormattingConversionService webFluxConversionService() { - WebConversionService conversionService = new WebConversionService(this.webFluxProperties.getDateFormat()); + Format format = this.webFluxProperties.getFormat(); + WebConversionService conversionService = new WebConversionService(new DateTimeFormatters() + .dateFormat(format.getDate()).timeFormat(format.getTime()).dateTimeFormat(format.getDateTime())); addFormatters(conversionService); return conversionService; } @@ -219,22 +273,49 @@ public Validator webFluxValidator() { @Override protected RequestMappingHandlerAdapter createRequestMappingHandlerAdapter() { - if (this.webFluxRegistrations != null - && this.webFluxRegistrations.getRequestMappingHandlerAdapter() != null) { - return this.webFluxRegistrations.getRequestMappingHandlerAdapter(); + if (this.webFluxRegistrations != null) { + RequestMappingHandlerAdapter adapter = this.webFluxRegistrations.getRequestMappingHandlerAdapter(); + if (adapter != null) { + return adapter; + } } return super.createRequestMappingHandlerAdapter(); } @Override protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() { - if (this.webFluxRegistrations != null - && this.webFluxRegistrations.getRequestMappingHandlerMapping() != null) { - return this.webFluxRegistrations.getRequestMappingHandlerMapping(); + if (this.webFluxRegistrations != null) { + RequestMappingHandlerMapping mapping = this.webFluxRegistrations.getRequestMappingHandlerMapping(); + if (mapping != null) { + return mapping; + } } return super.createRequestMappingHandlerMapping(); } + @Bean + @Override + @ConditionalOnMissingBean(name = WebHttpHandlerBuilder.LOCALE_CONTEXT_RESOLVER_BEAN_NAME) + public LocaleContextResolver localeContextResolver() { + if (this.webProperties.getLocaleResolver() == WebProperties.LocaleResolver.FIXED) { + return new FixedLocaleContextResolver(this.webProperties.getLocale()); + } + AcceptHeaderLocaleContextResolver localeContextResolver = new AcceptHeaderLocaleContextResolver(); + localeContextResolver.setDefaultLocale(this.webProperties.getLocale()); + return localeContextResolver; + } + + @Bean + @ConditionalOnMissingBean(name = WebHttpHandlerBuilder.WEB_SESSION_MANAGER_BEAN_NAME) + public WebSessionManager webSessionManager() { + DefaultWebSessionManager webSessionManager = new DefaultWebSessionManager(); + CookieWebSessionIdResolver webSessionIdResolver = new CookieWebSessionIdResolver(); + webSessionIdResolver.addCookieInitializer((cookie) -> cookie + .sameSite(this.webFluxProperties.getSession().getCookie().getSameSite().attribute())); + webSessionManager.setSessionIdResolver(webSessionIdResolver); + return webSessionManager; + } + } @Configuration(proxyBeanMethods = false) @@ -242,8 +323,13 @@ protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() { static class ResourceChainCustomizerConfiguration { @Bean - ResourceChainResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer() { - return new ResourceChainResourceHandlerRegistrationCustomizer(); + @SuppressWarnings("deprecation") + ResourceChainResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer( + org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties, + WebProperties webProperties) { + Resources resources = resourceProperties.hasBeenCustomized() ? resourceProperties + : webProperties.getResources(); + return new ResourceChainResourceHandlerRegistrationCustomizer(resources); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxProperties.java index 707ca1cf67e5..31b412347049 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.boot.autoconfigure.web.reactive; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.util.StringUtils; /** * {@link ConfigurationProperties properties} for Spring WebFlux. @@ -28,21 +29,46 @@ public class WebFluxProperties { /** - * Date format to use. For instance, `dd/MM/yyyy`. + * Base path for all web handlers. */ - private String dateFormat; + private String basePath; + + private final Format format = new Format(); + + private final Session session = new Session(); /** * Path pattern used for static resources. */ private String staticPathPattern = "/**"; - public String getDateFormat() { - return this.dateFormat; + public String getBasePath() { + return this.basePath; + } + + public void setBasePath(String basePath) { + this.basePath = cleanBasePath(basePath); + } + + private String cleanBasePath(String basePath) { + String candidate = StringUtils.trimWhitespace(basePath); + if (StringUtils.hasText(candidate)) { + if (!candidate.startsWith("/")) { + candidate = "/" + candidate; + } + if (candidate.endsWith("/")) { + candidate = candidate.substring(0, candidate.length() - 1); + } + } + return candidate; + } + + public Format getFormat() { + return this.format; } - public void setDateFormat(String dateFormat) { - this.dateFormat = dateFormat; + public Session getSession() { + return this.session; } public String getStaticPathPattern() { @@ -53,4 +79,105 @@ public void setStaticPathPattern(String staticPathPattern) { this.staticPathPattern = staticPathPattern; } + public static class Format { + + /** + * Date format to use, for example 'dd/MM/yyyy'. + */ + private String date; + + /** + * Time format to use, for example 'HH:mm:ss'. + */ + private String time; + + /** + * Date-time format to use, for example 'yyyy-MM-dd HH:mm:ss'. + */ + private String dateTime; + + public String getDate() { + return this.date; + } + + public void setDate(String date) { + this.date = date; + } + + public String getTime() { + return this.time; + } + + public void setTime(String time) { + this.time = time; + } + + public String getDateTime() { + return this.dateTime; + } + + public void setDateTime(String dateTime) { + this.dateTime = dateTime; + } + + } + + public static class Session { + + private final Cookie cookie = new Cookie(); + + public Cookie getCookie() { + return this.cookie; + } + + } + + public static class Cookie { + + /** + * SameSite attribute value for session Cookies. + */ + private SameSite sameSite = SameSite.LAX; + + public SameSite getSameSite() { + return this.sameSite; + } + + public void setSameSite(SameSite sameSite) { + this.sameSite = sameSite; + } + + } + + public enum SameSite { + + /** + * Cookies are sent in both first-party and cross-origin requests. + */ + NONE("None"), + + /** + * Cookies are sent in a first-party context, also when following a link to the + * origin site. + */ + LAX("Lax"), + + /** + * Cookies are only sent in a first-party context (i.e. not when following a link + * to the origin site). + */ + STRICT("Strict"); + + private final String attribute; + + SameSite(String attribute) { + this.attribute = attribute; + } + + public String attribute() { + return this.attribute; + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxRegistrations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxRegistrations.java index 7bce64dd0fdd..719295fdf4a3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxRegistrations.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxRegistrations.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.autoconfigure.web.reactive; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WelcomePageRouterFunctionFactory.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WelcomePageRouterFunctionFactory.java new file mode 100644 index 000000000000..656ff4342050 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WelcomePageRouterFunctionFactory.java @@ -0,0 +1,90 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.web.reactive; + +import java.util.Arrays; + +import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders; +import org.springframework.context.ApplicationContext; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; + +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; +import static org.springframework.web.reactive.function.server.RequestPredicates.accept; + +/** + * A {@link RouterFunction} factory for an application's welcome page. Supports both + * static and templated files. If both a static and templated index page are available, + * the static page is preferred. + * + * @author Brian Clozel + */ +final class WelcomePageRouterFunctionFactory { + + private final String staticPathPattern; + + private final Resource welcomePage; + + private final boolean welcomePageTemplateExists; + + WelcomePageRouterFunctionFactory(TemplateAvailabilityProviders templateAvailabilityProviders, + ApplicationContext applicationContext, String[] staticLocations, String staticPathPattern) { + this.staticPathPattern = staticPathPattern; + this.welcomePage = getWelcomePage(applicationContext, staticLocations); + this.welcomePageTemplateExists = welcomeTemplateExists(templateAvailabilityProviders, applicationContext); + } + + private Resource getWelcomePage(ResourceLoader resourceLoader, String[] staticLocations) { + return Arrays.stream(staticLocations).map((location) -> getIndexHtml(resourceLoader, location)) + .filter(this::isReadable).findFirst().orElse(null); + } + + private Resource getIndexHtml(ResourceLoader resourceLoader, String location) { + return resourceLoader.getResource(location + "index.html"); + } + + private boolean isReadable(Resource resource) { + try { + return resource.exists() && (resource.getURL() != null); + } + catch (Exception ex) { + return false; + } + } + + private boolean welcomeTemplateExists(TemplateAvailabilityProviders templateAvailabilityProviders, + ApplicationContext applicationContext) { + return templateAvailabilityProviders.getProvider("index", applicationContext) != null; + } + + RouterFunction createRouterFunction() { + if (this.welcomePage != null && "/**".equals(this.staticPathPattern)) { + return RouterFunctions.route(GET("/").and(accept(MediaType.TEXT_HTML)), + (req) -> ServerResponse.ok().contentType(MediaType.TEXT_HTML).bodyValue(this.welcomePage)); + } + else if (this.welcomePageTemplateExists) { + return RouterFunctions.route(GET("/").and(accept(MediaType.TEXT_HTML)), + (req) -> ServerResponse.ok().render("index")); + } + return null; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/AbstractErrorWebExceptionHandler.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/AbstractErrorWebExceptionHandler.java index 7c9065f616fb..35ac827b16d6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/AbstractErrorWebExceptionHandler.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/AbstractErrorWebExceptionHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,9 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders; -import org.springframework.boot.autoconfigure.web.ResourceProperties; +import org.springframework.boot.autoconfigure.web.WebProperties.Resources; +import org.springframework.boot.web.error.ErrorAttributeOptions; +import org.springframework.boot.web.error.ErrorAttributeOptions.Include; import org.springframework.boot.web.reactive.error.ErrorAttributes; import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler; import org.springframework.context.ApplicationContext; @@ -54,6 +56,7 @@ * Abstract base class for {@link ErrorWebExceptionHandler} implementations. * * @author Brian Clozel + * @author Scott Frederick * @since 2.0.0 * @see ErrorAttributes */ @@ -79,7 +82,7 @@ public abstract class AbstractErrorWebExceptionHandler implements ErrorWebExcept private final ErrorAttributes errorAttributes; - private final ResourceProperties resourceProperties; + private final Resources resources; private final TemplateAvailabilityProviders templateAvailabilityProviders; @@ -89,13 +92,35 @@ public abstract class AbstractErrorWebExceptionHandler implements ErrorWebExcept private List viewResolvers = Collections.emptyList(); - public AbstractErrorWebExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties, + /** + * Create a new {@code AbstractErrorWebExceptionHandler}. + * @param errorAttributes the error attributes + * @param resourceProperties the resource properties + * @param applicationContext the application context + * @deprecated since 2.4.0 for removal in 2.6.0 in favor of + * {@link #AbstractErrorWebExceptionHandler(ErrorAttributes, Resources, ApplicationContext)} + */ + @Deprecated + public AbstractErrorWebExceptionHandler(ErrorAttributes errorAttributes, + org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties, + ApplicationContext applicationContext) { + this(errorAttributes, (Resources) resourceProperties, applicationContext); + } + + /** + * Create a new {@code AbstractErrorWebExceptionHandler}. + * @param errorAttributes the error attributes + * @param resources the resources configuration properties + * @param applicationContext the application context + * @since 2.4.0 + */ + public AbstractErrorWebExceptionHandler(ErrorAttributes errorAttributes, Resources resources, ApplicationContext applicationContext) { Assert.notNull(errorAttributes, "ErrorAttributes must not be null"); - Assert.notNull(resourceProperties, "ResourceProperties must not be null"); + Assert.notNull(resources, "Resources must not be null"); Assert.notNull(applicationContext, "ApplicationContext must not be null"); this.errorAttributes = errorAttributes; - this.resourceProperties = resourceProperties; + this.resources = resources; this.applicationContext = applicationContext; this.templateAvailabilityProviders = new TemplateAvailabilityProviders(applicationContext); } @@ -131,10 +156,25 @@ public void setViewResolvers(List viewResolvers) { * views or JSON payloads. * @param request the source request * @param includeStackTrace whether to include the error stacktrace information - * @return the error attributes as a Map. + * @return the error attributes as a Map + * @deprecated since 2.3.0 for removal in 2.5.0 in favor of + * {@link #getErrorAttributes(ServerRequest, ErrorAttributeOptions)} */ + @Deprecated protected Map getErrorAttributes(ServerRequest request, boolean includeStackTrace) { - return this.errorAttributes.getErrorAttributes(request, includeStackTrace); + return getErrorAttributes(request, + (includeStackTrace) ? ErrorAttributeOptions.of(Include.STACK_TRACE) : ErrorAttributeOptions.defaults()); + } + + /** + * Extract the error attributes from the current request, to be used to populate error + * views or JSON payloads. + * @param request the source request + * @param options options to control error attributes + * @return the error attributes as a Map + */ + protected Map getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) { + return this.errorAttributes.getErrorAttributes(request, options); } /** @@ -152,7 +192,31 @@ protected Throwable getError(ServerRequest request) { * @return {@code true} if the error trace has been requested, {@code false} otherwise */ protected boolean isTraceEnabled(ServerRequest request) { - String parameter = request.queryParam("trace").orElse("false"); + return getBooleanParameter(request, "trace"); + } + + /** + * Check whether the message attribute has been set on the given request. + * @param request the source request + * @return {@code true} if the message attribute has been requested, {@code false} + * otherwise + */ + protected boolean isMessageEnabled(ServerRequest request) { + return getBooleanParameter(request, "message"); + } + + /** + * Check whether the errors attribute has been set on the given request. + * @param request the source request + * @return {@code true} if the errors attribute has been requested, {@code false} + * otherwise + */ + protected boolean isBindingErrorsEnabled(ServerRequest request) { + return getBooleanParameter(request, "errors"); + } + + private boolean getBooleanParameter(ServerRequest request, String parameterName) { + String parameter = request.queryParam(parameterName).orElse("false"); return !"false".equalsIgnoreCase(parameter); } @@ -182,7 +246,7 @@ private boolean isTemplateAvailable(String viewName) { } private Resource resolveResource(String viewName) { - for (String location : this.resourceProperties.getStaticLocations()) { + for (String location : this.resources.getStaticLocations()) { try { Resource resource = this.applicationContext.getResource(location); resource = resource.createRelative(viewName + ".html"); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandler.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandler.java index fdfc00e8add1..7ac683f6c8c2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandler.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,9 @@ import reactor.core.publisher.Mono; import org.springframework.boot.autoconfigure.web.ErrorProperties; -import org.springframework.boot.autoconfigure.web.ResourceProperties; +import org.springframework.boot.autoconfigure.web.WebProperties.Resources; +import org.springframework.boot.web.error.ErrorAttributeOptions; +import org.springframework.boot.web.error.ErrorAttributeOptions.Include; import org.springframework.boot.web.reactive.error.ErrorAttributes; import org.springframework.context.ApplicationContext; import org.springframework.http.HttpStatus; @@ -71,6 +73,7 @@ * payload. * * @author Brian Clozel + * @author Scott Frederick * @since 2.0.0 */ public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler { @@ -94,10 +97,27 @@ public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHa * @param resourceProperties the resources configuration properties * @param errorProperties the error configuration properties * @param applicationContext the current application context + * @deprecated since 2.4.0 for removal in 2.6.0 in favor of + * {@link #DefaultErrorWebExceptionHandler(ErrorAttributes, Resources, ErrorProperties, ApplicationContext)} */ - public DefaultErrorWebExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties, + @Deprecated + public DefaultErrorWebExceptionHandler(ErrorAttributes errorAttributes, + org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties, ErrorProperties errorProperties, ApplicationContext applicationContext) { - super(errorAttributes, resourceProperties, applicationContext); + this(errorAttributes, (Resources) resourceProperties, errorProperties, applicationContext); + } + + /** + * Create a new {@code DefaultErrorWebExceptionHandler} instance. + * @param errorAttributes the error attributes + * @param resources the resources configuration properties + * @param errorProperties the error configuration properties + * @param applicationContext the current application context + * @since 2.4.0 + */ + public DefaultErrorWebExceptionHandler(ErrorAttributes errorAttributes, Resources resources, + ErrorProperties errorProperties, ApplicationContext applicationContext) { + super(errorAttributes, resources, applicationContext); this.errorProperties = errorProperties; } @@ -112,8 +132,7 @@ protected RouterFunction getRoutingFunction(ErrorAttributes erro * @return a {@code Publisher} of the HTTP response */ protected Mono renderErrorView(ServerRequest request) { - boolean includeStackTrace = isIncludeStackTrace(request, MediaType.TEXT_HTML); - Map error = getErrorAttributes(request, includeStackTrace); + Map error = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)); int errorStatus = getHttpStatus(error); ServerResponse.BodyBuilder responseBody = ServerResponse.status(errorStatus).contentType(TEXT_HTML_UTF8); return Flux.just(getData(errorStatus).toArray(new String[] {})) @@ -140,12 +159,28 @@ private List getData(int errorStatus) { * @return a {@code Publisher} of the HTTP response */ protected Mono renderErrorResponse(ServerRequest request) { - boolean includeStackTrace = isIncludeStackTrace(request, MediaType.ALL); - Map error = getErrorAttributes(request, includeStackTrace); + Map error = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL)); return ServerResponse.status(getHttpStatus(error)).contentType(MediaType.APPLICATION_JSON) .body(BodyInserters.fromValue(error)); } + protected ErrorAttributeOptions getErrorAttributeOptions(ServerRequest request, MediaType mediaType) { + ErrorAttributeOptions options = ErrorAttributeOptions.defaults(); + if (this.errorProperties.isIncludeException()) { + options = options.including(Include.EXCEPTION); + } + if (isIncludeStackTrace(request, mediaType)) { + options = options.including(Include.STACK_TRACE); + } + if (isIncludeMessage(request, mediaType)) { + options = options.including(Include.MESSAGE); + } + if (isIncludeBindingErrors(request, mediaType)) { + options = options.including(Include.BINDING_ERRORS); + } + return options; + } + /** * Determine if the stacktrace attribute should be included. * @param request the source request @@ -153,14 +188,48 @@ protected Mono renderErrorResponse(ServerRequest request) { * @return if the stacktrace attribute should be included */ protected boolean isIncludeStackTrace(ServerRequest request, MediaType produces) { - ErrorProperties.IncludeStacktrace include = this.errorProperties.getIncludeStacktrace(); - if (include == ErrorProperties.IncludeStacktrace.ALWAYS) { + switch (this.errorProperties.getIncludeStacktrace()) { + case ALWAYS: return true; - } - if (include == ErrorProperties.IncludeStacktrace.ON_TRACE_PARAM) { + case ON_PARAM: return isTraceEnabled(request); + default: + return false; + } + } + + /** + * Determine if the message attribute should be included. + * @param request the source request + * @param produces the media type produced (or {@code MediaType.ALL}) + * @return if the message attribute should be included + */ + protected boolean isIncludeMessage(ServerRequest request, MediaType produces) { + switch (this.errorProperties.getIncludeMessage()) { + case ALWAYS: + return true; + case ON_PARAM: + return isMessageEnabled(request); + default: + return false; + } + } + + /** + * Determine if the errors attribute should be included. + * @param request the source request + * @param produces the media type produced (or {@code MediaType.ALL}) + * @return if the errors attribute should be included + */ + protected boolean isIncludeBindingErrors(ServerRequest request, MediaType produces) { + switch (this.errorProperties.getIncludeBindingErrors()) { + case ALWAYS: + return true; + case ON_PARAM: + return isBindingErrorsEnabled(request); + default: + return false; } - return false; } /** @@ -183,7 +252,7 @@ protected RequestPredicate acceptsTextHtml() { return (serverRequest) -> { try { List acceptedMediaTypes = serverRequest.headers().accept(); - acceptedMediaTypes.remove(MediaType.ALL); + acceptedMediaTypes.removeIf(MediaType.ALL::equalsTypeAndSubtype); MediaType.sortBySpecificityAndQuality(acceptedMediaTypes); return acceptedMediaTypes.stream().anyMatch(MediaType.TEXT_HTML::isCompatibleWith); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/ErrorWebFluxAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/ErrorWebFluxAutoConfiguration.java index 7b95da860b0d..e522e7d738b7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/ErrorWebFluxAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/ErrorWebFluxAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,8 +25,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.SearchStrategy; -import org.springframework.boot.autoconfigure.web.ResourceProperties; import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.autoconfigure.web.WebProperties; import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.reactive.error.DefaultErrorAttributes; @@ -45,13 +45,16 @@ * {@link org.springframework.web.server.WebExceptionHandler}. * * @author Brian Clozel + * @author Scott Frederick * @since 2.0.0 */ +@SuppressWarnings("deprecation") @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) @ConditionalOnClass(WebFluxConfigurer.class) @AutoConfigureBefore(WebFluxAutoConfiguration.class) -@EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class }) +@EnableConfigurationProperties({ ServerProperties.class, + org.springframework.boot.autoconfigure.web.ResourceProperties.class, WebProperties.class }) public class ErrorWebFluxAutoConfiguration { private final ServerProperties serverProperties; @@ -64,10 +67,12 @@ public ErrorWebFluxAutoConfiguration(ServerProperties serverProperties) { @ConditionalOnMissingBean(value = ErrorWebExceptionHandler.class, search = SearchStrategy.CURRENT) @Order(-1) public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes, - ResourceProperties resourceProperties, ObjectProvider viewResolvers, + org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties, + WebProperties webProperties, ObjectProvider viewResolvers, ServerCodecConfigurer serverCodecConfigurer, ApplicationContext applicationContext) { DefaultErrorWebExceptionHandler exceptionHandler = new DefaultErrorWebExceptionHandler(errorAttributes, - resourceProperties, this.serverProperties.getError(), applicationContext); + resourceProperties.hasBeenCustomized() ? resourceProperties : webProperties.getResources(), + this.serverProperties.getError(), applicationContext); exceptionHandler.setViewResolvers(viewResolvers.orderedStream().collect(Collectors.toList())); exceptionHandler.setMessageWriters(serverCodecConfigurer.getWriters()); exceptionHandler.setMessageReaders(serverCodecConfigurer.getReaders()); @@ -77,7 +82,7 @@ public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAt @Bean @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT) public DefaultErrorAttributes errorAttributes() { - return new DefaultErrorAttributes(this.serverProperties.getError().isIncludeException()); + return new DefaultErrorAttributes(); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorAutoConfiguration.java index 7e51c30cbf16..5da7cd7d80cd 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Lazy; import org.springframework.core.annotation.Order; import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.web.reactive.function.client.WebClient; @@ -39,10 +40,12 @@ */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(WebClient.class) -@Import({ ClientHttpConnectorConfiguration.ReactorNetty.class, ClientHttpConnectorConfiguration.JettyClient.class }) +@Import({ ClientHttpConnectorConfiguration.ReactorNetty.class, ClientHttpConnectorConfiguration.JettyClient.class, + ClientHttpConnectorConfiguration.HttpClient5.class }) public class ClientHttpConnectorAutoConfiguration { @Bean + @Lazy @Order(0) @ConditionalOnBean(ClientHttpConnector.class) public WebClientCustomizer clientConnectorCustomizer(ClientHttpConnector clientHttpConnector) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorConfiguration.java index e27ff0e93533..7e65578a5721 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,19 @@ package org.springframework.boot.autoconfigure.web.reactive.function.client; -import java.util.function.Function; - +import org.apache.hc.client5.http.impl.async.HttpAsyncClients; +import org.apache.hc.core5.http.nio.AsyncRequestProducer; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.http.client.reactive.HttpComponentsClientHttpConnector; import org.springframework.http.client.reactive.JettyClientHttpConnector; import org.springframework.http.client.reactive.JettyResourceFactory; import org.springframework.http.client.reactive.ReactorClientHttpConnector; @@ -45,17 +48,22 @@ class ClientHttpConnectorConfiguration { @Configuration(proxyBeanMethods = false) @ConditionalOnClass(reactor.netty.http.client.HttpClient.class) @ConditionalOnMissingBean(ClientHttpConnector.class) - public static class ReactorNetty { + static class ReactorNetty { @Bean @ConditionalOnMissingBean - public ReactorResourceFactory reactorClientResourceFactory() { + ReactorResourceFactory reactorClientResourceFactory() { return new ReactorResourceFactory(); } @Bean - public ReactorClientHttpConnector reactorClientHttpConnector(ReactorResourceFactory reactorResourceFactory) { - return new ReactorClientHttpConnector(reactorResourceFactory, Function.identity()); + @Lazy + ReactorClientHttpConnector reactorClientHttpConnector(ReactorResourceFactory reactorResourceFactory, + ObjectProvider mapperProvider) { + ReactorNettyHttpClientMapper mapper = mapperProvider.orderedStream() + .reduce((before, after) -> (client) -> after.configure(before.configure(client))) + .orElse((client) -> client); + return new ReactorClientHttpConnector(reactorResourceFactory, mapper::configure); } } @@ -63,16 +71,17 @@ public ReactorClientHttpConnector reactorClientHttpConnector(ReactorResourceFact @Configuration(proxyBeanMethods = false) @ConditionalOnClass(org.eclipse.jetty.reactive.client.ReactiveRequest.class) @ConditionalOnMissingBean(ClientHttpConnector.class) - public static class JettyClient { + static class JettyClient { @Bean @ConditionalOnMissingBean - public JettyResourceFactory jettyClientResourceFactory() { + JettyResourceFactory jettyClientResourceFactory() { return new JettyResourceFactory(); } @Bean - public JettyClientHttpConnector jettyClientHttpConnector(JettyResourceFactory jettyResourceFactory) { + @Lazy + JettyClientHttpConnector jettyClientHttpConnector(JettyResourceFactory jettyResourceFactory) { SslContextFactory sslContextFactory = new SslContextFactory.Client(); HttpClient httpClient = new HttpClient(sslContextFactory); return new JettyClientHttpConnector(httpClient, jettyResourceFactory); @@ -80,4 +89,17 @@ public JettyClientHttpConnector jettyClientHttpConnector(JettyResourceFactory je } + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass({ HttpAsyncClients.class, AsyncRequestProducer.class }) + @ConditionalOnMissingBean(ClientHttpConnector.class) + static class HttpClient5 { + + @Bean + @Lazy + HttpComponentsClientHttpConnector httpComponentsClientHttpConnector() { + return new HttpComponentsClientHttpConnector(); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ReactorNettyHttpClientMapper.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ReactorNettyHttpClientMapper.java new file mode 100644 index 000000000000..34a8d377629d --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ReactorNettyHttpClientMapper.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.web.reactive.function.client; + +import reactor.netty.http.client.HttpClient; + +import org.springframework.http.client.reactive.ReactorClientHttpConnector; + +/** + * Mapper that allows for custom modification of a {@link HttpClient} before it is used as + * the basis for a {@link ReactorClientHttpConnector}. + * + * @author Brian Clozel + * @since 2.3.0 + */ +@FunctionalInterface +public interface ReactorNettyHttpClientMapper { + + /** + * Configure the given {@link HttpClient} and return the newly created instance. + * @param httpClient the client to configure + * @return the new client instance + */ + HttpClient configure(HttpClient httpClient); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/WebClientAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/WebClientAutoConfiguration.java index 8625053128ce..a9b5b0dd145e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/WebClientAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/WebClientAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ package org.springframework.boot.autoconfigure.web.reactive.function.client; -import java.util.List; +import java.util.stream.Collectors; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfigureAfter; @@ -49,18 +49,13 @@ @AutoConfigureAfter({ CodecsAutoConfiguration.class, ClientHttpConnectorAutoConfiguration.class }) public class WebClientAutoConfiguration { - private final WebClient.Builder webClientBuilder; - - public WebClientAutoConfiguration(ObjectProvider customizerProvider) { - this.webClientBuilder = WebClient.builder(); - customizerProvider.orderedStream().forEach((customizer) -> customizer.customize(this.webClientBuilder)); - } - @Bean @Scope("prototype") @ConditionalOnMissingBean - public WebClient.Builder webClientBuilder() { - return this.webClientBuilder.clone(); + public WebClient.Builder webClientBuilder(ObjectProvider customizerProvider) { + WebClient.Builder builder = WebClient.builder(); + customizerProvider.orderedStream().forEach((customizer) -> customizer.customize(builder)); + return builder; } @Configuration(proxyBeanMethods = false) @@ -70,8 +65,8 @@ protected static class WebClientCodecsConfiguration { @Bean @ConditionalOnMissingBean @Order(0) - public WebClientCodecCustomizer exchangeStrategiesCustomizer(List codecCustomizers) { - return new WebClientCodecCustomizer(codecCustomizers); + public WebClientCodecCustomizer exchangeStrategiesCustomizer(ObjectProvider codecCustomizers) { + return new WebClientCodecCustomizer(codecCustomizers.orderedStream().collect(Collectors.toList())); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/WebClientCodecCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/WebClientCodecCustomizer.java index 380ac8d35b08..8a812baa6bd2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/WebClientCodecCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/WebClientCodecCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import org.springframework.boot.web.codec.CodecCustomizer; import org.springframework.boot.web.reactive.function.client.WebClientCustomizer; -import org.springframework.web.reactive.function.client.ExchangeStrategies; import org.springframework.web.reactive.function.client.WebClient; /** @@ -39,9 +38,8 @@ public WebClientCodecCustomizer(List codecCustomizers) { @Override public void customize(WebClient.Builder webClientBuilder) { - webClientBuilder.exchangeStrategies(ExchangeStrategies.builder() - .codecs((codecs) -> this.codecCustomizers.forEach((customizer) -> customizer.customize(codecs))) - .build()); + webClientBuilder + .codecs((codecs) -> this.codecCustomizers.forEach((customizer) -> customizer.customize(codecs))); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DefaultJerseyApplicationPath.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DefaultJerseyApplicationPath.java index bae786ae16c2..438acedaafc2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DefaultJerseyApplicationPath.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DefaultJerseyApplicationPath.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.autoconfigure.web.servlet; import javax.ws.rs.ApplicationPath; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfiguration.java index e84453ec75a5..721cbb9f1353 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; -import org.springframework.boot.autoconfigure.http.HttpProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; @@ -70,30 +69,30 @@ @AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class) public class DispatcherServletAutoConfiguration { - /* - * The bean name for a DispatcherServlet that will be mapped to the root URL "/" + /** + * The bean name for a DispatcherServlet that will be mapped to the root URL "/". */ public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "dispatcherServlet"; - /* - * The bean name for a ServletRegistrationBean for the DispatcherServlet "/" + /** + * The bean name for a ServletRegistrationBean for the DispatcherServlet "/". */ public static final String DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME = "dispatcherServletRegistration"; @Configuration(proxyBeanMethods = false) @Conditional(DefaultDispatcherServletCondition.class) @ConditionalOnClass(ServletRegistration.class) - @EnableConfigurationProperties({ HttpProperties.class, WebMvcProperties.class }) + @EnableConfigurationProperties(WebMvcProperties.class) protected static class DispatcherServletConfiguration { @Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME) - public DispatcherServlet dispatcherServlet(HttpProperties httpProperties, WebMvcProperties webMvcProperties) { + public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) { DispatcherServlet dispatcherServlet = new DispatcherServlet(); dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest()); dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest()); dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound()); dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents()); - dispatcherServlet.setEnableLoggingRequestDetails(httpProperties.isLogRequestDetails()); + dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails()); return dispatcherServlet; } @@ -169,10 +168,13 @@ public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeM } private ConditionOutcome checkDefaultDispatcherName(ConfigurableListableBeanFactory beanFactory) { + boolean containsDispatcherBean = beanFactory.containsBean(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME); + if (!containsDispatcherBean) { + return ConditionOutcome.match(); + } List servlets = Arrays .asList(beanFactory.getBeanNamesForType(DispatcherServlet.class, false, false)); - boolean containsDispatcherBean = beanFactory.containsBean(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME); - if (containsDispatcherBean && !servlets.contains(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)) { + if (!servlets.contains(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)) { return ConditionOutcome.noMatch( startMessage().found("non dispatcher servlet").items(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/HttpEncodingAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/HttpEncodingAutoConfiguration.java index 7316cfd4aba1..2c9b3aef02a7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/HttpEncodingAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/HttpEncodingAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,12 +21,12 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; -import org.springframework.boot.autoconfigure.http.HttpProperties; -import org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type; +import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.boot.web.servlet.filter.OrderedCharacterEncodingFilter; import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; +import org.springframework.boot.web.servlet.server.Encoding; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; @@ -41,16 +41,16 @@ * @since 2.0.0 */ @Configuration(proxyBeanMethods = false) -@EnableConfigurationProperties(HttpProperties.class) +@EnableConfigurationProperties(ServerProperties.class) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) @ConditionalOnClass(CharacterEncodingFilter.class) -@ConditionalOnProperty(prefix = "spring.http.encoding", value = "enabled", matchIfMissing = true) +@ConditionalOnProperty(prefix = "server.servlet.encoding", value = "enabled", matchIfMissing = true) public class HttpEncodingAutoConfiguration { - private final HttpProperties.Encoding properties; + private final Encoding properties; - public HttpEncodingAutoConfiguration(HttpProperties properties) { - this.properties = properties.getEncoding(); + public HttpEncodingAutoConfiguration(ServerProperties properties) { + this.properties = properties.getServlet().getEncoding(); } @Bean @@ -58,8 +58,8 @@ public HttpEncodingAutoConfiguration(HttpProperties properties) { public CharacterEncodingFilter characterEncodingFilter() { CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter(); filter.setEncoding(this.properties.getCharset().name()); - filter.setForceRequestEncoding(this.properties.shouldForce(Type.REQUEST)); - filter.setForceResponseEncoding(this.properties.shouldForce(Type.RESPONSE)); + filter.setForceRequestEncoding(this.properties.shouldForce(Encoding.Type.REQUEST)); + filter.setForceResponseEncoding(this.properties.shouldForce(Encoding.Type.RESPONSE)); return filter; } @@ -68,12 +68,12 @@ public LocaleCharsetMappingsCustomizer localeCharsetMappingsCustomizer() { return new LocaleCharsetMappingsCustomizer(this.properties); } - private static class LocaleCharsetMappingsCustomizer + static class LocaleCharsetMappingsCustomizer implements WebServerFactoryCustomizer, Ordered { - private final HttpProperties.Encoding properties; + private final Encoding properties; - LocaleCharsetMappingsCustomizer(HttpProperties.Encoding properties) { + LocaleCharsetMappingsCustomizer(Encoding properties) { this.properties = properties; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/JerseyApplicationPath.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/JerseyApplicationPath.java index 46c6a2e3a3cf..b85306cd5313 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/JerseyApplicationPath.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/JerseyApplicationPath.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.autoconfigure.web.servlet; import org.springframework.boot.web.servlet.ServletRegistrationBean; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfiguration.java index 2d74032b6e05..ef3223c463ba 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,16 @@ package org.springframework.boot.autoconfigure.web.servlet; +import java.util.function.Supplier; +import java.util.stream.Collectors; + import javax.servlet.DispatcherType; import javax.servlet.ServletRequest; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.RootBeanDefinition; @@ -36,6 +40,7 @@ import org.springframework.boot.web.server.ErrorPageRegistrarBeanPostProcessor; import org.springframework.boot.web.server.WebServerFactoryCustomizerBeanPostProcessor; import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.boot.web.servlet.WebListenerRegistrar; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -67,8 +72,10 @@ public class ServletWebServerFactoryAutoConfiguration { @Bean - public ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer(ServerProperties serverProperties) { - return new ServletWebServerFactoryCustomizer(serverProperties); + public ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer(ServerProperties serverProperties, + ObjectProvider webListenerRegistrars) { + return new ServletWebServerFactoryCustomizer(serverProperties, + webListenerRegistrars.orderedStream().collect(Collectors.toList())); } @Bean @@ -78,15 +85,34 @@ public TomcatServletWebServerFactoryCustomizer tomcatServletWebServerFactoryCust return new TomcatServletWebServerFactoryCustomizer(serverProperties); } - @Bean - @ConditionalOnMissingFilterBean(ForwardedHeaderFilter.class) + @Configuration(proxyBeanMethods = false) @ConditionalOnProperty(value = "server.forward-headers-strategy", havingValue = "framework") - public FilterRegistrationBean forwardedHeaderFilter() { - ForwardedHeaderFilter filter = new ForwardedHeaderFilter(); - FilterRegistrationBean registration = new FilterRegistrationBean<>(filter); - registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC, DispatcherType.ERROR); - registration.setOrder(Ordered.HIGHEST_PRECEDENCE); - return registration; + @ConditionalOnMissingFilterBean(ForwardedHeaderFilter.class) + static class ForwardedHeaderFilterConfiguration { + + @Bean + @ConditionalOnClass(name = "org.apache.catalina.startup.Tomcat") + ForwardedHeaderFilterCustomizer tomcatForwardedHeaderFilterCustomizer(ServerProperties serverProperties) { + return (filter) -> filter.setRelativeRedirects(serverProperties.getTomcat().isUseRelativeRedirects()); + } + + @Bean + FilterRegistrationBean forwardedHeaderFilter( + ObjectProvider customizerProvider) { + ForwardedHeaderFilter filter = new ForwardedHeaderFilter(); + customizerProvider.ifAvailable((customizer) -> customizer.customize(filter)); + FilterRegistrationBean registration = new FilterRegistrationBean<>(filter); + registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC, DispatcherType.ERROR); + registration.setOrder(Ordered.HIGHEST_PRECEDENCE); + return registration; + } + + } + + interface ForwardedHeaderFilterCustomizer { + + void customize(ForwardedHeaderFilter filter); + } /** @@ -111,14 +137,16 @@ public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, return; } registerSyntheticBeanIfMissing(registry, "webServerFactoryCustomizerBeanPostProcessor", - WebServerFactoryCustomizerBeanPostProcessor.class); + WebServerFactoryCustomizerBeanPostProcessor.class, + WebServerFactoryCustomizerBeanPostProcessor::new); registerSyntheticBeanIfMissing(registry, "errorPageRegistrarBeanPostProcessor", - ErrorPageRegistrarBeanPostProcessor.class); + ErrorPageRegistrarBeanPostProcessor.class, ErrorPageRegistrarBeanPostProcessor::new); } - private void registerSyntheticBeanIfMissing(BeanDefinitionRegistry registry, String name, Class beanClass) { + private void registerSyntheticBeanIfMissing(BeanDefinitionRegistry registry, String name, + Class beanClass, Supplier instanceSupplier) { if (ObjectUtils.isEmpty(this.beanFactory.getBeanNamesForType(beanClass, true, false))) { - RootBeanDefinition beanDefinition = new RootBeanDefinition(beanClass); + RootBeanDefinition beanDefinition = new RootBeanDefinition(beanClass, instanceSupplier); beanDefinition.setSynthetic(true); registry.registerBeanDefinition(name, beanDefinition); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryConfiguration.java index ee11aba45080..13a6c6a3ea63 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,6 +32,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.SearchStrategy; +import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.web.embedded.jetty.JettyServerCustomizer; import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory; import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer; @@ -65,10 +66,10 @@ class ServletWebServerFactoryConfiguration { @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class }) @ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT) - public static class EmbeddedTomcat { + static class EmbeddedTomcat { @Bean - public TomcatServletWebServerFactory tomcatServletWebServerFactory( + TomcatServletWebServerFactory tomcatServletWebServerFactory( ObjectProvider connectorCustomizers, ObjectProvider contextCustomizers, ObjectProvider> protocolHandlerCustomizers) { @@ -90,10 +91,10 @@ public TomcatServletWebServerFactory tomcatServletWebServerFactory( @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ Servlet.class, Server.class, Loader.class, WebAppContext.class }) @ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT) - public static class EmbeddedJetty { + static class EmbeddedJetty { @Bean - public JettyServletWebServerFactory JettyServletWebServerFactory( + JettyServletWebServerFactory JettyServletWebServerFactory( ObjectProvider serverCustomizers) { JettyServletWebServerFactory factory = new JettyServletWebServerFactory(); factory.getServerCustomizers().addAll(serverCustomizers.orderedStream().collect(Collectors.toList())); @@ -108,10 +109,10 @@ public JettyServletWebServerFactory JettyServletWebServerFactory( @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ Servlet.class, Undertow.class, SslClientAuthMode.class }) @ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT) - public static class EmbeddedUndertow { + static class EmbeddedUndertow { @Bean - public UndertowServletWebServerFactory undertowServletWebServerFactory( + UndertowServletWebServerFactory undertowServletWebServerFactory( ObjectProvider deploymentInfoCustomizers, ObjectProvider builderCustomizers) { UndertowServletWebServerFactory factory = new UndertowServletWebServerFactory(); @@ -121,6 +122,12 @@ public UndertowServletWebServerFactory undertowServletWebServerFactory( return factory; } + @Bean + UndertowServletWebServerFactoryCustomizer undertowServletWebServerFactoryCustomizer( + ServerProperties serverProperties) { + return new UndertowServletWebServerFactoryCustomizer(serverProperties); + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizer.java index e05f921910d9..ae259d1a17fb 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,13 @@ package org.springframework.boot.autoconfigure.web.servlet; +import java.util.Collections; +import java.util.List; + import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.boot.web.servlet.WebListenerRegistrar; import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; import org.springframework.core.Ordered; @@ -37,8 +41,16 @@ public class ServletWebServerFactoryCustomizer private final ServerProperties serverProperties; + private final Iterable webListenerRegistrars; + public ServletWebServerFactoryCustomizer(ServerProperties serverProperties) { + this(serverProperties, Collections.emptyList()); + } + + public ServletWebServerFactoryCustomizer(ServerProperties serverProperties, + List webListenerRegistrars) { this.serverProperties = serverProperties; + this.webListenerRegistrars = webListenerRegistrars; } @Override @@ -53,6 +65,7 @@ public void customize(ConfigurableServletWebServerFactory factory) { map.from(this.serverProperties::getAddress).to(factory::setAddress); map.from(this.serverProperties.getServlet()::getContextPath).to(factory::setContextPath); map.from(this.serverProperties.getServlet()::getApplicationDisplayName).to(factory::setDisplayName); + map.from(this.serverProperties.getServlet()::isRegisterDefaultServlet).to(factory::setRegisterDefaultServlet); map.from(this.serverProperties.getServlet()::getSession).to(factory::setSession); map.from(this.serverProperties::getSsl).to(factory::setSsl); map.from(this.serverProperties.getServlet()::getJsp).to(factory::setJsp); @@ -60,6 +73,10 @@ public void customize(ConfigurableServletWebServerFactory factory) { map.from(this.serverProperties::getHttp2).to(factory::setHttp2); map.from(this.serverProperties::getServerHeader).to(factory::setServerHeader); map.from(this.serverProperties.getServlet()::getContextParameters).to(factory::setInitParameters); + map.from(this.serverProperties.getShutdown()).to(factory::setShutdown); + for (WebListenerRegistrar registrar : this.webListenerRegistrars) { + registrar.register(factory); + } } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/TomcatServletWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/TomcatServletWebServerFactoryCustomizer.java index 8e4b320cfe6b..f1dcfb7e265a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/TomcatServletWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/TomcatServletWebServerFactoryCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,9 +54,7 @@ public void customize(TomcatServletWebServerFactory factory) { if (tomcatProperties.getRedirectContextRoot() != null) { customizeRedirectContextRoot(factory, tomcatProperties.getRedirectContextRoot()); } - if (tomcatProperties.getUseRelativeRedirects() != null) { - customizeUseRelativeRedirects(factory, tomcatProperties.getUseRelativeRedirects()); - } + customizeUseRelativeRedirects(factory, tomcatProperties.isUseRelativeRedirects()); factory.setDisableMBeanRegistry(!tomcatProperties.getMbeanregistry().isEnabled()); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/UndertowServletWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/UndertowServletWebServerFactoryCustomizer.java index 86362316f5e3..b87b702bfdd1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/UndertowServletWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/UndertowServletWebServerFactoryCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,8 +38,8 @@ public UndertowServletWebServerFactoryCustomizer(ServerProperties serverProperti @Override public void customize(UndertowServletWebServerFactory factory) { - factory.addDeploymentInfoCustomizers((deploymentInfo) -> deploymentInfo - .setEagerFilterInit(this.serverProperties.getUndertow().isEagerFilterInit())); + factory.setEagerFilterInit(this.serverProperties.getUndertow().isEagerFilterInit()); + factory.setPreservePathOnForward(this.serverProperties.getUndertow().isPreservePathOnForward()); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java index ca31a0c36a2a..a919f42862d8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,13 +17,14 @@ package org.springframework.boot.autoconfigure.web.servlet; import java.time.Duration; -import java.util.Arrays; import java.util.List; import java.util.ListIterator; +import java.util.Locale; import java.util.Map; -import java.util.Optional; +import java.util.function.Consumer; import javax.servlet.Servlet; +import javax.servlet.ServletContext; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -32,7 +33,6 @@ import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureOrder; @@ -49,11 +49,15 @@ import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.autoconfigure.validation.ValidatorAdapter; import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain; -import org.springframework.boot.autoconfigure.web.ResourceProperties; -import org.springframework.boot.autoconfigure.web.ResourceProperties.Strategy; +import org.springframework.boot.autoconfigure.web.WebProperties; +import org.springframework.boot.autoconfigure.web.WebProperties.Resources; +import org.springframework.boot.autoconfigure.web.WebProperties.Resources.Chain.Strategy; +import org.springframework.boot.autoconfigure.web.format.DateTimeFormatters; import org.springframework.boot.autoconfigure.web.format.WebConversionService; +import org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties.Format; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.convert.ApplicationConversionService; +import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.boot.web.servlet.filter.OrderedFormContentFilter; import org.springframework.boot.web.servlet.filter.OrderedHiddenHttpMethodFilter; import org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter; @@ -70,7 +74,6 @@ import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.format.FormatterRegistry; import org.springframework.format.support.FormattingConversionService; -import org.springframework.http.CacheControl; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.util.ClassUtils; @@ -80,17 +83,20 @@ import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.accept.ContentNegotiationStrategy; -import org.springframework.web.accept.PathExtensionContentNegotiationStrategy; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; +import org.springframework.web.context.ServletContextAware; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextListener; +import org.springframework.web.context.support.ServletContextResource; import org.springframework.web.filter.FormContentFilter; import org.springframework.web.filter.HiddenHttpMethodFilter; import org.springframework.web.filter.RequestContextFilter; import org.springframework.web.servlet.DispatcherServlet; +import org.springframework.web.servlet.FlashMapManager; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.servlet.ThemeResolver; import org.springframework.web.servlet.View; import org.springframework.web.servlet.ViewResolver; import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer; @@ -109,7 +115,6 @@ import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; -import org.springframework.web.servlet.resource.AppCacheManifestTransformer; import org.springframework.web.servlet.resource.EncodedResourceResolver; import org.springframework.web.servlet.resource.ResourceResolver; import org.springframework.web.servlet.resource.ResourceUrlProvider; @@ -117,6 +122,8 @@ import org.springframework.web.servlet.view.BeanNameViewResolver; import org.springframework.web.servlet.view.ContentNegotiatingViewResolver; import org.springframework.web.servlet.view.InternalResourceViewResolver; +import org.springframework.web.util.UrlPathHelper; +import org.springframework.web.util.pattern.PathPatternParser; /** * {@link EnableAutoConfiguration Auto-configuration} for {@link EnableWebMvc Web MVC}. @@ -130,6 +137,7 @@ * @author Kristine Jetzke * @author Bruce Brouwer * @author Artsiom Yudovin + * @author Scott Frederick * @since 2.0.0 */ @Configuration(proxyBeanMethods = false) @@ -141,15 +149,21 @@ ValidationAutoConfiguration.class }) public class WebMvcAutoConfiguration { + /** + * The default Spring MVC view prefix. + */ public static final String DEFAULT_PREFIX = ""; + /** + * The default Spring MVC view suffix. + */ public static final String DEFAULT_SUFFIX = ""; - private static final String[] SERVLET_LOCATIONS = { "/" }; + private static final String SERVLET_LOCATION = "/"; @Bean @ConditionalOnMissingBean(HiddenHttpMethodFilter.class) - @ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false) + @ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled") public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() { return new OrderedHiddenHttpMethodFilter(); } @@ -161,24 +175,19 @@ public OrderedFormContentFilter formContentFilter() { return new OrderedFormContentFilter(); } - static String[] getResourceLocations(String[] staticLocations) { - String[] locations = new String[staticLocations.length + SERVLET_LOCATIONS.length]; - System.arraycopy(staticLocations, 0, locations, 0, staticLocations.length); - System.arraycopy(SERVLET_LOCATIONS, 0, locations, staticLocations.length, SERVLET_LOCATIONS.length); - return locations; - } - // Defined as a nested config to ensure WebMvcConfigurer is not read when not // on the classpath + @SuppressWarnings("deprecation") @Configuration(proxyBeanMethods = false) @Import(EnableWebMvcConfiguration.class) - @EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class }) + @EnableConfigurationProperties({ WebMvcProperties.class, + org.springframework.boot.autoconfigure.web.ResourceProperties.class, WebProperties.class }) @Order(0) - public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer { + public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware { private static final Log logger = LogFactory.getLog(WebMvcConfigurer.class); - private final ResourceProperties resourceProperties; + private final Resources resourceProperties; private final WebMvcProperties mvcProperties; @@ -186,16 +195,35 @@ public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer { private final ObjectProvider messageConvertersProvider; - final ResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer; + private final ObjectProvider dispatcherServletPath; - public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebMvcProperties mvcProperties, - ListableBeanFactory beanFactory, ObjectProvider messageConvertersProvider, - ObjectProvider resourceHandlerRegistrationCustomizerProvider) { - this.resourceProperties = resourceProperties; + private final ObjectProvider> servletRegistrations; + + private final ResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer; + + private ServletContext servletContext; + + public WebMvcAutoConfigurationAdapter( + org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties, + WebProperties webProperties, WebMvcProperties mvcProperties, ListableBeanFactory beanFactory, + ObjectProvider messageConvertersProvider, + ObjectProvider resourceHandlerRegistrationCustomizerProvider, + ObjectProvider dispatcherServletPath, + ObjectProvider> servletRegistrations) { + this.resourceProperties = resourceProperties.hasBeenCustomized() ? resourceProperties + : webProperties.getResources(); this.mvcProperties = mvcProperties; this.beanFactory = beanFactory; this.messageConvertersProvider = messageConvertersProvider; this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable(); + this.dispatcherServletPath = dispatcherServletPath; + this.servletRegistrations = servletRegistrations; + this.mvcProperties.checkConfiguration(); + } + + @Override + public void setServletContext(ServletContext servletContext) { + this.servletContext = servletContext; } @Override @@ -221,9 +249,26 @@ public void configureAsyncSupport(AsyncSupportConfigurer configurer) { @Override public void configurePathMatch(PathMatchConfigurer configurer) { + if (this.mvcProperties.getPathmatch() + .getMatchingStrategy() == WebMvcProperties.MatchingStrategy.PATH_PATTERN_PARSER) { + configurer.setPatternParser(new PathPatternParser()); + } configurer.setUseSuffixPatternMatch(this.mvcProperties.getPathmatch().isUseSuffixPattern()); configurer.setUseRegisteredSuffixPatternMatch( this.mvcProperties.getPathmatch().isUseRegisteredSuffixPattern()); + this.dispatcherServletPath.ifAvailable((dispatcherPath) -> { + String servletUrlMapping = dispatcherPath.getServletUrlMapping(); + if (servletUrlMapping.equals("/") && singleDispatcherServlet()) { + UrlPathHelper urlPathHelper = new UrlPathHelper(); + urlPathHelper.setAlwaysUseFullPath(true); + configurer.setUrlPathHelper(urlPathHelper); + } + }); + } + + private boolean singleDispatcherServlet() { + return this.servletRegistrations.stream().map(ServletRegistrationBean::getServlet) + .filter(DispatcherServlet.class::isInstance).count() == 1; } @Override @@ -268,18 +313,6 @@ public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) { return resolver; } - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = "spring.mvc", name = "locale") - public LocaleResolver localeResolver() { - if (this.mvcProperties.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) { - return new FixedLocaleResolver(this.mvcProperties.getLocale()); - } - AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver(); - localeResolver.setDefaultLocale(this.mvcProperties.getLocale()); - return localeResolver; - } - @Override public MessageCodesResolver getMessageCodesResolver() { if (this.mvcProperties.getMessageCodesResolverFormat() != null) { @@ -301,19 +334,31 @@ public void addResourceHandlers(ResourceHandlerRegistry registry) { logger.debug("Default resource handling disabled"); return; } - Duration cachePeriod = this.resourceProperties.getCache().getPeriod(); - CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl(); - if (!registry.hasMappingForPattern("/webjars/**")) { - customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**") - .addResourceLocations("classpath:/META-INF/resources/webjars/") - .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl)); - } - String staticPathPattern = this.mvcProperties.getStaticPathPattern(); - if (!registry.hasMappingForPattern(staticPathPattern)) { - customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern) - .addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations())) - .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl)); + addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/"); + addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> { + registration.addResourceLocations(this.resourceProperties.getStaticLocations()); + if (this.servletContext != null) { + ServletContextResource resource = new ServletContextResource(this.servletContext, SERVLET_LOCATION); + registration.addResourceLocations(resource); + } + }); + } + + private void addResourceHandler(ResourceHandlerRegistry registry, String pattern, String... locations) { + addResourceHandler(registry, pattern, (registration) -> registration.addResourceLocations(locations)); + } + + private void addResourceHandler(ResourceHandlerRegistry registry, String pattern, + Consumer customizer) { + if (registry.hasMappingForPattern(pattern)) { + return; } + ResourceHandlerRegistration registration = registry.addResourceHandler(pattern); + customizer.accept(registration); + registration.setCachePeriod(getSeconds(this.resourceProperties.getCache().getPeriod())); + registration.setCacheControl(this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl()); + registration.setUseLastModified(this.resourceProperties.getCache().isUseLastModified()); + customizeResourceHandlerRegistration(registration); } private Integer getSeconds(Duration cachePeriod) { @@ -339,23 +384,32 @@ public static RequestContextFilter requestContextFilter() { * Configuration equivalent to {@code @EnableWebMvc}. */ @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties(WebProperties.class) public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware { - private final ResourceProperties resourceProperties; + private final Resources resourceProperties; private final WebMvcProperties mvcProperties; + private final WebProperties webProperties; + private final ListableBeanFactory beanFactory; private final WebMvcRegistrations mvcRegistrations; private ResourceLoader resourceLoader; - public EnableWebMvcConfiguration(ResourceProperties resourceProperties, - ObjectProvider mvcPropertiesProvider, - ObjectProvider mvcRegistrationsProvider, ListableBeanFactory beanFactory) { - this.resourceProperties = resourceProperties; - this.mvcProperties = mvcPropertiesProvider.getIfAvailable(); + @SuppressWarnings("deprecation") + public EnableWebMvcConfiguration( + org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties, + WebMvcProperties mvcProperties, WebProperties webProperties, + ObjectProvider mvcRegistrationsProvider, + ObjectProvider resourceHandlerRegistrationCustomizerProvider, + ListableBeanFactory beanFactory) { + this.resourceProperties = resourceProperties.hasBeenCustomized() ? resourceProperties + : webProperties.getResources(); + this.mvcProperties = mvcProperties; + this.webProperties = webProperties; this.mvcRegistrations = mvcRegistrationsProvider.getIfUnique(); this.beanFactory = beanFactory; } @@ -375,8 +429,11 @@ public RequestMappingHandlerAdapter requestMappingHandlerAdapter( @Override protected RequestMappingHandlerAdapter createRequestMappingHandlerAdapter() { - if (this.mvcRegistrations != null && this.mvcRegistrations.getRequestMappingHandlerAdapter() != null) { - return this.mvcRegistrations.getRequestMappingHandlerAdapter(); + if (this.mvcRegistrations != null) { + RequestMappingHandlerAdapter adapter = this.mvcRegistrations.getRequestMappingHandlerAdapter(); + if (adapter != null) { + return adapter; + } } return super.createRequestMappingHandlerAdapter(); } @@ -400,31 +457,78 @@ public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext ap new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(), this.mvcProperties.getStaticPathPattern()); welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider)); + welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations()); return welcomePageHandlerMapping; } - private Optional getWelcomePage() { - String[] locations = getResourceLocations(this.resourceProperties.getStaticLocations()); - return Arrays.stream(locations).map(this::getIndexHtml).filter(this::isReadable).findFirst(); + @Override + @Bean + @ConditionalOnMissingBean(name = DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME) + @SuppressWarnings("deprecation") + public LocaleResolver localeResolver() { + if (this.webProperties.getLocaleResolver() == WebProperties.LocaleResolver.FIXED) { + return new FixedLocaleResolver(this.webProperties.getLocale()); + } + if (this.mvcProperties.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) { + return new FixedLocaleResolver(this.mvcProperties.getLocale()); + } + AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver(); + Locale locale = (this.webProperties.getLocale() != null) ? this.webProperties.getLocale() + : this.mvcProperties.getLocale(); + localeResolver.setDefaultLocale(locale); + return localeResolver; + } + + @Override + @Bean + @ConditionalOnMissingBean(name = DispatcherServlet.THEME_RESOLVER_BEAN_NAME) + public ThemeResolver themeResolver() { + return super.themeResolver(); + } + + @Override + @Bean + @ConditionalOnMissingBean(name = DispatcherServlet.FLASH_MAP_MANAGER_BEAN_NAME) + public FlashMapManager flashMapManager() { + return super.flashMapManager(); + } + + private Resource getWelcomePage() { + for (String location : this.resourceProperties.getStaticLocations()) { + Resource indexHtml = getIndexHtml(location); + if (indexHtml != null) { + return indexHtml; + } + } + ServletContext servletContext = getServletContext(); + if (servletContext != null) { + return getIndexHtml(new ServletContextResource(servletContext, SERVLET_LOCATION)); + } + return null; } private Resource getIndexHtml(String location) { - return this.resourceLoader.getResource(location + "index.html"); + return getIndexHtml(this.resourceLoader.getResource(location)); } - private boolean isReadable(Resource resource) { + private Resource getIndexHtml(Resource location) { try { - return resource.exists() && (resource.getURL() != null); + Resource resource = location.createRelative("index.html"); + if (resource.exists() && (resource.getURL() != null)) { + return resource; + } } catch (Exception ex) { - return false; } + return null; } @Bean @Override public FormattingConversionService mvcConversionService() { - WebConversionService conversionService = new WebConversionService(this.mvcProperties.getDateFormat()); + Format format = this.mvcProperties.getFormat(); + WebConversionService conversionService = new WebConversionService(new DateTimeFormatters() + .dateFormat(format.getDate()).timeFormat(format.getTime()).dateTimeFormat(format.getDateTime())); addFormatters(conversionService); return conversionService; } @@ -440,8 +544,11 @@ public Validator mvcValidator() { @Override protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() { - if (this.mvcRegistrations != null && this.mvcRegistrations.getRequestMappingHandlerMapping() != null) { - return this.mvcRegistrations.getRequestMappingHandlerMapping(); + if (this.mvcRegistrations != null) { + RequestMappingHandlerMapping mapping = this.mvcRegistrations.getRequestMappingHandlerMapping(); + if (mapping != null) { + return mapping; + } } return super.createRequestMappingHandlerMapping(); } @@ -459,8 +566,12 @@ protected ConfigurableWebBindingInitializer getConfigurableWebBindingInitializer @Override protected ExceptionHandlerExceptionResolver createExceptionHandlerExceptionResolver() { - if (this.mvcRegistrations != null && this.mvcRegistrations.getExceptionHandlerExceptionResolver() != null) { - return this.mvcRegistrations.getExceptionHandlerExceptionResolver(); + if (this.mvcRegistrations != null) { + ExceptionHandlerExceptionResolver resolver = this.mvcRegistrations + .getExceptionHandlerExceptionResolver(); + if (resolver != null) { + return resolver; + } } return super.createExceptionHandlerExceptionResolver(); } @@ -479,13 +590,14 @@ protected void extendHandlerExceptionResolvers(List ex @Bean @Override + @SuppressWarnings("deprecation") public ContentNegotiationManager mvcContentNegotiationManager() { ContentNegotiationManager manager = super.mvcContentNegotiationManager(); List strategies = manager.getStrategies(); ListIterator iterator = strategies.listIterator(); while (iterator.hasNext()) { ContentNegotiationStrategy strategy = iterator.next(); - if (strategy instanceof PathExtensionContentNegotiationStrategy) { + if (strategy instanceof org.springframework.web.accept.PathExtensionContentNegotiationStrategy) { iterator.set(new OptionalPathExtensionContentNegotiationStrategy(strategy)); } } @@ -504,8 +616,12 @@ public void setResourceLoader(ResourceLoader resourceLoader) { static class ResourceChainCustomizerConfiguration { @Bean - ResourceChainResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer() { - return new ResourceChainResourceHandlerRegistrationCustomizer(); + @SuppressWarnings("deprecation") + ResourceChainResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer( + org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties, + WebProperties webProperties) { + return new ResourceChainResourceHandlerRegistrationCustomizer( + resourceProperties.hasBeenCustomized() ? resourceProperties : webProperties.getResources()); } } @@ -518,16 +634,20 @@ interface ResourceHandlerRegistrationCustomizer { static class ResourceChainResourceHandlerRegistrationCustomizer implements ResourceHandlerRegistrationCustomizer { - @Autowired - private ResourceProperties resourceProperties = new ResourceProperties(); + private final Resources resourceProperties; + + ResourceChainResourceHandlerRegistrationCustomizer(Resources resourceProperties) { + this.resourceProperties = resourceProperties; + } @Override public void customize(ResourceHandlerRegistration registration) { - ResourceProperties.Chain properties = this.resourceProperties.getChain(); + Resources.Chain properties = this.resourceProperties.getChain(); configureResourceChain(properties, registration.resourceChain(properties.isCache())); } - private void configureResourceChain(ResourceProperties.Chain properties, ResourceChainRegistration chain) { + @SuppressWarnings("deprecation") + private void configureResourceChain(Resources.Chain properties, ResourceChainRegistration chain) { Strategy strategy = properties.getStrategy(); if (properties.isCompressed()) { chain.addResolver(new EncodedResourceResolver()); @@ -535,12 +655,14 @@ private void configureResourceChain(ResourceProperties.Chain properties, Resourc if (strategy.getFixed().isEnabled() || strategy.getContent().isEnabled()) { chain.addResolver(getVersionResourceResolver(strategy)); } - if (properties.isHtmlApplicationCache()) { - chain.addTransformer(new AppCacheManifestTransformer()); + if (properties instanceof org.springframework.boot.autoconfigure.web.ResourceProperties.Chain + && ((org.springframework.boot.autoconfigure.web.ResourceProperties.Chain) properties) + .isHtmlApplicationCache()) { + chain.addTransformer(new org.springframework.web.servlet.resource.AppCacheManifestTransformer()); } } - private ResourceResolver getVersionResourceResolver(ResourceProperties.Strategy properties) { + private ResourceResolver getVersionResourceResolver(Strategy properties) { VersionResourceResolver resolver = new VersionResourceResolver(); if (properties.getFixed().isEnabled()) { String version = properties.getFixed().getVersion(); @@ -557,12 +679,15 @@ private ResourceResolver getVersionResourceResolver(ResourceProperties.Strategy } /** - * Decorator to make {@link PathExtensionContentNegotiationStrategy} optional - * depending on a request attribute. + * Decorator to make + * {@link org.springframework.web.accept.PathExtensionContentNegotiationStrategy} + * optional depending on a request attribute. */ static class OptionalPathExtensionContentNegotiationStrategy implements ContentNegotiationStrategy { - private static final String SKIP_ATTRIBUTE = PathExtensionContentNegotiationStrategy.class.getName() + ".SKIP"; + @SuppressWarnings("deprecation") + private static final String SKIP_ATTRIBUTE = org.springframework.web.accept.PathExtensionContentNegotiationStrategy.class + .getName() + ".SKIP"; private final ContentNegotiationStrategy delegate; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcProperties.java index da6e0ee7b6e1..1becb872359e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,8 @@ import java.util.Map; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; +import org.springframework.boot.context.properties.IncompatibleConfigurationException; import org.springframework.http.MediaType; import org.springframework.util.Assert; import org.springframework.validation.DefaultMessageCodesResolver; @@ -40,7 +42,7 @@ public class WebMvcProperties { /** - * Formatting strategy for message codes. For instance, `PREFIX_ERROR_CODE`. + * Formatting strategy for message codes. For instance, 'PREFIX_ERROR_CODE'. */ private DefaultMessageCodesResolver.Format messageCodesResolverFormat; @@ -55,10 +57,7 @@ public class WebMvcProperties { */ private LocaleResolver localeResolver = LocaleResolver.ACCEPT_HEADER; - /** - * Date format to use. For instance, `dd/MM/yyyy`. - */ - private String dateFormat; + private final Format format = new Format(); /** * Whether to dispatch TRACE requests to the FrameworkServlet doService method. @@ -87,6 +86,12 @@ public class WebMvcProperties { */ private boolean throwExceptionIfNoHandlerFound = false; + /** + * Whether logging of (potentially sensitive) request details at DEBUG and TRACE level + * is allowed. + */ + private boolean logRequestDetails; + /** * Whether to enable warn logging of exceptions resolved by a * "HandlerExceptionResolver", except for "DefaultHandlerExceptionResolver". @@ -116,6 +121,8 @@ public void setMessageCodesResolverFormat(DefaultMessageCodesResolver.Format mes this.messageCodesResolverFormat = messageCodesResolverFormat; } + @Deprecated + @DeprecatedConfigurationProperty(replacement = "spring.web.locale") public Locale getLocale() { return this.locale; } @@ -124,6 +131,8 @@ public void setLocale(Locale locale) { this.locale = locale; } + @Deprecated + @DeprecatedConfigurationProperty(replacement = "spring.web.locale-resolver") public LocaleResolver getLocaleResolver() { return this.localeResolver; } @@ -132,12 +141,19 @@ public void setLocaleResolver(LocaleResolver localeResolver) { this.localeResolver = localeResolver; } + @Deprecated + @DeprecatedConfigurationProperty(replacement = "spring.mvc.format.date") public String getDateFormat() { - return this.dateFormat; + return this.format.getDate(); } + @Deprecated public void setDateFormat(String dateFormat) { - this.dateFormat = dateFormat; + this.format.setDate(dateFormat); + } + + public Format getFormat() { + return this.format; } public boolean isIgnoreDefaultModelOnRedirect() { @@ -164,6 +180,14 @@ public void setThrowExceptionIfNoHandlerFound(boolean throwExceptionIfNoHandlerF this.throwExceptionIfNoHandlerFound = throwExceptionIfNoHandlerFound; } + public boolean isLogRequestDetails() { + return this.logRequestDetails; + } + + public void setLogRequestDetails(boolean logRequestDetails) { + this.logRequestDetails = logRequestDetails; + } + public boolean isLogResolvedException() { return this.logResolvedException; } @@ -216,12 +240,24 @@ public Pathmatch getPathmatch() { return this.pathmatch; } + public void checkConfiguration() { + if (this.getPathmatch().getMatchingStrategy() == MatchingStrategy.PATH_PATTERN_PARSER) { + if (this.getPathmatch().isUseSuffixPattern()) { + throw new IncompatibleConfigurationException("spring.mvc.pathmatch.matching-strategy", + "spring.mvc.pathmatch.use-suffix-pattern"); + } + if (this.getPathmatch().isUseRegisteredSuffixPattern()) { + throw new IncompatibleConfigurationException("spring.mvc.pathmatch.matching-strategy", + "spring.mvc.pathmatch.use-registered-suffix-pattern"); + } + } + } + public static class Async { /** * Amount of time before asynchronous request handling times out. If this value is - * not set, the default timeout of the underlying implementation is used, e.g. 10 - * seconds on Tomcat with Servlet 3. + * not set, the default timeout of the underlying implementation is used. */ private Duration requestTimeout; @@ -238,7 +274,8 @@ public void setRequestTimeout(Duration requestTimeout) { public static class Servlet { /** - * Path of the dispatcher servlet. + * Path of the dispatcher servlet. Setting a custom value for this property is not + * compatible with the PathPatternParser matching strategy. */ private String path = "/"; @@ -353,10 +390,14 @@ public static class Contentnegotiation { */ private String parameterName; + @DeprecatedConfigurationProperty( + reason = "Use of path extensions for request mapping and for content negotiation is discouraged.") + @Deprecated public boolean isFavorPathExtension() { return this.favorPathExtension; } + @Deprecated public void setFavorPathExtension(boolean favorPathExtension) { this.favorPathExtension = favorPathExtension; } @@ -389,9 +430,15 @@ public void setParameterName(String parameterName) { public static class Pathmatch { + /** + * Choice of strategy for matching request paths against registered mappings. + */ + private MatchingStrategy matchingStrategy = MatchingStrategy.ANT_PATH_MATCHER; + /** * Whether to use suffix pattern match (".*") when matching patterns to requests. - * If enabled a method mapped to "/users" also matches to "/users.*". + * If enabled a method mapped to "/users" also matches to "/users.*". Enabling + * this option is not compatible with the PathPatternParser matching strategy. */ private boolean useSuffixPattern = false; @@ -399,28 +446,112 @@ public static class Pathmatch { * Whether suffix pattern matching should work only against extensions registered * with "spring.mvc.contentnegotiation.media-types.*". This is generally * recommended to reduce ambiguity and to avoid issues such as when a "." appears - * in the path for other reasons. + * in the path for other reasons. Enabling this option is not compatible with the + * PathPatternParser matching strategy. */ private boolean useRegisteredSuffixPattern = false; + public MatchingStrategy getMatchingStrategy() { + return this.matchingStrategy; + } + + public void setMatchingStrategy(MatchingStrategy matchingStrategy) { + this.matchingStrategy = matchingStrategy; + } + + @DeprecatedConfigurationProperty( + reason = "Use of path extensions for request mapping and for content negotiation is discouraged.") + @Deprecated public boolean isUseSuffixPattern() { return this.useSuffixPattern; } + @Deprecated public void setUseSuffixPattern(boolean useSuffixPattern) { this.useSuffixPattern = useSuffixPattern; } + @DeprecatedConfigurationProperty( + reason = "Use of path extensions for request mapping and for content negotiation is discouraged.") + @Deprecated public boolean isUseRegisteredSuffixPattern() { return this.useRegisteredSuffixPattern; } + @Deprecated public void setUseRegisteredSuffixPattern(boolean useRegisteredSuffixPattern) { this.useRegisteredSuffixPattern = useRegisteredSuffixPattern; } } + public static class Format { + + /** + * Date format to use, for example 'dd/MM/yyyy'. + */ + private String date; + + /** + * Time format to use, for example 'HH:mm:ss'. + */ + private String time; + + /** + * Date-time format to use, for example 'yyyy-MM-dd HH:mm:ss'. + */ + private String dateTime; + + public String getDate() { + return this.date; + } + + public void setDate(String date) { + this.date = date; + } + + public String getTime() { + return this.time; + } + + public void setTime(String time) { + this.time = time; + } + + public String getDateTime() { + return this.dateTime; + } + + public void setDateTime(String dateTime) { + this.dateTime = dateTime; + } + + } + + /** + * Matching strategy options. + * @since 2.4.0 + */ + public enum MatchingStrategy { + + /** + * Use the {@code AntPathMatcher} implementation. + */ + ANT_PATH_MATCHER, + + /** + * Use the {@code PathPatternParser} implementation. + */ + PATH_PATTERN_PARSER + + } + + /** + * Locale resolution options. + * @deprecated since 2.4.0 for removal in 2.6.0 in favor of + * {@link org.springframework.boot.autoconfigure.web.WebProperties.LocaleResolver} + */ + @Deprecated public enum LocaleResolver { /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WelcomePageHandlerMapping.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WelcomePageHandlerMapping.java index 7717a8a548ed..2f33cbb3e9dc 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WelcomePageHandlerMapping.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WelcomePageHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ import java.util.Collections; import java.util.List; -import java.util.Optional; import javax.servlet.http.HttpServletRequest; @@ -49,9 +48,9 @@ final class WelcomePageHandlerMapping extends AbstractUrlHandlerMapping { private static final List MEDIA_TYPES_ALL = Collections.singletonList(MediaType.ALL); WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders, - ApplicationContext applicationContext, Optional welcomePage, String staticPathPattern) { - if (welcomePage.isPresent() && "/**".equals(staticPathPattern)) { - logger.info("Adding welcome page: " + welcomePage.get()); + ApplicationContext applicationContext, Resource welcomePage, String staticPathPattern) { + if (welcomePage != null && "/**".equals(staticPathPattern)) { + logger.info("Adding welcome page: " + welcomePage); setRootViewName("forward:index.html"); } else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/AbstractErrorController.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/AbstractErrorController.java index afb2f3dc4f82..9e00a0944e03 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/AbstractErrorController.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/AbstractErrorController.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,9 +20,11 @@ import java.util.List; import java.util.Map; +import javax.servlet.RequestDispatcher; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.springframework.boot.web.error.ErrorAttributeOptions; import org.springframework.boot.web.servlet.error.ErrorAttributes; import org.springframework.boot.web.servlet.error.ErrorController; import org.springframework.core.annotation.AnnotationAwareOrderComparator; @@ -38,6 +40,7 @@ * * @author Dave Syer * @author Phillip Webb + * @author Scott Frederick * @since 1.3.0 * @see ErrorAttributes */ @@ -66,13 +69,25 @@ private List sortErrorViewResolvers(List r return sorted; } - protected Map getErrorAttributes(HttpServletRequest request, boolean includeStackTrace) { + protected Map getErrorAttributes(HttpServletRequest request, ErrorAttributeOptions options) { WebRequest webRequest = new ServletWebRequest(request); - return this.errorAttributes.getErrorAttributes(webRequest, includeStackTrace); + return this.errorAttributes.getErrorAttributes(webRequest, options); } protected boolean getTraceParameter(HttpServletRequest request) { - String parameter = request.getParameter("trace"); + return getBooleanParameter(request, "trace"); + } + + protected boolean getMessageParameter(HttpServletRequest request) { + return getBooleanParameter(request, "message"); + } + + protected boolean getErrorsParameter(HttpServletRequest request) { + return getBooleanParameter(request, "errors"); + } + + protected boolean getBooleanParameter(HttpServletRequest request, String parameterName) { + String parameter = request.getParameter(parameterName); if (parameter == null) { return false; } @@ -80,7 +95,7 @@ protected boolean getTraceParameter(HttpServletRequest request) { } protected HttpStatus getStatus(HttpServletRequest request) { - Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code"); + Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); if (statusCode == null) { return HttpStatus.INTERNAL_SERVER_ERROR; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/BasicErrorController.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/BasicErrorController.java index 020db1c5781a..d9d0bb052a2b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/BasicErrorController.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/BasicErrorController.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,8 @@ import javax.servlet.http.HttpServletResponse; import org.springframework.boot.autoconfigure.web.ErrorProperties; -import org.springframework.boot.autoconfigure.web.ErrorProperties.IncludeStacktrace; +import org.springframework.boot.web.error.ErrorAttributeOptions; +import org.springframework.boot.web.error.ErrorAttributeOptions.Include; import org.springframework.boot.web.servlet.error.ErrorAttributes; import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory; import org.springframework.http.HttpStatus; @@ -32,6 +33,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.util.Assert; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.ModelAndView; @@ -45,6 +48,7 @@ * @author Phillip Webb * @author Michael Stummvoll * @author Stephane Nicoll + * @author Scott Frederick * @since 1.0.0 * @see ErrorAttributes * @see ErrorProperties @@ -77,16 +81,11 @@ public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties err this.errorProperties = errorProperties; } - @Override - public String getErrorPath() { - return this.errorProperties.getPath(); - } - @RequestMapping(produces = MediaType.TEXT_HTML_VALUE) public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { HttpStatus status = getStatus(request); Map model = Collections - .unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML))); + .unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); ModelAndView modelAndView = resolveErrorView(request, response, status, model); return (modelAndView != null) ? modelAndView : new ModelAndView("error", model); @@ -98,10 +97,33 @@ public ResponseEntity> error(HttpServletRequest request) { if (status == HttpStatus.NO_CONTENT) { return new ResponseEntity<>(status); } - Map body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL)); + Map body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL)); return new ResponseEntity<>(body, status); } + @ExceptionHandler(HttpMediaTypeNotAcceptableException.class) + public ResponseEntity mediaTypeNotAcceptable(HttpServletRequest request) { + HttpStatus status = getStatus(request); + return ResponseEntity.status(status).build(); + } + + protected ErrorAttributeOptions getErrorAttributeOptions(HttpServletRequest request, MediaType mediaType) { + ErrorAttributeOptions options = ErrorAttributeOptions.defaults(); + if (this.errorProperties.isIncludeException()) { + options = options.including(Include.EXCEPTION); + } + if (isIncludeStackTrace(request, mediaType)) { + options = options.including(Include.STACK_TRACE); + } + if (isIncludeMessage(request, mediaType)) { + options = options.including(Include.MESSAGE); + } + if (isIncludeBindingErrors(request, mediaType)) { + options = options.including(Include.BINDING_ERRORS); + } + return options; + } + /** * Determine if the stacktrace attribute should be included. * @param request the source request @@ -109,14 +131,48 @@ public ResponseEntity> error(HttpServletRequest request) { * @return if the stacktrace attribute should be included */ protected boolean isIncludeStackTrace(HttpServletRequest request, MediaType produces) { - IncludeStacktrace include = getErrorProperties().getIncludeStacktrace(); - if (include == IncludeStacktrace.ALWAYS) { + switch (getErrorProperties().getIncludeStacktrace()) { + case ALWAYS: return true; - } - if (include == IncludeStacktrace.ON_TRACE_PARAM) { + case ON_PARAM: return getTraceParameter(request); + default: + return false; + } + } + + /** + * Determine if the message attribute should be included. + * @param request the source request + * @param produces the media type produced (or {@code MediaType.ALL}) + * @return if the message attribute should be included + */ + protected boolean isIncludeMessage(HttpServletRequest request, MediaType produces) { + switch (getErrorProperties().getIncludeMessage()) { + case ALWAYS: + return true; + case ON_PARAM: + return getMessageParameter(request); + default: + return false; + } + } + + /** + * Determine if the errors attribute should be included. + * @param request the source request + * @param produces the media type produced (or {@code MediaType.ALL}) + * @return if the errors attribute should be included + */ + protected boolean isIncludeBindingErrors(HttpServletRequest request, MediaType produces) { + switch (getErrorProperties().getIncludeBindingErrors()) { + case ALWAYS: + return true; + case ON_PARAM: + return getErrorsParameter(request); + default: + return false; } - return false; } /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/DefaultErrorViewResolver.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/DefaultErrorViewResolver.java index b1181106d8fc..0a8ed8128b8b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/DefaultErrorViewResolver.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/DefaultErrorViewResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider; import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders; -import org.springframework.boot.autoconfigure.web.ResourceProperties; +import org.springframework.boot.autoconfigure.web.WebProperties.Resources; import org.springframework.context.ApplicationContext; import org.springframework.core.Ordered; import org.springframework.core.io.Resource; @@ -68,7 +68,7 @@ public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered { private ApplicationContext applicationContext; - private final ResourceProperties resourceProperties; + private final Resources resources; private final TemplateAvailabilityProviders templateAvailabilityProviders; @@ -78,21 +78,35 @@ public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered { * Create a new {@link DefaultErrorViewResolver} instance. * @param applicationContext the source application context * @param resourceProperties resource properties + * @deprecated since 2.4.0 for removal in 2.6.0 in favor of + * {@link #DefaultErrorViewResolver(ApplicationContext, Resources)} */ - public DefaultErrorViewResolver(ApplicationContext applicationContext, ResourceProperties resourceProperties) { + @Deprecated + public DefaultErrorViewResolver(ApplicationContext applicationContext, + org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties) { + this(applicationContext, (Resources) resourceProperties); + } + + /** + * Create a new {@link DefaultErrorViewResolver} instance. + * @param applicationContext the source application context + * @param resources resource properties + * @since 2.4.0 + */ + public DefaultErrorViewResolver(ApplicationContext applicationContext, Resources resources) { Assert.notNull(applicationContext, "ApplicationContext must not be null"); - Assert.notNull(resourceProperties, "ResourceProperties must not be null"); + Assert.notNull(resources, "Resources must not be null"); this.applicationContext = applicationContext; - this.resourceProperties = resourceProperties; + this.resources = resources; this.templateAvailabilityProviders = new TemplateAvailabilityProviders(applicationContext); } - DefaultErrorViewResolver(ApplicationContext applicationContext, ResourceProperties resourceProperties, + DefaultErrorViewResolver(ApplicationContext applicationContext, Resources resourceProperties, TemplateAvailabilityProviders templateAvailabilityProviders) { Assert.notNull(applicationContext, "ApplicationContext must not be null"); - Assert.notNull(resourceProperties, "ResourceProperties must not be null"); + Assert.notNull(resourceProperties, "Resources must not be null"); this.applicationContext = applicationContext; - this.resourceProperties = resourceProperties; + this.resources = resourceProperties; this.templateAvailabilityProviders = templateAvailabilityProviders; } @@ -116,7 +130,7 @@ private ModelAndView resolve(String viewName, Map model) { } private ModelAndView resolveResource(String viewName, Map model) { - for (String location : this.resourceProperties.getStaticLocations()) { + for (String location : this.resources.getStaticLocations()) { try { Resource resource = this.applicationContext.getResource(location); resource = resource.createRelative(viewName + ".html"); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfiguration.java index 458fc3de8d90..2ad277d4e221 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package org.springframework.boot.autoconfigure.web.servlet.error; import java.nio.charset.StandardCharsets; -import java.util.Date; import java.util.Map; import java.util.stream.Collectors; @@ -47,8 +46,9 @@ import org.springframework.boot.autoconfigure.condition.SpringBootCondition; import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider; import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders; -import org.springframework.boot.autoconfigure.web.ResourceProperties; import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.autoconfigure.web.WebProperties; +import org.springframework.boot.autoconfigure.web.WebProperties.Resources; import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties; @@ -81,6 +81,7 @@ * @author Andy Wilkinson * @author Stephane Nicoll * @author Brian Clozel + * @author Scott Frederick * @since 1.0.0 */ @Configuration(proxyBeanMethods = false) @@ -88,7 +89,7 @@ @ConditionalOnClass({ Servlet.class, DispatcherServlet.class }) // Load before the main WebMvcAutoConfiguration so that the error View is available @AutoConfigureBefore(WebMvcAutoConfiguration.class) -@EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class, WebMvcProperties.class }) +@EnableConfigurationProperties({ ServerProperties.class, WebMvcProperties.class }) public class ErrorMvcAutoConfiguration { private final ServerProperties serverProperties; @@ -100,7 +101,7 @@ public ErrorMvcAutoConfiguration(ServerProperties serverProperties) { @Bean @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT) public DefaultErrorAttributes errorAttributes() { - return new DefaultErrorAttributes(this.serverProperties.getError().isIncludeException()); + return new DefaultErrorAttributes(); } @Bean @@ -121,24 +122,29 @@ public static PreserveErrorControllerTargetClassPostProcessor preserveErrorContr return new PreserveErrorControllerTargetClassPostProcessor(); } + @SuppressWarnings("deprecation") @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties({ org.springframework.boot.autoconfigure.web.ResourceProperties.class, + WebProperties.class, WebMvcProperties.class }) static class DefaultErrorViewResolverConfiguration { private final ApplicationContext applicationContext; - private final ResourceProperties resourceProperties; + private final Resources resources; DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext, - ResourceProperties resourceProperties) { + org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties, + WebProperties webProperties) { this.applicationContext = applicationContext; - this.resourceProperties = resourceProperties; + this.resources = webProperties.getResources().hasBeenCustomized() ? webProperties.getResources() + : resourceProperties; } @Bean @ConditionalOnBean(DispatcherServlet.class) @ConditionalOnMissingBean(ErrorViewResolver.class) DefaultErrorViewResolver conventionErrorViewResolver() { - return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties); + return new DefaultErrorViewResolver(this.applicationContext, this.resources); } } @@ -206,7 +212,7 @@ public void render(Map model, HttpServletRequest request, HttpServlet } response.setContentType(TEXT_HTML_UTF8.toString()); StringBuilder builder = new StringBuilder(); - Date timestamp = (Date) model.get("timestamp"); + Object timestamp = model.get("timestamp"); Object message = model.get("message"); Object trace = model.get("trace"); if (response.getContentType() == null) { @@ -252,7 +258,7 @@ public String getContentType() { /** * {@link WebServerFactoryCustomizer} that configures the server's error pages. */ - private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered { + static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered { private final ServerProperties properties; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/reactive/TomcatWebSocketReactiveWebServerCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/reactive/TomcatWebSocketReactiveWebServerCustomizer.java index da424560d1d6..9bac4168f29e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/reactive/TomcatWebSocketReactiveWebServerCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/reactive/TomcatWebSocketReactiveWebServerCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ package org.springframework.boot.autoconfigure.websocket.reactive; -import org.apache.tomcat.websocket.server.WsContextListener; +import org.apache.tomcat.websocket.server.WsSci; import org.springframework.boot.web.embedded.tomcat.TomcatReactiveWebServerFactory; import org.springframework.boot.web.server.WebServerFactoryCustomizer; @@ -33,7 +33,7 @@ public class TomcatWebSocketReactiveWebServerCustomizer @Override public void customize(TomcatReactiveWebServerFactory factory) { - factory.addContextCustomizers((context) -> context.addApplicationListener(WsContextListener.class.getName())); + factory.addContextCustomizers((context) -> context.addServletContainerInitializer(new WsSci(), null)); } @Override diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/Jetty10WebSocketServletWebServerCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/Jetty10WebSocketServletWebServerCustomizer.java new file mode 100644 index 000000000000..c378d6683d02 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/Jetty10WebSocketServletWebServerCustomizer.java @@ -0,0 +1,110 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.websocket.servlet; + +import java.lang.reflect.Method; + +import javax.servlet.ServletContext; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.webapp.AbstractConfiguration; +import org.eclipse.jetty.webapp.WebAppContext; + +import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.core.Ordered; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * WebSocket customizer for {@link JettyServletWebServerFactory} with Jetty 10. + * + * @author Andy Wilkinson + */ +class Jetty10WebSocketServletWebServerCustomizer + implements WebServerFactoryCustomizer, Ordered { + + static final String JETTY_WEB_SOCKET_SERVER_CONTAINER = "org.eclipse.jetty.websocket.server.JettyWebSocketServerContainer"; + + static final String JAVAX_WEB_SOCKET_SERVER_CONTAINER = "org.eclipse.jetty.websocket.javax.server.internal.JavaxWebSocketServerContainer"; + + @Override + public void customize(JettyServletWebServerFactory factory) { + factory.addConfigurations(new AbstractConfiguration() { + + @Override + public void configure(WebAppContext context) throws Exception { + ServletContext servletContext = context.getServletContext(); + Class jettyContainer = ClassUtils.forName(JETTY_WEB_SOCKET_SERVER_CONTAINER, null); + Method getJettyContainer = ReflectionUtils.findMethod(jettyContainer, "getContainer", + ServletContext.class); + Server server = context.getServer(); + if (ReflectionUtils.invokeMethod(getJettyContainer, null, servletContext) == null) { + ensureWebSocketComponents(server, servletContext); + ensureContainer(jettyContainer, servletContext); + } + Class javaxContainer = ClassUtils.forName(JAVAX_WEB_SOCKET_SERVER_CONTAINER, null); + Method getJavaxContainer = ReflectionUtils.findMethod(javaxContainer, "getContainer", + ServletContext.class); + if (ReflectionUtils.invokeMethod(getJavaxContainer, "getContainer", servletContext) == null) { + ensureWebSocketComponents(server, servletContext); + ensureUpgradeFilter(servletContext); + ensureMappings(servletContext); + ensureContainer(javaxContainer, servletContext); + } + } + + private void ensureWebSocketComponents(Server server, ServletContext servletContext) + throws ClassNotFoundException { + Class webSocketServerComponents = ClassUtils + .forName("org.eclipse.jetty.websocket.core.server.WebSocketServerComponents", null); + Method ensureWebSocketComponents = ReflectionUtils.findMethod(webSocketServerComponents, + "ensureWebSocketComponents", Server.class, ServletContext.class); + ReflectionUtils.invokeMethod(ensureWebSocketComponents, null, server, servletContext); + } + + private void ensureContainer(Class container, ServletContext servletContext) + throws ClassNotFoundException { + Method ensureContainer = ReflectionUtils.findMethod(container, "ensureContainer", ServletContext.class); + ReflectionUtils.invokeMethod(ensureContainer, null, servletContext); + } + + private void ensureUpgradeFilter(ServletContext servletContext) throws ClassNotFoundException { + Class webSocketUpgradeFilter = ClassUtils + .forName("org.eclipse.jetty.websocket.servlet.WebSocketUpgradeFilter", null); + Method ensureFilter = ReflectionUtils.findMethod(webSocketUpgradeFilter, "ensureFilter", + ServletContext.class); + ReflectionUtils.invokeMethod(ensureFilter, null, servletContext); + } + + private void ensureMappings(ServletContext servletContext) throws ClassNotFoundException { + Class webSocketMappings = ClassUtils + .forName("org.eclipse.jetty.websocket.core.server.WebSocketMappings", null); + Method ensureMappings = ReflectionUtils.findMethod(webSocketMappings, "ensureMappings", + ServletContext.class); + ReflectionUtils.invokeMethod(ensureMappings, null, servletContext); + } + + }); + } + + @Override + public int getOrder() { + return 0; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/JettyWebSocketServletWebServerCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/JettyWebSocketServletWebServerCustomizer.java index 1d514dc2556e..6b057b071490 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/JettyWebSocketServletWebServerCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/JettyWebSocketServletWebServerCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,7 +43,7 @@ public void customize(JettyServletWebServerFactory factory) { @Override public void configure(WebAppContext context) throws Exception { - ServerContainer serverContainer = WebSocketServerContainerInitializer.configureContext(context); + ServerContainer serverContainer = WebSocketServerContainerInitializer.initialize(context); ShutdownThread.deregister(serverContainer); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/TomcatWebSocketServletWebServerCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/TomcatWebSocketServletWebServerCustomizer.java index 86c33f815816..e390b5bf2d10 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/TomcatWebSocketServletWebServerCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/TomcatWebSocketServletWebServerCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ package org.springframework.boot.autoconfigure.websocket.servlet; -import org.apache.tomcat.websocket.server.WsContextListener; +import org.apache.tomcat.websocket.server.WsSci; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.server.WebServerFactoryCustomizer; @@ -35,7 +35,7 @@ public class TomcatWebSocketServletWebServerCustomizer @Override public void customize(TomcatServletWebServerFactory factory) { - factory.addContextCustomizers((context) -> context.addApplicationListener(WsContextListener.class.getName())); + factory.addContextCustomizers((context) -> context.addServletContainerInitializer(new WsSci(), null)); } @Override diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketMessagingAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketMessagingAutoConfiguration.java index 902d37f40f6d..58a5c0c9c58a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketMessagingAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketMessagingAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.boot.LazyInitializationExcludeFilter; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -27,6 +28,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.converter.ByteArrayMessageConverter; import org.springframework.messaging.converter.DefaultContentTypeResolver; @@ -74,6 +76,11 @@ public boolean configureMessageConverters(List messageConverte return false; } + @Bean + static LazyInitializationExcludeFilter eagerStompWebSocketHandlerMapping() { + return (name, definition, type) -> name.equals("stompWebSocketHandlerMapping"); + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketServletAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketServletAutoConfiguration.java index de8a544e0b01..70f0d2bf4e62 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketServletAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketServletAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -83,6 +83,19 @@ JettyWebSocketServletWebServerCustomizer websocketServletWebServerCustomizer() { } + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(name = { Jetty10WebSocketServletWebServerCustomizer.JAVAX_WEB_SOCKET_SERVER_CONTAINER, + Jetty10WebSocketServletWebServerCustomizer.JETTY_WEB_SOCKET_SERVER_CONTAINER }) + static class Jetty10WebSocketConfiguration { + + @Bean + @ConditionalOnMissingBean(name = "websocketServletWebServerCustomizer") + Jetty10WebSocketServletWebServerCustomizer websocketServletWebServerCustomizer() { + return new Jetty10WebSocketServletWebServerCustomizer(); + } + + } + @Configuration(proxyBeanMethods = false) @ConditionalOnClass(io.undertow.websockets.jsr.Bootstrap.class) static class UndertowWebSocketConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 635da717b8fe..ceea4b65066c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -1,34 +1,59 @@ { + "groups": [], "properties": [ { - "name": "security.basic.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable basic authentication.", - "defaultValue": true, - "deprecation": { - "reason": "The security auto-configuration is no longer customizable. Provide your own WebSecurityConfigurer bean instead.", - "level": "error" - } + "name": "server.compression.enabled", + "description": "Whether response compression is enabled.", + "defaultValue": false }, { - "name": "security.filter-dispatcher-types", - "type": "java.util.Set", - "description": "Security filter chain dispatcher types.", - "deprecation": { - "replacement": "spring.security.filter.dispatcher-types", - "level": "error" - } + "name": "server.compression.excluded-user-agents", + "description": "Comma-separated list of user agents for which responses should not be compressed." }, { - "name": "security.filter-order", - "type": "java.lang.Integer", - "description": "Security filter chain order.", - "defaultValue": 0, + "name": "server.compression.mime-types", + "description": "Comma-separated list of MIME types that should be compressed.", + "defaultValue": [ + "text/html", + "text/xml", + "text/plain", + "text/css", + "text/javascript", + "application/javascript", + "application/json", + "application/xml" + ] + }, + { + "name": "server.compression.min-response-size", + "description": "Minimum \"Content-Length\" value that is required for compression to be performed.", + "defaultValue": "2KB" + }, + { + "name": "server.connection-timeout", + "type": "java.time.Duration", "deprecation": { - "replacement": "spring.security.filter.order", + "reason": "Each server behaves differently. Use server specific properties instead.", "level": "error" } }, + { + "name": "server.error.include-binding-errors", + "defaultValue": "never" + }, + { + "name": "server.error.include-message", + "defaultValue": "never" + }, + { + "name": "server.error.include-stacktrace", + "defaultValue": "never" + }, + { + "name": "server.http2.enabled", + "description": "Whether to enable HTTP/2 support, if the current environment supports it.", + "defaultValue": false + }, { "name": "server.jetty.accesslog.date-format", "deprecation": { @@ -43,6 +68,10 @@ "level": "error" } }, + { + "name": "server.jetty.accesslog.format", + "defaultValue": "ncsa" + }, { "name": "server.jetty.accesslog.locale", "deprecation": { @@ -79,46 +108,33 @@ } }, { - "name": "server.compression.enabled", - "description": "Whether response compression is enabled.", - "defaultValue": false - }, - { - "name": "server.compression.excluded-user-agents", - "description": "Comma-separated list of user agents for which responses should not be compressed." - }, - { - "name": "server.compression.mime-types", - "description": "Comma-separated list of MIME types that should be compressed.", - "defaultValue": [ - "text/html", - "text/xml", - "text/plain", - "text/css", - "text/javascript", - "application/javascript", - "application/json", - "application/xml" - ] - }, - { - "name": "server.compression.min-response-size", - "description": "Minimum \"Content-Length\" value that is required for compression to be performed.", - "defaultValue": "2KB" - }, - { - "name": "server.error.include-stacktrace", - "defaultValue": "never" + "name": "server.jetty.max-http-post-size", + "type": "org.springframework.util.unit.DataSize", + "deprecation": { + "replacement": "server.jetty.max-http-form-post-size", + "level": "error" + } }, { - "name": "server.http2.enabled", - "description": "Whether to enable HTTP/2 support, if the current environment supports it.", - "defaultValue": false + "name": "server.max-http-post-size", + "type": "java.lang.Integer", + "description": "Maximum size in bytes of the HTTP post content.", + "defaultValue": 0, + "deprecation": { + "reason": "Use dedicated property for each container.", + "level": "error" + } }, { "name": "server.port", "defaultValue": 8080 }, + { + "name": "server.servlet.encoding.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable http encoding support.", + "defaultValue": true + }, { "name": "server.servlet.jsp.class-name", "description": "Class name of the servlet to use for JSPs. If registered is true and this class\n\t * is on the classpath then it will be registered.", @@ -133,13 +149,23 @@ "description": "Whether the JSP servlet is registered.", "defaultValue": true }, + { + "name": "server.servlet.path", + "type": "java.lang.String", + "description": "Path of the main dispatcher servlet.", + "defaultValue": "/", + "deprecation": { + "replacement": "spring.mvc.servlet.path", + "level": "error" + } + }, { "name": "server.servlet.session.cookie.comment", "description": "Comment for the session cookie." }, { "name": "server.servlet.session.cookie.domain", - "description": " Domain for the session cookie." + "description": "Domain for the session cookie." }, { "name": "server.servlet.session.cookie.http-only", @@ -179,6 +205,10 @@ "name": "server.servlet.session.tracking-modes", "description": "Session tracking modes." }, + { + "name": "server.shutdown", + "defaultValue": "immediate" + }, { "name": "server.ssl.ciphers", "description": "Supported SSL ciphers." @@ -223,7 +253,7 @@ { "name": "server.ssl.protocol", "description": "SSL protocol to use.", - "defaultValue": "TLS" + "defaultValue": "TLS" }, { "name": "server.ssl.trust-store", @@ -242,16 +272,47 @@ "description": "Type of the trust store." }, { - "name": "spring.aop.auto", + "name": "server.tomcat.max-http-post-size", + "type": "org.springframework.util.unit.DataSize", + "deprecation": { + "replacement": "server.tomcat.max-http-form-post-size", + "level": "error" + } + }, + { + "name": "server.undertow.buffers-per-region", + "type": "java.lang.Integer", + "description": "Number of buffer per region.", + "deprecation": { + "level": "error" + } + }, + { + "name": "server.use-forward-headers", "type": "java.lang.Boolean", - "description": "Add @EnableAspectJAutoProxy.", - "defaultValue": true + "deprecation": { + "reason": "Replaced to support additional strategies.", + "replacement": "server.forward-headers-strategy", + "level": "error" + } }, { - "name": "spring.aop.proxy-target-class", + "name": "spring.activemq.pool.create-connection-on-startup", "type": "java.lang.Boolean", - "description": "Whether subclass-based (CGLIB) proxies are to be created (true), as opposed to standard Java interface-based proxies (false).", - "defaultValue": true + "description": "Whether to create a connection on startup. Can be used to warm up the pool on startup.", + "defaultValue": true, + "deprecation": { + "level": "error" + } + }, + { + "name": "spring.activemq.pool.expiry-timeout", + "type": "java.time.Duration", + "description": "Connection expiration timeout.", + "defaultValue": "0ms", + "deprecation": { + "level": "error" + } }, { "name": "spring.activemq.pool.maximum-active-session-per-connection", @@ -260,11 +321,26 @@ } }, { - "name": "spring.artemis.pool.maximum-active-session-per-connection", + "name": "spring.activemq.pool.reconnect-on-exception", + "type": "java.lang.Boolean", + "description": "Reset the connection when a \"JMSException\" occurs.", + "defaultValue": true, "deprecation": { - "replacement": "spring.artemis.pool.max-sessions-per-connection" + "level": "error" } }, + { + "name": "spring.aop.auto", + "type": "java.lang.Boolean", + "description": "Add @EnableAspectJAutoProxy.", + "defaultValue": true + }, + { + "name": "spring.aop.proxy-target-class", + "type": "java.lang.Boolean", + "description": "Whether subclass-based (CGLIB) proxies are to be created (true), as opposed to standard Java interface-based proxies (false).", + "defaultValue": true + }, { "name": "spring.application.admin.enabled", "type": "java.lang.Boolean", @@ -277,6 +353,16 @@ "description": "JMX name of the application admin MBean.", "defaultValue": "org.springframework.boot:type=Admin,name=SpringApplication" }, + { + "name": "spring.artemis.broker-url", + "defaultValue": "tcp://localhost:61616" + }, + { + "name": "spring.artemis.pool.maximum-active-session-per-connection", + "deprecation": { + "replacement": "spring.artemis.pool.max-sessions-per-connection" + } + }, { "name": "spring.autoconfigure.exclude", "type": "java.util.List", @@ -287,1471 +373,927 @@ "defaultValue": "embedded" }, { - "name": "spring.batch.job.enabled", + "name": "spring.batch.initializer.enabled", "type": "java.lang.Boolean", - "description": "Execute all Spring Batch jobs in the context on startup.", - "defaultValue": true + "description": "Create the required batch tables on startup if necessary. Enabled automatically\n if no custom table prefix is set or if a custom schema is configured.", + "deprecation": { + "replacement": "spring.batch.jdbc.initialize-schema", + "level": "error" + } }, { - "name": "spring.dao.exceptiontranslation.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable the PersistenceExceptionTranslationPostProcessor.", - "defaultValue": true + "name": "spring.batch.jdbc.initialize-schema", + "defaultValue": "embedded" }, { - "name": "spring.datasource.initialization-mode", - "defaultValue": "embedded" + "name": "spring.batch.job.enabled", + "type": "java.lang.Boolean", + "description": "Execute all Spring Batch jobs in the context on startup.", + "defaultValue": true }, { - "name": "spring.data.cassandra.contact-points", - "defaultValue": [ - "localhost" - ] + "name": "spring.couchbase.bootstrap-hosts", + "type": "java.util.List", + "description": "Couchbase nodes (host or IP address) to bootstrap from.", + "deprecation": { + "replacement": "spring.couchbase.connection-string", + "level": "error" + } }, { - "name": "spring.data.cassandra.compression", - "defaultValue": "none" + "name": "spring.couchbase.bucket.name", + "type": "java.lang.String", + "description": "Name of the bucket to connect to.", + "deprecation": { + "reason": "A bucket is no longer auto-configured.", + "level": "error" + } }, { - "name": "spring.data.cassandra.repositories.type", - "type": "org.springframework.boot.autoconfigure.data.RepositoryType", - "description": "Type of Cassandra repositories to enable.", - "defaultValue": "auto" + "name": "spring.couchbase.bucket.password", + "type": "java.lang.String", + "description": "Password of the bucket.", + "deprecation": { + "reason": "A bucket is no longer auto-configured.", + "level": "error" + } }, { - "name": "spring.data.cassandra.repositories.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable Cassandra repositories.", - "defaultValue": true, + "name": "spring.couchbase.env.bootstrap.http-direct-port", + "type": "java.lang.Integer", + "description": "Port for the HTTP bootstrap.", "deprecation": { - "replacement": "spring.data.cassandra.repositories.type", "level": "error" } }, { - "name": "spring.data.couchbase.consistency", - "defaultValue": "read-your-own-writes" + "name": "spring.couchbase.env.bootstrap.http-ssl-port", + "type": "java.lang.Integer", + "description": "Port for the HTTPS bootstrap.", + "deprecation": { + "level": "error" + } }, { - "name": "spring.data.couchbase.repositories.type", - "type": "org.springframework.boot.autoconfigure.data.RepositoryType", - "description": "Type of Couchbase repositories to enable.", - "defaultValue": "auto" + "name": "spring.couchbase.env.endpoints.key-value", + "type": "java.lang.Integer", + "description": "Number of sockets per node against the key/value service.", + "deprecation": { + "level": "error" + } }, { - "name": "spring.data.couchbase.repositories.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable Couchbase repositories.", - "defaultValue": true, + "name": "spring.couchbase.env.endpoints.query", + "type": "java.lang.Integer", + "description": "Number of sockets per node against the query (N1QL) service.", "deprecation": { - "replacement": "spring.data.couchbase.repositories.type", "level": "error" } }, { - "name": "spring.data.elasticsearch.cluster-name", - "type": "java.lang.String", - "description": "Elasticsearch cluster name.", + "name": "spring.couchbase.env.endpoints.queryservice.max-endpoints", + "type": "java.lang.Integer", + "description": "Maximum number of sockets per node.", "deprecation": { - "reason": "The transport client support is deprecated. Use other supported clients instead." + "replacement": "spring.couchbase.env.io.max-endpoints", + "level": "error" } }, { - "name": "spring.data.elasticsearch.cluster-nodes", - "type": "java.lang.String", - "description": "Comma-separated list of cluster node addresses.", + "name": "spring.couchbase.env.endpoints.queryservice.min-endpoints", + "type": "java.lang.Integer", + "description": "Minimum number of sockets per node.", "deprecation": { - "reason": "The transport client support is deprecated. Use other supported clients instead." + "replacement": "spring.couchbase.env.io.min-endpoints", + "level": "error" } }, { - "name": "spring.data.elasticsearch.properties", - "type": "java.util.Map", - "description": "Additional properties used to configure the client.", + "name": "spring.couchbase.env.endpoints.view", + "type": "java.lang.Integer", + "description": "Number of sockets per node against the view service.", "deprecation": { - "reason": "The transport client support is deprecated. Use other supported clients instead." + "level": "error" } }, { - "name": "spring.data.elasticsearch.repositories.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable Elasticsearch repositories.", - "defaultValue": true - }, - { - "name": "spring.data.jdbc.repositories.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable JDBC repositories.", - "defaultValue": true - }, - { - "name": "spring.data.jpa.repositories.bootstrap-mode", - "type": "org.springframework.data.repository.config.BootstrapMode", - "description": "Bootstrap mode for JPA repositories.", - "defaultValue": "default" - }, - { - "name": "spring.data.jpa.repositories.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable JPA repositories.", - "defaultValue": true - }, - { - "name": "spring.data.ldap.repositories.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable LDAP repositories.", - "defaultValue": true - }, - { - "name": "spring.data.mongodb.repositories.type", - "type": "org.springframework.boot.autoconfigure.data.RepositoryType", - "description": "Type of Mongo repositories to enable.", - "defaultValue": "auto" - }, - { - "name": "spring.data.mongodb.repositories.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable Mongo repositories.", - "defaultValue": true, + "name": "spring.couchbase.env.endpoints.viewservice.max-endpoints", + "type": "java.lang.Integer", + "description": "Maximum number of sockets per node.", "deprecation": { - "replacement": "spring.data.mongodb.repositories.type", + "replacement": "spring.couchbase.env.io.max-endpoints", "level": "error" } }, { - "name": "spring.data.mongodb.uri", - "defaultValue": "mongodb://localhost/test" - }, - { - "name": "spring.data.neo4j.auto-index", - "defaultValue": "none" - }, - { - "name": "spring.data.neo4j.open-in-view", - "defaultValue": true - }, - { - "name": "spring.data.neo4j.repositories.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable Neo4j repositories.", - "defaultValue": true - }, - { - "name": "spring.data.redis.repositories.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable Redis repositories.", - "defaultValue": true - }, - { - "name": "spring.data.rest.detection-strategy", - "defaultValue": "default" - }, - { - "name": "spring.data.solr.repositories.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable Solr repositories.", - "defaultValue": true - }, - { - "name": "spring.elasticsearch.jest.uris", - "defaultValue": [ - "http://localhost:9200" - ], + "name": "spring.couchbase.env.endpoints.viewservice.min-endpoints", + "type": "java.lang.Integer", + "description": "Minimum number of sockets per node.", "deprecation": { - "reason": "The Jest client support is deprecated. Use other supported clients instead." + "replacement": "spring.couchbase.env.io.min-endpoints", + "level": "error" } }, { - "name": "spring.elasticsearch.rest.uris", - "defaultValue": [ - "http://localhost:9200" - ] - }, - { - "name": "spring.info.build.location", - "defaultValue": "classpath:META-INF/build-info.properties" - }, - { - "name": "spring.info.git.location", - "defaultValue": "classpath:git.properties" - }, - { - "name": "spring.flyway.locations", - "sourceType": "org.springframework.boot.autoconfigure.flyway.FlywayProperties", - "defaultValue": [ - "classpath:db/migration" - ] - }, - { - "name": "spring.flyway.sql-migration-suffixes", - "sourceType": "org.springframework.boot.autoconfigure.flyway.FlywayProperties", - "defaultValue": [ - ".sql" - ] - }, - { - "name": "spring.freemarker.prefix", - "defaultValue": "" - }, - { - "name": "spring.freemarker.suffix", - "defaultValue": ".ftlh" - }, - { - "name": "spring.groovy.template.prefix", - "defaultValue": "" - }, - { - "name": "spring.groovy.template.suffix", - "defaultValue": ".tpl" - }, - { - "name": "spring.http.encoding.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable http encoding support.", - "defaultValue": true - }, - { - "name": "spring.http.converters.preferred-json-mapper", - "type": "java.lang.String", - "description": "Preferred JSON mapper to use for HTTP message conversion. By default, auto-detected according to the environment." - }, - { - "name": "spring.integration.jdbc.initialize-schema", - "defaultValue": "embedded" - }, - { - "name": "spring.jersey.type", - "defaultValue": "servlet" - }, - { - "name": "spring.jmx.default-domain", - "type": "java.lang.String", - "description": "JMX domain name." - }, - { - "name": "spring.jmx.enabled", - "type": "java.lang.Boolean", - "description": "Expose management beans to the JMX domain.", - "defaultValue": false - }, - { - "name": "spring.jmx.server", - "type": "java.lang.String", - "description": "MBeanServer bean name.", - "defaultValue": "mbeanServer" - }, - { - "name": "spring.jmx.unique-names", - "type": "java.lang.Boolean", - "description": "Whether unique runtime object names should be ensured.", - "defaultValue": false - }, - { - "name": "spring.jpa.open-in-view", - "defaultValue": true - }, - { - "name": "spring.jta.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable JTA support.", - "defaultValue": true - }, - { - "name": "spring.jta.bitronix.properties.allow-multiple-lrc", - "description": "Whether to allow multiple LRC resources to be enlisted into the same transaction.", - "defaultValue": false - }, - { - "name": "spring.jta.bitronix.properties.asynchronous2-pc", - "description": "Whether to enable asynchronously execution of two phase commit.", - "defaultValue": false - }, - { - "name": "spring.jta.bitronix.properties.background-recovery-interval", - "description": "Interval in minutes at which to run the recovery process in the background.", - "defaultValue": 1, + "name": "spring.couchbase.env.timeouts.socket-connect", + "type": "java.time.Duration", + "description": "Socket connect connections timeout.", "deprecation": { - "replacement": "spring.jta.bitronix.properties.background-recovery-interval-seconds" - } - }, - { - "name": "spring.jta.bitronix.properties.background-recovery-interval-seconds", - "description": "Interval in seconds at which to run the recovery process in the background.", - "defaultValue": 60 - }, - { - "name": "spring.jta.bitronix.properties.current-node-only-recovery", - "description": "Whether to recover only the current node. Should be enabled if you run multiple instances of the transaction manager on the same JMS and JDBC resources.", - "defaultValue": true - }, - { - "name": "spring.jta.bitronix.properties.debug-zero-resource-transaction", - "description": "Whether to log the creation and commit call stacks of transactions executed without a single enlisted resource.", - "defaultValue": false - }, - { - "name": "spring.jta.bitronix.properties.default-transaction-timeout", - "description": "Default transaction timeout, in seconds.", - "defaultValue": 60 - }, - { - "name": "spring.jta.bitronix.properties.disable-jmx", - "description": "Whether to enable JMX support.", - "defaultValue": false - }, - { - "name": "spring.jta.bitronix.properties.exception-analyzer", - "description": "Set the fully qualified name of the exception analyzer implementation to use." - }, - { - "name": "spring.jta.bitronix.properties.filter-log-status", - "description": "Whether to enable filtering of logs so that only mandatory logs are written.", - "defaultValue": false - }, - { - "name": "spring.jta.bitronix.properties.force-batching-enabled", - "description": "Whether disk forces are batched.", - "defaultValue": true - }, - { - "name": "spring.jta.bitronix.properties.forced-write-enabled", - "description": "Whether logs are forced to disk.", - "defaultValue": true - }, - { - "name": "spring.jta.bitronix.properties.graceful-shutdown-interval", - "description": "Maximum amount of seconds the TM waits for transactions to get done before aborting them at shutdown time.", - "defaultValue": 60 - }, - { - "name": "spring.jta.bitronix.properties.jndi-transaction-synchronization-registry-name", - "description": "JNDI name of the TransactionSynchronizationRegistry." - }, - { - "name": "spring.jta.bitronix.properties.jndi-user-transaction-name", - "description": "JNDI name of the UserTransaction." - }, - { - "name": "spring.jta.bitronix.properties.journal", - "description": "Name of the journal. Can be 'disk', 'null', or a class name.", - "defaultValue": "disk" - }, - { - "name": "spring.jta.bitronix.properties.log-part1-filename", - "description": "Name of the first fragment of the journal.", - "defaultValue": "btm1.tlog" - }, - { - "name": "spring.jta.bitronix.properties.log-part2-filename", - "description": "Name of the second fragment of the journal.", - "defaultValue": "btm2.tlog" - }, - { - "name": "spring.jta.bitronix.properties.max-log-size-in-mb", - "description": "Maximum size in megabytes of the journal fragments.", - "defaultValue": 2 - }, - { - "name": "spring.jta.bitronix.properties.resource-configuration-filename", - "description": "ResourceLoader configuration file name." - }, - { - "name": "spring.jta.bitronix.properties.server-id", - "description": "ASCII ID that must uniquely identify this TM instance. Defaults to the machine's IP address." - }, - { - "name": "spring.jta.bitronix.properties.skip-corrupted-logs", - "description": "Skip corrupted transactions log entries. Use only at last resort when all you have to recover is a pair of corrupted files.", - "defaultValue": false - }, - { - "name": "spring.jta.bitronix.properties.warn-about-zero-resource-transaction", - "description": "Whether to log a warning for transactions executed without a single enlisted resource.", - "defaultValue": true - }, - { - "name": "spring.kafka.jaas.control-flag", - "defaultValue": "required" - }, - { - "name": "spring.kafka.listener.type", - "defaultValue": "single" - }, - { - "name": "spring.mail.test-connection", - "description": "Whether to test that the mail server is available on startup.", - "sourceType": "org.springframework.boot.autoconfigure.mail.MailProperties", - "type": "java.lang.Boolean", - "defaultValue": false - }, - { - "name": "spring.mongodb.embedded.features", - "defaultValue": [ - "sync_delay" - ] - }, - { - "name": "spring.mustache.prefix", - "defaultValue": "classpath:/templates/" - }, - { - "name": "spring.mustache.suffix", - "defaultValue": ".mustache" - }, - { - "name": "spring.mvc.formcontent.putfilter.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable Spring's HttpPutFormContentFilter.", - "defaultValue": true, - "deprecation" : { - "replacement" : "spring.mvc.formcontent.filter.enabled", - "level" : "error" + "level": "error" } }, { - "name": "spring.mvc.formcontent.filter.enabled", + "name": "spring.dao.exceptiontranslation.enabled", "type": "java.lang.Boolean", - "description": "Whether to enable Spring's FormContentFilter.", + "description": "Whether to enable the PersistenceExceptionTranslationPostProcessor.", "defaultValue": true }, { - "name": "spring.mvc.hiddenmethod.filter.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable Spring's HiddenHttpMethodFilter.", - "defaultValue": false - }, - { - "name" : "spring.mvc.media-types", - "type" : "java.util.Map", - "description" : "Maps file extensions to media types for content negotiation, e.g. yml to text/yaml.", - "deprecation" : { - "replacement" : "spring.mvc.contentnegotiation.media-types", - "level" : "error" - } + "name": "spring.data.cassandra.compression", + "defaultValue": "none" }, { - "name": "spring.mvc.locale-resolver", - "defaultValue": "accept-header" + "name": "spring.data.cassandra.connection.connect-timeout", + "defaultValue": "5s" }, { - "name" : "spring.resources.chain.gzipped", - "type" : "java.lang.Boolean", - "description" : "Whether to enable resolution of already gzipped resources. Checks for a resource name variant with the \"*.gz\" extension.", - "deprecation" : { - "replacement" : "spring.resources.chain.compressed", - "level" : "error" - } + "name": "spring.data.cassandra.connection.init-query-timeout", + "defaultValue": "5s" }, { - "name": "spring.quartz.jdbc.comment-prefix", + "name": "spring.data.cassandra.contact-points", "defaultValue": [ - "#", - "--" + "127.0.0.1:9042" ] }, { - "name": "spring.quartz.jdbc.initialize-schema", - "defaultValue": "embedded" - }, - { - "name": "spring.quartz.job-store-type", - "defaultValue": "memory" - }, - { - "name": "spring.quartz.scheduler-name", - "defaultValue": "quartzScheduler" - }, - { - "name": "spring.rabbitmq.cache.connection.mode", - "defaultValue": "channel" - }, - { - "name": "spring.rabbitmq.dynamic", + "name": "spring.data.cassandra.jmx-enabled", "type": "java.lang.Boolean", - "description": "Whether to create an AmqpAdmin bean.", - "defaultValue": true - }, - { - "name": "spring.rabbitmq.listener.type", - "defaultValue": "simple" - }, - { - "name": "spring.reactor.stacktrace-mode.enabled", - "description": "Whether Reactor should collect stacktrace information at runtime.", - "defaultValue": false, + "description": "Whether to enable JMX reporting. Default to false as Cassandra JMX reporting is not compatible with Dropwizard Metrics.", "deprecation": { - "replacement": "spring.reactor.debug-agent.enabled" + "reason": "Cassandra no longer provides JMX metrics.", + "level": "error" } }, { - "name": "spring.security.filter.dispatcher-types", - "defaultValue": [ - "async", - "error", - "request" - ] - }, - { - "name": "spring.security.filter.order", - "defaultValue": -100 - }, - { - "name": "spring.session.jdbc.initialize-schema", - "defaultValue": "embedded" + "name": "spring.data.cassandra.load-balancing-policy", + "type": "java.lang.Class", + "description": "Class name of the load balancing policy. The class must have a default constructor.", + "deprecation": { + "level": "error" + } }, { - "name": "spring.session.hazelcast.flush-mode", - "defaultValue": "on-save" + "name": "spring.data.cassandra.pool.heartbeat-interval", + "defaultValue": "30s" }, { - "name": "spring.session.servlet.filter-dispatcher-types", - "defaultValue": [ - "async", - "error", - "request" - ] + "name": "spring.data.cassandra.pool.idle-timeout", + "defaultValue": "5s" }, { - "name": "spring.session.redis.flush-mode", - "defaultValue": "on-save" - }, - { - "name": "flyway.baseline-description", - "type": "java.lang.String", + "name": "spring.data.cassandra.pool.max-queue-size", + "type": "java.lang.Integer", "deprecation": { - "replacement": "spring.flyway.baseline-description", + "replacement": "spring.data.cassandra.request.throttler.max-queue-size", "level": "error" } }, { - "name": "flyway.baseline-on-migrate", - "type": "java.lang.Boolean", + "name": "spring.data.cassandra.pool.pool-timeout", + "type": "java.time.Duration", + "description": "Pool timeout when trying to acquire a connection from a host's pool.", "deprecation": { - "replacement": "spring.flyway.baseline-on-migrate", + "reason": "No longer available.", "level": "error" } }, { - "name": "flyway.baseline-version", - "type": "org.flywaydb.core.api.MigrationVersion", + "name": "spring.data.cassandra.reconnection-policy", + "type": "java.lang.Class", + "description": "Class name of the reconnection policy. The class must have a default constructor.", "deprecation": { - "replacement": "spring.flyway.baseline-version", "level": "error" } }, { - "name": "flyway.check-location", - "type": "java.lang.Boolean", - "description": "Check that migration scripts location exists.", - "defaultValue": false, - "deprecation": { - "replacement": "spring.flyway.check-location", - "level": "error" - } + "name": "spring.data.cassandra.repositories.type", + "type": "org.springframework.boot.autoconfigure.data.RepositoryType", + "description": "Type of Cassandra repositories to enable.", + "defaultValue": "auto" }, { - "name": "flyway.clean-on-validation-error", - "type": "java.lang.Boolean", - "deprecation": { - "replacement": "spring.flyway.clean-on-validation-error", - "level": "error" - } + "name": "spring.data.cassandra.request.page-size", + "defaultValue": 5000 }, { - "name": "flyway.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable flyway.", - "defaultValue": true, - "deprecation": { - "replacement": "spring.flyway.enabled", - "level": "error" - } + "name": "spring.data.cassandra.request.throttler.type", + "defaultValue": "none" }, { - "name": "flyway.encoding", - "type": "java.nio.charset.Charset", - "description": "Encoding of SQL migrations.", - "deprecation": { - "replacement": "spring.flyway.encoding", - "level": "error" - } + "name": "spring.data.cassandra.request.timeout", + "defaultValue": "2s" }, { - "name": "flyway.init-description", - "type": "java.lang.String", + "name": "spring.data.cassandra.retry-policy", + "type": "java.lang.Class", + "description": "Class name of the retry policy. The class must have a default constructor.", "deprecation": { "level": "error" } }, { - "name": "flyway.init-on-migrate", - "type": "java.lang.Boolean", + "name": "spring.data.couchbase.consistency", + "type": "org.springframework.data.couchbase.core.query.Consistency", "deprecation": { "level": "error" } }, { - "name": "flyway.init-sqls", - "type": "java.util.List", - "description": "SQL statements to execute to initialize a connection immediately after obtaining\n it.", + "name": "spring.data.couchbase.repositories.type", + "type": "org.springframework.boot.autoconfigure.data.RepositoryType", + "description": "Type of Couchbase repositories to enable.", + "defaultValue": "auto" + }, + { + "name": "spring.data.elasticsearch.client.reactive.endpoints", + "defaultValue": [ + "localhost:9200" + ] + }, + { + "name": "spring.data.elasticsearch.cluster-name", + "type": "java.lang.String", + "description": "Elasticsearch cluster name.", "deprecation": { - "replacement": "spring.flyway.init-sqls", "level": "error" } }, { - "name": "flyway.init-version", - "type": "org.flywaydb.core.api.MigrationVersion", + "name": "spring.data.elasticsearch.cluster-nodes", + "type": "java.lang.String", + "description": "Comma-separated list of cluster node addresses.", "deprecation": { "level": "error" } }, { - "name": "flyway.locations", - "type": "java.util.List", - "description": "Locations of migrations scripts. Can contain the special \"{vendor}\" placeholder to\n use vendor-specific locations.", + "name": "spring.data.elasticsearch.properties", + "type": "java.util.Map", + "description": "Additional properties used to configure the client.", "deprecation": { - "replacement": "spring.flyway.locations", "level": "error" } }, { - "name": "flyway.out-of-order", + "name": "spring.data.elasticsearch.repositories.enabled", "type": "java.lang.Boolean", - "deprecation": { - "replacement": "spring.flyway.out-of-order", - "level": "error" - } + "description": "Whether to enable Elasticsearch repositories.", + "defaultValue": true }, { - "name": "flyway.password", - "type": "java.lang.String", - "description": "Login password of the database to migrate.", - "deprecation": { - "replacement": "spring.flyway.password", - "level": "error" - } + "name": "spring.data.jdbc.repositories.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable JDBC repositories.", + "defaultValue": true }, { - "name": "flyway.placeholder-prefix", - "type": "java.lang.String", - "deprecation": { - "replacement": "spring.flyway.placeholder-prefix", - "level": "error" - } + "name": "spring.data.jpa.repositories.bootstrap-mode", + "type": "org.springframework.data.repository.config.BootstrapMode", + "description": "Bootstrap mode for JPA repositories.", + "defaultValue": "default" }, { - "name": "flyway.placeholder-replacement", + "name": "spring.data.jpa.repositories.enabled", "type": "java.lang.Boolean", - "deprecation": { - "replacement": "spring.flyway.placeholder-replacement", - "level": "error" - } + "description": "Whether to enable JPA repositories.", + "defaultValue": true }, { - "name": "flyway.placeholder-suffix", - "type": "java.lang.String", - "deprecation": { - "replacement": "spring.flyway.placeholder-suffix", - "level": "error" - } + "name": "spring.data.ldap.repositories.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable LDAP repositories.", + "defaultValue": true }, { - "name": "flyway.placeholders", - "type": "java.util.Map", - "deprecation": { - "replacement": "spring.flyway.placeholders", - "level": "error" - } + "name": "spring.data.mongodb.repositories.type", + "type": "org.springframework.boot.autoconfigure.data.RepositoryType", + "description": "Type of Mongo repositories to enable.", + "defaultValue": "auto" + }, + { + "name": "spring.data.mongodb.uri", + "defaultValue": "mongodb://localhost/test" }, { - "name": "flyway.schemas", - "type": "java.lang.String[]", - "deprecation": { - "replacement": "spring.flyway.schemas", - "level": "error" - } + "name": "spring.data.mongodb.uuid-representation", + "defaultValue": "java-legacy" }, { - "name": "flyway.sql-migration-prefix", - "type": "java.lang.String", + "name": "spring.data.neo4j.auto-index", + "description": "Auto index mode.", + "defaultValue": "none", "deprecation": { - "replacement": "spring.flyway.sql-migration-prefix", + "reason": "Automatic index creation is no longer supported.", "level": "error" } }, { - "name": "flyway.sql-migration-separator", - "type": "java.lang.String", + "name": "spring.data.neo4j.embedded.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable embedded mode if the embedded driver is available.", "deprecation": { - "replacement": "spring.flyway.sql-migration-separator", + "reason": "Embedded mode is no longer supported, please use Testcontainers instead.", "level": "error" } }, { - "name": "flyway.sql-migration-suffix", - "type": "java.lang.String", + "name": "spring.data.neo4j.open-in-view", + "type": "java.lang.Boolean", + "description": "Register OpenSessionInViewInterceptor that binds a Neo4j Session to the thread for the entire processing of the request.", "deprecation": { - "replacement": "spring.flyway.sql-migration-suffixes", "level": "error" } }, { - "name": "flyway.table", + "name": "spring.data.neo4j.password", "type": "java.lang.String", + "description": "Login password of the server.", "deprecation": { - "replacement": "spring.flyway.table", - "level": "error" + "replacement": "spring.neo4j.authentication.password", + "level": "warning" } }, { - "name": "flyway.target", - "type": "org.flywaydb.core.api.MigrationVersion", + "name": "spring.data.neo4j.repositories.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable Neo4j repositories.", + "defaultValue": true, "deprecation": { - "replacement": "spring.flyway.target", + "replacement": "spring.data.neo4j.repositories.type", "level": "error" } }, { - "name": "flyway.url", - "type": "java.lang.String", - "description": "JDBC url of the database to migrate. If not set, the primary configured data source\n is used.", - "deprecation": { - "replacement": "spring.flyway.url", - "level": "error" - } + "name": "spring.data.neo4j.repositories.type", + "type": "org.springframework.boot.autoconfigure.data.RepositoryType", + "description": "Type of Neo4j repositories to enable.", + "defaultValue": "auto" }, { - "name": "flyway.user", + "name": "spring.data.neo4j.uri", "type": "java.lang.String", - "description": "Login user of the database to migrate.", + "description": "URI used by the driver. Auto-detected by default.", "deprecation": { - "replacement": "spring.flyway.user", - "level": "error" + "replacement": "spring.neo4j.uri", + "level": "warning" } }, { - "name": "flyway.validate-on-migrate", + "name": "spring.data.neo4j.use-native-types", "type": "java.lang.Boolean", + "description": "Whether to use Neo4j native types wherever possible.", "deprecation": { - "replacement": "spring.flyway.validate-on-migrate", + "reason": "Native type support is now built-in.", "level": "error" } }, { - "name": "liquibase.change-log", + "name": "spring.data.neo4j.username", "type": "java.lang.String", - "description": "Change log configuration path.", - "defaultValue": "classpath:/db/changelog/db.changelog-master.yaml", + "description": "Login user of the server.", "deprecation": { - "replacement": "spring.liquibase.change-log", - "level": "error" + "replacement": "spring.neo4j.authentication.password", + "level": "warning" } }, { - "name": "liquibase.check-change-log-location", + "name": "spring.data.r2dbc.repositories.enabled", "type": "java.lang.Boolean", - "description": "Check the change log location exists.", - "defaultValue": true, - "deprecation": { - "reason": "Liquibase has its own check that checks if the change log location exists making this property redundant.", - "level": "error" - } + "description": "Whether to enable R2DBC repositories.", + "defaultValue": true }, { - "name": "liquibase.contexts", - "type": "java.lang.String", - "description": "Comma-separated list of runtime contexts to use.", + "name": "spring.data.redis.repositories.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable Redis repositories.", + "defaultValue": true + }, + { + "name": "spring.data.rest.detection-strategy", + "defaultValue": "default" + }, + { + "name": "spring.datasource.initialization-mode", + "defaultValue": "embedded" + }, + { + "name": "spring.datasource.jmx-enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable JMX support (if provided by the underlying pool).", + "defaultValue": false, "deprecation": { - "replacement": "spring.liquibase.contexts", - "level": "error" + "level": "error", + "replacement": "spring.datasource.tomcat.jmx-enabled" } }, { - "name": "liquibase.default-schema", - "type": "java.lang.String", - "description": "Default database schema.", + "name": "spring.elasticsearch.jest.connection-timeout", + "type": "java.time.Duration", + "description": "Connection timeout.", "deprecation": { - "replacement": "spring.liquibase.default-schema", "level": "error" } }, { - "name": "liquibase.drop-first", + "name": "spring.elasticsearch.jest.multi-threaded", "type": "java.lang.Boolean", - "description": "Drop the database schema first.", - "defaultValue": false, + "description": "Whether to enable connection requests from multiple execution threads.", "deprecation": { - "replacement": "spring.liquibase.drop-first", "level": "error" } }, { - "name": "liquibase.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable liquibase support.", - "defaultValue": true, + "name": "spring.elasticsearch.jest.password", + "type": "java.lang.String", + "description": "Login password.", "deprecation": { - "replacement": "spring.liquibase.enabled", "level": "error" } }, { - "name": "liquibase.labels", + "name": "spring.elasticsearch.jest.proxy.host", "type": "java.lang.String", - "description": "Comma-separated list of runtime labels to use.", + "description": "Proxy host the HTTP client should use.", "deprecation": { - "replacement": "spring.liquibase.labels", "level": "error" } }, { - "name": "liquibase.parameters", - "type": "java.util.Map", - "description": "Change log parameters.", + "name": "spring.elasticsearch.jest.proxy.port", + "type": "java.lang.Integer", + "description": "Proxy port the HTTP client should use.", "deprecation": { - "replacement": "spring.liquibase.parameters", "level": "error" } }, { - "name": "liquibase.password", - "type": "java.lang.String", - "description": "Login password of the database to migrate.", + "name": "spring.elasticsearch.jest.read-timeout", + "type": "java.time.Duration", + "description": "Read timeout.", "deprecation": { - "replacement": "spring.liquibase.password", "level": "error" } }, { - "name": "liquibase.rollback-file", - "type": "java.io.File", - "description": "File to which rollback SQL will be written when an update is performed.", + "name": "spring.elasticsearch.jest.uris", + "type": "java.util.List", + "description": "Comma-separated list of the Elasticsearch instances to use.", "deprecation": { - "replacement": "spring.liquibase.rollback-file", "level": "error" } }, { - "name": "liquibase.url", + "name": "spring.elasticsearch.jest.username", "type": "java.lang.String", - "description": "JDBC url of the database to migrate. If not set, the primary configured data source\n is used.", + "description": "Login username.", "deprecation": { - "replacement": "spring.liquibase.url", "level": "error" } }, { - "name": "liquibase.user", - "type": "java.lang.String", - "description": "Login user of the database to migrate.", + "name": "spring.elasticsearch.rest.uris", + "defaultValue": [ + "http://localhost:9200" + ] + }, + { + "name": "spring.flyway.dry-run-output", + "type": "java.io.OutputStream", "deprecation": { - "replacement": "spring.liquibase.user", - "level": "error" + "level": "error", + "reason": "Flyway pro edition only." } }, { - "name": "security.basic.authorize-mode", - "description": "Security authorize mode to apply.", - "defaultValue": "role", + "name": "spring.flyway.error-handlers", + "type": "org.flywaydb.core.api.errorhandler.ErrorHandler[]", "deprecation": { - "reason": "The security auto-configuration is no longer customizable. Provide your own WebSecurityConfigurer bean instead.", - "level": "error" + "level": "error", + "reason": "Flyway pro edition only." } }, { - "name": "security.basic.path", - "type": "java.lang.String[]", - "description": "Comma-separated list of paths to secure.", + "name": "spring.flyway.locations", + "sourceType": "org.springframework.boot.autoconfigure.flyway.FlywayProperties", "defaultValue": [ - "/**" - ], - "deprecation": { - "reason": "The security auto-configuration is no longer customizable. Provide your own WebSecurityConfigurer bean instead.", - "level": "error" - } + "classpath:db/migration" + ] + }, + { + "name": "spring.flyway.lock-retry-count", + "defaultValue": 50 }, { - "name": "security.basic.realm", + "name": "spring.flyway.sql-migration-suffix", "type": "java.lang.String", - "description": "HTTP basic realm name.", - "defaultValue": "Spring", "deprecation": { - "reason": "The security auto-configuration is no longer customizable. Provide your own WebSecurityConfigurer bean instead.", + "replacement": "spring.flyway.sql-migration-suffixes", "level": "error" } }, { - "name": "security.enable-csrf", - "type": "java.lang.Boolean", - "description": "Whether to enable Cross Site Request Forgery support.", - "defaultValue": false, - "deprecation": { - "reason": "The security auto-configuration is no longer customizable. Provide your own WebSecurityConfigurer bean instead.", - "level": "error" - } + "name": "spring.flyway.sql-migration-suffixes", + "sourceType": "org.springframework.boot.autoconfigure.flyway.FlywayProperties", + "defaultValue": [ + ".sql" + ] }, { - "name": "security.headers.cache", - "type": "java.lang.Boolean", - "description": "Whether to enable cache control HTTP headers.", - "defaultValue": true, + "name": "spring.flyway.undo-sql-migration-prefix", + "type": "java.lang.String", "deprecation": { - "reason": "The security auto-configuration is no longer customizable. Provide your own WebSecurityConfigurer bean instead.", - "level": "error" + "level": "error", + "reason": "Flyway pro edition only." } }, { - "name": "security.headers.content-security-policy", + "name": "spring.freemarker.prefix", + "defaultValue": "" + }, + { + "name": "spring.freemarker.suffix", + "defaultValue": ".ftlh" + }, + { + "name": "spring.git.properties", "type": "java.lang.String", - "description": "Value for content security policy header.", + "description": "Resource reference to a generated git info properties file.", "deprecation": { - "reason": "The security auto-configuration is no longer customizable. Provide your own WebSecurityConfigurer bean instead.", + "replacement": "spring.info.git.location", "level": "error" } }, { - "name": "security.headers.content-security-policy-mode", - "description": "Content security policy mode.", - "defaultValue": "default", - "deprecation": { - "reason": "The security auto-configuration is no longer customizable. Provide your own WebSecurityConfigurer bean instead.", - "level": "error" - } + "name": "spring.groovy.template.prefix", + "defaultValue": "" }, { - "name": "security.headers.content-type", - "type": "java.lang.Boolean", - "description": "Whether to enable \"X-Content-Type-Options\" header.", - "defaultValue": true, - "deprecation": { - "reason": "The security auto-configuration is no longer customizable. Provide your own WebSecurityConfigurer bean instead.", - "level": "error" - } + "name": "spring.groovy.template.suffix", + "defaultValue": ".tpl" }, { - "name": "security.headers.frame", - "type": "java.lang.Boolean", - "description": "Whether to enable \"X-Frame-Options\" header.", - "defaultValue": true, + "name": "spring.http.converters.preferred-json-mapper", + "type": "java.lang.String", + "description": "Preferred JSON mapper to use for HTTP message conversion. By default, auto-detected according to the environment.", "deprecation": { - "reason": "The security auto-configuration is no longer customizable. Provide your own WebSecurityConfigurer bean instead.", + "replacement": "spring.mvc.converters.preferred-json-mapper", "level": "error" } }, { - "name": "security.headers.hsts", - "description": "HTTP Strict Transport Security (HSTS) mode (none, domain, all).", - "defaultValue": "all", + "name": "spring.http.encoding.charset", + "type": "java.nio.charset.Charset", + "description": "Charset of HTTP requests and responses. Added to the Content-Type header if not set explicitly.", "deprecation": { - "reason": "The security auto-configuration is no longer customizable. Provide your own WebSecurityConfigurer bean instead.", + "replacement": "server.servlet.encoding.charset", "level": "error" } }, { - "name": "security.headers.xss", + "name": "spring.http.encoding.enabled", "type": "java.lang.Boolean", - "description": "Whether to enable cross site scripting (XSS) protection.", + "description": "Whether to enable http encoding support.", "defaultValue": true, "deprecation": { - "reason": "The security auto-configuration is no longer customizable. Provide your own WebSecurityConfigurer bean instead.", - "level": "error" - } - }, - { - "name": "security.ignored", - "type": "java.util.List", - "description": "Comma-separated list of paths to exclude from the default secured paths.", - "deprecation": { - "reason": "The security auto-configuration is no longer customizable. Provide your own WebSecurityConfigurer bean instead.", + "replacement": "server.servlet.encoding.enabled", "level": "error" } }, { - "name": "security.require-ssl", + "name": "spring.http.encoding.force", "type": "java.lang.Boolean", - "description": "Whether to enable secure channel for all requests.", + "description": "Whether to force the encoding to the configured charset on HTTP requests and responses.", "defaultValue": false, "deprecation": { - "reason": "The security auto-configuration is no longer customizable. Provide your own WebSecurityConfigurer bean instead.", + "replacement": "server.servlet.encoding.force", "level": "error" } }, { - "name": "security.sessions", - "type": "org.springframework.security.config.http.SessionCreationPolicy", - "description": "Session creation policy (always, never, if_required, stateless).", - "defaultValue": "stateless", + "name": "spring.http.encoding.force-request", + "type": "java.lang.Boolean", + "description": "Whether to force the encoding to the configured charset on HTTP requests. Defaults to true when force has not been specified.", + "defaultValue": true, "deprecation": { - "reason": "The security auto-configuration is no longer customizable. Provide your own WebSecurityConfigurer bean instead.", + "replacement": "server.servlet.encoding.force-request", "level": "error" } }, { - "name": "security.user.name", - "type": "java.lang.String", - "description": "Default user name.", - "defaultValue": "user", + "name": "spring.http.encoding.force-response", + "type": "java.lang.Boolean", + "description": "Whether to force the encoding to the configured charset on HTTP responses.", + "defaultValue": false, "deprecation": { - "replacement": "spring.security.user.name", + "replacement": "server.servlet.encoding.force-response", "level": "error" } }, { - "name": "security.user.password", - "type": "java.lang.String", - "description": "Password for the default user name.", + "name": "spring.http.encoding.mapping", + "type": "java.util.Map", + "description": "Locale in which to encode mapping.", "deprecation": { - "replacement": "spring.security.user.password", + "replacement": "server.servlet.encoding.mapping", "level": "error" } }, { - "name": "security.user.role", - "type": "java.util.List", - "description": "Granted roles for the default user name.", + "name": "spring.http.log-request-details", + "type": "java.lang.Boolean", + "description": "Whether logging of (potentially sensitive) request details at DEBUG and TRACE level is allowed.", + "defaultValue": false, "deprecation": { - "replacement": "spring.security.user.roles", + "replacement": "spring.mvc.log-request-details", "level": "error" } }, { - "name": "server.context-parameters", - "type": "java.util.Map", - "description": "ServletContext parameters.", - "deprecation": { - "replacement": "server.servlet.context-parameters", - "level": "error" - } + "name": "spring.info.build.location", + "defaultValue": "classpath:META-INF/build-info.properties" }, { - "name": "server.context-path", - "type": "java.lang.String", - "description": "Context path of the application.", - "deprecation": { - "replacement": "server.servlet.context-path", - "level": "error" - } + "name": "spring.info.git.location", + "defaultValue": "classpath:git.properties" }, { - "name" : "server.display-name", - "type" : "java.lang.String", - "description" : "Display name of the application.", - "defaultValue" : "application", - "deprecation" : { - "replacement" : "server.servlet.application-display-name", - "level" : "error" - } + "name": "spring.integration.jdbc.initialize-schema", + "defaultValue": "embedded" }, { - "name": "server.jsp-servlet.class-name", + "name": "spring.jackson.joda-date-time-format", "type": "java.lang.String", + "description": "Joda date time format string. If not configured, \"date-format\" is used as a fallback if it is configured with a format string.", "deprecation": { - "replacement": "server.servlet.jsp.class-name", "level": "error" } }, { - "name": "server.jsp-servlet.init-parameters", - "type": "java.util.Map", - "deprecation": { - "replacement": "server.servlet.jsp.init-parameters", - "level": "error" - } + "name": "spring.jersey.type", + "defaultValue": "servlet" }, { - "name": "server.jsp-servlet.registered", - "type": "java.lang.Boolean", - "deprecation": { - "replacement": "server.servlet.jsp.registered", - "level": "error" - } + "name": "spring.jmx.default-domain", + "type": "java.lang.String", + "description": "JMX domain name." }, { - "name": "server.max-http-post-size", - "type": "java.lang.Integer", - "description": "Maximum size in bytes of the HTTP post content.", - "defaultValue": 0, - "deprecation": { - "reason": "Use dedicated property for each container.", - "level": "error" - } + "name": "spring.jmx.enabled", + "type": "java.lang.Boolean", + "description": "Expose management beans to the JMX domain.", + "defaultValue": false }, { - "name": "server.servlet-path", + "name": "spring.jmx.server", "type": "java.lang.String", - "description": "Path of the main dispatcher servlet.", - "defaultValue": "/", - "deprecation": { - "replacement": "spring.mvc.servlet.path", - "level": "error" - } + "description": "MBeanServer bean name.", + "defaultValue": "mbeanServer" }, { - "name": "server.servlet.path", - "type": "java.lang.String", - "description": "Path of the main dispatcher servlet.", - "defaultValue": "/", - "deprecation": { - "replacement": "spring.mvc.servlet.path", - "level": "error" - } + "name": "spring.jmx.unique-names", + "type": "java.lang.Boolean", + "description": "Whether unique runtime object names should be ensured.", + "defaultValue": false }, { - "name" : "server.session.cookie.comment", - "type" : "java.lang.String", - "description" : "Comment for the session cookie.", - "deprecation" : { - "replacement" : "server.servlet.session.cookie.comment", - "level" : "error" - } - }, { - "name" : "server.session.cookie.domain", - "type" : "java.lang.String", - "description" : "Domain for the session cookie.", - "deprecation" : { - "replacement" : "server.servlet.session.cookie.domain", - "level" : "error" - } - }, { - "name" : "server.session.cookie.http-only", - "type" : "java.lang.Boolean", - "description" : "\"HttpOnly\" flag for the session cookie.", - "deprecation" : { - "replacement" : "server.servlet.session.cookie.http-only", - "level" : "error" - } - }, { - "name" : "server.session.cookie.max-age", - "type" : "java.time.Duration", - "description" : "Maximum age of the session cookie.", - "deprecation" : { - "replacement" : "server.servlet.session.cookie.max-age", - "level" : "error" - } - }, { - "name" : "server.session.cookie.name", - "type" : "java.lang.String", - "description" : "Session cookie name.", - "deprecation" : { - "replacement" : "server.servlet.session.cookie.name", - "level" : "error" - } - }, { - "name" : "server.session.cookie.path", - "type" : "java.lang.String", - "description" : "Path of the session cookie.", - "deprecation" : { - "replacement" : "server.servlet.session.cookie.path", - "level" : "error" - } - }, { - "name" : "server.session.cookie.secure", - "type" : "java.lang.Boolean", - "description" : "\"Secure\" flag for the session cookie.", - "deprecation" : { - "replacement" : "server.servlet.session.cookie.secure", - "level" : "error" - } - }, { - "name" : "server.session.persistent", - "type" : "java.lang.Boolean", - "description" : "Whether to persist session data between restarts.", - "defaultValue" : false, - "deprecation" : { - "replacement" : "server.servlet.session.persistent", - "level" : "error" - } - }, { - "name" : "server.session.store-dir", - "type" : "java.io.File", - "description" : "Directory used to store session data.", - "deprecation" : { - "replacement" : "server.servlet.session.store-dir", - "level" : "error" - } - }, { - "name" : "server.session.timeout", - "type" : "java.time.Duration", - "description" : "Session timeout. If a duration suffix is not specified, seconds will be used.", - "deprecation" : { - "replacement" : "server.servlet.session.timeout", - "level" : "error" - } - }, { - "name" : "server.session.tracking-modes", - "type" : "java.util.Set", - "description" : "Session tracking modes (one or more of the following: \"cookie\", \"url\", \"ssl\").", - "deprecation" : { - "replacement" : "server.servlet.session.tracking-modes", - "level" : "error" - } + "name": "spring.jpa.open-in-view", + "defaultValue": true }, { - "name": "server.undertow.buffers-per-region", - "type": "java.lang.Integer", - "description": "Number of buffer per region.", + "name": "spring.jta.bitronix.properties.allow-multiple-lrc", + "type": "java.lang.Boolean", "deprecation": { "level": "error" } }, { - "name": "spring.activemq.pool.create-connection-on-startup", + "name": "spring.jta.bitronix.properties.asynchronous2-pc", "type": "java.lang.Boolean", - "description": "Whether to create a connection on startup. Can be used to warm up the pool on startup.", - "defaultValue": true, "deprecation": { "level": "error" } }, { - "name": "spring.activemq.pool.expiry-timeout", - "type": "java.time.Duration", - "description": "Connection expiration timeout.", - "defaultValue": "0ms", + "name": "spring.jta.bitronix.properties.background-recovery-interval", + "type": "java.lang.Integer", "deprecation": { "level": "error" } }, { - "name": "spring.activemq.pool.reconnect-on-exception", - "type": "java.lang.Boolean", - "description": "Reset the connection when a \"JMSException\" occurs.", - "defaultValue": true, + "name": "spring.jta.bitronix.properties.background-recovery-interval-seconds", + "type": "java.lang.Integer", "deprecation": { "level": "error" } }, { - "name": "spring.batch.initializer.enabled", + "name": "spring.jta.bitronix.properties.current-node-only-recovery", "type": "java.lang.Boolean", - "description": "Create the required batch tables on startup if necessary. Enabled automatically\n if no custom table prefix is set or if a custom schema is configured.", - "deprecation": { - "replacement": "spring.batch.initialize-schema", - "level": "error" - } - }, - { - "name": "spring.couchbase.env.endpoints.query", - "type": "java.lang.Integer", - "description": "Number of sockets per node against the query (N1QL) service.", "deprecation": { "level": "error" } }, { - "name": "spring.couchbase.env.endpoints.view", - "type": "java.lang.Integer", - "description": "Number of sockets per node against the view service.", + "name": "spring.jta.bitronix.properties.debug-zero-resource-transaction", + "type": "java.lang.Boolean", "deprecation": { "level": "error" } }, { - "name": "spring.data.cassandra.connect-timeout-millis", + "name": "spring.jta.bitronix.properties.default-transaction-timeout", "type": "java.lang.Integer", - "description": "Socket option: connection time out.", "deprecation": { - "replacement": "spring.data.cassandra.connect-timeout", "level": "error" } }, { - "name": "spring.data.cassandra.read-timeout-millis", - "type": "java.lang.Integer", - "description": "Socket option: read time out.", + "name": "spring.jta.bitronix.properties.disable-jmx", + "type": "java.lang.Boolean", "deprecation": { - "replacement": "spring.data.cassandra.read-timeout", "level": "error" } }, { - "name": "spring.data.neo4j.compiler", + "name": "spring.jta.bitronix.properties.exception-analyzer", "type": "java.lang.String", - "description": "Compiler to use.", "deprecation": { - "reason": "Not supported anymore as of Neo4j 3.", "level": "error" } }, { - "name": "spring.datasource.jmx-enabled", + "name": "spring.jta.bitronix.properties.filter-log-status", "type": "java.lang.Boolean", - "description": "Whether to enable JMX support (if provided by the underlying pool).", - "defaultValue": false, "deprecation": { - "level": "error", - "replacement": "spring.datasource.tomcat.jmx-enabled" + "level": "error" } }, { - "name": "spring.datasource.initialize", - "defaultValue": true, + "name": "spring.jta.bitronix.properties.force-batching-enabled", + "type": "java.lang.Boolean", "deprecation": { - "replacement": "spring.datasource.initialization-mode", "level": "error" } }, { - "name": "spring.flyway.dry-run-output", - "type": "java.io.OutputStream", + "name": "spring.jta.bitronix.properties.forced-write-enabled", + "type": "java.lang.Boolean", "deprecation": { - "level": "error", - "reason": "Flyway pro edition only." + "level": "error" } }, { - "name": "spring.flyway.error-handlers", - "type": "org.flywaydb.core.api.errorhandler.ErrorHandler[]", + "name": "spring.jta.bitronix.properties.graceful-shutdown-interval", + "type": "java.lang.String", "deprecation": { - "level": "error", - "reason": "Flyway pro edition only." + "level": "error" } }, { - "name": "spring.flyway.sql-migration-suffix", + "name": "spring.jta.bitronix.properties.jndi-transaction-synchronization-registry-name", "type": "java.lang.String", "deprecation": { - "replacement": "spring.flyway.sql-migration-suffixes", "level": "error" } }, { - "name": "spring.flyway.undo-sql-migration-prefix", + "name": "spring.jta.bitronix.properties.jndi-user-transaction-name", "type": "java.lang.String", "deprecation": { - "level": "error", - "reason": "Flyway pro edition only." + "level": "error" } }, { - "name": "spring.git.properties", + "name": "spring.jta.bitronix.properties.journal", "type": "java.lang.String", - "description": "Resource reference to a generated git info properties file.", "deprecation": { - "replacement": "spring.info.git.location", "level": "error" } }, { - "name": "spring.http.multipart.enabled", - "type": "java.lang.Boolean", - "description": "Whether to enable support of multipart uploads.", - "defaultValue": true, + "name": "spring.jta.bitronix.properties.log-part1-filename", + "type": "java.lang.String", "deprecation": { - "replacement": "spring.servlet.multipart.enabled", "level": "error" } }, { - "name": "spring.http.multipart.file-size-threshold", + "name": "spring.jta.bitronix.properties.log-part2-filename", "type": "java.lang.String", - "description": "Threshold after which files will be written to disk. Values can use the suffixes\n \"MB\" or \"KB\" to indicate megabytes or kilobytes respectively.", - "defaultValue": "0", "deprecation": { - "replacement": "spring.servlet.multipart.file-size-threshold", "level": "error" } }, { - "name": "spring.http.multipart.location", - "type": "java.lang.String", - "description": "Intermediate location of uploaded files.", + "name": "spring.jta.bitronix.properties.max-log-size-in-mb", + "type": "java.lang.Integer", "deprecation": { - "replacement": "spring.servlet.multipart.location", "level": "error" } }, { - "name": "spring.http.multipart.max-file-size", + "name": "spring.jta.bitronix.properties.resource-configuration-filename", "type": "java.lang.String", - "description": "Max file size. Values can use the suffixes \"MB\" or \"KB\" to indicate megabytes or\n kilobytes respectively.", - "defaultValue": "1MB", "deprecation": { - "replacement": "spring.servlet.multipart.max-file-size", "level": "error" } }, { - "name": "spring.http.multipart.max-request-size", + "name": "spring.jta.bitronix.properties.server-id", "type": "java.lang.String", - "description": "Max request size. Values can use the suffixes \"MB\" or \"KB\" to indicate megabytes or\n kilobytes respectively.", - "defaultValue": "10MB", "deprecation": { - "replacement": "spring.servlet.multipart.max-request-size", "level": "error" } }, { - "name": "spring.http.multipart.resolve-lazily", + "name": "spring.jta.bitronix.properties.skip-corrupted-logs", "type": "java.lang.Boolean", - "description": "Whether to resolve the multipart request lazily at the time of file or parameter\n access.", - "defaultValue": false, "deprecation": { - "replacement": "spring.servlet.multipart.resolve-lazily", "level": "error" } }, { - "name": "spring.jpa.hibernate.naming.strategy", - "type": "java.lang.String", - "description": "Hibernate 4 naming strategy fully qualified name. Not supported with Hibernate\n 5.", + "name": "spring.jta.bitronix.properties.warn-about-zero-resource-transaction", + "type": "java.lang.Boolean", "deprecation": { - "reason": "Auto-configuration for Hibernate 4 is no longer provided.", "level": "error" } }, { - "name" : "spring.jta.narayana.default-timeout", - "type" : "java.time.Duration", - "description" : "Transaction timeout. If a duration suffix is not specified, seconds will be used.", - "defaultValue" : "60s", - "deprecation" : { + "name": "spring.jta.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable JTA support.", + "defaultValue": true + }, + { + "name": "spring.jta.narayana.default-timeout", + "type": "java.time.Duration", + "description": "Transaction timeout. If a duration suffix is not specified, seconds will be used.", + "defaultValue": "60s", + "deprecation": { "level": "error", "reason": "Narayana support has moved to third party starter." } - }, { - "name" : "spring.jta.narayana.expiry-scanners", - "type" : "java.util.List", - "description" : "Comma-separated list of expiry scanners.", - "defaultValue" : [ "com.arjuna.ats.internal.arjuna.recovery.ExpiredTransactionStatusManagerScanner" ], - "deprecation" : { + }, + { + "name": "spring.jta.narayana.expiry-scanners", + "type": "java.util.List", + "description": "Comma-separated list of expiry scanners.", + "defaultValue": [ + "com.arjuna.ats.internal.arjuna.recovery.ExpiredTransactionStatusManagerScanner" + ], + "deprecation": { "level": "error", "reason": "Narayana support has moved to third party starter." } - }, { - "name" : "spring.jta.narayana.log-dir", - "type" : "java.lang.String", - "description" : "Transaction object store directory.", - "deprecation" : { + }, + { + "name": "spring.jta.narayana.log-dir", + "type": "java.lang.String", + "description": "Transaction object store directory.", + "deprecation": { "level": "error", "reason": "Narayana support has moved to third party starter." } - }, { - "name" : "spring.jta.narayana.one-phase-commit", - "type" : "java.lang.Boolean", - "description" : "Whether to enable one phase commit optimization.", - "defaultValue" : true, - "deprecation" : { + }, + { + "name": "spring.jta.narayana.one-phase-commit", + "type": "java.lang.Boolean", + "description": "Whether to enable one phase commit optimization.", + "defaultValue": true, + "deprecation": { "level": "error", "reason": "Narayana support has moved to third party starter." } - }, { - "name" : "spring.jta.narayana.periodic-recovery-period", - "type" : "java.time.Duration", - "description" : "Interval in which periodic recovery scans are performed. If a duration suffix is not specified, seconds will be used.", - "defaultValue" : "120s", - "deprecation" : { + }, + { + "name": "spring.jta.narayana.periodic-recovery-period", + "type": "java.time.Duration", + "description": "Interval in which periodic recovery scans are performed. If a duration suffix is not specified, seconds will be used.", + "defaultValue": "120s", + "deprecation": { "level": "error", "reason": "Narayana support has moved to third party starter." } - }, { - "name" : "spring.jta.narayana.recovery-backoff-period", - "type" : "java.time.Duration", - "description" : "Back off period between first and second phases of the recovery scan. If a duration suffix is not specified, seconds will be used.", - "defaultValue" : "10s", - "deprecation" : { + }, + { + "name": "spring.jta.narayana.recovery-backoff-period", + "type": "java.time.Duration", + "description": "Back off period between first and second phases of the recovery scan. If a duration suffix is not specified, seconds will be used.", + "defaultValue": "10s", + "deprecation": { "level": "error", "reason": "Narayana support has moved to third party starter." } @@ -1856,6 +1398,10 @@ "level": "error" } }, + { + "name": "spring.kafka.consumer.isolation-level", + "defaultValue": "read-uncommitted" + }, { "name": "spring.kafka.consumer.ssl.keystore-location", "type": "org.springframework.core.io.Resource", @@ -1892,6 +1438,14 @@ "level": "error" } }, + { + "name": "spring.kafka.jaas.control-flag", + "defaultValue": "required" + }, + { + "name": "spring.kafka.listener.type", + "defaultValue": "single" + }, { "name": "spring.kafka.producer.ssl.keystore-location", "type": "org.springframework.core.io.Resource", @@ -1964,6 +1518,14 @@ "level": "error" } }, + { + "name": "spring.kafka.streams.cache-max-bytes-buffering", + "type": "java.lang.Integer", + "deprecation": { + "replacement": "spring.kafka.streams.cache-max-size-buffering", + "level": "error" + } + }, { "name": "spring.liquibase.check-change-log-location", "type": "java.lang.Boolean", @@ -1975,133 +1537,319 @@ } }, { - "name": "spring.messages.cache-seconds", - "type": "java.lang.Integer", - "description": "Loaded resource bundle files cache expiration, in seconds. When set to -1, bundles are cached forever.", - "deprecation": { - "replacement": "spring.messages.cache-duration", - "level": "error" - } + "name": "spring.mail.test-connection", + "description": "Whether to test that the mail server is available on startup.", + "sourceType": "org.springframework.boot.autoconfigure.mail.MailProperties", + "type": "java.lang.Boolean", + "defaultValue": false }, { - "name": "spring.redis.pool.max-active", - "type": "java.lang.Integer", - "description": "Max number of connections that can be allocated by the pool at a given time.\n Use a negative value for no limit.", - "defaultValue": 8, + "name": "spring.mongodb.embedded.features", + "defaultValue": [ + "sync_delay" + ] + }, + { + "name": "spring.mustache.prefix", + "defaultValue": "classpath:/templates/" + }, + { + "name": "spring.mustache.suffix", + "defaultValue": ".mustache" + }, + { + "name": "spring.mvc.converters.preferred-json-mapper", + "type": "java.lang.String", + "description": "Preferred JSON mapper to use for HTTP message conversion. By default, auto-detected according to the environment." + }, + { + "name": "spring.mvc.favicon.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable resolution of favicon.ico.", "deprecation": { - "replacement": "spring.redis.jedis.pool.max-idle", "level": "error" } }, { - "name": "spring.redis.pool.max-idle", - "type": "java.lang.Integer", - "description": "Max number of \"idle\" connections in the pool. Use a negative value to indicate\n an unlimited number of idle connections.", - "defaultValue": 8, + "name": "spring.mvc.formcontent.filter.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable Spring's FormContentFilter.", + "defaultValue": true + }, + { + "name": "spring.mvc.formcontent.putfilter.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable Spring's HttpPutFormContentFilter.", + "defaultValue": true, "deprecation": { - "replacement": "spring.redis.jedis.pool.max-idle", + "replacement": "spring.mvc.formcontent.filter.enabled", "level": "error" } }, { - "name": "spring.redis.pool.max-wait", + "name": "spring.mvc.hiddenmethod.filter.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable Spring's HiddenHttpMethodFilter.", + "defaultValue": false + }, + { + "name": "spring.mvc.locale-resolver", + "defaultValue": "accept-header" + }, + { + "name": "spring.mvc.pathmatch.matching-strategy", + "defaultValue": "ant-path-matcher" + }, + { + "name": "spring.neo4j.security.trust-strategy", + "defaultValue": "trust-system-ca-signed-certificates" + }, + { + "name": "spring.neo4j.uri", + "defaultValue": "bolt://localhost:7687" + }, + { + "name": "spring.netty.leak-detection", + "defaultValue": "simple" + }, + { + "name": "spring.quartz.jdbc.comment-prefix", + "defaultValue": [ + "#", + "--" + ] + }, + { + "name": "spring.quartz.jdbc.initialize-schema", + "defaultValue": "embedded" + }, + { + "name": "spring.quartz.job-store-type", + "defaultValue": "memory" + }, + { + "name": "spring.quartz.scheduler-name", + "defaultValue": "quartzScheduler" + }, + { + "name": "spring.r2dbc.pool.enabled", + "type": "java.lang.Boolean", + "description": "Whether pooling is enabled. Enabled automatically if \"r2dbc-pool\" is on the classpath." + }, + { + "name": "spring.r2dbc.pool.validation-depth", + "defaultValue": "local" + }, + { + "name": "spring.rabbitmq.address-shuffle-mode", + "defaultValue": "none" + }, + { + "name": "spring.rabbitmq.cache.connection.mode", + "defaultValue": "channel" + }, + { + "name": "spring.rabbitmq.dynamic", + "type": "java.lang.Boolean", + "description": "Whether to create an AmqpAdmin bean.", + "defaultValue": true + }, + { + "name": "spring.rabbitmq.listener.simple.transaction-size", "type": "java.lang.Integer", - "description": "Maximum amount of time (in milliseconds) a connection allocation should block\n before throwing an exception when the pool is exhausted. Use a negative value\n to block indefinitely.", - "defaultValue": -1, "deprecation": { - "replacement": "spring.redis.jedis.pool.max-wait", "level": "error" } }, { - "name": "spring.redis.pool.min-idle", - "type": "java.lang.Integer", - "description": "Target for the minimum number of idle connections to maintain in the pool. This\n setting only has an effect if it is positive.", - "defaultValue": 0, + "name": "spring.rabbitmq.listener.type", + "defaultValue": "simple" + }, + { + "name": "spring.rabbitmq.publisher-confirms", + "type": "java.lang.Boolean", "deprecation": { - "replacement": "spring.redis.jedis.pool.min-idle", "level": "error" } }, { - "name": "spring.resources.cache-period", - "type": "java.lang.Integer", - "description": "Cache period for the resources served by the resource handler. If a duration suffix is not specified, seconds will be used.", + "name": "spring.rabbitmq.template.queue", + "type": "java.lang.String", "deprecation": { - "replacement": "spring.resources.cache.period", + "replacement": "spring.rabbitmq.template.default-receive-queue", "level": "error" } }, { - "name": "spring.sendgrid.password", - "type": "java.lang.String", - "description": "SendGrid password.", + "name": "spring.reactor.stacktrace-mode.enabled", + "description": "Whether Reactor should collect stacktrace information at runtime.", + "defaultValue": false, "deprecation": { - "reason": "The use of a username and password is no longer supported (Use spring.sendgrid.api-key instead).", - "level": "error" + "replacement": "spring.reactor.debug-agent.enabled" } }, { - "name": "spring.sendgrid.username", - "type": "java.lang.String", - "description": "SendGrid username. Alternative to api key.", + "name": "spring.resources.chain.gzipped", + "type": "java.lang.Boolean", + "description": "Whether to enable resolution of already gzipped resources. Checks for a resource name variant with the \"*.gz\" extension.", "deprecation": { - "reason": "The use of a username and password is no longer supported (Use spring.sendgrid.api-key instead).", + "replacement": "spring.resources.chain.compressed", "level": "error" } }, { - "name": "spring.session.jdbc.initializer.enabled", + "name": "spring.rsocket.server.transport", + "defaultValue": "tcp" + }, + { + "name": "spring.security.filter.dispatcher-types", + "defaultValue": [ + "async", + "error", + "request" + ] + }, + { + "name": "spring.security.filter.order", + "defaultValue": -100 + }, + { + "name": "spring.session.hazelcast.flush-mode", + "defaultValue": "on-save" + }, + { + "name": "spring.session.hazelcast.save-mode", + "defaultValue": "on-set-attribute" + }, + { + "name": "spring.session.jdbc.flush-mode", + "defaultValue": "on-save" + }, + { + "name": "spring.session.jdbc.initialize-schema", + "defaultValue": "embedded" + }, + { + "name": "spring.session.jdbc.save-mode", + "defaultValue": "on-set-attribute" + }, + { + "name": "spring.session.redis.configure-action", + "defaultValue": "notify-keyspace-events" + }, + { + "name": "spring.session.redis.flush-mode", + "defaultValue": "on-save" + }, + { + "name": "spring.session.redis.save-mode", + "defaultValue": "on-set-attribute" + }, + { + "name": "spring.session.servlet.filter-dispatcher-types", + "defaultValue": [ + "async", + "error", + "request" + ] + }, + { + "name": "spring.sql.init.enabled", "type": "java.lang.Boolean", - "description": "Create the required session tables on startup if necessary. Enabled\n automatically if the default table name is set or a custom schema is\n configured.", - "deprecation": { - "replacement": "spring.session.jdbc.initialize-schema", - "level": "error" - } - }, - { - "name": "spring.session.mongo.collection-name", - "type": "java.lang.String", - "description": "Collection name used to store sessions.", - "defaultValue": "sessions", + "description": "Whether basic script-based initialization of an SQL database is enabled.", + "defaultValue": true, "deprecation": { - "replacement": "spring.session.mongodb.collection-name", - "level": "error" + "replacement": "spring.sql.init.mode", + "level": "warning" } }, { - "name": "spring.thymeleaf.content-type", - "type": "org.springframework.util.MimeType", - "description": "Content-Type value.", - "defaultValue": "text/html", - "deprecation": { - "replacement": "spring.thymeleaf.servlet.content-type", - "level": "error" - } + "name": "spring.sql.init.mode", + "defaultValue": "embedded" }, { "name": "spring.thymeleaf.prefix", "defaultValue": "classpath:/templates/" }, + { + "name": "spring.thymeleaf.reactive.media-types", + "defaultValue": [ + "text/html", + "application/xhtml+xml", + "application/xml", + "text/xml", + "application/rss+xml", + "application/atom+xml", + "application/javascript", + "application/ecmascript", + "text/javascript", + "text/ecmascript", + "application/json", + "text/css", + "text/plain", + "text/event-stream" + ] + }, { "name": "spring.thymeleaf.suffix", "defaultValue": ".html" }, { - "name": "spring.webservices.wsdl-locations", - "type": "java.util.List", - "description": "Comma-separated list of locations of WSDLs and accompanying XSDs to be exposed as beans." + "name": "spring.web.locale-resolver", + "defaultValue": "accept-header" }, { "name": "spring.webflux.hiddenmethod.filter.enabled", "type": "java.lang.Boolean", "description": "Whether to enable Spring's HiddenHttpMethodFilter.", "defaultValue": false + }, + { + "name": "spring.webflux.session.cookie.same-site", + "defaultValue": "lax" + }, + { + "name": "spring.webservices.wsdl-locations", + "type": "java.util.List", + "description": "Comma-separated list of locations of WSDLs and accompanying XSDs to be exposed as beans." } ], "hints": [ { - "name": "server.tomcat.relaxed-query-chars", + "name": "server.servlet.jsp.class-name", + "providers": [ + { + "name": "class-reference", + "parameters": { + "target": "javax.servlet.http.HttpServlet" + } + } + ] + }, + { + "name": "server.tomcat.accesslog.encoding", + "providers": [ + { + "name": "handle-as", + "parameters": { + "target": "java.nio.charset.Charset" + } + } + ] + }, + { + "name": "server.tomcat.accesslog.locale", + "providers": [ + { + "name": "handle-as", + "parameters": { + "target": "java.util.Locale" + } + } + ] + }, + { + "name": "server.tomcat.relaxed-path-chars", "values": [ { "value": "<" @@ -2136,7 +1884,7 @@ ] }, { - "name": "server.tomcat.relaxed-path-chars", + "name": "server.tomcat.relaxed-query-chars", "values": [ { "value": "<" @@ -2171,122 +1919,136 @@ ] }, { - "name": "server.tomcat.accesslog.encoding", + "name": "spring.cache.jcache.provider", "providers": [ { - "name": "handle-as", + "name": "class-reference", "parameters": { - "target": "java.nio.charset.Charset" + "target": "javax.cache.spi.CachingProvider" } } ] }, { - "name": "server.tomcat.accesslog.locale", + "name": "spring.data.cassandra.schema-action", "providers": [ { "name": "handle-as", "parameters": { - "target": "java.util.Locale" + "target": "org.springframework.data.cassandra.config.SchemaAction" } } ] }, { - "name": "spring.liquibase.change-log", + "name": "spring.data.mongodb.field-naming-strategy", "providers": [ { - "name": "handle-as", + "name": "class-reference", "parameters": { - "target": "org.springframework.core.io.Resource" + "target": "org.springframework.data.mapping.model.FieldNamingStrategy" } } ] }, { - "name": "server.servlet.jsp.class-name", + "name": "spring.datasource.data", "providers": [ { - "name": "class-reference", + "name": "handle-as", "parameters": { - "target": "javax.servlet.http.HttpServlet" + "target": "java.util.List" } } ] }, { - "name": "spring.cache.jcache.provider", + "name": "spring.datasource.driver-class-name", "providers": [ { "name": "class-reference", "parameters": { - "target": "javax.cache.spi.CachingProvider" + "target": "java.sql.Driver" } } ] }, { - "name": "spring.data.cassandra.schema-action", + "name": "spring.datasource.schema", "providers": [ { "name": "handle-as", "parameters": { - "target": "org.springframework.data.cassandra.config.SchemaAction" + "target": "java.util.List" } } ] }, { - "name": "spring.data.mongodb.field-naming-strategy", + "name": "spring.datasource.xa.data-source-class-name", "providers": [ { "name": "class-reference", "parameters": { - "target": "org.springframework.data.mapping.model.FieldNamingStrategy" + "target": "javax.sql.XADataSource" } } ] }, { - "name": "spring.datasource.data", + "name": "spring.jmx.server", "providers": [ { - "name": "handle-as", + "name": "spring-bean-reference", "parameters": { - "target": "java.util.List" + "target": "javax.management.MBeanServer" } } ] }, { - "name": "spring.datasource.driver-class-name", - "providers": [ + "name": "spring.jpa.hibernate.ddl-auto", + "values": [ { - "name": "class-reference", - "parameters": { - "target": "java.sql.Driver" - } + "value": "none", + "description": "Disable DDL handling." + }, + { + "value": "validate", + "description": "Validate the schema, make no changes to the database." + }, + { + "value": "update", + "description": "Update the schema if necessary." + }, + { + "value": "create", + "description": "Create the schema and destroy previous data." + }, + { + "value": "create-drop", + "description": "Create and then destroy the schema at the end of the session." } ] }, { - "name": "spring.datasource.schema", + "name": "spring.jpa.hibernate.naming.implicit-strategy", "providers": [ { - "name": "handle-as", + "name": "class-reference", "parameters": { - "target": "java.util.List" + "target": "org.hibernate.boot.model.naming.ImplicitNamingStrategy" } } ] }, { - "name": "spring.datasource.xa.data-source-class-name", + "name": "spring.jpa.hibernate.naming.physical-strategy", "providers": [ { "name": "class-reference", "parameters": { - "target": "javax.sql.XADataSource" + "target": "org.hibernate.boot.model.naming.PhysicalNamingStrategy" } } ] @@ -2362,7 +2124,18 @@ ] }, { - "name": "spring.http.converters.preferred-json-mapper", + "name": "spring.liquibase.change-log", + "providers": [ + { + "name": "handle-as", + "parameters": { + "target": "org.springframework.core.io.Resource" + } + } + ] + }, + { + "name": "spring.mvc.converters.preferred-json-mapper", "values": [ { "value": "gson" @@ -2381,63 +2154,150 @@ ] }, { - "name": "spring.jmx.server", + "name": "spring.mvc.format.date", + "values": [ + { + "value": "dd/MM/yyyy", + "description": "Example date format. Any format supported by DateTimeFormatter.parse can be used." + }, + { + "value": "iso", + "description": "ISO-8601 extended local date format." + } + ], "providers": [ { - "name": "spring-bean-reference", - "parameters": { - "target": "javax.management.MBeanServer" - } + "name": "any" } ] }, { - "name": "spring.jpa.hibernate.ddl-auto", + "name": "spring.mvc.format.date-time", "values": [ { - "value": "none", - "description": "Disable DDL handling." + "value": "yyyy-MM-dd HH:mm:ss", + "description": "Example date-time format. Any format supported by DateTimeFormatter.parse can be used." }, { - "value": "validate", - "description": "Validate the schema, make no changes to the database." + "value": "iso", + "description": "ISO-8601 extended local date-time format." }, { - "value": "update", - "description": "Update the schema if necessary." + "value": "iso-offset", + "description": "ISO offset date-time format." + } + ], + "providers": [ + { + "name": "any" + } + ] + }, + { + "name": "spring.mvc.format.time", + "values": [ + { + "value": "HH:mm:ss", + "description": "Example time format. Any format supported by DateTimeFormatter.parse can be used." }, { - "value": "create", - "description": "Create the schema and destroy previous data." + "value": "iso", + "description": "ISO-8601 extended local time format." }, { - "value": "create-drop", - "description": "Create and then destroy the schema at the end of the session." + "value": "iso-offset", + "description": "ISO offset time format." + } + ], + "providers": [ + { + "name": "any" } ] }, { - "name": "spring.jpa.hibernate.naming.implicit-strategy", + "name": "spring.sql.init.data-locations", "providers": [ { - "name": "class-reference", + "name": "handle-as", "parameters": { - "target": "org.hibernate.boot.model.naming.ImplicitNamingStrategy" + "target": "java.util.List" } } ] }, { - "name": "spring.jpa.hibernate.naming.physical-strategy", + "name": "spring.sql.init.schema-locations", "providers": [ { - "name": "class-reference", + "name": "handle-as", "parameters": { - "target": "org.hibernate.boot.model.naming.PhysicalNamingStrategy" + "target": "java.util.List" } } ] + }, + { + "name": "spring.webflux.format.date", + "values": [ + { + "value": "dd/MM/yyyy", + "description": "Example date format. Any format supported by DateTimeFormatter.parse can be used." + }, + { + "value": "iso", + "description": "ISO-8601 extended local date format." + } + ], + "providers": [ + { + "name": "any" + } + ] + }, + { + "name": "spring.webflux.format.date-time", + "values": [ + { + "value": "yyyy-MM-dd HH:mm:ss", + "description": "Example date-time format. Any format supported by DateTimeFormatter.parse can be used." + }, + { + "value": "iso", + "description": "ISO-8601 extended local date-time format." + }, + { + "value": "iso-offset", + "description": "ISO offset date-time format." + } + ], + "providers": [ + { + "name": "any" + } + ] + }, + { + "name": "spring.webflux.format.time", + "values": [ + { + "value": "HH:mm:ss", + "description": "Example time format. Any format supported by DateTimeFormatter.parse can be used." + }, + { + "value": "iso", + "description": "ISO-8601 extended local time format." + }, + { + "value": "iso-offset", + "description": "ISO offset time format." + } + ], + "providers": [ + { + "name": "any" + } + ] } ] } - diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index c7392ff2b068..c6a64bcc1f1d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -7,6 +7,10 @@ org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingL org.springframework.context.ApplicationListener=\ org.springframework.boot.autoconfigure.BackgroundPreinitializer +# Environment Post Processors +org.springframework.boot.env.EnvironmentPostProcessor=\ +org.springframework.boot.autoconfigure.integration.IntegrationPropertiesEnvironmentPostProcessor + # Auto Configuration Import Listeners org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\ org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener @@ -25,8 +29,8 @@ org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\ org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\ org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\ org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\ -org.springframework.boot.autoconfigure.cloud.CloudServiceConnectorsAutoConfiguration,\ org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\ +org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration,\ org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\ org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\ org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\ @@ -39,11 +43,10 @@ org.springframework.boot.autoconfigure.data.couchbase.CouchbaseDataAutoConfigura org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveDataAutoConfiguration,\ org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveRepositoriesAutoConfiguration,\ org.springframework.boot.autoconfigure.data.couchbase.CouchbaseRepositoriesAutoConfiguration,\ -org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchAutoConfiguration,\ org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration,\ org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration,\ org.springframework.boot.autoconfigure.data.elasticsearch.ReactiveElasticsearchRepositoriesAutoConfiguration,\ -org.springframework.boot.autoconfigure.data.elasticsearch.ReactiveRestClientAutoConfiguration,\ +org.springframework.boot.autoconfigure.data.elasticsearch.ReactiveElasticsearchRestClientAutoConfiguration,\ org.springframework.boot.autoconfigure.data.jdbc.JdbcRepositoriesAutoConfiguration,\ org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,\ org.springframework.boot.autoconfigure.data.ldap.LdapRepositoriesAutoConfiguration,\ @@ -52,17 +55,20 @@ org.springframework.boot.autoconfigure.data.mongo.MongoReactiveDataAutoConfigura org.springframework.boot.autoconfigure.data.mongo.MongoReactiveRepositoriesAutoConfiguration,\ org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration,\ org.springframework.boot.autoconfigure.data.neo4j.Neo4jDataAutoConfiguration,\ +org.springframework.boot.autoconfigure.data.neo4j.Neo4jReactiveDataAutoConfiguration,\ +org.springframework.boot.autoconfigure.data.neo4j.Neo4jReactiveRepositoriesAutoConfiguration,\ org.springframework.boot.autoconfigure.data.neo4j.Neo4jRepositoriesAutoConfiguration,\ -org.springframework.boot.autoconfigure.data.solr.SolrRepositoriesAutoConfiguration,\ +org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration,\ +org.springframework.boot.autoconfigure.data.r2dbc.R2dbcRepositoriesAutoConfiguration,\ org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\ org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\ org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\ org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration,\ org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration,\ -org.springframework.boot.autoconfigure.elasticsearch.jest.JestAutoConfiguration,\ -org.springframework.boot.autoconfigure.elasticsearch.rest.RestClientAutoConfiguration,\ +org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration,\ org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\ org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\ +org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\ org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\ org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,\ org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration,\ @@ -84,11 +90,11 @@ org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration,\ org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration,\ org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration,\ org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration,\ -org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\ org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration,\ org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration,\ org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration,\ org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration,\ +org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration,\ org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapAutoConfiguration,\ org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration,\ org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,\ @@ -98,8 +104,12 @@ org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfigura org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\ org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration,\ org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\ +org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration,\ +org.springframework.boot.autoconfigure.netty.NettyAutoConfiguration,\ org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\ org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration,\ +org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration,\ +org.springframework.boot.autoconfigure.r2dbc.R2dbcTransactionManagerAutoConfiguration,\ org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration,\ org.springframework.boot.autoconfigure.rsocket.RSocketRequesterAutoConfiguration,\ org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration,\ @@ -118,6 +128,7 @@ org.springframework.boot.autoconfigure.security.oauth2.client.reactive.ReactiveO org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration,\ org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration,\ org.springframework.boot.autoconfigure.solr.SolrAutoConfiguration,\ +org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration,\ org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration,\ org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration,\ org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration,\ @@ -146,10 +157,13 @@ org.springframework.boot.autoconfigure.webservices.client.WebServiceTemplateAuto # Failure analyzers org.springframework.boot.diagnostics.FailureAnalyzer=\ +org.springframework.boot.autoconfigure.data.redis.RedisUrlSyntaxFailureAnalyzer,\ org.springframework.boot.autoconfigure.diagnostics.analyzer.NoSuchBeanDefinitionFailureAnalyzer,\ org.springframework.boot.autoconfigure.flyway.FlywayMigrationScriptMissingFailureAnalyzer,\ org.springframework.boot.autoconfigure.jdbc.DataSourceBeanCreationFailureAnalyzer,\ org.springframework.boot.autoconfigure.jdbc.HikariDriverConfigurationFailureAnalyzer,\ +org.springframework.boot.autoconfigure.jooq.NoDslContextBeanFailureAnalyzer,\ +org.springframework.boot.autoconfigure.r2dbc.ConnectionFactoryBeanCreationFailureAnalyzer,\ org.springframework.boot.autoconfigure.session.NonUniqueSessionRepositoryFailureAnalyzer # Template availability providers @@ -159,3 +173,13 @@ org.springframework.boot.autoconfigure.mustache.MustacheTemplateAvailabilityProv org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAvailabilityProvider,\ org.springframework.boot.autoconfigure.thymeleaf.ThymeleafTemplateAvailabilityProvider,\ org.springframework.boot.autoconfigure.web.servlet.JspTemplateAvailabilityProvider + +# DataSource initializer detectors +org.springframework.boot.sql.init.dependency.DatabaseInitializerDetector=\ +org.springframework.boot.autoconfigure.flyway.FlywayMigrationInitializerDatabaseInitializerDetector + +# Depends on database initialization detectors +org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitializationDetector=\ +org.springframework.boot.autoconfigure.batch.JobRepositoryDependsOnDatabaseInitializationDetector,\ +org.springframework.boot.autoconfigure.quartz.SchedulerDependsOnDatabaseInitializationDetector,\ +org.springframework.boot.autoconfigure.session.JdbcIndexedSessionRepositoryDependsOnDatabaseInitializationDetector diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationImportSelectorTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationImportSelectorTests.java index c6297b2ed9b9..6e4583de109b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationImportSelectorTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationImportSelectorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,9 +24,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.MockitoAnnotations; -import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; @@ -62,7 +60,6 @@ class AutoConfigurationImportSelectorTests { @BeforeEach void setup() { - MockitoAnnotations.initMocks(this); this.importSelector.setBeanFactory(this.beanFactory); this.importSelector.setEnvironment(this.environment); this.importSelector.setResourceLoader(new DefaultResourceLoader()); @@ -202,6 +199,16 @@ void filterShouldSupportAware() { assertThat(filter.getBeanFactory()).isEqualTo(this.beanFactory); } + @Test + void getExclusionFilterReuseFilters() { + String[] allImports = new String[] { "com.example.A", "com.example.B", "com.example.C" }; + this.filters.add(new TestAutoConfigurationImportFilter(allImports, 0)); + this.filters.add(new TestAutoConfigurationImportFilter(allImports, 2)); + assertThat(this.importSelector.getExclusionFilter().test("com.example.A")).isTrue(); + assertThat(this.importSelector.getExclusionFilter().test("com.example.B")).isFalse(); + assertThat(this.importSelector.getExclusionFilter().test("com.example.C")).isTrue(); + } + private String[] selectImports(Class source) { return this.importSelector.selectImports(AnnotationMetadata.introspect(source)); } @@ -252,7 +259,7 @@ public boolean[] match(String[] autoConfigurationClasses, AutoConfigurationMetad } @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationPackagesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationPackagesTests.java index 433698ca482e..f7a2dd9486d4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationPackagesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationPackagesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,12 +20,10 @@ import org.junit.jupiter.api.Test; -import org.springframework.boot.autoconfigure.AutoConfigurationPackages.Registrar; import org.springframework.boot.autoconfigure.packagestest.one.FirstConfiguration; import org.springframework.boot.autoconfigure.packagestest.two.SecondConfiguration; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; @@ -37,11 +35,12 @@ * @author Oliver Gierke */ @SuppressWarnings("resource") -public class AutoConfigurationPackagesTests { +class AutoConfigurationPackagesTests { @Test void setAndGet() { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConfigWithRegistrar.class); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + ConfigWithAutoConfigurationPackage.class); assertThat(AutoConfigurationPackages.get(context.getBeanFactory())) .containsExactly(getClass().getPackage().getName()); } @@ -63,21 +62,43 @@ void detectsMultipleAutoConfigurationPackages() { assertThat(packages).containsOnly(package1.getName(), package2.getName()); } + @Test + void whenBasePackagesAreSpecifiedThenTheyAreRegistered() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + ConfigWithAutoConfigurationBasePackages.class); + List packages = AutoConfigurationPackages.get(context.getBeanFactory()); + assertThat(packages).containsExactly("com.example.alpha", "com.example.bravo"); + } + + @Test + void whenBasePackageClassesAreSpecifiedThenTheirPackagesAreRegistered() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + ConfigWithAutoConfigurationBasePackageClasses.class); + List packages = AutoConfigurationPackages.get(context.getBeanFactory()); + assertThat(packages).containsOnly(FirstConfiguration.class.getPackage().getName(), + SecondConfiguration.class.getPackage().getName()); + } + @Configuration(proxyBeanMethods = false) - @Import(AutoConfigurationPackages.Registrar.class) - static class ConfigWithRegistrar { + @AutoConfigurationPackage + static class ConfigWithAutoConfigurationPackage { } @Configuration(proxyBeanMethods = false) - static class EmptyConfig { + @AutoConfigurationPackage(basePackages = { "com.example.alpha", "com.example.bravo" }) + static class ConfigWithAutoConfigurationBasePackages { } - /** - * Test helper to allow {@link Registrar} to be referenced from other packages. - */ - public static class TestRegistrar extends Registrar { + @Configuration(proxyBeanMethods = false) + @AutoConfigurationPackage(basePackageClasses = { FirstConfiguration.class, SecondConfiguration.class }) + static class ConfigWithAutoConfigurationBasePackageClasses { + + } + + @Configuration(proxyBeanMethods = false) + static class EmptyConfig { } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationImportSelectorTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationImportSelectorTests.java index 2a82907cd304..19466225721b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationImportSelectorTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationImportSelectorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,22 +25,19 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration; import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration; import org.springframework.core.annotation.AliasFor; -import org.springframework.core.env.Environment; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; +import org.springframework.mock.env.MockEnvironment; import org.springframework.util.ClassUtils; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.verifyNoInteractions; /** * Tests for {@link ImportAutoConfigurationImportSelector}. @@ -54,12 +51,10 @@ class ImportAutoConfigurationImportSelectorTests { private final ConfigurableListableBeanFactory beanFactory = new DefaultListableBeanFactory(); - @Mock - private Environment environment; + private final MockEnvironment environment = new MockEnvironment(); @BeforeEach void setup() { - MockitoAnnotations.initMocks(this); this.importSelector.setBeanFactory(this.beanFactory); this.importSelector.setEnvironment(this.environment); this.importSelector.setResourceLoader(new DefaultResourceLoader()); @@ -80,10 +75,11 @@ void importsAreSelectedUsingClassesAttribute() throws Exception { } @Test - void propertyExclusionsAreNotApplied() throws Exception { - AnnotationMetadata annotationMetadata = getAnnotationMetadata(ImportFreeMarker.class); - this.importSelector.selectImports(annotationMetadata); - verifyNoInteractions(this.environment); + void propertyExclusionsAreApplied() throws IOException { + this.environment.setProperty("spring.autoconfigure.exclude", FreeMarkerAutoConfiguration.class.getName()); + AnnotationMetadata annotationMetadata = getAnnotationMetadata(MultipleImports.class); + String[] imports = this.importSelector.selectImports(annotationMetadata); + assertThat(imports).containsExactly(ThymeleafAutoConfiguration.class.getName()); } @Test @@ -171,12 +167,12 @@ void determineImportsWhenUsingMetaDifferentExcludeWithoutClassesShouldBeDifferen @Test void determineImportsShouldNotSetPackageImport() throws Exception { - Class packageImportClass = ClassUtils.resolveClassName( - "org.springframework.boot.autoconfigure.AutoConfigurationPackages.PackageImport", null); + Class packageImportsClass = ClassUtils.resolveClassName( + "org.springframework.boot.autoconfigure.AutoConfigurationPackages.PackageImports", null); Set selectedImports = this.importSelector .determineImports(getAnnotationMetadata(ImportMetaAutoConfigurationExcludeWithUnrelatedOne.class)); for (Object selectedImport : selectedImports) { - assertThat(selectedImport).isNotInstanceOf(packageImportClass); + assertThat(selectedImport).isNotInstanceOf(packageImportsClass); } } @@ -288,7 +284,9 @@ static class ImportMetaAutoConfigurationExcludeWithUnrelatedTwo { @interface MetaImportAutoConfiguration { @AliasFor(annotation = ImportAutoConfiguration.class) - Class[] exclude() default {}; + Class[] exclude() default { + + }; } @@ -308,7 +306,9 @@ static class ImportMetaAutoConfigurationExcludeWithUnrelatedTwo { @interface SelfAnnotating { @AliasFor(annotation = ImportAutoConfiguration.class, attribute = "exclude") - Class[] excludeAutoConfiguration() default {}; + Class[] excludeAutoConfiguration() default { + + }; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/SharedMetadataReaderFactoryContextInitializerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/SharedMetadataReaderFactoryContextInitializerTests.java index 8ee879d2deb1..ba4f4aa661c4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/SharedMetadataReaderFactoryContextInitializerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/SharedMetadataReaderFactoryContextInitializerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,32 +19,42 @@ import java.util.List; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; -import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; import org.springframework.boot.SpringApplication; import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer.CachingMetadataReaderFactoryPostProcessor; +import org.springframework.boot.type.classreading.ConcurrentReferenceCachingMetadataReaderFactory; import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.annotation.AnnotationConfigUtils; +import org.springframework.context.annotation.ConfigurationClassPostProcessor; import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; /** * Tests for {@link SharedMetadataReaderFactoryContextInitializer}. * * @author Dave Syer + * @author Phillip Webb */ class SharedMetadataReaderFactoryContextInitializerTests { @Test + @SuppressWarnings("unchecked") void checkOrderOfInitializer() { SpringApplication application = new SpringApplication(TestConfig.class); application.setWebApplicationType(WebApplicationType.NONE); - @SuppressWarnings("unchecked") List> initializers = (List>) ReflectionTestUtils .getField(application, "initializers"); // Simulate what would happen if an initializer was added using spring.factories @@ -55,6 +65,30 @@ void checkOrderOfInitializer() { assertThat(definition.getAttribute("seen")).isEqualTo(true); } + @Test + void initializeWhenUsingSupplierDecorates() { + GenericApplicationContext context = new GenericApplicationContext(); + BeanDefinitionRegistry registry = (BeanDefinitionRegistry) context.getBeanFactory(); + ConfigurationClassPostProcessor configurationAnnotationPostProcessor = mock( + ConfigurationClassPostProcessor.class); + BeanDefinition beanDefinition = BeanDefinitionBuilder + .genericBeanDefinition(ConfigurationClassPostProcessor.class).getBeanDefinition(); + ((AbstractBeanDefinition) beanDefinition).setInstanceSupplier(() -> configurationAnnotationPostProcessor); + registry.registerBeanDefinition(AnnotationConfigUtils.CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME, + beanDefinition); + CachingMetadataReaderFactoryPostProcessor postProcessor = new CachingMetadataReaderFactoryPostProcessor( + context); + postProcessor.postProcessBeanDefinitionRegistry(registry); + context.refresh(); + ConfigurationClassPostProcessor bean = context.getBean(ConfigurationClassPostProcessor.class); + assertThat(bean).isSameAs(configurationAnnotationPostProcessor); + ArgumentCaptor metadataReaderFactory = ArgumentCaptor + .forClass(MetadataReaderFactory.class); + then(configurationAnnotationPostProcessor).should().setMetadataReaderFactory(metadataReaderFactory.capture()); + assertThat(metadataReaderFactory.getValue()) + .isInstanceOf(ConcurrentReferenceCachingMetadataReaderFactory.class); + } + static class TestConfig { } @@ -71,11 +105,11 @@ public void initialize(GenericApplicationContext applicationContext) { static class PostProcessor implements BeanDefinitionRegistryPostProcessor { @Override - public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { } @Override - public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { for (String name : registry.getBeanDefinitionNames()) { BeanDefinition definition = registry.getBeanDefinition(name); definition.setAttribute("seen", true); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/SpringBootApplicationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/SpringBootApplicationTests.java index d4260e10f4bd..7f4ec9375762 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/SpringBootApplicationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/SpringBootApplicationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,9 @@ import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.beans.factory.support.DefaultBeanNameGenerator; +import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationAttributes; @@ -28,6 +31,7 @@ * Tests for {@link SpringBootApplication @SpringBootApplication}. * * @author Andy Wilkinson + * @author Stephane Nicoll */ class SpringBootApplicationTests { @@ -45,6 +49,20 @@ void proxyBeanMethodsCanBeDisabled() { assertThat(attributes.get("proxyBeanMethods")).isEqualTo(false); } + @Test + void nameGeneratorDefaultToBeanNameGenerator() { + AnnotationAttributes attributes = AnnotatedElementUtils + .getMergedAnnotationAttributes(DefaultSpringBootApplication.class, ComponentScan.class); + assertThat(attributes.get("nameGenerator")).isEqualTo(BeanNameGenerator.class); + } + + @Test + void nameGeneratorCanBeSpecified() { + AnnotationAttributes attributes = AnnotatedElementUtils + .getMergedAnnotationAttributes(CustomNameGeneratorConfiguration.class, ComponentScan.class); + assertThat(attributes.get("nameGenerator")).isEqualTo(TestBeanNameGenerator.class); + } + @SpringBootApplication static class DefaultSpringBootApplication { @@ -55,4 +73,13 @@ static class NoBeanMethodProxyingSpringBootApplication { } + @SpringBootApplication(nameGenerator = TestBeanNameGenerator.class) + static class CustomNameGeneratorConfiguration { + + } + + static class TestBeanNameGenerator extends DefaultBeanNameGenerator { + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/admin/SpringApplicationAdminJmxAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/admin/SpringApplicationAdminJmxAutoConfigurationTests.java index 12b2ebe7ac37..389ec27b0a1a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/admin/SpringApplicationAdminJmxAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/admin/SpringApplicationAdminJmxAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,6 +50,7 @@ * * @author Stephane Nicoll * @author Andy Wilkinson + * @author Nguyen Bao Sach */ class SpringApplicationAdminJmxAutoConfigurationTests { @@ -60,17 +61,10 @@ class SpringApplicationAdminJmxAutoConfigurationTests { private final MBeanServer server = ManagementFactory.getPlatformMBeanServer(); private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(MultipleMBeanExportersConfiguration.class, - SpringApplicationAdminJmxAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(SpringApplicationAdminJmxAutoConfiguration.class)); @Test - void notRegisteredByDefault() { - this.contextRunner.run((context) -> assertThatExceptionOfType(InstanceNotFoundException.class) - .isThrownBy(() -> this.server.getObjectInstance(createDefaultObjectName()))); - } - - @Test - void registeredWithProperty() { + void notRegisteredWhenThereAreNoMBeanExporter() { this.contextRunner.withPropertyValues(ENABLE_ADMIN_PROP).run((context) -> { ObjectName objectName = createDefaultObjectName(); ObjectInstance objectInstance = this.server.getObjectInstance(objectName); @@ -79,9 +73,27 @@ void registeredWithProperty() { } @Test - void registerWithCustomJmxName() { + void notRegisteredByDefaultWhenThereAreMultipleMBeanExporters() { + this.contextRunner.withUserConfiguration(MultipleMBeanExportersConfiguration.class) + .run((context) -> assertThatExceptionOfType(InstanceNotFoundException.class) + .isThrownBy(() -> this.server.getObjectInstance(createDefaultObjectName()))); + } + + @Test + void registeredWithPropertyWhenThereAreMultipleMBeanExporters() { + this.contextRunner.withUserConfiguration(MultipleMBeanExportersConfiguration.class) + .withPropertyValues(ENABLE_ADMIN_PROP).run((context) -> { + ObjectName objectName = createDefaultObjectName(); + ObjectInstance objectInstance = this.server.getObjectInstance(objectName); + assertThat(objectInstance).as("Lifecycle bean should have been registered").isNotNull(); + }); + } + + @Test + void registerWithCustomJmxNameWhenThereAreMultipleMBeanExporters() { String customJmxName = "org.acme:name=FooBar"; - this.contextRunner.withSystemProperties("spring.application.admin.jmx-name=" + customJmxName) + this.contextRunner.withUserConfiguration(MultipleMBeanExportersConfiguration.class) + .withSystemProperties("spring.application.admin.jmx-name=" + customJmxName) .withPropertyValues(ENABLE_ADMIN_PROP).run((context) -> { try { this.server.getObjectInstance(createObjectName(customJmxName)); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java index bfe14c13bc81..c4d17faa72e1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,16 +20,18 @@ import java.util.List; import java.util.concurrent.atomic.AtomicInteger; -import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManager; import com.rabbitmq.client.Address; import com.rabbitmq.client.Connection; -import com.rabbitmq.client.SslContextFactory; -import com.rabbitmq.client.TrustEverythingTrustManager; +import com.rabbitmq.client.JDKSaslConfig; +import com.rabbitmq.client.impl.CredentialsProvider; +import com.rabbitmq.client.impl.CredentialsRefreshService; +import com.rabbitmq.client.impl.DefaultCredentialsProvider; import org.aopalliance.aop.Advice; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InOrder; import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.core.AmqpAdmin; @@ -39,6 +41,7 @@ import org.springframework.amqp.rabbit.config.DirectRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.config.RabbitListenerConfigUtils; import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; +import org.springframework.amqp.rabbit.connection.AbstractConnectionFactory.AddressShuffleMode; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory.CacheMode; import org.springframework.amqp.rabbit.connection.ConnectionFactory; @@ -53,9 +56,13 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.retry.RetryPolicy; import org.springframework.retry.backoff.BackOffPolicy; import org.springframework.retry.backoff.ExponentialBackOffPolicy; @@ -71,8 +78,9 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link RabbitAutoConfiguration}. @@ -81,7 +89,9 @@ * @author Stephane Nicoll * @author Gary Russell * @author HaiTao Zhang + * @author Franjo Zilic */ +@ExtendWith(OutputCaptureExtension.class) class RabbitAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() @@ -99,8 +109,12 @@ void testDefaultRabbitConfiguration() { assertThat(messagingTemplate.getRabbitTemplate()).isEqualTo(rabbitTemplate); assertThat(amqpAdmin).isNotNull(); assertThat(connectionFactory.getHost()).isEqualTo("localhost"); + assertThat(getTargetConnectionFactory(context).getRequestedChannelMax()) + .isEqualTo(com.rabbitmq.client.ConnectionFactory.DEFAULT_CHANNEL_MAX); assertThat(connectionFactory.isPublisherConfirms()).isFalse(); assertThat(connectionFactory.isPublisherReturns()).isFalse(); + assertThat(connectionFactory.getRabbitConnectionFactory().getChannelRpcTimeout()) + .isEqualTo(com.rabbitmq.client.ConnectionFactory.DEFAULT_CHANNEL_RPC_TIMEOUT); assertThat(context.containsBean("rabbitListenerContainerFactory")) .as("Listener container factory should be created by default").isTrue(); }); @@ -131,15 +145,19 @@ void testDefaultConnectionFactoryConfiguration() { void testConnectionFactoryWithOverrides() { this.contextRunner.withUserConfiguration(TestConfiguration.class) .withPropertyValues("spring.rabbitmq.host:remote-server", "spring.rabbitmq.port:9000", - "spring.rabbitmq.username:alice", "spring.rabbitmq.password:secret", - "spring.rabbitmq.virtual_host:/vhost", "spring.rabbitmq.connection-timeout:123") + "spring.rabbitmq.address-shuffle-mode=random", "spring.rabbitmq.username:alice", + "spring.rabbitmq.password:secret", "spring.rabbitmq.virtual_host:/vhost", + "spring.rabbitmq.connection-timeout:123", "spring.rabbitmq.channel-rpc-timeout:140") .run((context) -> { CachingConnectionFactory connectionFactory = context.getBean(CachingConnectionFactory.class); assertThat(connectionFactory.getHost()).isEqualTo("remote-server"); assertThat(connectionFactory.getPort()).isEqualTo(9000); + assertThat(connectionFactory).hasFieldOrPropertyWithValue("addressShuffleMode", + AddressShuffleMode.RANDOM); assertThat(connectionFactory.getVirtualHost()).isEqualTo("/vhost"); com.rabbitmq.client.ConnectionFactory rcf = connectionFactory.getRabbitConnectionFactory(); assertThat(rcf.getConnectionTimeout()).isEqualTo(123); + assertThat(rcf.getChannelRpcTimeout()).isEqualTo(140); assertThat((List
    ) ReflectionTestUtils.getField(connectionFactory, "addresses")).hasSize(1); }); } @@ -155,10 +173,10 @@ void testConnectionFactoryWithCustomConnectionNameStrategy() { given(rcf.newConnection(isNull(), eq(addresses), anyString())).willReturn(mock(Connection.class)); ReflectionTestUtils.setField(connectionFactory, "rabbitConnectionFactory", rcf); connectionFactory.createConnection(); - verify(rcf).newConnection(isNull(), eq(addresses), eq("test#0")); + then(rcf).should().newConnection(isNull(), eq(addresses), eq("test#0")); connectionFactory.resetConnection(); connectionFactory.createConnection(); - verify(rcf).newConnection(isNull(), eq(addresses), eq("test#1")); + then(rcf).should().newConnection(isNull(), eq(addresses), eq("test#1")); }); } @@ -198,17 +216,6 @@ void testConnectionFactoryDefaultVirtualHost() { }); } - @Test - @Deprecated - void testConnectionFactoryPublisherConfirmTypeUsingDeprecatedProperty() { - this.contextRunner.withUserConfiguration(TestConfiguration.class) - .withPropertyValues("spring.rabbitmq.publisher-confirms=true").run((context) -> { - CachingConnectionFactory connectionFactory = context.getBean(CachingConnectionFactory.class); - assertThat(connectionFactory.isPublisherConfirms()).isTrue(); - assertThat(connectionFactory.isSimplePublisherConfirms()).isFalse(); - }); - } - @Test void testConnectionFactoryPublisherConfirmTypeCorrelated() { this.contextRunner.withUserConfiguration(TestConfiguration.class) @@ -330,6 +337,31 @@ void testRabbitTemplateMandatoryDisabledEvenIfPublisherReturnsIsSet() { }); } + @Test + void testRabbitTemplateConfigurersIsAvailable() { + this.contextRunner.withUserConfiguration(TestConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(RabbitTemplateConfigurer.class)); + } + + @Test + void testRabbitTemplateConfigurerUsesConfig() { + this.contextRunner.withUserConfiguration(MessageConvertersConfiguration.class) + .withPropertyValues("spring.rabbitmq.template.exchange:my-exchange", + "spring.rabbitmq.template.routing-key:my-routing-key", + "spring.rabbitmq.template.default-receive-queue:default-queue") + .run((context) -> { + RabbitTemplateConfigurer configurer = context.getBean(RabbitTemplateConfigurer.class); + RabbitTemplate template = mock(RabbitTemplate.class); + ConnectionFactory connectionFactory = mock(ConnectionFactory.class); + configurer.configure(template, connectionFactory); + then(template).should() + .setMessageConverter(context.getBean("myMessageConverter", MessageConverter.class)); + then(template).should().setExchange("my-exchange"); + then(template).should().setRoutingKey("my-routing-key"); + then(template).should().setDefaultReceiveQueue("default-queue"); + }); + } + @Test void testConnectionFactoryBackOff() { this.contextRunner.withUserConfiguration(TestConfiguration2.class).run((context) -> { @@ -397,7 +429,7 @@ void testRabbitListenerContainerFactoryBackOff() { SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory = context .getBean("rabbitListenerContainerFactory", SimpleRabbitListenerContainerFactory.class); rabbitListenerContainerFactory.setBatchSize(10); - verify(rabbitListenerContainerFactory).setBatchSize(10); + then(rabbitListenerContainerFactory).should().setBatchSize(10); assertThat(rabbitListenerContainerFactory.getAdviceChain()).isNull(); }); } @@ -432,18 +464,6 @@ void testSimpleRabbitListenerContainerFactoryWithCustomSettings() { }); } - @Test - @Deprecated - void testRabbitListenerContainerFactoryWithDeprecatedTransactionSizeStillWorks() { - this.contextRunner - .withUserConfiguration(MessageConvertersConfiguration.class, MessageRecoverersConfiguration.class) - .withPropertyValues("spring.rabbitmq.listener.simple.transactionSize:20").run((context) -> { - SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory = context - .getBean("rabbitListenerContainerFactory", SimpleRabbitListenerContainerFactory.class); - assertThat(rabbitListenerContainerFactory).hasFieldOrPropertyWithValue("batchSize", 20); - }); - } - @Test void testDirectRabbitListenerContainerFactoryWithCustomSettings() { this.contextRunner @@ -521,8 +541,7 @@ void testRabbitListenerContainerFactoryConfigurersAreAvailable() { @Test void testSimpleRabbitListenerContainerFactoryConfigurerUsesConfig() { this.contextRunner.withUserConfiguration(TestConfiguration.class) - .withPropertyValues("spring.rabbitmq.listener.type:direct", - "spring.rabbitmq.listener.simple.concurrency:5", + .withPropertyValues("spring.rabbitmq.listener.simple.concurrency:5", "spring.rabbitmq.listener.simple.maxConcurrency:10", "spring.rabbitmq.listener.simple.prefetch:40") .run((context) -> { @@ -530,25 +549,38 @@ void testSimpleRabbitListenerContainerFactoryConfigurerUsesConfig() { .getBean(SimpleRabbitListenerContainerFactoryConfigurer.class); SimpleRabbitListenerContainerFactory factory = mock(SimpleRabbitListenerContainerFactory.class); configurer.configure(factory, mock(ConnectionFactory.class)); - verify(factory).setConcurrentConsumers(5); - verify(factory).setMaxConcurrentConsumers(10); - verify(factory).setPrefetchCount(40); + then(factory).should().setConcurrentConsumers(5); + then(factory).should().setMaxConcurrentConsumers(10); + then(factory).should().setPrefetchCount(40); + }); + } + + @Test + void testSimpleRabbitListenerContainerFactoryConfigurerEnableDeBatchingWithConsumerBatchEnabled() { + this.contextRunner.withUserConfiguration(TestConfiguration.class) + .withPropertyValues("spring.rabbitmq.listener.simple.consumer-batch-enabled:true").run((context) -> { + SimpleRabbitListenerContainerFactoryConfigurer configurer = context + .getBean(SimpleRabbitListenerContainerFactoryConfigurer.class); + SimpleRabbitListenerContainerFactory factory = mock(SimpleRabbitListenerContainerFactory.class); + configurer.configure(factory, mock(ConnectionFactory.class)); + then(factory).should().setConsumerBatchEnabled(true); }); } @Test void testDirectRabbitListenerContainerFactoryConfigurerUsesConfig() { this.contextRunner.withUserConfiguration(TestConfiguration.class) - .withPropertyValues("spring.rabbitmq.listener.type:simple", - "spring.rabbitmq.listener.direct.consumers-per-queue:5", - "spring.rabbitmq.listener.direct.prefetch:40") + .withPropertyValues("spring.rabbitmq.listener.direct.consumers-per-queue:5", + "spring.rabbitmq.listener.direct.prefetch:40", + "spring.rabbitmq.listener.direct.de-batching-enabled:false") .run((context) -> { DirectRabbitListenerContainerFactoryConfigurer configurer = context .getBean(DirectRabbitListenerContainerFactoryConfigurer.class); DirectRabbitListenerContainerFactory factory = mock(DirectRabbitListenerContainerFactory.class); configurer.configure(factory, mock(ConnectionFactory.class)); - verify(factory).setConsumersPerQueue(5); - verify(factory).setPrefetchCount(40); + then(factory).should().setConsumersPerQueue(5); + then(factory).should().setPrefetchCount(40); + then(factory).should().setDeBatchingEnabled(false); }); } @@ -571,7 +603,7 @@ private void checkCommonProps(AssertableApplicationContext context, Message message = mock(Message.class); Exception ex = new Exception("test"); mir.recover(new Object[] { "foo", message }, ex); - verify(messageRecoverer).recover(message, ex); + then(messageRecoverer).should().recover(message, ex); RetryTemplate retryTemplate = (RetryTemplate) ReflectionTestUtils.getField(advice, "retryOperations"); assertThat(retryTemplate).isNotNull(); SimpleRetryPolicy retryPolicy = (SimpleRetryPolicy) ReflectionTestUtils.getField(retryTemplate, "retryPolicy"); @@ -600,6 +632,15 @@ void customizeRequestedHeartBeat() { }); } + @Test + void customizeRequestedChannelMax() { + this.contextRunner.withUserConfiguration(TestConfiguration.class) + .withPropertyValues("spring.rabbitmq.requestedChannelMax:12").run((context) -> { + com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory = getTargetConnectionFactory(context); + assertThat(rabbitConnectionFactory.getRequestedChannelMax()).isEqualTo(12); + }); + } + @Test void noSslByDefault() { this.contextRunner.withUserConfiguration(TestConfiguration.class).run((context) -> { @@ -682,37 +723,131 @@ void enableSslWithKeystoreTypeAndTrustStoreTypeShouldWork() { } @Test - void enableSslWithValidateServerCertificateFalse() throws Exception { + void enableSslWithValidateServerCertificateFalse(CapturedOutput output) { this.contextRunner.withUserConfiguration(TestConfiguration.class) .withPropertyValues("spring.rabbitmq.ssl.enabled:true", "spring.rabbitmq.ssl.validateServerCertificate=false") .run((context) -> { com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory = getTargetConnectionFactory(context); - TrustManager trustManager = getTrustManager(rabbitConnectionFactory); - assertThat(trustManager).isInstanceOf(TrustEverythingTrustManager.class); + assertThat(rabbitConnectionFactory.isSSL()).isTrue(); + assertThat(output).contains("TrustEverythingTrustManager", "SECURITY ALERT"); }); } @Test - void enableSslWithValidateServerCertificateDefault() throws Exception { + void enableSslWithValidateServerCertificateDefault(CapturedOutput output) { this.contextRunner.withUserConfiguration(TestConfiguration.class) .withPropertyValues("spring.rabbitmq.ssl.enabled:true").run((context) -> { com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory = getTargetConnectionFactory(context); - TrustManager trustManager = getTrustManager(rabbitConnectionFactory); - assertThat(trustManager).isNotInstanceOf(TrustEverythingTrustManager.class); + assertThat(rabbitConnectionFactory.isSSL()).isTrue(); + assertThat(output).doesNotContain("TrustEverythingTrustManager", "SECURITY ALERT"); }); } - private TrustManager getTrustManager(com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory) { - SslContextFactory sslContextFactory = (SslContextFactory) ReflectionTestUtils.getField(rabbitConnectionFactory, - "sslContextFactory"); - SSLContext sslContext = sslContextFactory.create("connection"); - Object spi = ReflectionTestUtils.getField(sslContext, "contextSpi"); - Object trustManager = ReflectionTestUtils.getField(spi, "trustManager"); - while (trustManager.getClass().getName().endsWith("Wrapper")) { - trustManager = ReflectionTestUtils.getField(trustManager, "tm"); - } - return (TrustManager) trustManager; + @Test + void enableSslWithValidStoreAlgorithmShouldWork() { + this.contextRunner.withUserConfiguration(TestConfiguration.class) + .withPropertyValues("spring.rabbitmq.ssl.enabled:true", + "spring.rabbitmq.ssl.keyStore=/org/springframework/boot/autoconfigure/amqp/test.jks", + "spring.rabbitmq.ssl.keyStoreType=jks", "spring.rabbitmq.ssl.keyStorePassword=secret", + "spring.rabbitmq.ssl.keyStoreAlgorithm=PKIX", + "spring.rabbitmq.ssl.trustStore=/org/springframework/boot/autoconfigure/amqp/test.jks", + "spring.rabbitmq.ssl.trustStoreType=jks", "spring.rabbitmq.ssl.trustStorePassword=secret", + "spring.rabbitmq.ssl.trustStoreAlgorithm=PKIX") + .run((context) -> assertThat(context).hasNotFailed()); + } + + @Test + void enableSslWithInvalidKeyStoreAlgorithmShouldFail() { + this.contextRunner.withUserConfiguration(TestConfiguration.class) + .withPropertyValues("spring.rabbitmq.ssl.enabled:true", + "spring.rabbitmq.ssl.keyStore=/org/springframework/boot/autoconfigure/amqp/test.jks", + "spring.rabbitmq.ssl.keyStoreType=jks", "spring.rabbitmq.ssl.keyStorePassword=secret", + "spring.rabbitmq.ssl.keyStoreAlgorithm=test-invalid-algo") + .run((context) -> { + assertThat(context).hasFailed(); + assertThat(context).getFailure().hasMessageContaining("test-invalid-algo"); + assertThat(context).getFailure().hasRootCauseInstanceOf(NoSuchAlgorithmException.class); + }); + } + + @Test + void enableSslWithInvalidTrustStoreAlgorithmShouldFail() { + this.contextRunner.withUserConfiguration(TestConfiguration.class) + .withPropertyValues("spring.rabbitmq.ssl.enabled:true", + "spring.rabbitmq.ssl.trustStore=/org/springframework/boot/autoconfigure/amqp/test.jks", + "spring.rabbitmq.ssl.trustStoreType=jks", "spring.rabbitmq.ssl.trustStorePassword=secret", + "spring.rabbitmq.ssl.trustStoreAlgorithm=test-invalid-algo") + .run((context) -> { + assertThat(context).hasFailed(); + assertThat(context).getFailure().hasMessageContaining("test-invalid-algo"); + assertThat(context).getFailure().hasRootCauseInstanceOf(NoSuchAlgorithmException.class); + }); + } + + @Test + void whenACredentialsProviderIsAvailableThenConnectionFactoryIsConfiguredToUseIt() { + this.contextRunner.withUserConfiguration(CredentialsProviderConfiguration.class) + .run((context) -> assertThat(getTargetConnectionFactory(context).params(null).getCredentialsProvider()) + .isEqualTo(CredentialsProviderConfiguration.credentialsProvider)); + } + + @Test + void whenAPrimaryCredentialsProviderIsAvailableThenConnectionFactoryIsConfiguredToUseIt() { + this.contextRunner.withUserConfiguration(PrimaryCredentialsProviderConfiguration.class) + .run((context) -> assertThat(getTargetConnectionFactory(context).params(null).getCredentialsProvider()) + .isEqualTo(PrimaryCredentialsProviderConfiguration.credentialsProvider)); + } + + @Test + void whenMultipleCredentialsProvidersAreAvailableThenConnectionFactoryUsesDefaultProvider() { + this.contextRunner.withUserConfiguration(MultipleCredentialsProvidersConfiguration.class) + .run((context) -> assertThat(getTargetConnectionFactory(context).params(null).getCredentialsProvider()) + .isInstanceOf(DefaultCredentialsProvider.class)); + } + + @Test + void whenACredentialsRefreshServiceIsAvailableThenConnectionFactoryIsConfiguredToUseIt() { + this.contextRunner.withUserConfiguration(CredentialsRefreshServiceConfiguration.class).run( + (context) -> assertThat(getTargetConnectionFactory(context).params(null).getCredentialsRefreshService()) + .isEqualTo(CredentialsRefreshServiceConfiguration.credentialsRefreshService)); + } + + @Test + void whenAPrimaryCredentialsRefreshServiceIsAvailableThenConnectionFactoryIsConfiguredToUseIt() { + this.contextRunner.withUserConfiguration(PrimaryCredentialsRefreshServiceConfiguration.class).run( + (context) -> assertThat(getTargetConnectionFactory(context).params(null).getCredentialsRefreshService()) + .isEqualTo(PrimaryCredentialsRefreshServiceConfiguration.credentialsRefreshService)); + } + + @Test + void whenMultipleCredentialsRefreshServiceAreAvailableThenConnectionFactoryHasNoCredentialsRefreshService() { + this.contextRunner.withUserConfiguration(MultipleCredentialsRefreshServicesConfiguration.class).run( + (context) -> assertThat(getTargetConnectionFactory(context).params(null).getCredentialsRefreshService()) + .isNull()); + } + + @Test + void whenAConnectionFactoryCustomizerIsDefinedThenItCustomizesTheConnectionFactory() { + this.contextRunner.withUserConfiguration(SaslConfigCustomizerConfiguration.class) + .run((context) -> assertThat(getTargetConnectionFactory(context).getSaslConfig()) + .isInstanceOf(JDKSaslConfig.class)); + } + + @Test + void whenMultipleConnectionFactoryCustomizersAreDefinedThenTheyAreCalledInOrder() { + this.contextRunner.withUserConfiguration(MultipleConnectionFactoryCustomizersConfiguration.class) + .run((context) -> { + ConnectionFactoryCustomizer firstCustomizer = context.getBean("firstCustomizer", + ConnectionFactoryCustomizer.class); + ConnectionFactoryCustomizer secondCustomizer = context.getBean("secondCustomizer", + ConnectionFactoryCustomizer.class); + InOrder inOrder = inOrder(firstCustomizer, secondCustomizer); + com.rabbitmq.client.ConnectionFactory targetConnectionFactory = getTargetConnectionFactory(context); + then(firstCustomizer).should(inOrder).customize(targetConnectionFactory); + then(secondCustomizer).should(inOrder).customize(targetConnectionFactory); + inOrder.verifyNoMoreInteractions(); + }); } private com.rabbitmq.client.ConnectionFactory getTargetConnectionFactory(AssertableApplicationContext context) { @@ -860,4 +995,123 @@ static class NoEnableRabbitConfiguration { } + @Configuration(proxyBeanMethods = false) + static class CredentialsProviderConfiguration { + + private static final CredentialsProvider credentialsProvider = mock(CredentialsProvider.class); + + @Bean + CredentialsProvider credentialsProvider() { + return credentialsProvider; + } + + } + + @Configuration(proxyBeanMethods = false) + static class PrimaryCredentialsProviderConfiguration { + + private static final CredentialsProvider credentialsProvider = mock(CredentialsProvider.class); + + @Bean + @Primary + CredentialsProvider credentialsProvider() { + return credentialsProvider; + } + + @Bean + CredentialsProvider credentialsProvider1() { + return mock(CredentialsProvider.class); + } + + } + + @Configuration(proxyBeanMethods = false) + static class MultipleCredentialsProvidersConfiguration { + + @Bean + CredentialsProvider credentialsProvider1() { + return mock(CredentialsProvider.class); + } + + @Bean + CredentialsProvider credentialsProvider2() { + return mock(CredentialsProvider.class); + } + + } + + @Configuration(proxyBeanMethods = false) + static class CredentialsRefreshServiceConfiguration { + + private static final CredentialsRefreshService credentialsRefreshService = mock( + CredentialsRefreshService.class); + + @Bean + CredentialsRefreshService credentialsRefreshService() { + return credentialsRefreshService; + } + + } + + @Configuration(proxyBeanMethods = false) + static class PrimaryCredentialsRefreshServiceConfiguration { + + private static final CredentialsRefreshService credentialsRefreshService = mock( + CredentialsRefreshService.class); + + @Bean + @Primary + CredentialsRefreshService credentialsRefreshService1() { + return credentialsRefreshService; + } + + @Bean + CredentialsRefreshService credentialsRefreshService2() { + return mock(CredentialsRefreshService.class); + } + + } + + @Configuration(proxyBeanMethods = false) + static class MultipleCredentialsRefreshServicesConfiguration { + + @Bean + CredentialsRefreshService credentialsRefreshService1() { + return mock(CredentialsRefreshService.class); + } + + @Bean + CredentialsRefreshService credentialsRefreshService2() { + return mock(CredentialsRefreshService.class); + } + + } + + @Configuration(proxyBeanMethods = false) + static class SaslConfigCustomizerConfiguration { + + @Bean + ConnectionFactoryCustomizer connectionFactoryCustomizer() { + return (connectionFactory) -> connectionFactory.setSaslConfig(new JDKSaslConfig(connectionFactory)); + } + + } + + @Configuration(proxyBeanMethods = false) + static class MultipleConnectionFactoryCustomizersConfiguration { + + @Bean + @Order(Ordered.LOWEST_PRECEDENCE) + ConnectionFactoryCustomizer secondCustomizer() { + return mock(ConnectionFactoryCustomizer.class); + } + + @Bean + @Order(0) + ConnectionFactoryCustomizer firstCustomizer() { + return mock(ConnectionFactoryCustomizer.class); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitPropertiesTests.java index 3a852a4b3a28..9dd47e9fce91 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitPropertiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import org.springframework.amqp.rabbit.config.DirectRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; -import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.listener.DirectMessageListenerContainer; import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; @@ -61,8 +60,8 @@ void determineHostReturnsHostPropertyWhenNoAddresses() { } @Test - void portDefaultsTo5672() { - assertThat(this.properties.getPort()).isEqualTo(5672); + void portDefaultsToNull() { + assertThat(this.properties.getPort()).isNull(); } @Test @@ -77,6 +76,17 @@ void determinePortReturnsPortOfFirstAddress() { assertThat(this.properties.determinePort()).isEqualTo(1234); } + @Test + void determinePortReturnsDefaultPortWhenNoAddresses() { + assertThat(this.properties.determinePort()).isEqualTo(5672); + } + + @Test + void determinePortWithSslReturnsDefaultSslPortWhenNoAddresses() { + this.properties.getSsl().setEnabled(true); + assertThat(this.properties.determinePort()).isEqualTo(5671); + } + @Test void determinePortReturnsPortPropertyWhenNoAddresses() { this.properties.setPort(1234); @@ -102,6 +112,14 @@ void determinePortUsingAmqpsReturnsPortOfFirstAddress() { assertThat(this.properties.determinePort()).isEqualTo(5671); } + @Test + void determinePortReturnsDefaultAmqpsPortWhenFirstAddressHasNoExplicitPortButSslEnabled() { + this.properties.getSsl().setEnabled(true); + this.properties.setPort(1234); + this.properties.setAddresses("rabbit1.example.com,rabbit2.example.com:2345"); + assertThat(this.properties.determinePort()).isEqualTo(5671); + } + @Test void virtualHostDefaultsToNull() { assertThat(this.properties.getVirtualHost()).isNull(); @@ -222,12 +240,37 @@ void customAddresses() { .isEqualTo("user:secret@rabbit1.example.com:1234/alpha,rabbit2.example.com"); } + @Test + void ipv6Address() { + this.properties.setAddresses("amqp://foo:bar@[aaaa:bbbb:cccc::d]:1234"); + assertThat(this.properties.determineHost()).isEqualTo("[aaaa:bbbb:cccc::d]"); + assertThat(this.properties.determinePort()).isEqualTo(1234); + } + + @Test + void ipv6AddressDefaultPort() { + this.properties.setAddresses("amqp://foo:bar@[aaaa:bbbb:cccc::d]"); + assertThat(this.properties.determineHost()).isEqualTo("[aaaa:bbbb:cccc::d]"); + assertThat(this.properties.determinePort()).isEqualTo(5672); + } + @Test void determineAddressesReturnsAddressesWithJustHostAndPort() { this.properties.setAddresses("user:secret@rabbit1.example.com:1234/alpha,rabbit2.example.com"); assertThat(this.properties.determineAddresses()).isEqualTo("rabbit1.example.com:1234,rabbit2.example.com:5672"); } + @Test + void determineAddressesUsesDefaultWhenNoAddressesSet() { + assertThat(this.properties.determineAddresses()).isEqualTo("localhost:5672"); + } + + @Test + void determineAddressesWithSslUsesDefaultWhenNoAddressesSet() { + this.properties.getSsl().setEnabled(true); + assertThat(this.properties.determineAddresses()).isEqualTo("localhost:5671"); + } + @Test void determineAddressesUsesHostAndPortPropertiesWhenNoAddressesSet() { this.properties.setHost("rabbit.example.com"); @@ -241,6 +284,20 @@ void determineSslUsingAmqpsReturnsStateOfFirstAddress() { assertThat(this.properties.getSsl().determineEnabled()).isTrue(); } + @Test + void sslDetermineEnabledIsTrueWhenAddressHasNoProtocolAndSslIsEnabled() { + this.properties.getSsl().setEnabled(true); + this.properties.setAddresses("root:password@otherhost"); + assertThat(this.properties.getSsl().determineEnabled()).isTrue(); + } + + @Test + void sslDetermineEnabledIsFalseWhenAddressHasNoProtocolAndSslIsDisabled() { + this.properties.getSsl().setEnabled(false); + this.properties.setAddresses("root:password@otherhost"); + assertThat(this.properties.getSsl().determineEnabled()).isFalse(); + } + @Test void determineSslUsingAmqpReturnsStateOfFirstAddress() { this.properties.setAddresses("amqp://root:password@otherhost,amqps://root:password2@otherhost2"); @@ -260,6 +317,8 @@ void simpleContainerUseConsistentDefaultValues() { RabbitProperties.SimpleContainer simple = this.properties.getListener().getSimple(); assertThat(simple.isAutoStartup()).isEqualTo(container.isAutoStartup()); assertThat(container).hasFieldOrPropertyWithValue("missingQueuesFatal", simple.isMissingQueuesFatal()); + assertThat(container).hasFieldOrPropertyWithValue("deBatchingEnabled", simple.isDeBatchingEnabled()); + assertThat(container).hasFieldOrPropertyWithValue("consumerBatchEnabled", simple.isConsumerBatchEnabled()); } @Test @@ -269,26 +328,7 @@ void directContainerUseConsistentDefaultValues() { RabbitProperties.DirectContainer direct = this.properties.getListener().getDirect(); assertThat(direct.isAutoStartup()).isEqualTo(container.isAutoStartup()); assertThat(container).hasFieldOrPropertyWithValue("missingQueuesFatal", direct.isMissingQueuesFatal()); - } - - @Test - @Deprecated - void isPublisherConfirmsShouldDefaultToFalse() { - assertThat(this.properties.isPublisherConfirms()).isEqualTo(false); - } - - @Test - @Deprecated - void isPublisherConfirmsWhenPublisherConfirmsTypeSimpleShouldBeFalse() { - this.properties.setPublisherConfirmType(CachingConnectionFactory.ConfirmType.SIMPLE); - assertThat(this.properties.isPublisherConfirms()).isEqualTo(false); - } - - @Test - @Deprecated - void isPublisherConfirmsWhenPublisherConfirmsTypeCorrelatedShouldBeTrue() { - this.properties.setPublisherConfirmType(CachingConnectionFactory.ConfirmType.CORRELATED); - assertThat(this.properties.isPublisherConfirms()).isEqualTo(true); + assertThat(container).hasFieldOrPropertyWithValue("deBatchingEnabled", direct.isDeBatchingEnabled()); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/aop/AopAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/aop/AopAutoConfigurationTests.java index 58a3e64d4621..18b69aa51f6b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/aop/AopAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/aop/AopAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,9 +18,12 @@ import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; +import org.aspectj.weaver.Advice; import org.junit.jupiter.api.Test; +import org.springframework.aop.support.AopUtils; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ContextConsumer; @@ -28,6 +31,9 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.context.annotation.Import; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.web.bind.annotation.RequestMapping; import static org.assertj.core.api.Assertions.assertThat; @@ -74,6 +80,17 @@ void aopWithDisabledProxyTargetClass() { @Test void customConfigurationWithProxyTargetClassDefaultDoesNotDisableProxying() { this.contextRunner.withUserConfiguration(CustomTestConfiguration.class).run(proxyTargetClassEnabled()); + + } + + @Test + void whenGlobalMethodSecurityIsEnabledAndAspectJIsNotAvailableThenClassProxyingIsStillUsedByDefault() { + this.contextRunner.withClassLoader(new FilteredClassLoader(Advice.class)) + .withUserConfiguration(ExampleController.class, EnableGlobalMethodSecurityConfiguration.class) + .run((context) -> { + ExampleController exampleController = context.getBean(ExampleController.class); + assertThat(AopUtils.isCglibProxy(exampleController)).isTrue(); + }); } private ContextConsumer proxyTargetClassEnabled() { @@ -149,4 +166,25 @@ interface TestInterface { } + @EnableGlobalMethodSecurity(prePostEnabled = true) + @Configuration(proxyBeanMethods = false) + static class EnableGlobalMethodSecurityConfiguration { + + } + + public static class ExampleController implements TestInterface { + + @RequestMapping("/test") + @PreAuthorize("true") + String demo() { + return "test"; + } + + @Override + public void foo() { + + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/aop/NonAspectJAopAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/aop/NonAspectJAopAutoConfigurationTests.java index 561b1a5b0b22..9c4c46ce108c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/aop/NonAspectJAopAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/aop/NonAspectJAopAutoConfigurationTests.java @@ -21,8 +21,6 @@ import org.springframework.aop.config.AopConfigUtils; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener; -import org.springframework.boot.logging.LogLevel; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.testsupport.classpath.ClassPathExclusions; @@ -41,12 +39,11 @@ class NonAspectJAopAutoConfigurationTests { @Test void whenAspectJIsAbsentAndProxyTargetClassIsEnabledProxyCreatorBeanIsDefined() { - this.contextRunner.withInitializer(new ConditionEvaluationReportLoggingListener(LogLevel.INFO)) - .run((context) -> { - BeanDefinition autoProxyCreator = context.getBeanFactory() - .getBeanDefinition(AopConfigUtils.AUTO_PROXY_CREATOR_BEAN_NAME); - assertThat(autoProxyCreator.getPropertyValues().get("proxyTargetClass")).isEqualTo(Boolean.TRUE); - }); + this.contextRunner.run((context) -> { + BeanDefinition autoProxyCreator = context.getBeanFactory() + .getBeanDefinition(AopConfigUtils.AUTO_PROXY_CREATOR_BEAN_NAME); + assertThat(autoProxyCreator.getPropertyValues().get("proxyTargetClass")).isEqualTo(Boolean.TRUE); + }); } @Test diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationTests.java index d80a77874458..0523c35a9888 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,30 +28,33 @@ import org.springframework.batch.core.Job; import org.springframework.batch.core.JobExecution; import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; import org.springframework.batch.core.Step; import org.springframework.batch.core.configuration.JobRegistry; import org.springframework.batch.core.configuration.annotation.BatchConfigurer; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; import org.springframework.batch.core.configuration.support.JobRegistryBeanPostProcessor; import org.springframework.batch.core.explore.JobExplorer; -import org.springframework.batch.core.explore.support.MapJobExplorerFactoryBean; import org.springframework.batch.core.job.AbstractJob; import org.springframework.batch.core.launch.JobLauncher; -import org.springframework.batch.core.launch.support.SimpleJobLauncher; import org.springframework.batch.core.repository.JobRepository; -import org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean; -import org.springframework.batch.support.transaction.ResourcelessTransactionManager; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.DefaultApplicationArguments; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage; +import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; +import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.test.City; import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.boot.jdbc.DataSourceInitializationMode; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; @@ -84,7 +87,7 @@ void testDefaultContext() { .run((context) -> { assertThat(context).hasSingleBean(JobLauncher.class); assertThat(context).hasSingleBean(JobExplorer.class); - assertThat(context.getBean(BatchProperties.class).getInitializeSchema()) + assertThat(context.getBean(BatchProperties.class).getJdbc().getInitializeSchema()) .isEqualTo(DataSourceInitializationMode.EMBEDDED); assertThat(new JdbcTemplate(context.getBean(DataSource.class)) .queryForList("select * from BATCH_JOB_EXECUTION")).isEmpty(); @@ -103,15 +106,6 @@ void whenThereIsAnEntityManagerFactoryButNoDataSourceAutoConfigurationBacksOff() .run((context) -> assertThat(context).doesNotHaveBean(BatchConfigurer.class)); } - @Test - void testCustomConfigurationWithNoDatabase() { - this.contextRunner.withUserConfiguration(TestCustomConfiguration.class).run((context) -> { - assertThat(context).hasSingleBean(JobLauncher.class); - JobExplorer explorer = context.getBean(JobExplorer.class); - assertThat(explorer.getJobInstances("job", 0, 100)).isEmpty(); - }); - } - @Test void testNoBatchConfiguration() { this.contextRunner.withUserConfiguration(EmptyConfiguration.class, EmbeddedDataSourceConfiguration.class) @@ -126,8 +120,25 @@ void testDefinesAndLaunchesJob() { this.contextRunner.withUserConfiguration(JobConfiguration.class, EmbeddedDataSourceConfiguration.class) .run((context) -> { assertThat(context).hasSingleBean(JobLauncher.class); - context.getBean(JobLauncherCommandLineRunner.class).run(); - assertThat(context.getBean(JobRepository.class).getLastJobExecution("job", new JobParameters())) + context.getBean(JobLauncherApplicationRunner.class) + .run(new DefaultApplicationArguments("jobParam=test")); + JobParameters jobParameters = new JobParametersBuilder().addString("jobParam", "test") + .toJobParameters(); + assertThat(context.getBean(JobRepository.class).getLastJobExecution("job", jobParameters)) + .isNotNull(); + }); + } + + @Test + void testDefinesAndLaunchesJobIgnoreOptionArguments() { + this.contextRunner.withUserConfiguration(JobConfiguration.class, EmbeddedDataSourceConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(JobLauncher.class); + context.getBean(JobLauncherApplicationRunner.class) + .run(new DefaultApplicationArguments("--spring.property=value", "jobParam=test")); + JobParameters jobParameters = new JobParametersBuilder().addString("jobParam", "test") + .toJobParameters(); + assertThat(context.getBean(JobRepository.class).getLastJobExecution("job", jobParameters)) .isNotNull(); }); } @@ -139,7 +150,7 @@ void testDefinesAndLaunchesNamedJob() { EmbeddedDataSourceConfiguration.class) .withPropertyValues("spring.batch.job.names:discreteRegisteredJob").run((context) -> { assertThat(context).hasSingleBean(JobLauncher.class); - context.getBean(JobLauncherCommandLineRunner.class).run(); + context.getBean(JobLauncherApplicationRunner.class).run(); assertThat(context.getBean(JobRepository.class).getLastJobExecution("discreteRegisteredJob", new JobParameters())).isNotNull(); }); @@ -151,7 +162,7 @@ void testDefinesAndLaunchesLocalJob() { .withUserConfiguration(NamedJobConfigurationWithLocalJob.class, EmbeddedDataSourceConfiguration.class) .withPropertyValues("spring.batch.job.names:discreteLocalJob").run((context) -> { assertThat(context).hasSingleBean(JobLauncher.class); - context.getBean(JobLauncherCommandLineRunner.class).run(); + context.getBean(JobLauncherApplicationRunner.class).run(); assertThat(context.getBean(JobRepository.class).getLastJobExecution("discreteLocalJob", new JobParameters())).isNotNull(); }); @@ -168,17 +179,30 @@ void testDisableLaunchesJob() { @Test void testDisableSchemaLoader() { + this.contextRunner.withUserConfiguration(TestConfiguration.class, EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.datasource.generate-unique-name=true", + "spring.batch.jdbc.initialize-schema:never") + .run(assertDatasourceIsNotInitialized()); + } + + @Test + @Deprecated + void testDisableSchemaLoaderWithDeprecatedProperty() { this.contextRunner.withUserConfiguration(TestConfiguration.class, EmbeddedDataSourceConfiguration.class) .withPropertyValues("spring.datasource.generate-unique-name=true", "spring.batch.initialize-schema:never") - .run((context) -> { - assertThat(context).hasSingleBean(JobLauncher.class); - assertThat(context.getBean(BatchProperties.class).getInitializeSchema()) - .isEqualTo(DataSourceInitializationMode.NEVER); - assertThatExceptionOfType(BadSqlGrammarException.class) - .isThrownBy(() -> new JdbcTemplate(context.getBean(DataSource.class)) - .queryForList("select * from BATCH_JOB_EXECUTION")); - }); + .run(assertDatasourceIsNotInitialized()); + } + + private ContextConsumer assertDatasourceIsNotInitialized() { + return (context) -> { + assertThat(context).hasSingleBean(JobLauncher.class); + assertThat(context.getBean(BatchProperties.class).getJdbc().getInitializeSchema()) + .isEqualTo(DataSourceInitializationMode.NEVER); + assertThatExceptionOfType(BadSqlGrammarException.class) + .isThrownBy(() -> new JdbcTemplate(context.getBean(DataSource.class)) + .queryForList("select * from BATCH_JOB_EXECUTION")); + }; } @Test @@ -203,19 +227,34 @@ void testRenamePrefix() { .withUserConfiguration(TestConfiguration.class, EmbeddedDataSourceConfiguration.class, HibernateJpaAutoConfiguration.class) .withPropertyValues("spring.datasource.generate-unique-name=true", - "spring.batch.schema:classpath:batch/custom-schema-hsql.sql", - "spring.batch.tablePrefix:PREFIX_") - .run((context) -> { - assertThat(context).hasSingleBean(JobLauncher.class); - assertThat(context.getBean(BatchProperties.class).getInitializeSchema()) - .isEqualTo(DataSourceInitializationMode.EMBEDDED); - assertThat(new JdbcTemplate(context.getBean(DataSource.class)) - .queryForList("select * from PREFIX_JOB_EXECUTION")).isEmpty(); - JobExplorer jobExplorer = context.getBean(JobExplorer.class); - assertThat(jobExplorer.findRunningJobExecutions("test")).isEmpty(); - JobRepository jobRepository = context.getBean(JobRepository.class); - assertThat(jobRepository.getLastJobExecution("test", new JobParameters())).isNull(); - }); + "spring.batch.jdbc.schema:classpath:batch/custom-schema.sql", + "spring.batch.jdbc.tablePrefix:PREFIX_") + .run(assertCustomTablePrefix()); + } + + @Test + @Deprecated + void testRenamePrefixWithDeprecatedProperty() { + this.contextRunner + .withUserConfiguration(TestConfiguration.class, EmbeddedDataSourceConfiguration.class, + HibernateJpaAutoConfiguration.class) + .withPropertyValues("spring.datasource.generate-unique-name=true", + "spring.batch.schema:classpath:batch/custom-schema.sql", "spring.batch.tablePrefix:PREFIX_") + .run(assertCustomTablePrefix()); + } + + private ContextConsumer assertCustomTablePrefix() { + return (context) -> { + assertThat(context).hasSingleBean(JobLauncher.class); + assertThat(context.getBean(BatchProperties.class).getJdbc().getInitializeSchema()) + .isEqualTo(DataSourceInitializationMode.EMBEDDED); + assertThat(new JdbcTemplate(context.getBean(DataSource.class)) + .queryForList("select * from PREFIX_JOB_EXECUTION")).isEmpty(); + JobExplorer jobExplorer = context.getBean(JobExplorer.class); + assertThat(jobExplorer.findRunningJobExecutions("test")).isEmpty(); + JobRepository jobRepository = context.getBean(JobRepository.class); + assertThat(jobRepository.getLastJobExecution("test", new JobParameters())).isNull(); + }; } @Test @@ -262,6 +301,50 @@ void testBatchDataSource() { }); } + @Test + void jobRepositoryBeansDependOnBatchDataSourceInitializer() { + this.contextRunner.withUserConfiguration(TestConfiguration.class, EmbeddedDataSourceConfiguration.class) + .run((context) -> { + ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); + String[] jobRepositoryNames = beanFactory.getBeanNamesForType(JobRepository.class); + assertThat(jobRepositoryNames).isNotEmpty(); + for (String jobRepositoryName : jobRepositoryNames) { + assertThat(beanFactory.getBeanDefinition(jobRepositoryName).getDependsOn()) + .contains("batchDataSourceInitializer"); + } + }); + } + + @Test + void jobRepositoryBeansDependOnFlyway() { + this.contextRunner.withUserConfiguration(TestConfiguration.class, EmbeddedDataSourceConfiguration.class) + .withUserConfiguration(FlywayAutoConfiguration.class) + .withPropertyValues("spring.batch.initialize-schema=never").run((context) -> { + ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); + String[] jobRepositoryNames = beanFactory.getBeanNamesForType(JobRepository.class); + assertThat(jobRepositoryNames).isNotEmpty(); + for (String jobRepositoryName : jobRepositoryNames) { + assertThat(beanFactory.getBeanDefinition(jobRepositoryName).getDependsOn()).contains("flyway", + "flywayInitializer"); + } + }); + } + + @Test + void jobRepositoryBeansDependOnLiquibase() { + this.contextRunner.withUserConfiguration(TestConfiguration.class, EmbeddedDataSourceConfiguration.class) + .withUserConfiguration(LiquibaseAutoConfiguration.class) + .withPropertyValues("spring.batch.initialize-schema=never").run((context) -> { + ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); + String[] jobRepositoryNames = beanFactory.getBeanNamesForType(JobRepository.class); + assertThat(jobRepositoryNames).isNotEmpty(); + for (String jobRepositoryName : jobRepositoryNames) { + assertThat(beanFactory.getBeanDefinition(jobRepositoryName).getDependsOn()) + .contains("liquibase"); + } + }); + } + @Configuration(proxyBeanMethods = false) protected static class BatchDataSourceConfiguration { @@ -300,44 +383,6 @@ EntityManagerFactory entityManagerFactory() { } - @EnableBatchProcessing - @TestAutoConfigurationPackage(City.class) - static class TestCustomConfiguration implements BatchConfigurer { - - private JobRepository jobRepository; - - private MapJobRepositoryFactoryBean factory = new MapJobRepositoryFactoryBean(); - - @Override - public JobRepository getJobRepository() throws Exception { - if (this.jobRepository == null) { - this.factory.afterPropertiesSet(); - this.jobRepository = this.factory.getObject(); - } - return this.jobRepository; - } - - @Override - public PlatformTransactionManager getTransactionManager() { - return new ResourcelessTransactionManager(); - } - - @Override - public JobLauncher getJobLauncher() { - SimpleJobLauncher launcher = new SimpleJobLauncher(); - launcher.setJobRepository(this.jobRepository); - return launcher; - } - - @Override - public JobExplorer getJobExplorer() throws Exception { - MapJobExplorerFactoryBean explorer = new MapJobExplorerFactoryBean(this.factory); - explorer.afterPropertiesSet(); - return explorer.getObject(); - } - - } - @Configuration(proxyBeanMethods = false) @EnableBatchProcessing static class NamedJobConfigurationWithRegisteredJob { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationWithoutJdbcTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationWithoutJdbcTests.java index 5784f4192ab9..3974c7e9093e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationWithoutJdbcTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationWithoutJdbcTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,7 +49,7 @@ class BatchAutoConfigurationWithoutJdbcTests { @Test void whenThereIsNoJdbcOnTheClasspathThenComponentsAreStillAutoConfigured() { this.contextRunner.run((context) -> { - assertThat(context).hasSingleBean(JobLauncherCommandLineRunner.class); + assertThat(context).hasSingleBean(JobLauncherApplicationRunner.class); assertThat(context).hasSingleBean(JobExecutionExitCodeGenerator.class); assertThat(context).hasSingleBean(SimpleJobOperator.class); }); @@ -60,22 +60,22 @@ void whenThereIsNoJdbcOnTheClasspathThenComponentsAreStillAutoConfigured() { static class BatchConfiguration implements BatchConfigurer { @Override - public JobRepository getJobRepository() throws Exception { + public JobRepository getJobRepository() { return mock(JobRepository.class); } @Override - public PlatformTransactionManager getTransactionManager() throws Exception { + public PlatformTransactionManager getTransactionManager() { return mock(PlatformTransactionManager.class); } @Override - public JobLauncher getJobLauncher() throws Exception { + public JobLauncher getJobLauncher() { return mock(JobLauncher.class); } @Override - public JobExplorer getJobExplorer() throws Exception { + public JobExplorer getJobExplorer() { return mock(JobExplorer.class); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationWithoutJpaTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationWithoutJpaTests.java index 218660dfc4dc..e32747ff0897 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationWithoutJpaTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationWithoutJpaTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,9 @@ import org.springframework.boot.autoconfigure.orm.jpa.test.City; import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration; import org.springframework.boot.jdbc.DataSourceInitializationMode; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.boot.testsupport.classpath.ClassPathExclusions; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.transaction.PlatformTransactionManager; @@ -59,7 +61,7 @@ void jdbcWithDefaultSettings() { assertThat(context).hasSingleBean(PlatformTransactionManager.class); assertThat(context.getBean(PlatformTransactionManager.class).toString()) .contains("DataSourceTransactionManager"); - assertThat(context.getBean(BatchProperties.class).getInitializeSchema()) + assertThat(context.getBean(BatchProperties.class).getJdbc().getInitializeSchema()) .isEqualTo(DataSourceInitializationMode.EMBEDDED); assertThat(new JdbcTemplate(context.getBean(DataSource.class)) .queryForList("select * from BATCH_JOB_EXECUTION")).isEmpty(); @@ -73,15 +75,27 @@ void jdbcWithDefaultSettings() { void jdbcWithCustomPrefix() { this.contextRunner.withUserConfiguration(DefaultConfiguration.class, EmbeddedDataSourceConfiguration.class) .withPropertyValues("spring.datasource.generate-unique-name=true", - "spring.batch.schema:classpath:batch/custom-schema-hsql.sql", - "spring.batch.tablePrefix:PREFIX_") - .run((context) -> { - assertThat(new JdbcTemplate(context.getBean(DataSource.class)) - .queryForList("select * from PREFIX_JOB_EXECUTION")).isEmpty(); - assertThat(context.getBean(JobExplorer.class).findRunningJobExecutions("test")).isEmpty(); - assertThat(context.getBean(JobRepository.class).getLastJobExecution("test", new JobParameters())) - .isNull(); - }); + "spring.batch.jdbc.schema:classpath:batch/custom-schema.sql", + "spring.batch.jdbc.tablePrefix:PREFIX_") + .run(assertCustomPrefix()); + } + + @Test + @Deprecated + void jdbcWithCustomPrefixWithDeprecatedProperties() { + this.contextRunner.withUserConfiguration(DefaultConfiguration.class, EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.datasource.generate-unique-name=true", + "spring.batch.schema:classpath:batch/custom-schema.sql", "spring.batch.tablePrefix:PREFIX_") + .run(assertCustomPrefix()); + } + + private ContextConsumer assertCustomPrefix() { + return (context) -> { + assertThat(new JdbcTemplate(context.getBean(DataSource.class)) + .queryForList("select * from PREFIX_JOB_EXECUTION")).isEmpty(); + assertThat(context.getBean(JobExplorer.class).findRunningJobExecutions("test")).isEmpty(); + assertThat(context.getBean(JobRepository.class).getLastJobExecution("test", new JobParameters())).isNull(); + }; } @EnableBatchProcessing diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchDataSourceInitializerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchDataSourceInitializerTests.java new file mode 100644 index 000000000000..dbb8ff501939 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchDataSourceInitializerTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.batch; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.DefaultResourceLoader; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link BatchDataSourceInitializer}. + * + * @author Stephane Nicoll + */ +class BatchDataSourceInitializerTests { + + @Test + void getDatabaseNameWithPlatformDoesNotTouchDataSource() { + DataSource dataSource = mock(DataSource.class); + BatchProperties properties = new BatchProperties(); + properties.getJdbc().setPlatform("test"); + BatchDataSourceInitializer initializer = new BatchDataSourceInitializer(dataSource, new DefaultResourceLoader(), + properties); + assertThat(initializer.getDatabaseName()).isEqualTo("test"); + then(dataSource).shouldHaveNoInteractions(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/JobLauncherApplicationRunnerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/JobLauncherApplicationRunnerTests.java new file mode 100644 index 000000000000..b80bedb45d6f --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/JobLauncherApplicationRunnerTests.java @@ -0,0 +1,235 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.batch; + +import java.util.List; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.Test; + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecutionException; +import org.springframework.batch.core.JobInstance; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; +import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; +import org.springframework.batch.core.explore.JobExplorer; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.job.builder.SimpleJobBuilder; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.core.launch.support.RunIdIncrementer; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.repository.JobRestartException; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration; +import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizers; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ResourceLoader; +import org.springframework.transaction.PlatformTransactionManager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.fail; + +/** + * Tests for {@link JobLauncherApplicationRunner}. + * + * @author Dave Syer + * @author Jean-Pierre Bergamin + * @author Mahmoud Ben Hassine + * @author Stephane Nicoll + */ +class JobLauncherApplicationRunnerTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of(DataSourceAutoConfiguration.class, TransactionAutoConfiguration.class)) + .withUserConfiguration(BatchConfiguration.class); + + @Test + void basicExecution() { + this.contextRunner.run((context) -> { + JobLauncherApplicationRunnerContext jobLauncherContext = new JobLauncherApplicationRunnerContext(context); + jobLauncherContext.executeJob(new JobParameters()); + assertThat(jobLauncherContext.jobInstances()).hasSize(1); + jobLauncherContext.executeJob(new JobParametersBuilder().addLong("id", 1L).toJobParameters()); + assertThat(jobLauncherContext.jobInstances()).hasSize(2); + }); + } + + @Test + void incrementExistingExecution() { + this.contextRunner.run((context) -> { + JobLauncherApplicationRunnerContext jobLauncherContext = new JobLauncherApplicationRunnerContext(context); + Job job = jobLauncherContext.configureJob().incrementer(new RunIdIncrementer()).build(); + jobLauncherContext.runner.execute(job, new JobParameters()); + jobLauncherContext.runner.execute(job, new JobParameters()); + assertThat(jobLauncherContext.jobInstances()).hasSize(2); + }); + } + + @Test + void retryFailedExecution() { + this.contextRunner.run((context) -> { + JobLauncherApplicationRunnerContext jobLauncherContext = new JobLauncherApplicationRunnerContext(context); + Job job = jobLauncherContext.jobBuilder() + .start(jobLauncherContext.stepBuilder().tasklet(throwingTasklet()).build()) + .incrementer(new RunIdIncrementer()).build(); + jobLauncherContext.runner.execute(job, new JobParameters()); + jobLauncherContext.runner.execute(job, new JobParametersBuilder().addLong("run.id", 1L).toJobParameters()); + assertThat(jobLauncherContext.jobInstances()).hasSize(1); + }); + } + + @Test + void runDifferentInstances() { + this.contextRunner.run((context) -> { + JobLauncherApplicationRunnerContext jobLauncherContext = new JobLauncherApplicationRunnerContext(context); + Job job = jobLauncherContext.jobBuilder() + .start(jobLauncherContext.stepBuilder().tasklet(throwingTasklet()).build()).build(); + // start a job instance + JobParameters jobParameters = new JobParametersBuilder().addString("name", "foo").toJobParameters(); + jobLauncherContext.runner.execute(job, jobParameters); + assertThat(jobLauncherContext.jobInstances()).hasSize(1); + // start a different job instance + JobParameters otherJobParameters = new JobParametersBuilder().addString("name", "bar").toJobParameters(); + jobLauncherContext.runner.execute(job, otherJobParameters); + assertThat(jobLauncherContext.jobInstances()).hasSize(2); + }); + } + + @Test + void retryFailedExecutionOnNonRestartableJob() { + this.contextRunner.run((context) -> { + JobLauncherApplicationRunnerContext jobLauncherContext = new JobLauncherApplicationRunnerContext(context); + Job job = jobLauncherContext.jobBuilder().preventRestart() + .start(jobLauncherContext.stepBuilder().tasklet(throwingTasklet()).build()) + .incrementer(new RunIdIncrementer()).build(); + jobLauncherContext.runner.execute(job, new JobParameters()); + jobLauncherContext.runner.execute(job, new JobParameters()); + // A failed job that is not restartable does not re-use the job params of + // the last execution, but creates a new job instance when running it again. + assertThat(jobLauncherContext.jobInstances()).hasSize(2); + assertThatExceptionOfType(JobRestartException.class).isThrownBy(() -> { + // try to re-run a failed execution + jobLauncherContext.runner.execute(job, + new JobParametersBuilder().addLong("run.id", 1L).toJobParameters()); + fail("expected JobRestartException"); + }).withMessageContaining("JobInstance already exists and is not restartable"); + }); + } + + @Test + void retryFailedExecutionWithNonIdentifyingParameters() { + this.contextRunner.run((context) -> { + JobLauncherApplicationRunnerContext jobLauncherContext = new JobLauncherApplicationRunnerContext(context); + Job job = jobLauncherContext.jobBuilder() + .start(jobLauncherContext.stepBuilder().tasklet(throwingTasklet()).build()) + .incrementer(new RunIdIncrementer()).build(); + JobParameters jobParameters = new JobParametersBuilder().addLong("id", 1L, false).addLong("foo", 2L, false) + .toJobParameters(); + jobLauncherContext.runner.execute(job, jobParameters); + assertThat(jobLauncherContext.jobInstances()).hasSize(1); + // try to re-run a failed execution with non identifying parameters + jobLauncherContext.runner.execute(job, + new JobParametersBuilder(jobParameters).addLong("run.id", 1L).toJobParameters()); + assertThat(jobLauncherContext.jobInstances()).hasSize(1); + }); + } + + private Tasklet throwingTasklet() { + return (contribution, chunkContext) -> { + throw new RuntimeException("Planned"); + }; + } + + static class JobLauncherApplicationRunnerContext { + + private final JobLauncherApplicationRunner runner; + + private final JobExplorer jobExplorer; + + private final JobBuilderFactory jobs; + + private final StepBuilderFactory steps; + + private final Job job; + + private final Step step; + + JobLauncherApplicationRunnerContext(ApplicationContext context) { + JobLauncher jobLauncher = context.getBean(JobLauncher.class); + JobRepository jobRepository = context.getBean(JobRepository.class); + this.jobs = new JobBuilderFactory(jobRepository); + this.steps = new StepBuilderFactory(jobRepository, context.getBean(PlatformTransactionManager.class)); + this.step = this.steps.get("step").tasklet((contribution, chunkContext) -> null).build(); + this.job = this.jobs.get("job").start(this.step).build(); + this.jobExplorer = context.getBean(JobExplorer.class); + this.runner = new JobLauncherApplicationRunner(jobLauncher, this.jobExplorer, jobRepository); + } + + List jobInstances() { + return this.jobExplorer.getJobInstances("job", 0, 100); + } + + void executeJob(JobParameters jobParameters) throws JobExecutionException { + this.runner.execute(this.job, jobParameters); + } + + JobBuilder jobBuilder() { + return this.jobs.get("job"); + } + + StepBuilder stepBuilder() { + return this.steps.get("step"); + } + + SimpleJobBuilder configureJob() { + return this.jobs.get("job").start(this.step); + } + + } + + @Configuration(proxyBeanMethods = false) + @EnableBatchProcessing + static class BatchConfiguration extends BasicBatchConfigurer { + + private final DataSource dataSource; + + protected BatchConfiguration(DataSource dataSource) { + super(new BatchProperties(), dataSource, new TransactionManagerCustomizers(null)); + this.dataSource = dataSource; + } + + @Bean + BatchDataSourceInitializer batchDataSourceInitializer(ResourceLoader resourceLoader) { + return new BatchDataSourceInitializer(this.dataSource, resourceLoader, new BatchProperties()); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/JobLauncherCommandLineRunnerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/JobLauncherCommandLineRunnerTests.java deleted file mode 100644 index e02e1c5eba73..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/JobLauncherCommandLineRunnerTests.java +++ /dev/null @@ -1,245 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.batch; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.springframework.batch.core.Job; -import org.springframework.batch.core.JobExecution; -import org.springframework.batch.core.JobInstance; -import org.springframework.batch.core.JobParameters; -import org.springframework.batch.core.JobParametersBuilder; -import org.springframework.batch.core.Step; -import org.springframework.batch.core.configuration.annotation.BatchConfigurer; -import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; -import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; -import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; -import org.springframework.batch.core.explore.JobExplorer; -import org.springframework.batch.core.explore.support.MapJobExplorerFactoryBean; -import org.springframework.batch.core.launch.JobLauncher; -import org.springframework.batch.core.launch.support.RunIdIncrementer; -import org.springframework.batch.core.launch.support.SimpleJobLauncher; -import org.springframework.batch.core.repository.JobRepository; -import org.springframework.batch.core.repository.JobRestartException; -import org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean; -import org.springframework.batch.core.step.tasklet.Tasklet; -import org.springframework.batch.support.transaction.ResourcelessTransactionManager; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.task.SyncTaskExecutor; -import org.springframework.transaction.PlatformTransactionManager; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.fail; - -/** - * Tests for {@link JobLauncherCommandLineRunner}. - * - * @author Dave Syer - * @author Jean-Pierre Bergamin - * @author Mahmoud Ben Hassine - */ -class JobLauncherCommandLineRunnerTests { - - private AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); - - private JobLauncherCommandLineRunner runner; - - private JobExplorer jobExplorer; - - private JobBuilderFactory jobs; - - private StepBuilderFactory steps; - - private Job job; - - private Step step; - - @BeforeEach - void init() { - this.context.register(BatchConfiguration.class); - this.context.refresh(); - JobRepository jobRepository = this.context.getBean(JobRepository.class); - JobLauncher jobLauncher = this.context.getBean(JobLauncher.class); - this.jobs = new JobBuilderFactory(jobRepository); - PlatformTransactionManager transactionManager = this.context.getBean(PlatformTransactionManager.class); - this.steps = new StepBuilderFactory(jobRepository, transactionManager); - Tasklet tasklet = (contribution, chunkContext) -> null; - this.step = this.steps.get("step").tasklet(tasklet).build(); - this.job = this.jobs.get("job").start(this.step).build(); - this.jobExplorer = this.context.getBean(JobExplorer.class); - this.runner = new JobLauncherCommandLineRunner(jobLauncher, this.jobExplorer, jobRepository); - this.context.getBean(BatchConfiguration.class).clear(); - } - - @AfterEach - void closeContext() { - this.context.close(); - } - - @Test - void basicExecution() throws Exception { - this.runner.execute(this.job, new JobParameters()); - assertThat(this.jobExplorer.getJobInstances("job", 0, 100)).hasSize(1); - this.runner.execute(this.job, new JobParametersBuilder().addLong("id", 1L).toJobParameters()); - assertThat(this.jobExplorer.getJobInstances("job", 0, 100)).hasSize(2); - } - - @Test - void incrementExistingExecution() throws Exception { - this.job = this.jobs.get("job").start(this.step).incrementer(new RunIdIncrementer()).build(); - this.runner.execute(this.job, new JobParameters()); - this.runner.execute(this.job, new JobParameters()); - assertThat(this.jobExplorer.getJobInstances("job", 0, 100)).hasSize(2); - } - - @Test - void retryFailedExecution() throws Exception { - this.job = this.jobs.get("job").start(this.steps.get("step").tasklet(throwingTasklet()).build()) - .incrementer(new RunIdIncrementer()).build(); - this.runner.execute(this.job, new JobParameters()); - this.runner.execute(this.job, new JobParametersBuilder().addLong("run.id", 1L).toJobParameters()); - assertThat(this.jobExplorer.getJobInstances("job", 0, 100)).hasSize(1); - } - - @Test - void runDifferentInstances() throws Exception { - this.job = this.jobs.get("job").start(this.steps.get("step").tasklet(throwingTasklet()).build()).build(); - // start a job instance - JobParameters jobParameters = new JobParametersBuilder().addString("name", "foo").toJobParameters(); - this.runner.execute(this.job, jobParameters); - assertThat(this.jobExplorer.getJobInstances("job", 0, 100)).hasSize(1); - // start a different job instance - JobParameters otherJobParameters = new JobParametersBuilder().addString("name", "bar").toJobParameters(); - this.runner.execute(this.job, otherJobParameters); - assertThat(this.jobExplorer.getJobInstances("job", 0, 100)).hasSize(2); - } - - @Test - void retryFailedExecutionOnNonRestartableJob() throws Exception { - this.job = this.jobs.get("job").preventRestart() - .start(this.steps.get("step").tasklet(throwingTasklet()).build()).incrementer(new RunIdIncrementer()) - .build(); - this.runner.execute(this.job, new JobParameters()); - this.runner.execute(this.job, new JobParameters()); - // A failed job that is not restartable does not re-use the job params of - // the last execution, but creates a new job instance when running it again. - assertThat(this.jobExplorer.getJobInstances("job", 0, 100)).hasSize(2); - assertThatExceptionOfType(JobRestartException.class).isThrownBy(() -> { - // try to re-run a failed execution - this.runner.execute(this.job, new JobParametersBuilder().addLong("run.id", 1L).toJobParameters()); - fail("expected JobRestartException"); - }).withMessageContaining("JobInstance already exists and is not restartable"); - } - - @Test - void retryFailedExecutionWithNonIdentifyingParameters() throws Exception { - this.job = this.jobs.get("job").start(this.steps.get("step").tasklet(throwingTasklet()).build()) - .incrementer(new RunIdIncrementer()).build(); - JobParameters jobParameters = new JobParametersBuilder().addLong("id", 1L, false).addLong("foo", 2L, false) - .toJobParameters(); - this.runner.execute(this.job, jobParameters); - assertThat(this.jobExplorer.getJobInstances("job", 0, 100)).hasSize(1); - // try to re-run a failed execution with non identifying parameters - this.runner.execute(this.job, new JobParametersBuilder(jobParameters).addLong("run.id", 1L).toJobParameters()); - assertThat(this.jobExplorer.getJobInstances("job", 0, 100)).hasSize(1); - } - - @Test - void retryFailedExecutionWithDifferentNonIdentifyingParametersFromPreviousExecution() throws Exception { - this.job = this.jobs.get("job").start(this.steps.get("step").tasklet(throwingTasklet()).build()) - .incrementer(new RunIdIncrementer()).build(); - JobParameters jobParameters = new JobParametersBuilder().addLong("id", 1L, false).addLong("foo", 2L, false) - .toJobParameters(); - this.runner.execute(this.job, jobParameters); - assertThat(this.jobExplorer.getJobInstances("job", 0, 100)).hasSize(1); - // try to re-run a failed execution with non identifying parameters - this.runner.execute(this.job, new JobParametersBuilder().addLong("run.id", 1L).addLong("id", 2L, false) - .addLong("foo", 3L, false).toJobParameters()); - assertThat(this.jobExplorer.getJobInstances("job", 0, 100)).hasSize(1); - JobInstance jobInstance = this.jobExplorer.getJobInstance(0L); - assertThat(this.jobExplorer.getJobExecutions(jobInstance)).hasSize(2); - // first execution - JobExecution firstJobExecution = this.jobExplorer.getJobExecution(0L); - JobParameters parameters = firstJobExecution.getJobParameters(); - assertThat(parameters.getLong("run.id")).isEqualTo(1L); - assertThat(parameters.getLong("id")).isEqualTo(1L); - assertThat(parameters.getLong("foo")).isEqualTo(2L); - // second execution - JobExecution secondJobExecution = this.jobExplorer.getJobExecution(1L); - parameters = secondJobExecution.getJobParameters(); - // identifying parameters should be the same as previous execution - assertThat(parameters.getLong("run.id")).isEqualTo(1L); - // non-identifying parameters should be the newly specified ones - assertThat(parameters.getLong("id")).isEqualTo(2L); - assertThat(parameters.getLong("foo")).isEqualTo(3L); - } - - private Tasklet throwingTasklet() { - return (contribution, chunkContext) -> { - throw new RuntimeException("Planned"); - }; - } - - @Configuration(proxyBeanMethods = false) - @EnableBatchProcessing - static class BatchConfiguration implements BatchConfigurer { - - private ResourcelessTransactionManager transactionManager = new ResourcelessTransactionManager(); - - private JobRepository jobRepository; - - private MapJobRepositoryFactoryBean jobRepositoryFactory = new MapJobRepositoryFactoryBean( - this.transactionManager); - - BatchConfiguration() throws Exception { - this.jobRepository = this.jobRepositoryFactory.getObject(); - } - - void clear() { - this.jobRepositoryFactory.clear(); - } - - @Override - public JobRepository getJobRepository() { - return this.jobRepository; - } - - @Override - public PlatformTransactionManager getTransactionManager() { - return this.transactionManager; - } - - @Override - public JobLauncher getJobLauncher() { - SimpleJobLauncher launcher = new SimpleJobLauncher(); - launcher.setJobRepository(this.jobRepository); - launcher.setTaskExecutor(new SyncTaskExecutor()); - return launcher; - } - - @Override - public JobExplorer getJobExplorer() throws Exception { - return new MapJobExplorerFactoryBean(this.jobRepositoryFactory).getObject(); - } - - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/AbstractCacheAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/AbstractCacheAutoConfigurationTests.java index a7cab890b159..b6f85a57184b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/AbstractCacheAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/AbstractCacheAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,6 @@ import java.util.List; import java.util.Map; -import com.couchbase.client.spring.cache.CouchbaseCacheManager; import com.hazelcast.spring.cache.HazelcastCacheManager; import org.infinispan.spring.embedded.provider.SpringEmbeddedCacheManager; @@ -36,6 +35,7 @@ import org.springframework.cache.support.SimpleCacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.couchbase.cache.CouchbaseCacheManager; import org.springframework.data.redis.cache.RedisCacheManager; import static org.assertj.core.api.Assertions.assertThat; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/CacheAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/CacheAutoConfigurationTests.java index d3a3a984b44a..a7740adcb145 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/CacheAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/CacheAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,13 +26,9 @@ import javax.cache.expiry.CreatedExpiryPolicy; import javax.cache.expiry.Duration; -import com.couchbase.client.java.Bucket; -import com.couchbase.client.java.bucket.BucketManager; -import com.couchbase.client.spring.cache.CouchbaseCache; -import com.couchbase.client.spring.cache.CouchbaseCacheManager; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.CaffeineSpec; -import com.hazelcast.cache.HazelcastCachingProvider; +import com.hazelcast.cache.impl.HazelcastServerCachingProvider; import com.hazelcast.core.Hazelcast; import com.hazelcast.core.HazelcastInstance; import com.hazelcast.spring.cache.HazelcastCacheManager; @@ -46,12 +42,13 @@ import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.cache.support.MockCachingProvider; +import org.springframework.boot.autoconfigure.cache.support.MockCachingProvider.MockCacheManager; import org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.testsupport.classpath.ClassPathExclusions; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; -import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.CachingConfigurer; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.caffeine.CaffeineCache; import org.springframework.cache.caffeine.CaffeineCacheManager; @@ -66,6 +63,10 @@ import org.springframework.context.annotation.Import; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; +import org.springframework.data.couchbase.CouchbaseClientFactory; +import org.springframework.data.couchbase.cache.CouchbaseCache; +import org.springframework.data.couchbase.cache.CouchbaseCacheConfiguration; +import org.springframework.data.couchbase.cache.CouchbaseCacheManager; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; @@ -73,9 +74,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; /** * Tests for {@link CacheAutoConfiguration}. @@ -196,7 +197,7 @@ void genericCacheExplicitWithCaches() { @Test void couchbaseCacheExplicit() { - this.contextRunner.withUserConfiguration(CouchbaseCacheConfiguration.class) + this.contextRunner.withUserConfiguration(CouchbaseConfiguration.class) .withPropertyValues("spring.cache.type=couchbase").run((context) -> { CouchbaseCacheManager cacheManager = getCacheManager(context, CouchbaseCacheManager.class); assertThat(cacheManager.getCacheNames()).isEmpty(); @@ -205,14 +206,14 @@ void couchbaseCacheExplicit() { @Test void couchbaseCacheWithCustomizers() { - this.contextRunner.withUserConfiguration(CouchbaseCacheAndCustomizersConfiguration.class) + this.contextRunner.withUserConfiguration(CouchbaseWithCustomizersConfiguration.class) .withPropertyValues("spring.cache.type=couchbase") .run(verifyCustomizers("allCacheManagerCustomizer", "couchbaseCacheManagerCustomizer")); } @Test void couchbaseCacheExplicitWithCaches() { - this.contextRunner.withUserConfiguration(CouchbaseCacheConfiguration.class) + this.contextRunner.withUserConfiguration(CouchbaseConfiguration.class) .withPropertyValues("spring.cache.type=couchbase", "spring.cache.cacheNames[0]=foo", "spring.cache.cacheNames[1]=bar") .run((context) -> { @@ -220,14 +221,13 @@ void couchbaseCacheExplicitWithCaches() { assertThat(cacheManager.getCacheNames()).containsOnly("foo", "bar"); Cache cache = cacheManager.getCache("foo"); assertThat(cache).isInstanceOf(CouchbaseCache.class); - assertThat(((CouchbaseCache) cache).getTtl()).isEqualTo(0); - assertThat(((CouchbaseCache) cache).getNativeCache()).isEqualTo(context.getBean("bucket")); + assertThat(((CouchbaseCache) cache).getCacheConfiguration().getExpiry()).hasSeconds(0); }); } @Test void couchbaseCacheExplicitWithTtl() { - this.contextRunner.withUserConfiguration(CouchbaseCacheConfiguration.class) + this.contextRunner.withUserConfiguration(CouchbaseConfiguration.class) .withPropertyValues("spring.cache.type=couchbase", "spring.cache.cacheNames=foo,bar", "spring.cache.couchbase.expiration=2000") .run((context) -> { @@ -235,8 +235,21 @@ void couchbaseCacheExplicitWithTtl() { assertThat(cacheManager.getCacheNames()).containsOnly("foo", "bar"); Cache cache = cacheManager.getCache("foo"); assertThat(cache).isInstanceOf(CouchbaseCache.class); - assertThat(((CouchbaseCache) cache).getTtl()).isEqualTo(2); - assertThat(((CouchbaseCache) cache).getNativeCache()).isEqualTo(context.getBean("bucket")); + assertThat(((CouchbaseCache) cache).getCacheConfiguration().getExpiry()).hasSeconds(2); + }); + } + + @Test + void couchbaseCacheWithCouchbaseCacheManagerBuilderCustomizer() { + this.contextRunner.withUserConfiguration(CouchbaseConfiguration.class) + .withPropertyValues("spring.cache.type=couchbase", "spring.cache.couchbase.expiration=15s") + .withBean(CouchbaseCacheManagerBuilderCustomizer.class, () -> (builder) -> builder.cacheDefaults( + CouchbaseCacheConfiguration.defaultCacheConfig().entryExpiry(java.time.Duration.ofSeconds(10)))) + .run((context) -> { + CouchbaseCacheManager cacheManager = getCacheManager(context, CouchbaseCacheManager.class); + CouchbaseCacheConfiguration couchbaseCacheConfiguration = getDefaultCouchbaseCacheConfiguration( + cacheManager); + assertThat(couchbaseCacheConfiguration.getExpiry()).isEqualTo(java.time.Duration.ofSeconds(10)); }); } @@ -252,7 +265,7 @@ void redisCacheExplicit() { RedisCacheConfiguration redisCacheConfiguration = getDefaultRedisCacheConfiguration(cacheManager); assertThat(redisCacheConfiguration.getTtl()).isEqualTo(java.time.Duration.ofSeconds(15)); assertThat(redisCacheConfiguration.getAllowCacheNullValues()).isFalse(); - assertThat(redisCacheConfiguration.getKeyPrefixFor("keyName")).isEqualTo("prefix"); + assertThat(redisCacheConfiguration.getKeyPrefixFor("MyCache")).isEqualTo("prefixMyCache::"); assertThat(redisCacheConfiguration.usePrefix()).isTrue(); }); } @@ -267,7 +280,7 @@ void redisCacheWithRedisCacheConfiguration() { assertThat(cacheManager.getCacheNames()).isEmpty(); RedisCacheConfiguration redisCacheConfiguration = getDefaultRedisCacheConfiguration(cacheManager); assertThat(redisCacheConfiguration.getTtl()).isEqualTo(java.time.Duration.ofSeconds(30)); - assertThat(redisCacheConfiguration.getKeyPrefixFor("")).isEqualTo("bar"); + assertThat(redisCacheConfiguration.getKeyPrefixFor("")).isEqualTo("bar::"); }); } @@ -357,8 +370,9 @@ void jCacheCacheWithCachesAndCustomConfig() { assertThat(cacheManager.getCacheNames()).containsOnly("one", "two"); CompleteConfiguration defaultCacheConfiguration = context .getBean(CompleteConfiguration.class); - verify(cacheManager.getCacheManager()).createCache("one", defaultCacheConfiguration); - verify(cacheManager.getCacheManager()).createCache("two", defaultCacheConfiguration); + MockCacheManager mockCacheManager = (MockCacheManager) cacheManager.getCacheManager(); + assertThat(mockCacheManager.getConfigurations()).containsEntry("one", defaultCacheConfiguration) + .containsEntry("two", defaultCacheConfiguration); }); } @@ -467,7 +481,7 @@ void hazelcastCacheWithHazelcastAutoConfiguration() { @Test void hazelcastAsJCacheWithCaches() { - String cachingProviderFqn = HazelcastCachingProvider.class.getName(); + String cachingProviderFqn = HazelcastServerCachingProvider.class.getName(); try { this.contextRunner.withUserConfiguration(DefaultCacheConfiguration.class) .withPropertyValues("spring.cache.type=jcache", @@ -486,7 +500,7 @@ void hazelcastAsJCacheWithCaches() { @Test void hazelcastAsJCacheWithConfig() { - String cachingProviderFqn = HazelcastCachingProvider.class.getName(); + String cachingProviderFqn = HazelcastServerCachingProvider.class.getName(); try { String configLocation = "org/springframework/boot/autoconfigure/hazelcast/hazelcast-specific.xml"; this.contextRunner.withUserConfiguration(DefaultCacheConfiguration.class) @@ -507,7 +521,7 @@ void hazelcastAsJCacheWithConfig() { @Test void hazelcastAsJCacheWithExistingHazelcastInstance() { - String cachingProviderFqn = HazelcastCachingProvider.class.getName(); + String cachingProviderFqn = HazelcastServerCachingProvider.class.getName(); this.contextRunner.withConfiguration(AutoConfigurations.of(HazelcastAutoConfiguration.class)) .withUserConfiguration(DefaultCacheConfiguration.class) .withPropertyValues("spring.cache.type=jcache", "spring.cache.jcache.provider=" + cachingProviderFqn) @@ -559,7 +573,7 @@ void infinispanCacheWithCachesAndCustomConfig() { .run((context) -> { assertThat(getCacheManager(context, SpringEmbeddedCacheManager.class).getCacheNames()) .containsOnly("foo", "bar"); - verify(context.getBean(ConfigurationBuilder.class), times(2)).build(); + then(context.getBean(ConfigurationBuilder.class)).should(times(2)).build(); }); } @@ -591,19 +605,19 @@ void infinispanAsJCacheWithConfig() { @Test void jCacheCacheWithCachesAndCustomizer() { - String cachingProviderClassName = HazelcastCachingProvider.class.getName(); + String cachingProviderFqn = HazelcastServerCachingProvider.class.getName(); try { this.contextRunner.withUserConfiguration(JCacheWithCustomizerConfiguration.class) .withPropertyValues("spring.cache.type=jcache", - "spring.cache.jcache.provider=" + cachingProviderClassName, - "spring.cache.cacheNames[0]=foo", "spring.cache.cacheNames[1]=bar") + "spring.cache.jcache.provider=" + cachingProviderFqn, "spring.cache.cacheNames[0]=foo", + "spring.cache.cacheNames[1]=bar") .run((context) -> // see customizer assertThat(getCacheManager(context, JCacheCacheManager.class).getCacheNames()).containsOnly("foo", "custom1")); } finally { - Caching.getCachingProvider(cachingProviderClassName).close(); + Caching.getCachingProvider(cachingProviderFqn).close(); } } @@ -669,6 +683,10 @@ private void validateCaffeineCacheWithStats(AssertableApplicationContext context assertThat(((CaffeineCache) foo).getNativeCache().stats().missCount()).isEqualTo(1L); } + private CouchbaseCacheConfiguration getDefaultCouchbaseCacheConfiguration(CouchbaseCacheManager cacheManager) { + return (CouchbaseCacheConfiguration) ReflectionTestUtils.getField(cacheManager, "defaultCacheConfig"); + } + private RedisCacheConfiguration getDefaultRedisCacheConfiguration(RedisCacheManager cacheManager) { return (RedisCacheConfiguration) ReflectionTestUtils.getField(cacheManager, "defaultCacheConfig"); } @@ -722,21 +740,18 @@ static class HazelcastCacheAndCustomizersConfiguration { @Configuration(proxyBeanMethods = false) @EnableCaching - static class CouchbaseCacheConfiguration { + static class CouchbaseConfiguration { @Bean - Bucket bucket() { - BucketManager bucketManager = mock(BucketManager.class); - Bucket bucket = mock(Bucket.class); - given(bucket.bucketManager()).willReturn(bucketManager); - return bucket; + CouchbaseClientFactory couchbaseClientFactory() { + return mock(CouchbaseClientFactory.class); } } @Configuration(proxyBeanMethods = false) - @Import({ CouchbaseCacheConfiguration.class, CacheManagerCustomizersConfiguration.class }) - static class CouchbaseCacheAndCustomizersConfiguration { + @Import({ CouchbaseConfiguration.class, CacheManagerCustomizersConfiguration.class }) + static class CouchbaseWithCustomizersConfiguration { } @@ -758,7 +773,7 @@ static class RedisWithCacheConfigurationConfiguration { @Bean org.springframework.data.redis.cache.RedisCacheConfiguration customRedisCacheConfiguration() { return org.springframework.data.redis.cache.RedisCacheConfiguration.defaultCacheConfig() - .entryTtl(java.time.Duration.ofSeconds(30)).prefixKeysWith("bar"); + .entryTtl(java.time.Duration.ofSeconds(30)).prefixCacheNameWith("bar"); } } @@ -873,7 +888,7 @@ CacheManager cacheManager() { @Configuration(proxyBeanMethods = false) @EnableCaching - static class CustomCacheManagerFromSupportConfiguration extends CachingConfigurerSupport { + static class CustomCacheManagerFromSupportConfiguration implements CachingConfigurer { @Override @Bean @@ -886,7 +901,7 @@ public CacheManager cacheManager() { @Configuration(proxyBeanMethods = false) @EnableCaching - static class CustomCacheResolverFromSupportConfiguration extends CachingConfigurerSupport { + static class CustomCacheResolverFromSupportConfiguration implements CachingConfigurer { @Override @Bean diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/support/MockCachingProvider.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/support/MockCachingProvider.java index 9c6380355f7b..962eef94e75d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/support/MockCachingProvider.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/support/MockCachingProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,8 +27,6 @@ import javax.cache.configuration.OptionalFeature; import javax.cache.spi.CachingProvider; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -41,25 +39,8 @@ public class MockCachingProvider implements CachingProvider { @Override - @SuppressWarnings("rawtypes") public CacheManager getCacheManager(URI uri, ClassLoader classLoader, Properties properties) { - CacheManager cacheManager = mock(CacheManager.class); - given(cacheManager.getURI()).willReturn(uri); - given(cacheManager.getClassLoader()).willReturn(classLoader); - final Map caches = new HashMap<>(); - given(cacheManager.getCacheNames()).willReturn(caches.keySet()); - given(cacheManager.getCache(anyString())).willAnswer((invocation) -> { - String cacheName = invocation.getArgument(0); - return caches.get(cacheName); - }); - given(cacheManager.createCache(anyString(), any(Configuration.class))).will((invocation) -> { - String cacheName = invocation.getArgument(0); - Cache cache = mock(Cache.class); - given(cache.getName()).willReturn(cacheName); - caches.put(cacheName, cache); - return cache; - }); - return cacheManager; + return new MockCacheManager(uri, classLoader, properties); } @Override @@ -104,4 +85,107 @@ public boolean isSupported(OptionalFeature optionalFeature) { return false; } + public static class MockCacheManager implements CacheManager { + + private final Map> configurations = new HashMap<>(); + + private final Map> caches = new HashMap<>(); + + private final URI uri; + + private final ClassLoader classLoader; + + private final Properties properties; + + private boolean closed; + + public MockCacheManager(URI uri, ClassLoader classLoader, Properties properties) { + this.uri = uri; + this.classLoader = classLoader; + this.properties = properties; + } + + @Override + public CachingProvider getCachingProvider() { + throw new UnsupportedOperationException(); + } + + @Override + public URI getURI() { + return this.uri; + } + + @Override + public ClassLoader getClassLoader() { + return this.classLoader; + } + + @Override + public Properties getProperties() { + return this.properties; + } + + @Override + @SuppressWarnings("unchecked") + public > Cache createCache(String cacheName, C configuration) { + this.configurations.put(cacheName, configuration); + Cache cache = mock(Cache.class); + given(cache.getName()).willReturn(cacheName); + this.caches.put(cacheName, cache); + return cache; + } + + @Override + @SuppressWarnings("unchecked") + public Cache getCache(String cacheName, Class keyType, Class valueType) { + return (Cache) this.caches.get(cacheName); + } + + @Override + @SuppressWarnings("unchecked") + public Cache getCache(String cacheName) { + return (Cache) this.caches.get(cacheName); + } + + @Override + public Iterable getCacheNames() { + return this.caches.keySet(); + } + + @Override + public void destroyCache(String cacheName) { + this.caches.remove(cacheName); + } + + @Override + public void enableManagement(String cacheName, boolean enabled) { + throw new UnsupportedOperationException(); + } + + @Override + public void enableStatistics(String cacheName, boolean enabled) { + throw new UnsupportedOperationException(); + } + + @Override + public void close() { + this.closed = true; + } + + @Override + public boolean isClosed() { + return this.closed; + } + + @Override + public T unwrap(Class clazz) { + throw new UnsupportedOperationException(); + } + + public Map> getConfigurations() { + return this.configurations; + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfigurationIntegrationTests.java new file mode 100644 index 000000000000..198289618f9e --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfigurationIntegrationTests.java @@ -0,0 +1,90 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.cassandra; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.config.DriverConfigLoader; +import org.junit.jupiter.api.Test; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.testsupport.testcontainers.CassandraContainer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.spy; + +/** + * Integration tests for {@link CassandraAutoConfiguration}. + * + * @author Andy Wilkinson + */ +@Testcontainers(disabledWithoutDocker = true) +class CassandraAutoConfigurationIntegrationTests { + + @Container + static final CassandraContainer cassandra = new CassandraContainer(); + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(CassandraAutoConfiguration.class)).withPropertyValues( + "spring.data.cassandra.contact-points:" + cassandra.getHost() + ":" + + cassandra.getFirstMappedPort(), + "spring.data.cassandra.local-datacenter=datacenter1", + "spring.data.cassandra.connection.connect-timeout=60s", + "spring.data.cassandra.connection.init-query-timeout=60s", + "spring.data.cassandra.request.timeout=60s"); + + @Test + void whenTheContextIsClosedThenTheDriverConfigLoaderIsClosed() { + this.contextRunner.withUserConfiguration(DriverConfigLoaderSpyConfiguration.class).run((context) -> { + assertThat(((BeanDefinitionRegistry) context.getSourceApplicationContext()) + .getBeanDefinition("cassandraDriverConfigLoader").getDestroyMethodName()).isEmpty(); + // Initialize lazy bean + context.getBean(CqlSession.class); + DriverConfigLoader driverConfigLoader = context.getBean(DriverConfigLoader.class); + context.close(); + then(driverConfigLoader).should().close(); + }); + } + + @Configuration(proxyBeanMethods = false) + static class DriverConfigLoaderSpyConfiguration { + + @Bean + static BeanPostProcessor driverConfigLoaderSpy() { + return new BeanPostProcessor() { + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + if (bean instanceof DriverConfigLoader) { + return spy(bean); + } + return bean; + } + + }; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfigurationTests.java index f2d9362ffe34..ed05b182aa52 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,18 @@ package org.springframework.boot.autoconfigure.cassandra; -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.Cluster.Initializer; -import com.datastax.driver.core.PoolingOptions; +import java.time.Duration; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.CqlSessionBuilder; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.config.DriverConfigLoader; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.internal.core.session.throttling.ConcurrencyLimitingRequestThrottler; +import com.datastax.oss.driver.internal.core.session.throttling.PassThroughRequestThrottler; +import com.datastax.oss.driver.internal.core.session.throttling.RateLimitingRequestThrottler; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -27,7 +36,7 @@ import org.springframework.context.annotation.Configuration; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Tests for {@link CassandraAutoConfiguration} @@ -41,110 +50,218 @@ class CassandraAutoConfigurationTests { .withConfiguration(AutoConfigurations.of(CassandraAutoConfiguration.class)); @Test - void createClusterWithDefault() { + void cqlSessionBuildHasScopePrototype() { this.contextRunner.run((context) -> { - assertThat(context).hasSingleBean(Cluster.class); - assertThat(context.getBean(Cluster.class).getClusterName()).startsWith("cluster"); + CqlIdentifier keyspace = CqlIdentifier.fromCql("test"); + CqlSessionBuilder firstBuilder = context.getBean(CqlSessionBuilder.class); + assertThat(firstBuilder.withKeyspace(keyspace)).hasFieldOrPropertyWithValue("keyspace", keyspace); + CqlSessionBuilder secondBuilder = context.getBean(CqlSessionBuilder.class); + assertThat(secondBuilder).hasFieldOrPropertyWithValue("keyspace", null); }); } @Test - void createClusterWithOverrides() { - this.contextRunner.withPropertyValues("spring.data.cassandra.cluster-name=testcluster").run((context) -> { - assertThat(context).hasSingleBean(Cluster.class); - assertThat(context.getBean(Cluster.class).getClusterName()).isEqualTo("testcluster"); + void driverConfigLoaderWithDefaultConfiguration() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(DriverConfigLoader.class); + assertThat(context.getBean(DriverConfigLoader.class).getInitialConfig().getDefaultProfile() + .isDefined(DefaultDriverOption.SESSION_NAME)).isFalse(); }); } @Test - void createCustomizeCluster() { - this.contextRunner.withUserConfiguration(MockCustomizerConfig.class).run((context) -> { - assertThat(context).hasSingleBean(Cluster.class); - assertThat(context).hasSingleBean(ClusterBuilderCustomizer.class); - }); + void driverConfigLoaderWithContactPoints() { + this.contextRunner.withPropertyValues("spring.data.cassandra.contact-points=cluster.example.com:9042", + "spring.data.cassandra.local-datacenter=cassandra-eu1").run((context) -> { + assertThat(context).hasSingleBean(DriverConfigLoader.class); + DriverExecutionProfile configuration = context.getBean(DriverConfigLoader.class).getInitialConfig() + .getDefaultProfile(); + assertThat(configuration.getStringList(DefaultDriverOption.CONTACT_POINTS)) + .containsOnly("cluster.example.com:9042"); + assertThat(configuration.getString(DefaultDriverOption.LOAD_BALANCING_LOCAL_DATACENTER)) + .isEqualTo("cassandra-eu1"); + }); } @Test - void customizerOverridesAutoConfig() { - this.contextRunner.withUserConfiguration(SimpleCustomizerConfig.class) - .withPropertyValues("spring.data.cassandra.cluster-name=testcluster").run((context) -> { - assertThat(context).hasSingleBean(Cluster.class); - assertThat(context.getBean(Cluster.class).getClusterName()).isEqualTo("overridden-name"); + void driverConfigLoaderWithContactPointAndNoPort() { + this.contextRunner + .withPropertyValues("spring.data.cassandra.contact-points=cluster.example.com,another.example.com:9041", + "spring.data.cassandra.local-datacenter=cassandra-eu1") + .run((context) -> { + assertThat(context).hasSingleBean(DriverConfigLoader.class); + DriverExecutionProfile configuration = context.getBean(DriverConfigLoader.class).getInitialConfig() + .getDefaultProfile(); + assertThat(configuration.getStringList(DefaultDriverOption.CONTACT_POINTS)) + .containsOnly("cluster.example.com:9042", "another.example.com:9041"); + assertThat(configuration.getString(DefaultDriverOption.LOAD_BALANCING_LOCAL_DATACENTER)) + .isEqualTo("cassandra-eu1"); }); } @Test - void defaultPoolOptions() { - this.contextRunner.run((context) -> { - assertThat(context).hasSingleBean(Cluster.class); - PoolingOptions poolingOptions = context.getBean(Cluster.class).getConfiguration().getPoolingOptions(); - assertThat(poolingOptions.getIdleTimeoutSeconds()).isEqualTo(PoolingOptions.DEFAULT_IDLE_TIMEOUT_SECONDS); - assertThat(poolingOptions.getPoolTimeoutMillis()).isEqualTo(PoolingOptions.DEFAULT_POOL_TIMEOUT_MILLIS); - assertThat(poolingOptions.getHeartbeatIntervalSeconds()) - .isEqualTo(PoolingOptions.DEFAULT_HEARTBEAT_INTERVAL_SECONDS); - assertThat(poolingOptions.getMaxQueueSize()).isEqualTo(PoolingOptions.DEFAULT_MAX_QUEUE_SIZE); + void driverConfigLoaderWithContactPointAndNoPortAndCustomPort() { + this.contextRunner + .withPropertyValues("spring.data.cassandra.contact-points=cluster.example.com:9041,another.example.com", + "spring.data.cassandra.port=9043", "spring.data.cassandra.local-datacenter=cassandra-eu1") + .run((context) -> { + assertThat(context).hasSingleBean(DriverConfigLoader.class); + DriverExecutionProfile configuration = context.getBean(DriverConfigLoader.class).getInitialConfig() + .getDefaultProfile(); + assertThat(configuration.getStringList(DefaultDriverOption.CONTACT_POINTS)) + .containsOnly("cluster.example.com:9041", "another.example.com:9043"); + assertThat(configuration.getString(DefaultDriverOption.LOAD_BALANCING_LOCAL_DATACENTER)) + .isEqualTo("cassandra-eu1"); + }); + } + + @Test + void driverConfigLoaderWithCustomSessionName() { + this.contextRunner.withPropertyValues("spring.data.cassandra.session-name=testcluster").run((context) -> { + assertThat(context).hasSingleBean(DriverConfigLoader.class); + assertThat(context.getBean(DriverConfigLoader.class).getInitialConfig().getDefaultProfile() + .getString(DefaultDriverOption.SESSION_NAME)).isEqualTo("testcluster"); }); } @Test - void customizePoolOptions() { - this.contextRunner.withPropertyValues("spring.data.cassandra.pool.idle-timeout=42", - "spring.data.cassandra.pool.pool-timeout=52", "spring.data.cassandra.pool.heartbeat-interval=62", - "spring.data.cassandra.pool.max-queue-size=72").run((context) -> { - assertThat(context).hasSingleBean(Cluster.class); - PoolingOptions poolingOptions = context.getBean(Cluster.class).getConfiguration() - .getPoolingOptions(); - assertThat(poolingOptions.getIdleTimeoutSeconds()).isEqualTo(42); - assertThat(poolingOptions.getPoolTimeoutMillis()).isEqualTo(52); - assertThat(poolingOptions.getHeartbeatIntervalSeconds()).isEqualTo(62); - assertThat(poolingOptions.getMaxQueueSize()).isEqualTo(72); + void driverConfigLoaderWithCustomSessionNameAndCustomizer() { + this.contextRunner.withUserConfiguration(SimpleDriverConfigLoaderBuilderCustomizerConfig.class) + .withPropertyValues("spring.data.cassandra.session-name=testcluster").run((context) -> { + assertThat(context).hasSingleBean(DriverConfigLoader.class); + assertThat(context.getBean(DriverConfigLoader.class).getInitialConfig().getDefaultProfile() + .getString(DefaultDriverOption.SESSION_NAME)).isEqualTo("overridden-name"); }); } @Test - void clusterFactoryIsCalledToCreateCluster() { - this.contextRunner.withUserConfiguration(ClusterFactoryConfig.class) - .run((context) -> assertThat(context.getBean(TestClusterFactory.class).initializer).isNotNull()); + void driverConfigLoaderCustomizeConnectionOptions() { + this.contextRunner.withPropertyValues("spring.data.cassandra.connection.connect-timeout=200ms", + "spring.data.cassandra.connection.init-query-timeout=10").run((context) -> { + DriverExecutionProfile config = context.getBean(DriverConfigLoader.class).getInitialConfig() + .getDefaultProfile(); + assertThat(config.getInt(DefaultDriverOption.CONNECTION_CONNECT_TIMEOUT)).isEqualTo(200); + assertThat(config.getInt(DefaultDriverOption.CONNECTION_INIT_QUERY_TIMEOUT)).isEqualTo(10); + }); } - @Configuration(proxyBeanMethods = false) - static class MockCustomizerConfig { - - @Bean - ClusterBuilderCustomizer customizer() { - return mock(ClusterBuilderCustomizer.class); - } + @Test + void driverConfigLoaderCustomizePoolOptions() { + this.contextRunner.withPropertyValues("spring.data.cassandra.pool.idle-timeout=42", + "spring.data.cassandra.pool.heartbeat-interval=62").run((context) -> { + DriverExecutionProfile config = context.getBean(DriverConfigLoader.class).getInitialConfig() + .getDefaultProfile(); + assertThat(config.getInt(DefaultDriverOption.HEARTBEAT_TIMEOUT)).isEqualTo(42); + assertThat(config.getInt(DefaultDriverOption.HEARTBEAT_INTERVAL)).isEqualTo(62); + }); + } + @Test + void driverConfigLoaderCustomizeRequestOptions() { + this.contextRunner.withPropertyValues("spring.data.cassandra.request.timeout=5s", + "spring.data.cassandra.request.consistency=two", + "spring.data.cassandra.request.serial-consistency=quorum", "spring.data.cassandra.request.page-size=42") + .run((context) -> { + DriverExecutionProfile config = context.getBean(DriverConfigLoader.class).getInitialConfig() + .getDefaultProfile(); + assertThat(config.getInt(DefaultDriverOption.REQUEST_TIMEOUT)).isEqualTo(5000); + assertThat(config.getString(DefaultDriverOption.REQUEST_CONSISTENCY)).isEqualTo("TWO"); + assertThat(config.getString(DefaultDriverOption.REQUEST_SERIAL_CONSISTENCY)).isEqualTo("QUORUM"); + assertThat(config.getInt(DefaultDriverOption.REQUEST_PAGE_SIZE)).isEqualTo(42); + }); } - @Configuration(proxyBeanMethods = false) - static class SimpleCustomizerConfig { + @Test + void driverConfigLoaderCustomizeControlConnectionOptions() { + this.contextRunner.withPropertyValues("spring.data.cassandra.controlconnection.timeout=200ms") + .run((context) -> { + DriverExecutionProfile config = context.getBean(DriverConfigLoader.class).getInitialConfig() + .getDefaultProfile(); + assertThat(config.getInt(DefaultDriverOption.CONTROL_CONNECTION_TIMEOUT)).isEqualTo(200); + }); + } - @Bean - ClusterBuilderCustomizer customizer() { - return (clusterBuilder) -> clusterBuilder.withClusterName("overridden-name"); - } + @Test + void driverConfigLoaderUsePassThroughLimitingRequestThrottlerByDefault() { + this.contextRunner.withPropertyValues().run((context) -> { + DriverExecutionProfile config = context.getBean(DriverConfigLoader.class).getInitialConfig() + .getDefaultProfile(); + assertThat(config.getString(DefaultDriverOption.REQUEST_THROTTLER_CLASS)) + .isEqualTo(PassThroughRequestThrottler.class.getSimpleName()); + }); + } + @Test + void driverConfigLoaderWithRateLimitingRequiresExtraConfiguration() { + this.contextRunner.withPropertyValues("spring.data.cassandra.request.throttler.type=rate-limiting") + .run((context) -> assertThatThrownBy(() -> context.getBean(CqlSession.class)) + .hasMessageContaining("Error instantiating class RateLimitingRequestThrottler") + .hasMessageContaining("No configuration setting found for key")); } - @Configuration(proxyBeanMethods = false) - static class ClusterFactoryConfig { + @Test + void driverConfigLoaderCustomizeConcurrencyLimitingRequestThrottler() { + this.contextRunner.withPropertyValues("spring.data.cassandra.request.throttler.type=concurrency-limiting", + "spring.data.cassandra.request.throttler.max-concurrent-requests=62", + "spring.data.cassandra.request.throttler.max-queue-size=72").run((context) -> { + DriverExecutionProfile config = context.getBean(DriverConfigLoader.class).getInitialConfig() + .getDefaultProfile(); + assertThat(config.getString(DefaultDriverOption.REQUEST_THROTTLER_CLASS)) + .isEqualTo(ConcurrencyLimitingRequestThrottler.class.getSimpleName()); + assertThat(config.getInt(DefaultDriverOption.REQUEST_THROTTLER_MAX_CONCURRENT_REQUESTS)) + .isEqualTo(62); + assertThat(config.getInt(DefaultDriverOption.REQUEST_THROTTLER_MAX_QUEUE_SIZE)).isEqualTo(72); + }); + } - @Bean - TestClusterFactory clusterFactory() { - return new TestClusterFactory(); - } + @Test + void driverConfigLoaderCustomizeRateLimitingRequestThrottler() { + this.contextRunner.withPropertyValues("spring.data.cassandra.request.throttler.type=rate-limiting", + "spring.data.cassandra.request.throttler.max-requests-per-second=62", + "spring.data.cassandra.request.throttler.max-queue-size=72", + "spring.data.cassandra.request.throttler.drain-interval=16ms").run((context) -> { + DriverExecutionProfile config = context.getBean(DriverConfigLoader.class).getInitialConfig() + .getDefaultProfile(); + assertThat(config.getString(DefaultDriverOption.REQUEST_THROTTLER_CLASS)) + .isEqualTo(RateLimitingRequestThrottler.class.getSimpleName()); + assertThat(config.getInt(DefaultDriverOption.REQUEST_THROTTLER_MAX_REQUESTS_PER_SECOND)) + .isEqualTo(62); + assertThat(config.getInt(DefaultDriverOption.REQUEST_THROTTLER_MAX_QUEUE_SIZE)).isEqualTo(72); + assertThat(config.getInt(DefaultDriverOption.REQUEST_THROTTLER_DRAIN_INTERVAL)).isEqualTo(16); + }); + } + @Test + void driverConfigLoaderWithConfigComplementSettings() { + String configLocation = "org/springframework/boot/autoconfigure/cassandra/simple.conf"; + this.contextRunner.withPropertyValues("spring.data.cassandra.session-name=testcluster", + "spring.data.cassandra.config=" + configLocation).run((context) -> { + assertThat(context).hasSingleBean(DriverConfigLoader.class); + assertThat(context.getBean(DriverConfigLoader.class).getInitialConfig().getDefaultProfile() + .getString(DefaultDriverOption.SESSION_NAME)).isEqualTo("testcluster"); + assertThat(context.getBean(DriverConfigLoader.class).getInitialConfig().getDefaultProfile() + .getDuration(DefaultDriverOption.REQUEST_TIMEOUT)).isEqualTo(Duration.ofMillis(500)); + }); } - static class TestClusterFactory implements ClusterFactory { + @Test + void driverConfigLoaderWithConfigCreateProfiles() { + String configLocation = "org/springframework/boot/autoconfigure/cassandra/profiles.conf"; + this.contextRunner.withPropertyValues("spring.data.cassandra.config=" + configLocation).run((context) -> { + assertThat(context).hasSingleBean(DriverConfigLoader.class); + DriverConfig driverConfig = context.getBean(DriverConfigLoader.class).getInitialConfig(); + assertThat(driverConfig.getProfiles()).containsOnlyKeys("default", "first", "second"); + assertThat(driverConfig.getProfile("first").getDuration(DefaultDriverOption.REQUEST_TIMEOUT)) + .isEqualTo(Duration.ofMillis(100)); + }); + } - private Initializer initializer = null; + @Configuration(proxyBeanMethods = false) + static class SimpleDriverConfigLoaderBuilderCustomizerConfig { - @Override - public Cluster create(Initializer initializer) { - this.initializer = initializer; - return Cluster.buildFrom(initializer); + @Bean + DriverConfigLoaderBuilderCustomizer customizer() { + return (builder) -> builder.withString(DefaultDriverOption.SESSION_NAME, "overridden-name"); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfigurationWithPasswordAuthenticationIntegrationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfigurationWithPasswordAuthenticationIntegrationTests.java new file mode 100644 index 000000000000..9b2ff9757d97 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfigurationWithPasswordAuthenticationIntegrationTests.java @@ -0,0 +1,124 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.cassandra; + +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.CqlSessionBuilder; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import org.junit.jupiter.api.Test; +import org.rnorth.ducttape.TimeoutException; +import org.rnorth.ducttape.unreliables.Unreliables; +import org.testcontainers.containers.ContainerLaunchException; +import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy; +import org.testcontainers.images.builder.Transferable; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.testsupport.testcontainers.CassandraContainer; +import org.springframework.util.StreamUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link CassandraAutoConfiguration} that only uses password authentication. + * + * @author Stephane Nicoll + */ +@Testcontainers(disabledWithoutDocker = true) +class CassandraAutoConfigurationWithPasswordAuthenticationIntegrationTests { + + @Container + static final CassandraContainer cassandra = new PasswordAuthenticatorCassandraContainer().withStartupAttempts(5) + .withStartupTimeout(Duration.ofMinutes(10)).waitingFor(new CassandraWaitStrategy()); + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(CassandraAutoConfiguration.class)).withPropertyValues( + "spring.data.cassandra.contact-points:" + cassandra.getHost() + ":" + + cassandra.getFirstMappedPort(), + "spring.data.cassandra.local-datacenter=datacenter1", + "spring.data.cassandra.connection.connect-timeout=60s", + "spring.data.cassandra.connection.init-query-timeout=60s", + "spring.data.cassandra.request.timeout=60s"); + + @Test + void authenticationWithValidUsernameAndPassword() { + this.contextRunner.withPropertyValues("spring.data.cassandra.username=cassandra", + "spring.data.cassandra.password=cassandra").run((context) -> { + SimpleStatement select = SimpleStatement.newInstance("SELECT release_version FROM system.local") + .setConsistencyLevel(ConsistencyLevel.LOCAL_ONE); + assertThat(context.getBean(CqlSession.class).execute(select).one()).isNotNull(); + }); + } + + @Test + void authenticationWithInvalidCredentials() { + this.contextRunner + .withPropertyValues("spring.data.cassandra.username=not-a-user", + "spring.data.cassandra.password=invalid-password") + .run((context) -> assertThatThrownBy(() -> context.getBean(CqlSession.class)) + .hasMessageContaining("Authentication error")); + } + + static final class PasswordAuthenticatorCassandraContainer extends CassandraContainer { + + @Override + protected void containerIsCreated(String containerId) { + String config = this.copyFileFromContainer("/etc/cassandra/cassandra.yaml", + (stream) -> StreamUtils.copyToString(stream, StandardCharsets.UTF_8)); + String updatedConfig = config.replace("authenticator: AllowAllAuthenticator", + "authenticator: PasswordAuthenticator"); + this.copyFileToContainer(Transferable.of(updatedConfig.getBytes(StandardCharsets.UTF_8)), + "/etc/cassandra/cassandra.yaml"); + } + + } + + static final class CassandraWaitStrategy extends AbstractWaitStrategy { + + @Override + protected void waitUntilReady() { + try { + Unreliables.retryUntilSuccess((int) this.startupTimeout.getSeconds(), TimeUnit.SECONDS, () -> { + getRateLimiter().doWhenReady(() -> cqlSessionBuilder().build()); + return true; + }); + } + catch (TimeoutException ex) { + throw new ContainerLaunchException( + "Timed out waiting for Cassandra to be accessible for query execution"); + } + } + + private CqlSessionBuilder cqlSessionBuilder() { + return CqlSession.builder() + .addContactPoint(new InetSocketAddress(this.waitStrategyTarget.getHost(), + this.waitStrategyTarget.getFirstMappedPort())) + .withLocalDatacenter("datacenter1").withAuthCredentials("cassandra", "cassandra"); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cassandra/CassandraPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cassandra/CassandraPropertiesTests.java new file mode 100644 index 000000000000..abb2e6fb9e39 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cassandra/CassandraPropertiesTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.cassandra; + +import java.time.Duration; + +import com.datastax.oss.driver.api.core.config.OptionsMap; +import com.datastax.oss.driver.api.core.config.TypedDriverOption; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link CassandraProperties}. + * + * @author Chris Bono + * @author Stephane Nicoll + */ +class CassandraPropertiesTests { + + /** + * To let a configuration file override values, {@link CassandraProperties} can't have + * any default hardcoded. This test makes sure that the default that we moved to + * manual meta-data are accurate. + */ + @Test + void defaultValuesInManualMetadataAreConsistent() { + OptionsMap driverDefaults = OptionsMap.driverDefaults(); + // spring.data.cassandra.connection.connect-timeout + assertThat(driverDefaults.get(TypedDriverOption.CONNECTION_CONNECT_TIMEOUT)).isEqualTo(Duration.ofSeconds(5)); + // spring.data.cassandra.connection.init-query-timeout + assertThat(driverDefaults.get(TypedDriverOption.CONNECTION_INIT_QUERY_TIMEOUT)) + .isEqualTo(Duration.ofSeconds(5)); + // spring.data.cassandra.request.timeout + assertThat(driverDefaults.get(TypedDriverOption.REQUEST_TIMEOUT)).isEqualTo(Duration.ofSeconds(2)); + // spring.data.cassandra.request.page-size + assertThat(driverDefaults.get(TypedDriverOption.REQUEST_PAGE_SIZE)).isEqualTo(5000); + // spring.data.cassandra.request.throttler.type + assertThat(driverDefaults.get(TypedDriverOption.REQUEST_THROTTLER_CLASS)) + .isEqualTo("PassThroughRequestThrottler"); // "none" + // spring.data.cassandra.pool.heartbeat-interval + assertThat(driverDefaults.get(TypedDriverOption.HEARTBEAT_INTERVAL)).isEqualTo(Duration.ofSeconds(30)); + // spring.data.cassandra.pool.idle-timeout + assertThat(driverDefaults.get(TypedDriverOption.HEARTBEAT_TIMEOUT)).isEqualTo(Duration.ofSeconds(5)); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cloud/CloudServiceConnectorsAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cloud/CloudServiceConnectorsAutoConfigurationTests.java deleted file mode 100644 index b83453b846c7..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cloud/CloudServiceConnectorsAutoConfigurationTests.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.cloud; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.autoconfigure.TestAutoConfigurationSorter; -import org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration; -import org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; -import org.springframework.core.type.classreading.CachingMetadataReaderFactory; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link CloudServiceConnectorsAutoConfiguration}. - * - * @author Phillip Webb - */ -class CloudServiceConnectorsAutoConfigurationTests { - - @Test - void testOrder() { - TestAutoConfigurationSorter sorter = new TestAutoConfigurationSorter(new CachingMetadataReaderFactory()); - Collection classNames = new ArrayList<>(); - classNames.add(MongoAutoConfiguration.class.getName()); - classNames.add(DataSourceAutoConfiguration.class.getName()); - classNames.add(MongoRepositoriesAutoConfiguration.class.getName()); - classNames.add(JpaRepositoriesAutoConfiguration.class.getName()); - classNames.add(CloudServiceConnectorsAutoConfiguration.class.getName()); - List ordered = sorter.getInPriorityOrder(classNames); - assertThat(ordered.get(0)).isEqualTo(CloudServiceConnectorsAutoConfiguration.class.getName()); - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionEvaluationReportTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionEvaluationReportTests.java index 86bf63ffb459..2a627c33c985 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionEvaluationReportTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionEvaluationReportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,8 +23,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory; @@ -53,6 +54,7 @@ * @author Greg Turnquist * @author Phillip Webb */ +@ExtendWith(MockitoExtension.class) class ConditionEvaluationReportTests { private DefaultListableBeanFactory beanFactory; @@ -76,7 +78,6 @@ class ConditionEvaluationReportTests { @BeforeEach void setup() { - MockitoAnnotations.initMocks(this); this.beanFactory = new DefaultListableBeanFactory(); this.report = ConditionEvaluationReport.get(this.beanFactory); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionMessageTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionMessageTests.java index 40d1cfce88f2..d8c47ecccc7a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionMessageTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionMessageTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.boot.autoconfigure.condition; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import org.junit.jupiter.api.Test; @@ -188,4 +189,19 @@ void availableShouldConstructMessage() { assertThat(message.toString()).isEqualTo("@Test JMX is available"); } + @Test + void itemsTolerateNullInput() { + Collection items = null; + ConditionMessage message = ConditionMessage.forCondition(Test.class).didNotFind("item").items(items); + assertThat(message.toString()).isEqualTo("@Test did not find item"); + } + + @Test + void quotedItemsTolerateNullInput() { + Collection items = null; + ConditionMessage message = ConditionMessage.forCondition(Test.class).didNotFind("item").items(Style.QUOTE, + items); + assertThat(message.toString()).isEqualTo("@Test did not find item"); + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBeanTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBeanTests.java index b588ccfe8c8b..5bfb79e00d00 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBeanTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.Collection; import java.util.Date; import java.util.function.Consumer; @@ -29,6 +30,7 @@ import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport.ConditionAndOutcomes; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.ConfigurableApplicationContext; @@ -134,10 +136,21 @@ private void hasBarBean(AssertableApplicationContext context) { assertThat(context.getBean("bar")).isEqualTo("bar"); } + @Test + void onBeanConditionOutputShouldNotContainConditionalOnMissingBeanClassInMessage() { + this.contextRunner.withUserConfiguration(OnBeanNameConfiguration.class).run((context) -> { + Collection conditionAndOutcomes = ConditionEvaluationReport + .get(context.getSourceApplicationContext().getBeanFactory()).getConditionAndOutcomesBySource() + .values(); + String message = conditionAndOutcomes.iterator().next().iterator().next().getOutcome().getMessage(); + assertThat(message).doesNotContain("@ConditionalOnMissingBean"); + }); + } + @Test void conditionEvaluationConsidersChangeInTypeWhenBeanIsOverridden() { - this.contextRunner.withUserConfiguration(OriginalDefinition.class, OverridingDefinition.class, - ConsumingConfiguration.class).run((context) -> { + this.contextRunner.withAllowBeanDefinitionOverriding(true).withUserConfiguration(OriginalDefinition.class, + OverridingDefinition.class, ConsumingConfiguration.class).run((context) -> { assertThat(context).hasBean("testBean"); assertThat(context).hasSingleBean(Integer.class); assertThat(context).doesNotHaveBean(ConsumingConfiguration.class); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnJavaTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnJavaTests.java index d445c720fd1d..f9ef8ab06660 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnJavaTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnJavaTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -104,7 +104,7 @@ void java8IsTheFallback() throws Exception { private String getJavaVersion(Class... hiddenClasses) throws Exception { FilteredClassLoader classLoader = new FilteredClassLoader(hiddenClasses); - Class javaVersionClass = classLoader.loadClass(JavaVersion.class.getName()); + Class javaVersionClass = Class.forName(JavaVersion.class.getName(), false, classLoader); Method getJavaVersionMethod = ReflectionUtils.findMethod(javaVersionClass, "getJavaVersion"); Object javaVersion = ReflectionUtils.invokeMethod(getJavaVersionMethod, null); classLoader.close(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanTests.java index 9a24b37c29a8..c5a01cc90a68 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.Collection; import java.util.Date; import java.util.function.Consumer; @@ -30,6 +31,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.boot.autoconfigure.condition.scan.ScanBean; import org.springframework.boot.autoconfigure.condition.scan.ScannedFactoryBeanConfiguration; import org.springframework.boot.autoconfigure.condition.scan.ScannedFactoryBeanWithBeanMethodArgumentsConfiguration; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; @@ -59,7 +61,7 @@ * @author Andy Wilkinson */ @SuppressWarnings("resource") -public class ConditionalOnMissingBeanTests { +class ConditionalOnMissingBeanTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner(); @@ -136,6 +138,17 @@ void testAnnotationOnMissingBeanConditionWithEagerFactoryBean() { }); } + @Test + void testOnMissingBeanConditionOutputShouldNotContainConditionalOnBeanClassInMessage() { + this.contextRunner.withUserConfiguration(OnBeanNameConfiguration.class).run((context) -> { + Collection conditionAndOutcomes = ConditionEvaluationReport + .get(context.getSourceApplicationContext().getBeanFactory()).getConditionAndOutcomesBySource() + .values(); + String message = conditionAndOutcomes.iterator().next().iterator().next().getOutcome().getMessage(); + assertThat(message).doesNotContain("@ConditionalOnBean"); + }); + } + @Test void testOnMissingBeanConditionWithFactoryBean() { this.contextRunner @@ -149,7 +162,7 @@ void testOnMissingBeanConditionWithComponentScannedFactoryBean() { this.contextRunner .withUserConfiguration(ComponentScannedFactoryBeanBeanMethodConfiguration.class, ConditionalOnFactoryBean.class, PropertyPlaceholderAutoConfiguration.class) - .run((context) -> assertThat(context.getBean(ExampleBean.class).toString()).isEqualTo("fromFactory")); + .run((context) -> assertThat(context.getBean(ScanBean.class).toString()).isEqualTo("fromFactory")); } @Test @@ -157,7 +170,7 @@ void testOnMissingBeanConditionWithComponentScannedFactoryBeanWithBeanMethodArgu this.contextRunner .withUserConfiguration(ComponentScannedFactoryBeanBeanMethodWithArgumentsConfiguration.class, ConditionalOnFactoryBean.class, PropertyPlaceholderAutoConfiguration.class) - .run((context) -> assertThat(context.getBean(ExampleBean.class).toString()).isEqualTo("fromFactory")); + .run((context) -> assertThat(context.getBean(ScanBean.class).toString()).isEqualTo("fromFactory")); } @Test @@ -380,7 +393,7 @@ FactoryBean exampleBeanFactoryBean() { } @Configuration(proxyBeanMethods = false) - @ComponentScan(basePackages = "org.springframework.boot.autoconfigure.condition.scan", + @ComponentScan(basePackages = "org.springframework.boot.autoconfigure.condition.scan", useDefaultFilters = false, includeFilters = @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = ScannedFactoryBeanConfiguration.class)) static class ComponentScannedFactoryBeanBeanMethodConfiguration { @@ -388,7 +401,7 @@ static class ComponentScannedFactoryBeanBeanMethodConfiguration { } @Configuration(proxyBeanMethods = false) - @ComponentScan(basePackages = "org.springframework.boot.autoconfigure.condition.scan", + @ComponentScan(basePackages = "org.springframework.boot.autoconfigure.condition.scan", useDefaultFilters = false, includeFilters = @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = ScannedFactoryBeanWithBeanMethodArgumentsConfiguration.class)) static class ComponentScannedFactoryBeanBeanMethodWithArgumentsConfiguration { @@ -605,9 +618,9 @@ ExampleBean exampleBean2() { } - public static class ExampleFactoryBean implements FactoryBean { + static class ExampleFactoryBean implements FactoryBean { - public ExampleFactoryBean(String value) { + ExampleFactoryBean(String value) { Assert.state(!value.contains("$"), "value should not contain '$'"); } @@ -726,11 +739,11 @@ TestParameterizedContainer conditionalCustomExampleBean() { } @TestAnnotation - public static class ExampleBean { + static class ExampleBean { private String value; - public ExampleBean(String value) { + ExampleBean(String value) { this.value = value; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnPropertyTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnPropertyTests.java index 1ed53089b7b6..ade027064d70 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnPropertyTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnPropertyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -299,7 +299,7 @@ String foo() { @Configuration(proxyBeanMethods = false) // i.e ${simple.myProperty:false} - @ConditionalOnProperty(prefix = "simple", name = "my-property", havingValue = "true", matchIfMissing = false) + @ConditionalOnProperty(prefix = "simple", name = "my-property", havingValue = "true") static class DisabledIfNotConfiguredOtherwiseConfig { @Bean @@ -399,7 +399,7 @@ String foo() { @Configuration(proxyBeanMethods = false) @ConditionalOnMyFeature - @ConditionalOnProperty(prefix = "my.other.feature", name = "enabled", havingValue = "true", matchIfMissing = false) + @ConditionalOnProperty(prefix = "my.other.feature", name = "enabled", havingValue = "true") static class MetaAnnotationAndDirectAnnotation { @Bean @@ -411,7 +411,7 @@ String foo() { @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE, ElementType.METHOD }) - @ConditionalOnProperty(prefix = "my.feature", name = "enabled", havingValue = "true", matchIfMissing = false) + @ConditionalOnProperty(prefix = "my.feature", name = "enabled", havingValue = "true") @interface ConditionalOnMyFeature { } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnSingleCandidateTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnSingleCandidateTests.java index 6742609bbcd7..1cd9cb7f1992 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnSingleCandidateTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnSingleCandidateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,16 @@ package org.springframework.boot.autoconfigure.condition; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * Tests for {@link ConditionalOnSingleCandidate @ConditionalOnSingleCandidate}. @@ -35,117 +35,114 @@ */ class ConditionalOnSingleCandidateTests { - private final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); - - @AfterEach - void close() { - if (this.context != null) { - this.context.close(); - } - } + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner(); @Test void singleCandidateNoCandidate() { - load(OnBeanSingleCandidateConfiguration.class); - assertThat(this.context.containsBean("baz")).isFalse(); + this.contextRunner.withUserConfiguration(OnBeanSingleCandidateConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean("consumer")); } @Test void singleCandidateOneCandidate() { - load(FooConfiguration.class, OnBeanSingleCandidateConfiguration.class); - assertThat(this.context.containsBean("baz")).isTrue(); - assertThat(this.context.getBean("baz")).isEqualTo("foo"); + this.contextRunner.withUserConfiguration(AlphaConfiguration.class, OnBeanSingleCandidateConfiguration.class) + .run((context) -> { + assertThat(context).hasBean("consumer"); + assertThat(context.getBean("consumer")).isEqualTo("alpha"); + }); + } + + @Test + void singleCandidateOneScopedProxyCandidate() { + this.contextRunner + .withUserConfiguration(AlphaScopedProxyConfiguration.class, OnBeanSingleCandidateConfiguration.class) + .run((context) -> { + assertThat(context).hasBean("consumer"); + assertThat(context.getBean("consumer").toString()).isEqualTo("alpha"); + }); } @Test void singleCandidateInAncestorsOneCandidateInCurrent() { - load(); - AnnotationConfigApplicationContext child = new AnnotationConfigApplicationContext(); - child.register(FooConfiguration.class, OnBeanSingleCandidateInAncestorsConfiguration.class); - child.setParent(this.context); - child.refresh(); - assertThat(child.containsBean("baz")).isFalse(); - child.close(); + this.contextRunner.run((parent) -> this.contextRunner + .withUserConfiguration(AlphaConfiguration.class, OnBeanSingleCandidateInAncestorsConfiguration.class) + .withParent(parent).run((child) -> assertThat(child).doesNotHaveBean("consumer"))); } @Test void singleCandidateInAncestorsOneCandidateInParent() { - load(FooConfiguration.class); - AnnotationConfigApplicationContext child = new AnnotationConfigApplicationContext(); - child.register(OnBeanSingleCandidateInAncestorsConfiguration.class); - child.setParent(this.context); - child.refresh(); - assertThat(child.containsBean("baz")).isTrue(); - assertThat(child.getBean("baz")).isEqualTo("foo"); - child.close(); + this.contextRunner.withUserConfiguration(AlphaConfiguration.class) + .run((parent) -> this.contextRunner + .withUserConfiguration(OnBeanSingleCandidateInAncestorsConfiguration.class).withParent(parent) + .run((child) -> { + assertThat(child).hasBean("consumer"); + assertThat(child.getBean("consumer")).isEqualTo("alpha"); + })); } @Test void singleCandidateInAncestorsOneCandidateInGrandparent() { - load(FooConfiguration.class); - AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext(); - parent.setParent(this.context); - parent.refresh(); - AnnotationConfigApplicationContext child = new AnnotationConfigApplicationContext(); - child.register(OnBeanSingleCandidateInAncestorsConfiguration.class); - child.setParent(parent); - child.refresh(); - assertThat(child.containsBean("baz")).isTrue(); - assertThat(child.getBean("baz")).isEqualTo("foo"); - child.close(); - parent.close(); + this.contextRunner.withUserConfiguration(AlphaConfiguration.class) + .run((grandparent) -> this.contextRunner.withParent(grandparent) + .run((parent) -> this.contextRunner + .withUserConfiguration(OnBeanSingleCandidateInAncestorsConfiguration.class) + .withParent(parent).run((child) -> { + assertThat(child).hasBean("consumer"); + assertThat(child.getBean("consumer")).isEqualTo("alpha"); + }))); } @Test void singleCandidateMultipleCandidates() { - load(FooConfiguration.class, BarConfiguration.class, OnBeanSingleCandidateConfiguration.class); - assertThat(this.context.containsBean("baz")).isFalse(); + this.contextRunner + .withUserConfiguration(AlphaConfiguration.class, BravoConfiguration.class, + OnBeanSingleCandidateConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean("consumer")); } @Test void singleCandidateMultipleCandidatesOnePrimary() { - load(FooPrimaryConfiguration.class, BarConfiguration.class, OnBeanSingleCandidateConfiguration.class); - assertThat(this.context.containsBean("baz")).isTrue(); - assertThat(this.context.getBean("baz")).isEqualTo("foo"); + this.contextRunner.withUserConfiguration(AlphaPrimaryConfiguration.class, BravoConfiguration.class, + OnBeanSingleCandidateConfiguration.class).run((context) -> { + assertThat(context).hasBean("consumer"); + assertThat(context.getBean("consumer")).isEqualTo("alpha"); + }); } @Test void singleCandidateMultipleCandidatesMultiplePrimary() { - load(FooPrimaryConfiguration.class, BarPrimaryConfiguration.class, OnBeanSingleCandidateConfiguration.class); - assertThat(this.context.containsBean("baz")).isFalse(); + this.contextRunner + .withUserConfiguration(AlphaPrimaryConfiguration.class, BravoPrimaryConfiguration.class, + OnBeanSingleCandidateConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean("consumer")); } @Test void invalidAnnotationTwoTypes() { - assertThatIllegalStateException().isThrownBy(() -> load(OnBeanSingleCandidateTwoTypesConfiguration.class)) - .withCauseInstanceOf(IllegalArgumentException.class) - .withMessageContaining(OnBeanSingleCandidateTwoTypesConfiguration.class.getName()); + this.contextRunner.withUserConfiguration(OnBeanSingleCandidateTwoTypesConfiguration.class).run((context) -> { + assertThat(context).hasFailed(); + assertThat(context).getFailure().hasCauseInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(OnBeanSingleCandidateTwoTypesConfiguration.class.getName()); + }); } @Test void invalidAnnotationNoType() { - assertThatIllegalStateException().isThrownBy(() -> load(OnBeanSingleCandidateNoTypeConfiguration.class)) - .withCauseInstanceOf(IllegalArgumentException.class) - .withMessageContaining(OnBeanSingleCandidateNoTypeConfiguration.class.getName()); + this.contextRunner.withUserConfiguration(OnBeanSingleCandidateNoTypeConfiguration.class).run((context) -> { + assertThat(context).hasFailed(); + assertThat(context).getFailure().hasCauseInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(OnBeanSingleCandidateNoTypeConfiguration.class.getName()); + }); } @Test void singleCandidateMultipleCandidatesInContextHierarchy() { - load(FooPrimaryConfiguration.class, BarConfiguration.class); - try (AnnotationConfigApplicationContext child = new AnnotationConfigApplicationContext()) { - child.setParent(this.context); - child.register(OnBeanSingleCandidateConfiguration.class); - child.refresh(); - assertThat(child.containsBean("baz")).isTrue(); - assertThat(child.getBean("baz")).isEqualTo("foo"); - } - } - - private void load(Class... classes) { - if (classes.length > 0) { - this.context.register(classes); - } - this.context.refresh(); + this.contextRunner.withUserConfiguration(AlphaPrimaryConfiguration.class, BravoConfiguration.class) + .run((parent) -> this.contextRunner.withUserConfiguration(OnBeanSingleCandidateConfiguration.class) + .withParent(parent).run((child) -> { + assertThat(child).hasBean("consumer"); + assertThat(child.getBean("consumer")).isEqualTo("alpha"); + })); } @Configuration(proxyBeanMethods = false) @@ -153,7 +150,7 @@ private void load(Class... classes) { static class OnBeanSingleCandidateConfiguration { @Bean - String baz(String s) { + CharSequence consumer(CharSequence s) { return s; } @@ -164,7 +161,7 @@ String baz(String s) { static class OnBeanSingleCandidateInAncestorsConfiguration { @Bean - String baz(String s) { + CharSequence consumer(CharSequence s) { return s; } @@ -183,43 +180,54 @@ static class OnBeanSingleCandidateNoTypeConfiguration { } @Configuration(proxyBeanMethods = false) - static class FooConfiguration { + static class AlphaConfiguration { @Bean - String foo() { - return "foo"; + String alpha() { + return "alpha"; } } @Configuration(proxyBeanMethods = false) - static class FooPrimaryConfiguration { + static class AlphaPrimaryConfiguration { @Bean @Primary - String foo() { - return "foo"; + String alpha() { + return "alpha"; + } + + } + + @Configuration(proxyBeanMethods = false) + static class AlphaScopedProxyConfiguration { + + @Bean + @Scope(proxyMode = ScopedProxyMode.INTERFACES) + String alpha() { + return "alpha"; } } @Configuration(proxyBeanMethods = false) - static class BarConfiguration { + static class BravoConfiguration { @Bean - String bar() { - return "bar"; + String bravo() { + return "bravo"; } } @Configuration(proxyBeanMethods = false) - static class BarPrimaryConfiguration { + static class BravoPrimaryConfiguration { @Bean @Primary - String bar() { - return "bar"; + String bravo() { + return "bravo"; } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnWarDeploymentTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnWarDeploymentTests.java new file mode 100644 index 000000000000..91eaea77348b --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnWarDeploymentTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.condition; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConditionalOnWarDeployment @ConditionalOnWarDeployment}. + * + * @author Madhura Bhave + */ +class ConditionalOnWarDeploymentTests { + + @Test + void nonWebApplicationShouldNotMatch() { + ApplicationContextRunner contextRunner = new ApplicationContextRunner(); + contextRunner.withUserConfiguration(TestConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean("forWar")); + } + + @Test + void reactiveWebApplicationShouldNotMatch() { + ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner(); + contextRunner.withUserConfiguration(TestConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean("forWar")); + } + + @Test + void embeddedServletWebApplicationShouldNotMatch() { + WebApplicationContextRunner contextRunner = new WebApplicationContextRunner( + AnnotationConfigServletWebApplicationContext::new); + contextRunner.withUserConfiguration(TestConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean("forWar")); + } + + @Test + void warDeployedServletWebApplicationShouldMatch() { + // sets a mock servletContext before context refresh which is what the + // SpringBootServletInitializer does for WAR deployments. + WebApplicationContextRunner contextRunner = new WebApplicationContextRunner(); + contextRunner.withUserConfiguration(TestConfiguration.class) + .run((context) -> assertThat(context).hasBean("forWar")); + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnWarDeployment + static class TestConfiguration { + + @Bean + String forWar() { + return "forWar"; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/scan/ScanBean.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/scan/ScanBean.java new file mode 100644 index 000000000000..2bb12a77563b --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/scan/ScanBean.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.condition.scan; + +public class ScanBean { + + private String value; + + public ScanBean(String value) { + this.value = value; + } + + @Override + public String toString() { + return this.value; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/scan/ScanFactoryBean.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/scan/ScanFactoryBean.java new file mode 100644 index 000000000000..0ec5d5d2c3e3 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/scan/ScanFactoryBean.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.condition.scan; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.util.Assert; + +class ScanFactoryBean implements FactoryBean { + + ScanFactoryBean(String value) { + Assert.state(!value.contains("$"), "value should not contain '$'"); + } + + @Override + public ScanBean getObject() { + return new ScanBean("fromFactory"); + } + + @Override + public Class getObjectType() { + return ScanBean.class; + } + + @Override + public boolean isSingleton() { + return false; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/scan/ScannedFactoryBeanConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/scan/ScannedFactoryBeanConfiguration.java index 0e1152126574..0feab301afa0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/scan/ScannedFactoryBeanConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/scan/ScannedFactoryBeanConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,6 @@ package org.springframework.boot.autoconfigure.condition.scan; import org.springframework.beans.factory.FactoryBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBeanTests.ExampleBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBeanTests.ExampleFactoryBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -32,8 +30,8 @@ public class ScannedFactoryBeanConfiguration { @Bean - public FactoryBean exampleBeanFactoryBean() { - return new ExampleFactoryBean("foo"); + public FactoryBean exampleBeanFactoryBean() { + return new ScanFactoryBean("foo"); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/scan/ScannedFactoryBeanWithBeanMethodArgumentsConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/scan/ScannedFactoryBeanWithBeanMethodArgumentsConfiguration.java index e18cc44b795c..5cbed4c436b1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/scan/ScannedFactoryBeanWithBeanMethodArgumentsConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/scan/ScannedFactoryBeanWithBeanMethodArgumentsConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.springframework.boot.autoconfigure.condition.scan; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBeanTests.ExampleFactoryBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -35,8 +34,8 @@ public Foo foo() { } @Bean - public ExampleFactoryBean exampleBeanFactoryBean(Foo foo) { - return new ExampleFactoryBean("foo"); + public ScanFactoryBean exampleBeanFactoryBean(Foo foo) { + return new ScanFactoryBean("foo"); } static class Foo { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/ConfigurationPropertiesAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/ConfigurationPropertiesAutoConfigurationTests.java index 1ba583cdacef..9e25c4008031 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/ConfigurationPropertiesAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/ConfigurationPropertiesAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,13 +46,13 @@ void tearDown() { @Test void processAnnotatedBean() { - load(new Class[] { AutoConfig.class, SampleBean.class }, "foo.name:test"); + load(new Class[] { AutoConfig.class, SampleBean.class }, "foo.name:test"); assertThat(this.context.getBean(SampleBean.class).getName()).isEqualTo("test"); } @Test void processAnnotatedBeanNoAutoConfig() { - load(new Class[] { SampleBean.class }, "foo.name:test"); + load(new Class[] { SampleBean.class }, "foo.name:test"); assertThat(this.context.getBean(SampleBean.class).getName()).isEqualTo("default"); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/LifecycleAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/LifecycleAutoConfigurationTests.java new file mode 100644 index 000000000000..c252a31fdd57 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/LifecycleAutoConfigurationTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.context; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.AbstractApplicationContext; +import org.springframework.context.support.DefaultLifecycleProcessor; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link LifecycleAutoConfiguration}. + * + * @author Andy Wilkinson + */ +class LifecycleAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(LifecycleAutoConfiguration.class)); + + @Test + void lifecycleProcessorIsConfiguredWithDefaultTimeout() { + this.contextRunner.run((context) -> { + assertThat(context).hasBean(AbstractApplicationContext.LIFECYCLE_PROCESSOR_BEAN_NAME); + Object processor = context.getBean(AbstractApplicationContext.LIFECYCLE_PROCESSOR_BEAN_NAME); + assertThat(processor).extracting("timeoutPerShutdownPhase").isEqualTo(30000L); + }); + } + + @Test + void lifecycleProcessorIsConfiguredWithCustomTimeout() { + this.contextRunner.withPropertyValues("spring.lifecycle.timeout-per-shutdown-phase=15s").run((context) -> { + assertThat(context).hasBean(AbstractApplicationContext.LIFECYCLE_PROCESSOR_BEAN_NAME); + Object processor = context.getBean(AbstractApplicationContext.LIFECYCLE_PROCESSOR_BEAN_NAME); + assertThat(processor).extracting("timeoutPerShutdownPhase").isEqualTo(15000L); + }); + } + + @Test + void lifecycleProcessorIsConfiguredWithCustomTimeoutInAChildContext() { + new ApplicationContextRunner().run((parent) -> { + this.contextRunner.withParent(parent).withPropertyValues("spring.lifecycle.timeout-per-shutdown-phase=15s") + .run((child) -> { + assertThat(child).hasBean(AbstractApplicationContext.LIFECYCLE_PROCESSOR_BEAN_NAME); + Object processor = child.getBean(AbstractApplicationContext.LIFECYCLE_PROCESSOR_BEAN_NAME); + assertThat(processor).extracting("timeoutPerShutdownPhase").isEqualTo(15000L); + }); + }); + } + + @Test + void whenUserDefinesALifecycleProcessorBeanThenTheAutoConfigurationBacksOff() { + this.contextRunner.withUserConfiguration(LifecycleProcessorConfiguration.class).run((context) -> { + assertThat(context).hasBean(AbstractApplicationContext.LIFECYCLE_PROCESSOR_BEAN_NAME); + Object processor = context.getBean(AbstractApplicationContext.LIFECYCLE_PROCESSOR_BEAN_NAME); + assertThat(processor).extracting("timeoutPerShutdownPhase").isEqualTo(5000L); + }); + } + + @Configuration(proxyBeanMethods = false) + static class LifecycleProcessorConfiguration { + + @Bean(name = AbstractApplicationContext.LIFECYCLE_PROCESSOR_BEAN_NAME) + DefaultLifecycleProcessor customLifecycleProcessor() { + DefaultLifecycleProcessor processor = new DefaultLifecycleProcessor(); + processor.setTimeoutPerShutdownPhase(5000); + return processor; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/MessageSourceAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/MessageSourceAutoConfigurationTests.java index 64509150cc38..eea8ea84c4b2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/MessageSourceAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/MessageSourceAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,6 @@ import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.context.MessageSource; import org.springframework.context.MessageSourceResolvable; -import org.springframework.context.NoSuchMessageException; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; @@ -106,13 +105,6 @@ void testMultipleMessageSourceCreated() { }); } - @Test - void testBadEncoding() { - // Bad encoding just means the messages are ignored - this.contextRunner.withPropertyValues("spring.messages.encoding:rubbish") - .run((context) -> assertThat(context.getMessage("foo", null, "blah", Locale.UK)).isEqualTo("blah")); - } - @Test @Disabled("Expected to fail per gh-1075") void testMessageSourceFromPropertySourceAnnotation() { @@ -223,12 +215,12 @@ public String getMessage(String code, Object[] args, String defaultMessage, Loca } @Override - public String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException { + public String getMessage(String code, Object[] args, Locale locale) { return code; } @Override - public String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException { + public String getMessage(MessageSourceResolvable resolvable, Locale locale) { return resolvable.getCodes()[0]; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/PropertyPlaceholderAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/PropertyPlaceholderAutoConfigurationTests.java index cb866b16f8d3..c0041e0d90f8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/PropertyPlaceholderAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/PropertyPlaceholderAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,14 @@ package org.springframework.boot.autoconfigure.context; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.util.TestPropertyValues; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; @@ -33,43 +35,75 @@ * Tests for {@link PropertyPlaceholderAutoConfiguration}. * * @author Dave Syer + * @author Andy Wilkinson */ class PropertyPlaceholderAutoConfigurationTests { - private final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner(); - @AfterEach - void close() { - if (this.context != null) { - this.context.close(); - } + @Test + void whenTheAutoConfigurationIsNotUsedThenBeanDefinitionPlaceholdersAreNotResolved() { + this.contextRunner.withPropertyValues("fruit:banana").withInitializer(this::definePlaceholderBean) + .run((context) -> assertThat(context.getBean(PlaceholderBean.class).fruit).isEqualTo("${fruit:apple}")); + } + + @Test + void whenTheAutoConfigurationIsUsedThenBeanDefinitionPlaceholdersAreResolved() { + this.contextRunner.withPropertyValues("fruit:banana").withInitializer(this::definePlaceholderBean) + .withConfiguration(AutoConfigurations.of(PropertyPlaceholderAutoConfiguration.class)) + .run((context) -> assertThat(context.getBean(PlaceholderBean.class).fruit).isEqualTo("banana")); } @Test - void propertyPlaceholders() { - this.context.register(PropertyPlaceholderAutoConfiguration.class, PlaceholderConfig.class); - TestPropertyValues.of("foo:two").applyTo(this.context); - this.context.refresh(); - assertThat(this.context.getBean(PlaceholderConfig.class).getFoo()).isEqualTo("two"); + void whenTheAutoConfigurationIsNotUsedThenValuePlaceholdersAreResolved() { + this.contextRunner.withPropertyValues("fruit:banana").withUserConfiguration(PlaceholderConfig.class) + .run((context) -> assertThat(context.getBean(PlaceholderConfig.class).fruit).isEqualTo("banana")); } @Test - void propertyPlaceholdersOverride() { - this.context.register(PropertyPlaceholderAutoConfiguration.class, PlaceholderConfig.class, - PlaceholdersOverride.class); - TestPropertyValues.of("foo:two").applyTo(this.context); - this.context.refresh(); - assertThat(this.context.getBean(PlaceholderConfig.class).getFoo()).isEqualTo("spam"); + void whenTheAutoConfigurationIsUsedThenValuePlaceholdersAreResolved() { + this.contextRunner.withPropertyValues("fruit:banana") + .withConfiguration(AutoConfigurations.of(PropertyPlaceholderAutoConfiguration.class)) + .withUserConfiguration(PlaceholderConfig.class) + .run((context) -> assertThat(context.getBean(PlaceholderConfig.class).fruit).isEqualTo("banana")); + } + + @Test + void whenThereIsAUserDefinedPropertySourcesPlaceholderConfigurerThenItIsUsedForBeanDefinitionPlaceholderResolution() { + this.contextRunner.withPropertyValues("fruit:banana").withInitializer(this::definePlaceholderBean) + .withConfiguration(AutoConfigurations.of(PropertyPlaceholderAutoConfiguration.class)) + .withUserConfiguration(PlaceholdersOverride.class) + .run((context) -> assertThat(context.getBean(PlaceholderBean.class).fruit).isEqualTo("orange")); + } + + @Test + void whenThereIsAUserDefinedPropertySourcesPlaceholderConfigurerThenItIsUsedForValuePlaceholderResolution() { + this.contextRunner.withPropertyValues("fruit:banana") + .withConfiguration(AutoConfigurations.of(PropertyPlaceholderAutoConfiguration.class)) + .withUserConfiguration(PlaceholderConfig.class, PlaceholdersOverride.class) + .run((context) -> assertThat(context.getBean(PlaceholderConfig.class).fruit).isEqualTo("orange")); + } + + private void definePlaceholderBean(ConfigurableApplicationContext context) { + ((BeanDefinitionRegistry) context.getBeanFactory()).registerBeanDefinition("placeholderBean", + BeanDefinitionBuilder.genericBeanDefinition(PlaceholderBean.class) + .addConstructorArgValue("${fruit:apple}").getBeanDefinition()); } @Configuration(proxyBeanMethods = false) static class PlaceholderConfig { - @Value("${foo:bar}") - private String foo; + @Value("${fruit:apple}") + private String fruit; + + } + + static class PlaceholderBean { + + private final String fruit; - String getFoo() { - return this.foo; + PlaceholderBean(String fruit) { + this.fruit = fruit; } } @@ -80,7 +114,8 @@ static class PlaceholdersOverride { @Bean static PropertySourcesPlaceholderConfigurer morePlaceholders() { PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer(); - configurer.setProperties(StringUtils.splitArrayElementsIntoProperties(new String[] { "foo=spam" }, "=")); + configurer + .setProperties(StringUtils.splitArrayElementsIntoProperties(new String[] { "fruit=orange" }, "=")); configurer.setLocalOverride(true); configurer.setOrder(0); return configurer; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfigurationIntegrationTests.java index d11bc5eda04c..9fc637c2d420 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfigurationIntegrationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfigurationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,65 +16,72 @@ package org.springframework.boot.autoconfigure.couchbase; +import java.time.Duration; + +import com.couchbase.client.core.diagnostics.ClusterState; +import com.couchbase.client.core.diagnostics.DiagnosticsResult; import com.couchbase.client.java.Bucket; import com.couchbase.client.java.Cluster; -import com.couchbase.client.java.CouchbaseBucket; -import com.couchbase.client.java.cluster.ClusterInfo; -import com.couchbase.client.java.env.CouchbaseEnvironment; +import com.couchbase.client.java.Collection; +import com.couchbase.client.java.env.ClusterEnvironment; +import com.couchbase.client.java.json.JsonObject; +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; +import org.testcontainers.couchbase.BucketDefinition; +import org.testcontainers.couchbase.CouchbaseContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; /** * Integration tests for {@link CouchbaseAutoConfiguration}. * * @author Stephane Nicoll + * @author Brian Clozel */ -@ExtendWith(LocalCouchbaseServer.class) +@Testcontainers(disabledWithoutDocker = true) class CouchbaseAutoConfigurationIntegrationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration( - AutoConfigurations.of(PropertyPlaceholderAutoConfiguration.class, CouchbaseAutoConfiguration.class)); + private static final String BUCKET_NAME = "cbbucket"; + + @Container + static final CouchbaseContainer couchbase = new CouchbaseContainer(DockerImageNames.couchbase()) + .withCredentials("spring", "password").withStartupAttempts(5).withStartupTimeout(Duration.ofMinutes(10)) + .withBucket(new BucketDefinition(BUCKET_NAME).withPrimaryIndex(false)); + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(CouchbaseAutoConfiguration.class)) + .withPropertyValues("spring.couchbase.connection-string: " + couchbase.getConnectionString(), + "spring.couchbase.username:spring", "spring.couchbase.password:password", + "spring.couchbase.bucket.name:" + BUCKET_NAME); @Test void defaultConfiguration() { - this.contextRunner.withPropertyValues("spring.couchbase.bootstrapHosts=localhost") - .run((context) -> assertThat(context).hasSingleBean(Cluster.class).hasSingleBean(ClusterInfo.class) - .hasSingleBean(CouchbaseEnvironment.class).hasSingleBean(Bucket.class)); + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(Cluster.class).hasSingleBean(ClusterEnvironment.class); + Cluster cluster = context.getBean(Cluster.class); + Bucket bucket = cluster.bucket(BUCKET_NAME); + bucket.waitUntilReady(Duration.ofMinutes(5)); + DiagnosticsResult diagnostics = cluster.diagnostics(); + assertThat(diagnostics.state()).isEqualTo(ClusterState.ONLINE); + }); } @Test - void customConfiguration() { - this.contextRunner.withUserConfiguration(CustomConfiguration.class) - .withPropertyValues("spring.couchbase.bootstrapHosts=localhost").run((context) -> { - assertThat(context.getBeansOfType(Cluster.class)).hasSize(2); - assertThat(context.getBeansOfType(ClusterInfo.class)).hasSize(1); - assertThat(context.getBeansOfType(CouchbaseEnvironment.class)).hasSize(1); - assertThat(context.getBeansOfType(Bucket.class)).hasSize(2); - }); - } - - @Configuration(proxyBeanMethods = false) - static class CustomConfiguration { - - @Bean - Cluster myCustomCouchbaseCluster() { - return mock(Cluster.class); - } - - @Bean - Bucket myCustomCouchbaseClient() { - return mock(CouchbaseBucket.class); - } - + void whenCouchbaseIsUsingCustomObjectMapperThenJsonCanBeRoundTripped() { + this.contextRunner.withBean(ObjectMapper.class, ObjectMapper::new).run((context) -> { + Cluster cluster = context.getBean(Cluster.class); + Bucket bucket = cluster.bucket(BUCKET_NAME); + bucket.waitUntilReady(Duration.ofMinutes(5)); + Collection collection = bucket.defaultCollection(); + collection.insert("test-document", JsonObject.create().put("a", "alpha")); + assertThat(collection.get("test-document").contentAsObject().get("a")).isEqualTo("alpha"); + }); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfigurationTests.java index 8859376f18dc..bec1f367bb82 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,20 +16,27 @@ package org.springframework.boot.autoconfigure.couchbase; +import java.time.Duration; +import java.util.HashSet; +import java.util.Set; import java.util.function.Consumer; -import com.couchbase.client.java.Bucket; +import com.couchbase.client.core.env.IoConfig; +import com.couchbase.client.core.env.SecurityConfig; +import com.couchbase.client.core.env.TimeoutConfig; import com.couchbase.client.java.Cluster; -import com.couchbase.client.java.CouchbaseBucket; -import com.couchbase.client.java.cluster.ClusterInfo; -import com.couchbase.client.java.env.CouchbaseEnvironment; -import com.couchbase.client.java.env.DefaultCouchbaseEnvironment; +import com.couchbase.client.java.codec.JacksonJsonSerializer; +import com.couchbase.client.java.codec.JsonSerializer; +import com.couchbase.client.java.env.ClusterEnvironment; +import com.couchbase.client.java.json.JsonValueModule; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import static org.assertj.core.api.Assertions.assertThat; @@ -43,157 +50,133 @@ */ class CouchbaseAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration( - AutoConfigurations.of(PropertyPlaceholderAutoConfiguration.class, CouchbaseAutoConfiguration.class)); + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(CouchbaseAutoConfiguration.class)); @Test - void bootstrapHostsIsRequired() { - this.contextRunner.run(this::assertNoCouchbaseBeans); + void connectionStringIsRequired() { + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(ClusterEnvironment.class) + .doesNotHaveBean(Cluster.class)); } @Test - void bootstrapHostsNotRequiredIfCouchbaseConfigurerIsSet() { - this.contextRunner.withUserConfiguration(CouchbaseTestConfigurer.class).run((context) -> { - assertThat(context).hasSingleBean(CouchbaseTestConfigurer.class); - // No beans are going to be created - assertNoCouchbaseBeans(context); - }); - } - - @Test - void bootstrapHostsIgnoredIfCouchbaseConfigurerIsSet() { - this.contextRunner.withUserConfiguration(CouchbaseTestConfigurer.class) - .withPropertyValues("spring.couchbase.bootstrapHosts=localhost").run((context) -> { - assertThat(context).hasSingleBean(CouchbaseTestConfigurer.class); - assertNoCouchbaseBeans(context); + void connectionStringCreateEnvironmentAndCluster() { + this.contextRunner.withUserConfiguration(CouchbaseTestConfiguration.class) + .withPropertyValues("spring.couchbase.connection-string=localhost").run((context) -> { + assertThat(context).hasSingleBean(ClusterEnvironment.class).hasSingleBean(Cluster.class); + assertThat(context.getBean(Cluster.class)) + .isSameAs(context.getBean(CouchbaseTestConfiguration.class).couchbaseCluster()); }); } - private void assertNoCouchbaseBeans(AssertableApplicationContext context) { - // No beans are going to be created - assertThat(context).doesNotHaveBean(CouchbaseEnvironment.class).doesNotHaveBean(ClusterInfo.class) - .doesNotHaveBean(Cluster.class).doesNotHaveBean(Bucket.class); - } - @Test - void customizeEnvEndpoints() { - testCouchbaseEnv((env) -> { - assertThat(env.kvServiceConfig().minEndpoints()).isEqualTo(2); - assertThat(env.kvServiceConfig().maxEndpoints()).isEqualTo(2); - assertThat(env.queryServiceConfig().minEndpoints()).isEqualTo(3); - assertThat(env.queryServiceConfig().maxEndpoints()).isEqualTo(5); - assertThat(env.viewServiceConfig().minEndpoints()).isEqualTo(4); - assertThat(env.viewServiceConfig().maxEndpoints()).isEqualTo(6); - }, "spring.couchbase.env.endpoints.key-value=2", "spring.couchbase.env.endpoints.queryservice.min-endpoints=3", - "spring.couchbase.env.endpoints.queryservice.max-endpoints=5", - "spring.couchbase.env.endpoints.viewservice.min-endpoints=4", - "spring.couchbase.env.endpoints.viewservice.max-endpoints=6"); + void whenObjectMapperBeanIsDefinedThenClusterEnvironmentObjectMapperIsDerivedFromIt() { + this.contextRunner.withUserConfiguration(CouchbaseTestConfiguration.class) + .withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class)) + .withPropertyValues("spring.couchbase.connection-string=localhost").run((context) -> { + ClusterEnvironment env = context.getBean(ClusterEnvironment.class); + Set expectedModuleIds = new HashSet<>( + context.getBean(ObjectMapper.class).getRegisteredModuleIds()); + expectedModuleIds.add(new JsonValueModule().getTypeId()); + JsonSerializer serializer = env.jsonSerializer(); + assertThat(serializer).extracting("wrapped").isInstanceOf(JacksonJsonSerializer.class) + .extracting("mapper").asInstanceOf(InstanceOfAssertFactories.type(ObjectMapper.class)) + .extracting(ObjectMapper::getRegisteredModuleIds).isEqualTo(expectedModuleIds); + }); } @Test - void customizeEnvEndpointsUsesNewInfrastructure() { - testCouchbaseEnv((env) -> { - assertThat(env.queryServiceConfig().minEndpoints()).isEqualTo(3); - assertThat(env.queryServiceConfig().maxEndpoints()).isEqualTo(5); - assertThat(env.viewServiceConfig().minEndpoints()).isEqualTo(4); - assertThat(env.viewServiceConfig().maxEndpoints()).isEqualTo(6); - }, "spring.couchbase.env.endpoints.queryservice.min-endpoints=3", - "spring.couchbase.env.endpoints.queryservice.max-endpoints=5", - "spring.couchbase.env.endpoints.viewservice.min-endpoints=4", - "spring.couchbase.env.endpoints.viewservice.max-endpoints=6"); + void customizeJsonSerializer() { + JsonSerializer customJsonSerializer = mock(JsonSerializer.class); + this.contextRunner.withUserConfiguration(CouchbaseTestConfiguration.class) + .withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class)) + .withBean(ClusterEnvironmentBuilderCustomizer.class, + () -> (builder) -> builder.jsonSerializer(customJsonSerializer)) + .withPropertyValues("spring.couchbase.connection-string=localhost").run((context) -> { + ClusterEnvironment env = context.getBean(ClusterEnvironment.class); + JsonSerializer serializer = env.jsonSerializer(); + assertThat(serializer).extracting("wrapped").isSameAs(customJsonSerializer); + }); } @Test - void customizeEnvEndpointsUsesNewInfrastructureWithOnlyMax() { - testCouchbaseEnv((env) -> { - assertThat(env.queryServiceConfig().minEndpoints()).isEqualTo(1); - assertThat(env.queryServiceConfig().maxEndpoints()).isEqualTo(5); - assertThat(env.viewServiceConfig().minEndpoints()).isEqualTo(1); - assertThat(env.viewServiceConfig().maxEndpoints()).isEqualTo(6); - }, "spring.couchbase.env.endpoints.queryservice.max-endpoints=5", - "spring.couchbase.env.endpoints.viewservice.max-endpoints=6"); + void customizeEnvIo() { + testClusterEnvironment((env) -> { + IoConfig ioConfig = env.ioConfig(); + assertThat(ioConfig.numKvConnections()).isEqualTo(2); + assertThat(ioConfig.maxHttpConnections()).isEqualTo(5); + assertThat(ioConfig.idleHttpConnectionTimeout()).isEqualTo(Duration.ofSeconds(3)); + }, "spring.couchbase.env.io.min-endpoints=2", "spring.couchbase.env.io.max-endpoints=5", + "spring.couchbase.env.io.idle-http-connection-timeout=3s"); } @Test void customizeEnvTimeouts() { - testCouchbaseEnv((env) -> { - assertThat(env.connectTimeout()).isEqualTo(100); - assertThat(env.kvTimeout()).isEqualTo(200); - assertThat(env.queryTimeout()).isEqualTo(300); - assertThat(env.socketConnectTimeout()).isEqualTo(400); - assertThat(env.viewTimeout()).isEqualTo(500); - }, "spring.couchbase.env.timeouts.connect=100", "spring.couchbase.env.timeouts.keyValue=200", - "spring.couchbase.env.timeouts.query=300", "spring.couchbase.env.timeouts.socket-connect=400", - "spring.couchbase.env.timeouts.view=500"); + testClusterEnvironment((env) -> { + TimeoutConfig timeoutConfig = env.timeoutConfig(); + assertThat(timeoutConfig.connectTimeout()).isEqualTo(Duration.ofSeconds(1)); + assertThat(timeoutConfig.disconnectTimeout()).isEqualTo(Duration.ofSeconds(2)); + assertThat(timeoutConfig.kvTimeout()).isEqualTo(Duration.ofMillis(500)); + assertThat(timeoutConfig.kvDurableTimeout()).isEqualTo(Duration.ofMillis(750)); + assertThat(timeoutConfig.queryTimeout()).isEqualTo(Duration.ofSeconds(3)); + assertThat(timeoutConfig.viewTimeout()).isEqualTo(Duration.ofSeconds(4)); + assertThat(timeoutConfig.searchTimeout()).isEqualTo(Duration.ofSeconds(5)); + assertThat(timeoutConfig.analyticsTimeout()).isEqualTo(Duration.ofSeconds(6)); + assertThat(timeoutConfig.managementTimeout()).isEqualTo(Duration.ofSeconds(7)); + }, "spring.couchbase.env.timeouts.connect=1s", "spring.couchbase.env.timeouts.disconnect=2s", + "spring.couchbase.env.timeouts.key-value=500ms", + "spring.couchbase.env.timeouts.key-value-durable=750ms", "spring.couchbase.env.timeouts.query=3s", + "spring.couchbase.env.timeouts.view=4s", "spring.couchbase.env.timeouts.search=5s", + "spring.couchbase.env.timeouts.analytics=6s", "spring.couchbase.env.timeouts.management=7s"); } @Test void enableSslNoEnabledFlag() { - testCouchbaseEnv((env) -> { - assertThat(env.sslEnabled()).isTrue(); - assertThat(env.sslKeystoreFile()).isEqualTo("foo"); - assertThat(env.sslKeystorePassword()).isEqualTo("secret"); - }, "spring.couchbase.env.ssl.keyStore=foo", "spring.couchbase.env.ssl.keyStorePassword=secret"); + testClusterEnvironment((env) -> { + SecurityConfig securityConfig = env.securityConfig(); + assertThat(securityConfig.tlsEnabled()).isTrue(); + assertThat(securityConfig.trustManagerFactory()).isNotNull(); + }, "spring.couchbase.env.ssl.keyStore=classpath:test.jks", "spring.couchbase.env.ssl.keyStorePassword=secret"); } @Test void disableSslEvenWithKeyStore() { - testCouchbaseEnv((env) -> { - assertThat(env.sslEnabled()).isFalse(); - assertThat(env.sslKeystoreFile()).isNull(); - assertThat(env.sslKeystorePassword()).isNull(); - }, "spring.couchbase.env.ssl.enabled=false", "spring.couchbase.env.ssl.keyStore=foo", + testClusterEnvironment((env) -> { + SecurityConfig securityConfig = env.securityConfig(); + assertThat(securityConfig.tlsEnabled()).isFalse(); + assertThat(securityConfig.trustManagerFactory()).isNull(); + }, "spring.couchbase.env.ssl.enabled=false", "spring.couchbase.env.ssl.keyStore=classpath:test.jks", "spring.couchbase.env.ssl.keyStorePassword=secret"); } - private void testCouchbaseEnv(Consumer environmentConsumer, String... environment) { - this.contextRunner.withUserConfiguration(CouchbaseTestConfigurer.class).withPropertyValues(environment) - .run((context) -> { - CouchbaseProperties properties = context.getBean(CouchbaseProperties.class); - DefaultCouchbaseEnvironment env = new CouchbaseConfiguration(properties) - .initializeEnvironmentBuilder(properties).build(); - environmentConsumer.accept(env); - }); + private void testClusterEnvironment(Consumer environmentConsumer, String... environment) { + this.contextRunner.withUserConfiguration(CouchbaseTestConfiguration.class) + .withPropertyValues("spring.couchbase.connection-string=localhost").withPropertyValues(environment) + .run((context) -> environmentConsumer.accept(context.getBean(ClusterEnvironment.class))); } @Test void customizeEnvWithCustomCouchbaseConfiguration() { - this.contextRunner.withUserConfiguration(CustomCouchbaseConfiguration.class) - .withPropertyValues("spring.couchbase.bootstrap-hosts=localhost", + this.contextRunner + .withUserConfiguration(CouchbaseTestConfiguration.class, + ClusterEnvironmentCustomizerConfiguration.class) + .withPropertyValues("spring.couchbase.connection-string=localhost", "spring.couchbase.env.timeouts.connect=100") .run((context) -> { - assertThat(context).hasSingleBean(CouchbaseConfiguration.class); - DefaultCouchbaseEnvironment env = context.getBean(DefaultCouchbaseEnvironment.class); - assertThat(env.socketConnectTimeout()).isEqualTo(5000); - assertThat(env.connectTimeout()).isEqualTo(2000); + assertThat(context).hasSingleBean(ClusterEnvironment.class); + ClusterEnvironment env = context.getBean(ClusterEnvironment.class); + assertThat(env.timeoutConfig().kvTimeout()).isEqualTo(Duration.ofSeconds(5)); + assertThat(env.timeoutConfig().connectTimeout()).isEqualTo(Duration.ofSeconds(2)); }); } - @Configuration - static class CustomCouchbaseConfiguration extends CouchbaseConfiguration { - - CustomCouchbaseConfiguration(CouchbaseProperties properties) { - super(properties); - } - - @Override - protected DefaultCouchbaseEnvironment.Builder initializeEnvironmentBuilder(CouchbaseProperties properties) { - return super.initializeEnvironmentBuilder(properties).socketConnectTimeout(5000).connectTimeout(2000); - } - - @Override - public Cluster couchbaseCluster() { - return mock(Cluster.class); - } - - @Override - public ClusterInfo couchbaseClusterInfo() { - return mock(ClusterInfo.class); - } + @Configuration(proxyBeanMethods = false) + static class ClusterEnvironmentCustomizerConfiguration { - @Override - public Bucket couchbaseClient() { - return mock(CouchbaseBucket.class); + @Bean + ClusterEnvironmentBuilderCustomizer clusterEnvironmentBuilderCustomizer() { + return (builder) -> builder.timeoutConfig().kvTimeout(Duration.ofSeconds(5)) + .connectTimeout(Duration.ofSeconds(2)); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbasePropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbasePropertiesTests.java new file mode 100644 index 000000000000..a2bd89b4942b --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbasePropertiesTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.couchbase; + +import com.couchbase.client.core.env.IoConfig; +import com.couchbase.client.core.env.TimeoutConfig; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.couchbase.CouchbaseProperties.Io; +import org.springframework.boot.autoconfigure.couchbase.CouchbaseProperties.Timeouts; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link CouchbaseProperties}. + * + * @author Stephane Nicoll + */ +class CouchbasePropertiesTests { + + @Test + void ioHaveConsistentDefaults() { + Io io = new CouchbaseProperties().getEnv().getIo(); + assertThat(io.getMinEndpoints()).isEqualTo(IoConfig.DEFAULT_NUM_KV_CONNECTIONS); + assertThat(io.getMaxEndpoints()).isEqualTo(IoConfig.DEFAULT_MAX_HTTP_CONNECTIONS); + assertThat(io.getIdleHttpConnectionTimeout()).isEqualTo(IoConfig.DEFAULT_IDLE_HTTP_CONNECTION_TIMEOUT); + } + + @Test + void timeoutsHaveConsistentDefaults() { + Timeouts timeouts = new CouchbaseProperties().getEnv().getTimeouts(); + assertThat(timeouts.getConnect()).isEqualTo(TimeoutConfig.DEFAULT_CONNECT_TIMEOUT); + assertThat(timeouts.getDisconnect()).isEqualTo(TimeoutConfig.DEFAULT_DISCONNECT_TIMEOUT); + assertThat(timeouts.getKeyValue()).isEqualTo(TimeoutConfig.DEFAULT_KV_TIMEOUT); + assertThat(timeouts.getKeyValueDurable()).isEqualTo(TimeoutConfig.DEFAULT_KV_DURABLE_TIMEOUT); + assertThat(timeouts.getQuery()).isEqualTo(TimeoutConfig.DEFAULT_QUERY_TIMEOUT); + assertThat(timeouts.getView()).isEqualTo(TimeoutConfig.DEFAULT_VIEW_TIMEOUT); + assertThat(timeouts.getSearch()).isEqualTo(TimeoutConfig.DEFAULT_SEARCH_TIMEOUT); + assertThat(timeouts.getAnalytics()).isEqualTo(TimeoutConfig.DEFAULT_ANALYTICS_TIMEOUT); + assertThat(timeouts.getManagement()).isEqualTo(TimeoutConfig.DEFAULT_MANAGEMENT_TIMEOUT); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseTestConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseTestConfiguration.java new file mode 100644 index 000000000000..b2b1cf8c8523 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseTestConfiguration.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.couchbase; + +import com.couchbase.client.java.Cluster; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.mockito.Mockito.mock; + +/** + * Test configuration for couchbase that mocks access. + * + * @author Stephane Nicoll + */ +@Configuration(proxyBeanMethods = false) +class CouchbaseTestConfiguration { + + private final Cluster cluster = mock(Cluster.class); + + @Bean + Cluster couchbaseCluster() { + return this.cluster; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseTestConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseTestConfigurer.java deleted file mode 100644 index dd6a362af207..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseTestConfigurer.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.couchbase; - -import com.couchbase.client.java.Bucket; -import com.couchbase.client.java.Cluster; -import com.couchbase.client.java.CouchbaseBucket; -import com.couchbase.client.java.cluster.ClusterInfo; -import com.couchbase.client.java.env.CouchbaseEnvironment; - -import org.springframework.data.couchbase.config.CouchbaseConfigurer; -import org.springframework.stereotype.Component; - -import static org.mockito.Mockito.mock; - -/** - * Test configurer for couchbase that mocks access. - * - * @author Stephane Nicoll - */ -@Component -public class CouchbaseTestConfigurer implements CouchbaseConfigurer { - - @Override - public CouchbaseEnvironment couchbaseEnvironment() throws Exception { - return mock(CouchbaseEnvironment.class); - } - - @Override - public Cluster couchbaseCluster() throws Exception { - return mock(Cluster.class); - } - - @Override - public ClusterInfo couchbaseClusterInfo() { - return mock(ClusterInfo.class); - } - - @Override - public Bucket couchbaseClient() { - return mock(CouchbaseBucket.class); - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/LocalCouchbaseServer.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/LocalCouchbaseServer.java deleted file mode 100644 index 80103bb32cb8..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/LocalCouchbaseServer.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.couchbase; - -import java.util.concurrent.TimeUnit; - -import com.couchbase.client.java.Bucket; -import com.couchbase.client.java.Cluster; -import com.couchbase.client.java.CouchbaseCluster; -import com.couchbase.client.java.env.CouchbaseEnvironment; -import com.couchbase.client.java.env.DefaultCouchbaseEnvironment; -import org.junit.jupiter.api.extension.ConditionEvaluationResult; -import org.junit.jupiter.api.extension.ExecutionCondition; -import org.junit.jupiter.api.extension.Extension; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.extension.TestExecutionExceptionHandler; - -import org.springframework.beans.factory.BeanCreationException; - -/** - * {@link Extension} for working with an optional Couchbase server. Expects a default - * {@link Bucket} with no password to be available on localhost. - * - * @author Stephane Nicoll - * @author Andy Wilkinson - */ -class LocalCouchbaseServer implements ExecutionCondition, TestExecutionExceptionHandler { - - @Override - public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { - try { - CouchbaseEnvironment environment = DefaultCouchbaseEnvironment.create(); - Cluster cluster = CouchbaseCluster.create(environment, "localhost"); - testConnection(cluster); - cluster.disconnect(); - environment.shutdownAsync(); - return ConditionEvaluationResult.enabled("Local Couchbase server available"); - } - catch (Exception ex) { - return ConditionEvaluationResult.disabled("Local Couchbase server not available"); - } - } - - private static void testConnection(Cluster cluster) { - Bucket bucket = cluster.openBucket(2, TimeUnit.SECONDS); - bucket.close(); - } - - @Override - public void handleTestExecutionException(ExtensionContext context, Throwable ex) throws Throwable { - if ((ex instanceof BeanCreationException) - && "couchbaseClient".equals(((BeanCreationException) ex).getBeanName())) { - return; - } - throw ex; - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/OnBootstrapHostsConditionTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/OnBootstrapHostsConditionTests.java deleted file mode 100644 index 5f3d3c9bd7bb..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/OnBootstrapHostsConditionTests.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.couchbase; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Conditional; -import org.springframework.context.annotation.Configuration; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link OnBootstrapHostsCondition}. - * - * @author Stephane Nicoll - */ -class OnBootstrapHostsConditionTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withUserConfiguration(TestConfig.class); - - @Test - void bootstrapHostsNotDefined() { - this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean("foo")); - } - - @Test - void bootstrapHostsDefinedAsCommaSeparated() { - this.contextRunner.withPropertyValues("spring.couchbase.bootstrap-hosts=value1") - .run((context) -> assertThat(context).hasBean("foo")); - } - - @Test - void bootstrapHostsDefinedAsList() { - this.contextRunner.withPropertyValues("spring.couchbase.bootstrap-hosts[0]=value1") - .run((context) -> assertThat(context).hasBean("foo")); - } - - @Configuration(proxyBeanMethods = false) - @Conditional(OnBootstrapHostsCondition.class) - static class TestConfig { - - @Bean - String foo() { - return "foo"; - } - - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/dao/PersistenceExceptionTranslationAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/dao/PersistenceExceptionTranslationAutoConfigurationTests.java index 4ae58190ea2d..c3ef53ba17e1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/dao/PersistenceExceptionTranslationAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/dao/PersistenceExceptionTranslationAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** * Tests for {@link PersistenceExceptionTranslationAutoConfiguration} @@ -86,15 +87,12 @@ void exceptionTranslationPostProcessorCanBeDisabled() { assertThat(beans).isEmpty(); } - // @Test - // public void - // persistOfNullThrowsIllegalArgumentExceptionWithoutExceptionTranslation() { - // this.context = new AnnotationConfigApplicationContext( - // EmbeddedDataSourceConfiguration.class, - // HibernateJpaAutoConfiguration.class, TestConfiguration.class); - // assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy( - // () -> this.context.getBean(TestRepository.class).doSomething()); - // } + @Test + void persistOfNullThrowsIllegalArgumentExceptionWithoutExceptionTranslation() { + this.context = new AnnotationConfigApplicationContext(EmbeddedDataSourceConfiguration.class, + HibernateJpaAutoConfiguration.class, TestConfiguration.class); + assertThatIllegalArgumentException().isThrownBy(() -> this.context.getBean(TestRepository.class).doSomething()); + } @Test void persistOfNullThrowsInvalidDataAccessApiUsageExceptionWithExceptionTranslation() { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/alt/solr/CitySolrRepository.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/alt/solr/CitySolrRepository.java deleted file mode 100644 index 65bf163ae9af..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/alt/solr/CitySolrRepository.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.data.alt.solr; - -import org.springframework.boot.autoconfigure.data.solr.city.City; -import org.springframework.data.repository.Repository; - -public interface CitySolrRepository extends Repository { - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraDataAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraDataAutoConfigurationIntegrationTests.java index c3c51c6a3de1..80392014d19e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraDataAutoConfigurationIntegrationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraDataAutoConfigurationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,24 +16,23 @@ package org.springframework.boot.autoconfigure.data.cassandra; -import java.time.Duration; - -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.Session; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.CqlSessionBuilder; import org.junit.jupiter.api.Test; -import org.testcontainers.containers.CassandraContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.boot.autoconfigure.AutoConfigurationPackages; +import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; import org.springframework.boot.autoconfigure.data.cassandra.city.City; -import org.springframework.boot.test.util.TestPropertyValues; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.data.cassandra.config.CassandraSessionFactoryBean; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.testsupport.testcontainers.CassandraContainer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.data.cassandra.config.SchemaAction; +import org.springframework.data.cassandra.config.SessionFactoryFactoryBean; import static org.assertj.core.api.Assertions.assertThat; @@ -47,57 +46,47 @@ class CassandraDataAutoConfigurationIntegrationTests { @Container - static final CassandraContainer cassandra = new CassandraContainer<>().withStartupAttempts(5) - .withStartupTimeout(Duration.ofMinutes(2)); - - private AnnotationConfigApplicationContext context; - - @BeforeEach - void setUp() { - this.context = new AnnotationConfigApplicationContext(); - TestPropertyValues - .of("spring.data.cassandra.port=" + cassandra.getFirstMappedPort(), - "spring.data.cassandra.read-timeout=24000", "spring.data.cassandra.connect-timeout=10000") - .applyTo(this.context.getEnvironment()); - } + static final CassandraContainer cassandra = new CassandraContainer(); - @AfterEach - void close() { - if (this.context != null) { - this.context.close(); - } - } + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of(CassandraAutoConfiguration.class, CassandraDataAutoConfiguration.class)) + .withPropertyValues( + "spring.data.cassandra.contact-points:" + cassandra.getHost() + ":" + + cassandra.getFirstMappedPort(), + "spring.data.cassandra.local-datacenter=datacenter1", + "spring.data.cassandra.connection.connect-timeout=60s", + "spring.data.cassandra.connection.init-query-timeout=60s", + "spring.data.cassandra.request.timeout=60s") + .withInitializer((context) -> AutoConfigurationPackages.register((BeanDefinitionRegistry) context, + City.class.getPackage().getName())); @Test void hasDefaultSchemaActionSet() { - String cityPackage = City.class.getPackage().getName(); - AutoConfigurationPackages.register(this.context, cityPackage); - this.context.register(CassandraAutoConfiguration.class, CassandraDataAutoConfiguration.class); - this.context.refresh(); - CassandraSessionFactoryBean bean = this.context.getBean(CassandraSessionFactoryBean.class); - assertThat(bean.getSchemaAction()).isEqualTo(SchemaAction.NONE); + this.contextRunner.run((context) -> assertThat(context.getBean(SessionFactoryFactoryBean.class)) + .hasFieldOrPropertyWithValue("schemaAction", SchemaAction.NONE)); } @Test void hasRecreateSchemaActionSet() { - createTestKeyspaceIfNotExists(); - String cityPackage = City.class.getPackage().getName(); - AutoConfigurationPackages.register(this.context, cityPackage); - TestPropertyValues.of("spring.data.cassandra.schemaAction=recreate_drop_unused", - "spring.data.cassandra.keyspaceName=boot_test").applyTo(this.context); - this.context.register(CassandraAutoConfiguration.class, CassandraDataAutoConfiguration.class); - this.context.refresh(); - CassandraSessionFactoryBean bean = this.context.getBean(CassandraSessionFactoryBean.class); - assertThat(bean.getSchemaAction()).isEqualTo(SchemaAction.RECREATE_DROP_UNUSED); + this.contextRunner.withUserConfiguration(KeyspaceTestConfiguration.class) + .withPropertyValues("spring.data.cassandra.schemaAction=recreate_drop_unused") + .run((context) -> assertThat(context.getBean(SessionFactoryFactoryBean.class)) + .hasFieldOrPropertyWithValue("schemaAction", SchemaAction.RECREATE_DROP_UNUSED)); } - private void createTestKeyspaceIfNotExists() { - Cluster cluster = Cluster.builder().withoutJMXReporting().withPort(cassandra.getFirstMappedPort()) - .addContactPoint(cassandra.getContainerIpAddress()).build(); - try (Session session = cluster.connect()) { - session.execute("CREATE KEYSPACE IF NOT EXISTS boot_test" - + " WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };"); + @Configuration(proxyBeanMethods = false) + static class KeyspaceTestConfiguration { + + @Bean + CqlSession cqlSession(CqlSessionBuilder cqlSessionBuilder) { + try (CqlSession session = cqlSessionBuilder.build()) { + session.execute("CREATE KEYSPACE IF NOT EXISTS boot_test" + + " WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };"); + } + return cqlSessionBuilder.withKeyspace("boot_test").build(); } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraDataAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraDataAutoConfigurationTests.java index fa2a4c82f747..c9d6ef743435 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraDataAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraDataAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import java.util.Collections; import java.util.Set; -import com.datastax.driver.core.Session; +import com.datastax.oss.driver.api.core.CqlSession; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -29,11 +29,10 @@ import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.FilterType; import org.springframework.core.convert.converter.Converter; import org.springframework.data.cassandra.core.CassandraTemplate; +import org.springframework.data.cassandra.core.convert.CassandraConverter; import org.springframework.data.cassandra.core.convert.CassandraCustomConversions; import org.springframework.data.cassandra.core.mapping.CassandraMappingContext; import org.springframework.data.cassandra.core.mapping.SimpleUserTypeResolver; @@ -41,7 +40,6 @@ import org.springframework.util.ObjectUtils; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; /** * Tests for {@link CassandraDataAutoConfiguration}. @@ -63,7 +61,7 @@ void close() { @Test void templateExists() { - load(TestExcludeConfiguration.class); + load(CassandraMockConfiguration.class); assertThat(this.context.getBeanNamesForType(CassandraTemplate.class)).hasSize(1); } @@ -80,9 +78,16 @@ void entityScanShouldSetInitialEntitySet() { @Test void userTypeResolverShouldBeSet() { load(); - CassandraMappingContext mappingContext = this.context.getBean(CassandraMappingContext.class); - assertThat(ReflectionTestUtils.getField(mappingContext, "userTypeResolver")) - .isInstanceOf(SimpleUserTypeResolver.class); + CassandraConverter cassandraConverter = this.context.getBean(CassandraConverter.class); + assertThat(cassandraConverter).extracting("userTypeResolver").isInstanceOf(SimpleUserTypeResolver.class); + } + + @Test + void codecRegistryShouldBeSet() { + load(); + CassandraConverter cassandraConverter = this.context.getBean(CassandraConverter.class); + assertThat(cassandraConverter.getCodecRegistry()) + .isSameAs(this.context.getBean(CassandraMockConfiguration.class).codecRegistry); } @Test @@ -97,13 +102,12 @@ void customConversions() { load(CustomConversionConfig.class); CassandraTemplate template = this.context.getBean(CassandraTemplate.class); assertThat(template.getConverter().getConversionService().canConvert(Person.class, String.class)).isTrue(); - } @Test void clusterDoesNotExist() { this.context = new AnnotationConfigApplicationContext(CassandraDataAutoConfiguration.class); - assertThat(this.context.getBeansOfType(Session.class)).isEmpty(); + assertThat(this.context.getBeansOfType(CqlSession.class)).isEmpty(); } void load(Class... config) { @@ -112,28 +116,12 @@ void load(Class... config) { if (!ObjectUtils.isEmpty(config)) { ctx.register(config); } - ctx.register(TestConfiguration.class, CassandraAutoConfiguration.class, CassandraDataAutoConfiguration.class); + ctx.register(CassandraMockConfiguration.class, CassandraAutoConfiguration.class, + CassandraDataAutoConfiguration.class); ctx.refresh(); this.context = ctx; } - @Configuration(proxyBeanMethods = false) - @ComponentScan( - excludeFilters = @ComponentScan.Filter(classes = { Session.class }, type = FilterType.ASSIGNABLE_TYPE)) - static class TestExcludeConfiguration { - - } - - @Configuration(proxyBeanMethods = false) - static class TestConfiguration { - - @Bean - Session getObject() { - return mock(Session.class); - } - - } - @Configuration(proxyBeanMethods = false) @EntityScan("org.springframework.boot.autoconfigure.data.cassandra.city") static class EntityScanConfig { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraMockConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraMockConfiguration.java new file mode 100644 index 000000000000..dc74693830b2 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraMockConfiguration.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.cassandra; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Test configuration that mocks access to Cassandra. + * + * @author Stephane Nicoll + */ +@Configuration(proxyBeanMethods = false) +class CassandraMockConfiguration { + + final CodecRegistry codecRegistry = mock(CodecRegistry.class); + + @Bean + CqlSession cqlSession() { + DriverContext context = mock(DriverContext.class); + given(context.getCodecRegistry()).willReturn(this.codecRegistry); + CqlSession cqlSession = mock(CqlSession.class); + given(cqlSession.getContext()).willReturn(context); + return cqlSession; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraReactiveDataAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraReactiveDataAutoConfigurationTests.java index bc2032d85919..c546888c0093 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraReactiveDataAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraReactiveDataAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ import java.util.Set; -import com.datastax.driver.core.Session; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -27,15 +26,14 @@ import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.cassandra.core.ReactiveCassandraTemplate; +import org.springframework.data.cassandra.core.convert.CassandraConverter; import org.springframework.data.cassandra.core.mapping.CassandraMappingContext; import org.springframework.data.cassandra.core.mapping.SimpleUserTypeResolver; import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; /** * Tests for {@link CassandraReactiveDataAutoConfiguration}. @@ -74,9 +72,8 @@ void entityScanShouldSetInitialEntitySet() { @Test void userTypeResolverShouldBeSet() { load("spring.data.cassandra.keyspaceName:boot_test"); - CassandraMappingContext mappingContext = this.context.getBean(CassandraMappingContext.class); - assertThat(ReflectionTestUtils.getField(mappingContext, "userTypeResolver")) - .isInstanceOf(SimpleUserTypeResolver.class); + CassandraConverter cassandraConverter = this.context.getBean(CassandraConverter.class); + assertThat(cassandraConverter).extracting("userTypeResolver").isInstanceOf(SimpleUserTypeResolver.class); } private void load(String... environment) { @@ -89,22 +86,12 @@ private void load(Class config, String... environment) { if (config != null) { ctx.register(config); } - ctx.register(TestConfiguration.class, CassandraAutoConfiguration.class, CassandraDataAutoConfiguration.class, - CassandraReactiveDataAutoConfiguration.class); + ctx.register(CassandraMockConfiguration.class, CassandraAutoConfiguration.class, + CassandraDataAutoConfiguration.class, CassandraReactiveDataAutoConfiguration.class); ctx.refresh(); this.context = ctx; } - @Configuration(proxyBeanMethods = false) - static class TestConfiguration { - - @Bean - Session session() { - return mock(Session.class); - } - - } - @Configuration(proxyBeanMethods = false) @EntityScan("org.springframework.boot.autoconfigure.data.cassandra.city") static class EntityScanConfig { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraReactiveRepositoriesAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraReactiveRepositoriesAutoConfigurationTests.java index 15a0c6c62a63..3f2b3b23f490 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraReactiveRepositoriesAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraReactiveRepositoriesAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,8 +18,7 @@ import java.util.Set; -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.Session; +import com.datastax.oss.driver.api.core.CqlSessionBuilder; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -32,18 +31,13 @@ import org.springframework.boot.autoconfigure.data.empty.EmptyDataPackage; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.FilterType; -import org.springframework.data.cassandra.ReactiveSession; +import org.springframework.context.annotation.Import; import org.springframework.data.cassandra.core.mapping.CassandraMappingContext; import org.springframework.data.cassandra.repository.config.EnableReactiveCassandraRepositories; import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; /** * Tests for {@link CassandraReactiveRepositoriesAutoConfiguration}. @@ -62,41 +56,39 @@ class CassandraReactiveRepositoriesAutoConfigurationTests { @Test void testDefaultRepositoryConfiguration() { - this.contextRunner.withUserConfiguration(TestConfiguration.class).run((context) -> { + this.contextRunner.withUserConfiguration(DefaultConfiguration.class).run((context) -> { assertThat(context).hasSingleBean(ReactiveCityRepository.class); - assertThat(context).hasSingleBean(Cluster.class); + assertThat(context).hasSingleBean(CqlSessionBuilder.class); assertThat(getInitialEntitySet(context)).hasSize(1); }); } @Test void testNoRepositoryConfiguration() { - this.contextRunner.withUserConfiguration(TestExcludeConfiguration.class, EmptyConfiguration.class) - .run((context) -> { - assertThat(context).hasSingleBean(Cluster.class); - assertThat(getInitialEntitySet(context)).hasSize(1).containsOnly(City.class); - }); + this.contextRunner.withUserConfiguration(EmptyConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(CqlSessionBuilder.class); + assertThat(getInitialEntitySet(context)).isEmpty(); + }); } @Test void doesNotTriggerDefaultRepositoryDetectionIfCustomized() { - this.contextRunner.withUserConfiguration(TestExcludeConfiguration.class, CustomizedConfiguration.class) - .run((context) -> { - assertThat(context).hasSingleBean(ReactiveCityCassandraRepository.class); - assertThat(getInitialEntitySet(context)).hasSize(1).containsOnly(City.class); - }); + this.contextRunner.withUserConfiguration(CustomizedConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(ReactiveCityCassandraRepository.class); + assertThat(getInitialEntitySet(context)).hasSize(1).containsOnly(City.class); + }); } @Test void enablingImperativeRepositoriesDisablesReactiveRepositories() { - this.contextRunner.withUserConfiguration(TestConfiguration.class) + this.contextRunner.withUserConfiguration(DefaultConfiguration.class) .withPropertyValues("spring.data.cassandra.repositories.type=imperative") .run((context) -> assertThat(context).doesNotHaveBean(ReactiveCityRepository.class)); } @Test void enablingNoRepositoriesDisablesReactiveRepositories() { - this.contextRunner.withUserConfiguration(TestConfiguration.class) + this.contextRunner.withUserConfiguration(DefaultConfiguration.class) .withPropertyValues("spring.data.cassandra.repositories.type=none") .run((context) -> assertThat(context).doesNotHaveBean(ReactiveCityRepository.class)); } @@ -108,33 +100,25 @@ private Set> getInitialEntitySet(ApplicationContext context) { } @Configuration(proxyBeanMethods = false) - @TestAutoConfigurationPackage(City.class) - static class TestConfiguration { - - @Bean - Session Session() { - return mock(Session.class); - } + @TestAutoConfigurationPackage(EmptyDataPackage.class) + @Import(CassandraMockConfiguration.class) + static class EmptyConfiguration { } @Configuration(proxyBeanMethods = false) - @TestAutoConfigurationPackage(EmptyDataPackage.class) - static class EmptyConfiguration { + @TestAutoConfigurationPackage(City.class) + @Import(CassandraMockConfiguration.class) + static class DefaultConfiguration { } @Configuration(proxyBeanMethods = false) @TestAutoConfigurationPackage(CassandraReactiveRepositoriesAutoConfigurationTests.class) @EnableReactiveCassandraRepositories(basePackageClasses = ReactiveCityCassandraRepository.class) + @Import(CassandraMockConfiguration.class) static class CustomizedConfiguration { } - @Configuration(proxyBeanMethods = false) - @ComponentScan(excludeFilters = @Filter(classes = { ReactiveSession.class }, type = FilterType.ASSIGNABLE_TYPE)) - static class TestExcludeConfiguration { - - } - } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraRepositoriesAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraRepositoriesAutoConfigurationTests.java index 022b23f3e2cd..017d1c101803 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraRepositoriesAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraRepositoriesAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,8 +18,7 @@ import java.util.Set; -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.Session; +import com.datastax.oss.driver.api.core.CqlSessionBuilder; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -32,16 +31,13 @@ import org.springframework.boot.autoconfigure.data.empty.EmptyDataPackage; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.FilterType; +import org.springframework.context.annotation.Import; import org.springframework.data.cassandra.core.mapping.CassandraMappingContext; import org.springframework.data.cassandra.repository.config.EnableCassandraRepositories; import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; /** * Tests for {@link CassandraRepositoriesAutoConfiguration}. @@ -58,41 +54,39 @@ class CassandraRepositoriesAutoConfigurationTests { @Test void testDefaultRepositoryConfiguration() { - this.contextRunner.withUserConfiguration(TestConfiguration.class).run((context) -> { + this.contextRunner.withUserConfiguration(DefaultConfiguration.class).run((context) -> { assertThat(context).hasSingleBean(CityRepository.class); - assertThat(context).hasSingleBean(Cluster.class); + assertThat(context).hasSingleBean(CqlSessionBuilder.class); assertThat(getInitialEntitySet(context)).hasSize(1); }); } @Test void testNoRepositoryConfiguration() { - this.contextRunner.withUserConfiguration(TestExcludeConfiguration.class, EmptyConfiguration.class) - .run((context) -> { - assertThat(context).hasSingleBean(Cluster.class); - assertThat(getInitialEntitySet(context)).hasSize(1).containsOnly(City.class); - }); + this.contextRunner.withUserConfiguration(EmptyConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(CqlSessionBuilder.class); + assertThat(getInitialEntitySet(context)).isEmpty(); + }); } @Test void doesNotTriggerDefaultRepositoryDetectionIfCustomized() { - this.contextRunner.withUserConfiguration(TestExcludeConfiguration.class, CustomizedConfiguration.class) - .run((context) -> { - assertThat(context).hasSingleBean(CityCassandraRepository.class); - assertThat(getInitialEntitySet(context)).hasSize(1).containsOnly(City.class); - }); + this.contextRunner.withUserConfiguration(CustomizedConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(CityCassandraRepository.class); + assertThat(getInitialEntitySet(context)).hasSize(1).containsOnly(City.class); + }); } @Test void enablingReactiveRepositoriesDisablesImperativeRepositories() { - this.contextRunner.withUserConfiguration(TestConfiguration.class) + this.contextRunner.withUserConfiguration(DefaultConfiguration.class) .withPropertyValues("spring.data.cassandra.repositories.type=reactive") .run((context) -> assertThat(context).doesNotHaveBean(CityCassandraRepository.class)); } @Test void enablingNoRepositoriesDisablesImperativeRepositories() { - this.contextRunner.withUserConfiguration(TestConfiguration.class) + this.contextRunner.withUserConfiguration(DefaultConfiguration.class) .withPropertyValues("spring.data.cassandra.repositories.type=none") .run((context) -> assertThat(context).doesNotHaveBean(CityCassandraRepository.class)); } @@ -104,34 +98,25 @@ private Set> getInitialEntitySet(AssertableApplicationContext context) } @Configuration(proxyBeanMethods = false) - @TestAutoConfigurationPackage(City.class) - static class TestConfiguration { - - @Bean - Session session() { - return mock(Session.class); - } + @TestAutoConfigurationPackage(EmptyDataPackage.class) + @Import(CassandraMockConfiguration.class) + static class EmptyConfiguration { } @Configuration(proxyBeanMethods = false) - @TestAutoConfigurationPackage(EmptyDataPackage.class) - static class EmptyConfiguration { + @TestAutoConfigurationPackage(City.class) + @Import(CassandraMockConfiguration.class) + static class DefaultConfiguration { } @Configuration(proxyBeanMethods = false) @TestAutoConfigurationPackage(CassandraRepositoriesAutoConfigurationTests.class) @EnableCassandraRepositories(basePackageClasses = CityCassandraRepository.class) + @Import(CassandraMockConfiguration.class) static class CustomizedConfiguration { } - @Configuration(proxyBeanMethods = false) - @ComponentScan( - excludeFilters = @ComponentScan.Filter(classes = { Session.class }, type = FilterType.ASSIGNABLE_TYPE)) - static class TestExcludeConfiguration { - - } - } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/city/City.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/city/City.java index 92d1b3b61114..8d1053a89ae8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/city/City.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/city/City.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,8 @@ package org.springframework.boot.autoconfigure.data.cassandra.city; -import com.datastax.driver.core.DataType.Name; - import org.springframework.data.cassandra.core.mapping.CassandraType; +import org.springframework.data.cassandra.core.mapping.CassandraType.Name; import org.springframework.data.cassandra.core.mapping.Column; import org.springframework.data.cassandra.core.mapping.PrimaryKey; import org.springframework.data.cassandra.core.mapping.Table; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseDataAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseDataAutoConfigurationTests.java index 93f420cfe55d..6cb4e0698fea 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseDataAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseDataAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,31 +19,26 @@ import java.util.Collections; import java.util.Set; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; -import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration; import org.springframework.boot.autoconfigure.couchbase.CouchbaseProperties; -import org.springframework.boot.autoconfigure.couchbase.CouchbaseTestConfigurer; import org.springframework.boot.autoconfigure.data.couchbase.city.City; import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; -import org.springframework.boot.test.util.TestPropertyValues; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.core.convert.converter.Converter; -import org.springframework.data.couchbase.config.AbstractCouchbaseDataConfiguration; import org.springframework.data.couchbase.config.BeanNames; -import org.springframework.data.couchbase.config.CouchbaseConfigurer; import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.core.convert.CouchbaseCustomConversions; +import org.springframework.data.couchbase.core.convert.DefaultCouchbaseTypeMapper; +import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter; import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext; import org.springframework.data.couchbase.core.mapping.event.ValidatingCouchbaseEventListener; -import org.springframework.data.couchbase.core.query.Consistency; -import org.springframework.data.couchbase.repository.support.IndexManager; import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -55,107 +50,58 @@ */ class CouchbaseDataAutoConfigurationTests { - private AnnotationConfigApplicationContext context; - - @AfterEach - void close() { - if (this.context != null) { - this.context.close(); - } - } + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ValidationAutoConfiguration.class, + CouchbaseAutoConfiguration.class, CouchbaseDataAutoConfiguration.class)); @Test void disabledIfCouchbaseIsNotConfigured() { - load(null); - assertThat(this.context.getBeansOfType(IndexManager.class)).isEmpty(); - } - - @Test - void customConfiguration() { - load(CustomCouchbaseConfiguration.class); - CouchbaseTemplate couchbaseTemplate = this.context.getBean(CouchbaseTemplate.class); - assertThat(couchbaseTemplate.getDefaultConsistency()).isEqualTo(Consistency.STRONGLY_CONSISTENT); + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(CouchbaseTemplate.class)); } @Test void validatorIsPresent() { - load(CouchbaseTestConfigurer.class); - assertThat(this.context.getBeansOfType(ValidatingCouchbaseEventListener.class)).hasSize(1); + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ValidatingCouchbaseEventListener.class)); } @Test - void autoIndexIsDisabledByDefault() { - load(CouchbaseTestConfigurer.class); - IndexManager indexManager = this.context.getBean(IndexManager.class); - assertThat(indexManager.isIgnoreViews()).isTrue(); - assertThat(indexManager.isIgnoreN1qlPrimary()).isTrue(); - assertThat(indexManager.isIgnoreN1qlSecondary()).isTrue(); - } - - @Test - void enableAutoIndex() { - load(CouchbaseTestConfigurer.class, "spring.data.couchbase.auto-index=true"); - IndexManager indexManager = this.context.getBean(IndexManager.class); - assertThat(indexManager.isIgnoreViews()).isFalse(); - assertThat(indexManager.isIgnoreN1qlPrimary()).isFalse(); - assertThat(indexManager.isIgnoreN1qlSecondary()).isFalse(); + @SuppressWarnings("unchecked") + void entityScanShouldSetInitialEntitySet() { + this.contextRunner.withUserConfiguration(EntityScanConfig.class).run((context) -> { + CouchbaseMappingContext mappingContext = context.getBean(CouchbaseMappingContext.class); + Set> initialEntitySet = (Set>) ReflectionTestUtils.getField(mappingContext, + "initialEntitySet"); + assertThat(initialEntitySet).containsOnly(City.class); + }); } @Test - void changeConsistency() { - load(CouchbaseTestConfigurer.class, "spring.data.couchbase.consistency=eventually-consistent"); - SpringBootCouchbaseDataConfiguration configuration = this.context - .getBean(SpringBootCouchbaseDataConfiguration.class); - assertThat(configuration.getDefaultConsistency()).isEqualTo(Consistency.EVENTUALLY_CONSISTENT); + void typeKeyDefault() { + this.contextRunner.withUserConfiguration(CouchbaseMockConfiguration.class) + .run((context) -> assertThat(context.getBean(MappingCouchbaseConverter.class).getTypeKey()) + .isEqualTo(DefaultCouchbaseTypeMapper.DEFAULT_TYPE_KEY)); } @Test - @SuppressWarnings("unchecked") - void entityScanShouldSetInitialEntitySet() { - load(EntityScanConfig.class); - CouchbaseMappingContext mappingContext = this.context.getBean(CouchbaseMappingContext.class); - Set> initialEntitySet = (Set>) ReflectionTestUtils.getField(mappingContext, - "initialEntitySet"); - assertThat(initialEntitySet).containsOnly(City.class); + void typeKeyCanBeCustomized() { + this.contextRunner.withUserConfiguration(CouchbaseMockConfiguration.class) + .withPropertyValues("spring.data.couchbase.type-key=_custom") + .run((context) -> assertThat(context.getBean(MappingCouchbaseConverter.class).getTypeKey()) + .isEqualTo("_custom")); } @Test void customConversions() { - load(CustomConversionsConfig.class); - CouchbaseTemplate template = this.context.getBean(CouchbaseTemplate.class); - assertThat(template.getConverter().getConversionService().canConvert(CouchbaseProperties.class, Boolean.class)) - .isTrue(); - } - - private void load(Class config, String... environment) { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); - TestPropertyValues.of(environment).applyTo(context); - if (config != null) { - context.register(config); - } - context.register(PropertyPlaceholderAutoConfiguration.class, ValidationAutoConfiguration.class, - CouchbaseAutoConfiguration.class, CouchbaseDataAutoConfiguration.class); - context.refresh(); - this.context = context; - } - - @Configuration - static class CustomCouchbaseConfiguration extends AbstractCouchbaseDataConfiguration { - - @Override - protected CouchbaseConfigurer couchbaseConfigurer() { - return new CouchbaseTestConfigurer(); - } - - @Override - protected Consistency getDefaultConsistency() { - return Consistency.STRONGLY_CONSISTENT; - } - + this.contextRunner.withUserConfiguration(CustomConversionsConfig.class).run((context) -> { + CouchbaseTemplate template = context.getBean(CouchbaseTemplate.class); + assertThat( + template.getConverter().getConversionService().canConvert(CouchbaseProperties.class, Boolean.class)) + .isTrue(); + }); } @Configuration(proxyBeanMethods = false) - @Import(CouchbaseTestConfigurer.class) + @Import(CouchbaseMockConfiguration.class) static class CustomConversionsConfig { @Bean(BeanNames.COUCHBASE_CUSTOM_CONVERSIONS) @@ -167,7 +113,7 @@ CouchbaseCustomConversions myCustomConversions() { @Configuration(proxyBeanMethods = false) @EntityScan("org.springframework.boot.autoconfigure.data.couchbase.city") - @Import(CustomCouchbaseConfiguration.class) + @Import(CouchbaseMockConfiguration.class) static class EntityScanConfig { } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseDataPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseDataPropertiesTests.java new file mode 100644 index 000000000000..47d3e3db6d26 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseDataPropertiesTests.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.couchbase; + +import org.junit.jupiter.api.Test; + +import org.springframework.data.couchbase.core.convert.DefaultCouchbaseTypeMapper; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link CouchbaseDataProperties}. + * + * @author Stephane Nicoll + */ +class CouchbaseDataPropertiesTests { + + @Test + void typeKeyHasConsistentDefault() { + assertThat(new CouchbaseDataProperties().getTypeKey()).isEqualTo(DefaultCouchbaseTypeMapper.DEFAULT_TYPE_KEY); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseMockConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseMockConfiguration.java new file mode 100644 index 000000000000..2e9ca2f8212a --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseMockConfiguration.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.couchbase; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.couchbase.CouchbaseClientFactory; + +import static org.mockito.Mockito.mock; + +/** + * Test configuration that mocks access to Couchbase. + * + * @author Stephane Nicoll + */ +@Configuration(proxyBeanMethods = false) +class CouchbaseMockConfiguration { + + @Bean + CouchbaseClientFactory couchbaseClientFactory() { + return mock(CouchbaseClientFactory.class); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseReactiveAndImperativeRepositoriesAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseReactiveAndImperativeRepositoriesAutoConfigurationTests.java index 64edbbbb10bf..2fbbac6c2270 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseReactiveAndImperativeRepositoriesAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseReactiveAndImperativeRepositoriesAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,16 +19,13 @@ import java.util.ArrayList; import java.util.List; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage; import org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration; -import org.springframework.boot.autoconfigure.couchbase.CouchbaseTestConfigurer; import org.springframework.boot.autoconfigure.data.couchbase.city.CityRepository; import org.springframework.boot.autoconfigure.data.couchbase.city.ReactiveCityRepository; -import org.springframework.boot.test.util.TestPropertyValues; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.ImportSelector; @@ -47,21 +44,12 @@ */ class CouchbaseReactiveAndImperativeRepositoriesAutoConfigurationTests { - private AnnotationConfigApplicationContext context; - - @AfterEach - void close() { - this.context.close(); - } - @Test void shouldCreateInstancesForReactiveAndImperativeRepositories() { - this.context = new AnnotationConfigApplicationContext(); - TestPropertyValues.of("spring.datasource.initialization-mode:never").applyTo(this.context); - this.context.register(ImperativeAndReactiveConfiguration.class, BaseConfiguration.class); - this.context.refresh(); - assertThat(this.context.getBean(CityRepository.class)).isNotNull(); - assertThat(this.context.getBean(ReactiveCityRepository.class)).isNotNull(); + new ApplicationContextRunner() + .withUserConfiguration(ImperativeAndReactiveConfiguration.class, BaseConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(CityRepository.class) + .hasSingleBean(ReactiveCityRepository.class)); } @Configuration(proxyBeanMethods = false) @@ -73,7 +61,7 @@ static class ImperativeAndReactiveConfiguration { } @Configuration(proxyBeanMethods = false) - @Import({ CouchbaseTestConfigurer.class, Registrar.class }) + @Import({ CouchbaseMockConfiguration.class, Registrar.class }) static class BaseConfiguration { } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseReactiveDataAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseReactiveDataAutoConfigurationTests.java index 6a95904f296a..a949f14aa108 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseReactiveDataAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseReactiveDataAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,31 +19,24 @@ import java.util.Collections; import java.util.Set; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; -import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration; import org.springframework.boot.autoconfigure.couchbase.CouchbaseProperties; -import org.springframework.boot.autoconfigure.couchbase.CouchbaseTestConfigurer; import org.springframework.boot.autoconfigure.data.couchbase.city.City; import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; -import org.springframework.boot.test.util.TestPropertyValues; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.core.convert.converter.Converter; -import org.springframework.data.couchbase.config.AbstractReactiveCouchbaseDataConfiguration; import org.springframework.data.couchbase.config.BeanNames; -import org.springframework.data.couchbase.config.CouchbaseConfigurer; -import org.springframework.data.couchbase.core.RxJavaCouchbaseTemplate; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import org.springframework.data.couchbase.core.convert.CouchbaseCustomConversions; import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext; import org.springframework.data.couchbase.core.mapping.event.ValidatingCouchbaseEventListener; -import org.springframework.data.couchbase.core.query.Consistency; -import org.springframework.data.couchbase.repository.support.IndexManager; import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -52,85 +45,47 @@ * Tests for {@link CouchbaseReactiveDataAutoConfiguration}. * * @author Alex Derkach + * @author Stephane Nicoll */ class CouchbaseReactiveDataAutoConfigurationTests { - private AnnotationConfigApplicationContext context; - - @AfterEach - void close() { - if (this.context != null) { - this.context.close(); - } - } + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration( + AutoConfigurations.of(ValidationAutoConfiguration.class, CouchbaseAutoConfiguration.class, + CouchbaseDataAutoConfiguration.class, CouchbaseReactiveDataAutoConfiguration.class)); @Test void disabledIfCouchbaseIsNotConfigured() { - load(null); - assertThat(this.context.getBeansOfType(IndexManager.class)).isEmpty(); - } - - @Test - void customConfiguration() { - load(CustomCouchbaseConfiguration.class); - RxJavaCouchbaseTemplate rxJavaCouchbaseTemplate = this.context.getBean(RxJavaCouchbaseTemplate.class); - assertThat(rxJavaCouchbaseTemplate.getDefaultConsistency()).isEqualTo(Consistency.STRONGLY_CONSISTENT); + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(ReactiveCouchbaseTemplate.class)); } @Test void validatorIsPresent() { - load(CouchbaseTestConfigurer.class); - assertThat(this.context.getBeansOfType(ValidatingCouchbaseEventListener.class)).hasSize(1); + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ValidatingCouchbaseEventListener.class)); } @Test @SuppressWarnings("unchecked") void entityScanShouldSetInitialEntitySet() { - load(EntityScanConfig.class); - CouchbaseMappingContext mappingContext = this.context.getBean(CouchbaseMappingContext.class); - Set> initialEntitySet = (Set>) ReflectionTestUtils.getField(mappingContext, - "initialEntitySet"); - assertThat(initialEntitySet).containsOnly(City.class); + this.contextRunner.withUserConfiguration(EntityScanConfig.class).run((context) -> { + CouchbaseMappingContext mappingContext = context.getBean(CouchbaseMappingContext.class); + Set> initialEntitySet = (Set>) ReflectionTestUtils.getField(mappingContext, + "initialEntitySet"); + assertThat(initialEntitySet).containsOnly(City.class); + }); } @Test void customConversions() { - load(CustomConversionsConfig.class); - RxJavaCouchbaseTemplate template = this.context.getBean(RxJavaCouchbaseTemplate.class); - assertThat(template.getConverter().getConversionService().canConvert(CouchbaseProperties.class, Boolean.class)) - .isTrue(); - } - - private void load(Class config, String... environment) { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); - TestPropertyValues.of(environment).applyTo(context); - if (config != null) { - context.register(config); - } - context.register(PropertyPlaceholderAutoConfiguration.class, ValidationAutoConfiguration.class, - CouchbaseAutoConfiguration.class, CouchbaseDataAutoConfiguration.class, - CouchbaseReactiveDataAutoConfiguration.class); - context.refresh(); - this.context = context; - } - - @Configuration - static class CustomCouchbaseConfiguration extends AbstractReactiveCouchbaseDataConfiguration { - - @Override - protected CouchbaseConfigurer couchbaseConfigurer() { - return new CouchbaseTestConfigurer(); - } - - @Override - protected Consistency getDefaultConsistency() { - return Consistency.STRONGLY_CONSISTENT; - } - + this.contextRunner.withUserConfiguration(CustomConversionsConfig.class).run((context) -> { + ReactiveCouchbaseTemplate template = context.getBean(ReactiveCouchbaseTemplate.class); + assertThat( + template.getConverter().getConversionService().canConvert(CouchbaseProperties.class, Boolean.class)) + .isTrue(); + }); } @Configuration(proxyBeanMethods = false) - @Import(CouchbaseTestConfigurer.class) + @Import(CouchbaseMockConfiguration.class) static class CustomConversionsConfig { @Bean(BeanNames.COUCHBASE_CUSTOM_CONVERSIONS) @@ -142,7 +97,7 @@ CouchbaseCustomConversions myCustomConversions() { @Configuration(proxyBeanMethods = false) @EntityScan("org.springframework.boot.autoconfigure.data.couchbase.city") - @Import(CustomCouchbaseConfiguration.class) + @Import(CouchbaseMockConfiguration.class) static class EntityScanConfig { } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseReactiveRepositoriesAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseReactiveRepositoriesAutoConfigurationTests.java index 913679474c51..060ea7f0d968 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseReactiveRepositoriesAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseReactiveRepositoriesAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,20 +16,17 @@ package org.springframework.boot.autoconfigure.data.couchbase; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage; -import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration; -import org.springframework.boot.autoconfigure.couchbase.CouchbaseTestConfigurer; import org.springframework.boot.autoconfigure.data.alt.couchbase.CityCouchbaseRepository; import org.springframework.boot.autoconfigure.data.alt.couchbase.ReactiveCityCouchbaseRepository; import org.springframework.boot.autoconfigure.data.couchbase.city.City; import org.springframework.boot.autoconfigure.data.couchbase.city.ReactiveCityRepository; import org.springframework.boot.autoconfigure.data.empty.EmptyDataPackage; -import org.springframework.boot.test.util.TestPropertyValues; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; @@ -40,77 +37,62 @@ * Tests for {@link CouchbaseReactiveRepositoriesAutoConfiguration}. * * @author Alex Derkach + * @author Stephane Nicoll */ class CouchbaseReactiveRepositoriesAutoConfigurationTests { - private AnnotationConfigApplicationContext context; - - @AfterEach - void close() { - if (this.context != null) { - this.context.close(); - } - } + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration( + AutoConfigurations.of(CouchbaseAutoConfiguration.class, CouchbaseDataAutoConfiguration.class, + CouchbaseRepositoriesAutoConfiguration.class, CouchbaseReactiveDataAutoConfiguration.class, + CouchbaseReactiveRepositoriesAutoConfiguration.class)); @Test void couchbaseNotAvailable() { - load(null); - assertThat(this.context.getBeansOfType(ReactiveCityRepository.class)).hasSize(0); + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(ReactiveCityRepository.class)); } @Test void defaultRepository() { - load(DefaultConfiguration.class); - assertThat(this.context.getBeansOfType(ReactiveCityRepository.class)).hasSize(1); + this.contextRunner.withUserConfiguration(DefaultConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(ReactiveCityRepository.class)); } @Test void imperativeRepositories() { - load(DefaultConfiguration.class, "spring.data.couchbase.repositories.type=imperative"); - assertThat(this.context.getBeansOfType(ReactiveCityRepository.class)).hasSize(0); + this.contextRunner.withUserConfiguration(DefaultConfiguration.class) + .withPropertyValues("spring.data.couchbase.repositories.type=imperative") + .run((context) -> assertThat(context).doesNotHaveBean(ReactiveCityRepository.class)); } @Test void disabledRepositories() { - load(DefaultConfiguration.class, "spring.data.couchbase.repositories.type=none"); - assertThat(this.context.getBeansOfType(ReactiveCityRepository.class)).hasSize(0); + this.contextRunner.withUserConfiguration(DefaultConfiguration.class) + .withPropertyValues("spring.data.couchbase.repositories.type=none") + .run((context) -> assertThat(context).doesNotHaveBean(ReactiveCityRepository.class)); } @Test void noRepositoryAvailable() { - load(NoRepositoryConfiguration.class); - assertThat(this.context.getBeansOfType(ReactiveCityRepository.class)).hasSize(0); + this.contextRunner.withUserConfiguration(NoRepositoryConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(ReactiveCityRepository.class)); } @Test void doesNotTriggerDefaultRepositoryDetectionIfCustomized() { - load(CustomizedConfiguration.class); - assertThat(this.context.getBeansOfType(ReactiveCityCouchbaseRepository.class)).isEmpty(); - } - - private void load(Class config, String... environment) { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); - TestPropertyValues.of(environment).applyTo(context); - if (config != null) { - context.register(config); - } - context.register(PropertyPlaceholderAutoConfiguration.class, CouchbaseAutoConfiguration.class, - CouchbaseDataAutoConfiguration.class, CouchbaseRepositoriesAutoConfiguration.class, - CouchbaseReactiveDataAutoConfiguration.class, CouchbaseReactiveRepositoriesAutoConfiguration.class); - context.refresh(); - this.context = context; + this.contextRunner.withUserConfiguration(CustomizedConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(ReactiveCityCouchbaseRepository.class)); } @Configuration(proxyBeanMethods = false) @TestAutoConfigurationPackage(City.class) - @Import(CouchbaseTestConfigurer.class) + @Import(CouchbaseMockConfiguration.class) static class DefaultConfiguration { } @Configuration(proxyBeanMethods = false) @TestAutoConfigurationPackage(EmptyDataPackage.class) - @Import(CouchbaseTestConfigurer.class) + @Import(CouchbaseMockConfiguration.class) static class NoRepositoryConfiguration { } @@ -118,7 +100,7 @@ static class NoRepositoryConfiguration { @Configuration(proxyBeanMethods = false) @TestAutoConfigurationPackage(CouchbaseReactiveRepositoriesAutoConfigurationTests.class) @EnableCouchbaseRepositories(basePackageClasses = CityCouchbaseRepository.class) - @Import(CouchbaseDataAutoConfigurationTests.CustomCouchbaseConfiguration.class) + @Import(CouchbaseMockConfiguration.class) static class CustomizedConfiguration { } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseRepositoriesAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseRepositoriesAutoConfigurationTests.java index 1659d1049d9b..ac628cc4fa92 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseRepositoriesAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseRepositoriesAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,18 +16,15 @@ package org.springframework.boot.autoconfigure.data.couchbase; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage; -import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration; -import org.springframework.boot.autoconfigure.couchbase.CouchbaseTestConfigurer; import org.springframework.boot.autoconfigure.data.couchbase.city.City; import org.springframework.boot.autoconfigure.data.couchbase.city.CityRepository; import org.springframework.boot.autoconfigure.data.empty.EmptyDataPackage; -import org.springframework.boot.test.util.TestPropertyValues; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -41,55 +38,39 @@ */ class CouchbaseRepositoriesAutoConfigurationTests { - private AnnotationConfigApplicationContext context; - - @AfterEach - void close() { - if (this.context != null) { - this.context.close(); - } - } + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(CouchbaseAutoConfiguration.class, + CouchbaseDataAutoConfiguration.class, CouchbaseRepositoriesAutoConfiguration.class)); @Test void couchbaseNotAvailable() { - load(null); - assertThat(this.context.getBeansOfType(CityRepository.class)).hasSize(0); + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(CityRepository.class)); } @Test void defaultRepository() { - load(DefaultConfiguration.class); - assertThat(this.context.getBeansOfType(CityRepository.class)).hasSize(1); + this.contextRunner.withUserConfiguration(DefaultConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(CityRepository.class)); } @Test void reactiveRepositories() { - load(DefaultConfiguration.class, "spring.data.couchbase.repositories.type=reactive"); - assertThat(this.context.getBeansOfType(CityRepository.class)).hasSize(0); + this.contextRunner.withUserConfiguration(DefaultConfiguration.class) + .withPropertyValues("spring.data.couchbase.repositories.type=reactive") + .run((context) -> assertThat(context).doesNotHaveBean(CityRepository.class)); } @Test void disabledRepositories() { - load(DefaultConfiguration.class, "spring.data.couchbase.repositories.type=none"); - assertThat(this.context.getBeansOfType(CityRepository.class)).hasSize(0); + this.contextRunner.withUserConfiguration(DefaultConfiguration.class) + .withPropertyValues("spring.data.couchbase.repositories.type=none") + .run((context) -> assertThat(context).doesNotHaveBean(CityRepository.class)); } @Test void noRepositoryAvailable() { - load(NoRepositoryConfiguration.class); - assertThat(this.context.getBeansOfType(CityRepository.class)).hasSize(0); - } - - private void load(Class config, String... environment) { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); - TestPropertyValues.of(environment).applyTo(context); - if (config != null) { - context.register(config); - } - context.register(PropertyPlaceholderAutoConfiguration.class, CouchbaseAutoConfiguration.class, - CouchbaseDataAutoConfiguration.class, CouchbaseRepositoriesAutoConfiguration.class); - context.refresh(); - this.context = context; + this.contextRunner.withUserConfiguration(NoRepositoryConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(CityRepository.class)); } @Configuration(proxyBeanMethods = false) @@ -100,14 +81,14 @@ static class CouchbaseNotAvailableConfiguration { @Configuration(proxyBeanMethods = false) @TestAutoConfigurationPackage(City.class) - @Import(CouchbaseTestConfigurer.class) + @Import(CouchbaseMockConfiguration.class) static class DefaultConfiguration { } @Configuration(proxyBeanMethods = false) @TestAutoConfigurationPackage(EmptyDataPackage.class) - @Import(CouchbaseTestConfigurer.class) + @Import(CouchbaseMockConfiguration.class) static class NoRepositoryConfiguration { } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/couchbase/city/City.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/couchbase/city/City.java index f98fecfd94a0..51c657d9a024 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/couchbase/city/City.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/couchbase/city/City.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,9 @@ package org.springframework.boot.autoconfigure.data.couchbase.city; -import com.couchbase.client.java.repository.annotation.Field; -import com.couchbase.client.java.repository.annotation.Id; - +import org.springframework.data.annotation.Id; import org.springframework.data.couchbase.core.mapping.Document; +import org.springframework.data.couchbase.core.mapping.Field; @Document public class City { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchAutoConfigurationTests.java deleted file mode 100644 index 1ad5f40bd857..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchAutoConfigurationTests.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.boot.autoconfigure.data.elasticsearch; - -import java.time.Duration; -import java.util.List; - -import org.elasticsearch.client.Client; -import org.elasticsearch.client.transport.TransportClient; -import org.elasticsearch.cluster.node.DiscoveryNode; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.testcontainers.elasticsearch.ElasticsearchContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; - -import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.test.util.TestPropertyValues; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link ElasticsearchAutoConfiguration}. - * - * @author Phillip Webb - * @author Andy Wilkinson - */ -@Testcontainers(disabledWithoutDocker = true) -class ElasticsearchAutoConfigurationTests { - - @Container - public static ElasticsearchContainer elasticsearch = new ElasticsearchContainer().withStartupAttempts(5) - .withStartupTimeout(Duration.ofMinutes(2)); - - private AnnotationConfigApplicationContext context; - - @BeforeEach - void setUp() { - System.setProperty("es.set.netty.runtime.available.processors", "false"); - } - - @AfterEach - void close() { - if (this.context != null) { - this.context.close(); - } - System.clearProperty("es.set.netty.runtime.available.processors"); - } - - @Test - void useExistingClient() { - this.context = new AnnotationConfigApplicationContext(); - this.context.register(CustomConfiguration.class, PropertyPlaceholderAutoConfiguration.class, - ElasticsearchAutoConfiguration.class); - this.context.refresh(); - assertThat(this.context.getBeanNamesForType(Client.class)).hasSize(1); - assertThat(this.context.getBean("myClient")).isSameAs(this.context.getBean(Client.class)); - } - - @Test - void createTransportClient() { - this.context = new AnnotationConfigApplicationContext(); - TestPropertyValues - .of("spring.data.elasticsearch.cluster-nodes:" + elasticsearch.getTcpHost().getHostString() + ":" - + elasticsearch.getTcpHost().getPort(), "spring.data.elasticsearch.cluster-name:docker-cluster") - .applyTo(this.context); - this.context.register(PropertyPlaceholderAutoConfiguration.class, ElasticsearchAutoConfiguration.class); - this.context.refresh(); - List connectedNodes = this.context.getBean(TransportClient.class).connectedNodes(); - assertThat(connectedNodes).hasSize(1); - } - - @Configuration(proxyBeanMethods = false) - static class CustomConfiguration { - - @Bean - Client myClient() { - return mock(Client.class); - } - - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchDataAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchDataAutoConfigurationTests.java index e6bc6728658e..794f856ea06d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchDataAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchDataAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,27 +16,28 @@ package org.springframework.boot.autoconfigure.data.elasticsearch; -import java.time.Duration; +import java.math.BigDecimal; +import java.util.Collections; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.testcontainers.elasticsearch.ElasticsearchContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.elasticsearch.rest.RestClientAutoConfiguration; +import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage; +import org.springframework.boot.autoconfigure.data.elasticsearch.city.City; +import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.elasticsearch.core.ElasticsearchEntityMapper; +import org.springframework.core.convert.converter.Converter; import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate; -import org.springframework.data.elasticsearch.core.ElasticsearchTemplate; -import org.springframework.data.elasticsearch.core.EntityMapper; import org.springframework.data.elasticsearch.core.ReactiveElasticsearchTemplate; import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter; +import org.springframework.data.elasticsearch.core.convert.ElasticsearchCustomConversions; import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext; +import org.springframework.data.mapping.model.SimpleTypeHolder; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -48,17 +49,14 @@ * @author Artur Konczak * @author Brian Clozel * @author Peter-Josef Meisch + * @author Scott Frederick + * @author Stephane Nicoll */ -@Testcontainers(disabledWithoutDocker = true) class ElasticsearchDataAutoConfigurationTests { - @Container - static ElasticsearchContainer elasticsearch = new ElasticsearchContainer().withStartupAttempts(5) - .withStartupTimeout(Duration.ofMinutes(2)); - - private ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration( - AutoConfigurations.of(ElasticsearchAutoConfiguration.class, RestClientAutoConfiguration.class, - ReactiveRestClientAutoConfiguration.class, ElasticsearchDataAutoConfiguration.class)); + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ElasticsearchRestClientAutoConfiguration.class, + ReactiveElasticsearchRestClientAutoConfiguration.class, ElasticsearchDataAutoConfiguration.class)); @BeforeEach void setUp() { @@ -70,40 +68,30 @@ void tearDown() { System.clearProperty("es.set.netty.runtime.available.processors"); } - @Test - void defaultTransportBeansAreRegistered() { - this.contextRunner - .withPropertyValues( - "spring.data.elasticsearch.cluster-nodes:" + elasticsearch.getTcpHost().getHostString() + ":" - + elasticsearch.getTcpHost().getPort(), - "spring.data.elasticsearch.cluster-name:docker-cluster") - .run((context) -> assertThat(context).hasSingleBean(ElasticsearchTemplate.class) - .hasSingleBean(SimpleElasticsearchMappingContext.class) - .hasSingleBean(ElasticsearchConverter.class)); - } - - @Test - void defaultTransportBeansNotRegisteredIfNoTransportClient() { - this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(ElasticsearchTemplate.class)); - } - @Test void defaultRestBeansRegistered() { this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ElasticsearchRestTemplate.class) .hasSingleBean(ReactiveElasticsearchTemplate.class).hasSingleBean(ElasticsearchConverter.class) - .hasSingleBean(SimpleElasticsearchMappingContext.class).hasSingleBean(EntityMapper.class) - .hasSingleBean(ElasticsearchConverter.class)); + .hasSingleBean(ElasticsearchConverter.class).hasSingleBean(ElasticsearchCustomConversions.class)); } @Test - void defaultEntityMapperRegistered() { - this.contextRunner.run((context) -> assertThat(context).hasSingleBean(EntityMapper.class)); + void defaultConversionsRegisterBigDecimalAsSimpleType() { + this.contextRunner.run((context) -> { + SimpleElasticsearchMappingContext mappingContext = context.getBean(SimpleElasticsearchMappingContext.class); + assertThat(mappingContext) + .extracting("simpleTypeHolder", InstanceOfAssertFactories.type(SimpleTypeHolder.class)).satisfies( + (simpleTypeHolder) -> assertThat(simpleTypeHolder.isSimpleType(BigDecimal.class)).isTrue()); + }); } @Test - void customTransportTemplateShouldBeUsed() { - this.contextRunner.withUserConfiguration(CustomTransportTemplate.class).run((context) -> assertThat(context) - .getBeanNames(ElasticsearchTemplate.class).hasSize(1).contains("elasticsearchTemplate")); + void customConversionsShouldBeUsed() { + this.contextRunner.withUserConfiguration(CustomElasticsearchCustomConversions.class).run((context) -> { + assertThat(context).hasSingleBean(ElasticsearchCustomConversions.class).hasBean("testCustomConversions"); + assertThat(context.getBean(ElasticsearchConverter.class).getConversionService() + .canConvert(ElasticsearchRestTemplate.class, Boolean.class)).isTrue(); + }); } @Test @@ -120,17 +108,19 @@ void customReactiveRestTemplateShouldBeUsed() { } @Test - void customEntityMapperShouldeBeUsed() { - this.contextRunner.withUserConfiguration(CustomEntityMapper.class).run((context) -> assertThat(context) - .getBeanNames(EntityMapper.class).containsExactly("elasticsearchEntityMapper")); + void shouldFilterInitialEntityScanWithDocumentAnnotation() { + this.contextRunner.withUserConfiguration(EntityScanConfig.class).run((context) -> { + SimpleElasticsearchMappingContext mappingContext = context.getBean(SimpleElasticsearchMappingContext.class); + assertThat(mappingContext.hasPersistentEntityFor(City.class)).isTrue(); + }); } @Configuration(proxyBeanMethods = false) - static class CustomTransportTemplate { + static class CustomElasticsearchCustomConversions { @Bean - ElasticsearchTemplate elasticsearchTemplate() { - return mock(ElasticsearchTemplate.class); + ElasticsearchCustomConversions testCustomConversions() { + return new ElasticsearchCustomConversions(Collections.singletonList(new MyConverter())); } } @@ -156,11 +146,16 @@ ReactiveElasticsearchTemplate reactiveElasticsearchTemplate() { } @Configuration(proxyBeanMethods = false) - static class CustomEntityMapper { + @TestAutoConfigurationPackage(City.class) + static class EntityScanConfig { - @Bean - EntityMapper elasticsearchEntityMapper() { - return mock(ElasticsearchEntityMapper.class); + } + + static class MyConverter implements Converter { + + @Override + public Boolean convert(ElasticsearchRestTemplate source) { + return null; } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchRepositoriesAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchRepositoriesAutoConfigurationTests.java index d84a72316b9c..1268b14d11c1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchRepositoriesAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchRepositoriesAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,8 +29,9 @@ import org.springframework.boot.autoconfigure.data.elasticsearch.city.City; import org.springframework.boot.autoconfigure.data.elasticsearch.city.CityRepository; import org.springframework.boot.autoconfigure.data.empty.EmptyDataPackage; -import org.springframework.boot.autoconfigure.elasticsearch.rest.RestClientAutoConfiguration; +import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; import org.springframework.context.annotation.Configuration; import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate; import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories; @@ -48,13 +49,12 @@ class ElasticsearchRepositoriesAutoConfigurationTests { @Container - static final ElasticsearchContainer elasticsearch = new ElasticsearchContainer().withStartupAttempts(5) - .withStartupTimeout(Duration.ofMinutes(2)); + static final ElasticsearchContainer elasticsearch = new ElasticsearchContainer(DockerImageNames.elasticsearch()) + .withStartupAttempts(5).withStartupTimeout(Duration.ofMinutes(10)); - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration( - AutoConfigurations.of(ElasticsearchAutoConfiguration.class, RestClientAutoConfiguration.class, - ElasticsearchRepositoriesAutoConfiguration.class, ElasticsearchDataAutoConfiguration.class)) + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ElasticsearchRestClientAutoConfiguration.class, + ElasticsearchRepositoriesAutoConfiguration.class, ElasticsearchDataAutoConfiguration.class)) .withPropertyValues("spring.elasticsearch.rest.uris=" + elasticsearch.getHttpHostAddress()); @Test diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRepositoriesAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRepositoriesAutoConfigurationTests.java index 1d615544ea2a..e6eddf64e3a1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRepositoriesAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRepositoriesAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ import org.springframework.boot.autoconfigure.data.elasticsearch.city.ReactiveCityRepository; import org.springframework.boot.autoconfigure.data.empty.EmptyDataPackage; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; import org.springframework.context.annotation.Configuration; import org.springframework.data.elasticsearch.core.ReactiveElasticsearchTemplate; import org.springframework.data.elasticsearch.repository.config.EnableReactiveElasticsearchRepositories; @@ -44,17 +45,19 @@ * @author Brian Clozel */ @Testcontainers(disabledWithoutDocker = true) -public class ReactiveElasticsearchRepositoriesAutoConfigurationTests { +class ReactiveElasticsearchRepositoriesAutoConfigurationTests { @Container - static ElasticsearchContainer elasticsearch = new ElasticsearchContainer().withStartupAttempts(5) - .withStartupTimeout(Duration.ofMinutes(2)); + static ElasticsearchContainer elasticsearch = new ElasticsearchContainer(DockerImageNames.elasticsearch()) + .withStartupAttempts(5).withStartupTimeout(Duration.ofMinutes(10)); - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(ReactiveRestClientAutoConfiguration.class, + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReactiveElasticsearchRestClientAutoConfiguration.class, ReactiveElasticsearchRepositoriesAutoConfiguration.class, ElasticsearchDataAutoConfiguration.class)) - .withPropertyValues("spring.data.elasticsearch.client.reactive.endpoints=" - + elasticsearch.getContainerIpAddress() + ":" + elasticsearch.getFirstMappedPort()); + .withPropertyValues( + "spring.data.elasticsearch.client.reactive.endpoints=" + elasticsearch.getHost() + ":" + + elasticsearch.getFirstMappedPort(), + "spring.data.elasticsearch.client.reactive.socket-timeout=30s"); @Test void testDefaultRepositoryConfiguration() { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientAutoConfigurationIntegrationTests.java new file mode 100644 index 000000000000..55a09707a456 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientAutoConfigurationIntegrationTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.elasticsearch; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + +import org.elasticsearch.action.get.GetRequest; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.index.get.GetResult; +import org.junit.jupiter.api.Test; +import org.testcontainers.elasticsearch.ElasticsearchContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; +import org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link ReactiveElasticsearchRestClientAutoConfiguration}. + * + * @author Brian Clozel + */ +@Testcontainers(disabledWithoutDocker = true) +class ReactiveElasticsearchRestClientAutoConfigurationIntegrationTests { + + @Container + static ElasticsearchContainer elasticsearch = new ElasticsearchContainer(DockerImageNames.elasticsearch()) + .withStartupAttempts(5).withStartupTimeout(Duration.ofMinutes(10)); + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReactiveElasticsearchRestClientAutoConfiguration.class)); + + @Test + void restClientCanQueryElasticsearchNode() { + this.contextRunner.withPropertyValues( + "spring.data.elasticsearch.client.reactive.endpoints=" + elasticsearch.getHost() + ":" + + elasticsearch.getFirstMappedPort(), + "spring.data.elasticsearch.client.reactive.connection-timeout=120s", + "spring.data.elasticsearch.client.reactive.socket-timeout=120s").run((context) -> { + ReactiveElasticsearchClient client = context.getBean(ReactiveElasticsearchClient.class); + Map source = new HashMap<>(); + source.put("a", "alpha"); + source.put("b", "bravo"); + IndexRequest indexRequest = new IndexRequest("foo").id("1").source(source); + GetRequest getRequest = new GetRequest("foo").id("1"); + GetResult getResult = client.index(indexRequest).then(client.get(getRequest)).block(); + assertThat(getResult).isNotNull(); + assertThat(getResult.isExists()).isTrue(); + }); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientAutoConfigurationTests.java new file mode 100644 index 000000000000..1de73382c0db --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveElasticsearchRestClientAutoConfigurationTests.java @@ -0,0 +1,168 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.elasticsearch; + +import java.net.InetSocketAddress; +import java.time.Duration; +import java.util.List; + +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.elasticsearch.client.ClientConfiguration; +import org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient; +import org.springframework.http.HttpHeaders; +import org.springframework.http.codec.CodecConfigurer.DefaultCodecConfig; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ReactiveElasticsearchRestClientAutoConfiguration}. + * + * @author Brian Clozel + */ +class ReactiveElasticsearchRestClientAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReactiveElasticsearchRestClientAutoConfiguration.class)); + + @Test + void configureShouldCreateDefaultBeans() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ClientConfiguration.class) + .hasSingleBean(ReactiveElasticsearchClient.class)); + } + + @Test + void configureWhenCustomClientShouldBackOff() { + this.contextRunner.withUserConfiguration(CustomClientConfiguration.class).run((context) -> assertThat(context) + .hasSingleBean(ReactiveElasticsearchClient.class).hasBean("customClient")); + } + + @Test + void configureWhenCustomClientConfig() { + this.contextRunner.withUserConfiguration(CustomClientConfigConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(ReactiveElasticsearchClient.class) + .hasSingleBean(ClientConfiguration.class).hasBean("customClientConfiguration")); + } + + @Test + void whenEndpointIsCustomizedThenClientConfigurationHasCustomEndpoint() { + this.contextRunner.withPropertyValues("spring.data.elasticsearch.client.reactive.endpoints=localhost:9876") + .run((context) -> { + List endpoints = context.getBean(ClientConfiguration.class).getEndpoints(); + assertThat(endpoints).hasSize(1); + assertThat(endpoints.get(0).getHostString()).isEqualTo("localhost"); + assertThat(endpoints.get(0).getPort()).isEqualTo(9876); + }); + } + + @Test + void whenMultipleEndpointsAreConfiguredThenClientConfigurationHasMultipleEndpoints() { + this.contextRunner + .withPropertyValues("spring.data.elasticsearch.client.reactive.endpoints=localhost:9876,localhost:8765") + .run((context) -> { + List endpoints = context.getBean(ClientConfiguration.class).getEndpoints(); + assertThat(endpoints).hasSize(2); + assertThat(endpoints.get(0).getHostString()).isEqualTo("localhost"); + assertThat(endpoints.get(0).getPort()).isEqualTo(9876); + assertThat(endpoints.get(1).getHostString()).isEqualTo("localhost"); + assertThat(endpoints.get(1).getPort()).isEqualTo(8765); + }); + } + + @Test + void whenConfiguredToUseSslThenClientConfigurationUsesSsl() { + this.contextRunner.withPropertyValues("spring.data.elasticsearch.client.reactive.use-ssl=true") + .run((context) -> assertThat(context.getBean(ClientConfiguration.class).useSsl()).isTrue()); + } + + @Test + void whenSocketTimeoutIsNotConfiguredThenClientConfigurationUsesDefault() { + this.contextRunner.run((context) -> assertThat(context.getBean(ClientConfiguration.class).getSocketTimeout()) + .isEqualTo(Duration.ofSeconds(5))); + } + + @Test + void whenConnectionTimeoutIsNotConfiguredThenClientConfigurationUsesDefault() { + this.contextRunner.run((context) -> assertThat(context.getBean(ClientConfiguration.class).getConnectTimeout()) + .isEqualTo(Duration.ofSeconds(10))); + } + + @Test + void whenSocketTimeoutIsConfiguredThenClientConfigurationHasCustomSocketTimeout() { + this.contextRunner.withPropertyValues("spring.data.elasticsearch.client.reactive.socket-timeout=2s") + .run((context) -> assertThat(context.getBean(ClientConfiguration.class).getSocketTimeout()) + .isEqualTo(Duration.ofSeconds(2))); + } + + @Test + void whenConnectionTimeoutIsConfiguredThenClientConfigurationHasCustomConnectTimeout() { + this.contextRunner.withPropertyValues("spring.data.elasticsearch.client.reactive.connection-timeout=2s") + .run((context) -> assertThat(context.getBean(ClientConfiguration.class).getConnectTimeout()) + .isEqualTo(Duration.ofSeconds(2))); + } + + @Test + void whenCredentialsAreConfiguredThenClientConfigurationHasDefaultAuthorizationHeader() { + this.contextRunner + .withPropertyValues("spring.data.elasticsearch.client.reactive.username=alice", + "spring.data.elasticsearch.client.reactive.password=secret") + .run((context) -> assertThat( + context.getBean(ClientConfiguration.class).getDefaultHeaders().get(HttpHeaders.AUTHORIZATION)) + .containsExactly("Basic YWxpY2U6c2VjcmV0")); + } + + @Test + void whenMaxInMemorySizeIsConfiguredThenUnderlyingWebClientHasCustomMaxInMemorySize() { + this.contextRunner.withPropertyValues("spring.data.elasticsearch.client.reactive.max-in-memory-size=1MB") + .run((context) -> { + WebClient client = context.getBean(ClientConfiguration.class).getWebClientConfigurer() + .apply(WebClient.create()); + assertThat(client).extracting("exchangeFunction").extracting("strategies") + .extracting("codecConfigurer").extracting("defaultCodecs") + .asInstanceOf(InstanceOfAssertFactories.type(DefaultCodecConfig.class)) + .extracting(DefaultCodecConfig::maxInMemorySize).isEqualTo(1024 * 1024); + }); + } + + @Configuration(proxyBeanMethods = false) + static class CustomClientConfiguration { + + @Bean + ReactiveElasticsearchClient customClient() { + return mock(ReactiveElasticsearchClient.class); + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomClientConfigConfiguration { + + @Bean + ClientConfiguration customClientConfiguration() { + return ClientConfiguration.localhost(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveRestClientAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveRestClientAutoConfigurationTests.java deleted file mode 100644 index 5f337ddfbdb1..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/ReactiveRestClientAutoConfigurationTests.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.data.elasticsearch; - -import java.time.Duration; -import java.util.HashMap; -import java.util.Map; - -import org.elasticsearch.action.get.GetRequest; -import org.elasticsearch.action.index.IndexRequest; -import org.elasticsearch.index.get.GetResult; -import org.junit.jupiter.api.Test; -import org.testcontainers.elasticsearch.ElasticsearchContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; - -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.elasticsearch.client.ClientConfiguration; -import org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link ReactiveRestClientAutoConfiguration} - * - * @author Brian Clozel - */ -@Testcontainers(disabledWithoutDocker = true) -public class ReactiveRestClientAutoConfigurationTests { - - @Container - static ElasticsearchContainer elasticsearch = new ElasticsearchContainer().withStartupAttempts(5) - .withStartupTimeout(Duration.ofMinutes(2)); - - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(ReactiveRestClientAutoConfiguration.class)); - - @Test - void configureShouldCreateDefaultBeans() { - this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ClientConfiguration.class) - .hasSingleBean(ReactiveElasticsearchClient.class)); - } - - @Test - void configureWhenCustomClientShouldBackOff() { - this.contextRunner.withUserConfiguration(CustomClientConfiguration.class).run((context) -> assertThat(context) - .hasSingleBean(ReactiveElasticsearchClient.class).hasBean("customClient")); - } - - @Test - void configureWhenCustomClientConfig() { - this.contextRunner.withUserConfiguration(CustomClientConfigConfiguration.class) - .run((context) -> assertThat(context).hasSingleBean(ReactiveElasticsearchClient.class) - .hasSingleBean(ClientConfiguration.class).hasBean("customClientConfiguration")); - } - - @Test - void restClientCanQueryElasticsearchNode() { - this.contextRunner - .withPropertyValues("spring.data.elasticsearch.client.reactive.endpoints=" - + elasticsearch.getContainerIpAddress() + ":" + elasticsearch.getFirstMappedPort()) - .run((context) -> { - ReactiveElasticsearchClient client = context.getBean(ReactiveElasticsearchClient.class); - Map source = new HashMap<>(); - source.put("a", "alpha"); - source.put("b", "bravo"); - IndexRequest index = new IndexRequest("foo", "bar", "1").source(source); - GetRequest getRequest = new GetRequest("foo", "bar", "1"); - GetResult getResult = client.index(index).then(client.get(getRequest)).block(); - assertThat(getResult.isExists()).isTrue(); - }); - } - - @Configuration(proxyBeanMethods = false) - static class CustomClientConfiguration { - - @Bean - ReactiveElasticsearchClient customClient() { - return mock(ReactiveElasticsearchClient.class); - } - - } - - @Configuration(proxyBeanMethods = false) - static class CustomClientConfigConfiguration { - - @Bean - ClientConfiguration customClientConfiguration() { - return ClientConfiguration.localhost(); - } - - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/city/City.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/city/City.java index 9b7fe46755e0..dd2f93324205 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/city/City.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/elasticsearch/city/City.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,8 +20,10 @@ import org.springframework.data.annotation.Id; import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Setting; -@Document(indexName = "city", type = "city", shards = 1, replicas = 0, refreshInterval = "-1") +@Document(indexName = "city") +@Setting(shards = 1, replicas = 0, refreshInterval = "-1") public class City implements Serializable { private static final long serialVersionUID = 1L; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/jdbc/JdbcRepositoriesAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/jdbc/JdbcRepositoriesAutoConfigurationTests.java index e947b1e992da..ffc40727b298 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/jdbc/JdbcRepositoriesAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/jdbc/JdbcRepositoriesAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration; +import org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Configuration; import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration; @@ -111,9 +112,12 @@ void honoursUsersEnableJdbcRepositoriesConfiguration() { } private Function database() { - return (runner) -> runner.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) - .withPropertyValues("spring.datasource.schema=classpath:data-jdbc-schema.sql", - "spring.datasource.data=classpath:city.sql", "spring.datasource.generate-unique-name:true"); + return (runner) -> runner + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, + SqlInitializationAutoConfiguration.class)) + .withPropertyValues("spring.sql.init.schema-locations=classpath:data-city-schema.sql", + "spring.sql.init.data-locations=classpath:city.sql", + "spring.datasource.generate-unique-name:true"); } @TestAutoConfigurationPackage(City.class) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/jpa/JpaRepositoriesAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/jpa/JpaRepositoriesAutoConfigurationTests.java index 6310e6294b1e..c25490bd5087 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/jpa/JpaRepositoriesAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/jpa/JpaRepositoriesAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,9 +23,9 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.data.alt.elasticsearch.CityElasticsearchDbRepository; import org.springframework.boot.autoconfigure.data.alt.jpa.CityJpaRepository; import org.springframework.boot.autoconfigure.data.alt.mongo.CityMongoDbRepository; -import org.springframework.boot.autoconfigure.data.alt.solr.CitySolrRepository; import org.springframework.boot.autoconfigure.data.jpa.city.City; import org.springframework.boot.autoconfigure.data.jpa.city.CityRepository; import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; @@ -51,6 +51,7 @@ * * @author Dave Syer * @author Oliver Gierke + * @author Scott Frederick */ class JpaRepositoriesAutoConfigurationTests { @@ -85,7 +86,7 @@ void autoConfigurationShouldNotKickInEvenIfManualConfigDidNotCreateAnyRepositori } @Test - void whenBootstrappingModeIsLazyWithMultipleAsyncExecutorBootstrapExecutorIsConfigured() { + void whenBootstrapModeIsLazyWithMultipleAsyncExecutorBootstrapExecutorIsConfigured() { this.contextRunner.withUserConfiguration(MultipleAsyncTaskExecutorConfiguration.class) .withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class, TaskSchedulingAutoConfiguration.class)) @@ -96,7 +97,7 @@ void whenBootstrappingModeIsLazyWithMultipleAsyncExecutorBootstrapExecutorIsConf } @Test - void whenBootstrappingModeIsLazyWithSingleAsyncExecutorBootstrapExecutorIsConfigured() { + void whenBootstrapModeIsLazyWithSingleAsyncExecutorBootstrapExecutorIsConfigured() { this.contextRunner.withUserConfiguration(SingleAsyncTaskExecutorConfiguration.class) .withPropertyValues("spring.data.jpa.repositories.bootstrap-mode=lazy") .run((context) -> assertThat( @@ -105,7 +106,7 @@ void whenBootstrappingModeIsLazyWithSingleAsyncExecutorBootstrapExecutorIsConfig } @Test - void whenBootstrappingModeIsDeferredBootstrapExecutorIsConfigured() { + void whenBootstrapModeIsDeferredBootstrapExecutorIsConfigured() { this.contextRunner.withUserConfiguration(MultipleAsyncTaskExecutorConfiguration.class) .withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class, TaskSchedulingAutoConfiguration.class)) @@ -116,7 +117,7 @@ void whenBootstrappingModeIsDeferredBootstrapExecutorIsConfigured() { } @Test - void whenBootstrappingModeIsDefaultBootstrapExecutorIsNotConfigured() { + void whenBootstrapModeIsDefaultBootstrapExecutorIsNotConfigured() { this.contextRunner.withUserConfiguration(MultipleAsyncTaskExecutorConfiguration.class) .withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class, TaskSchedulingAutoConfiguration.class)) @@ -124,6 +125,15 @@ void whenBootstrappingModeIsDefaultBootstrapExecutorIsNotConfigured() { context.getBean(LocalContainerEntityManagerFactoryBean.class).getBootstrapExecutor()).isNull()); } + @Test + void bootstrapModeIsDefaultByDefault() { + this.contextRunner.withUserConfiguration(MultipleAsyncTaskExecutorConfiguration.class) + .withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class, + TaskSchedulingAutoConfiguration.class)) + .run((context) -> assertThat( + context.getBean(LocalContainerEntityManagerFactoryBean.class).getBootstrapExecutor()).isNull()); + } + @Configuration(proxyBeanMethods = false) @EnableScheduling @Import(TestConfiguration.class) @@ -152,7 +162,7 @@ static class TestConfiguration { @EnableJpaRepositories( basePackageClasses = org.springframework.boot.autoconfigure.data.alt.jpa.CityJpaRepository.class, excludeFilters = { @Filter(type = FilterType.ASSIGNABLE_TYPE, value = CityMongoDbRepository.class), - @Filter(type = FilterType.ASSIGNABLE_TYPE, value = CitySolrRepository.class) }) + @Filter(type = FilterType.ASSIGNABLE_TYPE, value = CityElasticsearchDbRepository.class) }) @TestAutoConfigurationPackage(City.class) static class CustomConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/jpa/JpaWebAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/jpa/JpaWebAutoConfigurationTests.java index 20012a829817..8aa4b6e8a644 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/jpa/JpaWebAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/jpa/JpaWebAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,21 +16,21 @@ package org.springframework.boot.autoconfigure.data.jpa; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage; -import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.data.jpa.city.City; import org.springframework.boot.autoconfigure.data.jpa.city.CityRepository; import org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; -import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebApplicationContext; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.context.annotation.Configuration; +import org.springframework.data.geo.Distance; import org.springframework.data.web.PageableHandlerMethodArgumentResolver; +import org.springframework.data.web.SortHandlerMethodArgumentResolver; import org.springframework.format.support.FormattingConversionService; -import org.springframework.mock.web.MockServletContext; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import static org.assertj.core.api.Assertions.assertThat; @@ -40,27 +40,25 @@ * {@link JpaRepositoriesAutoConfiguration}. * * @author Dave Syer + * @author Stephane Nicoll */ class JpaWebAutoConfigurationTests { - private AnnotationConfigServletWebApplicationContext context; - - @AfterEach - void close() { - this.context.close(); - } + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of(DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class, + JpaRepositoriesAutoConfiguration.class, SpringDataWebAutoConfiguration.class)) + .withPropertyValues("spring.datasource.generate-unique-name=true"); @Test - void testDefaultRepositoryConfiguration() { - this.context = new AnnotationConfigServletWebApplicationContext(); - this.context.setServletContext(new MockServletContext()); - this.context.register(TestConfiguration.class, EmbeddedDataSourceConfiguration.class, - HibernateJpaAutoConfiguration.class, JpaRepositoriesAutoConfiguration.class, - SpringDataWebAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class); - this.context.refresh(); - assertThat(this.context.getBean(CityRepository.class)).isNotNull(); - assertThat(this.context.getBean(PageableHandlerMethodArgumentResolver.class)).isNotNull(); - assertThat(this.context.getBean(FormattingConversionService.class).canConvert(Long.class, City.class)).isTrue(); + void springDataWebIsConfiguredWithJpaRepositories() { + this.contextRunner.withUserConfiguration(TestConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(CityRepository.class); + assertThat(context).hasSingleBean(PageableHandlerMethodArgumentResolver.class); + assertThat(context).hasSingleBean(SortHandlerMethodArgumentResolver.class); + assertThat(context.getBean(FormattingConversionService.class).canConvert(String.class, Distance.class)) + .isTrue(); + }); } @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MixedMongoRepositoriesAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MixedMongoRepositoriesAutoConfigurationTests.java index e58cebd93590..3f6800aa2fb5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MixedMongoRepositoriesAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MixedMongoRepositoriesAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -62,7 +62,6 @@ void close() { @Test void testDefaultRepositoryConfiguration() { this.context = new AnnotationConfigApplicationContext(); - TestPropertyValues.of("spring.datasource.initialization-mode:never").applyTo(this.context); this.context.register(TestConfiguration.class, BaseConfiguration.class); this.context.refresh(); assertThat(this.context.getBean(CountryRepository.class)).isNotNull(); @@ -71,7 +70,6 @@ void testDefaultRepositoryConfiguration() { @Test void testMixedRepositoryConfiguration() { this.context = new AnnotationConfigApplicationContext(); - TestPropertyValues.of("spring.datasource.initialization-mode:never").applyTo(this.context); this.context.register(MixedConfiguration.class, BaseConfiguration.class); this.context.refresh(); assertThat(this.context.getBean(CountryRepository.class)).isNotNull(); @@ -81,7 +79,6 @@ void testMixedRepositoryConfiguration() { @Test void testJpaRepositoryConfigurationWithMongoTemplate() { this.context = new AnnotationConfigApplicationContext(); - TestPropertyValues.of("spring.datasource.initialization-mode:never").applyTo(this.context); this.context.register(JpaConfiguration.class, BaseConfiguration.class); this.context.refresh(); assertThat(this.context.getBean(CityRepository.class)).isNotNull(); @@ -90,7 +87,6 @@ void testJpaRepositoryConfigurationWithMongoTemplate() { @Test void testJpaRepositoryConfigurationWithMongoOverlap() { this.context = new AnnotationConfigApplicationContext(); - TestPropertyValues.of("spring.datasource.initialization-mode:never").applyTo(this.context); this.context.register(OverlapConfiguration.class, BaseConfiguration.class); this.context.refresh(); assertThat(this.context.getBean(CityRepository.class)).isNotNull(); @@ -99,9 +95,7 @@ void testJpaRepositoryConfigurationWithMongoOverlap() { @Test void testJpaRepositoryConfigurationWithMongoOverlapDisabled() { this.context = new AnnotationConfigApplicationContext(); - TestPropertyValues - .of("spring.datasource.initialization-mode:never", "spring.data.mongodb.repositories.type:none") - .applyTo(this.context); + TestPropertyValues.of("spring.data.mongodb.repositories.type:none").applyTo(this.context); this.context.register(OverlapConfiguration.class, BaseConfiguration.class); this.context.refresh(); assertThat(this.context.getBean(CityRepository.class)).isNotNull(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MongoDataAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MongoDataAutoConfigurationTests.java index 06512ca783e4..2052e91ca767 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MongoDataAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MongoDataAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ import java.util.Arrays; import java.util.Set; -import com.mongodb.MongoClient; +import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; import org.junit.jupiter.api.Test; @@ -31,9 +31,7 @@ import org.springframework.boot.autoconfigure.data.mongo.city.City; import org.springframework.boot.autoconfigure.data.mongo.country.Country; import org.springframework.boot.autoconfigure.domain.EntityScan; -import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; -import org.springframework.boot.logging.LogLevel; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; @@ -42,13 +40,12 @@ import org.springframework.data.mapping.model.CamelCaseAbbreviatingFieldNamingStrategy; import org.springframework.data.mapping.model.FieldNamingStrategy; import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy; -import org.springframework.data.mongodb.MongoDbFactory; +import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.data.mongodb.core.SimpleMongoClientDbFactory; -import org.springframework.data.mongodb.core.SimpleMongoDbFactory; +import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory; import org.springframework.data.mongodb.core.convert.MongoCustomConversions; -import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.gridfs.GridFsTemplate; import org.springframework.test.util.ReflectionTestUtils; @@ -65,8 +62,7 @@ class MongoDataAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(PropertyPlaceholderAutoConfiguration.class, - MongoAutoConfiguration.class, MongoDataAutoConfiguration.class)) - .withInitializer(new ConditionEvaluationReportLoggingListener(LogLevel.INFO)); + MongoAutoConfiguration.class, MongoDataAutoConfiguration.class)); @Test void templateExists() { @@ -74,9 +70,33 @@ void templateExists() { } @Test - void gridFsTemplateExists() { - this.contextRunner.withPropertyValues("spring.data.mongodb.gridFsDatabase:grid") - .run((context) -> assertThat(context).hasSingleBean(GridFsTemplate.class)); + void whenGridFsDatabaseIsConfiguredThenGridFsTemplateIsAutoConfiguredAndUsesIt() { + this.contextRunner.withPropertyValues("spring.data.mongodb.gridfs.database:grid").run((context) -> { + assertThat(context).hasSingleBean(GridFsTemplate.class); + GridFsTemplate template = context.getBean(GridFsTemplate.class); + MongoDatabaseFactory factory = (MongoDatabaseFactory) ReflectionTestUtils.getField(template, "dbFactory"); + assertThat(factory.getMongoDatabase().getName()).isEqualTo("grid"); + }); + } + + @Test + @Deprecated + void whenGridFsDatabaseIsConfiguredWithDeprecatedPropertyThenGridFsTemplateIsAutoConfiguredAndUsesIt() { + this.contextRunner.withPropertyValues("spring.data.mongodb.gridFsDatabase:grid").run((context) -> { + assertThat(context).hasSingleBean(GridFsTemplate.class); + GridFsTemplate template = context.getBean(GridFsTemplate.class); + MongoDatabaseFactory factory = (MongoDatabaseFactory) ReflectionTestUtils.getField(template, "dbFactory"); + assertThat(factory.getMongoDatabase().getName()).isEqualTo("grid"); + }); + } + + @Test + void whenGridFsBucketIsConfiguredThenGridFsTemplateIsAutoConfiguredAndUsesIt() { + this.contextRunner.withPropertyValues("spring.data.mongodb.gridfs.bucket:test-bucket").run((context) -> { + assertThat(context).hasSingleBean(GridFsTemplate.class); + GridFsTemplate template = context.getBean(GridFsTemplate.class); + assertThat(template).hasFieldOrPropertyWithValue("bucket", "test-bucket"); + }); } @Test @@ -127,13 +147,21 @@ void customFieldNamingStrategy() { } @Test - void customAutoIndexCreation() { - this.contextRunner.withPropertyValues("spring.data.mongodb.autoIndexCreation:false").run((context) -> { + void defaultAutoIndexCreation() { + this.contextRunner.run((context) -> { MongoMappingContext mappingContext = context.getBean(MongoMappingContext.class); assertThat(mappingContext.isAutoIndexCreation()).isFalse(); }); } + @Test + void customAutoIndexCreation() { + this.contextRunner.withPropertyValues("spring.data.mongodb.autoIndexCreation:true").run((context) -> { + MongoMappingContext mappingContext = context.getBean(MongoMappingContext.class); + assertThat(mappingContext.isAutoIndexCreation()).isTrue(); + }); + } + @Test void interfaceFieldNamingStrategy() { this.contextRunner @@ -157,7 +185,7 @@ void entityScanShouldSetInitialEntitySet() { void registersDefaultSimpleTypesWithMappingContext() { this.contextRunner.run((context) -> { MongoMappingContext mappingContext = context.getBean(MongoMappingContext.class); - BasicMongoPersistentEntity entity = mappingContext.getPersistentEntity(Sample.class); + MongoPersistentEntity entity = mappingContext.getPersistentEntity(Sample.class); MongoPersistentProperty dateProperty = entity.getPersistentProperty("date"); assertThat(dateProperty.isEntity()).isFalse(); }); @@ -172,24 +200,24 @@ void backsOffIfMongoClientBeanIsNotPresent() { } @Test - void createsMongoDbFactoryForPreferredMongoClient() { + void createsMongoDatabaseFactoryForPreferredMongoClient() { this.contextRunner.run((context) -> { - MongoDbFactory dbFactory = context.getBean(MongoDbFactory.class); - assertThat(dbFactory).isInstanceOf(SimpleMongoDbFactory.class); + MongoDatabaseFactory dbFactory = context.getBean(MongoDatabaseFactory.class); + assertThat(dbFactory).isInstanceOf(SimpleMongoClientDatabaseFactory.class); }); } @Test - void createsMongoDbFactoryForFallbackMongoClient() { + void createsMongoDatabaseFactoryForFallbackMongoClient() { this.contextRunner.withUserConfiguration(FallbackMongoClientConfiguration.class).run((context) -> { - MongoDbFactory dbFactory = context.getBean(MongoDbFactory.class); - assertThat(dbFactory).isInstanceOf(SimpleMongoClientDbFactory.class); + MongoDatabaseFactory dbFactory = context.getBean(MongoDatabaseFactory.class); + assertThat(dbFactory).isInstanceOf(SimpleMongoClientDatabaseFactory.class); }); } @Test - void autoConfiguresIfUserProvidesMongoDbFactoryButNoClient() { - this.contextRunner.withUserConfiguration(MongoDbFactoryConfiguration.class) + void autoConfiguresIfUserProvidesMongoDatabaseFactoryButNoClient() { + this.contextRunner.withUserConfiguration(MongoDatabaseFactoryConfiguration.class) .run((context) -> assertThat(context).hasSingleBean(MongoTemplate.class)); } @@ -226,11 +254,11 @@ com.mongodb.client.MongoClient fallbackMongoClient() { } @Configuration(proxyBeanMethods = false) - static class MongoDbFactoryConfiguration { + static class MongoDatabaseFactoryConfiguration { @Bean - MongoDbFactory mongoDbFactory() { - return new SimpleMongoClientDbFactory(MongoClients.create(), "test"); + MongoDatabaseFactory mongoDatabaseFactory() { + return new SimpleMongoClientDatabaseFactory(MongoClients.create(), "test"); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MongoReactiveAndBlockingRepositoriesAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MongoReactiveAndBlockingRepositoriesAutoConfigurationTests.java index a197912c0ecf..4ac412c659f6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MongoReactiveAndBlockingRepositoriesAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MongoReactiveAndBlockingRepositoriesAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,6 @@ import org.springframework.boot.autoconfigure.data.mongo.city.ReactiveCityRepository; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration; -import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -57,7 +56,6 @@ void close() { @Test void shouldCreateInstancesForReactiveAndBlockingRepositories() { this.context = new AnnotationConfigApplicationContext(); - TestPropertyValues.of("spring.datasource.initialization-mode:never").applyTo(this.context); this.context.register(BlockingAndReactiveConfiguration.class, BaseConfiguration.class); this.context.refresh(); assertThat(this.context.getBean(CityRepository.class)).isNotNull(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MongoReactiveDataAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MongoReactiveDataAutoConfigurationTests.java index 2d88d8115041..f949c4906813 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MongoReactiveDataAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MongoReactiveDataAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,9 +21,12 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory; import org.springframework.data.mongodb.core.ReactiveMongoTemplate; import org.springframework.data.mongodb.gridfs.ReactiveGridFsTemplate; +import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -45,8 +48,30 @@ void templateExists() { } @Test - void gridFsTemplateExists() { - this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ReactiveGridFsTemplate.class)); + void whenNoGridFsDatabaseIsConfiguredTheGridFsTemplateUsesTheMainDatabase() { + this.contextRunner.run((context) -> assertThat(grisFsTemplateDatabaseName(context)).isEqualTo("test")); + } + + @Test + void whenGridFsDatabaseIsConfiguredThenGridFsTemplateUsesIt() { + this.contextRunner.withPropertyValues("spring.data.mongodb.gridfs.database:grid") + .run((context) -> assertThat(grisFsTemplateDatabaseName(context)).isEqualTo("grid")); + } + + @Test + @Deprecated + void whenGridFsDatabaseIsConfiguredWithDeprecatedPropertyThenGridFsTemplateUsesIt() { + this.contextRunner.withPropertyValues("spring.data.mongodb.gridFsDatabase:grid") + .run((context) -> assertThat(grisFsTemplateDatabaseName(context)).isEqualTo("grid")); + } + + @Test + void whenGridFsBucketIsConfiguredThenGridFsTemplateUsesIt() { + this.contextRunner.withPropertyValues("spring.data.mongodb.gridfs.bucket:test-bucket").run((context) -> { + assertThat(context).hasSingleBean(ReactiveGridFsTemplate.class); + ReactiveGridFsTemplate template = context.getBean(ReactiveGridFsTemplate.class); + assertThat(template).hasFieldOrPropertyWithValue("bucket", "test-bucket"); + }); } @Test @@ -56,4 +81,12 @@ void backsOffIfMongoClientBeanIsNotPresent() { runner.run((context) -> assertThat(context).doesNotHaveBean(MongoReactiveDataAutoConfiguration.class)); } + private String grisFsTemplateDatabaseName(AssertableApplicationContext context) { + assertThat(context).hasSingleBean(ReactiveGridFsTemplate.class); + ReactiveGridFsTemplate template = context.getBean(ReactiveGridFsTemplate.class); + ReactiveMongoDatabaseFactory factory = (ReactiveMongoDatabaseFactory) ReflectionTestUtils.getField(template, + "dbFactory"); + return factory.getMongoDatabase().block().getName(); + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MongoRepositoriesAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MongoRepositoriesAutoConfigurationTests.java index a2efc5b9cb6d..651068050491 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MongoRepositoriesAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MongoRepositoriesAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ import java.util.Set; -import com.mongodb.MongoClient; +import com.mongodb.client.MongoClient; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/city/PersistentEntity.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/city/PersistentEntity.java new file mode 100644 index 000000000000..0f4b92940958 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/city/PersistentEntity.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.mongo.city; + +import java.io.Serializable; + +import org.springframework.data.annotation.Persistent; + +@Persistent +public class PersistentEntity implements Serializable { + + private static final long serialVersionUID = 1L; + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/MixedNeo4jRepositoriesAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/MixedNeo4jRepositoriesAutoConfigurationTests.java index 419c940790c6..56d194b1bae8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/MixedNeo4jRepositoriesAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/MixedNeo4jRepositoriesAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,10 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.neo4j.ogm.drivers.embedded.driver.EmbeddedDriver; +import org.neo4j.driver.Config; +import org.neo4j.driver.Driver; +import org.neo4j.driver.GraphDatabase; +import org.neo4j.driver.internal.logging.Slf4jLogging; import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage; import org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration; @@ -31,11 +34,12 @@ import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; -import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.data.neo4j.config.AbstractNeo4jConfig; import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; import static org.assertj.core.api.Assertions.assertThat; @@ -48,6 +52,7 @@ * @author Michael Hunger * @author Vince Bickers * @author Stephane Nicoll + * @author Michael J. Simons */ class MixedNeo4jRepositoriesAutoConfigurationTests { @@ -94,12 +99,12 @@ void testJpaRepositoryConfigurationWithNeo4jOverlapDisabled() { private void load(Class config, String... environment) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); - context.setClassLoader(new FilteredClassLoader(EmbeddedDriver.class)); - TestPropertyValues.of(environment).and("spring.datasource.initialization-mode=never").applyTo(context); + TestPropertyValues.of(environment).applyTo(context); context.register(config); context.register(DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class, JpaRepositoriesAutoConfiguration.class, Neo4jDataAutoConfiguration.class, - Neo4jRepositoriesAutoConfiguration.class); + Neo4jReactiveDataAutoConfiguration.class, Neo4jRepositoriesAutoConfiguration.class, + Neo4jReactiveRepositoriesAutoConfiguration.class); context.refresh(); this.context = context; } @@ -108,7 +113,14 @@ private void load(Class config, String... environment) { @TestAutoConfigurationPackage(EmptyMarker.class) // Not this package or its parent @EnableNeo4jRepositories(basePackageClasses = Country.class) - static class TestConfiguration { + static class TestConfiguration extends AbstractNeo4jConfig { + + @Override + @Bean + public Driver driver() { + return GraphDatabase.driver("bolt://neo4j.test:7687", + Config.builder().withLogging(new Slf4jLogging()).build()); + } } @@ -117,7 +129,14 @@ static class TestConfiguration { @EnableNeo4jRepositories(basePackageClasses = Country.class) @EntityScan(basePackageClasses = City.class) @EnableJpaRepositories(basePackageClasses = CityRepository.class) - static class MixedConfiguration { + static class MixedConfiguration extends AbstractNeo4jConfig { + + @Override + @Bean + public Driver driver() { + return GraphDatabase.driver("bolt://neo4j.test:7687", + Config.builder().withLogging(new Slf4jLogging()).build()); + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/MockedDriverConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/MockedDriverConfiguration.java new file mode 100644 index 000000000000..8c2a1a611f49 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/MockedDriverConfiguration.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.neo4j; + +import org.mockito.ArgumentMatchers; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Session; +import org.neo4j.driver.SessionConfig; +import org.neo4j.driver.types.TypeSystem; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Driver configuration mocked to avoid instantiation of a real driver with connection + * creation. + * + * @author Michael J. Simons + */ +@Configuration(proxyBeanMethods = false) +class MockedDriverConfiguration { + + @Bean + Driver driver() { + Driver driver = mock(Driver.class); + TypeSystem typeSystem = mock(TypeSystem.class); + Session session = mock(Session.class); + given(driver.defaultTypeSystem()).willReturn(typeSystem); + given(driver.session(ArgumentMatchers.any(SessionConfig.class))).willReturn(session); + return driver; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jDataAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jDataAutoConfigurationTests.java index 8c63012c34bb..c5b46119ea81 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jDataAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jDataAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,258 +16,163 @@ package org.springframework.boot.autoconfigure.data.neo4j; -import com.github.benmanes.caffeine.cache.Caffeine; import org.junit.jupiter.api.Test; -import org.neo4j.ogm.driver.NativeTypesNotAvailableException; -import org.neo4j.ogm.driver.NativeTypesNotSupportedException; -import org.neo4j.ogm.drivers.embedded.driver.EmbeddedDriver; -import org.neo4j.ogm.session.Session; -import org.neo4j.ogm.session.SessionFactory; -import org.neo4j.ogm.session.event.Event; -import org.neo4j.ogm.session.event.EventListener; -import org.neo4j.ogm.session.event.PersistenceEvent; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.boot.autoconfigure.AutoConfigurationPackages; import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.data.neo4j.city.City; -import org.springframework.boot.autoconfigure.data.neo4j.country.Country; -import org.springframework.boot.autoconfigure.domain.EntityScan; -import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration; -import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage; +import org.springframework.boot.autoconfigure.data.neo4j.scan.TestNode; +import org.springframework.boot.autoconfigure.data.neo4j.scan.TestNonAnnotated; +import org.springframework.boot.autoconfigure.data.neo4j.scan.TestPersistent; +import org.springframework.boot.autoconfigure.data.neo4j.scan.TestRelationshipProperties; +import org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.boot.test.context.runner.WebApplicationContextRunner; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.neo4j.annotation.EnableBookmarkManagement; -import org.springframework.data.neo4j.bookmark.BookmarkManager; -import org.springframework.data.neo4j.mapping.Neo4jMappingContext; -import org.springframework.data.neo4j.transaction.Neo4jTransactionManager; -import org.springframework.data.neo4j.web.support.OpenSessionInViewInterceptor; -import org.springframework.web.context.WebApplicationContext; +import org.springframework.data.neo4j.core.DatabaseSelection; +import org.springframework.data.neo4j.core.DatabaseSelectionProvider; +import org.springframework.data.neo4j.core.Neo4jClient; +import org.springframework.data.neo4j.core.Neo4jOperations; +import org.springframework.data.neo4j.core.Neo4jTemplate; +import org.springframework.data.neo4j.core.convert.Neo4jConversions; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.ReactiveTransactionManager; +import org.springframework.transaction.TransactionManager; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** - * Tests for {@link Neo4jDataAutoConfiguration}. Tests should not use the embedded driver - * as it requires the complete Neo4j-Kernel and server to function properly. + * Tests for {@link Neo4jDataAutoConfiguration}. * * @author Stephane Nicoll * @author Michael Hunger * @author Vince Bickers * @author Andy Wilkinson * @author Kazuki Shimizu - * @author Michael Simons + * @author Michael J. Simons */ class Neo4jDataAutoConfigurationTests { - private WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() - .withClassLoader(new FilteredClassLoader(EmbeddedDriver.class)) - .withUserConfiguration(TestConfiguration.class).withConfiguration( - AutoConfigurations.of(Neo4jDataAutoConfiguration.class, TransactionAutoConfiguration.class)); + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withUserConfiguration(MockedDriverConfiguration.class) + .withConfiguration(AutoConfigurations.of(Neo4jAutoConfiguration.class, Neo4jDataAutoConfiguration.class)); @Test - void defaultConfiguration() { - this.contextRunner.withPropertyValues("spring.data.neo4j.uri=http://localhost:8989").run((context) -> { - assertThat(context).hasSingleBean(org.neo4j.ogm.config.Configuration.class); - assertThat(context).hasSingleBean(SessionFactory.class); - assertThat(context).hasSingleBean(Neo4jTransactionManager.class); - assertThat(context).hasSingleBean(OpenSessionInViewInterceptor.class); - assertThat(context).doesNotHaveBean(BookmarkManager.class); - }); - } - - @Test - void customNeo4jTransactionManagerUsingProperties() { - this.contextRunner.withPropertyValues("spring.transaction.default-timeout=30", - "spring.transaction.rollback-on-commit-failure:true").run((context) -> { - Neo4jTransactionManager transactionManager = context.getBean(Neo4jTransactionManager.class); - assertThat(transactionManager.getDefaultTimeout()).isEqualTo(30); - assertThat(transactionManager.isRollbackOnCommitFailure()).isTrue(); - }); + void shouldProvideConversions() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(Neo4jConversions.class)); } @Test - void customSessionFactory() { - this.contextRunner.withUserConfiguration(CustomSessionFactory.class).run((context) -> { - assertThat(context).doesNotHaveBean(org.neo4j.ogm.config.Configuration.class); - assertThat(context).hasSingleBean(SessionFactory.class); + void shouldProvideDefaultDatabaseNameProvider() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(DatabaseSelectionProvider.class); + assertThat(context.getBean(DatabaseSelectionProvider.class)) + .isSameAs(DatabaseSelectionProvider.getDefaultSelectionProvider()); }); } @Test - void customSessionFactoryShouldNotDisableOtherDefaults() { - this.contextRunner.withUserConfiguration(CustomSessionFactory.class).run((context) -> { - assertThat(context).hasSingleBean(SessionFactory.class); - assertThat(context.getBean(SessionFactory.class)).isSameAs(context.getBean("customSessionFactory")); - assertThat(context).hasSingleBean(Neo4jTransactionManager.class); - assertThat(context).hasSingleBean(OpenSessionInViewInterceptor.class); + void shouldUseDatabaseNameIfSet() { + this.contextRunner.withPropertyValues("spring.data.neo4j.database=test").run((context) -> { + assertThat(context).hasSingleBean(DatabaseSelectionProvider.class); + assertThat(context.getBean(DatabaseSelectionProvider.class).getDatabaseSelection()) + .isEqualTo(DatabaseSelection.byName("test")); }); } @Test - void customConfiguration() { - this.contextRunner.withUserConfiguration(CustomConfiguration.class).run((context) -> { - assertThat(context.getBean(org.neo4j.ogm.config.Configuration.class)) - .isSameAs(context.getBean("myConfiguration")); - assertThat(context).hasSingleBean(SessionFactory.class); - assertThat(context).hasSingleBean(org.neo4j.ogm.config.Configuration.class); - }); + void shouldReuseExistingDatabaseNameProvider() { + this.contextRunner.withPropertyValues("spring.data.neo4j.database=ignored") + .withUserConfiguration(CustomDatabaseSelectionProviderConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(DatabaseSelectionProvider.class); + assertThat(context.getBean(DatabaseSelectionProvider.class).getDatabaseSelection()) + .isEqualTo(DatabaseSelection.byName("custom")); + }); } @Test - void usesAutoConfigurationPackageToPickUpDomainTypes() { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); - context.setClassLoader(new FilteredClassLoader(EmbeddedDriver.class)); - String cityPackage = City.class.getPackage().getName(); - AutoConfigurationPackages.register(context, cityPackage); - context.register(Neo4jDataAutoConfiguration.class, Neo4jRepositoriesAutoConfiguration.class); - try { - context.refresh(); - assertDomainTypesDiscovered(context.getBean(Neo4jMappingContext.class), City.class); - } - finally { - context.close(); - } + void shouldProvideNeo4jClient() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(Neo4jClient.class)); } @Test - void openSessionInViewInterceptorCanBeDisabled() { - this.contextRunner.withPropertyValues("spring.data.neo4j.open-in-view:false") - .run((context) -> assertThat(context).doesNotHaveBean(OpenSessionInViewInterceptor.class)); + void shouldProvideNeo4jClientWithCustomDatabaseSelectionProvider() { + this.contextRunner.withUserConfiguration(CustomDatabaseSelectionProviderConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(Neo4jClient.class); + assertThat(context.getBean(Neo4jClient.class)).extracting("databaseSelectionProvider") + .isSameAs(context.getBean(DatabaseSelectionProvider.class)); + }); } @Test - void shouldBeAbleToUseNativeTypesWithBolt() { - this.contextRunner - .withPropertyValues("spring.data.neo4j.uri=bolt://localhost:7687", - "spring.data.neo4j.use-native-types:true") - .withConfiguration( - AutoConfigurations.of(Neo4jDataAutoConfiguration.class, TransactionAutoConfiguration.class)) - .run((context) -> assertThat(context).getBean(org.neo4j.ogm.config.Configuration.class) - .hasFieldOrPropertyWithValue("useNativeTypes", true)); + void shouldReuseExistingNeo4jClient() { + this.contextRunner.withBean("myCustomClient", Neo4jClient.class, () -> mock(Neo4jClient.class)) + .run((context) -> assertThat(context).hasSingleBean(Neo4jClient.class).hasBean("myCustomClient")); } @Test - void shouldFailWhenNativeTypesAreNotAvailable() { - this.contextRunner.withClassLoader(new FilteredClassLoader("org.neo4j.ogm.drivers.bolt.types")) - .withPropertyValues("spring.data.neo4j.uri=bolt://localhost:7687", - "spring.data.neo4j.use-native-types:true") - .withConfiguration( - AutoConfigurations.of(Neo4jDataAutoConfiguration.class, TransactionAutoConfiguration.class)) - .run((context) -> { - assertThat(context).hasFailed(); - assertThat(context.getStartupFailure()) - .hasRootCauseInstanceOf(NativeTypesNotAvailableException.class); - }); + void shouldProvideNeo4jTemplate() { + this.contextRunner.withUserConfiguration(CustomDatabaseSelectionProviderConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(Neo4jTemplate.class)); } @Test - void shouldFailWhenNativeTypesAreNotSupported() { - this.contextRunner - .withPropertyValues("spring.data.neo4j.uri=http://localhost:7474", - "spring.data.neo4j.use-native-types:true") - .withConfiguration( - AutoConfigurations.of(Neo4jDataAutoConfiguration.class, TransactionAutoConfiguration.class)) - .run((context) -> { - assertThat(context).hasFailed(); - assertThat(context.getStartupFailure()) - .hasRootCauseInstanceOf(NativeTypesNotSupportedException.class); - }); + void shouldReuseExistingNeo4jTemplate() { + this.contextRunner.withBean("myCustomOperations", Neo4jOperations.class, () -> mock(Neo4jOperations.class)).run( + (context) -> assertThat(context).hasSingleBean(Neo4jOperations.class).hasBean("myCustomOperations")); } @Test - void eventListenersAreAutoRegistered() { - this.contextRunner.withUserConfiguration(EventListenerConfiguration.class).run((context) -> { - Session session = context.getBean(SessionFactory.class).openSession(); - session.notifyListeners(new PersistenceEvent(null, Event.TYPE.PRE_SAVE)); - verify(context.getBean("eventListenerOne", EventListener.class)).onPreSave(any(Event.class)); - verify(context.getBean("eventListenerTwo", EventListener.class)).onPreSave(any(Event.class)); + void shouldProvideTransactionManager() { + this.contextRunner.withUserConfiguration(CustomDatabaseSelectionProviderConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(Neo4jTransactionManager.class); + assertThat(context.getBean(Neo4jTransactionManager.class)).extracting("databaseSelectionProvider") + .isSameAs(context.getBean(DatabaseSelectionProvider.class)); }); } @Test - void providesARequestScopedBookmarkManagerIfNecessaryAndPossible() { - this.contextRunner.withUserConfiguration(BookmarkManagementEnabledConfiguration.class).run((context) -> { - BeanDefinition bookmarkManagerBean = context.getBeanFactory() - .getBeanDefinition("scopedTarget.bookmarkManager"); - assertThat(bookmarkManagerBean.getScope()).isEqualTo(WebApplicationContext.SCOPE_REQUEST); - }); + void shouldBackoffIfReactiveTransactionManagerIsSet() { + this.contextRunner.withBean(ReactiveTransactionManager.class, () -> mock(ReactiveTransactionManager.class)) + .run((context) -> assertThat(context).doesNotHaveBean(Neo4jTransactionManager.class) + .hasSingleBean(TransactionManager.class)); } @Test - void providesASingletonScopedBookmarkManagerIfNecessaryAndPossible() { - new ApplicationContextRunner().withClassLoader(new FilteredClassLoader(EmbeddedDriver.class)) - .withUserConfiguration(TestConfiguration.class, BookmarkManagementEnabledConfiguration.class) - .withConfiguration( - AutoConfigurations.of(Neo4jDataAutoConfiguration.class, TransactionAutoConfiguration.class)) - .run((context) -> { - assertThat(context).hasSingleBean(BookmarkManager.class); - assertThat(context.getBeanDefinitionNames()).doesNotContain("scopedTarget.bookmarkManager"); - }); + void shouldReuseExistingTransactionManager() { + this.contextRunner + .withBean("myCustomTransactionManager", PlatformTransactionManager.class, + () -> mock(PlatformTransactionManager.class)) + .run((context) -> assertThat(context).hasSingleBean(PlatformTransactionManager.class) + .hasBean("myCustomTransactionManager")); } @Test - void doesNotProvideABookmarkManagerIfNotPossible() { - this.contextRunner.withClassLoader(new FilteredClassLoader(Caffeine.class, EmbeddedDriver.class)) - .withUserConfiguration(BookmarkManagementEnabledConfiguration.class) - .run((context) -> assertThat(context).doesNotHaveBean(BookmarkManager.class)); - } - - private static void assertDomainTypesDiscovered(Neo4jMappingContext mappingContext, Class... types) { - for (Class type : types) { - assertThat(mappingContext.getPersistentEntity(type)).isNotNull(); - } - } - - @Configuration(proxyBeanMethods = false) - @EntityScan(basePackageClasses = Country.class) - static class TestConfiguration { - - } - - @Configuration(proxyBeanMethods = false) - static class CustomSessionFactory { - - @Bean - SessionFactory customSessionFactory() { - return mock(SessionFactory.class); - } - + void shouldFilterInitialEntityScanWithKnownAnnotations() { + this.contextRunner.withUserConfiguration(EntityScanConfig.class).run((context) -> { + Neo4jMappingContext mappingContext = context.getBean(Neo4jMappingContext.class); + assertThat(mappingContext.hasPersistentEntityFor(TestNode.class)).isTrue(); + assertThat(mappingContext.hasPersistentEntityFor(TestPersistent.class)).isFalse(); + assertThat(mappingContext.hasPersistentEntityFor(TestRelationshipProperties.class)).isTrue(); + assertThat(mappingContext.hasPersistentEntityFor(TestNonAnnotated.class)).isFalse(); + }); } @Configuration(proxyBeanMethods = false) - static class CustomConfiguration { + static class CustomDatabaseSelectionProviderConfiguration { @Bean - org.neo4j.ogm.config.Configuration myConfiguration() { - return new org.neo4j.ogm.config.Configuration.Builder().uri("http://localhost:12345").build(); + DatabaseSelectionProvider databaseSelectionProvider() { + return () -> DatabaseSelection.byName("custom"); } } @Configuration(proxyBeanMethods = false) - @EnableBookmarkManagement - static class BookmarkManagementEnabledConfiguration { - - } - - @Configuration(proxyBeanMethods = false) - static class EventListenerConfiguration { - - @Bean - EventListener eventListenerOne() { - return mock(EventListener.class); - } - - @Bean - EventListener eventListenerTwo() { - return mock(EventListener.class); - } + @TestAutoConfigurationPackage(TestPersistent.class) + static class EntityScanConfig { } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jPropertiesTests.java deleted file mode 100644 index afbad5cce298..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jPropertiesTests.java +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.data.neo4j; - -import java.util.Base64; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; -import org.neo4j.ogm.config.AutoIndexMode; -import org.neo4j.ogm.config.Configuration; -import org.neo4j.ogm.config.Credentials; -import org.neo4j.ogm.drivers.embedded.driver.EmbeddedDriver; - -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.test.context.FilteredClassLoader; -import org.springframework.boot.test.util.TestPropertyValues; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link Neo4jProperties}. - * - * @author Stephane Nicoll - * @author Michael Simons - */ -class Neo4jPropertiesTests { - - private AnnotationConfigApplicationContext context; - - @AfterEach - void close() { - if (this.context != null) { - this.context.close(); - } - } - - @Test - void defaultUseEmbeddedInMemoryIfAvailable() { - Neo4jProperties properties = load(true); - Configuration configuration = properties.createConfiguration(); - assertDriver(configuration, Neo4jProperties.EMBEDDED_DRIVER, null); - } - - @Test - void defaultUseBoltDriverIfEmbeddedDriverIsNotAvailable() { - Neo4jProperties properties = load(false); - Configuration configuration = properties.createConfiguration(); - assertDriver(configuration, Neo4jProperties.BOLT_DRIVER, Neo4jProperties.DEFAULT_BOLT_URI); - } - - @Test - void httpUriUseHttpDriver() { - Neo4jProperties properties = load(true, "spring.data.neo4j.uri=http://localhost:7474"); - Configuration configuration = properties.createConfiguration(); - assertDriver(configuration, Neo4jProperties.HTTP_DRIVER, "http://localhost:7474"); - } - - @Test - void httpsUriUseHttpDriver() { - Neo4jProperties properties = load(true, "spring.data.neo4j.uri=https://localhost:7474"); - Configuration configuration = properties.createConfiguration(); - assertDriver(configuration, Neo4jProperties.HTTP_DRIVER, "https://localhost:7474"); - } - - @Test - void boltUriUseBoltDriver() { - Neo4jProperties properties = load(true, "spring.data.neo4j.uri=bolt://localhost:7687"); - Configuration configuration = properties.createConfiguration(); - assertDriver(configuration, Neo4jProperties.BOLT_DRIVER, "bolt://localhost:7687"); - } - - @Test - void fileUriUseEmbeddedServer() { - Neo4jProperties properties = load(true, "spring.data.neo4j.uri=file://var/tmp/graph.db"); - Configuration configuration = properties.createConfiguration(); - assertDriver(configuration, Neo4jProperties.EMBEDDED_DRIVER, "file://var/tmp/graph.db"); - } - - @Test - void credentialsAreSet() { - Neo4jProperties properties = load(true, "spring.data.neo4j.uri=http://localhost:7474", - "spring.data.neo4j.username=user", "spring.data.neo4j.password=secret"); - Configuration configuration = properties.createConfiguration(); - assertDriver(configuration, Neo4jProperties.HTTP_DRIVER, "http://localhost:7474"); - assertCredentials(configuration, "user", "secret"); - } - - @Test - void credentialsAreSetFromUri() { - Neo4jProperties properties = load(true, "spring.data.neo4j.uri=https://user:secret@my-server:7474"); - Configuration configuration = properties.createConfiguration(); - assertDriver(configuration, Neo4jProperties.HTTP_DRIVER, "https://my-server:7474"); - assertCredentials(configuration, "user", "secret"); - } - - @Test - void autoIndexNoneByDefault() { - Neo4jProperties properties = load(true); - Configuration configuration = properties.createConfiguration(); - assertThat(configuration.getAutoIndex()).isEqualTo(AutoIndexMode.NONE); - } - - @Test - void autoIndexCanBeConfigured() { - Neo4jProperties properties = load(true, "spring.data.neo4j.auto-index=validate"); - Configuration configuration = properties.createConfiguration(); - assertThat(configuration.getAutoIndex()).isEqualTo(AutoIndexMode.VALIDATE); - } - - @Test - void embeddedModeDisabledUseBoltUri() { - Neo4jProperties properties = load(true, "spring.data.neo4j.embedded.enabled=false"); - Configuration configuration = properties.createConfiguration(); - assertDriver(configuration, Neo4jProperties.BOLT_DRIVER, Neo4jProperties.DEFAULT_BOLT_URI); - } - - @Test - void embeddedModeWithRelativeLocation() { - Neo4jProperties properties = load(true, "spring.data.neo4j.uri=file:relative/path/to/my.db"); - Configuration configuration = properties.createConfiguration(); - assertDriver(configuration, Neo4jProperties.EMBEDDED_DRIVER, "file:relative/path/to/my.db"); - } - - @Test - void nativeTypesAreSetToFalseByDefault() { - Neo4jProperties properties = load(true); - Configuration configuration = properties.createConfiguration(); - assertThat(configuration.getUseNativeTypes()).isFalse(); - } - - @Test - void nativeTypesCanBeConfigured() { - Neo4jProperties properties = load(true, "spring.data.neo4j.use-native-types=true"); - Configuration configuration = properties.createConfiguration(); - assertThat(configuration.getUseNativeTypes()).isTrue(); - } - - private static void assertDriver(Configuration actual, String driver, String uri) { - assertThat(actual).isNotNull(); - assertThat(actual.getDriverClassName()).isEqualTo(driver); - assertThat(actual.getURI()).isEqualTo(uri); - } - - private static void assertCredentials(Configuration actual, String username, String password) { - Credentials credentials = actual.getCredentials(); - if (username == null && password == null) { - assertThat(credentials).isNull(); - } - else { - assertThat(credentials).isNotNull(); - Object content = credentials.credentials(); - assertThat(content).isInstanceOf(String.class); - String[] auth = new String(Base64.getDecoder().decode((String) content)).split(":"); - assertThat(auth).containsExactly(username, password); - } - } - - Neo4jProperties load(boolean embeddedAvailable, String... environment) { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); - if (!embeddedAvailable) { - ctx.setClassLoader(new FilteredClassLoader(EmbeddedDriver.class)); - } - TestPropertyValues.of(environment).applyTo(ctx); - ctx.register(TestConfiguration.class); - ctx.refresh(); - this.context = ctx; - return this.context.getBean(Neo4jProperties.class); - } - - @org.springframework.context.annotation.Configuration(proxyBeanMethods = false) - @EnableConfigurationProperties(Neo4jProperties.class) - static class TestConfiguration { - - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jReactiveDataAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jReactiveDataAutoConfigurationTests.java new file mode 100644 index 000000000000..e04690355f08 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jReactiveDataAutoConfigurationTests.java @@ -0,0 +1,163 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.neo4j; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage; +import org.springframework.boot.autoconfigure.data.neo4j.scan.TestNode; +import org.springframework.boot.autoconfigure.data.neo4j.scan.TestNonAnnotated; +import org.springframework.boot.autoconfigure.data.neo4j.scan.TestPersistent; +import org.springframework.boot.autoconfigure.data.neo4j.scan.TestRelationshipProperties; +import org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.neo4j.core.DatabaseSelection; +import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; +import org.springframework.data.neo4j.core.ReactiveNeo4jClient; +import org.springframework.data.neo4j.core.ReactiveNeo4jOperations; +import org.springframework.data.neo4j.core.ReactiveNeo4jTemplate; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.transaction.ReactiveTransactionManager; +import org.springframework.transaction.TransactionManager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link Neo4jReactiveDataAutoConfiguration}. + * + * @author Michael J. Simons + * @author Stephane Nicoll + */ +class Neo4jReactiveDataAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withUserConfiguration(MockedDriverConfiguration.class) + .withConfiguration(AutoConfigurations.of(Neo4jAutoConfiguration.class, Neo4jDataAutoConfiguration.class, + Neo4jReactiveDataAutoConfiguration.class)); + + @Test + void shouldProvideDefaultDatabaseNameProvider() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(ReactiveDatabaseSelectionProvider.class); + assertThat(context.getBean(ReactiveDatabaseSelectionProvider.class)) + .isSameAs(ReactiveDatabaseSelectionProvider.getDefaultSelectionProvider()); + }); + } + + @Test + void shouldUseDatabaseNameIfSet() { + this.contextRunner.withPropertyValues("spring.data.neo4j.database=test").run((context) -> { + assertThat(context).hasSingleBean(ReactiveDatabaseSelectionProvider.class); + StepVerifier.create(context.getBean(ReactiveDatabaseSelectionProvider.class).getDatabaseSelection()) + .consumeNextWith((databaseSelection) -> assertThat(databaseSelection.getValue()).isEqualTo("test")) + .expectComplete(); + }); + } + + @Test + void shouldReuseExistingDatabaseNameProvider() { + this.contextRunner.withPropertyValues("spring.data.neo4j.database=ignored") + .withUserConfiguration(CustomReactiveDatabaseSelectionProviderConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(ReactiveDatabaseSelectionProvider.class); + StepVerifier.create(context.getBean(ReactiveDatabaseSelectionProvider.class).getDatabaseSelection()) + .consumeNextWith( + (databaseSelection) -> assertThat(databaseSelection.getValue()).isEqualTo("custom")) + .expectComplete(); + }); + } + + @Test + void shouldProvideReactiveNeo4jClient() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ReactiveNeo4jClient.class)); + } + + @Test + void shouldProvideReactiveNeo4jClientWithCustomDatabaseSelectionProvider() { + this.contextRunner.withUserConfiguration(CustomReactiveDatabaseSelectionProviderConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(ReactiveNeo4jClient.class); + assertThat(context.getBean(ReactiveNeo4jClient.class)).extracting("databaseSelectionProvider") + .isSameAs(context.getBean(ReactiveDatabaseSelectionProvider.class)); + }); + } + + @Test + void shouldReuseExistingReactiveNeo4jClient() { + this.contextRunner + .withBean("myCustomReactiveClient", ReactiveNeo4jClient.class, () -> mock(ReactiveNeo4jClient.class)) + .run((context) -> assertThat(context).hasSingleBean(ReactiveNeo4jClient.class) + .hasBean("myCustomReactiveClient")); + } + + @Test + void shouldProvideReactiveNeo4jTemplate() { + this.contextRunner.withUserConfiguration(CustomReactiveDatabaseSelectionProviderConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(ReactiveNeo4jTemplate.class)); + } + + @Test + void shouldReuseExistingReactiveNeo4jTemplate() { + this.contextRunner + .withBean("myCustomReactiveOperations", ReactiveNeo4jOperations.class, + () -> mock(ReactiveNeo4jOperations.class)) + .run((context) -> assertThat(context).hasSingleBean(ReactiveNeo4jOperations.class) + .hasBean("myCustomReactiveOperations")); + } + + @Test + void shouldUseExistingReactiveTransactionManager() { + this.contextRunner + .withBean("myCustomReactiveTransactionManager", ReactiveTransactionManager.class, + () -> mock(ReactiveTransactionManager.class)) + .run((context) -> assertThat(context).hasSingleBean(ReactiveTransactionManager.class) + .hasSingleBean(TransactionManager.class)); + } + + @Test + void shouldFilterInitialEntityScanWithKnownAnnotations() { + this.contextRunner.withUserConfiguration(EntityScanConfig.class).run((context) -> { + Neo4jMappingContext mappingContext = context.getBean(Neo4jMappingContext.class); + assertThat(mappingContext.hasPersistentEntityFor(TestNode.class)).isTrue(); + assertThat(mappingContext.hasPersistentEntityFor(TestPersistent.class)).isFalse(); + assertThat(mappingContext.hasPersistentEntityFor(TestRelationshipProperties.class)).isTrue(); + assertThat(mappingContext.hasPersistentEntityFor(TestNonAnnotated.class)).isFalse(); + }); + } + + @Configuration(proxyBeanMethods = false) + static class CustomReactiveDatabaseSelectionProviderConfiguration { + + @Bean + ReactiveDatabaseSelectionProvider databaseNameProvider() { + return () -> Mono.just(DatabaseSelection.byName("custom")); + } + + } + + @Configuration(proxyBeanMethods = false) + @TestAutoConfigurationPackage(TestPersistent.class) + static class EntityScanConfig { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jReactiveRepositoriesAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jReactiveRepositoriesAutoConfigurationTests.java new file mode 100644 index 000000000000..c78b4a14268b --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jReactiveRepositoriesAutoConfigurationTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.neo4j; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage; +import org.springframework.boot.autoconfigure.data.empty.EmptyDataPackage; +import org.springframework.boot.autoconfigure.data.neo4j.city.City; +import org.springframework.boot.autoconfigure.data.neo4j.city.CityRepository; +import org.springframework.boot.autoconfigure.data.neo4j.city.ReactiveCityRepository; +import org.springframework.boot.autoconfigure.data.neo4j.country.CountryRepository; +import org.springframework.boot.autoconfigure.data.neo4j.country.ReactiveCountryRepository; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.neo4j.core.ReactiveNeo4jTemplate; +import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; +import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepositories; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Neo4jReactiveRepositoriesAutoConfiguration}. + * + * @author Stephane Nicoll + * @author Michael J. Simons + */ +class Neo4jReactiveRepositoriesAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withUserConfiguration(MockedDriverConfiguration.class) + .withConfiguration(AutoConfigurations.of(Neo4jDataAutoConfiguration.class, + Neo4jReactiveDataAutoConfiguration.class, Neo4jRepositoriesAutoConfiguration.class, + Neo4jReactiveRepositoriesAutoConfiguration.class)); + + @Test + void configurationWithDefaultRepositories() { + this.contextRunner.withUserConfiguration(TestConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(ReactiveCityRepository.class)); + } + + @Test + void configurationWithNoRepositories() { + this.contextRunner.withUserConfiguration(EmptyConfiguration.class).run((context) -> assertThat(context) + .hasSingleBean(ReactiveNeo4jTemplate.class).doesNotHaveBean(ReactiveNeo4jRepository.class)); + } + + @Test + void configurationWithDisabledRepositories() { + this.contextRunner.withUserConfiguration(TestConfiguration.class) + .withPropertyValues("spring.data.neo4j.repositories.type=none") + .run((context) -> assertThat(context).doesNotHaveBean(ReactiveNeo4jRepository.class)); + } + + @Test + void autoConfigurationShouldNotKickInEvenIfManualConfigDidNotCreateAnyRepositories() { + this.contextRunner.withUserConfiguration(SortOfInvalidCustomConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(ReactiveNeo4jTemplate.class) + .doesNotHaveBean(ReactiveNeo4jRepository.class)); + } + + @Test + void shouldRespectAtEnableReactiveNeo4jRepositories() { + this.contextRunner + .withUserConfiguration(SortOfInvalidCustomConfiguration.class, WithCustomReactiveRepositoryScan.class) + .withPropertyValues("spring.data.neo4j.repositories.type=reactive") + .run((context) -> assertThat(context).doesNotHaveBean(CityRepository.class) + .doesNotHaveBean(ReactiveCityRepository.class).doesNotHaveBean(CountryRepository.class) + .hasSingleBean(ReactiveCountryRepository.class)); + } + + @Configuration(proxyBeanMethods = false) + @TestAutoConfigurationPackage(City.class) + static class TestConfiguration { + + } + + @Configuration(proxyBeanMethods = false) + @TestAutoConfigurationPackage(EmptyDataPackage.class) + static class EmptyConfiguration { + + } + + @Configuration(proxyBeanMethods = false) + @EnableReactiveNeo4jRepositories("foo.bar") + @TestAutoConfigurationPackage(Neo4jReactiveRepositoriesAutoConfigurationTests.class) + static class SortOfInvalidCustomConfiguration { + + } + + @Configuration(proxyBeanMethods = false) + @EnableReactiveNeo4jRepositories(basePackageClasses = ReactiveCountryRepository.class) + static class WithCustomReactiveRepositoryScan { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jRepositoriesAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jRepositoriesAutoConfigurationIntegrationTests.java new file mode 100644 index 000000000000..41b4c7248776 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jRepositoriesAutoConfigurationIntegrationTests.java @@ -0,0 +1,75 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.neo4j; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.Neo4jContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.data.neo4j.country.CountryRepository; +import org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test to ensure that the properties get read and applied during the auto-configuration. + * + * @author Michael J. Simons + */ +@SpringBootTest +@Testcontainers(disabledWithoutDocker = true) +class Neo4jRepositoriesAutoConfigurationIntegrationTests { + + @Container + private static final Neo4jContainer neo4jServer = new Neo4jContainer<>(DockerImageNames.neo4j()) + .withStartupAttempts(5).withStartupTimeout(Duration.ofMinutes(10)); + + @DynamicPropertySource + static void neo4jProperties(DynamicPropertyRegistry registry) { + registry.add("spring.neo4j.uri", neo4jServer::getBoltUrl); + registry.add("spring.neo4j.authentication.username", () -> "neo4j"); + registry.add("spring.neo4j.authentication.password", neo4jServer::getAdminPassword); + } + + @Autowired + private CountryRepository countryRepository; + + @Test + void ensureRepositoryIsReady() { + assertThat(this.countryRepository.count()).isEqualTo(0); + } + + @Configuration + @EnableNeo4jRepositories(basePackageClasses = CountryRepository.class) + @ImportAutoConfiguration({ Neo4jAutoConfiguration.class, Neo4jDataAutoConfiguration.class, + Neo4jRepositoriesAutoConfiguration.class }) + static class TestConfiguration { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jRepositoriesAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jRepositoriesAutoConfigurationTests.java index c1539936da2b..708b4372bead 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jRepositoriesAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jRepositoriesAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,25 +16,26 @@ package org.springframework.boot.autoconfigure.data.neo4j; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; -import org.neo4j.ogm.session.SessionFactory; +import org.mockito.Mockito; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage; -import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.autoconfigure.data.alt.neo4j.CityNeo4jRepository; import org.springframework.boot.autoconfigure.data.empty.EmptyDataPackage; import org.springframework.boot.autoconfigure.data.neo4j.city.City; import org.springframework.boot.autoconfigure.data.neo4j.city.CityRepository; -import org.springframework.boot.test.util.TestPropertyValues; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.boot.autoconfigure.data.neo4j.city.ReactiveCityRepository; +import org.springframework.boot.autoconfigure.data.neo4j.country.CountryRepository; +import org.springframework.boot.autoconfigure.data.neo4j.country.ReactiveCountryRepository; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.neo4j.mapping.Neo4jMappingContext; +import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; +import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; +import org.springframework.data.neo4j.repository.support.ReactiveNeo4jRepositoryFactoryBean; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * Tests for {@link Neo4jRepositoriesAutoConfiguration}. @@ -44,73 +45,77 @@ * @author Michael Hunger * @author Vince Bickers * @author Stephane Nicoll + * @author Michael J. Simons */ class Neo4jRepositoriesAutoConfigurationTests { - private AnnotationConfigApplicationContext context; + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withUserConfiguration(MockedDriverConfiguration.class).withConfiguration( + AutoConfigurations.of(Neo4jDataAutoConfiguration.class, Neo4jRepositoriesAutoConfiguration.class)); - @AfterEach - void close() { - this.context.close(); + @Test + void configurationWithDefaultRepositories() { + this.contextRunner.withUserConfiguration(TestConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(CityRepository.class)); } @Test - void testDefaultRepositoryConfiguration() { - prepareApplicationContext(TestConfiguration.class); - assertThat(this.context.getBean(CityRepository.class)).isNotNull(); - Neo4jMappingContext mappingContext = this.context.getBean(Neo4jMappingContext.class); - assertThat(mappingContext.getPersistentEntity(City.class)).isNotNull(); + void configurationWithNoRepositories() { + this.contextRunner.withUserConfiguration(EmptyConfiguration.class).run((context) -> assertThat(context) + .hasSingleBean(Neo4jTransactionManager.class).doesNotHaveBean(Neo4jRepository.class)); } @Test - void testNoRepositoryConfiguration() { - prepareApplicationContext(EmptyConfiguration.class); - assertThat(this.context.getBean(SessionFactory.class)).isNotNull(); + void configurationWithDisabledRepositories() { + this.contextRunner.withUserConfiguration(TestConfiguration.class) + .withPropertyValues("spring.data.neo4j.repositories.type=none") + .run((context) -> assertThat(context).doesNotHaveBean(Neo4jRepository.class)); } @Test - void doesNotTriggerDefaultRepositoryDetectionIfCustomized() { - prepareApplicationContext(CustomizedConfiguration.class); - assertThat(this.context.getBean(CityNeo4jRepository.class)).isNotNull(); + void autoConfigurationShouldNotKickInEvenIfManualConfigDidNotCreateAnyRepositories() { + this.contextRunner.withUserConfiguration(SortOfInvalidCustomConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(Neo4jTransactionManager.class) + .doesNotHaveBean(Neo4jRepository.class)); } @Test - void autoConfigurationShouldNotKickInEvenIfManualConfigDidNotCreateAnyRepositories() { - prepareApplicationContext(SortOfInvalidCustomConfiguration.class); - assertThatExceptionOfType(NoSuchBeanDefinitionException.class) - .isThrownBy(() -> this.context.getBean(CityRepository.class)); + void shouldRespectAtEnableNeo4jRepositories() { + this.contextRunner.withUserConfiguration(SortOfInvalidCustomConfiguration.class, WithCustomRepositoryScan.class) + .run((context) -> assertThat(context).doesNotHaveBean(CityRepository.class) + .doesNotHaveBean(ReactiveCityRepository.class).hasSingleBean(CountryRepository.class) + .doesNotHaveBean(ReactiveCountryRepository.class)); } - private void prepareApplicationContext(Class... configurationClasses) { - this.context = new AnnotationConfigApplicationContext(); - TestPropertyValues.of("spring.data.neo4j.uri=http://localhost:9797").applyTo(this.context); - this.context.register(configurationClasses); - this.context.register(Neo4jDataAutoConfiguration.class, Neo4jRepositoriesAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class); - this.context.refresh(); + @Configuration(proxyBeanMethods = false) + @EnableNeo4jRepositories(basePackageClasses = CountryRepository.class) + static class WithCustomRepositoryScan { + } @Configuration(proxyBeanMethods = false) - @TestAutoConfigurationPackage(City.class) - static class TestConfiguration { + static class WithFakeEnabledReactiveNeo4jRepositories { + + @Bean + ReactiveNeo4jRepositoryFactoryBean reactiveNeo4jRepositoryFactoryBean() { + return Mockito.mock(ReactiveNeo4jRepositoryFactoryBean.class); + } } @Configuration(proxyBeanMethods = false) - @TestAutoConfigurationPackage(EmptyDataPackage.class) - static class EmptyConfiguration { + @TestAutoConfigurationPackage(City.class) + static class TestConfiguration { } @Configuration(proxyBeanMethods = false) - @TestAutoConfigurationPackage(Neo4jRepositoriesAutoConfigurationTests.class) - @EnableNeo4jRepositories(basePackageClasses = CityNeo4jRepository.class) - static class CustomizedConfiguration { + @TestAutoConfigurationPackage(EmptyDataPackage.class) + static class EmptyConfiguration { } @Configuration(proxyBeanMethods = false) - // To not find any repositories @EnableNeo4jRepositories("foo.bar") @TestAutoConfigurationPackage(Neo4jRepositoriesAutoConfigurationTests.class) static class SortOfInvalidCustomConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/city/City.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/city/City.java index 1ae1b828c099..29e84bdca3b0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/city/City.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/city/City.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,13 +18,12 @@ import java.io.Serializable; -import org.neo4j.ogm.annotation.GeneratedValue; -import org.neo4j.ogm.annotation.Id; -import org.neo4j.ogm.annotation.NodeEntity; - import org.springframework.boot.autoconfigure.data.neo4j.country.Country; +import org.springframework.data.neo4j.core.schema.GeneratedValue; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.Node; -@NodeEntity +@Node public class City implements Serializable { private static final long serialVersionUID = 1L; @@ -41,9 +40,6 @@ public class City implements Serializable { private String map; - public City() { - } - public City(String name, Country country) { this.name = name; this.country = country; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/city/ReactiveCityRepository.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/city/ReactiveCityRepository.java new file mode 100644 index 000000000000..8b88301d5a43 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/city/ReactiveCityRepository.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.neo4j.city; + +import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; + +public interface ReactiveCityRepository extends ReactiveNeo4jRepository { + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/country/Country.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/country/Country.java index d1e26318910b..2ab5a4267179 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/country/Country.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/country/Country.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,11 +18,11 @@ import java.io.Serializable; -import org.neo4j.ogm.annotation.GeneratedValue; -import org.neo4j.ogm.annotation.Id; -import org.neo4j.ogm.annotation.NodeEntity; +import org.springframework.data.neo4j.core.schema.GeneratedValue; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.Node; -@NodeEntity +@Node public class Country implements Serializable { private static final long serialVersionUID = 1L; @@ -33,9 +33,6 @@ public class Country implements Serializable { private String name; - public Country() { - } - public Country(String name) { this.name = name; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/country/ReactiveCountryRepository.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/country/ReactiveCountryRepository.java new file mode 100644 index 000000000000..c97759cadc60 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/country/ReactiveCountryRepository.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.neo4j.country; + +import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; + +public interface ReactiveCountryRepository extends ReactiveNeo4jRepository { + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/scan/TestNode.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/scan/TestNode.java new file mode 100644 index 000000000000..50293f98b833 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/scan/TestNode.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.neo4j.scan; + +import org.springframework.data.neo4j.core.schema.GeneratedValue; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.Node; + +@Node +public class TestNode { + + @Id + @GeneratedValue + private Long id; + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/scan/TestNonAnnotated.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/scan/TestNonAnnotated.java new file mode 100644 index 000000000000..c754c3877503 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/scan/TestNonAnnotated.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.neo4j.scan; + +import org.springframework.data.neo4j.core.schema.GeneratedValue; +import org.springframework.data.neo4j.core.schema.Id; + +public class TestNonAnnotated { + + @Id + @GeneratedValue + private Long id; + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/scan/TestPersistent.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/scan/TestPersistent.java new file mode 100644 index 000000000000..54aee30b7b14 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/scan/TestPersistent.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.neo4j.scan; + +import org.springframework.data.annotation.Persistent; +import org.springframework.data.neo4j.core.schema.GeneratedValue; +import org.springframework.data.neo4j.core.schema.Id; + +@Persistent +public class TestPersistent { + + @Id + @GeneratedValue + private Long id; + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/scan/TestRelationshipProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/scan/TestRelationshipProperties.java new file mode 100644 index 000000000000..872742da88ee --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/neo4j/scan/TestRelationshipProperties.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.neo4j.scan; + +import org.springframework.data.neo4j.core.schema.GeneratedValue; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.RelationshipProperties; + +@RelationshipProperties +public class TestRelationshipProperties { + + @Id + @GeneratedValue + Long id; + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/r2dbc/R2dbcDataAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/r2dbc/R2dbcDataAutoConfigurationTests.java new file mode 100644 index 000000000000..8a3b8d6733b9 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/r2dbc/R2dbcDataAutoConfigurationTests.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.r2dbc; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.data.r2dbc.core.R2dbcEntityTemplate; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link R2dbcDataAutoConfiguration}. + * + * @author Mark Paluch + */ +class R2dbcDataAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class)); + + @Test + void r2dbcEntityTemplateIsConfigured() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(R2dbcEntityTemplate.class)); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/r2dbc/R2dbcRepositoriesAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/r2dbc/R2dbcRepositoriesAutoConfigurationTests.java new file mode 100644 index 000000000000..99e372dc8062 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/r2dbc/R2dbcRepositoriesAutoConfigurationTests.java @@ -0,0 +1,134 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.r2dbc; + +import io.r2dbc.spi.ConnectionFactory; +import org.junit.jupiter.api.Test; +import reactor.test.StepVerifier; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage; +import org.springframework.boot.autoconfigure.data.empty.EmptyDataPackage; +import org.springframework.boot.autoconfigure.data.r2dbc.city.City; +import org.springframework.boot.autoconfigure.data.r2dbc.city.CityRepository; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories; +import org.springframework.data.r2dbc.repository.config.R2dbcRepositoryConfigurationExtension; +import org.springframework.data.repository.Repository; +import org.springframework.r2dbc.connection.init.ResourceDatabasePopulator; +import org.springframework.r2dbc.core.DatabaseClient; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link R2dbcRepositoriesAutoConfiguration}. + * + * @author Mark Paluch + */ +class R2dbcRepositoriesAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(R2dbcRepositoriesAutoConfiguration.class)); + + @Test + void backsOffWithNoConnectionFactory() { + this.contextRunner.withUserConfiguration(TestConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(R2dbcRepositoryConfigurationExtension.class)); + } + + @Test + void backsOffWithNoDatabaseClientOperations() { + this.contextRunner.withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class)) + .withClassLoader(new FilteredClassLoader("org.springframework.r2dbc")) + .withUserConfiguration(TestConfiguration.class).run((context) -> { + assertThat(context).doesNotHaveBean(DatabaseClient.class); + assertThat(context).doesNotHaveBean(R2dbcRepositoryConfigurationExtension.class); + }); + } + + @Test + void basicAutoConfiguration() { + this.contextRunner + .withConfiguration( + AutoConfigurations.of(R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class)) + .withUserConfiguration(DatabaseInitializationConfiguration.class, TestConfiguration.class) + .withPropertyValues("spring.r2dbc.generate-unique-name:true").run((context) -> { + assertThat(context).hasSingleBean(CityRepository.class); + context.getBean(CityRepository.class).findById(2000L).as(StepVerifier::create).expectNextCount(1) + .verifyComplete(); + }); + } + + @Test + void autoConfigurationWithNoRepositories() { + this.contextRunner.withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class)) + .withUserConfiguration(EmptyConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(Repository.class)); + } + + @Test + void honorsUsersEnableR2dbcRepositoriesConfiguration() { + this.contextRunner + .withConfiguration( + AutoConfigurations.of(R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class)) + .withUserConfiguration(DatabaseInitializationConfiguration.class, EnableRepositoriesConfiguration.class) + .withPropertyValues("spring.r2dbc.generate-unique-name:true").run((context) -> { + assertThat(context).hasSingleBean(CityRepository.class); + context.getBean(CityRepository.class).findById(2000L).as(StepVerifier::create).expectNextCount(1) + .verifyComplete(); + }); + } + + @Configuration(proxyBeanMethods = false) + static class DatabaseInitializationConfiguration { + + @Autowired + void initializeDatabase(ConnectionFactory connectionFactory) { + ResourceLoader resourceLoader = new DefaultResourceLoader(); + Resource[] scripts = new Resource[] { resourceLoader.getResource("classpath:data-city-schema.sql"), + resourceLoader.getResource("classpath:city.sql") }; + new ResourceDatabasePopulator(scripts).populate(connectionFactory).block(); + } + + } + + @Configuration(proxyBeanMethods = false) + @TestAutoConfigurationPackage(City.class) + static class TestConfiguration { + + } + + @Configuration(proxyBeanMethods = false) + @TestAutoConfigurationPackage(EmptyDataPackage.class) + static class EmptyConfiguration { + + } + + @Configuration(proxyBeanMethods = false) + @EnableR2dbcRepositories(basePackageClasses = City.class) + static class EnableRepositoriesConfiguration { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/r2dbc/city/City.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/r2dbc/city/City.java new file mode 100644 index 000000000000..3543139530e9 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/r2dbc/city/City.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.r2dbc.city; + +import org.springframework.data.annotation.Id; +import org.springframework.data.relational.core.mapping.Table; + +@Table("CITY") +public class City { + + @Id + private Long id; + + private String name; + + private String state; + + private String country; + + private String map; + + protected City() { + } + + public City(String name, String country) { + this.name = name; + this.country = country; + } + + public String getName() { + return this.name; + } + + public String getState() { + return this.state; + } + + public String getCountry() { + return this.country; + } + + public String getMap() { + return this.map; + } + + @Override + public String toString() { + return getName() + "," + getState() + "," + getCountry(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/r2dbc/city/CityRepository.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/r2dbc/city/CityRepository.java new file mode 100644 index 000000000000..adff0d8121d2 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/r2dbc/city/CityRepository.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.r2dbc.city; + +import org.springframework.data.repository.reactive.ReactiveCrudRepository; + +public interface CityRepository extends ReactiveCrudRepository { + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationJedisTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationJedisTests.java index 9bc153a49f26..7686f34808e6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationJedisTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationJedisTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,15 +18,16 @@ import org.junit.jupiter.api.Test; -import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.testsupport.classpath.ClassPathExclusions; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.jedis.JedisClientConfiguration.JedisClientConfigurationBuilder; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; +import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -42,12 +43,25 @@ class RedisAutoConfigurationJedisTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class)); + @Test + void connectionFactoryDefaultsToJedis() { + this.contextRunner.run((context) -> assertThat(context.getBean("redisConnectionFactory")) + .isInstanceOf(JedisConnectionFactory.class)); + } + + @Test + void connectionFactoryIsNotCreatedWhenLettuceIsSelected() { + this.contextRunner.withPropertyValues("spring.redis.client-type=lettuce") + .run((context) -> assertThat(context).doesNotHaveBean(RedisConnectionFactory.class)); + } + @Test void testOverrideRedisConfiguration() { this.contextRunner.withPropertyValues("spring.redis.host:foo", "spring.redis.database:1").run((context) -> { JedisConnectionFactory cf = context.getBean(JedisConnectionFactory.class); assertThat(cf.getHostName()).isEqualTo("foo"); assertThat(cf.getDatabase()).isEqualTo(1); + assertThat(getUserName(cf)).isNull(); assertThat(cf.getPassword()).isNull(); assertThat(cf.isUseSsl()).isFalse(); }); @@ -69,6 +83,7 @@ void testRedisUrlConfiguration() { JedisConnectionFactory cf = context.getBean(JedisConnectionFactory.class); assertThat(cf.getHostName()).isEqualTo("example"); assertThat(cf.getPort()).isEqualTo(33); + assertThat(getUserName(cf)).isEqualTo("user"); assertThat(cf.getPassword()).isEqualTo("password"); assertThat(cf.isUseSsl()).isFalse(); }); @@ -83,6 +98,7 @@ void testOverrideUrlRedisConfiguration() { JedisConnectionFactory cf = context.getBean(JedisConnectionFactory.class); assertThat(cf.getHostName()).isEqualTo("example"); assertThat(cf.getPort()).isEqualTo(33); + assertThat(getUserName(cf)).isEqualTo("user"); assertThat(cf.getPassword()).isEqualTo("password"); assertThat(cf.isUseSsl()).isTrue(); }); @@ -91,18 +107,22 @@ void testOverrideUrlRedisConfiguration() { @Test void testPasswordInUrlWithColon() { this.contextRunner.withPropertyValues("spring.redis.url:redis://:pass:word@example:33").run((context) -> { - assertThat(context.getBean(JedisConnectionFactory.class).getHostName()).isEqualTo("example"); - assertThat(context.getBean(JedisConnectionFactory.class).getPort()).isEqualTo(33); - assertThat(context.getBean(JedisConnectionFactory.class).getPassword()).isEqualTo("pass:word"); + JedisConnectionFactory cf = context.getBean(JedisConnectionFactory.class); + assertThat(cf.getHostName()).isEqualTo("example"); + assertThat(cf.getPort()).isEqualTo(33); + assertThat(getUserName(cf)).isEqualTo(""); + assertThat(cf.getPassword()).isEqualTo("pass:word"); }); } @Test void testPasswordInUrlStartsWithColon() { this.contextRunner.withPropertyValues("spring.redis.url:redis://user::pass:word@example:33").run((context) -> { - assertThat(context.getBean(JedisConnectionFactory.class).getHostName()).isEqualTo("example"); - assertThat(context.getBean(JedisConnectionFactory.class).getPort()).isEqualTo(33); - assertThat(context.getBean(JedisConnectionFactory.class).getPassword()).isEqualTo(":pass:word"); + JedisConnectionFactory cf = context.getBean(JedisConnectionFactory.class); + assertThat(cf.getHostName()).isEqualTo("example"); + assertThat(cf.getPort()).isEqualTo(33); + assertThat(getUserName(cf)).isEqualTo("user"); + assertThat(cf.getPassword()).isEqualTo(":pass:word"); }); } @@ -123,11 +143,23 @@ void testRedisConfigurationWithPool() { } @Test - void testRedisConfigurationWithTimeout() { - this.contextRunner.withPropertyValues("spring.redis.host:foo", "spring.redis.timeout:100").run((context) -> { + void testRedisConfigurationWithTimeoutAndConnectTimeout() { + this.contextRunner.withPropertyValues("spring.redis.host:foo", "spring.redis.timeout:250", + "spring.redis.connect-timeout:1000").run((context) -> { + JedisConnectionFactory cf = context.getBean(JedisConnectionFactory.class); + assertThat(cf.getHostName()).isEqualTo("foo"); + assertThat(cf.getTimeout()).isEqualTo(250); + assertThat(cf.getClientConfiguration().getConnectTimeout().toMillis()).isEqualTo(1000); + }); + } + + @Test + void testRedisConfigurationWithDefaultTimeouts() { + this.contextRunner.withPropertyValues("spring.redis.host:foo").run((context) -> { JedisConnectionFactory cf = context.getBean(JedisConnectionFactory.class); assertThat(cf.getHostName()).isEqualTo("foo"); - assertThat(cf.getTimeout()).isEqualTo(100); + assertThat(cf.getTimeout()).isEqualTo(2000); + assertThat(cf.getClientConfiguration().getConnectTimeout().toMillis()).isEqualTo(2000); }); } @@ -153,13 +185,13 @@ void testRedisConfigurationWithSentinel() { } @Test - void testRedisConfigurationWithSentinelAndPassword() { - this.contextRunner - .withPropertyValues("spring.redis.password=password", "spring.redis.sentinel.master:mymaster", - "spring.redis.sentinel.nodes:127.0.0.1:26379,127.0.0.1:26380") + void testRedisConfigurationWithSentinelAndAuthentication() { + this.contextRunner.withPropertyValues("spring.redis.username=user", "spring.redis.password=password", + "spring.redis.sentinel.master:mymaster", "spring.redis.sentinel.nodes:127.0.0.1:26379,127.0.0.1:26380") .withUserConfiguration(JedisConnectionFactoryCaptorConfiguration.class).run((context) -> { assertThat(context).hasFailed(); assertThat(JedisConnectionFactoryCaptor.connectionFactory.isRedisSentinelAware()).isTrue(); + assertThat(getUserName(JedisConnectionFactoryCaptor.connectionFactory)).isEqualTo("user"); assertThat(JedisConnectionFactoryCaptor.connectionFactory.getPassword()).isEqualTo("password"); }); } @@ -171,6 +203,10 @@ void testRedisConfigurationWithCluster() { .isNotNull()); } + private String getUserName(JedisConnectionFactory factory) { + return ReflectionTestUtils.invokeMethod(factory, "getRedisUsername"); + } + @Configuration(proxyBeanMethods = false) static class CustomConfiguration { @@ -196,7 +232,7 @@ static class JedisConnectionFactoryCaptor implements BeanPostProcessor { static JedisConnectionFactory connectionFactory; @Override - public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + public Object postProcessBeforeInitialization(Object bean, String beanName) { if (bean instanceof JedisConnectionFactory) { connectionFactory = (JedisConnectionFactory) bean; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationTests.java index 68c4ffb08222..1b7a64aa607b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,19 +17,31 @@ package org.springframework.boot.autoconfigure.data.redis; import java.util.Arrays; +import java.util.EnumSet; import java.util.List; import java.util.Set; +import java.util.function.Consumer; import java.util.stream.Collectors; +import io.lettuce.core.ClientOptions; +import io.lettuce.core.cluster.ClusterClientOptions; +import io.lettuce.core.cluster.ClusterTopologyRefreshOptions; +import io.lettuce.core.cluster.ClusterTopologyRefreshOptions.RefreshTrigger; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisClusterConfiguration; +import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisNode; +import org.springframework.data.redis.connection.RedisSentinelConfiguration; +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration.LettuceClientConfigurationBuilder; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration; @@ -39,6 +51,7 @@ import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * Tests for {@link RedisAutoConfiguration}. @@ -51,17 +64,20 @@ * @author Mark Paluch * @author Stephane Nicoll * @author Alen Turkovic + * @author Scott Frederick */ class RedisAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class)); @Test void testDefaultRedisConfiguration() { this.contextRunner.run((context) -> { - assertThat(context.getBean("redisTemplate", RedisOperations.class)).isNotNull(); - assertThat(context.getBean(StringRedisTemplate.class)).isNotNull(); + assertThat(context.getBean("redisTemplate")).isInstanceOf(RedisOperations.class); + assertThat(context).hasSingleBean(StringRedisTemplate.class); + assertThat(context).hasSingleBean(RedisConnectionFactory.class); + assertThat(context.getBean(RedisConnectionFactory.class)).isInstanceOf(LettuceConnectionFactory.class); }); } @@ -72,6 +88,7 @@ void testOverrideRedisConfiguration() { LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); assertThat(cf.getHostName()).isEqualTo("foo"); assertThat(cf.getDatabase()).isEqualTo(1); + assertThat(getUserName(cf)).isNull(); assertThat(cf.getPassword()).isNull(); assertThat(cf.isUseSsl()).isFalse(); assertThat(cf.getShutdownTimeout()).isEqualTo(500); @@ -94,6 +111,7 @@ void testRedisUrlConfiguration() { LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); assertThat(cf.getHostName()).isEqualTo("example"); assertThat(cf.getPort()).isEqualTo(33); + assertThat(getUserName(cf)).isEqualTo("user"); assertThat(cf.getPassword()).isEqualTo("password"); assertThat(cf.isUseSsl()).isFalse(); }); @@ -108,6 +126,7 @@ void testOverrideUrlRedisConfiguration() { LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); assertThat(cf.getHostName()).isEqualTo("example"); assertThat(cf.getPort()).isEqualTo(33); + assertThat(getUserName(cf)).isEqualTo("user"); assertThat(cf.getPassword()).isEqualTo("password"); assertThat(cf.isUseSsl()).isTrue(); }); @@ -119,6 +138,7 @@ void testPasswordInUrlWithColon() { LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); assertThat(cf.getHostName()).isEqualTo("example"); assertThat(cf.getPort()).isEqualTo(33); + assertThat(getUserName(cf)).isEqualTo(""); assertThat(cf.getPassword()).isEqualTo("pass:word"); }); } @@ -129,6 +149,7 @@ void testPasswordInUrlStartsWithColon() { LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); assertThat(cf.getHostName()).isEqualTo("example"); assertThat(cf.getPort()).isEqualTo(33); + assertThat(getUserName(cf)).isEqualTo("user"); assertThat(cf.getPassword()).isEqualTo(":pass:word"); }); } @@ -152,11 +173,25 @@ void testRedisConfigurationWithPool() { } @Test - void testRedisConfigurationWithTimeout() { - this.contextRunner.withPropertyValues("spring.redis.host:foo", "spring.redis.timeout:100").run((context) -> { + void testRedisConfigurationWithTimeoutAndConnectTimeout() { + this.contextRunner.withPropertyValues("spring.redis.host:foo", "spring.redis.timeout:250", + "spring.redis.connect-timeout:1000").run((context) -> { + LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); + assertThat(cf.getHostName()).isEqualTo("foo"); + assertThat(cf.getTimeout()).isEqualTo(250); + assertThat(cf.getClientConfiguration().getClientOptions().get().getSocketOptions() + .getConnectTimeout().toMillis()).isEqualTo(1000); + }); + } + + @Test + void testRedisConfigurationWithDefaultTimeouts() { + this.contextRunner.withPropertyValues("spring.redis.host:foo").run((context) -> { LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); assertThat(cf.getHostName()).isEqualTo("foo"); - assertThat(cf.getTimeout()).isEqualTo(100); + assertThat(cf.getTimeout()).isEqualTo(60000); + assertThat(cf.getClientConfiguration().getClientOptions().get().getSocketOptions().getConnectTimeout() + .toMillis()).isEqualTo(10000); }); } @@ -170,6 +205,22 @@ void testRedisConfigurationWithClientName() { }); } + @Test + void connectionFactoryWithJedisClientType() { + this.contextRunner.withPropertyValues("spring.redis.client-type:jedis").run((context) -> { + assertThat(context).hasSingleBean(RedisConnectionFactory.class); + assertThat(context.getBean(RedisConnectionFactory.class)).isInstanceOf(JedisConnectionFactory.class); + }); + } + + @Test + void connectionFactoryWithLettuceClientType() { + this.contextRunner.withPropertyValues("spring.redis.client-type:lettuce").run((context) -> { + assertThat(context).hasSingleBean(RedisConnectionFactory.class); + assertThat(context.getBean(RedisConnectionFactory.class)).isInstanceOf(LettuceConnectionFactory.class); + }); + } + @Test void testRedisConfigurationWithSentinel() { List sentinels = Arrays.asList("127.0.0.1:26379", "127.0.0.1:26380"); @@ -191,17 +242,48 @@ void testRedisConfigurationWithSentinelAndDatabase() { } @Test - void testRedisConfigurationWithSentinelAndPassword() { - this.contextRunner.withPropertyValues("spring.redis.password=password", "spring.redis.sentinel.master:mymaster", + void testRedisConfigurationWithSentinelAndAuthentication() { + this.contextRunner.withPropertyValues("spring.redis.username=user", "spring.redis.password=password", + "spring.redis.sentinel.master:mymaster", "spring.redis.sentinel.nodes:127.0.0.1:26379, 127.0.0.1:26380").run((context) -> { LettuceConnectionFactory connectionFactory = context.getBean(LettuceConnectionFactory.class); + assertThat(getUserName(connectionFactory)).isEqualTo("user"); assertThat(connectionFactory.getPassword()).isEqualTo("password"); + RedisSentinelConfiguration sentinelConfiguration = connectionFactory.getSentinelConfiguration(); + assertThat(sentinelConfiguration.getSentinelPassword().isPresent()).isFalse(); Set sentinels = connectionFactory.getSentinelConfiguration().getSentinels(); assertThat(sentinels.stream().map(Object::toString).collect(Collectors.toSet())) .contains("127.0.0.1:26379", "127.0.0.1:26380"); }); } + @Test + void testRedisConfigurationWithSentinelPasswordAndDataNodePassword() { + this.contextRunner.withPropertyValues("spring.redis.password=password", "spring.redis.sentinel.password=secret", + "spring.redis.sentinel.master:mymaster", + "spring.redis.sentinel.nodes:127.0.0.1:26379, 127.0.0.1:26380").run((context) -> { + LettuceConnectionFactory connectionFactory = context.getBean(LettuceConnectionFactory.class); + assertThat(getUserName(connectionFactory)).isNull(); + assertThat(connectionFactory.getPassword()).isEqualTo("password"); + RedisSentinelConfiguration sentinelConfiguration = connectionFactory.getSentinelConfiguration(); + assertThat(new String(sentinelConfiguration.getSentinelPassword().get())).isEqualTo("secret"); + Set sentinels = sentinelConfiguration.getSentinels(); + assertThat(sentinels.stream().map(Object::toString).collect(Collectors.toSet())) + .contains("127.0.0.1:26379", "127.0.0.1:26380"); + }); + } + + @Test + void testRedisSentinelUrlConfiguration() { + this.contextRunner + .withPropertyValues( + "spring.redis.url=redis-sentinel://username:password@127.0.0.1:26379,127.0.0.1:26380/mymaster") + .run((context) -> assertThatIllegalStateException() + .isThrownBy(() -> context.getBean(LettuceConnectionFactory.class)) + .withRootCauseInstanceOf(RedisUrlSyntaxException.class).havingRootCause().withMessageContaining( + "Invalid Redis URL 'redis-sentinel://username:password@127.0.0.1:26379,127.0.0.1:26380/mymaster'")); + } + @Test void testRedisConfigurationWithCluster() { List clusterNodes = Arrays.asList("127.0.0.1:27379", "127.0.0.1:27380"); @@ -218,20 +300,110 @@ void testRedisConfigurationWithCluster() { } @Test - void testRedisConfigurationWithClusterAndPassword() { + void testRedisConfigurationWithClusterAndAuthentication() { List clusterNodes = Arrays.asList("127.0.0.1:27379", "127.0.0.1:27380"); + this.contextRunner.withPropertyValues("spring.redis.username=user", "spring.redis.password=password", + "spring.redis.cluster.nodes[0]:" + clusterNodes.get(0), + "spring.redis.cluster.nodes[1]:" + clusterNodes.get(1)).run((context) -> { + LettuceConnectionFactory connectionFactory = context.getBean(LettuceConnectionFactory.class); + assertThat(getUserName(connectionFactory)).isEqualTo("user"); + assertThat(connectionFactory.getPassword()).isEqualTo("password"); + } + + ); + } + + @Test + void testRedisConfigurationCreateClientOptionsByDefault() { + this.contextRunner.run(assertClientOptions(ClientOptions.class, (options) -> { + assertThat(options.getTimeoutOptions().isApplyConnectionTimeout()).isTrue(); + assertThat(options.getTimeoutOptions().isTimeoutCommands()).isTrue(); + })); + } + + @Test + void testRedisConfigurationWithClusterCreateClusterClientOptions() { + this.contextRunner.withPropertyValues("spring.redis.cluster.nodes=127.0.0.1:27379,127.0.0.1:27380") + .run(assertClientOptions(ClusterClientOptions.class, (options) -> { + assertThat(options.getTimeoutOptions().isApplyConnectionTimeout()).isTrue(); + assertThat(options.getTimeoutOptions().isTimeoutCommands()).isTrue(); + })); + } + + @Test + void testRedisConfigurationWithClusterRefreshPeriod() { + this.contextRunner + .withPropertyValues("spring.redis.cluster.nodes=127.0.0.1:27379,127.0.0.1:27380", + "spring.redis.lettuce.cluster.refresh.period=30s") + .run(assertClientOptions(ClusterClientOptions.class, + (options) -> assertThat(options.getTopologyRefreshOptions().getRefreshPeriod()) + .hasSeconds(30))); + } + + @Test + void testRedisConfigurationWithClusterAdaptiveRefresh() { this.contextRunner - .withPropertyValues("spring.redis.password=password", - "spring.redis.cluster.nodes[0]:" + clusterNodes.get(0), - "spring.redis.cluster.nodes[1]:" + clusterNodes.get(1)) - .run((context) -> assertThat(context.getBean(LettuceConnectionFactory.class).getPassword()) - .isEqualTo("password") + .withPropertyValues("spring.redis.cluster.nodes=127.0.0.1:27379,127.0.0.1:27380", + "spring.redis.lettuce.cluster.refresh.adaptive=true") + .run(assertClientOptions(ClusterClientOptions.class, + (options) -> assertThat(options.getTopologyRefreshOptions().getAdaptiveRefreshTriggers()) + .isEqualTo(EnumSet.allOf(RefreshTrigger.class)))); + } - ); + @Test + void testRedisConfigurationWithClusterRefreshPeriodHasNoEffectWithNonClusteredConfiguration() { + this.contextRunner.withPropertyValues("spring.redis.cluster.refresh.period=30s").run(assertClientOptions( + ClientOptions.class, (options) -> assertThat(options.getClass()).isEqualTo(ClientOptions.class))); + } + + @Test + void testRedisConfigurationWithClusterDynamicRefreshSourcesEnabled() { + this.contextRunner + .withPropertyValues("spring.redis.cluster.nodes=127.0.0.1:27379,127.0.0.1:27380", + "spring.redis.lettuce.cluster.refresh.dynamic-refresh-sources=true") + .run(assertClientOptions(ClusterClientOptions.class, + (options) -> assertThat(options.getTopologyRefreshOptions().useDynamicRefreshSources()) + .isTrue())); + } + + @Test + void testRedisConfigurationWithClusterDynamicRefreshSourcesDisabled() { + this.contextRunner + .withPropertyValues("spring.redis.cluster.nodes=127.0.0.1:27379,127.0.0.1:27380", + "spring.redis.lettuce.cluster.refresh.dynamic-refresh-sources=false") + .run(assertClientOptions(ClusterClientOptions.class, + (options) -> assertThat(options.getTopologyRefreshOptions().useDynamicRefreshSources()) + .isFalse())); + } + + @Test + void testRedisConfigurationWithClusterDynamicSourcesUnspecifiedUsesDefault() { + this.contextRunner + .withPropertyValues("spring.redis.cluster.nodes=127.0.0.1:27379,127.0.0.1:27380", + "spring.redis.lettuce.cluster.refresh.dynamic-sources=") + .run(assertClientOptions(ClusterClientOptions.class, + (options) -> assertThat(options.getTopologyRefreshOptions().useDynamicRefreshSources()) + .isEqualTo(ClusterTopologyRefreshOptions.DEFAULT_DYNAMIC_REFRESH_SOURCES))); + } + + private ContextConsumer assertClientOptions( + Class expectedType, Consumer options) { + return (context) -> { + LettuceClientConfiguration clientConfiguration = context.getBean(LettuceConnectionFactory.class) + .getClientConfiguration(); + assertThat(clientConfiguration.getClientOptions()).isPresent(); + ClientOptions clientOptions = clientConfiguration.getClientOptions().get(); + assertThat(clientOptions.getClass()).isEqualTo(expectedType); + options.accept(expectedType.cast(clientOptions)); + }; } private LettucePoolingClientConfiguration getPoolingClientConfiguration(LettuceConnectionFactory factory) { - return (LettucePoolingClientConfiguration) ReflectionTestUtils.getField(factory, "clientConfiguration"); + return (LettucePoolingClientConfiguration) factory.getClientConfiguration(); + } + + private String getUserName(LettuceConnectionFactory factory) { + return ReflectionTestUtils.invokeMethod(factory, "getRedisUsername"); } @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisPropertiesTests.java new file mode 100644 index 000000000000..0a42e51a733b --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisPropertiesTests.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.redis; + +import io.lettuce.core.cluster.ClusterTopologyRefreshOptions; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.data.redis.RedisProperties.Lettuce; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link RedisProperties}. + * + * @author Stephane Nicoll + */ +class RedisPropertiesTests { + + @Test + void lettuceDefaultsAreConsistent() { + Lettuce lettuce = new RedisProperties().getLettuce(); + ClusterTopologyRefreshOptions defaultClusterTopologyRefreshOptions = ClusterTopologyRefreshOptions.builder() + .build(); + assertThat(lettuce.getCluster().getRefresh().isDynamicRefreshSources()) + .isEqualTo(defaultClusterTopologyRefreshOptions.useDynamicRefreshSources()); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisReactiveAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisReactiveAutoConfigurationTests.java index ba4738700997..fb0a6abd1411 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisReactiveAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisReactiveAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ */ class RedisReactiveAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration( + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration( AutoConfigurations.of(RedisAutoConfiguration.class, RedisReactiveAutoConfiguration.class)); @Test diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisRepositoriesAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisRepositoriesAutoConfigurationTests.java index 08eca2b28c4a..fd9c4338e17a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisRepositoriesAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisRepositoriesAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,13 +48,14 @@ class RedisRepositoriesAutoConfigurationTests { @Container public static RedisContainer redis = new RedisContainer().withStartupAttempts(5) - .withStartupTimeout(Duration.ofMinutes(2)); + .withStartupTimeout(Duration.ofMinutes(10)); private AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); @BeforeEach void setUp() { - TestPropertyValues.of("spring.redis.port=" + redis.getFirstMappedPort()).applyTo(this.context.getEnvironment()); + TestPropertyValues.of("spring.redis.host=" + redis.getHost(), "spring.redis.port=" + redis.getFirstMappedPort()) + .applyTo(this.context.getEnvironment()); } @AfterEach diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisUrlSyntaxFailureAnalyzerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisUrlSyntaxFailureAnalyzerTests.java new file mode 100644 index 000000000000..7e13fec03d55 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisUrlSyntaxFailureAnalyzerTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.data.redis; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.diagnostics.FailureAnalysis; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link RedisUrlSyntaxFailureAnalyzer}. + * + * @author Scott Frederick + */ +class RedisUrlSyntaxFailureAnalyzerTests { + + @Test + void analyzeInvalidUrlSyntax() { + RedisUrlSyntaxException exception = new RedisUrlSyntaxException("redis://invalid"); + FailureAnalysis analysis = new RedisUrlSyntaxFailureAnalyzer().analyze(exception); + assertThat(analysis.getDescription()).contains("The URL 'redis://invalid' is not valid"); + assertThat(analysis.getAction()).contains("Review the value of the property 'spring.redis.url'"); + } + + @Test + void analyzeRedisHttpUrl() { + RedisUrlSyntaxException exception = new RedisUrlSyntaxException("http://127.0.0.1:26379/mymaster"); + FailureAnalysis analysis = new RedisUrlSyntaxFailureAnalyzer().analyze(exception); + assertThat(analysis.getDescription()).contains("The URL 'http://127.0.0.1:26379/mymaster' is not valid") + .contains("The scheme 'http' is not supported"); + assertThat(analysis.getAction()).contains("Use the scheme 'redis://' for insecure or 'rediss://' for secure"); + } + + @Test + void analyzeRedisSentinelUrl() { + RedisUrlSyntaxException exception = new RedisUrlSyntaxException( + "redis-sentinel://username:password@127.0.0.1:26379,127.0.0.1:26380/mymaster"); + FailureAnalysis analysis = new RedisUrlSyntaxFailureAnalyzer().analyze(exception); + assertThat(analysis.getDescription()).contains( + "The URL 'redis-sentinel://username:password@127.0.0.1:26379,127.0.0.1:26380/mymaster' is not valid") + .contains("The scheme 'redis-sentinel' is not supported"); + assertThat(analysis.getAction()).contains("Use spring.redis.sentinel properties"); + } + + @Test + void analyzeRedisSocketUrl() { + RedisUrlSyntaxException exception = new RedisUrlSyntaxException("redis-socket:///redis/redis.sock"); + FailureAnalysis analysis = new RedisUrlSyntaxFailureAnalyzer().analyze(exception); + assertThat(analysis.getDescription()).contains("The URL 'redis-socket:///redis/redis.sock' is not valid") + .contains("The scheme 'redis-socket' is not supported"); + assertThat(analysis.getAction()).contains("Configure the appropriate Spring Data Redis connection beans"); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/rest/RepositoryRestMvcAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/rest/RepositoryRestMvcAutoConfigurationTests.java index 236f8b506da1..9a14631112f4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/rest/RepositoryRestMvcAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/rest/RepositoryRestMvcAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,6 +42,7 @@ import org.springframework.http.MediaType; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.mock.web.MockServletContext; +import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import static org.assertj.core.api.Assertions.assertThat; @@ -76,7 +77,7 @@ void testWithCustomBasePath() { assertThat(this.context.getBean(RepositoryRestMvcConfiguration.class)).isNotNull(); RepositoryRestConfiguration bean = this.context.getBean(RepositoryRestConfiguration.class); URI expectedUri = URI.create("/foo"); - assertThat(bean.getBaseUri()).as("Custom basePath not set").isEqualTo(expectedUri); + assertThat(bean.getBasePath()).as("Custom basePath not set").isEqualTo(expectedUri); BaseUri baseUri = this.context.getBean(BaseUri.class); assertThat(expectedUri).as("Custom basePath has not been applied to BaseUri bean").isEqualTo(baseUri.getUri()); } @@ -119,7 +120,7 @@ void backOffWithCustomConfiguration() { load(TestConfigurationWithRestMvcConfig.class, "spring.data.rest.base-path:foo"); assertThat(this.context.getBean(RepositoryRestMvcConfiguration.class)).isNotNull(); RepositoryRestConfiguration bean = this.context.getBean(RepositoryRestConfiguration.class); - assertThat(bean.getBaseUri()).isEqualTo(URI.create("")); + assertThat(bean.getBasePath()).isEqualTo(URI.create("")); } private void load(Class config, String... environment) { @@ -174,7 +175,7 @@ Jackson2ObjectMapperBuilder objectMapperBuilder() { static class TestRepositoryRestConfigurer implements RepositoryRestConfigurer { @Override - public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) { + public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config, CorsRegistry cors) { config.setRepositoryDetectionStrategy(RepositoryDetectionStrategies.ALL); config.setDefaultMediaType(MediaType.parseMediaType("application/my-custom-json")); config.setMaxPageSize(78); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/solr/SolrRepositoriesAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/solr/SolrRepositoriesAutoConfigurationTests.java deleted file mode 100644 index 7f9b6216bd74..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/solr/SolrRepositoriesAutoConfigurationTests.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.data.solr; - -import org.apache.solr.client.solrj.SolrClient; -import org.apache.solr.client.solrj.impl.HttpSolrClient; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.NoSuchBeanDefinitionException; -import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage; -import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.autoconfigure.data.alt.solr.CitySolrRepository; -import org.springframework.boot.autoconfigure.data.empty.EmptyDataPackage; -import org.springframework.boot.autoconfigure.data.solr.city.City; -import org.springframework.boot.autoconfigure.data.solr.city.CityRepository; -import org.springframework.boot.autoconfigure.solr.SolrAutoConfiguration; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.solr.repository.config.EnableSolrRepositories; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -/** - * Tests for {@link SolrRepositoriesAutoConfiguration}. - * - * @author Christoph Strobl - * @author Oliver Gierke - */ -class SolrRepositoriesAutoConfigurationTests { - - private AnnotationConfigApplicationContext context; - - @AfterEach - void close() { - this.context.close(); - } - - @Test - void testDefaultRepositoryConfiguration() { - initContext(TestConfiguration.class); - assertThat(this.context.getBean(CityRepository.class)).isNotNull(); - assertThat(this.context.getBean(SolrClient.class)).isInstanceOf(HttpSolrClient.class); - } - - @Test - void testNoRepositoryConfiguration() { - initContext(EmptyConfiguration.class); - assertThat(this.context.getBean(SolrClient.class)).isInstanceOf(HttpSolrClient.class); - } - - @Test - void doesNotTriggerDefaultRepositoryDetectionIfCustomized() { - initContext(CustomizedConfiguration.class); - assertThat(this.context.getBean(CitySolrRepository.class)).isNotNull(); - } - - @Test - void autoConfigurationShouldNotKickInEvenIfManualConfigDidNotCreateAnyRepositories() { - initContext(SortOfInvalidCustomConfiguration.class); - assertThatExceptionOfType(NoSuchBeanDefinitionException.class) - .isThrownBy(() -> this.context.getBean(CityRepository.class)); - } - - private void initContext(Class configClass) { - - this.context = new AnnotationConfigApplicationContext(); - this.context.register(configClass, SolrAutoConfiguration.class, SolrRepositoriesAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class); - this.context.refresh(); - } - - @Configuration(proxyBeanMethods = false) - @TestAutoConfigurationPackage(City.class) - static class TestConfiguration { - - } - - @Configuration(proxyBeanMethods = false) - @TestAutoConfigurationPackage(EmptyDataPackage.class) - static class EmptyConfiguration { - - } - - @Configuration(proxyBeanMethods = false) - @TestAutoConfigurationPackage(SolrRepositoriesAutoConfigurationTests.class) - @EnableSolrRepositories(basePackageClasses = CitySolrRepository.class) - static class CustomizedConfiguration { - - } - - @Configuration(proxyBeanMethods = false) - @TestAutoConfigurationPackage(SolrRepositoriesAutoConfigurationTests.class) - // To not find any repositories - @EnableSolrRepositories("foo.bar") - static class SortOfInvalidCustomConfiguration { - - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/solr/city/City.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/solr/city/City.java deleted file mode 100644 index 61b73b0e6869..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/solr/city/City.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.data.solr.city; - -import org.springframework.data.annotation.Id; -import org.springframework.data.solr.core.mapping.Indexed; -import org.springframework.data.solr.core.mapping.SolrDocument; - -/** - * @author Christoph Strobl - */ -@SolrDocument(collection = "collection1") -public class City { - - @Id - private String id; - - @Indexed - private String name; - - public String getId() { - return this.id; - } - - public void setId(String id) { - this.id = id; - } - - public String getName() { - return this.name; - } - - public void setName(String name) { - this.name = name; - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/solr/city/CityRepository.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/solr/city/CityRepository.java deleted file mode 100644 index d2c3928183d9..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/solr/city/CityRepository.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.data.solr.city; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.repository.Repository; - -public interface CityRepository extends Repository { - - Page findByNameStartingWith(String name, Pageable page); - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/diagnostics/analyzer/NoSuchBeanDefinitionFailureAnalyzerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/diagnostics/analyzer/NoSuchBeanDefinitionFailureAnalyzerTests.java index bde0ae433aff..063987883509 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/diagnostics/analyzer/NoSuchBeanDefinitionFailureAnalyzerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/diagnostics/analyzer/NoSuchBeanDefinitionFailureAnalyzerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.diagnostics.FailureAnalysis; import org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter; +import org.springframework.boot.system.JavaVersion; import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; @@ -153,8 +154,14 @@ void failureAnalysisForNullBeanByType() { @Test void failureAnalysisForUnmatchedQualifier() { FailureAnalysis analysis = analyzeFailure(createFailure(QualifiedBeanConfiguration.class)); - assertThat(analysis.getDescription()) - .containsPattern("@org.springframework.beans.factory.annotation.Qualifier\\(value=\"*alpha\"*\\)"); + assertThat(analysis.getDescription()).containsPattern(determineAnnotationValuePattern()); + } + + private String determineAnnotationValuePattern() { + if (JavaVersion.getJavaVersion().isEqualOrNewerThan(JavaVersion.FOURTEEN)) { + return "@org.springframework.beans.factory.annotation.Qualifier\\(\"*alpha\"*\\)"; + } + return "@org.springframework.beans.factory.annotation.Qualifier\\(value=\"*alpha\"*\\)"; } @Test diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/domain/EntityScannerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/domain/EntityScannerTests.java index 1110a73a845a..2ea5904677cb 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/domain/EntityScannerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/domain/EntityScannerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,14 @@ package org.springframework.boot.autoconfigure.domain; +import java.util.Collections; import java.util.Set; import javax.persistence.Embeddable; import javax.persistence.Entity; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.springframework.boot.autoconfigure.domain.scan.a.EmbeddableA; import org.springframework.boot.autoconfigure.domain.scan.a.EntityA; @@ -29,11 +31,18 @@ import org.springframework.boot.autoconfigure.domain.scan.b.EntityB; import org.springframework.boot.autoconfigure.domain.scan.c.EmbeddableC; import org.springframework.boot.autoconfigure.domain.scan.c.EntityC; +import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; import org.springframework.context.annotation.Configuration; +import org.springframework.core.type.filter.AnnotationTypeFilter; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; /** * Tests for {@link EntityScanner}. @@ -57,6 +66,19 @@ void scanShouldScanFromSinglePackage() throws Exception { context.close(); } + @Test + void scanShouldScanFromResolvedPlaceholderPackage() throws Exception { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + TestPropertyValues.of("com.example.entity-package=org.springframework.boot.autoconfigure.domain.scan") + .applyTo(context); + context.register(ScanPlaceholderConfig.class); + context.refresh(); + EntityScanner scanner = new EntityScanner(context); + Set> scanned = scanner.scan(Entity.class); + assertThat(scanned).containsOnly(EntityA.class, EntityB.class, EntityC.class); + context.close(); + } + @Test void scanShouldScanFromMultiplePackages() throws Exception { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ScanAConfig.class, @@ -79,6 +101,41 @@ void scanShouldFilterOnAnnotation() throws Exception { context.close(); } + @Test + void scanShouldUseCustomCandidateComponentProvider() throws ClassNotFoundException { + ClassPathScanningCandidateComponentProvider candidateComponentProvider = mock( + ClassPathScanningCandidateComponentProvider.class); + given(candidateComponentProvider.findCandidateComponents("org.springframework.boot.autoconfigure.domain.scan")) + .willReturn(Collections.emptySet()); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ScanConfig.class); + TestEntityScanner scanner = new TestEntityScanner(context, candidateComponentProvider); + scanner.scan(Entity.class); + ArgumentCaptor annotationTypeFilter = ArgumentCaptor.forClass(AnnotationTypeFilter.class); + then(candidateComponentProvider).should().addIncludeFilter(annotationTypeFilter.capture()); + then(candidateComponentProvider).should() + .findCandidateComponents("org.springframework.boot.autoconfigure.domain.scan"); + then(candidateComponentProvider).shouldHaveNoMoreInteractions(); + assertThat(annotationTypeFilter.getValue().getAnnotationType()).isEqualTo(Entity.class); + } + + private static class TestEntityScanner extends EntityScanner { + + private final ClassPathScanningCandidateComponentProvider candidateComponentProvider; + + TestEntityScanner(ApplicationContext context, + ClassPathScanningCandidateComponentProvider candidateComponentProvider) { + super(context); + this.candidateComponentProvider = candidateComponentProvider; + } + + @Override + protected ClassPathScanningCandidateComponentProvider createClassPathScanningCandidateComponentProvider( + ApplicationContext context) { + return this.candidateComponentProvider; + } + + } + @Configuration(proxyBeanMethods = false) @EntityScan("org.springframework.boot.autoconfigure.domain.scan") static class ScanConfig { @@ -97,4 +154,10 @@ static class ScanBConfig { } + @Configuration(proxyBeanMethods = false) + @EntityScan("${com.example.entity-package}") + static class ScanPlaceholderConfig { + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationIntegrationTests.java new file mode 100644 index 000000000000..6165778f71aa --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationIntegrationTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.elasticsearch; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + +import org.elasticsearch.action.get.GetRequest; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.RestHighLevelClient; +import org.junit.jupiter.api.Test; +import org.testcontainers.elasticsearch.ElasticsearchContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link ElasticsearchRestClientAutoConfiguration}. + * + * @author Brian Clozel + * @author Vedran Pavic + * @author Evgeniy Cheban + */ +@Testcontainers(disabledWithoutDocker = true) +class ElasticsearchRestClientAutoConfigurationIntegrationTests { + + @Container + static final ElasticsearchContainer elasticsearch = new ElasticsearchContainer(DockerImageNames.elasticsearch()) + .withStartupAttempts(5).withStartupTimeout(Duration.ofMinutes(10)); + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ElasticsearchRestClientAutoConfiguration.class)); + + @Test + void restClientCanQueryElasticsearchNode() { + this.contextRunner + .withPropertyValues("spring.elasticsearch.rest.uris=http://" + elasticsearch.getHttpHostAddress()) + .run((context) -> { + RestHighLevelClient client = context.getBean(RestHighLevelClient.class); + Map source = new HashMap<>(); + source.put("a", "alpha"); + source.put("b", "bravo"); + IndexRequest index = new IndexRequest("test").id("1").source(source); + client.index(index, RequestOptions.DEFAULT); + GetRequest getRequest = new GetRequest("test").id("1"); + assertThat(client.get(getRequest, RequestOptions.DEFAULT).isExists()).isTrue(); + }); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationTests.java new file mode 100644 index 000000000000..eaf8a5edda7c --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationTests.java @@ -0,0 +1,282 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.elasticsearch; + +import java.time.Duration; +import java.util.Map; + +import org.apache.http.HttpHost; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.Credentials; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.impl.nio.client.HttpAsyncClientBuilder; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.elasticsearch.client.Node; +import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestClientBuilder; +import org.elasticsearch.client.RestHighLevelClient; +import org.elasticsearch.client.sniff.Sniffer; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ElasticsearchRestClientAutoConfiguration}. + * + * @author Brian Clozel + * @author Vedran Pavic + * @author Evgeniy Cheban + */ +class ElasticsearchRestClientAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ElasticsearchRestClientAutoConfiguration.class)); + + @Test + void configureShouldOnlyCreateHighLevelRestClient() { + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(RestClient.class) + .hasSingleBean(RestHighLevelClient.class)); + } + + @Test + void configureWhenCustomRestClientShouldBackOff() { + this.contextRunner.withBean("customRestClient", RestClient.class, () -> mock(RestClient.class)) + .run((context) -> assertThat(context).doesNotHaveBean(RestHighLevelClient.class) + .hasSingleBean(RestClient.class).hasBean("customRestClient")); + } + + @Test + void configureWhenCustomRestHighLevelClientShouldBackOff() { + this.contextRunner.withUserConfiguration(CustomRestHighLevelClientConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(RestHighLevelClient.class)); + } + + @Test + void configureWhenDefaultRestClientShouldCreateWhenNoUniqueRestHighLevelClient() { + this.contextRunner.withUserConfiguration(TwoCustomRestHighLevelClientConfiguration.class).run((context) -> { + Map restHighLevelClients = context.getBeansOfType(RestHighLevelClient.class); + assertThat(restHighLevelClients).hasSize(2); + }); + } + + @Test + void configureWhenBuilderCustomizerShouldApply() { + this.contextRunner.withUserConfiguration(BuilderCustomizerConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(RestHighLevelClient.class); + RestHighLevelClient restClient = context.getBean(RestHighLevelClient.class); + RestClient lowLevelClient = restClient.getLowLevelClient(); + assertThat(lowLevelClient).hasFieldOrPropertyWithValue("pathPrefix", "/test"); + assertThat(lowLevelClient).extracting("client.connmgr.pool.maxTotal").isEqualTo(100); + assertThat(lowLevelClient).extracting("client.defaultConfig.cookieSpec").isEqualTo("rfc6265-lax"); + }); + } + + @Test + void configureWithNoTimeoutsApplyDefaults() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(RestHighLevelClient.class); + RestHighLevelClient restClient = context.getBean(RestHighLevelClient.class); + assertTimeouts(restClient, Duration.ofMillis(RestClientBuilder.DEFAULT_CONNECT_TIMEOUT_MILLIS), + Duration.ofMillis(RestClientBuilder.DEFAULT_SOCKET_TIMEOUT_MILLIS)); + }); + } + + @Test + void configureWithCustomTimeouts() { + this.contextRunner.withPropertyValues("spring.elasticsearch.rest.connection-timeout=15s", + "spring.elasticsearch.rest.read-timeout=1m").run((context) -> { + assertThat(context).hasSingleBean(RestHighLevelClient.class); + RestHighLevelClient restClient = context.getBean(RestHighLevelClient.class); + assertTimeouts(restClient, Duration.ofSeconds(15), Duration.ofMinutes(1)); + }); + } + + private static void assertTimeouts(RestHighLevelClient restClient, Duration connectTimeout, Duration readTimeout) { + assertThat(restClient.getLowLevelClient()).extracting("client.defaultConfig.socketTimeout") + .isEqualTo(Math.toIntExact(readTimeout.toMillis())); + assertThat(restClient.getLowLevelClient()).extracting("client.defaultConfig.connectTimeout") + .isEqualTo(Math.toIntExact(connectTimeout.toMillis())); + } + + @Test + void configureUriWithUsernameOnly() { + this.contextRunner.withPropertyValues("spring.elasticsearch.rest.uris=http://user@localhost:9200") + .run((context) -> { + RestClient client = context.getBean(RestHighLevelClient.class).getLowLevelClient(); + assertThat(client.getNodes().stream().map(Node::getHost).map(HttpHost::toString)) + .containsExactly("http://localhost:9200"); + assertThat(client).extracting("client") + .extracting("credentialsProvider", + InstanceOfAssertFactories.type(CredentialsProvider.class)) + .satisfies((credentialsProvider) -> { + Credentials credentials = credentialsProvider + .getCredentials(new AuthScope("localhost", 9200)); + assertThat(credentials.getUserPrincipal().getName()).isEqualTo("user"); + assertThat(credentials.getPassword()).isNull(); + }); + }); + } + + @Test + void configureUriWithUsernameAndEmptyPassword() { + this.contextRunner.withPropertyValues("spring.elasticsearch.rest.uris=http://user:@localhost:9200") + .run((context) -> { + RestClient client = context.getBean(RestHighLevelClient.class).getLowLevelClient(); + assertThat(client.getNodes().stream().map(Node::getHost).map(HttpHost::toString)) + .containsExactly("http://localhost:9200"); + assertThat(client).extracting("client") + .extracting("credentialsProvider", + InstanceOfAssertFactories.type(CredentialsProvider.class)) + .satisfies((credentialsProvider) -> { + Credentials credentials = credentialsProvider + .getCredentials(new AuthScope("localhost", 9200)); + assertThat(credentials.getUserPrincipal().getName()).isEqualTo("user"); + assertThat(credentials.getPassword()).isEmpty(); + }); + }); + } + + @Test + void configureUriWithUsernameAndPasswordWhenUsernameAndPasswordPropertiesSet() { + this.contextRunner + .withPropertyValues("spring.elasticsearch.rest.uris=http://user:password@localhost:9200,localhost:9201", + "spring.elasticsearch.rest.username=admin", "spring.elasticsearch.rest.password=admin") + .run((context) -> { + RestClient client = context.getBean(RestHighLevelClient.class).getLowLevelClient(); + assertThat(client.getNodes().stream().map(Node::getHost).map(HttpHost::toString)) + .containsExactly("http://localhost:9200", "http://localhost:9201"); + assertThat(client).extracting("client") + .extracting("credentialsProvider", + InstanceOfAssertFactories.type(CredentialsProvider.class)) + .satisfies((credentialsProvider) -> { + Credentials uriCredentials = credentialsProvider + .getCredentials(new AuthScope("localhost", 9200)); + assertThat(uriCredentials.getUserPrincipal().getName()).isEqualTo("user"); + assertThat(uriCredentials.getPassword()).isEqualTo("password"); + Credentials defaultCredentials = credentialsProvider + .getCredentials(new AuthScope("localhost", 9201)); + assertThat(defaultCredentials.getUserPrincipal().getName()).isEqualTo("admin"); + assertThat(defaultCredentials.getPassword()).isEqualTo("admin"); + }); + }); + } + + @Test + void configureWithoutSnifferLibraryShouldNotCreateSniffer() { + this.contextRunner.withClassLoader(new FilteredClassLoader("org.elasticsearch.client.sniff")) + .run((context) -> assertThat(context).hasSingleBean(RestHighLevelClient.class) + .doesNotHaveBean(Sniffer.class)); + } + + @Test + void configureShouldCreateSnifferUsingRestHighLevelClient() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(Sniffer.class); + assertThat(context.getBean(Sniffer.class)).hasFieldOrPropertyWithValue("restClient", + context.getBean(RestHighLevelClient.class).getLowLevelClient()); + // Validate shutdown order as the sniffer must be shutdown before the client + assertThat(context.getBeanFactory().getDependentBeans("elasticsearchRestHighLevelClient")) + .contains("elasticsearchSniffer"); + }); + } + + @Test + void configureWithCustomSnifferSettings() { + this.contextRunner.withPropertyValues("spring.elasticsearch.rest.sniffer.interval=180s", + "spring.elasticsearch.rest.sniffer.delay-after-failure=30s").run((context) -> { + assertThat(context).hasSingleBean(Sniffer.class); + Sniffer sniffer = context.getBean(Sniffer.class); + assertThat(sniffer).hasFieldOrPropertyWithValue("sniffIntervalMillis", + Duration.ofMinutes(3).toMillis()); + assertThat(sniffer).hasFieldOrPropertyWithValue("sniffAfterFailureDelayMillis", + Duration.ofSeconds(30).toMillis()); + }); + } + + @Test + void configureWhenCustomSnifferShouldBackOff() { + Sniffer customSniffer = mock(Sniffer.class); + this.contextRunner.withBean(Sniffer.class, () -> customSniffer).run((context) -> { + assertThat(context).hasSingleBean(Sniffer.class); + Sniffer sniffer = context.getBean(Sniffer.class); + assertThat(sniffer).isSameAs(customSniffer); + then(customSniffer).shouldHaveNoInteractions(); + }); + } + + @Configuration(proxyBeanMethods = false) + static class BuilderCustomizerConfiguration { + + @Bean + RestClientBuilderCustomizer myCustomizer() { + return new RestClientBuilderCustomizer() { + + @Override + public void customize(RestClientBuilder builder) { + builder.setPathPrefix("/test"); + } + + @Override + public void customize(HttpAsyncClientBuilder builder) { + builder.setMaxConnTotal(100); + } + + @Override + public void customize(RequestConfig.Builder builder) { + builder.setCookieSpec("rfc6265-lax"); + } + + }; + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomRestHighLevelClientConfiguration { + + @Bean + RestHighLevelClient customRestHighLevelClient(RestClientBuilder builder) { + return new RestHighLevelClient(builder); + } + + } + + @Configuration(proxyBeanMethods = false) + static class TwoCustomRestHighLevelClientConfiguration { + + @Bean + RestHighLevelClient customRestHighLevelClient(RestClientBuilder builder) { + return new RestHighLevelClient(builder); + } + + @Bean + RestHighLevelClient customRestHighLevelClient1(RestClientBuilder builder) { + return new RestHighLevelClient(builder); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/jest/JestAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/jest/JestAutoConfigurationTests.java deleted file mode 100644 index f656b651e9cd..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/jest/JestAutoConfigurationTests.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.elasticsearch.jest; - -import java.io.IOException; -import java.time.Duration; -import java.util.HashMap; -import java.util.Map; - -import com.google.gson.Gson; -import io.searchbox.action.Action; -import io.searchbox.client.JestClient; -import io.searchbox.client.JestResult; -import io.searchbox.client.http.JestHttpClient; -import io.searchbox.core.Get; -import io.searchbox.core.Index; -import org.junit.jupiter.api.Test; -import org.testcontainers.elasticsearch.ElasticsearchContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; - -import org.springframework.beans.factory.BeanCreationException; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link JestAutoConfiguration}. - * - * @author Stephane Nicoll - * @author Andy Wilkinson - */ -@Deprecated -@Testcontainers(disabledWithoutDocker = true) -class JestAutoConfigurationTests { - - @Container - static final ElasticsearchContainer elasticsearch = new ElasticsearchContainer().withStartupAttempts(5) - .withStartupTimeout(Duration.ofMinutes(2)); - - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(GsonAutoConfiguration.class, JestAutoConfiguration.class)); - - @Test - void jestClientOnLocalhostByDefault() { - this.contextRunner.run((context) -> assertThat(context).hasSingleBean(JestClient.class)); - } - - @Test - void customJestClient() { - this.contextRunner.withUserConfiguration(CustomJestClient.class) - .withPropertyValues("spring.elasticsearch.jest.uris[0]=http://localhost:9200") - .run((context) -> assertThat(context).hasSingleBean(JestClient.class)); - } - - @Test - void customGson() { - this.contextRunner.withUserConfiguration(CustomGson.class) - .withPropertyValues("spring.elasticsearch.jest.uris=http://localhost:9200").run((context) -> { - JestHttpClient client = (JestHttpClient) context.getBean(JestClient.class); - assertThat(client.getGson()).isSameAs(context.getBean("customGson")); - }); - } - - @Test - void customizerOverridesAutoConfig() { - this.contextRunner.withUserConfiguration(BuilderCustomizer.class) - .withPropertyValues("spring.elasticsearch.jest.uris=http://localhost:9200").run((context) -> { - JestHttpClient client = (JestHttpClient) context.getBean(JestClient.class); - assertThat(client.getGson()).isSameAs(context.getBean(BuilderCustomizer.class).getGson()); - }); - } - - @Test - void proxyHostWithoutPort() { - this.contextRunner - .withPropertyValues("spring.elasticsearch.jest.uris=http://localhost:9200", - "spring.elasticsearch.jest.proxy.host=proxy.example.com") - .run((context) -> assertThat(context.getStartupFailure()).isInstanceOf(BeanCreationException.class) - .hasMessageContaining("Proxy port must not be null")); - } - - @Test - void jestCanCommunicateWithElasticsearchInstance() { - this.contextRunner - .withPropertyValues("spring.elasticsearch.jest.uris=http://" + elasticsearch.getHttpHostAddress()) - .run((context) -> { - JestClient client = context.getBean(JestClient.class); - Map source = new HashMap<>(); - source.put("a", "alpha"); - source.put("b", "bravo"); - Index index = new Index.Builder(source).index("foo").type("bar").id("1").build(); - execute(client, index); - Get getRequest = new Get.Builder("foo", "1").build(); - assertThat(execute(client, getRequest).getResponseCode()).isEqualTo(200); - }); - } - - private JestResult execute(JestClient client, Action action) { - for (int i = 0; i < 2; i++) { - try { - return client.execute(action); - } - catch (IOException ex) { - // Continue - } - } - try { - return client.execute(action); - } - catch (IOException ex) { - throw new RuntimeException(ex); - } - } - - @Configuration(proxyBeanMethods = false) - static class CustomJestClient { - - @Bean - JestClient customJestClient() { - return mock(JestClient.class); - } - - } - - @Configuration(proxyBeanMethods = false) - static class CustomGson { - - @Bean - Gson customGson() { - return new Gson(); - } - - } - - @Configuration(proxyBeanMethods = false) - @Import(CustomGson.class) - static class BuilderCustomizer { - - private final Gson gson = new Gson(); - - @Bean - HttpClientConfigBuilderCustomizer customizer() { - return (builder) -> builder.gson(BuilderCustomizer.this.gson); - } - - Gson getGson() { - return this.gson; - } - - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/rest/RestClientAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/rest/RestClientAutoConfigurationTests.java deleted file mode 100644 index a76c0584f0d3..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/rest/RestClientAutoConfigurationTests.java +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.elasticsearch.rest; - -import java.time.Duration; -import java.util.HashMap; -import java.util.Map; - -import org.elasticsearch.action.get.GetRequest; -import org.elasticsearch.action.index.IndexRequest; -import org.elasticsearch.client.RequestOptions; -import org.elasticsearch.client.RestClient; -import org.elasticsearch.client.RestClientBuilder; -import org.elasticsearch.client.RestHighLevelClient; -import org.junit.jupiter.api.Test; -import org.testcontainers.elasticsearch.ElasticsearchContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; - -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.FilteredClassLoader; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.test.util.ReflectionTestUtils; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link RestClientAutoConfiguration} - * - * @author Brian Clozel - */ -@Testcontainers(disabledWithoutDocker = true) -class RestClientAutoConfigurationTests { - - @Container - static final ElasticsearchContainer elasticsearch = new ElasticsearchContainer().withStartupAttempts(5) - .withStartupTimeout(Duration.ofMinutes(2)); - - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class)); - - @Test - void configureShouldCreateBothRestClientVariants() { - this.contextRunner.run((context) -> { - assertThat(context).hasSingleBean(RestClient.class).hasSingleBean(RestHighLevelClient.class); - assertThat(context.getBean(RestClient.class)) - .isSameAs(context.getBean(RestHighLevelClient.class).getLowLevelClient()); - }); - } - - @Test - void configureWhenCustomClientShouldBackOff() { - this.contextRunner.withUserConfiguration(CustomRestClientConfiguration.class) - .run((context) -> assertThat(context).getBeanNames(RestClient.class).containsOnly("customRestClient")); - } - - @Test - void configureWhenCustomRestHighLevelClientShouldBackOff() { - this.contextRunner.withUserConfiguration(CustomRestHighLevelClientConfiguration.class).run((context) -> { - assertThat(context).hasSingleBean(RestClient.class).hasSingleBean(RestHighLevelClient.class); - assertThat(context.getBean(RestClient.class)) - .isSameAs(context.getBean(RestHighLevelClient.class).getLowLevelClient()); - }); - } - - @Test - void configureWhenDefaultRestClientShouldCreateWhenNoUniqueRestHighLevelClient() { - this.contextRunner.withUserConfiguration(TwoCustomRestHighLevelClientConfiguration.class).run((context) -> { - assertThat(context).hasSingleBean(RestClient.class); - RestClient restClient = context.getBean(RestClient.class); - Map restHighLevelClients = context.getBeansOfType(RestHighLevelClient.class); - assertThat(restHighLevelClients).hasSize(2); - for (RestHighLevelClient restHighLevelClient : restHighLevelClients.values()) { - assertThat(restHighLevelClient.getLowLevelClient()).isNotSameAs(restClient); - } - }); - } - - @Test - void configureWhenHighLevelClientIsNotAvailableShouldCreateRestClientOnly() { - this.contextRunner.withClassLoader(new FilteredClassLoader(RestHighLevelClient.class)) - .run((context) -> assertThat(context).hasSingleBean(RestClient.class) - .doesNotHaveBean(RestHighLevelClient.class)); - } - - @Test - void configureWhenBuilderCustomizerShouldApply() { - this.contextRunner.withUserConfiguration(BuilderCustomizerConfiguration.class).run((context) -> { - assertThat(context).hasSingleBean(RestClient.class); - RestClient restClient = context.getBean(RestClient.class); - assertThat(restClient).hasFieldOrPropertyWithValue("pathPrefix", "/test"); - }); - } - - @Test - void configureWithNoTimeoutsApplyDefaults() { - this.contextRunner.run((context) -> { - assertThat(context).hasSingleBean(RestClient.class); - RestClient restClient = context.getBean(RestClient.class); - assertTimeouts(restClient, Duration.ofMillis(RestClientBuilder.DEFAULT_CONNECT_TIMEOUT_MILLIS), - Duration.ofMillis(RestClientBuilder.DEFAULT_SOCKET_TIMEOUT_MILLIS)); - }); - } - - @Test - void configureWithCustomTimeouts() { - this.contextRunner.withPropertyValues("spring.elasticsearch.rest.connection-timeout=15s", - "spring.elasticsearch.rest.read-timeout=1m").run((context) -> { - assertThat(context).hasSingleBean(RestClient.class); - RestClient restClient = context.getBean(RestClient.class); - assertTimeouts(restClient, Duration.ofSeconds(15), Duration.ofMinutes(1)); - }); - } - - private static void assertTimeouts(RestClient restClient, Duration connectTimeout, Duration readTimeout) { - Object client = ReflectionTestUtils.getField(restClient, "client"); - Object config = ReflectionTestUtils.getField(client, "defaultConfig"); - assertThat(config).hasFieldOrPropertyWithValue("socketTimeout", Math.toIntExact(readTimeout.toMillis())); - assertThat(config).hasFieldOrPropertyWithValue("connectTimeout", Math.toIntExact(connectTimeout.toMillis())); - } - - @Test - void restClientCanQueryElasticsearchNode() { - this.contextRunner - .withPropertyValues("spring.elasticsearch.rest.uris=http://" + elasticsearch.getHttpHostAddress()) - .run((context) -> { - RestHighLevelClient client = context.getBean(RestHighLevelClient.class); - Map source = new HashMap<>(); - source.put("a", "alpha"); - source.put("b", "bravo"); - IndexRequest index = new IndexRequest("foo", "bar", "1").source(source); - client.index(index, RequestOptions.DEFAULT); - GetRequest getRequest = new GetRequest("foo", "bar", "1"); - assertThat(client.get(getRequest, RequestOptions.DEFAULT).isExists()).isTrue(); - }); - } - - @Configuration(proxyBeanMethods = false) - static class CustomRestClientConfiguration { - - @Bean - RestClient customRestClient() { - return mock(RestClient.class); - } - - } - - @Configuration(proxyBeanMethods = false) - static class BuilderCustomizerConfiguration { - - @Bean - RestClientBuilderCustomizer myCustomizer() { - return (builder) -> builder.setPathPrefix("/test"); - } - - } - - @Configuration(proxyBeanMethods = false) - static class CustomRestHighLevelClientConfiguration { - - @Bean - RestHighLevelClient customRestHighLevelClient(RestClientBuilder builder) { - return new RestHighLevelClient(builder); - } - - } - - @Configuration(proxyBeanMethods = false) - static class TwoCustomRestHighLevelClientConfiguration { - - @Bean - RestHighLevelClient customRestHighLevelClient(RestClientBuilder builder) { - return new RestHighLevelClient(builder); - } - - @Bean - RestHighLevelClient customRestHighLevelClient1(RestClientBuilder builder) { - return new RestHighLevelClient(builder); - } - - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/Flyway5xAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/Flyway5xAutoConfigurationTests.java index 4ebf652fbde2..a32ef4d16ed8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/Flyway5xAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/Flyway5xAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,7 +39,7 @@ @ClassPathOverrides("org.flywaydb:flyway-core:5.2.4") class Flyway5xAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(FlywayAutoConfiguration.class)) .withPropertyValues("spring.datasource.generate-unique-name=true"); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/Flyway6xAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/Flyway6xAutoConfigurationTests.java new file mode 100644 index 000000000000..fd169165fd8a --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/Flyway6xAutoConfigurationTests.java @@ -0,0 +1,102 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.flyway; + +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.Location; +import org.flywaydb.core.api.callback.Callback; +import org.flywaydb.core.api.callback.Context; +import org.flywaydb.core.api.callback.Event; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.testsupport.classpath.ClassPathOverrides; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link FlywayAutoConfiguration} with Flyway 6.x. + * + * @author Andy Wilkinson + */ +@ClassPathOverrides("org.flywaydb:flyway-core:6.5.6") +class Flyway6xAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(FlywayAutoConfiguration.class)) + .withPropertyValues("spring.datasource.generate-unique-name=true"); + + @Test + void defaultFlyway() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(Flyway.class); + Flyway flyway = context.getBean(Flyway.class); + assertThat(flyway.getConfiguration().getLocations()) + .containsExactly(new Location("classpath:db/migration")); + }); + } + + @Test + void callbacksAreConfiguredAndOrdered() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class, CallbackConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(Flyway.class); + Flyway flyway = context.getBean(Flyway.class); + Callback callbackOne = context.getBean("callbackOne", Callback.class); + Callback callbackTwo = context.getBean("callbackTwo", Callback.class); + assertThat(flyway.getConfiguration().getCallbacks()).hasSize(2); + assertThat(flyway.getConfiguration().getCallbacks()).containsExactly(callbackTwo, callbackOne); + InOrder orderedCallbacks = inOrder(callbackOne, callbackTwo); + orderedCallbacks.verify(callbackTwo).handle(any(Event.class), any(Context.class)); + orderedCallbacks.verify(callbackOne).handle(any(Event.class), any(Context.class)); + }); + } + + @Configuration(proxyBeanMethods = false) + static class CallbackConfiguration { + + @Bean + @Order(1) + Callback callbackOne() { + return mockCallback(); + } + + @Bean + @Order(0) + Callback callbackTwo() { + return mockCallback(); + } + + private Callback mockCallback() { + Callback callback = mock(Callback.class); + given(callback.supports(any(Event.class), any(Context.class))).willReturn(true); + return callback; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java index a814ecd822f2..64d6c120c41d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.boot.autoconfigure.flyway; +import java.sql.Connection; +import java.sql.SQLException; import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -30,18 +32,30 @@ import org.flywaydb.core.api.callback.Context; import org.flywaydb.core.api.callback.Event; import org.flywaydb.core.api.migration.JavaMigration; -import org.flywaydb.core.internal.license.FlywayProUpgradeRequiredException; +import org.flywaydb.core.internal.license.FlywayTeamsUpgradeRequiredException; import org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform; +import org.jooq.DSLContext; +import org.jooq.SQLDialect; +import org.jooq.impl.DefaultDSLContext; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InOrder; import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.boot.jdbc.SchemaManagement; import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.ContextConsumer; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; @@ -53,6 +67,10 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.datasource.SimpleDriverDataSource; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; import org.springframework.stereotype.Component; @@ -74,10 +92,13 @@ * @author Stephane Nicoll * @author Dominic Gunn * @author András Deák + * @author Takaaki Shimbo + * @author Chris Bono */ +@ExtendWith(OutputCaptureExtension.class) class FlywayAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(FlywayAutoConfiguration.class)) .withPropertyValues("spring.datasource.generate-unique-name=true"); @@ -95,6 +116,13 @@ void createsDataSourceWithNoDataSourceBeanAndFlywayUrl() { }); } + @Test + void backsOffWithFlywayUrlAndNoSpringJdbc() { + this.contextRunner.withPropertyValues("spring.flyway.url:jdbc:hsqldb:mem:" + UUID.randomUUID()) + .withClassLoader(new FilteredClassLoader("org.springframework.jdbc")) + .run((context) -> assertThat(context).doesNotHaveBean(Flyway.class)); + } + @Test void createDataSourceWithUrl() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) @@ -116,25 +144,42 @@ void createDataSourceWithUser() { } @Test - void createDataSourceFallbackToEmbeddedProperties() { + void createDataSourceDoesNotFallbackToEmbeddedProperties() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) .withPropertyValues("spring.flyway.url:jdbc:hsqldb:mem:flywaytest").run((context) -> { assertThat(context).hasSingleBean(Flyway.class); DataSource dataSource = context.getBean(Flyway.class).getConfiguration().getDataSource(); assertThat(dataSource).isNotNull(); - assertThat(dataSource).hasFieldOrPropertyWithValue("user", "sa"); + assertThat(dataSource).hasFieldOrPropertyWithValue("username", null); assertThat(dataSource).hasFieldOrPropertyWithValue("password", ""); }); } @Test void createDataSourceWithUserAndFallbackToEmbeddedProperties() { - this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) - .withPropertyValues("spring.flyway.user:sa").run((context) -> { + this.contextRunner.withUserConfiguration(PropertiesBackedH2DataSourceConfiguration.class) + .withPropertyValues("spring.flyway.user:test", "spring.flyway.password:secret").run((context) -> { assertThat(context).hasSingleBean(Flyway.class); DataSource dataSource = context.getBean(Flyway.class).getConfiguration().getDataSource(); assertThat(dataSource).isNotNull(); assertThat(dataSource).extracting("url").asString().startsWith("jdbc:h2:mem:"); + assertThat(dataSource).extracting("username").asString().isEqualTo("test"); + }); + } + + @Test + void createDataSourceWithUserAndCustomEmbeddedProperties() { + this.contextRunner.withUserConfiguration(CustomBackedH2DataSourceConfiguration.class) + .withPropertyValues("spring.flyway.user:test", "spring.flyway.password:secret").run((context) -> { + assertThat(context).hasSingleBean(Flyway.class); + String expectedName = context.getBean(CustomBackedH2DataSourceConfiguration.class).name; + String propertiesName = context.getBean(DataSourceProperties.class).determineDatabaseName(); + assertThat(expectedName).isNotEqualTo(propertiesName); + DataSource dataSource = context.getBean(Flyway.class).getConfiguration().getDataSource(); + assertThat(dataSource).isNotNull(); + assertThat(dataSource).extracting("url").asString().startsWith("jdbc:h2:mem:") + .contains(expectedName); + assertThat(dataSource).extracting("username").asString().isEqualTo("test"); }); } @@ -158,6 +203,15 @@ void flywayDataSourceWithoutDataSourceAutoConfiguration() { }); } + @Test + void flywayMultipleDataSources() { + this.contextRunner.withUserConfiguration(FlywayMultipleDataSourcesConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(Flyway.class); + assertThat(context.getBean(Flyway.class).getConfiguration().getDataSource()) + .isEqualTo(context.getBean("flywayDataSource")); + }); + } + @Test void schemaManagementProviderDetectsDataSource() { this.contextRunner @@ -218,6 +272,20 @@ void overrideSchemas() { }); } + @Test + void overrideDataSourceAndDriverClassName() { + String jdbcUrl = "jdbc:hsqldb:mem:flyway" + UUID.randomUUID(); + String driverClassName = "org.hsqldb.jdbcDriver"; + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class).withPropertyValues( + "spring.flyway.url:" + jdbcUrl, "spring.flyway.driver-class-name:" + driverClassName).run((context) -> { + Flyway flyway = context.getBean(Flyway.class); + SimpleDriverDataSource dataSource = (SimpleDriverDataSource) flyway.getConfiguration() + .getDataSource(); + assertThat(dataSource.getUrl()).isEqualTo(jdbcUrl); + assertThat(dataSource.getDriver().getClass().getName()).isEqualTo(driverClassName); + }); + } + @Test void changeLogDoesNotExist() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) @@ -366,7 +434,7 @@ void useOneLocationWithVendorDirectory() { } @Test - void callbacksAreConfiguredAndOrdered() { + void callbacksAreConfiguredAndOrderedByName() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class, CallbackConfiguration.class) .run((context) -> { assertThat(context).hasSingleBean(Flyway.class); @@ -374,7 +442,6 @@ void callbacksAreConfiguredAndOrdered() { Callback callbackOne = context.getBean("callbackOne", Callback.class); Callback callbackTwo = context.getBean("callbackTwo", Callback.class); assertThat(flyway.getConfiguration().getCallbacks()).hasSize(2); - assertThat(flyway.getConfiguration().getCallbacks()).containsExactly(callbackTwo, callbackOne); InOrder orderedCallbacks = inOrder(callbackOne, callbackTwo); orderedCallbacks.verify(callbackTwo).handle(any(Event.class), any(Context.class)); orderedCallbacks.verify(callbackOne).handle(any(Event.class), any(Context.class)); @@ -396,89 +463,55 @@ void configurationCustomizersAreConfiguredAndOrdered() { @Test void batchIsCorrectlyMapped() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) - .withPropertyValues("spring.flyway.batch=true").run((context) -> { - assertThat(context).hasFailed(); - Throwable failure = context.getStartupFailure(); - assertThat(failure).hasRootCauseInstanceOf(FlywayProUpgradeRequiredException.class); - assertThat(failure).hasMessageContaining(" batch "); - }); + .withPropertyValues("spring.flyway.batch=true").run(validateFlywayTeamsPropertyOnly("batch")); } @Test void dryRunOutputIsCorrectlyMapped() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) - .withPropertyValues("spring.flyway.dryRunOutput=dryrun.sql").run((context) -> { - assertThat(context).hasFailed(); - Throwable failure = context.getStartupFailure(); - assertThat(failure).hasRootCauseInstanceOf(FlywayProUpgradeRequiredException.class); - assertThat(failure).hasMessageContaining(" dryRunOutput "); - }); + .withPropertyValues("spring.flyway.dryRunOutput=dryrun.sql") + .run(validateFlywayTeamsPropertyOnly("dryRunOutput")); } @Test void errorOverridesIsCorrectlyMapped() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) - .withPropertyValues("spring.flyway.errorOverrides=D12345").run((context) -> { - assertThat(context).hasFailed(); - Throwable failure = context.getStartupFailure(); - assertThat(failure).hasRootCauseInstanceOf(FlywayProUpgradeRequiredException.class); - assertThat(failure).hasMessageContaining(" errorOverrides "); - }); + .withPropertyValues("spring.flyway.errorOverrides=D12345") + .run(validateFlywayTeamsPropertyOnly("errorOverrides")); } @Test - void licenseKeyIsCorrectlyMapped() { + void licenseKeyIsCorrectlyMapped(CapturedOutput output) { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) - .withPropertyValues("spring.flyway.license-key=<>").run((context) -> { - assertThat(context).hasFailed(); - Throwable failure = context.getStartupFailure(); - assertThat(failure).hasRootCauseInstanceOf(FlywayProUpgradeRequiredException.class); - assertThat(failure).hasMessageContaining(" licenseKey "); - }); + .withPropertyValues("spring.flyway.license-key=<>").run((context) -> assertThat(output) + .contains("<> is not supported by Flyway Community Edition")); } @Test void oracleSqlplusIsCorrectlyMapped() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) - .withPropertyValues("spring.flyway.oracle-sqlplus=true").run((context) -> { - assertThat(context).hasFailed(); - Throwable failure = context.getStartupFailure(); - assertThat(failure).hasRootCauseInstanceOf(FlywayProUpgradeRequiredException.class); - assertThat(failure).hasMessageContaining(" oracle.sqlplus "); - }); + .withPropertyValues("spring.flyway.oracle-sqlplus=true") + .run(validateFlywayTeamsPropertyOnly("oracle.sqlplus")); } @Test void oracleSqlplusWarnIsCorrectlyMapped() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) - .withPropertyValues("spring.flyway.oracle-sqlplus-warn=true").run((context) -> { - assertThat(context).hasFailed(); - Throwable failure = context.getStartupFailure(); - assertThat(failure).hasRootCauseInstanceOf(FlywayProUpgradeRequiredException.class); - assertThat(failure).hasMessageContaining(" oracle.sqlplusWarn "); - }); + .withPropertyValues("spring.flyway.oracle-sqlplus-warn=true") + .run(validateFlywayTeamsPropertyOnly("oracle.sqlplusWarn")); } @Test void streamIsCorrectlyMapped() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) - .withPropertyValues("spring.flyway.stream=true").run((context) -> { - assertThat(context).hasFailed(); - Throwable failure = context.getStartupFailure(); - assertThat(failure).hasRootCauseInstanceOf(FlywayProUpgradeRequiredException.class); - assertThat(failure).hasMessageContaining(" stream "); - }); + .withPropertyValues("spring.flyway.stream=true").run(validateFlywayTeamsPropertyOnly("stream")); } @Test void undoSqlMigrationPrefix() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) - .withPropertyValues("spring.flyway.undo-sql-migration-prefix=undo").run((context) -> { - assertThat(context).hasFailed(); - Throwable failure = context.getStartupFailure(); - assertThat(failure).hasRootCauseInstanceOf(FlywayProUpgradeRequiredException.class); - assertThat(failure).hasMessageContaining(" undoSqlMigrationPrefix "); - }); + .withPropertyValues("spring.flyway.undo-sql-migration-prefix=undo") + .run(validateFlywayTeamsPropertyOnly("undoSqlMigrationPrefix")); } @Test @@ -492,6 +525,123 @@ void customFlywayClassLoader() { }); } + @Test + void initSqlsWithDataSource() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.flyway.init-sqls=SELECT 1").run((context) -> { + Flyway flyway = context.getBean(Flyway.class); + assertThat(flyway.getConfiguration().getInitSql()).isEqualTo("SELECT 1"); + }); + } + + @Test + void initSqlsWithFlywayUrl() { + this.contextRunner.withPropertyValues("spring.flyway.url:jdbc:h2:mem:" + UUID.randomUUID(), + "spring.flyway.init-sqls=SELECT 1").run((context) -> { + Flyway flyway = context.getBean(Flyway.class); + assertThat(flyway.getConfiguration().getInitSql()).isEqualTo("SELECT 1"); + }); + } + + @Test + void cherryPickIsCorrectlyMapped() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.flyway.cherry-pick=1.1").run(validateFlywayTeamsPropertyOnly("cherryPick")); + } + + @Test + void jdbcPropertiesAreCorrectlyMapped() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.flyway.jdbc-properties.prop=value") + .run(validateFlywayTeamsPropertyOnly("jdbcProperties")); + } + + @Test + void oracleKerberosCacheFileIsCorrectlyMapped() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.flyway.oracle-kerberos-cache-file=/tmp/cache") + .run(validateFlywayTeamsPropertyOnly("oracle.kerberosCacheFile")); + } + + @Test + void oracleKerberosConfigFileIsCorrectlyMapped() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.flyway.oracle-kerberos-config-file=/tmp/config") + .run(validateFlywayTeamsPropertyOnly("oracle.kerberosConfigFile")); + } + + @Test + void outputQueryResultsIsCorrectlyMapped() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.flyway.output-query-results=false") + .run(validateFlywayTeamsPropertyOnly("outputQueryResults")); + } + + @Test + void skipExecutingMigrationsIsCorrectlyMapped() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.flyway.skip-executing-migrations=true") + .run(validateFlywayTeamsPropertyOnly("skipExecutingMigrations")); + } + + @Test + void vaultUrlIsCorrectlyMapped() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.flyway.vault-url=https://example.com/secrets") + .run(validateFlywayTeamsPropertyOnly("vaultUrl")); + } + + @Test + void vaultTokenIsCorrectlyMapped() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.flyway.vault-token=5150") + .run(validateFlywayTeamsPropertyOnly("vaultToken")); + } + + @Test + void vaultSecretsIsCorrectlyMapped() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.flyway.vault-secrets=kv/data/test/1/config,kv/data/test/2/config") + .run(validateFlywayTeamsPropertyOnly("vaultSecrets")); + } + + @Test + void whenFlywayIsAutoConfiguredThenJooqDslContextDependsOnFlywayBeans() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class, JooqConfiguration.class) + .run((context) -> { + BeanDefinition beanDefinition = context.getBeanFactory().getBeanDefinition("dslContext"); + assertThat(beanDefinition.getDependsOn()).containsExactlyInAnyOrder("flywayInitializer", "flyway"); + }); + } + + @Test + void whenCustomMigrationInitializerIsDefinedThenJooqDslContextDependsOnIt() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class, JooqConfiguration.class, + CustomFlywayMigrationInitializer.class).run((context) -> { + BeanDefinition beanDefinition = context.getBeanFactory().getBeanDefinition("dslContext"); + assertThat(beanDefinition.getDependsOn()).containsExactlyInAnyOrder("flywayMigrationInitializer", + "flyway"); + }); + } + + @Test + void whenCustomFlywayIsDefinedThenJooqDslContextDependsOnIt() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class, JooqConfiguration.class, + CustomFlyway.class).run((context) -> { + BeanDefinition beanDefinition = context.getBeanFactory().getBeanDefinition("dslContext"); + assertThat(beanDefinition.getDependsOn()).containsExactlyInAnyOrder("customFlyway"); + }); + } + + private ContextConsumer validateFlywayTeamsPropertyOnly(String propertyName) { + return (context) -> { + assertThat(context).hasFailed(); + Throwable failure = context.getStartupFailure(); + assertThat(failure).hasRootCauseInstanceOf(FlywayTeamsUpgradeRequiredException.class); + assertThat(failure).hasMessageContaining(String.format(" %s ", propertyName)); + }; + } + @Configuration(proxyBeanMethods = false) static class FlywayDataSourceConfiguration { @@ -509,6 +659,27 @@ DataSource flywayDataSource() { } + @Configuration(proxyBeanMethods = false) + static class FlywayMultipleDataSourcesConfiguration { + + @Bean + DataSource firstDataSource() { + return DataSourceBuilder.create().url("jdbc:hsqldb:mem:first").username("sa").build(); + } + + @Bean + DataSource secondDataSource() { + return DataSourceBuilder.create().url("jdbc:hsqldb:mem:second").username("sa").build(); + } + + @FlywayDataSource + @Bean + DataSource flywayDataSource() { + return DataSourceBuilder.create().url("jdbc:hsqldb:mem:flywaytest").username("sa").build(); + } + + } + @Configuration(proxyBeanMethods = false) static class FlywayJavaMigrationsConfiguration { @@ -547,6 +718,16 @@ FlywayMigrationInitializer flywayMigrationInitializer(Flyway flyway) { } + @Configuration(proxyBeanMethods = false) + static class CustomFlyway { + + @Bean + Flyway customFlyway() { + return Flyway.configure().load(); + } + + } + @Configuration(proxyBeanMethods = false) static class CustomFlywayMigrationInitializerWithJpaConfiguration { @@ -663,20 +844,19 @@ void assertCalled() { static class CallbackConfiguration { @Bean - @Order(1) Callback callbackOne() { - return mockCallback(); + return mockCallback("b"); } @Bean - @Order(0) Callback callbackTwo() { - return mockCallback(); + return mockCallback("a"); } - private Callback mockCallback() { + private Callback mockCallback(String name) { Callback callback = mock(Callback.class); given(callback.supports(any(Event.class), any(Context.class))).willReturn(true); + given(callback.getCallbackName()).willReturn(name); return callback; } @@ -699,6 +879,61 @@ FlywayConfigurationCustomizer customizerTwo() { } + @Configuration(proxyBeanMethods = false) + static class JooqConfiguration { + + @Bean + DSLContext dslContext() { + return new DefaultDSLContext(SQLDialect.H2); + } + + } + + @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties(DataSourceProperties.class) + abstract static class AbstractUserH2DataSourceConfiguration { + + @Bean(destroyMethod = "shutdown") + EmbeddedDatabase dataSource(DataSourceProperties properties) throws SQLException { + EmbeddedDatabase database = new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2) + .setName(getDatabaseName(properties)).build(); + insertUser(database); + return database; + } + + protected abstract String getDatabaseName(DataSourceProperties properties); + + private void insertUser(EmbeddedDatabase database) throws SQLException { + try (Connection connection = database.getConnection()) { + connection.prepareStatement("CREATE USER test password 'secret'").execute(); + connection.prepareStatement("ALTER USER test ADMIN TRUE").execute(); + } + } + + } + + @Configuration(proxyBeanMethods = false) + static class PropertiesBackedH2DataSourceConfiguration extends AbstractUserH2DataSourceConfiguration { + + @Override + protected String getDatabaseName(DataSourceProperties properties) { + return properties.determineDatabaseName(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomBackedH2DataSourceConfiguration extends AbstractUserH2DataSourceConfiguration { + + private final String name = UUID.randomUUID().toString(); + + @Override + protected String getDatabaseName(DataSourceProperties properties) { + return this.name; + } + + } + static final class CustomClassLoader extends ClassLoader { private CustomClassLoader(ClassLoader parent) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayMigrationScriptMissingFailureAnalyzerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayMigrationScriptMissingFailureAnalyzerTests.java index 01cdfc1b4975..87253526704f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayMigrationScriptMissingFailureAnalyzerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayMigrationScriptMissingFailureAnalyzerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,9 @@ * Tests for {@link FlywayMigrationScriptMissingFailureAnalyzer}. * * @author Anand Shastri + * @deprecated since 2.5.0 for removal in 2.7.0 as location checking is deprecated */ +@Deprecated class FlywayMigrationScriptMissingFailureAnalyzerTests { @Test diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java index b200c6cf3f84..0e0c8138304e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,6 +40,7 @@ * Tests for {@link FlywayProperties}. * * @author Stephane Nicoll + * @author Chris Bono */ class FlywayPropertiesTests { @@ -51,7 +52,12 @@ void defaultValuesAreConsistent() { .isEqualTo(configuration.getLocations()); assertThat(properties.getEncoding()).isEqualTo(configuration.getEncoding()); assertThat(properties.getConnectRetries()).isEqualTo(configuration.getConnectRetries()); + // Can't assert lock retry count default as it is new in Flyway 7.1 + // Asserting hard-coded value in the metadata instead + assertThat(configuration.getLockRetryCount()).isEqualTo(50); + assertThat(properties.getDefaultSchema()).isEqualTo(configuration.getDefaultSchema()); assertThat(properties.getSchemas()).isEqualTo(Arrays.asList(configuration.getSchemas())); + assertThat(properties.isCreateSchemas()).isEqualTo(configuration.getCreateSchemas()); assertThat(properties.getTable()).isEqualTo(configuration.getTable()); assertThat(properties.getBaselineDescription()).isEqualTo(configuration.getBaselineDescription()); assertThat(MigrationVersion.fromVersion(properties.getBaselineVersion())) @@ -83,6 +89,7 @@ void defaultValuesAreConsistent() { assertThat(configuration.isOutOfOrder()).isEqualTo(properties.isOutOfOrder()); assertThat(configuration.isSkipDefaultCallbacks()).isEqualTo(properties.isSkipDefaultCallbacks()); assertThat(configuration.isSkipDefaultResolvers()).isEqualTo(properties.isSkipDefaultResolvers()); + assertThat(configuration.isValidateMigrationNaming()).isEqualTo(properties.isValidateMigrationNaming()); assertThat(configuration.isValidateOnMigrate()).isEqualTo(properties.isValidateOnMigrate()); } @@ -93,10 +100,11 @@ void expectedPropertiesAreManaged() { Map configuration = indexProperties( PropertyAccessorFactory.forBeanPropertyAccess(new ClassicConfiguration())); // Properties specific settings - ignoreProperties(properties, "url", "user", "password", "enabled", "checkLocation", "createDataSource"); - + ignoreProperties(properties, "url", "driverClassName", "user", "password", "enabled", "checkLocation", + "createDataSource"); // High level object we can't set with properties - ignoreProperties(configuration, "callbacks", "classLoader", "dataSource", "javaMigrations", "resolvers"); + ignoreProperties(configuration, "callbacks", "dataSource", "javaMigrations", "javaMigrationClassProvider", + "resourceProvider", "resolvers"); // Properties we don't want to expose ignoreProperties(configuration, "resolversAsClassNames", "callbacksAsClassNames"); // Handled by the conversion service @@ -107,11 +115,15 @@ void expectedPropertiesAreManaged() { ignoreProperties(properties, "initSqls"); // Handled as dryRunOutput ignoreProperties(configuration, "dryRunOutputAsFile", "dryRunOutputAsFileName"); + // Handled as createSchemas + ignoreProperties(configuration, "shouldCreateSchemas"); + // Getters for the DataSource settings rather than actual properties + ignoreProperties(configuration, "password", "url", "user"); List configurationKeys = new ArrayList<>(configuration.keySet()); Collections.sort(configurationKeys); List propertiesKeys = new ArrayList<>(properties.keySet()); Collections.sort(propertiesKeys); - assertThat(configurationKeys).isEqualTo(propertiesKeys); + assertThat(configurationKeys).containsExactlyElementsOf(propertiesKeys); } private void ignoreProperties(Map index, String... propertyNames) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerAutoConfigurationTests.java index 27fa3f18cba5..6b60297a2b22 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,7 +37,7 @@ * @author Kazuki Shimizu */ @ExtendWith(OutputCaptureExtension.class) -public class FreeMarkerAutoConfigurationTests { +class FreeMarkerAutoConfigurationTests { private final BuildOutput buildOutput = new BuildOutput(getClass()); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfigurationTests.java index b7c390840ab4..c0bdc391b28c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,23 +21,22 @@ import javax.sql.DataSource; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanCreationException; +import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; -import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.boot.web.servlet.ServletRegistrationBean; -import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebApplicationContext; -import org.springframework.mock.web.MockServletContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; /** * Tests for {@link H2ConsoleAutoConfiguration} @@ -45,93 +44,103 @@ * @author Andy Wilkinson * @author Marten Deinum * @author Stephane Nicoll + * @author Shraddha Yeole */ class H2ConsoleAutoConfigurationTests { - private AnnotationConfigServletWebApplicationContext context = new AnnotationConfigServletWebApplicationContext(); - - @BeforeEach - void setupContext() { - this.context.setServletContext(new MockServletContext()); - } - - @AfterEach - void close() { - if (this.context != null) { - this.context.close(); - } - } + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(H2ConsoleAutoConfiguration.class)); @Test void consoleIsDisabledByDefault() { - this.context.register(H2ConsoleAutoConfiguration.class); - this.context.refresh(); - assertThat(this.context.getBeansOfType(ServletRegistrationBean.class)).isEmpty(); + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(ServletRegistrationBean.class)); } @Test void propertyCanEnableConsole() { - this.context.register(H2ConsoleAutoConfiguration.class); - TestPropertyValues.of("spring.h2.console.enabled:true").applyTo(this.context); - this.context.refresh(); - assertThat(this.context.getBeansOfType(ServletRegistrationBean.class)).hasSize(1); - ServletRegistrationBean registrationBean = this.context.getBean(ServletRegistrationBean.class); - assertThat(registrationBean.getUrlMappings()).contains("/h2-console/*"); - assertThat(registrationBean.getInitParameters()).doesNotContainKey("trace"); - assertThat(registrationBean.getInitParameters()).doesNotContainKey("webAllowOthers"); + this.contextRunner.withPropertyValues("spring.h2.console.enabled=true").run((context) -> { + assertThat(context).hasSingleBean(ServletRegistrationBean.class); + ServletRegistrationBean registrationBean = context.getBean(ServletRegistrationBean.class); + assertThat(registrationBean.getUrlMappings()).contains("/h2-console/*"); + assertThat(registrationBean.getInitParameters()).doesNotContainKey("trace"); + assertThat(registrationBean.getInitParameters()).doesNotContainKey("webAllowOthers"); + assertThat(registrationBean.getInitParameters()).doesNotContainKey("webAdminPassword"); + }); } @Test void customPathMustBeginWithASlash() { - this.context.register(H2ConsoleAutoConfiguration.class); - TestPropertyValues.of("spring.h2.console.enabled:true", "spring.h2.console.path:custom").applyTo(this.context); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy(this.context::refresh) - .withMessageContaining("Failed to bind properties under 'spring.h2.console'"); + this.contextRunner.withPropertyValues("spring.h2.console.enabled=true", "spring.h2.console.path=custom") + .run((context) -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()).isInstanceOf(BeanCreationException.class) + .hasMessageContaining("Failed to bind properties under 'spring.h2.console'"); + }); } @Test void customPathWithTrailingSlash() { - this.context.register(H2ConsoleAutoConfiguration.class); - TestPropertyValues.of("spring.h2.console.enabled:true", "spring.h2.console.path:/custom/") - .applyTo(this.context); - this.context.refresh(); - assertThat(this.context.getBeansOfType(ServletRegistrationBean.class)).hasSize(1); - ServletRegistrationBean servletRegistrationBean = this.context.getBean(ServletRegistrationBean.class); - assertThat(servletRegistrationBean.getUrlMappings()).contains("/custom/*"); + this.contextRunner.withPropertyValues("spring.h2.console.enabled=true", "spring.h2.console.path=/custom/") + .run((context) -> { + assertThat(context).hasSingleBean(ServletRegistrationBean.class); + ServletRegistrationBean registrationBean = context.getBean(ServletRegistrationBean.class); + assertThat(registrationBean.getUrlMappings()).contains("/custom/*"); + }); } @Test void customPath() { - this.context.register(H2ConsoleAutoConfiguration.class); - TestPropertyValues.of("spring.h2.console.enabled:true", "spring.h2.console.path:/custom").applyTo(this.context); - this.context.refresh(); - assertThat(this.context.getBeansOfType(ServletRegistrationBean.class)).hasSize(1); - ServletRegistrationBean servletRegistrationBean = this.context.getBean(ServletRegistrationBean.class); - assertThat(servletRegistrationBean.getUrlMappings()).contains("/custom/*"); + this.contextRunner.withPropertyValues("spring.h2.console.enabled=true", "spring.h2.console.path=/custom") + .run((context) -> { + assertThat(context).hasSingleBean(ServletRegistrationBean.class); + ServletRegistrationBean registrationBean = context.getBean(ServletRegistrationBean.class); + assertThat(registrationBean.getUrlMappings()).contains("/custom/*"); + }); } @Test void customInitParameters() { - this.context.register(H2ConsoleAutoConfiguration.class); - TestPropertyValues.of("spring.h2.console.enabled:true", "spring.h2.console.settings.trace=true", - "spring.h2.console.settings.webAllowOthers=true").applyTo(this.context); - this.context.refresh(); - assertThat(this.context.getBeansOfType(ServletRegistrationBean.class)).hasSize(1); - ServletRegistrationBean registrationBean = this.context.getBean(ServletRegistrationBean.class); - assertThat(registrationBean.getUrlMappings()).contains("/h2-console/*"); - assertThat(registrationBean.getInitParameters()).containsEntry("trace", ""); - assertThat(registrationBean.getInitParameters()).containsEntry("webAllowOthers", ""); + this.contextRunner.withPropertyValues("spring.h2.console.enabled=true", "spring.h2.console.settings.trace=true", + "spring.h2.console.settings.web-allow-others=true", + "spring.h2.console.settings.web-admin-password=abcd").run((context) -> { + assertThat(context).hasSingleBean(ServletRegistrationBean.class); + ServletRegistrationBean registrationBean = context.getBean(ServletRegistrationBean.class); + assertThat(registrationBean.getUrlMappings()).contains("/h2-console/*"); + assertThat(registrationBean.getInitParameters()).containsEntry("trace", ""); + assertThat(registrationBean.getInitParameters()).containsEntry("webAllowOthers", ""); + assertThat(registrationBean.getInitParameters()).containsEntry("webAdminPassword", "abcd"); + }); } @Test @ExtendWith(OutputCaptureExtension.class) - void dataSourceUrlIsLoggedWhenAvailable(CapturedOutput output) throws BeansException, SQLException { - this.context.register(DataSourceAutoConfiguration.class, H2ConsoleAutoConfiguration.class); - TestPropertyValues.of("spring.h2.console.enabled:true").applyTo(this.context); - this.context.refresh(); - try (Connection connection = this.context.getBean(DataSource.class).getConnection()) { - assertThat(output).contains("Database available at '" + connection.getMetaData().getURL() + "'"); + void dataSourceUrlIsLoggedWhenAvailable(CapturedOutput output) { + this.contextRunner.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) + .withPropertyValues("spring.h2.console.enabled=true").run((context) -> { + try (Connection connection = context.getBean(DataSource.class).getConnection()) { + assertThat(output) + .contains("Database available at '" + connection.getMetaData().getURL() + "'"); + } + }); + } + + @Test + void h2ConsoleShouldNotFailIfDatabaseConnectionFails() { + this.contextRunner.withUserConfiguration(CustomDataSourceConfiguration.class) + .withPropertyValues("spring.h2.console.enabled=true") + .run((context) -> assertThat(context.isRunning()).isTrue()); + } + + @Configuration(proxyBeanMethods = false) + static class CustomDataSourceConfiguration { + + @Bean + DataSource dataSource() throws SQLException { + DataSource dataSource = mock(DataSource.class); + given(dataSource.getConnection()).willThrow(IllegalStateException.class); + return dataSource; } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsolePropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsolePropertiesTests.java index eb3918b0e274..ddcd6e114546 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsolePropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsolePropertiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,26 +27,24 @@ */ class H2ConsolePropertiesTests { - private H2ConsoleProperties properties; - @Test void pathMustNotBeEmpty() { - this.properties = new H2ConsoleProperties(); - assertThatIllegalArgumentException().isThrownBy(() -> this.properties.setPath("")) + H2ConsoleProperties properties = new H2ConsoleProperties(); + assertThatIllegalArgumentException().isThrownBy(() -> properties.setPath("")) .withMessageContaining("Path must have length greater than 1"); } @Test void pathMustHaveLengthGreaterThanOne() { - this.properties = new H2ConsoleProperties(); - assertThatIllegalArgumentException().isThrownBy(() -> this.properties.setPath("/")) + H2ConsoleProperties properties = new H2ConsoleProperties(); + assertThatIllegalArgumentException().isThrownBy(() -> properties.setPath("/")) .withMessageContaining("Path must have length greater than 1"); } @Test void customPathMustBeginWithASlash() { - this.properties = new H2ConsoleProperties(); - assertThatIllegalArgumentException().isThrownBy(() -> this.properties.setPath("custom")) + H2ConsoleProperties properties = new H2ConsoleProperties(); + assertThatIllegalArgumentException().isThrownBy(() -> properties.setPath("custom")) .withMessageContaining("Path must start with '/'"); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hateoas/HypermediaAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hateoas/HypermediaAutoConfigurationTests.java index 229eaf7c28de..9931093c1dbb 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hateoas/HypermediaAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hateoas/HypermediaAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ import java.util.Optional; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; @@ -26,20 +25,20 @@ import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; -import org.springframework.boot.test.util.TestPropertyValues; -import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebApplicationContext; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.context.annotation.Configuration; import org.springframework.hateoas.MediaTypes; +import org.springframework.hateoas.RepresentationModel; import org.springframework.hateoas.client.LinkDiscoverer; import org.springframework.hateoas.client.LinkDiscoverers; import org.springframework.hateoas.config.EnableHypermediaSupport; import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; import org.springframework.hateoas.mediatype.hal.HalLinkDiscoverer; import org.springframework.hateoas.server.EntityLinks; -import org.springframework.hateoas.server.mvc.TypeConstrainedMappingJackson2HttpMessageConverter; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.mock.web.MockServletContext; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import static org.assertj.core.api.Assertions.assertThat; @@ -50,78 +49,68 @@ * @author Roy Clarkson * @author Oliver Gierke * @author Andy Wilkinson + * @author Madhura Bhave */ class HypermediaAutoConfigurationTests { - private AnnotationConfigServletWebApplicationContext context; + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withUserConfiguration(BaseConfig.class); - @AfterEach - void close() { - if (this.context != null) { - this.context.close(); - } + @Test + void autoConfigurationWhenSpringMvcNotOnClasspathShouldBackOff() { + this.contextRunner.withClassLoader(new FilteredClassLoader(RequestMappingHandlerAdapter.class)) + .run((context) -> assertThat(context.getBeansOfType(HypermediaConfiguration.class)).isEmpty()); } @Test void linkDiscoverersCreated() { - this.context = new AnnotationConfigServletWebApplicationContext(); - this.context.setServletContext(new MockServletContext()); - this.context.register(BaseConfig.class); - this.context.refresh(); - LinkDiscoverers discoverers = this.context.getBean(LinkDiscoverers.class); - assertThat(discoverers).isNotNull(); - Optional discoverer = discoverers.getLinkDiscovererFor(MediaTypes.HAL_JSON); - assertThat(discoverer).containsInstanceOf(HalLinkDiscoverer.class); + this.contextRunner.run((context) -> { + LinkDiscoverers discoverers = context.getBean(LinkDiscoverers.class); + assertThat(discoverers).isNotNull(); + Optional discoverer = discoverers.getLinkDiscovererFor(MediaTypes.HAL_JSON); + assertThat(discoverer).containsInstanceOf(HalLinkDiscoverer.class); + }); } @Test void entityLinksCreated() { - this.context = new AnnotationConfigServletWebApplicationContext(); - this.context.setServletContext(new MockServletContext()); - this.context.register(BaseConfig.class); - this.context.refresh(); - EntityLinks discoverers = this.context.getBean(EntityLinks.class); - assertThat(discoverers).isNotNull(); + this.contextRunner.run((context) -> { + EntityLinks discoverers = context.getBean(EntityLinks.class); + assertThat(discoverers).isNotNull(); + }); } @Test void doesBackOffIfEnableHypermediaSupportIsDeclaredManually() { - this.context = new AnnotationConfigServletWebApplicationContext(); - this.context.setServletContext(new MockServletContext()); - this.context.register(EnableHypermediaSupportConfig.class, BaseConfig.class); - TestPropertyValues.of("spring.jackson.serialization.INDENT_OUTPUT:true").applyTo(this.context); - this.context.refresh(); - assertThat(this.context.getBeansOfType(HypermediaConfiguration.class)).isEmpty(); + this.contextRunner.withUserConfiguration(EnableHypermediaSupportConfig.class) + .withPropertyValues("spring.jackson.serialization.INDENT_OUTPUT:true") + .run((context) -> assertThat(context.getBeansOfType(HypermediaConfiguration.class)).isEmpty()); } @Test - void supportedMediaTypesOfTypeConstrainedConvertersIsCustomized() { - this.context = new AnnotationConfigServletWebApplicationContext(); - this.context.setServletContext(new MockServletContext()); - this.context.register(BaseConfig.class); - this.context.refresh(); - RequestMappingHandlerAdapter handlerAdapter = this.context.getBean(RequestMappingHandlerAdapter.class); - for (HttpMessageConverter converter : handlerAdapter.getMessageConverters()) { - if (converter instanceof TypeConstrainedMappingJackson2HttpMessageConverter) { - assertThat(converter.getSupportedMediaTypes()).contains(MediaType.APPLICATION_JSON, - MediaTypes.HAL_JSON); - } - } + void whenUsingTheDefaultConfigurationThenMappingJacksonConverterCanWriteHateoasTypeAsApplicationJson() { + this.contextRunner.run((context) -> { + RequestMappingHandlerAdapter handlerAdapter = context.getBean(RequestMappingHandlerAdapter.class); + Optional> mappingJacksonConverter = handlerAdapter.getMessageConverters().stream() + .filter(MappingJackson2HttpMessageConverter.class::isInstance).findFirst(); + assertThat(mappingJacksonConverter).isPresent().hasValueSatisfying( + (converter) -> assertThat(converter.canWrite(RepresentationModel.class, MediaType.APPLICATION_JSON)) + .isTrue()); + }); } @Test - void customizationOfSupportedMediaTypesCanBeDisabled() { - this.context = new AnnotationConfigServletWebApplicationContext(); - this.context.setServletContext(new MockServletContext()); - this.context.register(BaseConfig.class); - TestPropertyValues.of("spring.hateoas.use-hal-as-default-json-media-type:false").applyTo(this.context); - this.context.refresh(); - RequestMappingHandlerAdapter handlerAdapter = this.context.getBean(RequestMappingHandlerAdapter.class); - for (HttpMessageConverter converter : handlerAdapter.getMessageConverters()) { - if (converter instanceof TypeConstrainedMappingJackson2HttpMessageConverter) { - assertThat(converter.getSupportedMediaTypes()).containsExactly(MediaTypes.HAL_JSON); - } - } + void whenHalIsNotTheDefaultJsonMediaTypeThenMappingJacksonConverterCannotWriteHateoasTypeAsApplicationJson() { + this.contextRunner.withPropertyValues("spring.hateoas.use-hal-as-default-json-media-type:false") + .run((context) -> { + RequestMappingHandlerAdapter handlerAdapter = context.getBean(RequestMappingHandlerAdapter.class); + Optional> mappingJacksonConverter = handlerAdapter.getMessageConverters() + .stream().filter(MappingJackson2HttpMessageConverter.class::isInstance).findFirst(); + assertThat(mappingJacksonConverter).isPresent() + .hasValueSatisfying((converter) -> assertThat( + converter.canWrite(RepresentationModel.class, MediaType.APPLICATION_JSON)) + .isFalse()); + }); } @ImportAutoConfiguration({ HttpMessageConvertersAutoConfiguration.class, WebMvcAutoConfiguration.class, diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hazelcast/Hazelcast3AutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hazelcast/Hazelcast3AutoConfigurationTests.java new file mode 100644 index 000000000000..1478cb35c725 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hazelcast/Hazelcast3AutoConfigurationTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.hazelcast; + +import com.hazelcast.client.impl.clientside.HazelcastClientProxy; +import com.hazelcast.config.Config; +import com.hazelcast.core.Hazelcast; +import com.hazelcast.core.HazelcastInstance; +import org.assertj.core.api.Condition; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.ContextConsumer; +import org.springframework.boot.testsupport.classpath.ClassPathExclusions; +import org.springframework.boot.testsupport.classpath.ClassPathOverrides; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link HazelcastAutoConfiguration} with Hazelcast 3. + * + * @author Stephane Nicoll + */ +@ClassPathExclusions("hazelcast*.jar") +@ClassPathOverrides({ "com.hazelcast:hazelcast:3.12.8", "com.hazelcast:hazelcast-client:3.12.8" }) +class Hazelcast3AutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(HazelcastAutoConfiguration.class)); + + @Test + void defaultConfigFile() { + // no hazelcast-client.xml and hazelcast.xml is present in root classpath + // this also asserts that XML has priority over YAML + // as both hazelcast.yaml and hazelcast.xml in test classpath. + this.contextRunner.run((context) -> { + Config config = context.getBean(HazelcastInstance.class).getConfig(); + assertThat(config.getConfigurationUrl()).isEqualTo(new ClassPathResource("hazelcast.xml").getURL()); + }); + } + + @Test + void explicitConfigFileWithXml() { + HazelcastInstance hazelcastServer = Hazelcast.newHazelcastInstance(); + try { + this.contextRunner + .withPropertyValues("spring.hazelcast.config=org/springframework/boot/autoconfigure/" + + "hazelcast/hazelcast-client-specific.xml") + .run(assertSpecificHazelcastClient("explicit-xml")); + } + finally { + hazelcastServer.shutdown(); + } + } + + private ContextConsumer assertSpecificHazelcastClient(String label) { + return (context) -> assertThat(context).getBean(HazelcastInstance.class).isInstanceOf(HazelcastInstance.class) + .has(labelEqualTo(label)); + } + + private static Condition labelEqualTo(String label) { + return new Condition<>((o) -> ((HazelcastClientProxy) o).getClientConfig().getLabels().stream() + .anyMatch((e) -> e.equals(label)), "Label equals to " + label); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastAutoConfigurationClientTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastAutoConfigurationClientTests.java index 87d15d0a0251..27d98497a307 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastAutoConfigurationClientTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastAutoConfigurationClientTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.boot.autoconfigure.hazelcast; +import com.hazelcast.client.HazelcastClient; import com.hazelcast.client.config.ClientConfig; import com.hazelcast.client.impl.clientside.HazelcastClientProxy; import com.hazelcast.config.Config; @@ -124,6 +125,28 @@ void clientConfigTakesPrecedence() { .getBean(HazelcastInstance.class).isInstanceOf(HazelcastClientProxy.class)); } + @Test + void clientConfigWithInstanceNameCreatesClientIfNecessary() { + assertThat(HazelcastClient.getHazelcastClientByName("spring-boot")).isNull(); + this.contextRunner + .withPropertyValues("spring.hazelcast.config=classpath:org/springframework/" + + "boot/autoconfigure/hazelcast/hazelcast-client-instance.xml") + .run((context) -> assertThat(context).getBean(HazelcastInstance.class) + .extracting(HazelcastInstance::getName).isEqualTo("spring-boot")); + } + + @Test + void autoConfiguredClientConfigUsesApplicationClassLoader() { + this.contextRunner.withPropertyValues("spring.hazelcast.config=org/springframework/boot/autoconfigure/" + + "hazelcast/hazelcast-client-specific.xml").run((context) -> { + HazelcastInstance hazelcast = context.getBean(HazelcastInstance.class); + assertThat(hazelcast).isInstanceOf(HazelcastClientProxy.class); + ClientConfig clientConfig = ((HazelcastClientProxy) hazelcast).getClientConfig(); + assertThat(clientConfig.getClassLoader()) + .isSameAs(context.getSourceApplicationContext().getClassLoader()); + }); + } + private ContextConsumer assertSpecificHazelcastClient(String label) { return (context) -> assertThat(context).getBean(HazelcastInstance.class).isInstanceOf(HazelcastInstance.class) .has(labelEqualTo(label)); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastAutoConfigurationServerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastAutoConfigurationServerTests.java index 25e8c4271d30..3856dc994160 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastAutoConfigurationServerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastAutoConfigurationServerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -156,6 +156,14 @@ void configInstanceWithoutName() { }); } + @Test + void autoConfiguredConfigUsesApplicationClassLoader() { + this.contextRunner.run((context) -> { + Config config = context.getBean(HazelcastInstance.class).getConfig(); + assertThat(config.getClassLoader()).isSameAs(context.getSourceApplicationContext().getClassLoader()); + }); + } + @Configuration(proxyBeanMethods = false) static class HazelcastConfigWithName { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastAutoConfigurationTests.java index f389c683a7cf..6e941f928253 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,10 @@ package org.springframework.boot.autoconfigure.hazelcast; +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; + import com.hazelcast.config.Config; import com.hazelcast.core.HazelcastInstance; import org.junit.jupiter.api.Test; @@ -23,6 +27,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; import static org.assertj.core.api.Assertions.assertThat; @@ -47,4 +52,41 @@ void defaultConfigFile() { }); } + @Test + void hazelcastInstanceNotCreatedWhenJetIsPresent() { + this.contextRunner.withClassLoader(new JetConfigClassLoader()) + .run((context) -> assertThat(context).doesNotHaveBean(HazelcastInstance.class)); + } + + /** + * A test {@link URLClassLoader} that emulates the default Hazelcast Jet configuration + * file exists on the classpath. + */ + static class JetConfigClassLoader extends URLClassLoader { + + private static final Resource FALLBACK = new ClassPathResource("hazelcast.yaml"); + + JetConfigClassLoader() { + super(new URL[0], JetConfigClassLoader.class.getClassLoader()); + } + + @Override + public URL getResource(String name) { + if (name.equals("hazelcast-jet-default.yaml")) { + return getEmulatedJetConfigUrl(); + } + return super.getResource(name); + } + + private URL getEmulatedJetConfigUrl() { + try { + return FALLBACK.getURL(); + } + catch (IOException ex) { + throw new IllegalArgumentException(ex); + } + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastClientConfigAvailableConditionTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastClientConfigAvailableConditionTests.java new file mode 100644 index 000000000000..f26e71ffb895 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastClientConfigAvailableConditionTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.hazelcast; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.env.Environment; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.mock.env.MockEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link HazelcastClientConfigAvailableCondition}. + * + * @author Stephane Nicoll + */ +class HazelcastClientConfigAvailableConditionTests { + + private final HazelcastClientConfigAvailableCondition condition = new HazelcastClientConfigAvailableCondition(); + + @Test + void explicitConfigurationWithClientConfigMatches() { + ConditionOutcome outcome = getMatchOutcome(new MockEnvironment().withProperty("spring.hazelcast.config", + "classpath:org/springframework/boot/autoconfigure/hazelcast/hazelcast-client-specific.xml")); + assertThat(outcome.isMatch()).isTrue(); + assertThat(outcome.getMessage()).contains("Hazelcast client configuration detected"); + } + + @Test + void explicitConfigurationWithServerConfigDoesNotMatch() { + ConditionOutcome outcome = getMatchOutcome(new MockEnvironment().withProperty("spring.hazelcast.config", + "classpath:org/springframework/boot/autoconfigure/hazelcast/hazelcast-specific.xml")); + assertThat(outcome.isMatch()).isFalse(); + assertThat(outcome.getMessage()).contains("Hazelcast server configuration detected"); + } + + @Test + void explicitConfigurationWithMissingConfigDoesNotMatch() { + ConditionOutcome outcome = getMatchOutcome(new MockEnvironment().withProperty("spring.hazelcast.config", + "classpath:org/springframework/boot/autoconfigure/hazelcast/test-config-does-not-exist.xml")); + assertThat(outcome.isMatch()).isFalse(); + assertThat(outcome.getMessage()).contains("Hazelcast configuration does not exist"); + } + + private ConditionOutcome getMatchOutcome(Environment environment) { + ConditionContext conditionContext = mock(ConditionContext.class); + given(conditionContext.getEnvironment()).willReturn(environment); + given(conditionContext.getResourceLoader()).willReturn(new DefaultResourceLoader()); + return this.condition.getMatchOutcome(conditionContext, mock(AnnotatedTypeMetadata.class)); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastJpaDependencyAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastJpaDependencyAutoConfigurationTests.java index 6fa67cad4aa0..c50db4119d5e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastJpaDependencyAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hazelcast/HazelcastJpaDependencyAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,9 +26,9 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.data.jpa.EntityManagerFactoryDependsOnPostProcessor; import org.springframework.boot.autoconfigure.hazelcast.HazelcastJpaDependencyAutoConfiguration.HazelcastInstanceEntityManagerFactoryDependsOnPostProcessor; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.orm.jpa.EntityManagerFactoryDependsOnPostProcessor; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -48,11 +48,10 @@ class HazelcastJpaDependencyAutoConfigurationTests { private static final String POST_PROCESSOR_BEAN_NAME = HazelcastInstanceEntityManagerFactoryDependsOnPostProcessor.class .getName(); - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class, HazelcastJpaDependencyAutoConfiguration.class)) - .withPropertyValues("spring.datasource.generate-unique-name=true", - "spring.datasource.initialization-mode=never"); + .withPropertyValues("spring.datasource.generate-unique-name=true"); @Test void registrationIfHazelcastInstanceHasRegularBeanName() { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/http/HttpMessageConvertersAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/http/HttpMessageConvertersAutoConfigurationTests.java index 85af1e0b0ed2..7851b1a0d4a2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/http/HttpMessageConvertersAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/http/HttpMessageConvertersAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.boot.autoconfigure.http; +import java.nio.charset.StandardCharsets; + import javax.json.bind.Jsonb; import com.fasterxml.jackson.databind.ObjectMapper; @@ -28,6 +30,7 @@ import org.springframework.boot.autoconfigure.http.JacksonHttpMessageConvertersConfiguration.MappingJackson2HttpMessageConverterConfiguration; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration; +import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -62,7 +65,7 @@ */ class HttpMessageConvertersAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class)); @Test @@ -121,7 +124,7 @@ void gsonCustomConverter() { @Test void gsonCanBePreferred() { - allOptionsRunner().withPropertyValues("spring.http.converters.preferred-json-mapper:gson").run((context) -> { + allOptionsRunner().withPropertyValues("spring.mvc.converters.preferred-json-mapper:gson").run((context) -> { assertConverterBeanExists(context, GsonHttpMessageConverter.class, "gsonHttpMessageConverter"); assertConverterBeanRegisteredWithHttpMessageConverters(context, GsonHttpMessageConverter.class); assertThat(context).doesNotHaveBean(JsonbHttpMessageConverter.class); @@ -152,7 +155,7 @@ void jsonbCustomConverter() { @Test void jsonbCanBePreferred() { - allOptionsRunner().withPropertyValues("spring.http.converters.preferred-json-mapper:jsonb").run((context) -> { + allOptionsRunner().withPropertyValues("spring.mvc.converters.preferred-json-mapper:jsonb").run((context) -> { assertConverterBeanExists(context, JsonbHttpMessageConverter.class, "jsonbHttpMessageConverter"); assertConverterBeanRegisteredWithHttpMessageConverters(context, JsonbHttpMessageConverter.class); assertThat(context).doesNotHaveBean(GsonHttpMessageConverter.class); @@ -237,6 +240,38 @@ void whenReactiveWebApplicationHttpMessageConvertersIsNotConfigured() { .run((context) -> assertThat(context).doesNotHaveBean(HttpMessageConverters.class)); } + @Test + void whenEncodingCharsetIsNotConfiguredThenStringMessageConverterUsesUtf8() { + new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class)) + .run((context) -> { + assertThat(context).hasSingleBean(StringHttpMessageConverter.class); + assertThat(context.getBean(StringHttpMessageConverter.class).getDefaultCharset()) + .isEqualTo(StandardCharsets.UTF_8); + }); + } + + @Test + void whenEncodingCharsetIsConfiguredThenStringMessageConverterUsesSpecificCharset() { + new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class)) + .withPropertyValues("server.servlet.encoding.charset=UTF-16").run((context) -> { + assertThat(context).hasSingleBean(StringHttpMessageConverter.class); + assertThat(context.getBean(StringHttpMessageConverter.class).getDefaultCharset()) + .isEqualTo(StandardCharsets.UTF_16); + }); + } + + @Test // gh-21789 + void whenAutoConfigurationIsActiveThenServerPropertiesConfigurationPropertiesAreNotEnabled() { + new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class)) + .run((context) -> { + assertThat(context).hasSingleBean(HttpMessageConverters.class); + assertThat(context).doesNotHaveBean(ServerProperties.class); + }); + } + private ApplicationContextRunner allOptionsRunner() { return this.contextRunner.withConfiguration(AutoConfigurations.of(GsonAutoConfiguration.class, JacksonAutoConfiguration.class, JsonbAutoConfiguration.class)); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/http/HttpMessageConvertersTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/http/HttpMessageConvertersTests.java index f8468fa85bfe..06a0184a6e38 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/http/HttpMessageConvertersTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/http/HttpMessageConvertersTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.stream.Collectors; import org.junit.jupiter.api.Test; @@ -28,12 +29,12 @@ import org.springframework.http.converter.ResourceRegionHttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.cbor.MappingJackson2CborHttpMessageConverter; +import org.springframework.http.converter.json.GsonHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter; import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; import org.springframework.http.converter.xml.SourceHttpMessageConverter; -import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -81,6 +82,16 @@ void addBeforeExistingConverter() { assertThat(converters.getConverters().indexOf(converter1)).isNotEqualTo(0); } + @Test + void addBeforeExistingEquivalentConverter() { + GsonHttpMessageConverter converter1 = new GsonHttpMessageConverter(); + HttpMessageConverters converters = new HttpMessageConverters(converter1); + List> converterClasses = converters.getConverters().stream().map(HttpMessageConverter::getClass) + .collect(Collectors.toList()); + assertThat(converterClasses).containsSequence(GsonHttpMessageConverter.class, + MappingJackson2HttpMessageConverter.class); + } + @Test void addNewConverters() { HttpMessageConverter converter1 = mock(HttpMessageConverter.class); @@ -143,10 +154,9 @@ protected List> postProcessPartConverters( MappingJackson2HttpMessageConverter.class, MappingJackson2SmileHttpMessageConverter.class); } - @SuppressWarnings("unchecked") private List> extractFormPartConverters(List> converters) { AllEncompassingFormHttpMessageConverter formConverter = findFormConverter(converters); - return (List>) ReflectionTestUtils.getField(formConverter, "partConverters"); + return formConverter.getPartConverters(); } private AllEncompassingFormHttpMessageConverter findFormConverter(Collection> converters) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/http/codec/CodecsAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/http/codec/CodecsAutoConfigurationTests.java index fcd5b78359bd..cb8ac25521e6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/http/codec/CodecsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/http/codec/CodecsAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.autoconfigure.http.codec; import java.lang.reflect.Method; @@ -23,7 +24,6 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.codec.CodecProperties; -import org.springframework.boot.autoconfigure.http.HttpProperties; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.web.codec.CodecCustomizer; import org.springframework.context.annotation.Bean; @@ -43,7 +43,7 @@ */ class CodecsAutoConfigurationTests { - private WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(CodecsAutoConfiguration.class)); @Test @@ -59,7 +59,7 @@ void autoConfigShouldProvideALoggingRequestDetailsCustomizer() { @Test void loggingRequestDetailsCustomizerShouldUseHttpProperties() { - this.contextRunner.withPropertyValues("spring.http.log-request-details=true").run((context) -> { + this.contextRunner.withPropertyValues("spring.codec.log-request-details=true").run((context) -> { CodecCustomizer customizer = context.getBean(CodecCustomizer.class); CodecConfigurer configurer = new DefaultClientCodecConfigurer(); customizer.customize(configurer); @@ -72,7 +72,7 @@ void defaultCodecCustomizerBeanShouldHaveOrderZero() { this.contextRunner.run((context) -> { Method customizerMethod = ReflectionUtils.findMethod( CodecsAutoConfiguration.DefaultCodecsConfiguration.class, "defaultCodecCustomizer", - HttpProperties.class, CodecProperties.class); + CodecProperties.class); Integer order = new TestAnnotationAwareOrderComparator().findOrder(customizerMethod); assertThat(order).isEqualTo(0); }); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/influx/InfluxDbAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/influx/InfluxDbAutoConfigurationTests.java index a3b22d8a5c6d..34033cd66959 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/influx/InfluxDbAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/influx/InfluxDbAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,21 +46,21 @@ class InfluxDbAutoConfigurationTests { @Test void influxDbRequiresUrl() { - this.contextRunner.run((context) -> assertThat(context.getBeansOfType(InfluxDB.class)).isEmpty()); + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(InfluxDB.class)); } @Test void influxDbCanBeCustomized() { this.contextRunner - .withPropertyValues("spring.influx.url=http://localhost", "spring.influx.password:password", - "spring.influx.user:user") - .run(((context) -> assertThat(context.getBeansOfType(InfluxDB.class)).hasSize(1))); + .withPropertyValues("spring.influx.url=http://localhost", "spring.influx.user=user", + "spring.influx.password=password") + .run((context) -> assertThat(context).hasSingleBean(InfluxDB.class)); } @Test void influxDbCanBeCreatedWithoutCredentials() { this.contextRunner.withPropertyValues("spring.influx.url=http://localhost").run((context) -> { - assertThat(context.getBeansOfType(InfluxDB.class)).hasSize(1); + assertThat(context).hasSingleBean(InfluxDB.class); int readTimeout = getReadTimeoutProperty(context); assertThat(readTimeout).isEqualTo(10_000); }); @@ -70,15 +70,25 @@ void influxDbCanBeCreatedWithoutCredentials() { void influxDbWithOkHttpClientBuilderProvider() { this.contextRunner.withUserConfiguration(CustomOkHttpClientBuilderProviderConfig.class) .withPropertyValues("spring.influx.url=http://localhost").run((context) -> { - assertThat(context.getBeansOfType(InfluxDB.class)).hasSize(1); + assertThat(context).hasSingleBean(InfluxDB.class); int readTimeout = getReadTimeoutProperty(context); assertThat(readTimeout).isEqualTo(40_000); }); } + @Test + void influxDbWithCustomizer() { + this.contextRunner.withBean(InfluxDbCustomizer.class, () -> (influxDb) -> influxDb.setDatabase("test")) + .withPropertyValues("spring.influx.url=http://localhost").run((context) -> { + assertThat(context).hasSingleBean(InfluxDB.class); + InfluxDB influxDb = context.getBean(InfluxDB.class); + assertThat(influxDb).hasFieldOrPropertyWithValue("database", "test"); + }); + } + private int getReadTimeoutProperty(AssertableApplicationContext context) { - InfluxDB influxDB = context.getBean(InfluxDB.class); - Retrofit retrofit = (Retrofit) ReflectionTestUtils.getField(influxDB, "retrofit"); + InfluxDB influxDb = context.getBean(InfluxDB.class); + Retrofit retrofit = (Retrofit) ReflectionTestUtils.getField(influxDb, "retrofit"); OkHttpClient callFactory = (OkHttpClient) retrofit.callFactory(); return callFactory.readTimeoutMillis(); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/info/ProjectInfoAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/info/ProjectInfoAutoConfigurationTests.java index 37325b654fbf..d4d79ed6421f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/info/ProjectInfoAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/info/ProjectInfoAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,7 +37,7 @@ */ class ProjectInfoAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration( + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration( AutoConfigurations.of(PropertyPlaceholderAutoConfiguration.class, ProjectInfoAutoConfiguration.class)); @Test diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/integration/IntegrationAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/integration/IntegrationAutoConfigurationTests.java index b006eb048fc3..9814cb0d372f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/integration/IntegrationAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/integration/IntegrationAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,14 +18,23 @@ import javax.management.MBeanServer; +import io.rsocket.transport.ClientTransport; +import io.rsocket.transport.netty.client.TcpClientTransport; import org.junit.jupiter.api.Test; +import org.springframework.beans.DirectFieldAccessor; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration; import org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration.IntegrationComponentScanConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration; import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration; +import org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration; +import org.springframework.boot.autoconfigure.rsocket.RSocketRequesterAutoConfiguration; +import org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration; +import org.springframework.boot.autoconfigure.rsocket.RSocketStrategiesAutoConfiguration; +import org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration; import org.springframework.boot.jdbc.DataSourceInitializationMode; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; @@ -34,14 +43,20 @@ import org.springframework.integration.annotation.IntegrationComponentScan; import org.springframework.integration.annotation.MessagingGateway; import org.springframework.integration.config.IntegrationManagementConfigurer; -import org.springframework.integration.core.MessageSource; +import org.springframework.integration.context.IntegrationContextUtils; import org.springframework.integration.endpoint.MessageProcessorMessageSource; import org.springframework.integration.gateway.RequestReplyExchanger; -import org.springframework.integration.handler.MessageProcessor; +import org.springframework.integration.rsocket.ClientRSocketConnector; +import org.springframework.integration.rsocket.IntegrationRSocketEndpoint; +import org.springframework.integration.rsocket.ServerRSocketConnector; +import org.springframework.integration.rsocket.ServerRSocketMessageHandler; import org.springframework.integration.support.channel.HeaderChannelRegistry; import org.springframework.jdbc.BadSqlGrammarException; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jmx.export.MBeanExporter; +import org.springframework.messaging.Message; +import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler; +import org.springframework.scheduling.TaskScheduler; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -56,7 +71,7 @@ */ class IntegrationAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(JmxAutoConfiguration.class, IntegrationAutoConfiguration.class)); @Test @@ -149,6 +164,27 @@ void integrationJdbcDataSourceInitializerEnabled() { }); } + @Test + void whenIntegrationJdbcDataSourceInitializerIsEnabledThenFlywayCanBeUsed() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withConfiguration(AutoConfigurations.of(DataSourceTransactionManagerAutoConfiguration.class, + JdbcTemplateAutoConfiguration.class, IntegrationAutoConfiguration.class, + FlywayAutoConfiguration.class)) + .withPropertyValues("spring.datasource.generate-unique-name=true", + "spring.integration.jdbc.initialize-schema=always") + .run((context) -> { + IntegrationProperties properties = context.getBean(IntegrationProperties.class); + assertThat(properties.getJdbc().getInitializeSchema()) + .isEqualTo(DataSourceInitializationMode.ALWAYS); + JdbcOperations jdbc = context.getBean(JdbcOperations.class); + assertThat(jdbc.queryForList("select * from INT_MESSAGE")).isEmpty(); + assertThat(jdbc.queryForList("select * from INT_GROUP_TO_MESSAGE")).isEmpty(); + assertThat(jdbc.queryForList("select * from INT_MESSAGE_GROUP")).isEmpty(); + assertThat(jdbc.queryForList("select * from INT_LOCK")).isEmpty(); + assertThat(jdbc.queryForList("select * from INT_CHANNEL_MESSAGE")).isEmpty(); + }); + } + @Test void integrationJdbcDataSourceInitializerDisabled() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) @@ -181,13 +217,144 @@ void integrationJdbcDataSourceInitializerEnabledByDefaultWithEmbeddedDb() { } @Test - void integrationEnablesDefaultCounts() { - this.contextRunner.withUserConfiguration(MessageSourceConfiguration.class).run((context) -> { - assertThat(context).hasBean("myMessageSource"); - assertThat(((MessageProcessorMessageSource) context.getBean("myMessageSource")).isCountsEnabled()).isTrue(); + void rsocketSupportEnabled() { + this.contextRunner.withUserConfiguration(RSocketServerConfiguration.class) + .withConfiguration(AutoConfigurations.of(RSocketServerAutoConfiguration.class, + RSocketStrategiesAutoConfiguration.class, RSocketMessagingAutoConfiguration.class, + RSocketRequesterAutoConfiguration.class, IntegrationAutoConfiguration.class)) + .withPropertyValues("spring.rsocket.server.port=0", "spring.integration.rsocket.client.port=0", + "spring.integration.rsocket.client.host=localhost", + "spring.integration.rsocket.server.message-mapping-enabled=true") + .run((context) -> { + assertThat(context).hasSingleBean(ClientRSocketConnector.class).hasBean("clientRSocketConnector") + .hasSingleBean(ServerRSocketConnector.class) + .hasSingleBean(ServerRSocketMessageHandler.class) + .hasSingleBean(RSocketMessageHandler.class); + + ServerRSocketMessageHandler serverRSocketMessageHandler = context + .getBean(ServerRSocketMessageHandler.class); + assertThat(context).getBean(RSocketMessageHandler.class).isSameAs(serverRSocketMessageHandler); + + ClientRSocketConnector clientRSocketConnector = context.getBean(ClientRSocketConnector.class); + ClientTransport clientTransport = (ClientTransport) new DirectFieldAccessor(clientRSocketConnector) + .getPropertyValue("clientTransport"); + + assertThat(clientTransport).isInstanceOf(TcpClientTransport.class); + }); + } + + @Test + void taskSchedulerIsNotOverridden() { + this.contextRunner.withConfiguration(AutoConfigurations.of(TaskSchedulingAutoConfiguration.class)) + .withPropertyValues("spring.task.scheduling.thread-name-prefix=integration-scheduling-", + "spring.task.scheduling.pool.size=3") + .run((context) -> { + assertThat(context).hasSingleBean(TaskScheduler.class); + assertThat(context).getBean(IntegrationContextUtils.TASK_SCHEDULER_BEAN_NAME, TaskScheduler.class) + .hasFieldOrPropertyWithValue("threadNamePrefix", "integration-scheduling-") + .hasFieldOrPropertyWithValue("scheduledExecutor.corePoolSize", 3); + }); + } + + @Test + void taskSchedulerCanBeCustomized() { + TaskScheduler customTaskScheduler = mock(TaskScheduler.class); + this.contextRunner.withConfiguration(AutoConfigurations.of(TaskSchedulingAutoConfiguration.class)) + .withBean(IntegrationContextUtils.TASK_SCHEDULER_BEAN_NAME, TaskScheduler.class, + () -> customTaskScheduler) + .run((context) -> { + assertThat(context).hasSingleBean(TaskScheduler.class); + assertThat(context).getBean(IntegrationContextUtils.TASK_SCHEDULER_BEAN_NAME) + .isSameAs(customTaskScheduler); + }); + } + + @Test + void integrationGlobalPropertiesAutoConfigured() { + this.contextRunner.withPropertyValues("spring.integration.channel.auto-create=false", + "spring.integration.channel.max-unicast-subscribers=2", + "spring.integration.channel.max-broadcast-subscribers=3", + "spring.integration.error.require-subscribers=false", "spring.integration.error.ignore-failures=false", + "spring.integration.endpoint.throw-exception-on-late-reply=true", + "spring.integration.endpoint.read-only-headers=ignoredHeader", + "spring.integration.endpoint.no-auto-startup=notStartedEndpoint,_org.springframework.integration.errorLogger") + .run((context) -> { + assertThat(context) + .hasSingleBean(org.springframework.integration.context.IntegrationProperties.class); + org.springframework.integration.context.IntegrationProperties integrationProperties = context + .getBean(org.springframework.integration.context.IntegrationProperties.class); + assertThat(integrationProperties.isChannelsAutoCreate()).isFalse(); + assertThat(integrationProperties.getChannelsMaxUnicastSubscribers()).isEqualTo(2); + assertThat(integrationProperties.getChannelsMaxBroadcastSubscribers()).isEqualTo(3); + assertThat(integrationProperties.isErrorChannelRequireSubscribers()).isFalse(); + assertThat(integrationProperties.isErrorChannelIgnoreFailures()).isFalse(); + assertThat(integrationProperties.isMessagingTemplateThrowExceptionOnLateReply()).isTrue(); + assertThat(integrationProperties.getReadOnlyHeaders()).containsOnly("ignoredHeader"); + assertThat(integrationProperties.getNoAutoStartupEndpoints()).containsOnly("notStartedEndpoint", + "_org.springframework.integration.errorLogger"); + }); + } + + @Test + void integrationGlobalPropertiesUseConsistentDefault() { + org.springframework.integration.context.IntegrationProperties defaultIntegrationProperties = new org.springframework.integration.context.IntegrationProperties(); + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(org.springframework.integration.context.IntegrationProperties.class); + org.springframework.integration.context.IntegrationProperties integrationProperties = context + .getBean(org.springframework.integration.context.IntegrationProperties.class); + assertThat(integrationProperties.isChannelsAutoCreate()) + .isEqualTo(defaultIntegrationProperties.isChannelsAutoCreate()); + assertThat(integrationProperties.getChannelsMaxUnicastSubscribers()) + .isEqualTo(defaultIntegrationProperties.getChannelsMaxBroadcastSubscribers()); + assertThat(integrationProperties.getChannelsMaxBroadcastSubscribers()) + .isEqualTo(defaultIntegrationProperties.getChannelsMaxBroadcastSubscribers()); + assertThat(integrationProperties.isErrorChannelRequireSubscribers()) + .isEqualTo(defaultIntegrationProperties.isErrorChannelIgnoreFailures()); + assertThat(integrationProperties.isErrorChannelIgnoreFailures()) + .isEqualTo(defaultIntegrationProperties.isErrorChannelIgnoreFailures()); + assertThat(integrationProperties.isMessagingTemplateThrowExceptionOnLateReply()) + .isEqualTo(defaultIntegrationProperties.isMessagingTemplateThrowExceptionOnLateReply()); + assertThat(integrationProperties.getReadOnlyHeaders()) + .isEqualTo(defaultIntegrationProperties.getReadOnlyHeaders()); + assertThat(integrationProperties.getNoAutoStartupEndpoints()) + .isEqualTo(defaultIntegrationProperties.getNoAutoStartupEndpoints()); }); } + @Test + void integrationGlobalPropertiesUserBeanOverridesAutoConfiguration() { + org.springframework.integration.context.IntegrationProperties userIntegrationProperties = new org.springframework.integration.context.IntegrationProperties(); + this.contextRunner.withPropertyValues() + .withBean(IntegrationContextUtils.INTEGRATION_GLOBAL_PROPERTIES_BEAN_NAME, + org.springframework.integration.context.IntegrationProperties.class, + () -> userIntegrationProperties) + .run((context) -> { + assertThat(context) + .hasSingleBean(org.springframework.integration.context.IntegrationProperties.class); + assertThat(context.getBean(org.springframework.integration.context.IntegrationProperties.class)) + .isSameAs(userIntegrationProperties); + }); + } + + @Test + void integrationGlobalPropertiesFromSpringIntegrationPropertiesFile() { + this.contextRunner + .withPropertyValues("spring.integration.channel.auto-create=false", + "spring.integration.endpoint.read-only-headers=ignoredHeader") + .withInitializer((applicationContext) -> new IntegrationPropertiesEnvironmentPostProcessor() + .postProcessEnvironment(applicationContext.getEnvironment(), null)) + .run((context) -> { + assertThat(context) + .hasSingleBean(org.springframework.integration.context.IntegrationProperties.class); + org.springframework.integration.context.IntegrationProperties integrationProperties = context + .getBean(org.springframework.integration.context.IntegrationProperties.class); + assertThat(integrationProperties.isChannelsAutoCreate()).isFalse(); + assertThat(integrationProperties.getReadOnlyHeaders()).containsOnly("ignoredHeader"); + // See META-INF/spring.integration.properties + assertThat(integrationProperties.getNoAutoStartupEndpoints()).containsOnly("testService*"); + }); + } + @Configuration(proxyBeanMethods = false) static class CustomMBeanExporter { @@ -214,8 +381,31 @@ interface TestGateway extends RequestReplyExchanger { static class MessageSourceConfiguration { @Bean - MessageSource myMessageSource() { - return new MessageProcessorMessageSource(mock(MessageProcessor.class)); + org.springframework.integration.core.MessageSource myMessageSource() { + return new MessageProcessorMessageSource( + mock(org.springframework.integration.handler.MessageProcessor.class)); + } + + } + + @Configuration(proxyBeanMethods = false) + static class RSocketServerConfiguration { + + @Bean + IntegrationRSocketEndpoint mockIntegrationRSocketEndpoint() { + return new IntegrationRSocketEndpoint() { + + @Override + public reactor.core.publisher.Mono handleMessage(Message message) { + return null; + } + + @Override + public String[] getPath() { + return new String[] { "/rsocketTestPath" }; + } + + }; } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/integration/IntegrationDataSourceInitializerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/integration/IntegrationDataSourceInitializerTests.java new file mode 100644 index 000000000000..fff4b1e695de --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/integration/IntegrationDataSourceInitializerTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.integration; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.DefaultResourceLoader; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link IntegrationDataSourceInitializer}. + * + * @author Stephane Nicoll + */ +class IntegrationDataSourceInitializerTests { + + @Test + void getDatabaseNameWithPlatformDoesNotTouchDataSource() { + DataSource dataSource = mock(DataSource.class); + IntegrationProperties properties = new IntegrationProperties(); + properties.getJdbc().setPlatform("test"); + IntegrationDataSourceInitializer initializer = new IntegrationDataSourceInitializer(dataSource, + new DefaultResourceLoader(), properties); + assertThat(initializer.getDatabaseName()).isEqualTo("test"); + then(dataSource).shouldHaveNoInteractions(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/integration/IntegrationPropertiesEnvironmentPostProcessorTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/integration/IntegrationPropertiesEnvironmentPostProcessorTests.java new file mode 100644 index 000000000000..d5961921ecd7 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/integration/IntegrationPropertiesEnvironmentPostProcessorTests.java @@ -0,0 +1,125 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.integration; + +import java.io.FileNotFoundException; +import java.util.Collections; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.origin.Origin; +import org.springframework.boot.origin.OriginLookup; +import org.springframework.boot.origin.TextResourceOrigin; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link IntegrationPropertiesEnvironmentPostProcessor}. + * + * @author Stephane Nicoll + */ +class IntegrationPropertiesEnvironmentPostProcessorTests { + + @Test + void postProcessEnvironmentAddPropertySource() { + ConfigurableEnvironment environment = new StandardEnvironment(); + new IntegrationPropertiesEnvironmentPostProcessor().postProcessEnvironment(environment, + mock(SpringApplication.class)); + assertThat(environment.getPropertySources().contains("META-INF/spring.integration.properties")).isTrue(); + assertThat(environment.getProperty("spring.integration.endpoint.no-auto-startup")).isEqualTo("testService*"); + } + + @Test + void postProcessEnvironmentAddPropertySourceLast() { + ConfigurableEnvironment environment = new StandardEnvironment(); + environment.getPropertySources().addLast(new MapPropertySource("test", + Collections.singletonMap("spring.integration.endpoint.no-auto-startup", "another*"))); + new IntegrationPropertiesEnvironmentPostProcessor().postProcessEnvironment(environment, + mock(SpringApplication.class)); + assertThat(environment.getPropertySources().contains("META-INF/spring.integration.properties")).isTrue(); + assertThat(environment.getProperty("spring.integration.endpoint.no-auto-startup")).isEqualTo("another*"); + } + + @Test + void registerIntegrationPropertiesPropertySourceWithUnknownResourceThrowsException() { + ConfigurableEnvironment environment = new StandardEnvironment(); + ClassPathResource unknown = new ClassPathResource("does-not-exist.properties", getClass()); + assertThatThrownBy(() -> new IntegrationPropertiesEnvironmentPostProcessor() + .registerIntegrationPropertiesPropertySource(environment, unknown)) + .isInstanceOf(IllegalStateException.class).hasCauseInstanceOf(FileNotFoundException.class) + .hasMessageContaining(unknown.toString()); + } + + @Test + void registerIntegrationPropertiesPropertySourceWithResourceAddPropertySource() { + ConfigurableEnvironment environment = new StandardEnvironment(); + new IntegrationPropertiesEnvironmentPostProcessor().registerIntegrationPropertiesPropertySource(environment, + new ClassPathResource("spring.integration.properties", getClass())); + assertThat(environment.getProperty("spring.integration.channel.auto-create", Boolean.class)).isFalse(); + assertThat(environment.getProperty("spring.integration.channel.max-unicast-subscribers", Integer.class)) + .isEqualTo(4); + assertThat(environment.getProperty("spring.integration.channel.max-broadcast-subscribers", Integer.class)) + .isEqualTo(6); + assertThat(environment.getProperty("spring.integration.error.require-subscribers", Boolean.class)).isFalse(); + assertThat(environment.getProperty("spring.integration.error.ignore-failures", Boolean.class)).isFalse(); + assertThat(environment.getProperty("spring.integration.endpoint.throw-exception-on-late-reply", Boolean.class)) + .isTrue(); + assertThat(environment.getProperty("spring.integration.endpoint.read-only-headers", String.class)) + .isEqualTo("header1,header2"); + assertThat(environment.getProperty("spring.integration.endpoint.no-auto-startup", String.class)) + .isEqualTo("testService,anotherService"); + } + + @Test + @SuppressWarnings("unchecked") + void registerIntegrationPropertiesPropertySourceWithResourceCanRetrieveOrigin() { + ConfigurableEnvironment environment = new StandardEnvironment(); + ClassPathResource resource = new ClassPathResource("spring.integration.properties", getClass()); + new IntegrationPropertiesEnvironmentPostProcessor().registerIntegrationPropertiesPropertySource(environment, + resource); + PropertySource ps = environment.getPropertySources().get("META-INF/spring.integration.properties"); + assertThat(ps).isNotNull().isInstanceOf(OriginLookup.class); + OriginLookup originLookup = (OriginLookup) ps; + assertThat(originLookup.getOrigin("spring.integration.channel.auto-create")) + .satisfies(textOrigin(resource, 0, 39)); + assertThat(originLookup.getOrigin("spring.integration.channel.max-unicast-subscribers")) + .satisfies(textOrigin(resource, 1, 50)); + assertThat(originLookup.getOrigin("spring.integration.channel.max-broadcast-subscribers")) + .satisfies(textOrigin(resource, 2, 52)); + } + + private Consumer textOrigin(Resource resource, int line, int column) { + return (origin) -> { + assertThat(origin).isInstanceOf(TextResourceOrigin.class); + TextResourceOrigin textOrigin = (TextResourceOrigin) origin; + assertThat(textOrigin.getResource()).isEqualTo(resource); + assertThat(textOrigin.getLocation().getLine()).isEqualTo(line); + assertThat(textOrigin.getLocation().getColumn()).isEqualTo(column); + }; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/Jackson211AutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/Jackson211AutoConfigurationTests.java new file mode 100644 index 000000000000..dda24d1cace0 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/Jackson211AutoConfigurationTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jackson; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.testsupport.classpath.ClassPathExclusions; +import org.springframework.boot.testsupport.classpath.ClassPathOverrides; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JacksonAutoConfiguration} using Jackson 2.11.x + * + * @author Stephane Nicoll + */ +@ClassPathExclusions({ "jackson-databind*.jar", "jackson-dataformat-xml*.jar" }) +@ClassPathOverrides({ "com.fasterxml.jackson.core:jackson-databind:2.11.3", + "com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.11.3" }) +class Jackson211AutoConfigurationTests extends JacksonAutoConfigurationTests { + + public static final String STRATEGY_CLASS_NAME = "com.fasterxml.jackson.databind.PropertyNamingStrategy$SnakeCaseStrategy"; + + @Test + void customPropertyNamingStrategyField() { + this.contextRunner.withPropertyValues("spring.jackson.property-naming-strategy:SNAKE_CASE").run((context) -> { + ObjectMapper mapper = context.getBean(ObjectMapper.class); + assertThat(mapper.getPropertyNamingStrategy().getClass().getName()).isEqualTo(STRATEGY_CLASS_NAME); + }); + } + + @Test + void customPropertyNamingStrategyClass() { + this.contextRunner.withPropertyValues( + "spring.jackson.property-naming-strategy:com.fasterxml.jackson.databind.PropertyNamingStrategy.SnakeCaseStrategy") + .run((context) -> { + ObjectMapper mapper = context.getBean(ObjectMapper.class); + assertThat(mapper.getPropertyNamingStrategy().getClass().getName()).isEqualTo(STRATEGY_CLASS_NAME); + }); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java index 85f751f43518..6c770b029887 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.io.IOException; import java.text.DateFormat; import java.text.SimpleDateFormat; +import java.time.Duration; import java.util.Date; import java.util.HashSet; import java.util.Set; @@ -28,7 +29,6 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.ObjectCodec; import com.fasterxml.jackson.databind.AnnotationIntrospector; import com.fasterxml.jackson.databind.DeserializationConfig; @@ -37,16 +37,12 @@ import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.PropertyNamingStrategy.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.util.StdDateFormat; -import com.fasterxml.jackson.datatype.joda.cfg.FormatConfig; import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; -import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; -import org.joda.time.LocalDateTime; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -76,18 +72,9 @@ */ class JacksonAutoConfigurationTests { - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + protected final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class)); - @Test - @Deprecated - void registersJodaModuleAutomatically() { - this.contextRunner.run((context) -> { - ObjectMapper objectMapper = context.getBean(ObjectMapper.class); - assertThat(objectMapper.canSerialize(LocalDateTime.class)).isTrue(); - }); - } - @Test void doubleModuleRegistration() { this.contextRunner.withUserConfiguration(DoubleModulesConfig.class) @@ -116,19 +103,6 @@ void customDateFormat() { }); } - @Test - @Deprecated - void customJodaDateTimeFormat() throws Exception { - this.contextRunner.withPropertyValues("spring.jackson.date-format:yyyyMMddHHmmss", - "spring.jackson.joda-date-time-format:yyyy-MM-dd HH:mm:ss").run((context) -> { - ObjectMapper mapper = context.getBean(ObjectMapper.class); - DateTime dateTime = new DateTime(1988, 6, 25, 20, 30, DateTimeZone.UTC); - assertThat(mapper.writeValueAsString(dateTime)).isEqualTo("\"1988-06-25 20:30:00\""); - Date date = dateTime.toDate(); - assertThat(mapper.writeValueAsString(date)).isEqualTo("\"19880625203000\""); - }); - } - @Test void customDateFormatClass() { this.contextRunner.withPropertyValues( @@ -158,7 +132,7 @@ void customPropertyNamingStrategyField() { @Test void customPropertyNamingStrategyClass() { this.contextRunner.withPropertyValues( - "spring.jackson.property-naming-strategy:com.fasterxml.jackson.databind.PropertyNamingStrategy.SnakeCaseStrategy") + "spring.jackson.property-naming-strategy:com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy") .run((context) -> { ObjectMapper mapper = context.getBean(ObjectMapper.class); assertThat(mapper.getPropertyNamingStrategy()).isInstanceOf(SnakeCaseStrategy.class); @@ -256,11 +230,12 @@ void disableParserFeature() { @Test void enableGeneratorFeature() { - this.contextRunner.withPropertyValues("spring.jackson.generator.write_numbers_as_strings:true") + this.contextRunner.withPropertyValues("spring.jackson.generator.strict_duplicate_detection:true") .run((context) -> { ObjectMapper mapper = context.getBean(ObjectMapper.class); - assertThat(JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS.enabledByDefault()).isFalse(); - assertThat(mapper.getFactory().isEnabled(JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS)).isTrue(); + JsonGenerator.Feature feature = JsonGenerator.Feature.STRICT_DUPLICATE_DETECTION; + assertThat(feature.enabledByDefault()).isFalse(); + assertThat(mapper.getFactory().isEnabled(feature)).isTrue(); }); } @@ -293,8 +268,7 @@ void defaultObjectMapperBuilder() { void moduleBeansAndWellKnownModulesAreRegisteredWithTheObjectMapperBuilder() { this.contextRunner.withUserConfiguration(ModuleConfig.class).run((context) -> { ObjectMapper objectMapper = context.getBean(Jackson2ObjectMapperBuilder.class).build(); - assertThat(context.getBean(CustomModule.class).getOwners()).contains((ObjectCodec) objectMapper); - assertThat(objectMapper.canSerialize(LocalDateTime.class)).isTrue(); + assertThat(context.getBean(CustomModule.class).getOwners()).contains(objectMapper); assertThat(objectMapper.canSerialize(Baz.class)).isTrue(); }); } @@ -318,17 +292,7 @@ void customSerializationInclusion() { } @Test - void customTimeZoneFormattingADateTime() { - this.contextRunner.withPropertyValues("spring.jackson.time-zone:America/Los_Angeles", - "spring.jackson.date-format:zzzz", "spring.jackson.locale:en").run((context) -> { - ObjectMapper objectMapper = context.getBean(Jackson2ObjectMapperBuilder.class).build(); - DateTime dateTime = new DateTime(1436966242231L, DateTimeZone.UTC); - assertThat(objectMapper.writeValueAsString(dateTime)).isEqualTo("\"Pacific Daylight Time\""); - }); - } - - @Test - void customTimeZoneFormattingADate() throws JsonProcessingException { + void customTimeZoneFormattingADate() { this.contextRunner.withPropertyValues("spring.jackson.time-zone:GMT+10", "spring.jackson.date-format:z") .run((context) -> { ObjectMapper objectMapper = context.getBean(Jackson2ObjectMapperBuilder.class).build(); @@ -337,17 +301,6 @@ void customTimeZoneFormattingADate() throws JsonProcessingException { }); } - @Test - @Deprecated - void customLocaleWithJodaTime() throws JsonProcessingException { - this.contextRunner.withPropertyValues("spring.jackson.locale:de_DE", "spring.jackson.date-format:zzzz", - "spring.jackson.serialization.write-dates-with-zone-id:true").run((context) -> { - ObjectMapper objectMapper = context.getBean(ObjectMapper.class); - DateTime jodaTime = new DateTime(1478424650000L, DateTimeZone.forID("Europe/Rome")); - assertThat(objectMapper.writeValueAsString(jodaTime)).startsWith("\"Mitteleuropäische "); - }); - } - @Test void additionalJacksonBuilderCustomization() { this.contextRunner.withUserConfiguration(ObjectMapperBuilderCustomConfig.class).run((context) -> { @@ -368,13 +321,11 @@ void customParameterNamesModuleCanBeConfigured() { } @Test - void writeDatesAsTimestampsDefault() { + void writeDurationAsTimestampsDefault() { this.contextRunner.run((context) -> { ObjectMapper mapper = context.getBean(ObjectMapper.class); - DateTime dateTime = new DateTime(1988, 6, 25, 20, 30, DateTimeZone.UTC); - String expected = FormatConfig.DEFAULT_DATETIME_PRINTER.rawFormatter().withZone(DateTimeZone.UTC) - .print(dateTime); - assertThat(mapper.writeValueAsString(dateTime)).isEqualTo("\"" + expected + "\""); + Duration duration = Duration.ofHours(2); + assertThat(mapper.writeValueAsString(duration)).isEqualTo("\"PT2H\""); }); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationTests.java index 109c05b296db..7cc2f74cf2d6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,11 +27,14 @@ import java.util.Properties; import java.util.Random; import java.util.function.Consumer; +import java.util.function.Function; import java.util.logging.Logger; import javax.sql.DataSource; import com.zaxxer.hikari.HikariDataSource; +import io.r2dbc.spi.ConnectionFactory; +import oracle.ucp.jdbc.PoolDataSourceImpl; import org.apache.commons.dbcp2.BasicDataSource; import org.junit.jupiter.api.Test; @@ -40,6 +43,8 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.jdbc.DatabaseDriver; import org.springframework.boot.jdbc.EmbeddedDatabaseConnection; +import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer; +import org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitialization; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -47,6 +52,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.SimpleDriverDataSource; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -62,8 +68,7 @@ class DataSourceAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) - .withPropertyValues("spring.datasource.initialization-mode=never", - "spring.datasource.url:jdbc:hsqldb:mem:testdb-" + new Random().nextInt()); + .withPropertyValues("spring.datasource.url:jdbc:hsqldb:mem:testdb-" + new Random().nextInt()); @Test void testDefaultDataSourceExists() { @@ -93,6 +98,12 @@ void testBadDriverClass() { .hasMessageContaining("org.none.jdbcDriver")); } + @Test + void datasourceWhenConnectionFactoryPresentIsNotAutoConfigured() { + this.contextRunner.withBean(ConnectionFactory.class, () -> mock(ConnectionFactory.class)) + .run((context) -> assertThat(context).doesNotHaveBean(DataSource.class)); + } + @Test void hikariValidatesConnectionByDefault() { assertDataSource(HikariDataSource.class, Collections.singletonList("org.apache.tomcat"), (dataSource) -> @@ -126,8 +137,25 @@ void commonsDbcp2ValidatesConnectionByDefault() { assertDataSource(org.apache.commons.dbcp2.BasicDataSource.class, Arrays.asList("com.zaxxer.hikari", "org.apache.tomcat"), (dataSource) -> { assertThat(dataSource.getTestOnBorrow()).isTrue(); - assertThat(dataSource.getValidationQuery()).isNull(); // Use - // Connection#isValid() + // Use Connection#isValid() + assertThat(dataSource.getValidationQuery()).isNull(); + }); + } + + @Test + void oracleUcpIsFallback() { + assertDataSource(PoolDataSourceImpl.class, + Arrays.asList("com.zaxxer.hikari", "org.apache.tomcat", "org.apache.commons.dbcp2"), + (dataSource) -> assertThat(dataSource.getURL()).startsWith("jdbc:hsqldb:mem:testdb")); + } + + @Test + void oracleUcpValidatesConnectionByDefault() { + assertDataSource(PoolDataSourceImpl.class, + Arrays.asList("com.zaxxer.hikari", "org.apache.tomcat", "org.apache.commons.dbcp2"), (dataSource) -> { + assertThat(dataSource.getValidateConnectionOnBorrow()).isTrue(); + // Use an internal ping when using an Oracle JDBC driver + assertThat(dataSource.getSQLForValidateConnection()).isNull(); }); } @@ -143,15 +171,20 @@ void testEmbeddedTypeDefaultsUsername() { }); } + @Test + void dataSourceWhenNoConnectionPoolsAreAvailableWithUrlDoesNotCreateDataSource() { + this.contextRunner.with(hideConnectionPools()) + .withPropertyValues("spring.datasource.url:jdbc:hsqldb:mem:testdb") + .run((context) -> assertThat(context).doesNotHaveBean(DataSource.class)); + } + /** * This test makes sure that if no supported data source is present, a datasource is * still created if "spring.datasource.type" is present. */ @Test - void explicitTypeNoSupportedDataSource() { - this.contextRunner - .withClassLoader(new FilteredClassLoader("org.apache.tomcat", "com.zaxxer.hikari", - "org.apache.commons.dbcp", "org.apache.commons.dbcp2")) + void dataSourceWhenNoConnectionPoolsAreAvailableWithUrlAndTypeCreatesDataSource() { + this.contextRunner.with(hideConnectionPools()) .withPropertyValues("spring.datasource.driverClassName:org.hsqldb.jdbcDriver", "spring.datasource.url:jdbc:hsqldb:mem:testdb", "spring.datasource.type:" + SimpleDriverDataSource.class.getName()) @@ -190,11 +223,37 @@ void testDefaultDataSourceCanBeOverridden() { } @Test + void whenThereIsAUserProvidedDataSourceAnUnresolvablePlaceholderDoesNotCauseAProblem() { + this.contextRunner.withUserConfiguration(TestDataSourceConfiguration.class) + .withPropertyValues("spring.datasource.url:${UNRESOLVABLE_PLACEHOLDER}") + .run((context) -> assertThat(context).getBean(DataSource.class).isInstanceOf(BasicDataSource.class)); + } + + @Test + void whenThereIsAnEmptyUserProvidedDataSource() { + this.contextRunner.with(hideConnectionPools()).withPropertyValues("spring.datasource.url:") + .run((context) -> assertThat(context).getBean(DataSource.class).isInstanceOf(EmbeddedDatabase.class)); + } + + @Test + @Deprecated void testDataSourceIsInitializedEarly() { this.contextRunner.withUserConfiguration(TestInitializedDataSourceConfiguration.class) - .withPropertyValues("spring.datasource.initialization-mode=always") - .run((context) -> assertThat(context.getBean(TestInitializedDataSourceConfiguration.class).called) - .isTrue()); + .withPropertyValues("spring.datasource.initialization-mode=always").run((context) -> { + assertThat(context).hasSingleBean(DataSourceScriptDatabaseInitializer.class); + assertThat(context.getBean(TestInitializedDataSourceConfiguration.class).called).isTrue(); + }); + } + + @Test + void whenNoInitializationRelatedSpringDataSourcePropertiesAreConfiguredThenInitializationBacksOff() { + this.contextRunner + .run((context) -> assertThat(context).doesNotHaveBean(DataSourceScriptDatabaseInitializer.class)); + } + + private static Function hideConnectionPools() { + return (runner) -> runner.withClassLoader(new FilteredClassLoader("org.apache.tomcat", "com.zaxxer.hikari", + "org.apache.commons.dbcp2", "oracle.ucp.jdbc")); } private void assertDataSource(Class expectedType, List hiddenPackages, @@ -224,6 +283,7 @@ DataSource dataSource() { } @Configuration(proxyBeanMethods = false) + @DependsOnDatabaseInitialization static class TestInitializedDataSourceConfiguration { private boolean called; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializationIntegrationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializationIntegrationTests.java new file mode 100644 index 000000000000..c8a8d279edfe --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializationIntegrationTests.java @@ -0,0 +1,440 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jdbc; + +import java.io.IOException; +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Logger; + +import javax.sql.DataSource; + +import com.zaxxer.hikari.HikariDataSource; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.ContextConsumer; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; +import org.springframework.context.support.SimpleThreadScope; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternUtils; +import org.springframework.jdbc.BadSqlGrammarException; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Integration tests for DataSource initialization. + * + * @author Dave Syer + * @author Stephane Nicoll + */ +@Deprecated +class DataSourceInitializationIntegrationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) + .withPropertyValues("spring.datasource.initialization-mode=never", + "spring.datasource.url:jdbc:hsqldb:mem:init-" + UUID.randomUUID()); + + @Test + void dataSourceInitialized() { + this.contextRunner.withPropertyValues("spring.datasource.initialization-mode:always").run((context) -> { + assertThat(context).hasSingleBean(DataSource.class); + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).isInstanceOf(HikariDataSource.class); + assertDataSourceIsInitialized(dataSource); + }); + } + + @Test + void initializationAppliesToCustomDataSource() { + this.contextRunner.withUserConfiguration(OneDataSource.class) + .withPropertyValues("spring.datasource.initialization-mode:always").run((context) -> { + assertThat(context).hasSingleBean(DataSource.class); + assertDataSourceIsInitialized(context.getBean(DataSource.class)); + }); + } + + @Test + void initializationWithUsernameAndPasswordAppliesToCustomDataSource() { + this.contextRunner.withUserConfiguration(OneDataSource.class) + .withPropertyValues("spring.datasource.initialization-mode:always", + "spring.datasource.schema-username=test", "spring.datasource.schema-password=secret") + .run((context) -> { + assertThat(context).hasSingleBean(DataSource.class); + assertDataSourceIsInitialized(context.getBean(DataSource.class)); + }); + } + + private void assertDataSourceIsInitialized(DataSource dataSource) { + JdbcOperations template = new JdbcTemplate(dataSource); + assertThat(template.queryForObject("SELECT COUNT(*) from BAR", Integer.class)).isEqualTo(1); + } + + @Test + void dataSourceInitializedWithExplicitScript() { + this.contextRunner.withPropertyValues("spring.datasource.initialization-mode:always", + "spring.datasource.schema:" + getRelativeLocationFor("schema.sql"), + "spring.datasource.data:" + getRelativeLocationFor("data.sql")).run((context) -> { + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).isInstanceOf(HikariDataSource.class); + assertThat(dataSource).isNotNull(); + JdbcOperations template = new JdbcTemplate(dataSource); + assertThat(template.queryForObject("SELECT COUNT(*) from FOO", Integer.class)).isEqualTo(1); + }); + } + + @Test + void dataSourceInitializedWithMultipleScripts() { + this.contextRunner.withPropertyValues("spring.datasource.initialization-mode:always", + "spring.datasource.schema:" + getRelativeLocationFor("schema.sql") + "," + + getRelativeLocationFor("another.sql"), + "spring.datasource.data:" + getRelativeLocationFor("data.sql")).run((context) -> { + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).isInstanceOf(HikariDataSource.class); + assertThat(dataSource).isNotNull(); + JdbcOperations template = new JdbcTemplate(dataSource); + assertThat(template.queryForObject("SELECT COUNT(*) from FOO", Integer.class)).isEqualTo(1); + assertThat(template.queryForObject("SELECT COUNT(*) from SPAM", Integer.class)).isEqualTo(0); + }); + } + + @Test + void dataSourceInitializedWithExplicitSqlScriptEncoding() { + this.contextRunner.withPropertyValues("spring.datasource.initialization-mode:always", + "spring.datasource.sqlScriptEncoding:UTF-8", + "spring.datasource.schema:" + getRelativeLocationFor("encoding-schema.sql"), + "spring.datasource.data:" + getRelativeLocationFor("encoding-data.sql")).run((context) -> { + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).isInstanceOf(HikariDataSource.class); + assertThat(dataSource).isNotNull(); + JdbcOperations template = new JdbcTemplate(dataSource); + assertThat(template.queryForObject("SELECT COUNT(*) from BAR", Integer.class)).isEqualTo(2); + assertThat(template.queryForObject("SELECT name from BAR WHERE id=1", String.class)) + .isEqualTo("bar"); + assertThat(template.queryForObject("SELECT name from BAR WHERE id=2", String.class)) + .isEqualTo("ã°ãƒ¼"); + }); + } + + @Test + void initializationDisabled() { + this.contextRunner.run(assertInitializationIsDisabled()); + } + + @Test + void initializationDoesNotApplyWithSeveralDataSources() { + this.contextRunner.withUserConfiguration(TwoDataSources.class) + .withPropertyValues("spring.datasource.initialization-mode:always").run((context) -> { + assertThat(context.getBeanNamesForType(DataSource.class)).hasSize(2); + assertDataSourceNotInitialized(context.getBean("oneDataSource", DataSource.class)); + assertDataSourceNotInitialized(context.getBean("twoDataSource", DataSource.class)); + }); + } + + private ContextConsumer assertInitializationIsDisabled() { + return (context) -> { + assertThat(context).hasSingleBean(DataSource.class); + DataSource dataSource = context.getBean(DataSource.class); + assertDataSourceNotInitialized(dataSource); + }; + } + + private void assertDataSourceNotInitialized(DataSource dataSource) { + JdbcOperations template = new JdbcTemplate(dataSource); + assertThatExceptionOfType(BadSqlGrammarException.class) + .isThrownBy(() -> template.queryForObject("SELECT COUNT(*) from BAR", Integer.class)) + .satisfies((ex) -> { + SQLException sqlException = ex.getSQLException(); + int expectedCode = -5501; // user lacks privilege or object not found + assertThat(sqlException.getErrorCode()).isEqualTo(expectedCode); + }); + } + + @Test + void dataSourceInitializedWithSchemaCredentials() { + this.contextRunner + .withPropertyValues("spring.datasource.initialization-mode:always", + "spring.datasource.sqlScriptEncoding:UTF-8", + "spring.datasource.schema:" + getRelativeLocationFor("encoding-schema.sql"), + "spring.datasource.data:" + getRelativeLocationFor("encoding-data.sql"), + "spring.datasource.schema-username:admin", "spring.datasource.schema-password:admin") + .run((context) -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()).isInstanceOf(BeanCreationException.class) + .hasMessageContaining("invalid authorization specification"); + context.getStartupFailure().printStackTrace(); + }); + } + + @Test + void dataSourceInitializedWithDataCredentials() { + this.contextRunner + .withPropertyValues("spring.datasource.initialization-mode:always", + "spring.datasource.sqlScriptEncoding:UTF-8", + "spring.datasource.schema:" + getRelativeLocationFor("encoding-schema.sql"), + "spring.datasource.data:" + getRelativeLocationFor("encoding-data.sql"), + "spring.datasource.data-username:admin", "spring.datasource.data-password:admin") + .run((context) -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()).isInstanceOf(BeanCreationException.class) + .hasMessageContaining("invalid authorization specification"); + }); + } + + @Test + void multipleScriptsAppliedInLexicalOrder() { + new ApplicationContextRunner(() -> { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.setResourceLoader(new ReverseOrderResourceLoader(new DefaultResourceLoader())); + return context; + }).withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) + .withPropertyValues("spring.datasource.initialization-mode=always", + "spring.datasource.url:jdbc:hsqldb:mem:testdb-" + new Random().nextInt(), + "spring.datasource.schema:classpath*:" + getRelativeLocationFor("lexical-schema-*.sql"), + "spring.datasource.data:classpath*:" + getRelativeLocationFor("data.sql")) + .run((context) -> { + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).isInstanceOf(HikariDataSource.class); + assertThat(dataSource).isNotNull(); + JdbcOperations template = new JdbcTemplate(dataSource); + assertThat(template.queryForObject("SELECT COUNT(*) from FOO", Integer.class)).isEqualTo(1); + }); + } + + @Test + void testDataSourceInitializedWithInvalidSchemaResource() { + this.contextRunner.withPropertyValues("spring.datasource.initialization-mode:always", + "spring.datasource.schema:classpath:does/not/exist.sql").run((context) -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()).isInstanceOf(BeanCreationException.class); + assertThat(context.getStartupFailure()) + .hasMessageContaining("No schema scripts found at location 'classpath:does/not/exist.sql'"); + }); + } + + @Test + void dataSourceInitializedWithInvalidDataResource() { + this.contextRunner.withPropertyValues("spring.datasource.initialization-mode:always", + "spring.datasource.schema:" + getRelativeLocationFor("schema.sql"), + "spring.datasource.data:classpath:does/not/exist.sql").run((context) -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()).isInstanceOf(BeanCreationException.class); + assertThat(context.getStartupFailure()) + .hasMessageContaining("No data scripts found at location 'classpath:does/not/exist.sql'"); + }); + } + + @Test + void whenDataSourceIsProxiedByABeanPostProcessorThenDataSourceInitializationUsesTheProxy() { + this.contextRunner.withPropertyValues("spring.datasource.initialization-mode:always") + .withUserConfiguration(DataSourceProxyConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(DataSource.class); + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).isInstanceOf(DataSourceProxy.class); + assertThat(((DataSourceProxy) dataSource).connectionsRetrieved).hasPositiveValue(); + assertDataSourceIsInitialized(dataSource); + }); + } + + @Test + // gh-13042 + void whenDataSourceIsScopedAndJpaIsInvolvedThenInitializationCompletesSuccessfully() { + this.contextRunner.withPropertyValues("spring.datasource.initialization-mode:always") + .withConfiguration(AutoConfigurations.of(HibernateJpaAutoConfiguration.class)) + .withUserConfiguration(ScopedDataSourceConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(DataSource.class); + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).isInstanceOf(HikariDataSource.class); + assertDataSourceIsInitialized(dataSource); + }); + } + + private String getRelativeLocationFor(String resource) { + return ClassUtils.addResourcePathToPackagePath(getClass(), resource); + } + + @Configuration(proxyBeanMethods = false) + static class OneDataSource { + + @Bean + DataSource oneDataSource() { + return new TestDataSource(true); + } + + } + + @Configuration(proxyBeanMethods = false) + static class TwoDataSources extends OneDataSource { + + @Bean + DataSource twoDataSource() { + return new TestDataSource(true); + } + + } + + /** + * {@link ResourcePatternResolver} used to ensure consistently wrong resource + * ordering. + */ + static class ReverseOrderResourceLoader implements ResourcePatternResolver { + + private final ResourcePatternResolver resolver; + + ReverseOrderResourceLoader(ResourceLoader loader) { + this.resolver = ResourcePatternUtils.getResourcePatternResolver(loader); + } + + @Override + public Resource getResource(String location) { + return this.resolver.getResource(location); + } + + @Override + public ClassLoader getClassLoader() { + return this.resolver.getClassLoader(); + } + + @Override + public Resource[] getResources(String locationPattern) throws IOException { + Resource[] resources = this.resolver.getResources(locationPattern); + Arrays.sort(resources, Comparator.comparing(Resource::getFilename).reversed()); + return resources; + } + + } + + @Configuration(proxyBeanMethods = true) + static class DataSourceProxyConfiguration { + + @Bean + static BeanPostProcessor dataSourceProxy() { + return new BeanPostProcessor() { + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + if (bean instanceof DataSource) { + return new DataSourceProxy((DataSource) bean); + } + return bean; + } + + }; + } + + } + + static class DataSourceProxy implements DataSource { + + private final AtomicInteger connectionsRetrieved = new AtomicInteger(); + + private final DataSource delegate; + + DataSourceProxy(DataSource delegate) { + this.delegate = delegate; + } + + @Override + public PrintWriter getLogWriter() throws SQLException { + return this.delegate.getLogWriter(); + } + + @Override + public void setLogWriter(PrintWriter out) throws SQLException { + this.delegate.setLogWriter(out); + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + return this.delegate.isWrapperFor(iface); + } + + @Override + public T unwrap(Class iface) throws SQLException { + return this.delegate.unwrap(iface); + } + + @Override + public Connection getConnection() throws SQLException { + this.connectionsRetrieved.incrementAndGet(); + return this.delegate.getConnection(); + } + + @Override + public Connection getConnection(String username, String password) throws SQLException { + this.connectionsRetrieved.incrementAndGet(); + return this.delegate.getConnection(username, password); + } + + @Override + public int getLoginTimeout() throws SQLException { + return this.delegate.getLoginTimeout(); + } + + @Override + public void setLoginTimeout(int seconds) throws SQLException { + this.delegate.setLoginTimeout(seconds); + } + + @Override + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + return this.delegate.getParentLogger(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class ScopedDataSourceConfiguration { + + @Bean + static BeanFactoryPostProcessor fooScope() { + return (beanFactory) -> beanFactory.registerScope("test", new SimpleThreadScope()); + } + + @Bean + @Scope("test") + HikariDataSource dataSource(DataSourceProperties properties) { + return properties.initializeDataSourceBuilder().type(HikariDataSource.class).build(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializerInvokerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializerInvokerTests.java deleted file mode 100644 index 232388412305..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializerInvokerTests.java +++ /dev/null @@ -1,296 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.jdbc; - -import java.io.IOException; -import java.sql.SQLException; -import java.util.Arrays; -import java.util.Comparator; -import java.util.Random; -import java.util.UUID; - -import javax.sql.DataSource; - -import com.zaxxer.hikari.HikariDataSource; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.BeanCreationException; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.assertj.AssertableApplicationContext; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.boot.test.context.runner.ContextConsumer; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.io.DefaultResourceLoader; -import org.springframework.core.io.Resource; -import org.springframework.core.io.ResourceLoader; -import org.springframework.core.io.support.ResourcePatternResolver; -import org.springframework.core.io.support.ResourcePatternUtils; -import org.springframework.jdbc.BadSqlGrammarException; -import org.springframework.jdbc.core.JdbcOperations; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.util.ClassUtils; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -/** - * Tests for {@link DataSourceInitializerInvoker}. - * - * @author Dave Syer - * @author Stephane Nicoll - */ -class DataSourceInitializerInvokerTests { - - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) - .withPropertyValues("spring.datasource.initialization-mode=never", - "spring.datasource.url:jdbc:hsqldb:mem:init-" + UUID.randomUUID()); - - @Test - void dataSourceInitialized() { - this.contextRunner.withPropertyValues("spring.datasource.initialization-mode:always").run((context) -> { - assertThat(context).hasSingleBean(DataSource.class); - DataSource dataSource = context.getBean(DataSource.class); - assertThat(dataSource).isInstanceOf(HikariDataSource.class); - assertDataSourceIsInitialized(dataSource); - }); - } - - @Test - void initializationAppliesToCustomDataSource() { - this.contextRunner.withUserConfiguration(OneDataSource.class) - .withPropertyValues("spring.datasource.initialization-mode:always").run((context) -> { - assertThat(context).hasSingleBean(DataSource.class); - assertDataSourceIsInitialized(context.getBean(DataSource.class)); - }); - } - - private void assertDataSourceIsInitialized(DataSource dataSource) { - JdbcOperations template = new JdbcTemplate(dataSource); - assertThat(template.queryForObject("SELECT COUNT(*) from BAR", Integer.class)).isEqualTo(1); - } - - @Test - void dataSourceInitializedWithExplicitScript() { - this.contextRunner.withPropertyValues("spring.datasource.initialization-mode:always", - "spring.datasource.schema:" + getRelativeLocationFor("schema.sql"), - "spring.datasource.data:" + getRelativeLocationFor("data.sql")).run((context) -> { - DataSource dataSource = context.getBean(DataSource.class); - assertThat(dataSource).isInstanceOf(HikariDataSource.class); - assertThat(dataSource).isNotNull(); - JdbcOperations template = new JdbcTemplate(dataSource); - assertThat(template.queryForObject("SELECT COUNT(*) from FOO", Integer.class)).isEqualTo(1); - }); - } - - @Test - void dataSourceInitializedWithMultipleScripts() { - this.contextRunner.withPropertyValues("spring.datasource.initialization-mode:always", - "spring.datasource.schema:" + getRelativeLocationFor("schema.sql") + "," - + getRelativeLocationFor("another.sql"), - "spring.datasource.data:" + getRelativeLocationFor("data.sql")).run((context) -> { - DataSource dataSource = context.getBean(DataSource.class); - assertThat(dataSource).isInstanceOf(HikariDataSource.class); - assertThat(dataSource).isNotNull(); - JdbcOperations template = new JdbcTemplate(dataSource); - assertThat(template.queryForObject("SELECT COUNT(*) from FOO", Integer.class)).isEqualTo(1); - assertThat(template.queryForObject("SELECT COUNT(*) from SPAM", Integer.class)).isEqualTo(0); - }); - } - - @Test - void dataSourceInitializedWithExplicitSqlScriptEncoding() { - this.contextRunner.withPropertyValues("spring.datasource.initialization-mode:always", - "spring.datasource.sqlScriptEncoding:UTF-8", - "spring.datasource.schema:" + getRelativeLocationFor("encoding-schema.sql"), - "spring.datasource.data:" + getRelativeLocationFor("encoding-data.sql")).run((context) -> { - DataSource dataSource = context.getBean(DataSource.class); - assertThat(dataSource).isInstanceOf(HikariDataSource.class); - assertThat(dataSource).isNotNull(); - JdbcOperations template = new JdbcTemplate(dataSource); - assertThat(template.queryForObject("SELECT COUNT(*) from BAR", Integer.class)).isEqualTo(2); - assertThat(template.queryForObject("SELECT name from BAR WHERE id=1", String.class)) - .isEqualTo("bar"); - assertThat(template.queryForObject("SELECT name from BAR WHERE id=2", String.class)) - .isEqualTo("ã°ãƒ¼"); - }); - } - - @Test - void initializationDisabled() { - this.contextRunner.run(assertInitializationIsDisabled()); - } - - @Test - void initializationDoesNotApplyWithSeveralDataSources() { - this.contextRunner.withUserConfiguration(TwoDataSources.class) - .withPropertyValues("spring.datasource.initialization-mode:always").run((context) -> { - assertThat(context.getBeanNamesForType(DataSource.class)).hasSize(2); - assertDataSourceNotInitialized(context.getBean("oneDataSource", DataSource.class)); - assertDataSourceNotInitialized(context.getBean("twoDataSource", DataSource.class)); - }); - } - - private ContextConsumer assertInitializationIsDisabled() { - return (context) -> { - assertThat(context).hasSingleBean(DataSource.class); - DataSource dataSource = context.getBean(DataSource.class); - context.publishEvent(new DataSourceSchemaCreatedEvent(dataSource)); - assertDataSourceNotInitialized(dataSource); - }; - } - - private void assertDataSourceNotInitialized(DataSource dataSource) { - JdbcOperations template = new JdbcTemplate(dataSource); - assertThatExceptionOfType(BadSqlGrammarException.class) - .isThrownBy(() -> template.queryForObject("SELECT COUNT(*) from BAR", Integer.class)) - .satisfies((ex) -> { - SQLException sqlException = ex.getSQLException(); - int expectedCode = -5501; // user lacks privilege or object not found - assertThat(sqlException.getErrorCode()).isEqualTo(expectedCode); - }); - } - - @Test - void dataSourceInitializedWithSchemaCredentials() { - this.contextRunner - .withPropertyValues("spring.datasource.initialization-mode:always", - "spring.datasource.sqlScriptEncoding:UTF-8", - "spring.datasource.schema:" + getRelativeLocationFor("encoding-schema.sql"), - "spring.datasource.data:" + getRelativeLocationFor("encoding-data.sql"), - "spring.datasource.schema-username:admin", "spring.datasource.schema-password:admin") - .run((context) -> { - assertThat(context).hasFailed(); - assertThat(context.getStartupFailure()).isInstanceOf(BeanCreationException.class); - }); - } - - @Test - void dataSourceInitializedWithDataCredentials() { - this.contextRunner - .withPropertyValues("spring.datasource.initialization-mode:always", - "spring.datasource.sqlScriptEncoding:UTF-8", - "spring.datasource.schema:" + getRelativeLocationFor("encoding-schema.sql"), - "spring.datasource.data:" + getRelativeLocationFor("encoding-data.sql"), - "spring.datasource.data-username:admin", "spring.datasource.data-password:admin") - .run((context) -> { - assertThat(context).hasFailed(); - assertThat(context.getStartupFailure()).isInstanceOf(BeanCreationException.class); - }); - } - - @Test - void multipleScriptsAppliedInLexicalOrder() { - new ApplicationContextRunner(() -> { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); - context.setResourceLoader(new ReverseOrderResourceLoader(new DefaultResourceLoader())); - return context; - }).withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) - .withPropertyValues("spring.datasource.initialization-mode=always", - "spring.datasource.url:jdbc:hsqldb:mem:testdb-" + new Random().nextInt(), - "spring.datasource.schema:classpath*:" + getRelativeLocationFor("lexical-schema-*.sql"), - "spring.datasource.data:classpath*:" + getRelativeLocationFor("data.sql")) - .run((context) -> { - DataSource dataSource = context.getBean(DataSource.class); - assertThat(dataSource).isInstanceOf(HikariDataSource.class); - assertThat(dataSource).isNotNull(); - JdbcOperations template = new JdbcTemplate(dataSource); - assertThat(template.queryForObject("SELECT COUNT(*) from FOO", Integer.class)).isEqualTo(1); - }); - } - - @Test - void testDataSourceInitializedWithInvalidSchemaResource() { - this.contextRunner.withPropertyValues("spring.datasource.initialization-mode:always", - "spring.datasource.schema:classpath:does/not/exist.sql").run((context) -> { - assertThat(context).hasFailed(); - assertThat(context.getStartupFailure()).isInstanceOf(BeanCreationException.class); - assertThat(context.getStartupFailure()).hasMessageContaining("does/not/exist.sql"); - assertThat(context.getStartupFailure()).hasMessageContaining("spring.datasource.schema"); - }); - } - - @Test - void dataSourceInitializedWithInvalidDataResource() { - this.contextRunner.withPropertyValues("spring.datasource.initialization-mode:always", - "spring.datasource.schema:" + getRelativeLocationFor("schema.sql"), - "spring.datasource.data:classpath:does/not/exist.sql").run((context) -> { - assertThat(context).hasFailed(); - assertThat(context.getStartupFailure()).isInstanceOf(BeanCreationException.class); - assertThat(context.getStartupFailure()).hasMessageContaining("does/not/exist.sql"); - assertThat(context.getStartupFailure()).hasMessageContaining("spring.datasource.data"); - }); - } - - private String getRelativeLocationFor(String resource) { - return ClassUtils.addResourcePathToPackagePath(getClass(), resource); - } - - @Configuration(proxyBeanMethods = false) - static class OneDataSource { - - @Bean - DataSource oneDataSource() { - return new TestDataSource(); - } - - } - - @Configuration(proxyBeanMethods = false) - static class TwoDataSources extends OneDataSource { - - @Bean - DataSource twoDataSource() { - return new TestDataSource(); - } - - } - - /** - * {@link ResourcePatternResolver} used to ensure consistently wrong resource - * ordering. - */ - static class ReverseOrderResourceLoader implements ResourcePatternResolver { - - private final ResourcePatternResolver resolver; - - ReverseOrderResourceLoader(ResourceLoader loader) { - this.resolver = ResourcePatternUtils.getResourcePatternResolver(loader); - } - - @Override - public Resource getResource(String location) { - return this.resolver.getResource(location); - } - - @Override - public ClassLoader getClassLoader() { - return this.resolver.getClassLoader(); - } - - @Override - public Resource[] getResources(String locationPattern) throws IOException { - Resource[] resources = this.resolver.getResources(locationPattern); - Arrays.sort(resources, Comparator.comparing(Resource::getFilename).reversed()); - return resources; - } - - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializerTests.java deleted file mode 100644 index a9e8d212cd60..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializerTests.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.jdbc; - -import java.sql.Connection; -import java.sql.DatabaseMetaData; -import java.sql.SQLException; -import java.util.UUID; - -import javax.sql.DataSource; - -import com.zaxxer.hikari.HikariDataSource; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.jdbc.DataSourceBuilder; -import org.springframework.boot.jdbc.DataSourceInitializationMode; -import org.springframework.jdbc.core.JdbcTemplate; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -/** - * Tests for {@link DataSourceInitializer}. - * - * @author Stephane Nicoll - */ -class DataSourceInitializerTests { - - @Test - void initializeEmbeddedByDefault() { - try (HikariDataSource dataSource = createDataSource()) { - DataSourceInitializer initializer = new DataSourceInitializer(dataSource, new DataSourceProperties()); - JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); - assertThat(initializer.createSchema()).isTrue(); - assertNumberOfRows(jdbcTemplate, 0); - initializer.initSchema(); - assertNumberOfRows(jdbcTemplate, 1); - } - } - - @Test - void initializeWithModeAlways() { - try (HikariDataSource dataSource = createDataSource()) { - DataSourceProperties properties = new DataSourceProperties(); - properties.setInitializationMode(DataSourceInitializationMode.ALWAYS); - DataSourceInitializer initializer = new DataSourceInitializer(dataSource, properties); - JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); - assertThat(initializer.createSchema()).isTrue(); - assertNumberOfRows(jdbcTemplate, 0); - initializer.initSchema(); - assertNumberOfRows(jdbcTemplate, 1); - } - } - - private void assertNumberOfRows(JdbcTemplate jdbcTemplate, int count) { - assertThat(jdbcTemplate.queryForObject("SELECT COUNT(*) from BAR", Integer.class)).isEqualTo(count); - } - - @Test - void initializeWithModeNever() { - try (HikariDataSource dataSource = createDataSource()) { - DataSourceProperties properties = new DataSourceProperties(); - properties.setInitializationMode(DataSourceInitializationMode.NEVER); - DataSourceInitializer initializer = new DataSourceInitializer(dataSource, properties); - assertThat(initializer.createSchema()).isFalse(); - } - } - - @Test - void initializeOnlyEmbeddedByDefault() throws SQLException { - DatabaseMetaData metadata = mock(DatabaseMetaData.class); - given(metadata.getDatabaseProductName()).willReturn("MySQL"); - Connection connection = mock(Connection.class); - given(connection.getMetaData()).willReturn(metadata); - DataSource dataSource = mock(DataSource.class); - given(dataSource.getConnection()).willReturn(connection); - DataSourceInitializer initializer = new DataSourceInitializer(dataSource, new DataSourceProperties()); - assertThat(initializer.createSchema()).isFalse(); - verify(dataSource).getConnection(); - } - - private HikariDataSource createDataSource() { - return DataSourceBuilder.create().type(HikariDataSource.class).url("jdbc:h2:mem:" + UUID.randomUUID()).build(); - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceJmxConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceJmxConfigurationTests.java index a1d3835a017e..175925fa10c8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceJmxConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceJmxConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -63,7 +63,11 @@ void hikariAutoConfiguredCanUseRegisterMBeans() { "spring.datasource.name=" + poolName, "spring.datasource.hikari.register-mbeans=true") .run((context) -> { assertThat(context).hasSingleBean(HikariDataSource.class); - assertThat(context.getBean(HikariDataSource.class).isRegisterMbeans()).isTrue(); + HikariDataSource hikariDataSource = context.getBean(HikariDataSource.class); + assertThat(hikariDataSource.isRegisterMbeans()).isTrue(); + // Ensure that the pool has been initialized, triggering MBean + // registration + hikariDataSource.getConnection().close(); MBeanServer mBeanServer = context.getBean(MBeanServer.class); validateHikariMBeansRegistration(mBeanServer, poolName, true); }); @@ -77,23 +81,30 @@ void hikariAutoConfiguredWithoutDataSourceName() throws MalformedObjectNameExcep this.contextRunner.withPropertyValues("spring.datasource.type=" + HikariDataSource.class.getName(), "spring.datasource.hikari.register-mbeans=true").run((context) -> { assertThat(context).hasSingleBean(HikariDataSource.class); - assertThat(context.getBean(HikariDataSource.class).isRegisterMbeans()).isTrue(); + HikariDataSource hikariDataSource = context.getBean(HikariDataSource.class); + assertThat(hikariDataSource.isRegisterMbeans()).isTrue(); + // Ensure that the pool has been initialized, triggering MBean + // registration + hikariDataSource.getConnection().close(); // We can't rely on the number of MBeans so we're checking that the - // pool and pool - // config MBeans were registered + // pool and pool config MBeans were registered assertThat(mBeanServer.queryMBeans(new ObjectName("com.zaxxer.hikari:type=*"), null).size()) .isEqualTo(existingInstances.size() + 2); }); } @Test - void hikariAutoConfiguredUsesJmsFlag() { + void hikariAutoConfiguredUsesJmxFlag() { String poolName = UUID.randomUUID().toString(); this.contextRunner.withPropertyValues("spring.datasource.type=" + HikariDataSource.class.getName(), "spring.jmx.enabled=false", "spring.datasource.name=" + poolName, "spring.datasource.hikari.register-mbeans=true").run((context) -> { assertThat(context).hasSingleBean(HikariDataSource.class); - assertThat(context.getBean(HikariDataSource.class).isRegisterMbeans()).isTrue(); + HikariDataSource hikariDataSource = context.getBean(HikariDataSource.class); + assertThat(hikariDataSource.isRegisterMbeans()).isTrue(); + // Ensure that the pool has been initialized, triggering MBean + // registration + hikariDataSource.getConnection().close(); // Hikari can still register mBeans validateHikariMBeansRegistration(ManagementFactory.getPlatformMBeanServer(), poolName, true); }); @@ -111,6 +122,9 @@ void hikariProxiedCanUseRegisterMBeans() { HikariDataSource hikariDataSource = context.getBean(javax.sql.DataSource.class) .unwrap(HikariDataSource.class); assertThat(hikariDataSource.isRegisterMbeans()).isTrue(); + // Ensure that the pool has been initialized, triggering MBean + // registration + hikariDataSource.getConnection().close(); MBeanServer mBeanServer = context.getBean(MBeanServer.class); validateHikariMBeansRegistration(mBeanServer, poolName, true); }); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourcePropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourcePropertiesTests.java index 551caea1fa6c..74b9ad9ef496 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourcePropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourcePropertiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ * @author Maciej Walkowiak * @author Stephane Nicoll * @author Eddú Meléndez + * @author Scott Frederick */ class DataSourcePropertiesTests { @@ -51,8 +52,9 @@ void determineDriverWithExplicitConfig() { } @Test - void determineUrl() throws Exception { + void determineUrlWithoutGenerateUniqueName() throws Exception { DataSourceProperties properties = new DataSourceProperties(); + properties.setGenerateUniqueName(false); properties.afterPropertiesSet(); assertThat(properties.getUrl()).isNull(); assertThat(properties.determineUrl()).isEqualTo(EmbeddedDatabaseConnection.H2.getUrl("testdb")); @@ -67,6 +69,24 @@ void determineUrlWithNoEmbeddedSupport() throws Exception { .isThrownBy(properties::determineUrl).withMessageContaining("Failed to determine suitable jdbc url"); } + @Test + void determineUrlWithSpecificEmbeddedConnection() throws Exception { + DataSourceProperties properties = new DataSourceProperties(); + properties.setGenerateUniqueName(false); + properties.setEmbeddedDatabaseConnection(EmbeddedDatabaseConnection.HSQLDB); + properties.afterPropertiesSet(); + assertThat(properties.determineUrl()).isEqualTo(EmbeddedDatabaseConnection.HSQLDB.getUrl("testdb")); + } + + @Test + void whenEmbeddedConnectionIsNoneAndNoUrlIsConfiguredThenDetermineUrlThrows() { + DataSourceProperties properties = new DataSourceProperties(); + properties.setGenerateUniqueName(false); + properties.setEmbeddedDatabaseConnection(EmbeddedDatabaseConnection.NONE); + assertThatExceptionOfType(DataSourceProperties.DataSourceBeanCreationException.class) + .isThrownBy(properties::determineUrl).withMessageContaining("Failed to determine suitable jdbc url"); + } + @Test void determineUrlWithExplicitConfig() throws Exception { DataSourceProperties properties = new DataSourceProperties(); @@ -79,7 +99,6 @@ void determineUrlWithExplicitConfig() throws Exception { @Test void determineUrlWithGenerateUniqueName() throws Exception { DataSourceProperties properties = new DataSourceProperties(); - properties.setGenerateUniqueName(true); properties.afterPropertiesSet(); assertThat(properties.determineUrl()).isEqualTo(properties.determineUrl()); @@ -97,6 +116,24 @@ void determineUsername() throws Exception { assertThat(properties.determineUsername()).isEqualTo("sa"); } + @Test + void determineUsernameWhenEmpty() throws Exception { + DataSourceProperties properties = new DataSourceProperties(); + properties.setUsername(""); + properties.afterPropertiesSet(); + assertThat(properties.getUsername()).isEqualTo(""); + assertThat(properties.determineUsername()).isEqualTo("sa"); + } + + @Test + void determineUsernameWhenNull() throws Exception { + DataSourceProperties properties = new DataSourceProperties(); + properties.setUsername(null); + properties.afterPropertiesSet(); + assertThat(properties.getUsername()).isNull(); + assertThat(properties.determineUsername()).isEqualTo("sa"); + } + @Test void determineUsernameWithExplicitConfig() throws Exception { DataSourceProperties properties = new DataSourceProperties(); @@ -106,6 +143,15 @@ void determineUsernameWithExplicitConfig() throws Exception { assertThat(properties.determineUsername()).isEqualTo("foo"); } + @Test + void determineUsernameWithNonEmbeddedUrl() throws Exception { + DataSourceProperties properties = new DataSourceProperties(); + properties.setUrl("jdbc:h2:~/test"); + properties.afterPropertiesSet(); + assertThat(properties.getPassword()).isNull(); + assertThat(properties.determineUsername()).isNull(); + } + @Test void determinePassword() throws Exception { DataSourceProperties properties = new DataSourceProperties(); @@ -124,6 +170,16 @@ void determinePasswordWithExplicitConfig() throws Exception { } @Test + void determinePasswordWithNonEmbeddedUrl() throws Exception { + DataSourceProperties properties = new DataSourceProperties(); + properties.setUrl("jdbc:h2:~/test"); + properties.afterPropertiesSet(); + assertThat(properties.getPassword()).isNull(); + assertThat(properties.determinePassword()).isNull(); + } + + @Test + @Deprecated void determineCredentialsForSchemaScripts() { DataSourceProperties properties = new DataSourceProperties(); properties.setSchemaUsername("foo"); @@ -133,6 +189,7 @@ void determineCredentialsForSchemaScripts() { } @Test + @Deprecated void determineCredentialsForDataScripts() { DataSourceProperties properties = new DataSourceProperties(); properties.setDataUsername("foo"); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceTransactionManagerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceTransactionManagerAutoConfigurationTests.java index 0ac4f095d2c2..9d9d42e25fdb 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceTransactionManagerAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceTransactionManagerAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,18 +16,18 @@ package org.springframework.boot.autoconfigure.jdbc; +import java.util.UUID; + import javax.sql.DataSource; import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration; -import org.springframework.boot.test.util.TestPropertyValues; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.jdbc.datasource.DataSourceTransactionManager; -import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.annotation.AbstractTransactionManagementConfiguration; +import org.springframework.jdbc.support.JdbcTransactionManager; +import org.springframework.transaction.TransactionManager; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -41,82 +41,85 @@ */ class DataSourceTransactionManagerAutoConfigurationTests { - private final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(TransactionAutoConfiguration.class, + DataSourceTransactionManagerAutoConfiguration.class)) + .withPropertyValues("spring.datasource.url:jdbc:hsqldb:mem:test-" + UUID.randomUUID()); @Test - void testDataSourceExists() { - this.context.register(EmbeddedDataSourceConfiguration.class, - DataSourceTransactionManagerAutoConfiguration.class, TransactionAutoConfiguration.class); - this.context.refresh(); - assertThat(this.context.getBean(DataSource.class)).isNotNull(); - assertThat(this.context.getBean(DataSourceTransactionManager.class)).isNotNull(); + void transactionManagerWithoutDataSourceIsNotConfigured() { + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(TransactionManager.class)); } @Test - void testNoDataSourceExists() { - this.context.register(DataSourceTransactionManagerAutoConfiguration.class, TransactionAutoConfiguration.class); - this.context.refresh(); - assertThat(this.context.getBeanNamesForType(DataSource.class)).isEmpty(); - assertThat(this.context.getBeanNamesForType(DataSourceTransactionManager.class)).isEmpty(); + void transactionManagerWithExistingDataSourceIsConfigured() { + this.contextRunner.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) + .run((context) -> { + assertThat(context).hasSingleBean(TransactionManager.class) + .hasSingleBean(JdbcTransactionManager.class); + assertThat(context.getBean(JdbcTransactionManager.class).getDataSource()) + .isSameAs(context.getBean(DataSource.class)); + }); } @Test - void testManualConfiguration() { - this.context.register(EmbeddedDataSourceConfiguration.class, - DataSourceTransactionManagerAutoConfiguration.class, TransactionAutoConfiguration.class); - this.context.refresh(); - assertThat(this.context.getBean(DataSource.class)).isNotNull(); - assertThat(this.context.getBean(DataSourceTransactionManager.class)).isNotNull(); + void transactionManagerWithCustomizationIsConfigured() { + this.contextRunner.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) + .withPropertyValues("spring.transaction.default-timeout=1m", + "spring.transaction.rollback-on-commit-failure=true") + .run((context) -> { + assertThat(context).hasSingleBean(TransactionManager.class) + .hasSingleBean(JdbcTransactionManager.class); + JdbcTransactionManager transactionManager = context.getBean(JdbcTransactionManager.class); + assertThat(transactionManager.getDefaultTimeout()).isEqualTo(60); + assertThat(transactionManager.isRollbackOnCommitFailure()).isTrue(); + }); } @Test - void testExistingTransactionManager() { - this.context.register(TransactionManagerConfiguration.class, EmbeddedDataSourceConfiguration.class, - DataSourceTransactionManagerAutoConfiguration.class, TransactionAutoConfiguration.class); - this.context.refresh(); - assertThat(this.context.getBeansOfType(PlatformTransactionManager.class)).hasSize(1); - assertThat(this.context.getBean(PlatformTransactionManager.class)) - .isEqualTo(this.context.getBean("myTransactionManager")); + void transactionManagerWithExistingTransactionManagerIsNotOverridden() { + this.contextRunner + .withBean("myTransactionManager", TransactionManager.class, () -> mock(TransactionManager.class)) + .run((context) -> assertThat(context).hasSingleBean(TransactionManager.class) + .hasBean("myTransactionManager")); } - @Test - void testMultiDataSource() { - this.context.register(MultiDataSourceConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class, - TransactionAutoConfiguration.class); - this.context.refresh(); - assertThat(this.context.getBeansOfType(PlatformTransactionManager.class)).isEmpty(); + @Test // gh-24321 + void transactionManagerWithDaoExceptionTranslationDisabled() { + this.contextRunner.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) + .withPropertyValues("spring.dao.exceptiontranslation.enabled=false") + .run((context) -> assertThat(context.getBean(TransactionManager.class)) + .isExactlyInstanceOf(DataSourceTransactionManager.class)); } - @Test - void testMultiDataSourceUsingPrimary() { - this.context.register(MultiDataSourceUsingPrimaryConfiguration.class, - DataSourceTransactionManagerAutoConfiguration.class, TransactionAutoConfiguration.class); - this.context.refresh(); - assertThat(this.context.getBean(DataSourceTransactionManager.class)).isNotNull(); - assertThat(this.context.getBean(AbstractTransactionManagementConfiguration.class)).isNotNull(); + @Test // gh-24321 + void transactionManagerWithDaoExceptionTranslationEnabled() { + this.contextRunner.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) + .withPropertyValues("spring.dao.exceptiontranslation.enabled=true") + .run((context) -> assertThat(context.getBean(TransactionManager.class)) + .isExactlyInstanceOf(JdbcTransactionManager.class)); } - @Test - void testCustomizeDataSourceTransactionManagerUsingProperties() { - TestPropertyValues - .of("spring.transaction.default-timeout:30", "spring.transaction.rollback-on-commit-failure:true") - .applyTo(this.context); - this.context.register(EmbeddedDataSourceConfiguration.class, - DataSourceTransactionManagerAutoConfiguration.class, TransactionAutoConfiguration.class); - this.context.refresh(); - DataSourceTransactionManager transactionManager = this.context.getBean(DataSourceTransactionManager.class); - assertThat(transactionManager.getDefaultTimeout()).isEqualTo(30); - assertThat(transactionManager.isRollbackOnCommitFailure()).isTrue(); + @Test // gh-24321 + void transactionManagerWithDaoExceptionTranslationDefault() { + this.contextRunner.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) + .run((context) -> assertThat(context.getBean(TransactionManager.class)) + .isExactlyInstanceOf(JdbcTransactionManager.class)); } - @Configuration(proxyBeanMethods = false) - static class TransactionManagerConfiguration { - - @Bean - PlatformTransactionManager myTransactionManager() { - return mock(PlatformTransactionManager.class); - } + @Test + void transactionWithMultipleDataSourcesIsNotConfigured() { + this.contextRunner.withUserConfiguration(MultiDataSourceConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(TransactionManager.class)); + } + @Test + void transactionWithMultipleDataSourcesAndPrimaryCandidateIsConfigured() { + this.contextRunner.withUserConfiguration(MultiDataSourceUsingPrimaryConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(TransactionManager.class).hasSingleBean(JdbcTransactionManager.class); + assertThat(context.getBean(JdbcTransactionManager.class).getDataSource()) + .isSameAs(context.getBean("test1DataSource")); + }); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfigurationTests.java index 81268761a8c0..ebd28e9cfb13 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,10 +34,9 @@ */ class HikariDataSourceConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) - .withPropertyValues("spring.datasource.initialization-mode=never", - "spring.datasource.type=" + HikariDataSource.class.getName()); + .withPropertyValues("spring.datasource.type=" + HikariDataSource.class.getName()); @Test void testDataSourceExists() { @@ -55,7 +54,6 @@ void testDataSourcePropertiesOverridden() { assertThat(ds.getJdbcUrl()).isEqualTo("jdbc:foo//bar/spam"); assertThat(ds.getMaxLifetime()).isEqualTo(1234); }); - // TODO: test JDBC4 isValid() } @Test diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDriverConfigurationFailureAnalyzerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDriverConfigurationFailureAnalyzerTests.java index c75ee30c5169..0937cdfbb36f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDriverConfigurationFailureAnalyzerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDriverConfigurationFailureAnalyzerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import org.springframework.beans.factory.BeanCreationException; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration; import org.springframework.boot.diagnostics.FailureAnalysis; import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @@ -60,8 +61,8 @@ private FailureAnalysis performAnalysis(Class configuration) { private BeanCreationException createFailure(Class configuration) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); TestPropertyValues.of("spring.datasource.type=" + HikariDataSource.class.getName(), - "spring.datasource.hikari.data-source-class-name=com.example.Foo", - "spring.datasource.initialization-mode=always").applyTo(context); + "spring.datasource.hikari.data-source-class-name=com.example.Foo", "spring.sql.init.mode=always") + .applyTo(context); context.register(configuration); try { context.refresh(); @@ -74,7 +75,7 @@ private BeanCreationException createFailure(Class configuration) { } @Configuration(proxyBeanMethods = false) - @ImportAutoConfiguration(DataSourceAutoConfiguration.class) + @ImportAutoConfiguration({ DataSourceAutoConfiguration.class, SqlInitializationAutoConfiguration.class }) static class TestConfiguration { } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JdbcTemplateAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JdbcTemplateAutoConfigurationTests.java index 2ac583fe1a2c..5f51ab8b736a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JdbcTemplateAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JdbcTemplateAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration; import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration; +import org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -48,9 +49,7 @@ class JdbcTemplateAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.datasource.initialization-mode=never", - "spring.datasource.generate-unique-name=true") - .withConfiguration( + .withPropertyValues("spring.datasource.generate-unique-name=true").withConfiguration( AutoConfigurations.of(DataSourceAutoConfiguration.class, JdbcTemplateAutoConfiguration.class)); @Test @@ -147,7 +146,8 @@ void testExistingCustomNamedParameterJdbcTemplate() { } @Test - void testDependencyToDataSourceInitialization() { + @Deprecated + void testDependencyToDeprecatedDataSourceInitialization() { this.contextRunner.withUserConfiguration(DataSourceInitializationValidator.class) .withPropertyValues("spring.datasource.initialization-mode=always").run((context) -> { assertThat(context).hasNotFailed(); @@ -155,6 +155,15 @@ void testDependencyToDataSourceInitialization() { }); } + @Test + void testDependencyToScriptBasedDataSourceInitialization() { + this.contextRunner.withConfiguration(AutoConfigurations.of(SqlInitializationAutoConfiguration.class)) + .withUserConfiguration(DataSourceInitializationValidator.class).run((context) -> { + assertThat(context).hasNotFailed(); + assertThat(context.getBean(DataSourceInitializationValidator.class).count).isEqualTo(1); + }); + } + @Test void testDependencyToFlyway() { this.contextRunner.withUserConfiguration(DataSourceMigrationValidator.class) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JndiDataSourceAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JndiDataSourceAutoConfigurationTests.java index 358b52ae4fe9..f483327fad6c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JndiDataSourceAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JndiDataSourceAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ import java.util.Set; import javax.naming.Context; -import javax.naming.NamingException; import javax.sql.DataSource; import org.apache.commons.dbcp2.BasicDataSource; @@ -80,7 +79,7 @@ void close() { } @Test - void dataSourceIsAvailableFromJndi() throws IllegalStateException, NamingException { + void dataSourceIsAvailableFromJndi() { DataSource dataSource = new BasicDataSource(); configureJndi("foo", dataSource); @@ -94,7 +93,7 @@ void dataSourceIsAvailableFromJndi() throws IllegalStateException, NamingExcepti @SuppressWarnings("unchecked") @Test - void mbeanDataSourceIsExcludedFromExport() throws IllegalStateException, NamingException { + void mbeanDataSourceIsExcludedFromExport() { DataSource dataSource = new BasicDataSource(); configureJndi("foo", dataSource); @@ -111,7 +110,7 @@ void mbeanDataSourceIsExcludedFromExport() throws IllegalStateException, NamingE @SuppressWarnings("unchecked") @Test - void mbeanDataSourceIsExcludedFromExportByAllExporters() throws IllegalStateException, NamingException { + void mbeanDataSourceIsExcludedFromExportByAllExporters() { DataSource dataSource = new BasicDataSource(); configureJndi("foo", dataSource); this.context = new AnnotationConfigApplicationContext(); @@ -128,7 +127,7 @@ void mbeanDataSourceIsExcludedFromExportByAllExporters() throws IllegalStateExce @SuppressWarnings("unchecked") @Test - void standardDataSourceIsNotExcludedFromExport() throws IllegalStateException, NamingException { + void standardDataSourceIsNotExcludedFromExport() { DataSource dataSource = mock(DataSource.class); configureJndi("foo", dataSource); @@ -143,7 +142,7 @@ void standardDataSourceIsNotExcludedFromExport() throws IllegalStateException, N assertThat(excludedBeans).isEmpty(); } - private void configureJndi(String name, DataSource dataSource) throws IllegalStateException { + private void configureJndi(String name, DataSource dataSource) { TestableInitialContextFactory.bind(name, dataSource); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/MultiDataSourceConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/MultiDataSourceConfiguration.java index eed6efcd522d..86427ae518f1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/MultiDataSourceConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/MultiDataSourceConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,12 +32,12 @@ class MultiDataSourceConfiguration { @Bean DataSource test1DataSource() { - return new TestDataSource("test1"); + return new TestDataSource("test1", false); } @Bean DataSource test2DataSource() { - return new TestDataSource("test2"); + return new TestDataSource("test2", false); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/MultiDataSourceUsingPrimaryConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/MultiDataSourceUsingPrimaryConfiguration.java index 05b5f2c45efa..3a4d13928253 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/MultiDataSourceUsingPrimaryConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/MultiDataSourceUsingPrimaryConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ import org.springframework.context.annotation.Primary; /** - * Configuration for multiple {@link DataSource} (one being {@code @Primary}. + * Configuration for multiple {@link DataSource} (one being {@code @Primary}). * * @author Phillip Webb * @author Kazuki Shimizu @@ -34,12 +34,12 @@ class MultiDataSourceUsingPrimaryConfiguration { @Bean @Primary DataSource test1DataSource() { - return new TestDataSource("test1"); + return new TestDataSource("test1", false); } @Bean DataSource test2DataSource() { - return new TestDataSource("test2"); + return new TestDataSource("test2", false); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/OracleUcpDataSourceConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/OracleUcpDataSourceConfigurationTests.java new file mode 100644 index 000000000000..353b2777d81f --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/OracleUcpDataSourceConfigurationTests.java @@ -0,0 +1,107 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jdbc; + +import java.sql.Connection; + +import javax.sql.DataSource; + +import oracle.ucp.jdbc.PoolDataSource; +import oracle.ucp.jdbc.PoolDataSourceImpl; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DataSourceAutoConfiguration} with Oracle UCP. + * + * @author Fabio Grassi + * @author Stephane Nicoll + */ +class OracleUcpDataSourceConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) + .withPropertyValues("spring.datasource.type=" + PoolDataSource.class.getName()); + + @Test + void testDataSourceExists() { + this.contextRunner.run((context) -> { + assertThat(context.getBeansOfType(DataSource.class)).hasSize(1); + assertThat(context.getBeansOfType(PoolDataSourceImpl.class)).hasSize(1); + try (Connection connection = context.getBean(DataSource.class).getConnection()) { + assertThat(connection.isValid(1000)).isTrue(); + } + }); + } + + @Test + void testDataSourcePropertiesOverridden() { + this.contextRunner.withPropertyValues("spring.datasource.oracleucp.url=jdbc:foo//bar/spam", + "spring.datasource.oracleucp.max-idle-time=1234").run((context) -> { + PoolDataSourceImpl ds = context.getBean(PoolDataSourceImpl.class); + assertThat(ds.getURL()).isEqualTo("jdbc:foo//bar/spam"); + assertThat(ds.getMaxIdleTime()).isEqualTo(1234); + }); + } + + @Test + void testDataSourceConnectionPropertiesOverridden() { + this.contextRunner.withPropertyValues("spring.datasource.oracleucp.connection-properties.autoCommit=false") + .run((context) -> { + PoolDataSourceImpl ds = context.getBean(PoolDataSourceImpl.class); + assertThat(ds.getConnectionProperty("autoCommit")).isEqualTo("false"); + }); + } + + @Test + void testDataSourceDefaultsPreserved() { + this.contextRunner.run((context) -> { + PoolDataSourceImpl ds = context.getBean(PoolDataSourceImpl.class); + assertThat(ds.getInitialPoolSize()).isEqualTo(0); + assertThat(ds.getMinPoolSize()).isEqualTo(0); + assertThat(ds.getMaxPoolSize()).isEqualTo(Integer.MAX_VALUE); + assertThat(ds.getInactiveConnectionTimeout()).isEqualTo(0); + assertThat(ds.getConnectionWaitTimeout()).isEqualTo(3); + assertThat(ds.getTimeToLiveConnectionTimeout()).isEqualTo(0); + assertThat(ds.getAbandonedConnectionTimeout()).isEqualTo(0); + assertThat(ds.getTimeoutCheckInterval()).isEqualTo(30); + assertThat(ds.getFastConnectionFailoverEnabled()).isFalse(); + }); + } + + @Test + void nameIsAliasedToPoolName() { + this.contextRunner.withPropertyValues("spring.datasource.name=myDS").run((context) -> { + PoolDataSourceImpl ds = context.getBean(PoolDataSourceImpl.class); + assertThat(ds.getConnectionPoolName()).isEqualTo("myDS"); + }); + } + + @Test + void poolNameTakesPrecedenceOverName() { + this.contextRunner.withPropertyValues("spring.datasource.name=myDS", + "spring.datasource.oracleucp.connection-pool-name=myOracleUcpDS").run((context) -> { + PoolDataSourceImpl ds = context.getBean(PoolDataSourceImpl.class); + assertThat(ds.getConnectionPoolName()).isEqualTo("myOracleUcpDS"); + }); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/TestDataSource.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/TestDataSource.java index efc8ec139ae3..37c5f666cfe6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/TestDataSource.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/TestDataSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,14 @@ package org.springframework.boot.autoconfigure.jdbc; +import java.sql.Connection; +import java.sql.SQLException; import java.util.UUID; import org.apache.commons.dbcp2.BasicDataSource; +import org.springframework.jdbc.datasource.SimpleDriverDataSource; + /** * {@link BasicDataSource} used for testing. * @@ -27,23 +31,45 @@ * @author Kazuki Shimizu * @author Stephane Nicoll */ -public class TestDataSource extends BasicDataSource { +public class TestDataSource extends SimpleDriverDataSource { + + /** + * Create an in-memory database with a random name. + */ + public TestDataSource() { + this(false); + } + + /** + * Create an in-memory database with a random name. + * @param addTestUser if a test user should be added + */ + public TestDataSource(boolean addTestUser) { + this(UUID.randomUUID().toString(), addTestUser); + } /** * Create an in-memory database with the specified name. * @param name the name of the database + * @param addTestUser if a test user should be added */ - public TestDataSource(String name) { - setDriverClassName("org.hsqldb.jdbcDriver"); + public TestDataSource(String name, boolean addTestUser) { + setDriverClass(org.hsqldb.jdbc.JDBCDriver.class); setUrl("jdbc:hsqldb:mem:" + name); setUsername("sa"); + setupDatabase(addTestUser); + setUrl(getUrl() + ";create=false"); } - /** - * Create an in-memory database with a random name. - */ - public TestDataSource() { - this(UUID.randomUUID().toString()); + private void setupDatabase(boolean addTestUser) { + try (Connection connection = getConnection()) { + if (addTestUser) { + connection.prepareStatement("CREATE USER \"test\" password \"secret\" ADMIN").execute(); + } + } + catch (SQLException ex) { + throw new IllegalStateException(ex); + } } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfigurationTests.java index 91f0f78c167f..336de2b14e64 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/XADataSourceAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,10 +19,14 @@ import javax.sql.DataSource; import javax.sql.XADataSource; +import com.ibm.db2.jcc.DB2XADataSource; import org.hsqldb.jdbc.pool.JDBCXADataSource; import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.jdbc.XADataSourceWrapper; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @@ -60,6 +64,20 @@ void createFromUrl() { assertThat(dataSource.getUser()).isEqualTo("un"); } + @Test + void createNonEmbeddedFromXAProperties() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(XADataSourceAutoConfiguration.class)) + .withUserConfiguration(FromProperties.class) + .withClassLoader(new FilteredClassLoader("org.h2.Driver", "org.hsqldb.jdbcDriver")) + .withPropertyValues("spring.datasource.xa.data-source-class-name:com.ibm.db2.jcc.DB2XADataSource", + "spring.datasource.xa.properties.user:test", "spring.datasource.xa.properties.password:secret") + .run((context) -> { + MockXADataSourceWrapper wrapper = context.getBean(MockXADataSourceWrapper.class); + XADataSource xaDataSource = wrapper.getXaDataSource(); + assertThat(xaDataSource).isInstanceOf(DB2XADataSource.class); + }); + } + @Test void createFromClass() throws Exception { ApplicationContext context = createContext(FromProperties.class, diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomFilterContextPathTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomFilterContextPathTests.java index 308be4f1861b..dcad4c620a4a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomFilterContextPathTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomFilterContextPathTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,7 +51,8 @@ * @author Dave Syer */ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, - properties = { "spring.jersey.type=filter", "server.servlet.context-path=/app" }) + properties = { "spring.jersey.type=filter", "server.servlet.context-path=/app", + "server.servlet.register-default-servlet=true" }) @DirtiesContext class JerseyAutoConfigurationCustomFilterContextPathTests { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomFilterPathTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomFilterPathTests.java index 5ef1b34603aa..4efb451a825c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomFilterPathTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomFilterPathTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,7 +50,8 @@ * * @author Dave Syer */ -@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "spring.jersey.type=filter") +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, + properties = { "spring.jersey.type=filter", "server.servlet.register-default-servlet=true" }) @DirtiesContext class JerseyAutoConfigurationCustomFilterPathTests { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationDefaultFilterPathTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationDefaultFilterPathTests.java index afe01f326abb..7484bb7a3fea 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationDefaultFilterPathTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationDefaultFilterPathTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,7 +49,8 @@ * * @author Dave Syer */ -@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "spring.jersey.type=filter") +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, + properties = { "spring.jersey.type=filter", "server.servlet.register-default-servlet=true" }) @DirtiesContext class JerseyAutoConfigurationDefaultFilterPathTests { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationTests.java index 0edbf673cf1e..4ac47266e122 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationTests.java @@ -23,8 +23,6 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; -import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener; -import org.springframework.boot.logging.LogLevel; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.web.servlet.FilterRegistrationBean; @@ -43,7 +41,6 @@ class JerseyAutoConfigurationTests { private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(JerseyAutoConfiguration.class)) - .withInitializer(new ConditionEvaluationReportLoggingListener(LogLevel.INFO)) .withUserConfiguration(ResourceConfigConfiguration.class); @Test diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java index 4989e3ce0fa4..fcc01eb88ad5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,13 +17,13 @@ package org.springframework.boot.autoconfigure.jms; import javax.jms.ConnectionFactory; +import javax.jms.ExceptionListener; import javax.jms.Session; import org.apache.activemq.ActiveMQConnectionFactory; import org.junit.jupiter.api.Test; import org.messaginghub.pooled.jms.JmsPoolConnectionFactory; -import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration; @@ -56,6 +56,7 @@ * @author Greg Turnquist * @author Stephane Nicoll * @author Aurélien Leboulanger + * @author Eddú Meléndez */ class JmsAutoConfigurationTests { @@ -210,6 +211,16 @@ void testDefaultContainerFactoryWithMessageConverters() { }); } + @Test + void testDefaultContainerFactoryWithExceptionListener() { + ExceptionListener exceptionListener = mock(ExceptionListener.class); + this.contextRunner.withUserConfiguration(EnableJmsConfiguration.class) + .withBean(ExceptionListener.class, () -> exceptionListener).run((context) -> { + DefaultMessageListenerContainer container = getContainer(context, "jmsListenerContainerFactory"); + assertThat(container.getExceptionListener()).isSameAs(exceptionListener); + }); + } + @Test void testCustomContainerFactoryWithConfigurer() { this.contextRunner.withUserConfiguration(TestConfiguration9.class, EnableJmsConfiguration.class) @@ -428,7 +439,7 @@ JmsTemplate jmsTemplate(ConnectionFactory connectionFactory) { static class TestConfiguration4 implements BeanPostProcessor { @Override - public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + public Object postProcessAfterInitialization(Object bean, String beanName) { if (bean.getClass().isAssignableFrom(JmsTemplate.class)) { JmsTemplate jmsTemplate = (JmsTemplate) bean; jmsTemplate.setPubSubDomain(true); @@ -437,7 +448,7 @@ public Object postProcessAfterInitialization(Object bean, String beanName) throw } @Override - public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + public Object postProcessBeforeInitialization(Object bean, String beanName) { return bean; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsPropertiesTests.java index 2f339d767050..50b1c3df83eb 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsPropertiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -83,7 +83,7 @@ void setTimeToLiveEnablesQoS() { @Test void defaultReceiveTimeoutMatchesListenerContainersDefault() { assertThat(new JmsProperties().getListener().getReceiveTimeout()) - .isEqualTo(Duration.ofMillis(AbstractPollingMessageListenerContainer.DEFAULT_RECEIVE_TIMEOUT)); + .hasMillis(AbstractPollingMessageListenerContainer.DEFAULT_RECEIVE_TIMEOUT); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JndiConnectionFactoryAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JndiConnectionFactoryAutoConfigurationTests.java index 086836f7a3c0..84f9a955432b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JndiConnectionFactoryAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JndiConnectionFactoryAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -111,8 +111,9 @@ void jndiNamePropertySetWithWrongValue() { private ContextConsumer assertConnectionFactory(ConnectionFactory connectionFactory) { return (context) -> { - assertThat(context).hasSingleBean(ConnectionFactory.class); - assertThat(context.getBean(ConnectionFactory.class)).isSameAs(connectionFactory); + assertThat(context).hasSingleBean(ConnectionFactory.class).hasBean("jmsConnectionFactory"); + assertThat(context.getBean(ConnectionFactory.class)).isSameAs(connectionFactory) + .isSameAs(context.getBean("jmsConnectionFactory")); }; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQAutoConfigurationTests.java index a66dafbc6905..81457c4cd5d1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,13 +50,12 @@ class ActiveMQAutoConfigurationTests { @Test void brokerIsEmbeddedByDefault() { this.contextRunner.withUserConfiguration(EmptyConfiguration.class).run((context) -> { - assertThat(context).hasSingleBean(CachingConnectionFactory.class); - CachingConnectionFactory cachingConnectionFactory = context.getBean(CachingConnectionFactory.class); - assertThat(cachingConnectionFactory.getTargetConnectionFactory()) - .isInstanceOf(ActiveMQConnectionFactory.class); - assertThat( - ((ActiveMQConnectionFactory) cachingConnectionFactory.getTargetConnectionFactory()).getBrokerURL()) - .isEqualTo("vm://localhost?broker.persistent=false"); + assertThat(context).hasSingleBean(CachingConnectionFactory.class).hasBean("jmsConnectionFactory"); + CachingConnectionFactory connectionFactory = context.getBean(CachingConnectionFactory.class); + assertThat(context.getBean("jmsConnectionFactory")).isSameAs(connectionFactory); + assertThat(connectionFactory.getTargetConnectionFactory()).isInstanceOf(ActiveMQConnectionFactory.class); + assertThat(((ActiveMQConnectionFactory) connectionFactory.getTargetConnectionFactory()).getBrokerURL()) + .isEqualTo("vm://localhost?broker.persistent=false"); }); } @@ -69,9 +68,10 @@ void configurationBacksOffWhenCustomConnectionFactoryExists() { @Test void connectionFactoryIsCachedByDefault() { this.contextRunner.withUserConfiguration(EmptyConfiguration.class).run((context) -> { - assertThat(context).hasSingleBean(ConnectionFactory.class); - assertThat(context).hasSingleBean(CachingConnectionFactory.class); + assertThat(context).hasSingleBean(ConnectionFactory.class).hasSingleBean(CachingConnectionFactory.class) + .hasBean("jmsConnectionFactory"); CachingConnectionFactory connectionFactory = context.getBean(CachingConnectionFactory.class); + assertThat(context.getBean("jmsConnectionFactory")).isSameAs(connectionFactory); assertThat(connectionFactory.getTargetConnectionFactory()).isInstanceOf(ActiveMQConnectionFactory.class); assertThat(connectionFactory.isCacheConsumers()).isFalse(); assertThat(connectionFactory.isCacheProducers()).isTrue(); @@ -85,9 +85,10 @@ void connectionFactoryCachingCanBeCustomized() { .withPropertyValues("spring.jms.cache.consumers=true", "spring.jms.cache.producers=false", "spring.jms.cache.session-cache-size=10") .run((context) -> { - assertThat(context).hasSingleBean(ConnectionFactory.class); - assertThat(context).hasSingleBean(CachingConnectionFactory.class); + assertThat(context).hasSingleBean(ConnectionFactory.class) + .hasSingleBean(CachingConnectionFactory.class).hasBean("jmsConnectionFactory"); CachingConnectionFactory connectionFactory = context.getBean(CachingConnectionFactory.class); + assertThat(context.getBean("jmsConnectionFactory")).isSameAs(connectionFactory); assertThat(connectionFactory.isCacheConsumers()).isTrue(); assertThat(connectionFactory.isCacheProducers()).isFalse(); assertThat(connectionFactory.getSessionCacheSize()).isEqualTo(10); @@ -98,8 +99,10 @@ void connectionFactoryCachingCanBeCustomized() { void connectionFactoryCachingCanBeDisabled() { this.contextRunner.withUserConfiguration(EmptyConfiguration.class) .withPropertyValues("spring.jms.cache.enabled=false").run((context) -> { - assertThat(context.getBeansOfType(ActiveMQConnectionFactory.class)).hasSize(1); + assertThat(context).hasSingleBean(ConnectionFactory.class) + .hasSingleBean(ActiveMQConnectionFactory.class).hasBean("jmsConnectionFactory"); ActiveMQConnectionFactory connectionFactory = context.getBean(ActiveMQConnectionFactory.class); + assertThat(context.getBean("jmsConnectionFactory")).isSameAs(connectionFactory); ActiveMQConnectionFactory defaultFactory = new ActiveMQConnectionFactory( "vm://localhost?broker.persistent=false"); assertThat(connectionFactory.getUserName()).isEqualTo(defaultFactory.getUserName()); @@ -123,8 +126,10 @@ void customConnectionFactoryIsApplied() { "spring.activemq.nonBlockingRedelivery=true", "spring.activemq.sendTimeout=1000", "spring.activemq.packages.trust-all=false", "spring.activemq.packages.trusted=com.example.acme") .run((context) -> { - assertThat(context.getBeansOfType(ActiveMQConnectionFactory.class)).hasSize(1); + assertThat(context).hasSingleBean(ConnectionFactory.class) + .hasSingleBean(ActiveMQConnectionFactory.class).hasBean("jmsConnectionFactory"); ActiveMQConnectionFactory connectionFactory = context.getBean(ActiveMQConnectionFactory.class); + assertThat(context.getBean("jmsConnectionFactory")).isSameAs(connectionFactory); assertThat(connectionFactory.getUserName()).isEqualTo("foo"); assertThat(connectionFactory.getPassword()).isEqualTo("bar"); assertThat(connectionFactory.getCloseTimeout()).isEqualTo(500); @@ -139,8 +144,10 @@ void customConnectionFactoryIsApplied() { void defaultPoolConnectionFactoryIsApplied() { this.contextRunner.withUserConfiguration(EmptyConfiguration.class) .withPropertyValues("spring.activemq.pool.enabled=true").run((context) -> { - assertThat(context.getBeansOfType(JmsPoolConnectionFactory.class)).hasSize(1); + assertThat(context).hasSingleBean(ConnectionFactory.class) + .hasSingleBean(JmsPoolConnectionFactory.class).hasBean("jmsConnectionFactory"); JmsPoolConnectionFactory connectionFactory = context.getBean(JmsPoolConnectionFactory.class); + assertThat(context.getBean("jmsConnectionFactory")).isSameAs(connectionFactory); JmsPoolConnectionFactory defaultFactory = new JmsPoolConnectionFactory(); assertThat(connectionFactory.isBlockIfSessionPoolIsFull()) .isEqualTo(defaultFactory.isBlockIfSessionPoolIsFull()); @@ -167,8 +174,10 @@ void customPoolConnectionFactoryIsApplied() { "spring.activemq.pool.timeBetweenExpirationCheck=2048", "spring.activemq.pool.useAnonymousProducers=false") .run((context) -> { - assertThat(context.getBeansOfType(JmsPoolConnectionFactory.class)).hasSize(1); + assertThat(context).hasSingleBean(ConnectionFactory.class) + .hasSingleBean(JmsPoolConnectionFactory.class).hasBean("jmsConnectionFactory"); JmsPoolConnectionFactory connectionFactory = context.getBean(JmsPoolConnectionFactory.class); + assertThat(context.getBean("jmsConnectionFactory")).isSameAs(connectionFactory); assertThat(connectionFactory.isBlockIfSessionPoolIsFull()).isFalse(); assertThat(connectionFactory.getBlockIfSessionPoolIsFullTimeout()).isEqualTo(64); assertThat(connectionFactory.getConnectionIdleTimeout()).isEqualTo(512); @@ -183,7 +192,10 @@ void customPoolConnectionFactoryIsApplied() { void poolConnectionFactoryConfiguration() { this.contextRunner.withUserConfiguration(EmptyConfiguration.class) .withPropertyValues("spring.activemq.pool.enabled:true").run((context) -> { + assertThat(context).hasSingleBean(ConnectionFactory.class) + .hasSingleBean(JmsPoolConnectionFactory.class).hasBean("jmsConnectionFactory"); ConnectionFactory factory = context.getBean(ConnectionFactory.class); + assertThat(context.getBean("jmsConnectionFactory")).isSameAs(factory); assertThat(factory).isInstanceOf(JmsPoolConnectionFactory.class); context.getSourceApplicationContext().close(); assertThat(factory.createConnection()).isNull(); @@ -194,14 +206,20 @@ void poolConnectionFactoryConfiguration() { void cachingConnectionFactoryNotOnTheClasspathThenSimpleConnectionFactoryAutoConfigured() { this.contextRunner.withClassLoader(new FilteredClassLoader(CachingConnectionFactory.class)) .withPropertyValues("spring.activemq.pool.enabled=false", "spring.jms.cache.enabled=false") - .run((context) -> assertThat(context).hasSingleBean(ActiveMQConnectionFactory.class)); + .run((context) -> { + assertThat(context).hasSingleBean(ConnectionFactory.class) + .hasSingleBean(ActiveMQConnectionFactory.class).hasBean("jmsConnectionFactory"); + ActiveMQConnectionFactory connectionFactory = context.getBean(ActiveMQConnectionFactory.class); + assertThat(context.getBean("jmsConnectionFactory")).isSameAs(connectionFactory); + }); } @Test void cachingConnectionFactoryNotOnTheClasspathAndCacheEnabledThenSimpleConnectionFactoryNotConfigured() { this.contextRunner.withClassLoader(new FilteredClassLoader(CachingConnectionFactory.class)) .withPropertyValues("spring.activemq.pool.enabled=false", "spring.jms.cache.enabled=true") - .run((context) -> assertThat(context).doesNotHaveBean(ActiveMQConnectionFactory.class)); + .run((context) -> assertThat(context).doesNotHaveBean(ConnectionFactory.class) + .doesNotHaveBean(ActiveMQConnectionFactory.class).doesNotHaveBean("jmsConnectionFactory")); } @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisAutoConfigurationTests.java index 2fec14d579e4..515a4306bfdf 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,6 +47,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; @@ -70,9 +71,10 @@ class ArtemisAutoConfigurationTests { @Test void connectionFactoryIsCachedByDefault() { this.contextRunner.withUserConfiguration(EmptyConfiguration.class).run((context) -> { - assertThat(context).hasSingleBean(ConnectionFactory.class); - assertThat(context).hasSingleBean(CachingConnectionFactory.class); + assertThat(context).hasSingleBean(ConnectionFactory.class).hasSingleBean(CachingConnectionFactory.class) + .hasBean("jmsConnectionFactory"); CachingConnectionFactory connectionFactory = context.getBean(CachingConnectionFactory.class); + assertThat(context.getBean("jmsConnectionFactory")).isSameAs(connectionFactory); assertThat(connectionFactory.getTargetConnectionFactory()).isInstanceOf(ActiveMQConnectionFactory.class); assertThat(connectionFactory.isCacheConsumers()).isFalse(); assertThat(connectionFactory.isCacheProducers()).isTrue(); @@ -86,9 +88,10 @@ void connectionFactoryCachingCanBeCustomized() { .withPropertyValues("spring.jms.cache.consumers=true", "spring.jms.cache.producers=false", "spring.jms.cache.session-cache-size=10") .run((context) -> { - assertThat(context).hasSingleBean(ConnectionFactory.class); - assertThat(context).hasSingleBean(CachingConnectionFactory.class); + assertThat(context).hasSingleBean(ConnectionFactory.class) + .hasSingleBean(CachingConnectionFactory.class).hasBean("jmsConnectionFactory"); CachingConnectionFactory connectionFactory = context.getBean(CachingConnectionFactory.class); + assertThat(context.getBean("jmsConnectionFactory")).isSameAs(connectionFactory); assertThat(connectionFactory.isCacheConsumers()).isTrue(); assertThat(connectionFactory.isCacheProducers()).isFalse(); assertThat(connectionFactory.getSessionCacheSize()).isEqualTo(10); @@ -99,9 +102,9 @@ void connectionFactoryCachingCanBeCustomized() { void connectionFactoryCachingCanBeDisabled() { this.contextRunner.withUserConfiguration(EmptyConfiguration.class) .withPropertyValues("spring.jms.cache.enabled=false").run((context) -> { - assertThat(context).hasSingleBean(ConnectionFactory.class); assertThat(context).doesNotHaveBean(CachingConnectionFactory.class); - assertThat(context.getBean(ConnectionFactory.class)).isInstanceOf(ActiveMQConnectionFactory.class); + ConnectionFactory connectionFactory = getConnectionFactory(context); + assertThat(connectionFactory).isInstanceOf(ActiveMQConnectionFactory.class); }); } @@ -110,7 +113,7 @@ void nativeConnectionFactory() { this.contextRunner.withUserConfiguration(EmptyConfiguration.class) .withPropertyValues("spring.artemis.mode:native").run((context) -> { JmsTemplate jmsTemplate = context.getBean(JmsTemplate.class); - ConnectionFactory connectionFactory = context.getBean(ConnectionFactory.class); + ConnectionFactory connectionFactory = getConnectionFactory(context); assertThat(connectionFactory).isEqualTo(jmsTemplate.getConnectionFactory()); ActiveMQConnectionFactory activeMQConnectionFactory = getActiveMQConnectionFactory( connectionFactory); @@ -121,12 +124,31 @@ void nativeConnectionFactory() { } @Test + void nativeConnectionFactoryCustomBrokerUrl() { + this.contextRunner.withUserConfiguration(EmptyConfiguration.class) + .withPropertyValues("spring.artemis.mode:native", "spring.artemis.broker-url:tcp://192.168.1.144:9876") + .run((context) -> assertNettyConnectionFactory( + getActiveMQConnectionFactory(getConnectionFactory(context)), "192.168.1.144", 9876)); + } + + @Test + @Deprecated void nativeConnectionFactoryCustomHost() { this.contextRunner.withUserConfiguration(EmptyConfiguration.class) .withPropertyValues("spring.artemis.mode:native", "spring.artemis.host:192.168.1.144", "spring.artemis.port:9876") .run((context) -> assertNettyConnectionFactory( - getActiveMQConnectionFactory(context.getBean(ConnectionFactory.class)), "192.168.1.144", 9876)); + getActiveMQConnectionFactory(getConnectionFactory(context)), "192.168.1.144", 9876)); + } + + @Test + @Deprecated + void nativeConnectionFactoryCustomBrokerUrlAndHost() { + this.contextRunner.withUserConfiguration(EmptyConfiguration.class) + .withPropertyValues("spring.artemis.mode:native", "spring.artemis.host:192.168.1.144", + "spring.artemis.port:9876", "spring.artemis.broker-url=tcp://192.168.1.221:6543") + .run((context) -> assertNettyConnectionFactory( + getActiveMQConnectionFactory(getConnectionFactory(context)), "192.168.1.221", 6543)); } @Test @@ -136,7 +158,7 @@ void nativeConnectionFactoryCredentials() { "spring.artemis.password:secret") .run((context) -> { JmsTemplate jmsTemplate = context.getBean(JmsTemplate.class); - ConnectionFactory connectionFactory = context.getBean(ConnectionFactory.class); + ConnectionFactory connectionFactory = getConnectionFactory(context); assertThat(connectionFactory).isEqualTo(jmsTemplate.getConnectionFactory()); ActiveMQConnectionFactory activeMQConnectionFactory = getActiveMQConnectionFactory( connectionFactory); @@ -157,7 +179,7 @@ void embeddedConnectionFactory() { .getBean(org.apache.activemq.artemis.core.config.Configuration.class); assertThat(configuration.isPersistenceEnabled()).isFalse(); assertThat(configuration.isSecurityEnabled()).isFalse(); - assertInVmConnectionFactory(getActiveMQConnectionFactory(context.getBean(ConnectionFactory.class))); + assertInVmConnectionFactory(getActiveMQConnectionFactory(getConnectionFactory(context))); }); } @@ -170,7 +192,7 @@ void embeddedConnectionFactoryByDefault() { .getBean(org.apache.activemq.artemis.core.config.Configuration.class); assertThat(configuration.isPersistenceEnabled()).isFalse(); assertThat(configuration.isSecurityEnabled()).isFalse(); - assertInVmConnectionFactory(getActiveMQConnectionFactory(context.getBean(ConnectionFactory.class))); + assertInVmConnectionFactory(getActiveMQConnectionFactory(getConnectionFactory(context))); }); } @@ -180,7 +202,7 @@ void nativeConnectionFactoryIfEmbeddedServiceDisabledExplicitly() { this.contextRunner.withUserConfiguration(EmptyConfiguration.class) .withPropertyValues("spring.artemis.embedded.enabled:false").run((context) -> { assertThat(context).doesNotHaveBean(ActiveMQServer.class); - assertNettyConnectionFactory(getActiveMQConnectionFactory(context.getBean(ConnectionFactory.class)), + assertNettyConnectionFactory(getActiveMQConnectionFactory(getConnectionFactory(context)), "localhost", 61616); }); } @@ -192,7 +214,7 @@ void embeddedConnectionFactoryEvenIfEmbeddedServiceDisabled() { .withPropertyValues("spring.artemis.mode:embedded", "spring.artemis.embedded.enabled:false") .run((context) -> { assertThat(context.getBeansOfType(ActiveMQServer.class)).isEmpty(); - assertInVmConnectionFactory(getActiveMQConnectionFactory(context.getBean(ConnectionFactory.class))); + assertInVmConnectionFactory(getActiveMQConnectionFactory(getConnectionFactory(context))); }); } @@ -242,13 +264,13 @@ void embeddedServiceWithCustomArtemisConfiguration() { @Test void embeddedWithPersistentMode(@TempDir Path temp) throws IOException { - File dataFolder = Files.createTempDirectory(temp, null).toFile(); + File dataDirectory = Files.createTempDirectory(temp, null).toFile(); final String messageId = UUID.randomUUID().toString(); // Start the server and post a message to some queue this.contextRunner.withUserConfiguration(EmptyConfiguration.class) .withPropertyValues("spring.artemis.embedded.queues=TestQueue", "spring.artemis.embedded.persistent:true", - "spring.artemis.embedded.dataDirectory:" + dataFolder.getAbsolutePath()) + "spring.artemis.embedded.dataDirectory:" + dataDirectory.getAbsolutePath()) .run((context) -> context.getBean(JmsTemplate.class).send("TestQueue", (session) -> session.createTextMessage(messageId))) .run((context) -> { @@ -344,13 +366,20 @@ void customPoolConnectionFactoryIsApplied() { @Test void poolConnectionFactoryConfiguration() { this.contextRunner.withPropertyValues("spring.artemis.pool.enabled:true").run((context) -> { - ConnectionFactory factory = context.getBean(ConnectionFactory.class); + ConnectionFactory factory = getConnectionFactory(context); assertThat(factory).isInstanceOf(JmsPoolConnectionFactory.class); context.getSourceApplicationContext().close(); assertThat(factory.createConnection()).isNull(); }); } + private ConnectionFactory getConnectionFactory(AssertableApplicationContext context) { + assertThat(context).hasSingleBean(ConnectionFactory.class).hasBean("jmsConnectionFactory"); + ConnectionFactory connectionFactory = context.getBean(ConnectionFactory.class); + assertThat(connectionFactory).isSameAs(context.getBean("jmsConnectionFactory")); + return connectionFactory; + } + private ActiveMQConnectionFactory getActiveMQConnectionFactory(ConnectionFactory connectionFactory) { assertThat(connectionFactory).isInstanceOf(CachingConnectionFactory.class); return (ActiveMQConnectionFactory) ((CachingConnectionFactory) connectionFactory).getTargetConnectionFactory(); @@ -367,7 +396,11 @@ private TransportConfiguration assertNettyConnectionFactory(ActiveMQConnectionFa TransportConfiguration transportConfig = getSingleTransportConfiguration(connectionFactory); assertThat(transportConfig.getFactoryClassName()).isEqualTo(NettyConnectorFactory.class.getName()); assertThat(transportConfig.getParams().get("host")).isEqualTo(host); - assertThat(transportConfig.getParams().get("port")).isEqualTo(port); + Object transportConfigPort = transportConfig.getParams().get("port"); + if (transportConfigPort instanceof String) { + transportConfigPort = Integer.parseInt((String) transportConfigPort); + } + assertThat(transportConfigPort).isEqualTo(port); return transportConfig; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jmx/ParentAwareNamingStrategyTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jmx/ParentAwareNamingStrategyTests.java new file mode 100644 index 000000000000..5f0531971b2b --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jmx/ParentAwareNamingStrategyTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jmx; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource; +import org.springframework.jmx.export.annotation.ManagedResource; +import org.springframework.util.ObjectUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ParentAwareNamingStrategy}. + * + * @author Andy Wilkinson + */ +class ParentAwareNamingStrategyTests { + + private ApplicationContextRunner contextRunner = new ApplicationContextRunner(); + + @Test + void objectNameMatchesManagedResourceByDefault() throws MalformedObjectNameException { + this.contextRunner.withBean("testManagedResource", TestManagedResource.class).run((context) -> { + ParentAwareNamingStrategy strategy = new ParentAwareNamingStrategy(new AnnotationJmxAttributeSource()); + strategy.setApplicationContext(context); + assertThat(strategy.getObjectName(context.getBean("testManagedResource"), "testManagedResource") + .getKeyPropertyListString()).isEqualTo("type=something,name1=def,name2=ghi"); + }); + } + + @Test + void uniqueObjectNameAddsIdentityProperty() throws MalformedObjectNameException { + this.contextRunner.withBean("testManagedResource", TestManagedResource.class).run((context) -> { + ParentAwareNamingStrategy strategy = new ParentAwareNamingStrategy(new AnnotationJmxAttributeSource()); + strategy.setApplicationContext(context); + strategy.setEnsureUniqueRuntimeObjectNames(true); + Object resource = context.getBean("testManagedResource"); + ObjectName objectName = strategy.getObjectName(resource, "testManagedResource"); + assertThat(objectName.getDomain()).isEqualTo("ABC"); + assertThat(objectName.getCanonicalKeyPropertyListString()).isEqualTo( + "identity=" + ObjectUtils.getIdentityHexString(resource) + ",name1=def,name2=ghi,type=something"); + }); + } + + @Test + void sameBeanInParentContextAddsContextProperty() throws MalformedObjectNameException { + this.contextRunner.withBean("testManagedResource", TestManagedResource.class).run((parent) -> this.contextRunner + .withBean("testManagedResource", TestManagedResource.class).withParent(parent).run((context) -> { + ParentAwareNamingStrategy strategy = new ParentAwareNamingStrategy( + new AnnotationJmxAttributeSource()); + strategy.setApplicationContext(context); + Object resource = context.getBean("testManagedResource"); + ObjectName objectName = strategy.getObjectName(resource, "testManagedResource"); + assertThat(objectName.getDomain()).isEqualTo("ABC"); + assertThat(objectName.getCanonicalKeyPropertyListString()).isEqualTo("context=" + + ObjectUtils.getIdentityHexString(context) + ",name1=def,name2=ghi,type=something"); + })); + } + + @Test + void uniqueObjectNameAndSameBeanInParentContextOnlyAddsIdentityProperty() throws MalformedObjectNameException { + this.contextRunner.withBean("testManagedResource", TestManagedResource.class).run((parent) -> this.contextRunner + .withBean("testManagedResource", TestManagedResource.class).withParent(parent).run((context) -> { + ParentAwareNamingStrategy strategy = new ParentAwareNamingStrategy( + new AnnotationJmxAttributeSource()); + strategy.setApplicationContext(context); + strategy.setEnsureUniqueRuntimeObjectNames(true); + Object resource = context.getBean("testManagedResource"); + ObjectName objectName = strategy.getObjectName(resource, "testManagedResource"); + assertThat(objectName.getDomain()).isEqualTo("ABC"); + assertThat(objectName.getCanonicalKeyPropertyListString()).isEqualTo("identity=" + + ObjectUtils.getIdentityHexString(resource) + ",name1=def,name2=ghi,type=something"); + })); + } + + @ManagedResource(objectName = "ABC:type=something,name1=def,name2=ghi") + public static class TestManagedResource { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jndi/TestableInitialContextFactory.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jndi/TestableInitialContextFactory.java index 806963a94a64..1d8cbc69f9ee 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jndi/TestableInitialContextFactory.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jndi/TestableInitialContextFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,7 @@ public class TestableInitialContextFactory implements InitialContextFactory { private static TestableContext context; @Override - public Context getInitialContext(Hashtable environment) throws NamingException { + public Context getInitialContext(Hashtable environment) { return getContext(); } @@ -78,7 +78,7 @@ public void bind(String name, Object obj) throws NamingException { } @Override - public Object lookup(String name) throws NamingException { + public Object lookup(String name) { return this.bindings.get(name); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfigurationTests.java index e57a8aa34049..938327660e4e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,28 +16,23 @@ package org.springframework.boot.autoconfigure.jooq; -import java.util.concurrent.Executor; - import javax.sql.DataSource; +import org.jooq.CharsetProvider; +import org.jooq.ConnectionProvider; +import org.jooq.ConverterProvider; import org.jooq.DSLContext; import org.jooq.ExecuteListener; import org.jooq.ExecuteListenerProvider; import org.jooq.ExecutorProvider; -import org.jooq.Record; -import org.jooq.RecordListener; import org.jooq.RecordListenerProvider; -import org.jooq.RecordMapper; import org.jooq.RecordMapperProvider; -import org.jooq.RecordType; -import org.jooq.RecordUnmapper; import org.jooq.RecordUnmapperProvider; import org.jooq.SQLDialect; -import org.jooq.TransactionListener; import org.jooq.TransactionListenerProvider; import org.jooq.TransactionalRunnable; -import org.jooq.VisitListener; import org.jooq.VisitListenerProvider; +import org.jooq.impl.DataSourceConnectionProvider; import org.jooq.impl.DefaultExecuteListenerProvider; import org.junit.jupiter.api.Test; @@ -49,10 +44,12 @@ import org.springframework.core.annotation.Order; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy; import org.springframework.transaction.PlatformTransactionManager; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.mock; /** * Tests for {@link JooqAutoConfiguration}. @@ -65,7 +62,7 @@ */ class JooqAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(JooqAutoConfiguration.class)) .withPropertyValues("spring.datasource.name:jooqtest"); @@ -107,28 +104,79 @@ void jooqWithTx() { "insert into jooqtest (name) values ('foo');"))); dsl.transaction(new AssertFetch(dsl, "select count(*) as total from jooqtest_tx;", "1")); }); + } + @Test + void jooqWithDefaultConnectionProvider() { + this.contextRunner.withUserConfiguration(JooqDataSourceConfiguration.class).run((context) -> { + DSLContext dsl = context.getBean(DSLContext.class); + ConnectionProvider connectionProvider = dsl.configuration().connectionProvider(); + assertThat(connectionProvider).isInstanceOf(DataSourceConnectionProvider.class); + DataSource connectionProviderDataSource = ((DataSourceConnectionProvider) connectionProvider).dataSource(); + assertThat(connectionProviderDataSource).isInstanceOf(TransactionAwareDataSourceProxy.class); + }); } @Test - void customProvidersArePickedUp() { - this.contextRunner.withUserConfiguration(JooqDataSourceConfiguration.class, TxManagerConfiguration.class, - TestRecordMapperProvider.class, TestRecordUnmapperProvider.class, TestRecordListenerProvider.class, - TestExecuteListenerProvider.class, TestVisitListenerProvider.class, - TestTransactionListenerProvider.class, TestExecutorProvider.class).run((context) -> { + void jooqWithDefaultExecuteListenerProvider() { + this.contextRunner.withUserConfiguration(JooqDataSourceConfiguration.class).run((context) -> { + DSLContext dsl = context.getBean(DSLContext.class); + assertThat(dsl.configuration().executeListenerProviders()).hasSize(1); + }); + } + + @Test + void jooqWithSeveralExecuteListenerProviders() { + this.contextRunner.withUserConfiguration(JooqDataSourceConfiguration.class, TestExecuteListenerProvider.class) + .run((context) -> { DSLContext dsl = context.getBean(DSLContext.class); - assertThat(dsl.configuration().recordMapperProvider().getClass()) - .isEqualTo(TestRecordMapperProvider.class); - assertThat(dsl.configuration().recordUnmapperProvider().getClass()) - .isEqualTo(TestRecordUnmapperProvider.class); - assertThat(dsl.configuration().executorProvider().getClass()).isEqualTo(TestExecutorProvider.class); - assertThat(dsl.configuration().recordListenerProviders()).hasSize(1); ExecuteListenerProvider[] executeListenerProviders = dsl.configuration().executeListenerProviders(); assertThat(executeListenerProviders).hasSize(2); assertThat(executeListenerProviders[0]).isInstanceOf(DefaultExecuteListenerProvider.class); assertThat(executeListenerProviders[1]).isInstanceOf(TestExecuteListenerProvider.class); - assertThat(dsl.configuration().visitListenerProviders()).hasSize(1); - assertThat(dsl.configuration().transactionListenerProviders()).hasSize(1); + }); + } + + @Test + void dslContextWithConfigurationCustomizersAreApplied() { + ConverterProvider converterProvider = mock(ConverterProvider.class); + CharsetProvider charsetProvider = mock(CharsetProvider.class); + this.contextRunner.withUserConfiguration(JooqDataSourceConfiguration.class) + .withBean("configurationCustomizer1", DefaultConfigurationCustomizer.class, + () -> (configuration) -> configuration.set(converterProvider)) + .withBean("configurationCustomizer2", DefaultConfigurationCustomizer.class, + () -> (configuration) -> configuration.set(charsetProvider)) + .run((context) -> { + DSLContext dsl = context.getBean(DSLContext.class); + assertThat(dsl.configuration().converterProvider()).isSameAs(converterProvider); + assertThat(dsl.configuration().charsetProvider()).isSameAs(charsetProvider); + }); + } + + @Test + @Deprecated + void customProvidersArePickedUp() { + RecordMapperProvider recordMapperProvider = mock(RecordMapperProvider.class); + RecordUnmapperProvider recordUnmapperProvider = mock(RecordUnmapperProvider.class); + RecordListenerProvider recordListenerProvider = mock(RecordListenerProvider.class); + VisitListenerProvider visitListenerProvider = mock(VisitListenerProvider.class); + TransactionListenerProvider transactionListenerProvider = mock(TransactionListenerProvider.class); + ExecutorProvider executorProvider = mock(ExecutorProvider.class); + this.contextRunner.withUserConfiguration(JooqDataSourceConfiguration.class, TxManagerConfiguration.class) + .withBean(RecordMapperProvider.class, () -> recordMapperProvider) + .withBean(RecordUnmapperProvider.class, () -> recordUnmapperProvider) + .withBean(RecordListenerProvider.class, () -> recordListenerProvider) + .withBean(VisitListenerProvider.class, () -> visitListenerProvider) + .withBean(TransactionListenerProvider.class, () -> transactionListenerProvider) + .withBean(ExecutorProvider.class, () -> executorProvider).run((context) -> { + DSLContext dsl = context.getBean(DSLContext.class); + assertThat(dsl.configuration().recordMapperProvider()).isSameAs(recordMapperProvider); + assertThat(dsl.configuration().recordUnmapperProvider()).isSameAs(recordUnmapperProvider); + assertThat(dsl.configuration().executorProvider()).isSameAs(executorProvider); + assertThat(dsl.configuration().recordListenerProviders()).containsExactly(recordListenerProvider); + assertThat(dsl.configuration().visitListenerProviders()).containsExactly(visitListenerProvider); + assertThat(dsl.configuration().transactionListenerProviders()) + .containsExactly(transactionListenerProvider); }); } @@ -201,33 +249,6 @@ PlatformTransactionManager transactionManager(DataSource dataSource) { } - static class TestRecordMapperProvider implements RecordMapperProvider { - - @Override - public RecordMapper provide(RecordType recordType, Class aClass) { - return null; - } - - } - - static class TestRecordUnmapperProvider implements RecordUnmapperProvider { - - @Override - public RecordUnmapper provide(Class aClass, RecordType recordType) { - return null; - } - - } - - static class TestRecordListenerProvider implements RecordListenerProvider { - - @Override - public RecordListener provide() { - return null; - } - - } - @Order(100) static class TestExecuteListenerProvider implements ExecuteListenerProvider { @@ -238,31 +259,4 @@ public ExecuteListener provide() { } - static class TestVisitListenerProvider implements VisitListenerProvider { - - @Override - public VisitListener provide() { - return null; - } - - } - - static class TestTransactionListenerProvider implements TransactionListenerProvider { - - @Override - public TransactionListener provide() { - return null; - } - - } - - static class TestExecutorProvider implements ExecutorProvider { - - @Override - public Executor provide() { - return null; - } - - } - } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslatorTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslatorTests.java index ea88b1c7efb5..f0dae1359ece 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslatorTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import org.jooq.Configuration; import org.jooq.ExecuteContext; import org.jooq.SQLDialect; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.ArgumentCaptor; @@ -28,9 +29,11 @@ import org.springframework.jdbc.BadSqlGrammarException; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.never; /** * Tests for {@link JooqExceptionTranslator} @@ -51,10 +54,21 @@ void exceptionTranslation(SQLDialect dialect, SQLException sqlException) { given(context.sqlException()).willReturn(sqlException); this.exceptionTranslator.exception(context); ArgumentCaptor captor = ArgumentCaptor.forClass(RuntimeException.class); - verify(context).exception(captor.capture()); + then(context).should().exception(captor.capture()); assertThat(captor.getValue()).isInstanceOf(BadSqlGrammarException.class); } + @Test + void whenExceptionCannotBeTranslatedThenExecuteContextExceptionIsNotCalled() { + ExecuteContext context = mock(ExecuteContext.class); + Configuration configuration = mock(Configuration.class); + given(context.configuration()).willReturn(configuration); + given(configuration.dialect()).willReturn(SQLDialect.POSTGRES); + given(context.sqlException()).willReturn(new SQLException(null, null, 123456789)); + this.exceptionTranslator.exception(context); + then(context).should(never()).exception(any()); + } + static Object[] exceptionTranslation() { return new Object[] { new Object[] { SQLDialect.DERBY, sqlException("42802") }, new Object[] { SQLDialect.H2, sqlException(42000) }, diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqPropertiesTests.java index 98d10be1dba4..88263b4bb3e3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqPropertiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,9 +33,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; /** * Tests for {@link JooqProperties}. @@ -59,7 +59,7 @@ void determineSqlDialectNoCheckIfDialectIsSet() throws SQLException { DataSource dataSource = mockStandaloneDataSource(); SQLDialect sqlDialect = properties.determineSqlDialect(dataSource); assertThat(sqlDialect).isEqualTo(SQLDialect.POSTGRES); - verify(dataSource, never()).getConnection(); + then(dataSource).should(never()).getConnection(); } @Test diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/NoDslContextBeanFailureAnalyzerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/NoDslContextBeanFailureAnalyzerTests.java new file mode 100644 index 000000000000..d70078b777c5 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/NoDslContextBeanFailureAnalyzerTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jooq; + +import org.jooq.DSLContext; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link NoDslContextBeanFailureAnalyzer}. + * + * @author Andy Wilkinson + */ +class NoDslContextBeanFailureAnalyzerTests { + + private final NoDslContextBeanFailureAnalyzer failureAnalyzer = new NoDslContextBeanFailureAnalyzer(); + + @Test + void noAnalysisWithoutR2dbcAutoConfiguration() { + new ApplicationContextRunner().run((context) -> { + this.failureAnalyzer.setBeanFactory(context.getBeanFactory()); + assertThat(this.failureAnalyzer.analyze(new NoSuchBeanDefinitionException(DSLContext.class))).isNull(); + }); + } + + @Test + void analysisWithR2dbcAutoConfiguration() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class)) + .run((context) -> { + this.failureAnalyzer.setBeanFactory(context.getBeanFactory()); + assertThat(this.failureAnalyzer.analyze(new NoSuchBeanDefinitionException(DSLContext.class))) + .isNotNull(); + }); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationIntegrationTests.java index 71c4d635e2ce..7c9925c92286 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationIntegrationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,9 +18,14 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; import org.apache.kafka.clients.admin.NewTopic; import org.apache.kafka.clients.producer.Producer; +import org.apache.kafka.streams.StreamsBuilder; +import org.apache.kafka.streams.kstream.KStream; +import org.apache.kafka.streams.kstream.KTable; +import org.apache.kafka.streams.kstream.Materialized; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -124,6 +129,12 @@ NewTopic adminCreated() { @EnableKafkaStreams static class KafkaStreamsConfig { + @Bean + KTable table(StreamsBuilder builder) { + KStream stream = builder.stream(Pattern.compile("test")); + return stream.groupByKey().count(Materialized.as("store")); + } + } static class Listener { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationTests.java index dd5c4b08197f..c2446cf6fe6c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import javax.security.auth.login.AppConfigurationEntry; +import org.apache.kafka.clients.CommonClientConfigs; import org.apache.kafka.clients.admin.AdminClientConfig; import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.producer.ProducerConfig; @@ -35,14 +36,13 @@ import org.apache.kafka.common.serialization.LongSerializer; import org.apache.kafka.streams.StreamsBuilder; import org.apache.kafka.streams.StreamsConfig; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.kafka.KafkaProperties.Listener; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; import org.springframework.kafka.annotation.EnableKafkaStreams; import org.springframework.kafka.annotation.KafkaStreamsDefaultConfiguration; import org.springframework.kafka.config.AbstractKafkaListenerContainerFactory; @@ -50,6 +50,8 @@ import org.springframework.kafka.config.KafkaListenerContainerFactory; import org.springframework.kafka.config.KafkaStreamsConfiguration; import org.springframework.kafka.config.StreamsBuilderFactoryBean; +import org.springframework.kafka.core.CleanupConfig; +import org.springframework.kafka.core.ConsumerFactory; import org.springframework.kafka.core.DefaultKafkaConsumerFactory; import org.springframework.kafka.core.DefaultKafkaProducerFactory; import org.springframework.kafka.core.KafkaAdmin; @@ -61,22 +63,21 @@ import org.springframework.kafka.listener.RecordInterceptor; import org.springframework.kafka.listener.SeekToCurrentBatchErrorHandler; import org.springframework.kafka.listener.SeekToCurrentErrorHandler; +import org.springframework.kafka.listener.adapter.RecordFilterStrategy; import org.springframework.kafka.security.jaas.KafkaJaasLoginModuleInitializer; import org.springframework.kafka.support.converter.BatchMessageConverter; import org.springframework.kafka.support.converter.BatchMessagingMessageConverter; import org.springframework.kafka.support.converter.MessagingMessageConverter; import org.springframework.kafka.support.converter.RecordMessageConverter; -import org.springframework.kafka.transaction.ChainedKafkaTransactionManager; import org.springframework.kafka.transaction.KafkaAwareTransactionManager; import org.springframework.kafka.transaction.KafkaTransactionManager; import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.transaction.PlatformTransactionManager; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; /** * Tests for {@link KafkaAutoConfiguration}. @@ -106,6 +107,7 @@ void consumerProperties() { "spring.kafka.consumer.properties.fiz.buz=fix.fox", "spring.kafka.consumer.fetch-min-size=1KB", "spring.kafka.consumer.group-id=bar", "spring.kafka.consumer.heartbeat-interval=234", "spring.kafka.consumer.isolation-level = read-committed", + "spring.kafka.consumer.security.protocol = SSL", "spring.kafka.consumer.key-deserializer = org.apache.kafka.common.serialization.LongDeserializer", "spring.kafka.consumer.value-deserializer = org.apache.kafka.common.serialization.IntegerDeserializer") .run((context) -> { @@ -137,6 +139,7 @@ void consumerProperties() { assertThat(configs.get(ConsumerConfig.ISOLATION_LEVEL_CONFIG)).isEqualTo("read_committed"); assertThat(configs.get(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG)) .isEqualTo(LongDeserializer.class); + assertThat(configs.get(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG)).isEqualTo("SSL"); assertThat(configs.get(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG)) .isEqualTo(IntegerDeserializer.class); assertThat(configs.get(ConsumerConfig.MAX_POLL_RECORDS_CONFIG)).isEqualTo(42); @@ -156,7 +159,7 @@ void producerProperties() { "spring.kafka.producer.buffer-memory=4KB", "spring.kafka.producer.compression-type=gzip", "spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.LongSerializer", "spring.kafka.producer.retries=2", "spring.kafka.producer.properties.fiz.buz=fix.fox", - "spring.kafka.producer.ssl.key-password=p4", + "spring.kafka.producer.security.protocol=SSL", "spring.kafka.producer.ssl.key-password=p4", "spring.kafka.producer.ssl.key-store-location=classpath:ksLocP", "spring.kafka.producer.ssl.key-store-password=p5", "spring.kafka.producer.ssl.key-store-type=PKCS12", "spring.kafka.producer.ssl.trust-store-location=classpath:tsLocP", @@ -177,6 +180,7 @@ void producerProperties() { assertThat(configs.get(ProducerConfig.BUFFER_MEMORY_CONFIG)).isEqualTo(4096L); assertThat(configs.get(ProducerConfig.COMPRESSION_TYPE_CONFIG)).isEqualTo("gzip"); assertThat(configs.get(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG)).isEqualTo(LongSerializer.class); + assertThat(configs.get(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG)).isEqualTo("SSL"); assertThat(configs.get(SslConfigs.SSL_KEY_PASSWORD_CONFIG)).isEqualTo("p4"); assertThat((String) configs.get(SslConfigs.SSL_KEYSTORE_LOCATION_CONFIG)) .endsWith(File.separator + "ksLocP"); @@ -202,7 +206,7 @@ void adminProperties() { this.contextRunner .withPropertyValues("spring.kafka.clientId=cid", "spring.kafka.properties.foo.bar.baz=qux.fiz.buz", "spring.kafka.admin.fail-fast=true", "spring.kafka.admin.properties.fiz.buz=fix.fox", - "spring.kafka.admin.ssl.key-password=p4", + "spring.kafka.admin.security.protocol=SSL", "spring.kafka.admin.ssl.key-password=p4", "spring.kafka.admin.ssl.key-store-location=classpath:ksLocP", "spring.kafka.admin.ssl.key-store-password=p5", "spring.kafka.admin.ssl.key-store-type=PKCS12", "spring.kafka.admin.ssl.trust-store-location=classpath:tsLocP", @@ -210,10 +214,11 @@ void adminProperties() { "spring.kafka.admin.ssl.trust-store-type=PKCS12", "spring.kafka.admin.ssl.protocol=TLSv1.2") .run((context) -> { KafkaAdmin admin = context.getBean(KafkaAdmin.class); - Map configs = admin.getConfig(); + Map configs = admin.getConfigurationProperties(); // common assertThat(configs.get(AdminClientConfig.CLIENT_ID_CONFIG)).isEqualTo("cid"); // admin + assertThat(configs.get(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG)).isEqualTo("SSL"); assertThat(configs.get(SslConfigs.SSL_KEY_PASSWORD_CONFIG)).isEqualTo("p4"); assertThat((String) configs.get(SslConfigs.SSL_KEYSTORE_LOCATION_CONFIG)) .endsWith(File.separator + "ksLocP"); @@ -240,7 +245,7 @@ void streamsProperties() { "spring.kafka.streams.auto-startup=false", "spring.kafka.streams.cache-max-size-buffering=1KB", "spring.kafka.streams.client-id=override", "spring.kafka.streams.properties.fiz.buz=fix.fox", "spring.kafka.streams.replication-factor=2", "spring.kafka.streams.state-dir=/tmp/state", - "spring.kafka.streams.ssl.key-password=p7", + "spring.kafka.streams.security.protocol=SSL", "spring.kafka.streams.ssl.key-password=p7", "spring.kafka.streams.ssl.key-store-location=classpath:ksLocP", "spring.kafka.streams.ssl.key-store-password=p8", "spring.kafka.streams.ssl.key-store-type=PKCS12", "spring.kafka.streams.ssl.trust-store-location=classpath:tsLocP", @@ -255,6 +260,7 @@ void streamsProperties() { assertThat(configs.get(StreamsConfig.CACHE_MAX_BYTES_BUFFERING_CONFIG)).isEqualTo(1024); assertThat(configs.get(StreamsConfig.CLIENT_ID_CONFIG)).isEqualTo("override"); assertThat(configs.get(StreamsConfig.REPLICATION_FACTOR_CONFIG)).isEqualTo(2); + assertThat(configs.get(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG)).isEqualTo("SSL"); assertThat(configs.get(StreamsConfig.STATE_DIR_CONFIG)).isEqualTo("/tmp/state"); assertThat(configs.get(SslConfigs.SSL_KEY_PASSWORD_CONFIG)).isEqualTo("p7"); assertThat((String) configs.get(SslConfigs.SSL_KEYSTORE_LOCATION_CONFIG)) @@ -326,10 +332,30 @@ void streamsWithSeveralStreamsBuilderFactoryBeans() { .asProperties(); assertThat((List) configs.get(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG)) .containsExactly("localhost:9092", "localhost:9093"); - verify(context.getBean("&firstStreamsBuilderFactoryBean", StreamsBuilderFactoryBean.class), never()) - .setAutoStartup(false); - verify(context.getBean("&secondStreamsBuilderFactoryBean", StreamsBuilderFactoryBean.class), - never()).setAutoStartup(false); + then(context.getBean("&firstStreamsBuilderFactoryBean", StreamsBuilderFactoryBean.class)) + .should(never()).setAutoStartup(false); + then(context.getBean("&secondStreamsBuilderFactoryBean", StreamsBuilderFactoryBean.class)) + .should(never()).setAutoStartup(false); + }); + } + + @Test + void streamsWithCleanupConfig() { + this.contextRunner + .withUserConfiguration(EnableKafkaStreamsConfiguration.class, TestKafkaStreamsConfiguration.class) + .withPropertyValues("spring.application.name=my-test-app", + "spring.kafka.bootstrap-servers=localhost:9092,localhost:9093", + "spring.kafka.streams.auto-startup=false", "spring.kafka.streams.cleanup.on-startup=true", + "spring.kafka.streams.cleanup.on-shutdown=false") + .run((context) -> { + StreamsBuilderFactoryBean streamsBuilderFactoryBean = context + .getBean(StreamsBuilderFactoryBean.class); + assertThat(streamsBuilderFactoryBean) + .extracting("cleanupConfig", InstanceOfAssertFactories.type(CleanupConfig.class)) + .satisfies((cleanupConfig) -> { + assertThat(cleanupConfig.cleanupOnStart()).isTrue(); + assertThat(cleanupConfig.cleanupOnStop()).isFalse(); + }); }); } @@ -361,9 +387,10 @@ void listenerProperties() { "spring.kafka.listener.ack-count=123", "spring.kafka.listener.ack-time=456", "spring.kafka.listener.concurrency=3", "spring.kafka.listener.poll-timeout=2000", "spring.kafka.listener.no-poll-threshold=2.5", "spring.kafka.listener.type=batch", - "spring.kafka.listener.idle-event-interval=1s", "spring.kafka.listener.monitor-interval=45", - "spring.kafka.listener.log-container-config=true", - "spring.kafka.listener.missing-topics-fatal=false", "spring.kafka.jaas.enabled=true", + "spring.kafka.listener.idle-between-polls=1s", "spring.kafka.listener.idle-event-interval=1s", + "spring.kafka.listener.monitor-interval=45", "spring.kafka.listener.log-container-config=true", + "spring.kafka.listener.only-log-record-metadata=true", + "spring.kafka.listener.missing-topics-fatal=true", "spring.kafka.jaas.enabled=true", "spring.kafka.producer.transaction-id-prefix=foo", "spring.kafka.jaas.login-module=foo", "spring.kafka.jaas.control-flag=REQUISITE", "spring.kafka.jaas.options.useKeyTab=true") .run((context) -> { @@ -385,11 +412,13 @@ void listenerProperties() { assertThat(containerProperties.getAckTime()).isEqualTo(456L); assertThat(containerProperties.getPollTimeout()).isEqualTo(2000L); assertThat(containerProperties.getNoPollThreshold()).isEqualTo(2.5f); + assertThat(containerProperties.getIdleBetweenPolls()).isEqualTo(1000L); assertThat(containerProperties.getIdleEventInterval()).isEqualTo(1000L); assertThat(containerProperties.getMonitorInterval()).isEqualTo(45); assertThat(containerProperties.isLogContainerConfig()).isTrue(); - assertThat(containerProperties.isMissingTopicsFatal()).isFalse(); - assertThat(ReflectionTestUtils.getField(kafkaListenerContainerFactory, "concurrency")).isEqualTo(3); + assertThat(containerProperties.isOnlyLogRecordMetadata()).isTrue(); + assertThat(containerProperties.isMissingTopicsFatal()).isTrue(); + assertThat(kafkaListenerContainerFactory).extracting("concurrency").isEqualTo(3); assertThat(kafkaListenerContainerFactory.isBatchListener()).isTrue(); assertThat(context.getBeansOfType(KafkaJaasLoginModuleInitializer.class)).hasSize(1); KafkaJaasLoginModuleInitializer jaas = context.getBean(KafkaJaasLoginModuleInitializer.class); @@ -402,17 +431,6 @@ void listenerProperties() { }); } - @Test - void listenerPropertiesMatchDefaults() { - this.contextRunner.run((context) -> { - Listener listenerProperties = new KafkaProperties().getListener(); - AbstractKafkaListenerContainerFactory kafkaListenerContainerFactory = (AbstractKafkaListenerContainerFactory) context - .getBean(KafkaListenerContainerFactory.class); - ContainerProperties containerProperties = kafkaListenerContainerFactory.getContainerProperties(); - assertThat(containerProperties.isMissingTopicsFatal()).isEqualTo(listenerProperties.isMissingTopicsFatal()); - }); - } - @Test void testKafkaTemplateRecordMessageConverters() { this.contextRunner.withUserConfiguration(MessageConverterConfiguration.class) @@ -469,6 +487,25 @@ void testConcurrentKafkaListenerContainerFactoryInBatchModeWithNoMessageConverte }); } + @Test + void testConcurrentKafkaListenerContainerFactoryWithDefaultRecordFilterStrategy() { + this.contextRunner.run((context) -> { + ConcurrentKafkaListenerContainerFactory factory = context + .getBean(ConcurrentKafkaListenerContainerFactory.class); + assertThat(factory).hasFieldOrPropertyWithValue("recordFilterStrategy", null); + }); + } + + @Test + void testConcurrentKafkaListenerContainerFactoryWithCustomRecordFilterStrategy() { + this.contextRunner.withUserConfiguration(RecordFilterStrategyConfiguration.class).run((context) -> { + ConcurrentKafkaListenerContainerFactory factory = context + .getBean(ConcurrentKafkaListenerContainerFactory.class); + assertThat(factory).hasFieldOrPropertyWithValue("recordFilterStrategy", + context.getBean("recordFilterStrategy")); + }); + } + @Test void testConcurrentKafkaListenerContainerFactoryWithCustomErrorHandler() { this.contextRunner.withUserConfiguration(ErrorHandlerConfiguration.class).run((context) -> { @@ -520,13 +557,16 @@ void testConcurrentKafkaListenerContainerFactoryWithDefaultTransactionManager() } @Test + @SuppressWarnings("unchecked") void testConcurrentKafkaListenerContainerFactoryWithCustomTransactionManager() { - this.contextRunner.withUserConfiguration(TransactionManagerConfiguration.class) + KafkaTransactionManager customTransactionManager = mock(KafkaTransactionManager.class); + this.contextRunner + .withBean("customTransactionManager", KafkaTransactionManager.class, () -> customTransactionManager) .withPropertyValues("spring.kafka.producer.transaction-id-prefix=test").run((context) -> { ConcurrentKafkaListenerContainerFactory factory = context .getBean(ConcurrentKafkaListenerContainerFactory.class); assertThat(factory.getContainerProperties().getTransactionManager()) - .isSameAs(context.getBean("chainedTransactionManager")); + .isSameAs(context.getBean("customTransactionManager")); }); } @@ -569,6 +609,30 @@ void testConcurrentKafkaListenerContainerFactoryWithKafkaTemplate() { }); } + @Test + void testConcurrentKafkaListenerContainerFactoryWithCustomConsumerFactory() { + this.contextRunner.withUserConfiguration(ConsumerFactoryConfiguration.class).run((context) -> { + ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory = context + .getBean(ConcurrentKafkaListenerContainerFactory.class); + assertThat(kafkaListenerContainerFactory.getConsumerFactory()) + .isNotSameAs(context.getBean(ConsumerFactoryConfiguration.class).consumerFactory); + }); + } + + @Test + void specificSecurityProtocolOverridesCommonSecurityProtocol() { + this.contextRunner.withPropertyValues("spring.kafka.security.protocol=SSL", + "spring.kafka.admin.security.protocol=PLAINTEXT").run((context) -> { + DefaultKafkaProducerFactory producerFactory = context + .getBean(DefaultKafkaProducerFactory.class); + Map producerConfigs = producerFactory.getConfigurationProperties(); + assertThat(producerConfigs.get(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG)).isEqualTo("SSL"); + KafkaAdmin admin = context.getBean(KafkaAdmin.class); + Map configs = admin.getConfigurationProperties(); + assertThat(configs.get(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG)).isEqualTo("PLAINTEXT"); + }); + } + @Configuration(proxyBeanMethods = false) static class MessageConverterConfiguration { @@ -589,6 +653,16 @@ BatchMessageConverter myBatchMessageConverter() { } + @Configuration(proxyBeanMethods = false) + static class RecordFilterStrategyConfiguration { + + @Bean + RecordFilterStrategy recordFilterStrategy() { + return (record) -> false; + } + + } + @Configuration(proxyBeanMethods = false) static class ErrorHandlerConfiguration { @@ -610,25 +684,26 @@ SeekToCurrentBatchErrorHandler batchErrorHandler() { } @Configuration(proxyBeanMethods = false) - static class TransactionManagerConfiguration { + static class AfterRollbackProcessorConfiguration { @Bean - @Primary - PlatformTransactionManager chainedTransactionManager( - KafkaTransactionManager kafkaTransactionManager) { - return new ChainedKafkaTransactionManager(kafkaTransactionManager); + AfterRollbackProcessor afterRollbackProcessor() { + return (records, consumer, container, ex, recoverable, eosMode) -> { + // no-op + }; } } @Configuration(proxyBeanMethods = false) - static class AfterRollbackProcessorConfiguration { + static class ConsumerFactoryConfiguration { + + @SuppressWarnings("unchecked") + private final ConsumerFactory consumerFactory = mock(ConsumerFactory.class); @Bean - AfterRollbackProcessor afterRollbackProcessor() { - return (records, consumer, ex, recoverable) -> { - // no-op - }; + ConsumerFactory myConsumerFactory() { + return this.consumerFactory; } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaPropertiesTests.java new file mode 100644 index 000000000000..9e67fdb4cd83 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaPropertiesTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.kafka; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.kafka.KafkaProperties.Cleanup; +import org.springframework.boot.autoconfigure.kafka.KafkaProperties.IsolationLevel; +import org.springframework.boot.autoconfigure.kafka.KafkaProperties.Listener; +import org.springframework.kafka.core.CleanupConfig; +import org.springframework.kafka.listener.ContainerProperties; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link KafkaProperties}. + * + * @author Stephane Nicoll + */ +class KafkaPropertiesTests { + + @SuppressWarnings("rawtypes") + @Test + void isolationLevelEnumConsistentWithKafkaVersion() { + org.apache.kafka.common.IsolationLevel[] original = org.apache.kafka.common.IsolationLevel.values(); + assertThat(original).extracting(Enum::name).containsExactly(IsolationLevel.READ_UNCOMMITTED.name(), + IsolationLevel.READ_COMMITTED.name()); + assertThat(original).extracting("id").containsExactly(IsolationLevel.READ_UNCOMMITTED.id(), + IsolationLevel.READ_COMMITTED.id()); + assertThat(original).hasSize(IsolationLevel.values().length); + } + + @Test + void listenerDefaultValuesAreConsistent() { + ContainerProperties container = new ContainerProperties("test"); + Listener listenerProperties = new KafkaProperties().getListener(); + assertThat(listenerProperties.isOnlyLogRecordMetadata()).isEqualTo(container.isOnlyLogRecordMetadata()); + assertThat(listenerProperties.isMissingTopicsFatal()).isEqualTo(container.isMissingTopicsFatal()); + } + + @Test + void cleanupConfigDefaultValuesAreConsistent() { + CleanupConfig cleanupConfig = new CleanupConfig(); + Cleanup cleanup = new KafkaProperties().getStreams().getCleanup(); + assertThat(cleanup.isOnStartup()).isEqualTo(cleanupConfig.cleanupOnStart()); + assertThat(cleanup.isOnShutdown()).isEqualTo(cleanupConfig.cleanupOnStop()); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationAvailabilityAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationAvailabilityAutoConfigurationTests.java new file mode 100644 index 000000000000..144db9dcd04b --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kubernetes/ApplicationAvailabilityAutoConfigurationTests.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.kubernetes; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration; +import org.springframework.boot.availability.ApplicationAvailability; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ApplicationAvailabilityAutoConfiguration} + * + * @author Brian Clozel + */ +class ApplicationAvailabilityAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ApplicationAvailabilityAutoConfiguration.class)); + + @Test + void providerIsPresent() { + this.contextRunner.run(((context) -> assertThat(context).hasSingleBean(ApplicationAvailability.class))); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ldap/LdapAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ldap/LdapAutoConfigurationTests.java index 796b3a99f52e..e6f7dc88e900 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ldap/LdapAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ldap/LdapAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,11 +24,14 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.ldap.core.LdapTemplate; +import org.springframework.ldap.core.support.DirContextAuthenticationStrategy; import org.springframework.ldap.core.support.LdapContextSource; +import org.springframework.ldap.core.support.SimpleDirContextAuthenticationStrategy; import org.springframework.ldap.pool2.factory.PoolConfig; import org.springframework.ldap.pool2.factory.PooledContextSource; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * Tests for {@link LdapAutoConfiguration}. @@ -39,7 +42,7 @@ */ class LdapAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(LdapAutoConfiguration.class)); @Test @@ -47,7 +50,7 @@ void contextSourceWithDefaultUrl() { this.contextRunner.run((context) -> { LdapContextSource contextSource = context.getBean(LdapContextSource.class); assertThat(contextSource.getUrls()).containsExactly("ldap://localhost:389"); - assertThat(contextSource.isAnonymousReadOnly()).isFalse(); + assertThat(contextSource.isAnonymousReadOnly()).isTrue(); }); } @@ -70,6 +73,15 @@ void contextSourceWithSeveralUrls() { }); } + @Test + void contextSourceWithUserDoesNotEnableAnonymousReadOnly() { + this.contextRunner.withPropertyValues("spring.ldap.username:root").run((context) -> { + LdapContextSource contextSource = context.getBean(LdapContextSource.class); + assertThat(contextSource.getUserDn()).isEqualTo("root"); + assertThat(contextSource.isAnonymousReadOnly()).isFalse(); + }); + } + @Test void contextSourceWithExtraCustomization() { this.contextRunner.withPropertyValues("spring.ldap.urls:ldap://localhost:123", "spring.ldap.username:root", @@ -93,15 +105,34 @@ void contextSourceWithNoCustomization() { LdapContextSource contextSource = context.getBean(LdapContextSource.class); assertThat(contextSource.getUserDn()).isEqualTo(""); assertThat(contextSource.getPassword()).isEqualTo(""); - assertThat(contextSource.isAnonymousReadOnly()).isFalse(); + assertThat(contextSource.isAnonymousReadOnly()).isTrue(); assertThat(contextSource.getBaseLdapPathAsString()).isEqualTo(""); }); } @Test void templateExists() { - this.contextRunner.withPropertyValues("spring.ldap.urls:ldap://localhost:389") - .run((context) -> assertThat(context).hasSingleBean(LdapTemplate.class)); + this.contextRunner.withPropertyValues("spring.ldap.urls:ldap://localhost:389").run((context) -> { + assertThat(context).hasSingleBean(LdapTemplate.class); + LdapTemplate ldapTemplate = context.getBean(LdapTemplate.class); + assertThat(ldapTemplate).hasFieldOrPropertyWithValue("ignorePartialResultException", false); + assertThat(ldapTemplate).hasFieldOrPropertyWithValue("ignoreNameNotFoundException", false); + assertThat(ldapTemplate).hasFieldOrPropertyWithValue("ignoreSizeLimitExceededException", true); + }); + } + + @Test + void templateConfigurationCanBeCustomized() { + this.contextRunner.withPropertyValues("spring.ldap.urls:ldap://localhost:389", + "spring.ldap.template.ignorePartialResultException=true", + "spring.ldap.template.ignoreNameNotFoundException=true", + "spring.ldap.template.ignoreSizeLimitExceededException=false").run((context) -> { + assertThat(context).hasSingleBean(LdapTemplate.class); + LdapTemplate ldapTemplate = context.getBean(LdapTemplate.class); + assertThat(ldapTemplate).hasFieldOrPropertyWithValue("ignorePartialResultException", true); + assertThat(ldapTemplate).hasFieldOrPropertyWithValue("ignoreNameNotFoundException", true); + assertThat(ldapTemplate).hasFieldOrPropertyWithValue("ignoreSizeLimitExceededException", false); + }); } @Test @@ -109,10 +140,34 @@ void contextSourceWithUserProvidedPooledContextSource() { this.contextRunner.withUserConfiguration(PooledContextSourceConfig.class).run((context) -> { LdapContextSource contextSource = context.getBean(LdapContextSource.class); assertThat(contextSource.getUrls()).containsExactly("ldap://localhost:389"); - assertThat(contextSource.isAnonymousReadOnly()).isFalse(); + assertThat(contextSource.isAnonymousReadOnly()).isTrue(); + }); + } + + @Test + void contextSourceWithCustomUniqueDirContextAuthenticationStrategy() { + this.contextRunner.withUserConfiguration(CustomDirContextAuthenticationStrategy.class).run((context) -> { + assertThat(context).hasSingleBean(DirContextAuthenticationStrategy.class); + LdapContextSource contextSource = context.getBean(LdapContextSource.class); + assertThat(contextSource).extracting("authenticationStrategy") + .isSameAs(context.getBean("customDirContextAuthenticationStrategy")); }); } + @Test + void contextSourceWithCustomNonUniqueDirContextAuthenticationStrategy() { + this.contextRunner.withUserConfiguration(CustomDirContextAuthenticationStrategy.class, + AnotherCustomDirContextAuthenticationStrategy.class).run((context) -> { + assertThat(context).hasBean("customDirContextAuthenticationStrategy") + .hasBean("anotherCustomDirContextAuthenticationStrategy"); + LdapContextSource contextSource = context.getBean(LdapContextSource.class); + assertThat(contextSource).extracting("authenticationStrategy") + .isNotSameAs(context.getBean("customDirContextAuthenticationStrategy")) + .isNotSameAs(context.getBean("anotherCustomDirContextAuthenticationStrategy")) + .isInstanceOf(SimpleDirContextAuthenticationStrategy.class); + }); + } + @Configuration(proxyBeanMethods = false) static class PooledContextSourceConfig { @@ -126,4 +181,24 @@ PooledContextSource pooledContextSource(LdapContextSource ldapContextSource) { } + @Configuration(proxyBeanMethods = false) + static class CustomDirContextAuthenticationStrategy { + + @Bean + DirContextAuthenticationStrategy customDirContextAuthenticationStrategy() { + return mock(DirContextAuthenticationStrategy.class); + } + + } + + @Configuration(proxyBeanMethods = false) + static class AnotherCustomDirContextAuthenticationStrategy { + + @Bean + DirContextAuthenticationStrategy anotherCustomDirContextAuthenticationStrategy() { + return mock(DirContextAuthenticationStrategy.class); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ldap/LdapPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ldap/LdapPropertiesTests.java new file mode 100644 index 000000000000..c8d4a295e57a --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ldap/LdapPropertiesTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.ldap; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.ldap.LdapProperties.Template; +import org.springframework.ldap.core.LdapTemplate; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link LdapProperties} + * + * @author Filip Hrisafov + */ +class LdapPropertiesTests { + + @Test + void ldapTemplatePropertiesUseConsistentLdapTemplateDefaultValues() { + Template templateProperties = new LdapProperties().getTemplate(); + LdapTemplate ldapTemplate = new LdapTemplate(); + assertThat(ldapTemplate).hasFieldOrPropertyWithValue("ignorePartialResultException", + templateProperties.isIgnorePartialResultException()); + assertThat(ldapTemplate).hasFieldOrPropertyWithValue("ignoreNameNotFoundException", + templateProperties.isIgnoreNameNotFoundException()); + assertThat(ldapTemplate).hasFieldOrPropertyWithValue("ignoreSizeLimitExceededException", + templateProperties.isIgnoreSizeLimitExceededException()); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ldap/embedded/EmbeddedLdapAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ldap/embedded/EmbeddedLdapAutoConfigurationTests.java index 77604b248aab..dc3d02c6f9eb 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ldap/embedded/EmbeddedLdapAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ldap/embedded/EmbeddedLdapAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,12 +27,15 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.ldap.core.ContextSource; import org.springframework.ldap.core.LdapTemplate; +import org.springframework.ldap.core.support.LdapContextSource; import static org.assertj.core.api.Assertions.assertThat; @@ -43,7 +46,7 @@ */ class EmbeddedLdapAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(EmbeddedLdapAutoConfiguration.class)); @Test @@ -146,6 +149,35 @@ void testMultiBaseDn() { }); } + @Test + void ldapContextSourceWithCredentialsIsCreated() { + this.contextRunner.withPropertyValues("spring.ldap.embedded.base-dn:dc=spring,dc=org", + "spring.ldap.embedded.credential.username:uid=root", "spring.ldap.embedded.credential.password:boot") + .run((context) -> { + LdapContextSource ldapContextSource = context.getBean(LdapContextSource.class); + assertThat(ldapContextSource.getUrls()).isNotEmpty(); + assertThat(ldapContextSource.getUserDn()).isEqualTo("uid=root"); + }); + } + + @Test + void ldapContextSourceWithoutCredentialsIsCreated() { + this.contextRunner.withPropertyValues("spring.ldap.embedded.base-dn:dc=spring,dc=org").run((context) -> { + LdapContextSource ldapContextSource = context.getBean(LdapContextSource.class); + assertThat(ldapContextSource.getUrls()).isNotEmpty(); + assertThat(ldapContextSource.getUserDn()).isEmpty(); + }); + } + + @Test + void ldapContextWithoutSpringLdapIsNotCreated() { + this.contextRunner.withPropertyValues("spring.ldap.embedded.base-dn:dc=spring,dc=org") + .withClassLoader(new FilteredClassLoader(ContextSource.class)).run((context) -> { + assertThat(context).hasNotFailed(); + assertThat(context).doesNotHaveBean(LdapContextSource.class); + }); + } + @Configuration(proxyBeanMethods = false) static class LdapClientConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java index 8774a35154a8..58959e3c7b73 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,31 +20,28 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.sql.Connection; +import java.sql.SQLException; import java.util.Map; +import java.util.UUID; import java.util.function.Consumer; import javax.sql.DataSource; -import com.zaxxer.hikari.HikariDataSource; import liquibase.integration.spring.SpringLiquibase; -import liquibase.logging.core.Slf4jLogger; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledOnJre; -import org.junit.jupiter.api.condition.JRE; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.io.TempDir; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration; +import org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; -import org.springframework.boot.context.event.ApplicationStartingEvent; import org.springframework.boot.jdbc.DataSourceBuilder; -import org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener; +import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ContextConsumer; @@ -54,6 +51,10 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.SimpleDriverDataSource; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -69,17 +70,13 @@ * @author Dominic Gunn * @author András Deák * @author Andrii Hrytsiuk + * @author Ferenc Gratzer + * @author Evgeniy Cheban */ @ExtendWith(OutputCaptureExtension.class) class LiquibaseAutoConfigurationTests { - @BeforeEach - void init() { - new LiquibaseServiceLocatorApplicationListener() - .onApplicationEvent(new ApplicationStartingEvent(new SpringApplication(Object.class), new String[0])); - } - - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(LiquibaseAutoConfiguration.class)) .withPropertyValues("spring.datasource.generate-unique-name=true"); @@ -90,12 +87,18 @@ void backsOffWithNoDataSourceBeanAndNoLiquibaseUrl() { @Test void createsDataSourceWithNoDataSourceBeanAndLiquibaseUrl() { - this.contextRunner.withPropertyValues("spring.liquibase.url:jdbc:hsqldb:mem:liquibase") - .run(assertLiquibase((liquibase) -> { - DataSource dataSource = liquibase.getDataSource(); - assertThat(((HikariDataSource) dataSource).isClosed()).isTrue(); - assertThat(((HikariDataSource) dataSource).getJdbcUrl()).isEqualTo("jdbc:hsqldb:mem:liquibase"); - })); + String jdbcUrl = "jdbc:hsqldb:mem:liquibase" + UUID.randomUUID(); + this.contextRunner.withPropertyValues("spring.liquibase.url:" + jdbcUrl).run(assertLiquibase((liquibase) -> { + SimpleDriverDataSource dataSource = (SimpleDriverDataSource) liquibase.getDataSource(); + assertThat(dataSource.getUrl()).isEqualTo(jdbcUrl); + })); + } + + @Test + void backsOffWithLiquibaseUrlAndNoSpringJdbc() { + this.contextRunner.withPropertyValues("spring.liquibase.url:jdbc:hsqldb:mem:" + UUID.randomUUID()) + .withClassLoader(new FilteredClassLoader("org.springframework.jdbc")) + .run((context) -> assertThat(context).doesNotHaveBean(SpringLiquibase.class)); } @Test @@ -106,6 +109,7 @@ void defaultSpringLiquibase() { assertThat(liquibase.getContexts()).isNull(); assertThat(liquibase.getDefaultSchema()).isNull(); assertThat(liquibase.isDropFirst()).isFalse(); + assertThat(liquibase.isClearCheckSums()).isFalse(); })); } @@ -126,7 +130,6 @@ void changelogJson() { } @Test - @EnabledOnJre(JRE.JAVA_8) void changelogSql() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) .withPropertyValues("spring.liquibase.change-log:classpath:/db/changelog/db.changelog-override.sql") @@ -143,6 +146,7 @@ void defaultValues() { assertThat(liquibase.getDatabaseChangeLogLockTable()) .isEqualTo(properties.getDatabaseChangeLogLockTable()); assertThat(liquibase.isDropFirst()).isEqualTo(properties.isDropFirst()); + assertThat(liquibase.isClearCheckSums()).isEqualTo(properties.isClearChecksums()); assertThat(liquibase.isTestRollbackOnUpdate()).isEqualTo(properties.isTestRollbackOnUpdate()); })); } @@ -189,40 +193,72 @@ void overrideDropFirst() { .run(assertLiquibase((liquibase) -> assertThat(liquibase.isDropFirst()).isTrue())); } + @Test + void overrideClearChecksums() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.liquibase.clear-checksums:true") + .run(assertLiquibase((liquibase) -> assertThat(liquibase.isClearCheckSums()).isTrue())); + } + @Test void overrideDataSource() { + String jdbcUrl = "jdbc:hsqldb:mem:liquibase" + UUID.randomUUID(); + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.liquibase.url:" + jdbcUrl).run(assertLiquibase((liquibase) -> { + SimpleDriverDataSource dataSource = (SimpleDriverDataSource) liquibase.getDataSource(); + assertThat(dataSource.getUrl()).isEqualTo(jdbcUrl); + assertThat(dataSource.getDriver().getClass().getName()).isEqualTo("org.hsqldb.jdbc.JDBCDriver"); + })); + } + + @Test + void overrideDataSourceAndDriverClassName() { + String jdbcUrl = "jdbc:hsqldb:mem:liquibase" + UUID.randomUUID(); + String driverClassName = "org.hsqldb.jdbcDriver"; this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) - .withPropertyValues("spring.liquibase.url:jdbc:hsqldb:mem:liquibase") + .withPropertyValues("spring.liquibase.url:" + jdbcUrl, + "spring.liquibase.driver-class-name:" + driverClassName) .run(assertLiquibase((liquibase) -> { - DataSource dataSource = liquibase.getDataSource(); - assertThat(((HikariDataSource) dataSource).isClosed()).isTrue(); - assertThat(((HikariDataSource) dataSource).getJdbcUrl()).isEqualTo("jdbc:hsqldb:mem:liquibase"); + SimpleDriverDataSource dataSource = (SimpleDriverDataSource) liquibase.getDataSource(); + assertThat(dataSource.getUrl()).isEqualTo(jdbcUrl); + assertThat(dataSource.getDriver().getClass().getName()).isEqualTo(driverClassName); })); } @Test void overrideUser() { - String jdbcUrl = "jdbc:hsqldb:mem:normal"; + String databaseName = "normal" + UUID.randomUUID(); this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) - .withPropertyValues("spring.datasource.url:" + jdbcUrl, "spring.datasource.username:not-sa", + .withPropertyValues("spring.datasource.generate-unique-name:false", + "spring.datasource.name:" + databaseName, "spring.datasource.username:not-sa", "spring.liquibase.user:sa") .run(assertLiquibase((liquibase) -> { - DataSource dataSource = liquibase.getDataSource(); - assertThat(((HikariDataSource) dataSource).isClosed()).isTrue(); - assertThat(((HikariDataSource) dataSource).getJdbcUrl()).isEqualTo(jdbcUrl); - assertThat(((HikariDataSource) dataSource).getUsername()).isEqualTo("sa"); + SimpleDriverDataSource dataSource = (SimpleDriverDataSource) liquibase.getDataSource(); + assertThat(dataSource.getUrl()).contains("jdbc:h2:mem:" + databaseName); + assertThat(dataSource.getUsername()).isEqualTo("sa"); })); } @Test - void overrideDataSourceAndFallbackToEmbeddedProperties() { + void overrideUserWhenCustom() { + this.contextRunner.withUserConfiguration(CustomDataSourceConfiguration.class) + .withPropertyValues("spring.liquibase.user:test", "spring.liquibase.password:secret").run((context) -> { + String expectedName = context.getBean(CustomDataSourceConfiguration.class).name; + SpringLiquibase liquibase = context.getBean(SpringLiquibase.class); + SimpleDriverDataSource dataSource = (SimpleDriverDataSource) liquibase.getDataSource(); + assertThat(dataSource.getUrl()).contains(expectedName); + assertThat(dataSource.getUsername()).isEqualTo("test"); + }); + } + + @Test + void createDataSourceDoesNotFallbackToEmbeddedProperties() { + String jdbcUrl = "jdbc:hsqldb:mem:liquibase" + UUID.randomUUID(); this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) - .withPropertyValues("spring.liquibase.url:jdbc:hsqldb:mem:liquibase") - .run(assertLiquibase((liquibase) -> { - DataSource dataSource = liquibase.getDataSource(); - assertThat(((HikariDataSource) dataSource).isClosed()).isTrue(); - assertThat(((HikariDataSource) dataSource).getUsername()).isEqualTo("sa"); - assertThat(((HikariDataSource) dataSource).getPassword()).isEqualTo(""); + .withPropertyValues("spring.liquibase.url:" + jdbcUrl).run(assertLiquibase((liquibase) -> { + SimpleDriverDataSource dataSource = (SimpleDriverDataSource) liquibase.getDataSource(); + assertThat(dataSource.getUsername()).isNull(); + assertThat(dataSource.getPassword()).isNull(); })); } @@ -230,9 +266,8 @@ void overrideDataSourceAndFallbackToEmbeddedProperties() { void overrideUserAndFallbackToEmbeddedProperties() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) .withPropertyValues("spring.liquibase.user:sa").run(assertLiquibase((liquibase) -> { - DataSource dataSource = liquibase.getDataSource(); - assertThat(((HikariDataSource) dataSource).isClosed()).isTrue(); - assertThat(((HikariDataSource) dataSource).getJdbcUrl()).startsWith("jdbc:h2:mem:"); + SimpleDriverDataSource dataSource = (SimpleDriverDataSource) liquibase.getDataSource(); + assertThat(dataSource.getUrl()).startsWith("jdbc:h2:mem:"); })); } @@ -257,11 +292,7 @@ void changeLogDoesNotExist() { @Test void logging(CapturedOutput output) { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) - .run(assertLiquibase((liquibase) -> { - Object log = ReflectionTestUtils.getField(liquibase, "log"); - assertThat(log).isInstanceOf(Slf4jLogger.class); - assertThat(output).doesNotContain(": liquibase:"); - })); + .run(assertLiquibase((liquibase) -> assertThat(output).doesNotContain(": liquibase:"))); } @Test @@ -278,7 +309,7 @@ void testOverrideParameters() { .withPropertyValues("spring.liquibase.parameters.foo:bar").run(assertLiquibase((liquibase) -> { Map parameters = (Map) ReflectionTestUtils.getField(liquibase, "parameters"); - assertThat(parameters.containsKey("foo")).isTrue(); + assertThat(parameters).containsKey("foo"); assertThat(parameters.get("foo")).isEqualTo("bar"); })); } @@ -343,6 +374,32 @@ void userConfigurationJdbcTemplateDependency() { }); } + @Test + void overrideTag() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.liquibase.tag:1.0.0") + .run(assertLiquibase((liquibase) -> assertThat(liquibase.getTag()).isEqualTo("1.0.0"))); + } + + @Test + void whenLiquibaseIsAutoConfiguredThenJooqDslContextDependsOnSpringLiquibaseBeans() { + this.contextRunner.withConfiguration(AutoConfigurations.of(JooqAutoConfiguration.class)) + .withUserConfiguration(EmbeddedDataSourceConfiguration.class).run((context) -> { + BeanDefinition beanDefinition = context.getBeanFactory().getBeanDefinition("dslContext"); + assertThat(beanDefinition.getDependsOn()).containsExactly("liquibase"); + }); + } + + @Test + void whenCustomSpringLiquibaseIsDefinedThenJooqDslContextDependsOnSpringLiquibaseBeans() { + this.contextRunner.withConfiguration(AutoConfigurations.of(JooqAutoConfiguration.class)) + .withUserConfiguration(LiquibaseUserConfiguration.class, EmbeddedDataSourceConfiguration.class) + .run((context) -> { + BeanDefinition beanDefinition = context.getBeanFactory().getBeanDefinition("dslContext"); + assertThat(beanDefinition.getDependsOn()).containsExactly("springLiquibase"); + }); + } + private ContextConsumer assertLiquibase(Consumer consumer) { return (context) -> { assertThat(context).hasSingleBean(SpringLiquibase.class); @@ -357,13 +414,14 @@ static class LiquibaseDataSourceConfiguration { @Bean @Primary DataSource normalDataSource() { - return DataSourceBuilder.create().url("jdbc:hsqldb:mem:normal").username("sa").build(); + return DataSourceBuilder.create().url("jdbc:hsqldb:mem:normal" + UUID.randomUUID()).username("sa").build(); } @LiquibaseDataSource @Bean DataSource liquibaseDataSource() { - return DataSourceBuilder.create().url("jdbc:hsqldb:mem:liquibasetest").username("sa").build(); + return DataSourceBuilder.create().url("jdbc:hsqldb:mem:liquibasetest" + UUID.randomUUID()).username("sa") + .build(); } } @@ -382,4 +440,47 @@ SpringLiquibase springLiquibase(DataSource dataSource) { } + @Configuration(proxyBeanMethods = false) + static class CustomDataSourceConfiguration { + + private String name = UUID.randomUUID().toString(); + + @Bean(destroyMethod = "shutdown") + EmbeddedDatabase dataSource() throws SQLException { + EmbeddedDatabase database = new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2) + .setName(this.name).build(); + insertUser(database); + return database; + } + + private void insertUser(EmbeddedDatabase database) throws SQLException { + try (Connection connection = database.getConnection()) { + connection.prepareStatement("CREATE USER test password 'secret'").execute(); + connection.prepareStatement("ALTER USER test ADMIN TRUE").execute(); + } + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomDriverConfiguration { + + private String name = UUID.randomUUID().toString(); + + @Bean + SimpleDriverDataSource dataSource() throws SQLException { + SimpleDriverDataSource dataSource = new SimpleDriverDataSource(); + dataSource.setDriverClass(CustomH2Driver.class); + dataSource.setUrl(String.format("jdbc:h2:mem:%s;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false", this.name)); + dataSource.setUsername("sa"); + dataSource.setPassword(""); + return dataSource; + } + + } + + static class CustomH2Driver extends org.h2.Driver { + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mail/MailSenderAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mail/MailSenderAutoConfigurationTests.java index 03d83d47142d..3b4089357dcf 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mail/MailSenderAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mail/MailSenderAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,10 +37,9 @@ import org.springframework.mail.javamail.JavaMailSenderImpl; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; /** * Tests for {@link MailSenderAutoConfiguration}. @@ -223,7 +222,7 @@ void connectionOnStartup() { .withPropertyValues("spring.mail.host:10.0.0.23", "spring.mail.test-connection:true").run((context) -> { assertThat(context).hasSingleBean(JavaMailSenderImpl.class); JavaMailSenderImpl mailSender = context.getBean(JavaMailSenderImpl.class); - verify(mailSender, times(1)).testConnection(); + then(mailSender).should().testConnection(); }); } @@ -234,11 +233,11 @@ void connectionOnStartupNotCalled() { .run((context) -> { assertThat(context).hasSingleBean(JavaMailSenderImpl.class); JavaMailSenderImpl mailSender = context.getBean(JavaMailSenderImpl.class); - verify(mailSender, never()).testConnection(); + then(mailSender).should(never()).testConnection(); }); } - private Session configureJndiSession(String name) throws IllegalStateException { + private Session configureJndiSession(String name) { Properties properties = new Properties(); Session session = Session.getDefaultInstance(properties); TestableInitialContextFactory.bind(name, session); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoAutoConfigurationTests.java index 4d64ff2a50fb..b6ff9c552122 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,26 +16,28 @@ package org.springframework.boot.autoconfigure.mongo; -import javax.net.SocketFactory; +import java.util.concurrent.TimeUnit; -import com.mongodb.MongoClient; -import com.mongodb.MongoClientOptions; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; +import com.mongodb.client.internal.MongoClientImpl; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; /** * Tests for {@link MongoAutoConfiguration}. * * @author Dave Syer * @author Stephane Nicoll + * @author Scott Frederick */ class MongoAutoConfigurationTests { @@ -48,69 +50,83 @@ void clientExists() { } @Test - void optionsAdded() { - this.contextRunner.withUserConfiguration(OptionsConfig.class).run( - (context) -> assertThat(context.getBean(MongoClient.class).getMongoClientOptions().getSocketTimeout()) - .isEqualTo(300)); + void settingsAdded() { + this.contextRunner.withUserConfiguration(SettingsConfig.class) + .run((context) -> assertThat( + getSettings(context).getSocketSettings().getConnectTimeout(TimeUnit.MILLISECONDS)) + .isEqualTo(300)); } @Test - void optionsAddedButNoHost() { - this.contextRunner.withUserConfiguration(OptionsConfig.class).run( - (context) -> assertThat(context.getBean(MongoClient.class).getMongoClientOptions().getSocketTimeout()) - .isEqualTo(300)); + void settingsAddedButNoHost() { + this.contextRunner.withUserConfiguration(SettingsConfig.class) + .run((context) -> assertThat( + getSettings(context).getSocketSettings().getConnectTimeout(TimeUnit.MILLISECONDS)) + .isEqualTo(300)); } @Test - void optionsSslConfig() { - this.contextRunner.withUserConfiguration(SslOptionsConfig.class).run((context) -> { - assertThat(context).hasSingleBean(MongoClient.class); - MongoClient mongo = context.getBean(MongoClient.class); - MongoClientOptions options = mongo.getMongoClientOptions(); - assertThat(options.isSslEnabled()).isTrue(); - assertThat(options.getSocketFactory()).isSameAs(context.getBean("mySocketFactory")); - }); + void settingsSslConfig() { + this.contextRunner.withUserConfiguration(SslSettingsConfig.class) + .run((context) -> assertThat(getSettings(context).getSslSettings().isEnabled()).isTrue()); } @Test - void doesNotCreateMongoClientWhenAlreadyDefined() { - this.contextRunner.withUserConfiguration(FallbackMongoClientConfig.class).run((context) -> { - assertThat(context).doesNotHaveBean(MongoClient.class); - assertThat(context).hasSingleBean(com.mongodb.client.MongoClient.class); - }); + void configuresSingleClient() { + this.contextRunner.withUserConfiguration(FallbackMongoClientConfig.class) + .run((context) -> assertThat(context).hasSingleBean(MongoClient.class)); + } + + @Test + void customizerOverridesAutoConfig() { + this.contextRunner.withPropertyValues("spring.data.mongodb.uri:mongodb://localhost/test?appname=auto-config") + .withUserConfiguration(SimpleCustomizerConfig.class) + .run((context) -> assertThat(getSettings(context).getApplicationName()).isEqualTo("overridden-name")); + } + + private MongoClientSettings getSettings(AssertableApplicationContext context) { + assertThat(context).hasSingleBean(MongoClient.class); + MongoClientImpl client = (MongoClientImpl) context.getBean(MongoClient.class); + return client.getSettings(); } @Configuration(proxyBeanMethods = false) - static class OptionsConfig { + static class SettingsConfig { @Bean - MongoClientOptions mongoOptions() { - return MongoClientOptions.builder().socketTimeout(300).build(); + MongoClientSettings mongoClientSettings() { + return MongoClientSettings.builder().applyToSocketSettings( + (socketSettings) -> socketSettings.connectTimeout(300, TimeUnit.MILLISECONDS)).build(); } } @Configuration(proxyBeanMethods = false) - static class SslOptionsConfig { + static class SslSettingsConfig { @Bean - MongoClientOptions mongoClientOptions(SocketFactory socketFactory) { - return MongoClientOptions.builder().sslEnabled(true).socketFactory(socketFactory).build(); + MongoClientSettings mongoClientSettings() { + return MongoClientSettings.builder().applyToSslSettings((ssl) -> ssl.enabled(true)).build(); } + } + + @Configuration(proxyBeanMethods = false) + static class FallbackMongoClientConfig { + @Bean - SocketFactory mySocketFactory() { - return mock(SocketFactory.class); + MongoClient fallbackMongoClient() { + return MongoClients.create(); } } @Configuration(proxyBeanMethods = false) - static class FallbackMongoClientConfig { + static class SimpleCustomizerConfig { @Bean - com.mongodb.client.MongoClient fallbackMongoClient() { - return MongoClients.create(); + MongoClientSettingsBuilderCustomizer customizer() { + return (clientSettingsBuilder) -> clientSettingsBuilder.applicationName("overridden-name"); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoClientFactorySupportTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoClientFactorySupportTests.java new file mode 100644 index 000000000000..1829ec803cbe --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoClientFactorySupportTests.java @@ -0,0 +1,137 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.mongo; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import com.mongodb.MongoClientSettings; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link MongoClientFactorySupport}. + * + * @param the mongo client type + * @author Phillip Webb + * @author Andy Wilkinson + * @author Stephane Nicoll + * @author Mark Paluch + * @author Artsiom Yudovin + * @author Scott Frederick + */ +abstract class MongoClientFactorySupportTests { + + @Test + void canBindCharArrayPassword() { + // gh-1572 + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + TestPropertyValues.of("spring.data.mongodb.password:word").applyTo(context); + context.register(Config.class); + context.refresh(); + MongoProperties properties = context.getBean(MongoProperties.class); + assertThat(properties.getPassword()).isEqualTo("word".toCharArray()); + } + + @Test + void allMongoClientSettingsCanBeSet() { + MongoClientSettings.Builder builder = MongoClientSettings.builder(); + builder.applyToSocketSettings((settings) -> { + settings.connectTimeout(1000, TimeUnit.MILLISECONDS); + settings.readTimeout(1000, TimeUnit.MILLISECONDS); + }).applyToServerSettings((settings) -> { + settings.heartbeatFrequency(10001, TimeUnit.MILLISECONDS); + settings.minHeartbeatFrequency(501, TimeUnit.MILLISECONDS); + }).applyToConnectionPoolSettings((settings) -> { + settings.maxWaitTime(120001, TimeUnit.MILLISECONDS); + settings.maxConnectionLifeTime(60000, TimeUnit.MILLISECONDS); + settings.maxConnectionIdleTime(60000, TimeUnit.MILLISECONDS); + }).applyToSslSettings((settings) -> settings.enabled(true)).applicationName("test"); + + MongoClientSettings settings = builder.build(); + T client = createMongoClient(settings); + MongoClientSettings wrapped = getClientSettings(client); + assertThat(wrapped.getSocketSettings().getConnectTimeout(TimeUnit.MILLISECONDS)) + .isEqualTo(settings.getSocketSettings().getConnectTimeout(TimeUnit.MILLISECONDS)); + assertThat(wrapped.getSocketSettings().getReadTimeout(TimeUnit.MILLISECONDS)) + .isEqualTo(settings.getSocketSettings().getReadTimeout(TimeUnit.MILLISECONDS)); + assertThat(wrapped.getServerSettings().getHeartbeatFrequency(TimeUnit.MILLISECONDS)) + .isEqualTo(settings.getServerSettings().getHeartbeatFrequency(TimeUnit.MILLISECONDS)); + assertThat(wrapped.getServerSettings().getMinHeartbeatFrequency(TimeUnit.MILLISECONDS)) + .isEqualTo(settings.getServerSettings().getMinHeartbeatFrequency(TimeUnit.MILLISECONDS)); + assertThat(wrapped.getApplicationName()).isEqualTo(settings.getApplicationName()); + assertThat(wrapped.getConnectionPoolSettings().getMaxWaitTime(TimeUnit.MILLISECONDS)) + .isEqualTo(settings.getConnectionPoolSettings().getMaxWaitTime(TimeUnit.MILLISECONDS)); + assertThat(wrapped.getConnectionPoolSettings().getMaxConnectionLifeTime(TimeUnit.MILLISECONDS)) + .isEqualTo(settings.getConnectionPoolSettings().getMaxConnectionLifeTime(TimeUnit.MILLISECONDS)); + assertThat(wrapped.getConnectionPoolSettings().getMaxConnectionIdleTime(TimeUnit.MILLISECONDS)) + .isEqualTo(settings.getConnectionPoolSettings().getMaxConnectionIdleTime(TimeUnit.MILLISECONDS)); + assertThat(wrapped.getSslSettings().isEnabled()).isEqualTo(settings.getSslSettings().isEnabled()); + } + + @Test + void customizerIsInvoked() { + MongoClientSettingsBuilderCustomizer customizer = mock(MongoClientSettingsBuilderCustomizer.class); + createMongoClient(customizer); + then(customizer).should().customize(any(MongoClientSettings.Builder.class)); + } + + @Test + void canBindAutoIndexCreation() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + TestPropertyValues.of("spring.data.mongodb.autoIndexCreation:true").applyTo(context); + context.register(Config.class); + context.refresh(); + MongoProperties properties = context.getBean(MongoProperties.class); + assertThat(properties.isAutoIndexCreation()).isTrue(); + } + + protected T createMongoClient() { + return createMongoClient(null, MongoClientSettings.builder().build()); + } + + protected T createMongoClient(MongoClientSettings settings) { + return createMongoClient(null, settings); + } + + protected void createMongoClient(MongoClientSettingsBuilderCustomizer... customizers) { + createMongoClient((customizers != null) ? Arrays.asList(customizers) : null, + MongoClientSettings.builder().build()); + } + + protected abstract T createMongoClient(List customizers, + MongoClientSettings settings); + + protected abstract MongoClientSettings getClientSettings(T client); + + @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties(MongoProperties.class) + static class Config { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoClientFactoryTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoClientFactoryTests.java index b5a858d2e754..d793db83aaec 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoClientFactoryTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoClientFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,17 +18,9 @@ import java.util.List; -import com.mongodb.MongoClient; -import com.mongodb.MongoCredential; -import com.mongodb.ServerAddress; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.env.Environment; -import org.springframework.mock.env.MockEnvironment; - -import static org.assertj.core.api.Assertions.assertThat; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.internal.MongoClientImpl; /** * Tests for {@link MongoClientFactory}. @@ -37,123 +29,19 @@ * @author Andy Wilkinson * @author Stephane Nicoll * @author Mark Paluch + * @author Scott Frederick */ -class MongoClientFactoryTests { - - private MockEnvironment environment = new MockEnvironment(); - - @Test - void portCanBeCustomized() { - MongoProperties properties = new MongoProperties(); - properties.setPort(12345); - MongoClient client = createMongoClient(properties); - List allAddresses = getAllAddresses(client); - assertThat(allAddresses).hasSize(1); - assertServerAddress(allAddresses.get(0), "localhost", 12345); - } - - @Test - void hostCanBeCustomized() { - MongoProperties properties = new MongoProperties(); - properties.setHost("mongo.example.com"); - MongoClient client = createMongoClient(properties); - List allAddresses = getAllAddresses(client); - assertThat(allAddresses).hasSize(1); - assertServerAddress(allAddresses.get(0), "mongo.example.com", 27017); - } - - @Test - void credentialsCanBeCustomized() { - MongoProperties properties = new MongoProperties(); - properties.setUsername("user"); - properties.setPassword("secret".toCharArray()); - MongoClient client = createMongoClient(properties); - assertMongoCredential(getCredentials(client).get(0), "user", "secret", "test"); - } - - @Test - void databaseCanBeCustomized() { - MongoProperties properties = new MongoProperties(); - properties.setDatabase("foo"); - properties.setUsername("user"); - properties.setPassword("secret".toCharArray()); - MongoClient client = createMongoClient(properties); - assertMongoCredential(getCredentials(client).get(0), "user", "secret", "foo"); - } - - @Test - void authenticationDatabaseCanBeCustomized() { - MongoProperties properties = new MongoProperties(); - properties.setAuthenticationDatabase("foo"); - properties.setUsername("user"); - properties.setPassword("secret".toCharArray()); - MongoClient client = createMongoClient(properties); - assertMongoCredential(getCredentials(client).get(0), "user", "secret", "foo"); - } - - @Test - void uriCanBeCustomized() { - MongoProperties properties = new MongoProperties(); - properties.setUri("mongodb://user:secret@mongo1.example.com:12345,mongo2.example.com:23456/test"); - MongoClient client = createMongoClient(properties); - List allAddresses = getAllAddresses(client); - assertThat(allAddresses).hasSize(2); - assertServerAddress(allAddresses.get(0), "mongo1.example.com", 12345); - assertServerAddress(allAddresses.get(1), "mongo2.example.com", 23456); - List credentialsList = getCredentials(client); - assertThat(credentialsList).hasSize(1); - assertMongoCredential(credentialsList.get(0), "user", "secret", "test"); - } - - @Test - void uriIsIgnoredInEmbeddedMode() { - MongoProperties properties = new MongoProperties(); - properties.setUri("mongodb://mongo.example.com:1234/mydb"); - this.environment.setProperty("local.mongo.port", "4000"); - MongoClient client = createMongoClient(properties, this.environment); - List allAddresses = getAllAddresses(client); - assertThat(allAddresses).hasSize(1); - assertServerAddress(allAddresses.get(0), "localhost", 4000); - } - - private MongoClient createMongoClient(MongoProperties properties) { - return createMongoClient(properties, null); - } - - private MongoClient createMongoClient(MongoProperties properties, Environment environment) { - return new MongoClientFactory(properties, environment).createMongoClient(null); - } +class MongoClientFactoryTests extends MongoClientFactorySupportTests { - @SuppressWarnings("deprecation") - private List getAllAddresses(MongoClient client) { - // At some point we'll probably need to use reflection to find the address but for - // now, we can use the deprecated getAllAddress method. - return client.getAllAddress(); + @Override + protected MongoClient createMongoClient(List customizers, + MongoClientSettings settings) { + return new MongoClientFactory(customizers).createMongoClient(settings); } - @SuppressWarnings("deprecation") - private List getCredentials(MongoClient client) { - // At some point we'll probably need to use reflection to find the credentials but - // for now, we can use the deprecated getCredentialsList method. - return client.getCredentialsList(); - } - - private void assertServerAddress(ServerAddress serverAddress, String expectedHost, int expectedPort) { - assertThat(serverAddress.getHost()).isEqualTo(expectedHost); - assertThat(serverAddress.getPort()).isEqualTo(expectedPort); - } - - private void assertMongoCredential(MongoCredential credentials, String expectedUsername, String expectedPassword, - String expectedSource) { - assertThat(credentials.getUserName()).isEqualTo(expectedUsername); - assertThat(credentials.getPassword()).isEqualTo(expectedPassword.toCharArray()); - assertThat(credentials.getSource()).isEqualTo(expectedSource); - } - - @Configuration(proxyBeanMethods = false) - @EnableConfigurationProperties(MongoProperties.class) - static class Config { - + @Override + protected MongoClientSettings getClientSettings(MongoClient client) { + return ((MongoClientImpl) client).getSettings(); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoPropertiesClientSettingsBuilderCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoPropertiesClientSettingsBuilderCustomizerTests.java new file mode 100644 index 000000000000..77130fd5b6b0 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoPropertiesClientSettingsBuilderCustomizerTests.java @@ -0,0 +1,210 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.mongo; + +import java.util.List; + +import com.mongodb.MongoClientSettings; +import com.mongodb.MongoCredential; +import com.mongodb.ServerAddress; +import org.bson.UuidRepresentation; +import org.junit.jupiter.api.Test; + +import org.springframework.mock.env.MockEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link MongoPropertiesClientSettingsBuilderCustomizer}. + * + * @author Scott Frederick + */ +class MongoPropertiesClientSettingsBuilderCustomizerTests { + + private final MongoProperties properties = new MongoProperties(); + + private final MockEnvironment environment = new MockEnvironment(); + + @Test + void portCanBeCustomized() { + this.properties.setPort(12345); + MongoClientSettings settings = customizeSettings(); + List allAddresses = getAllAddresses(settings); + assertThat(allAddresses).hasSize(1); + assertServerAddress(allAddresses.get(0), "localhost", 12345); + } + + @Test + void hostCanBeCustomized() { + this.properties.setHost("mongo.example.com"); + MongoClientSettings settings = customizeSettings(); + List allAddresses = getAllAddresses(settings); + assertThat(allAddresses).hasSize(1); + assertServerAddress(allAddresses.get(0), "mongo.example.com", 27017); + } + + @Test + void credentialsCanBeCustomized() { + this.properties.setUsername("user"); + this.properties.setPassword("secret".toCharArray()); + MongoClientSettings settings = customizeSettings(); + assertMongoCredential(settings.getCredential(), "user", "secret", "test"); + } + + @Test + void replicaSetCanBeCustomized() { + this.properties.setReplicaSetName("test"); + MongoClientSettings settings = customizeSettings(); + assertThat(settings.getClusterSettings().getRequiredReplicaSetName()).isEqualTo("test"); + } + + @Test + void databaseCanBeCustomized() { + this.properties.setDatabase("foo"); + this.properties.setUsername("user"); + this.properties.setPassword("secret".toCharArray()); + MongoClientSettings settings = customizeSettings(); + assertMongoCredential(settings.getCredential(), "user", "secret", "foo"); + } + + @Test + void uuidRepresentationDefaultToJavaLegacy() { + MongoClientSettings settings = customizeSettings(); + assertThat(settings.getUuidRepresentation()).isEqualTo(UuidRepresentation.JAVA_LEGACY); + } + + @Test + void uuidRepresentationCanBeCustomized() { + this.properties.setUuidRepresentation(UuidRepresentation.STANDARD); + MongoClientSettings settings = customizeSettings(); + assertThat(settings.getUuidRepresentation()).isEqualTo(UuidRepresentation.STANDARD); + } + + @Test + void authenticationDatabaseCanBeCustomized() { + this.properties.setAuthenticationDatabase("foo"); + this.properties.setUsername("user"); + this.properties.setPassword("secret".toCharArray()); + MongoClientSettings settings = customizeSettings(); + assertMongoCredential(settings.getCredential(), "user", "secret", "foo"); + } + + @Test + void onlyHostAndPortSetShouldUseThat() { + this.properties.setHost("localhost"); + this.properties.setPort(27017); + MongoClientSettings settings = customizeSettings(); + List allAddresses = getAllAddresses(settings); + assertThat(allAddresses).hasSize(1); + assertServerAddress(allAddresses.get(0), "localhost", 27017); + } + + @Test + void onlyUriSetShouldUseThat() { + this.properties.setUri("mongodb://mongo1.example.com:12345"); + MongoClientSettings settings = customizeSettings(); + List allAddresses = getAllAddresses(settings); + assertThat(allAddresses).hasSize(1); + assertServerAddress(allAddresses.get(0), "mongo1.example.com", 12345); + } + + @Test + void noCustomAddressAndNoUriUsesDefaultUri() { + MongoClientSettings settings = customizeSettings(); + List allAddresses = getAllAddresses(settings); + assertThat(allAddresses).hasSize(1); + assertServerAddress(allAddresses.get(0), "localhost", 27017); + } + + @Test + void uriCanBeCustomized() { + this.properties.setUri("mongodb://user:secret@mongo1.example.com:12345,mongo2.example.com:23456/test"); + MongoClientSettings settings = customizeSettings(); + List allAddresses = getAllAddresses(settings); + assertThat(allAddresses).hasSize(2); + assertServerAddress(allAddresses.get(0), "mongo1.example.com", 12345); + assertServerAddress(allAddresses.get(1), "mongo2.example.com", 23456); + assertMongoCredential(settings.getCredential(), "user", "secret", "test"); + } + + @Test + void uriIsIgnoredInEmbeddedMode() { + this.properties.setUri("mongodb://mongo.example.com:1234/mydb"); + this.environment.setProperty("local.mongo.port", "4000"); + MongoClientSettings settings = customizeSettings(); + List allAddresses = getAllAddresses(settings); + assertThat(allAddresses).hasSize(1); + assertServerAddress(allAddresses.get(0), "localhost", 4000); + } + + @Test + void uriCannotBeSetWithCredentials() { + this.properties.setUri("mongodb://127.0.0.1:1234/mydb"); + this.properties.setUsername("user"); + this.properties.setPassword("secret".toCharArray()); + assertThatIllegalStateException().isThrownBy(this::customizeSettings).withMessageContaining( + "Invalid mongo configuration, either uri or host/port/credentials/replicaSet must be specified"); + } + + @Test + void uriCannotBeSetWithReplicaSetName() { + this.properties.setUri("mongodb://127.0.0.1:1234/mydb"); + this.properties.setReplicaSetName("test"); + assertThatIllegalStateException().isThrownBy(this::customizeSettings).withMessageContaining( + "Invalid mongo configuration, either uri or host/port/credentials/replicaSet must be specified"); + } + + @Test + void uriCannotBeSetWithHostPort() { + this.properties.setUri("mongodb://127.0.0.1:1234/mydb"); + this.properties.setHost("localhost"); + this.properties.setPort(4567); + assertThatIllegalStateException().isThrownBy(this::customizeSettings).withMessageContaining( + "Invalid mongo configuration, either uri or host/port/credentials/replicaSet must be specified"); + } + + @Test + void retryWritesIsPropagatedFromUri() { + this.properties.setUri("mongodb://localhost/test?retryWrites=false"); + MongoClientSettings settings = customizeSettings(); + assertThat(settings.getRetryWrites()).isFalse(); + } + + private MongoClientSettings customizeSettings() { + MongoClientSettings.Builder settings = MongoClientSettings.builder(); + new MongoPropertiesClientSettingsBuilderCustomizer(this.properties, this.environment).customize(settings); + return settings.build(); + } + + private List getAllAddresses(MongoClientSettings settings) { + return settings.getClusterSettings().getHosts(); + } + + protected void assertServerAddress(ServerAddress serverAddress, String expectedHost, int expectedPort) { + assertThat(serverAddress.getHost()).isEqualTo(expectedHost); + assertThat(serverAddress.getPort()).isEqualTo(expectedPort); + } + + protected void assertMongoCredential(MongoCredential credentials, String expectedUsername, String expectedPassword, + String expectedSource) { + assertThat(credentials.getUserName()).isEqualTo(expectedUsername); + assertThat(credentials.getPassword()).isEqualTo(expectedPassword.toCharArray()); + assertThat(credentials.getSource()).isEqualTo(expectedSource); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoPropertiesTests.java deleted file mode 100644 index a3717838699a..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoPropertiesTests.java +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.mongo; - -import java.util.List; - -import com.mongodb.MongoClient; -import com.mongodb.MongoClientOptions; -import com.mongodb.ServerAddress; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.test.util.TestPropertyValues; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Configuration; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link MongoProperties}. - * - * @author Phillip Webb - * @author Andy Wilkinson - * @author Stephane Nicoll - * @author Mark Paluch - * @author Artsiom Yudovin - */ -class MongoPropertiesTests { - - @Test - void canBindCharArrayPassword() { - // gh-1572 - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); - TestPropertyValues.of("spring.data.mongodb.password:word").applyTo(context); - context.register(Config.class); - context.refresh(); - MongoProperties properties = context.getBean(MongoProperties.class); - assertThat(properties.getPassword()).isEqualTo("word".toCharArray()); - } - - @Test - @SuppressWarnings("deprecation") - void allMongoClientOptionsCanBeSet() { - MongoClientOptions.Builder builder = MongoClientOptions.builder(); - builder.alwaysUseMBeans(true); - builder.connectionsPerHost(101); - builder.connectTimeout(10001); - builder.cursorFinalizerEnabled(false); - builder.description("test"); - builder.maxWaitTime(120001); - builder.socketKeepAlive(false); - builder.socketTimeout(1000); - builder.threadsAllowedToBlockForConnectionMultiplier(6); - builder.minConnectionsPerHost(0); - builder.maxConnectionIdleTime(60000); - builder.maxConnectionLifeTime(60000); - builder.heartbeatFrequency(10001); - builder.minHeartbeatFrequency(501); - builder.heartbeatConnectTimeout(20001); - builder.heartbeatSocketTimeout(20001); - builder.localThreshold(20); - builder.requiredReplicaSetName("testReplicaSetName"); - MongoClientOptions options = builder.build(); - MongoProperties properties = new MongoProperties(); - MongoClient client = new MongoClientFactory(properties, null).createMongoClient(options); - MongoClientOptions wrapped = client.getMongoClientOptions(); - assertThat(wrapped.isAlwaysUseMBeans()).isEqualTo(options.isAlwaysUseMBeans()); - assertThat(wrapped.getConnectionsPerHost()).isEqualTo(options.getConnectionsPerHost()); - assertThat(wrapped.getConnectTimeout()).isEqualTo(options.getConnectTimeout()); - assertThat(wrapped.isCursorFinalizerEnabled()).isEqualTo(options.isCursorFinalizerEnabled()); - assertThat(wrapped.getDescription()).isEqualTo(options.getDescription()); - assertThat(wrapped.getMaxWaitTime()).isEqualTo(options.getMaxWaitTime()); - assertThat(wrapped.getSocketTimeout()).isEqualTo(options.getSocketTimeout()); - assertThat(wrapped.isSocketKeepAlive()).isEqualTo(options.isSocketKeepAlive()); - assertThat(wrapped.getThreadsAllowedToBlockForConnectionMultiplier()) - .isEqualTo(options.getThreadsAllowedToBlockForConnectionMultiplier()); - assertThat(wrapped.getMinConnectionsPerHost()).isEqualTo(options.getMinConnectionsPerHost()); - assertThat(wrapped.getMaxConnectionIdleTime()).isEqualTo(options.getMaxConnectionIdleTime()); - assertThat(wrapped.getMaxConnectionLifeTime()).isEqualTo(options.getMaxConnectionLifeTime()); - assertThat(wrapped.getHeartbeatFrequency()).isEqualTo(options.getHeartbeatFrequency()); - assertThat(wrapped.getMinHeartbeatFrequency()).isEqualTo(options.getMinHeartbeatFrequency()); - assertThat(wrapped.getHeartbeatConnectTimeout()).isEqualTo(options.getHeartbeatConnectTimeout()); - assertThat(wrapped.getHeartbeatSocketTimeout()).isEqualTo(options.getHeartbeatSocketTimeout()); - assertThat(wrapped.getLocalThreshold()).isEqualTo(options.getLocalThreshold()); - assertThat(wrapped.getRequiredReplicaSetName()).isEqualTo(options.getRequiredReplicaSetName()); - } - - @Test - void uriOverridesHostAndPort() { - MongoProperties properties = new MongoProperties(); - properties.setHost("localhost"); - properties.setPort(27017); - properties.setUri("mongodb://mongo1.example.com:12345"); - MongoClient client = new MongoClientFactory(properties, null).createMongoClient(null); - List allAddresses = getAllAddresses(client); - assertThat(allAddresses).hasSize(1); - assertServerAddress(allAddresses.get(0), "mongo1.example.com", 12345); - } - - @Test - void onlyHostAndPortSetShouldUseThat() { - MongoProperties properties = new MongoProperties(); - properties.setHost("localhost"); - properties.setPort(27017); - MongoClient client = new MongoClientFactory(properties, null).createMongoClient(null); - List allAddresses = getAllAddresses(client); - assertThat(allAddresses).hasSize(1); - assertServerAddress(allAddresses.get(0), "localhost", 27017); - } - - @Test - void onlyUriSetShouldUseThat() { - MongoProperties properties = new MongoProperties(); - properties.setUri("mongodb://mongo1.example.com:12345"); - MongoClient client = new MongoClientFactory(properties, null).createMongoClient(null); - List allAddresses = getAllAddresses(client); - assertThat(allAddresses).hasSize(1); - assertServerAddress(allAddresses.get(0), "mongo1.example.com", 12345); - } - - @Test - void noCustomAddressAndNoUriUsesDefaultUri() { - MongoProperties properties = new MongoProperties(); - MongoClient client = new MongoClientFactory(properties, null).createMongoClient(null); - List allAddresses = getAllAddresses(client); - assertThat(allAddresses).hasSize(1); - assertServerAddress(allAddresses.get(0), "localhost", 27017); - } - - @SuppressWarnings("deprecation") - private List getAllAddresses(MongoClient client) { - // At some point we'll probably need to use reflection to find the address but for - // now, we can use the deprecated getAllAddress method. - return client.getAllAddress(); - } - - @Test - void canBindAutoIndexCreation() { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); - TestPropertyValues.of("spring.data.mongodb.autoIndexCreation:true").applyTo(context); - context.register(Config.class); - context.refresh(); - MongoProperties properties = context.getBean(MongoProperties.class); - assertThat(properties.isAutoIndexCreation()).isTrue(); - } - - private void assertServerAddress(ServerAddress serverAddress, String expectedHost, int expectedPort) { - assertThat(serverAddress.getHost()).isEqualTo(expectedHost); - assertThat(serverAddress.getPort()).isEqualTo(expectedPort); - } - - @Configuration(proxyBeanMethods = false) - @EnableConfigurationProperties(MongoProperties.class) - static class Config { - - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoReactiveAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoReactiveAutoConfigurationTests.java index ee7fd0c2f434..30ed753e991e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoReactiveAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoReactiveAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import com.mongodb.connection.StreamFactoryFactory; import com.mongodb.connection.netty.NettyStreamFactoryFactory; import com.mongodb.reactivestreams.client.MongoClient; +import com.mongodb.reactivestreams.client.internal.MongoClientImpl; import io.netty.channel.EventLoopGroup; import org.junit.jupiter.api.Test; @@ -58,25 +59,25 @@ void clientExists() { } @Test - void optionsAdded() { + void settingsAdded() { this.contextRunner.withPropertyValues("spring.data.mongodb.host:localhost") - .withUserConfiguration(OptionsConfig.class) + .withUserConfiguration(SettingsConfig.class) .run((context) -> assertThat(getSettings(context).getSocketSettings().getReadTimeout(TimeUnit.SECONDS)) .isEqualTo(300)); } @Test - void optionsAddedButNoHost() { + void settingsAddedButNoHost() { this.contextRunner.withPropertyValues("spring.data.mongodb.uri:mongodb://localhost/test") - .withUserConfiguration(OptionsConfig.class) + .withUserConfiguration(SettingsConfig.class) .run((context) -> assertThat(getSettings(context).getReadPreference()) .isEqualTo(ReadPreference.nearest())); } @Test - void optionsSslConfig() { + void settingsSslConfig() { this.contextRunner.withPropertyValues("spring.data.mongodb.uri:mongodb://localhost/test") - .withUserConfiguration(SslOptionsConfig.class).run((context) -> { + .withUserConfiguration(SslSettingsConfig.class).run((context) -> { assertThat(context).hasSingleBean(MongoClient.class); MongoClientSettings settings = getSettings(context); assertThat(settings.getApplicationName()).isEqualTo("test-config"); @@ -110,14 +111,13 @@ void customizerOverridesAutoConfig() { }); } - @SuppressWarnings("deprecation") private MongoClientSettings getSettings(ApplicationContext context) { - MongoClient client = context.getBean(MongoClient.class); - return (MongoClientSettings) ReflectionTestUtils.getField(client.getSettings(), "wrapped"); + MongoClientImpl client = (MongoClientImpl) context.getBean(MongoClient.class); + return client.getSettings(); } @Configuration(proxyBeanMethods = false) - static class OptionsConfig { + static class SettingsConfig { @Bean MongoClientSettings mongoClientSettings() { @@ -128,7 +128,7 @@ MongoClientSettings mongoClientSettings() { } @Configuration(proxyBeanMethods = false) - static class SslOptionsConfig { + static class SslSettingsConfig { @Bean MongoClientSettings mongoClientSettings(StreamFactoryFactory streamFactoryFactory) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/ReactiveMongoClientFactoryTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/ReactiveMongoClientFactoryTests.java index aa1d9801fd80..c2b43d0e4c8c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/ReactiveMongoClientFactoryTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/ReactiveMongoClientFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,198 +16,30 @@ package org.springframework.boot.autoconfigure.mongo; -import java.util.Arrays; import java.util.List; import com.mongodb.MongoClientSettings; -import com.mongodb.MongoCredential; -import com.mongodb.ServerAddress; -import com.mongodb.connection.ClusterSettings; import com.mongodb.reactivestreams.client.MongoClient; -import org.junit.jupiter.api.Test; - -import org.springframework.core.env.Environment; -import org.springframework.mock.env.MockEnvironment; -import org.springframework.test.util.ReflectionTestUtils; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; +import com.mongodb.reactivestreams.client.internal.MongoClientImpl; /** * Tests for {@link ReactiveMongoClientFactory}. * * @author Mark Paluch * @author Stephane Nicoll + * @author Scott Frederick */ -class ReactiveMongoClientFactoryTests { - - private MockEnvironment environment = new MockEnvironment(); - - @Test - void portCanBeCustomized() { - MongoProperties properties = new MongoProperties(); - properties.setPort(12345); - MongoClient client = createMongoClient(properties); - List allAddresses = extractServerAddresses(client); - assertThat(allAddresses).hasSize(1); - assertServerAddress(allAddresses.get(0), "localhost", 12345); - } - - @Test - void hostCanBeCustomized() { - MongoProperties properties = new MongoProperties(); - properties.setHost("mongo.example.com"); - MongoClient client = createMongoClient(properties); - List allAddresses = extractServerAddresses(client); - assertThat(allAddresses).hasSize(1); - assertServerAddress(allAddresses.get(0), "mongo.example.com", 27017); - } - - @Test - void credentialsCanBeCustomized() { - MongoProperties properties = new MongoProperties(); - properties.setUsername("user"); - properties.setPassword("secret".toCharArray()); - MongoClient client = createMongoClient(properties); - assertMongoCredential(extractMongoCredentials(client), "user", "secret", "test"); - } - - @Test - void databaseCanBeCustomized() { - MongoProperties properties = new MongoProperties(); - properties.setDatabase("foo"); - properties.setUsername("user"); - properties.setPassword("secret".toCharArray()); - MongoClient client = createMongoClient(properties); - assertMongoCredential(extractMongoCredentials(client), "user", "secret", "foo"); - } - - @Test - void authenticationDatabaseCanBeCustomized() { - MongoProperties properties = new MongoProperties(); - properties.setAuthenticationDatabase("foo"); - properties.setUsername("user"); - properties.setPassword("secret".toCharArray()); - MongoClient client = createMongoClient(properties); - assertMongoCredential(extractMongoCredentials(client), "user", "secret", "foo"); - } - - @Test - void uriCanBeCustomized() { - MongoProperties properties = new MongoProperties(); - properties.setUri("mongodb://user:secret@mongo1.example.com:12345,mongo2.example.com:23456/test"); - MongoClient client = createMongoClient(properties); - List allAddresses = extractServerAddresses(client); - assertThat(allAddresses).hasSize(2); - assertServerAddress(allAddresses.get(0), "mongo1.example.com", 12345); - assertServerAddress(allAddresses.get(1), "mongo2.example.com", 23456); - MongoCredential credential = extractMongoCredentials(client); - assertMongoCredential(credential, "user", "secret", "test"); - } - - @Test - void retryWritesIsPropagatedFromUri() { - MongoProperties properties = new MongoProperties(); - properties.setUri("mongodb://localhost/test?retryWrites=true"); - MongoClient client = createMongoClient(properties); - assertThat(getSettings(client).getRetryWrites()).isTrue(); - } - - @Test - void uriCannotBeSetWithCredentials() { - MongoProperties properties = new MongoProperties(); - properties.setUri("mongodb://127.0.0.1:1234/mydb"); - properties.setUsername("user"); - properties.setPassword("secret".toCharArray()); - assertThatIllegalStateException().isThrownBy(() -> createMongoClient(properties)).withMessageContaining( - "Invalid mongo configuration, either uri or host/port/credentials must be specified"); - } - - @Test - void uriCannotBeSetWithHostPort() { - MongoProperties properties = new MongoProperties(); - properties.setUri("mongodb://127.0.0.1:1234/mydb"); - properties.setHost("localhost"); - properties.setPort(4567); - assertThatIllegalStateException().isThrownBy(() -> createMongoClient(properties)).withMessageContaining( - "Invalid mongo configuration, either uri or host/port/credentials must be specified"); - } - - @Test - void uriIsIgnoredInEmbeddedMode() { - MongoProperties properties = new MongoProperties(); - properties.setUri("mongodb://mongo.example.com:1234/mydb"); - this.environment.setProperty("local.mongo.port", "4000"); - MongoClient client = createMongoClient(properties, this.environment); - List allAddresses = extractServerAddresses(client); - assertThat(allAddresses).hasSize(1); - assertServerAddress(allAddresses.get(0), "localhost", 4000); - } - - @Test - void customizerIsInvoked() { - MongoProperties properties = new MongoProperties(); - MongoClientSettingsBuilderCustomizer customizer = mock(MongoClientSettingsBuilderCustomizer.class); - createMongoClient(properties, this.environment, customizer); - verify(customizer).customize(any(MongoClientSettings.Builder.class)); - } - - @Test - void customizerIsInvokedWhenHostIsSet() { - MongoProperties properties = new MongoProperties(); - properties.setHost("localhost"); - MongoClientSettingsBuilderCustomizer customizer = mock(MongoClientSettingsBuilderCustomizer.class); - createMongoClient(properties, this.environment, customizer); - verify(customizer).customize(any(MongoClientSettings.Builder.class)); - } - - @Test - void customizerIsInvokedForEmbeddedMongo() { - MongoProperties properties = new MongoProperties(); - this.environment.setProperty("local.mongo.port", "27017"); - MongoClientSettingsBuilderCustomizer customizer = mock(MongoClientSettingsBuilderCustomizer.class); - createMongoClient(properties, this.environment, customizer); - verify(customizer).customize(any(MongoClientSettings.Builder.class)); - } - - private MongoClient createMongoClient(MongoProperties properties) { - return createMongoClient(properties, this.environment); - } - - private MongoClient createMongoClient(MongoProperties properties, Environment environment, - MongoClientSettingsBuilderCustomizer... customizers) { - return new ReactiveMongoClientFactory(properties, environment, Arrays.asList(customizers)) - .createMongoClient(null); - } - - private List extractServerAddresses(MongoClient client) { - MongoClientSettings settings = getSettings(client); - ClusterSettings clusterSettings = settings.getClusterSettings(); - return clusterSettings.getHosts(); - } - - private MongoCredential extractMongoCredentials(MongoClient client) { - return getSettings(client).getCredential(); - } - - @SuppressWarnings("deprecation") - private MongoClientSettings getSettings(MongoClient client) { - return (MongoClientSettings) ReflectionTestUtils.getField(client.getSettings(), "wrapped"); - } +class ReactiveMongoClientFactoryTests extends MongoClientFactorySupportTests { - private void assertServerAddress(ServerAddress serverAddress, String expectedHost, int expectedPort) { - assertThat(serverAddress.getHost()).isEqualTo(expectedHost); - assertThat(serverAddress.getPort()).isEqualTo(expectedPort); + @Override + protected MongoClient createMongoClient(List customizers, + MongoClientSettings settings) { + return new ReactiveMongoClientFactory(customizers).createMongoClient(settings); } - private void assertMongoCredential(MongoCredential credentials, String expectedUsername, String expectedPassword, - String expectedSource) { - assertThat(credentials.getUserName()).isEqualTo(expectedUsername); - assertThat(credentials.getPassword()).isEqualTo(expectedPassword.toCharArray()); - assertThat(credentials.getSource()).isEqualTo(expectedSource); + @Override + protected MongoClientSettings getClientSettings(MongoClient client) { + return ((MongoClientImpl) client).getSettings(); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/embedded/EmbeddedMongoAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/embedded/EmbeddedMongoAutoConfigurationTests.java index 622bb9c14606..bfefd08dc868 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/embedded/EmbeddedMongoAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/embedded/EmbeddedMongoAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,24 +17,25 @@ package org.springframework.boot.autoconfigure.mongo.embedded; import java.io.File; -import java.io.IOException; import java.nio.file.Path; import java.util.EnumSet; import java.util.Map; import java.util.stream.Collectors; -import com.mongodb.MongoClient; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; import de.flapdoodle.embed.mongo.MongodExecutable; import de.flapdoodle.embed.mongo.MongodStarter; -import de.flapdoodle.embed.mongo.config.IMongodConfig; +import de.flapdoodle.embed.mongo.config.MongodConfig; import de.flapdoodle.embed.mongo.config.Storage; import de.flapdoodle.embed.mongo.distribution.Feature; import de.flapdoodle.embed.mongo.distribution.Version; -import de.flapdoodle.embed.process.config.IRuntimeConfig; -import de.flapdoodle.embed.process.config.store.IDownloadConfig; +import de.flapdoodle.embed.process.config.RuntimeConfig; +import de.flapdoodle.embed.process.config.store.DownloadConfig; import org.bson.Document; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.io.TempDir; import org.springframework.beans.DirectFieldAccessor; @@ -44,6 +45,7 @@ import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.boot.testsupport.junit.DisabledOnOs; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; @@ -61,6 +63,9 @@ * @author Stephane Nicoll * @author Issam El-atif */ + +@DisabledOnOs(os = OS.LINUX, architecture = "aarch64", + disabledReason = "Embedded Mongo doesn't support Linux aarch64, see https://github.com/flapdoodle-oss/de.flapdoodle.embed.mongo/issues/379") class EmbeddedMongoAutoConfigurationTests { private AnnotationConfigApplicationContext context; @@ -96,7 +101,7 @@ void customFeatures() { features.add(Feature.ONLY_WINDOWS_2008_SERVER); } load("spring.mongodb.embedded.features=" - + String.join(", ", features.stream().map(Feature::name).collect(Collectors.toList()))); + + features.stream().map(Feature::name).collect(Collectors.joining(", "))); assertThat(this.context.getBean(EmbeddedMongoProperties.class).getFeatures()) .containsExactlyElementsOf(features); } @@ -142,14 +147,14 @@ void portIsAvailableInParentContext() { @Test void defaultStorageConfiguration() { load(MongoClientConfiguration.class); - Storage replication = this.context.getBean(IMongodConfig.class).replication(); + Storage replication = this.context.getBean(MongodConfig.class).replication(); assertThat(replication.getOplogSize()).isEqualTo(0); assertThat(replication.getDatabaseDir()).isNull(); assertThat(replication.getReplSetName()).isNull(); } @Test - void mongoWritesToCustomDatabaseDir(@TempDir Path temp) throws IOException { + void mongoWritesToCustomDatabaseDir(@TempDir Path temp) { File customDatabaseDir = new File(temp.toFile(), "custom-database-dir"); FileSystemUtils.deleteRecursively(customDatabaseDir); load("spring.mongodb.embedded.storage.databaseDir=" + customDatabaseDir.getPath()); @@ -160,26 +165,26 @@ void mongoWritesToCustomDatabaseDir(@TempDir Path temp) throws IOException { @Test void customOpLogSizeIsAppliedToConfiguration() { load("spring.mongodb.embedded.storage.oplogSize=1024KB"); - assertThat(this.context.getBean(IMongodConfig.class).replication().getOplogSize()).isEqualTo(1); + assertThat(this.context.getBean(MongodConfig.class).replication().getOplogSize()).isEqualTo(1); } @Test void customOpLogSizeUsesMegabytesPerDefault() { load("spring.mongodb.embedded.storage.oplogSize=10"); - assertThat(this.context.getBean(IMongodConfig.class).replication().getOplogSize()).isEqualTo(10); + assertThat(this.context.getBean(MongodConfig.class).replication().getOplogSize()).isEqualTo(10); } @Test void customReplicaSetNameIsAppliedToConfiguration() { load("spring.mongodb.embedded.storage.replSetName=testing"); - assertThat(this.context.getBean(IMongodConfig.class).replication().getReplSetName()).isEqualTo("testing"); + assertThat(this.context.getBean(MongodConfig.class).replication().getReplSetName()).isEqualTo("testing"); } @Test void customizeDownloadConfiguration() { load(DownloadConfigBuilderCustomizerConfiguration.class); - IRuntimeConfig runtimeConfig = this.context.getBean(IRuntimeConfig.class); - IDownloadConfig downloadConfig = (IDownloadConfig) new DirectFieldAccessor(runtimeConfig.getArtifactStore()) + RuntimeConfig runtimeConfig = this.context.getBean(RuntimeConfig.class); + DownloadConfig downloadConfig = (DownloadConfig) new DirectFieldAccessor(runtimeConfig.artifactStore()) .getPropertyValue("downloadConfig"); assertThat(downloadConfig.getUserAgent()).isEqualTo("Test User Agent"); } @@ -236,11 +241,8 @@ private boolean isWindows() { return File.separatorChar == '\\'; } - @SuppressWarnings("deprecation") private int getPort(MongoClient client) { - // At some point we'll probably need to use reflection to find the address but for - // now, we can use the deprecated getAddress method. - return client.getAddress().getPort(); + return client.getClusterDescription().getClusterSettings().getHosts().get(0).getPort(); } @Configuration(proxyBeanMethods = false) @@ -248,7 +250,7 @@ static class MongoClientConfiguration { @Bean MongoClient mongoClient(@Value("${local.mongo.port}") int port) { - return new MongoClient("localhost", port); + return MongoClients.create("mongodb://localhost:" + port); } } @@ -267,7 +269,7 @@ DownloadConfigBuilderCustomizer testDownloadConfigBuilderCustomizer() { static class CustomMongoConfiguration { @Bean(initMethod = "start", destroyMethod = "stop") - MongodExecutable customMongoServer(IRuntimeConfig runtimeConfig, IMongodConfig mongodConfig) { + MongodExecutable customMongoServer(RuntimeConfig runtimeConfig, MongodConfig mongodConfig) { MongodStarter mongodStarter = MongodStarter.getInstance(runtimeConfig); return mongodStarter.prepare(mongodConfig); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mustache/MustacheAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mustache/MustacheAutoConfigurationTests.java index 095118fae181..87bf8e5fcdde 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mustache/MustacheAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mustache/MustacheAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,21 @@ package org.springframework.boot.autoconfigure.mustache; +import java.util.function.Supplier; + import com.samskivert.mustache.Mustache; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; -import org.springframework.boot.test.util.TestPropertyValues; -import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebApplicationContext; -import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebApplicationContext; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.AbstractApplicationContextRunner; +import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.web.servlet.view.MustacheViewResolver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; import static org.assertj.core.api.Assertions.assertThat; @@ -33,77 +38,174 @@ * Tests for {@link MustacheAutoConfiguration}. * * @author Brian Clozel + * @author Andy Wilkinson */ class MustacheAutoConfigurationTests { - private AnnotationConfigServletWebApplicationContext webContext; - - private AnnotationConfigReactiveWebApplicationContext reactiveWebContext; - @Test void registerBeansForServletApp() { - loadWithServlet(null); - assertThat(this.webContext.getBeansOfType(Mustache.Compiler.class)).hasSize(1); - assertThat(this.webContext.getBeansOfType(MustacheResourceTemplateLoader.class)).hasSize(1); - assertThat(this.webContext.getBeansOfType(MustacheViewResolver.class)).hasSize(1); + configure(new WebApplicationContextRunner()).run((context) -> { + assertThat(context).hasSingleBean(Mustache.Compiler.class); + assertThat(context).hasSingleBean(MustacheResourceTemplateLoader.class); + assertThat(context).hasSingleBean(MustacheViewResolver.class); + }); + } + + @Test + void servletViewResolverCanBeDisabled() { + configure(new WebApplicationContextRunner()).withPropertyValues("spring.mustache.enabled=false") + .run((context) -> { + assertThat(context).hasSingleBean(Mustache.Compiler.class); + assertThat(context).hasSingleBean(MustacheResourceTemplateLoader.class); + assertThat(context).doesNotHaveBean(MustacheViewResolver.class); + }); } @Test void registerCompilerForServletApp() { - loadWithServlet(CustomCompilerConfiguration.class); - assertThat(this.webContext.getBeansOfType(MustacheResourceTemplateLoader.class)).hasSize(1); - assertThat(this.webContext.getBeansOfType(MustacheViewResolver.class)).hasSize(1); - assertThat(this.webContext.getBeansOfType(Mustache.Compiler.class)).hasSize(1); - assertThat(this.webContext.getBean(Mustache.Compiler.class).standardsMode).isTrue(); + configure(new WebApplicationContextRunner()).withUserConfiguration(CustomCompilerConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(Mustache.Compiler.class); + assertThat(context).hasSingleBean(MustacheResourceTemplateLoader.class); + assertThat(context).hasSingleBean(MustacheViewResolver.class); + assertThat(context.getBean(Mustache.Compiler.class).standardsMode).isTrue(); + }); } @Test void registerBeansForReactiveApp() { - loadWithReactive(null); - assertThat(this.reactiveWebContext.getBeansOfType(Mustache.Compiler.class)).hasSize(1); - assertThat(this.reactiveWebContext.getBeansOfType(MustacheResourceTemplateLoader.class)).hasSize(1); - assertThat(this.reactiveWebContext.getBeansOfType(MustacheViewResolver.class)).isEmpty(); - assertThat(this.reactiveWebContext - .getBeansOfType(org.springframework.boot.web.reactive.result.view.MustacheViewResolver.class)) - .hasSize(1); + configure(new ReactiveWebApplicationContextRunner()).run((context) -> { + assertThat(context).hasSingleBean(Mustache.Compiler.class); + assertThat(context).hasSingleBean(MustacheResourceTemplateLoader.class); + assertThat(context).doesNotHaveBean(MustacheViewResolver.class); + assertThat(context) + .hasSingleBean(org.springframework.boot.web.reactive.result.view.MustacheViewResolver.class); + }); + } + + @Test + void reactiveViewResolverCanBeDisabled() { + configure(new ReactiveWebApplicationContextRunner()).withPropertyValues("spring.mustache.enabled=false") + .run((context) -> { + assertThat(context).hasSingleBean(Mustache.Compiler.class); + assertThat(context).hasSingleBean(MustacheResourceTemplateLoader.class); + assertThat(context).doesNotHaveBean( + org.springframework.boot.web.reactive.result.view.MustacheViewResolver.class); + }); } @Test void registerCompilerForReactiveApp() { - loadWithReactive(CustomCompilerConfiguration.class); - assertThat(this.reactiveWebContext.getBeansOfType(Mustache.Compiler.class)).hasSize(1); - assertThat(this.reactiveWebContext.getBeansOfType(MustacheResourceTemplateLoader.class)).hasSize(1); - assertThat(this.reactiveWebContext.getBeansOfType(MustacheViewResolver.class)).isEmpty(); - assertThat(this.reactiveWebContext - .getBeansOfType(org.springframework.boot.web.reactive.result.view.MustacheViewResolver.class)) - .hasSize(1); - assertThat(this.reactiveWebContext.getBean(Mustache.Compiler.class).standardsMode).isTrue(); - } - - private void loadWithServlet(Class config) { - this.webContext = new AnnotationConfigServletWebApplicationContext(); - TestPropertyValues.of("spring.mustache.prefix=classpath:/mustache-templates/").applyTo(this.webContext); - if (config != null) { - this.webContext.register(config); - } - this.webContext.register(BaseConfiguration.class); - this.webContext.refresh(); + configure(new ReactiveWebApplicationContextRunner()).withUserConfiguration(CustomCompilerConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(Mustache.Compiler.class); + assertThat(context).hasSingleBean(MustacheResourceTemplateLoader.class); + assertThat(context).doesNotHaveBean(MustacheViewResolver.class); + assertThat(context).hasSingleBean( + org.springframework.boot.web.reactive.result.view.MustacheViewResolver.class); + assertThat(context.getBean(Mustache.Compiler.class).standardsMode).isTrue(); + }); } - private void loadWithReactive(Class config) { - this.reactiveWebContext = new AnnotationConfigReactiveWebApplicationContext(); - TestPropertyValues.of("spring.mustache.prefix=classpath:/mustache-templates/").applyTo(this.reactiveWebContext); - if (config != null) { - this.reactiveWebContext.register(config); - } - this.reactiveWebContext.register(BaseConfiguration.class); - this.reactiveWebContext.refresh(); + @Test + void defaultServletViewResolverConfiguration() { + configure(new WebApplicationContextRunner()).run((context) -> { + MustacheViewResolver viewResolver = context.getBean(MustacheViewResolver.class); + assertThat(viewResolver).extracting("allowRequestOverride", InstanceOfAssertFactories.BOOLEAN).isFalse(); + assertThat(viewResolver).extracting("allowSessionOverride", InstanceOfAssertFactories.BOOLEAN).isFalse(); + assertThat(viewResolver).extracting("cache", InstanceOfAssertFactories.BOOLEAN).isFalse(); + assertThat(viewResolver).extracting("charset").isEqualTo("UTF-8"); + assertThat(viewResolver).extracting("exposeRequestAttributes", InstanceOfAssertFactories.BOOLEAN).isFalse(); + assertThat(viewResolver).extracting("exposeSessionAttributes", InstanceOfAssertFactories.BOOLEAN).isFalse(); + assertThat(viewResolver).extracting("exposeSpringMacroHelpers", InstanceOfAssertFactories.BOOLEAN).isTrue(); + assertThat(viewResolver).extracting("prefix").isEqualTo("classpath:/templates/"); + assertThat(viewResolver).extracting("requestContextAttribute").isNull(); + assertThat(viewResolver).extracting("suffix").isEqualTo(".mustache"); + }); } - @Configuration(proxyBeanMethods = false) - @Import({ MustacheAutoConfiguration.class }) - static class BaseConfiguration { + @Test + void defaultReactiveViewResolverConfiguration() { + configure(new ReactiveWebApplicationContextRunner()).run((context) -> { + org.springframework.boot.web.reactive.result.view.MustacheViewResolver viewResolver = context + .getBean(org.springframework.boot.web.reactive.result.view.MustacheViewResolver.class); + assertThat(viewResolver).extracting("charset").isEqualTo("UTF-8"); + assertThat(viewResolver).extracting("prefix").isEqualTo("classpath:/templates/"); + assertThat(viewResolver).extracting("requestContextAttribute").isNull(); + assertThat(viewResolver).extracting("suffix").isEqualTo(".mustache"); + }); + } + + @Test + void allowRequestOverrideCanBeCustomizedOnServletViewResolver() { + assertViewResolverProperty(ViewResolverKind.SERVLET, "spring.mustache.allow-request-override=true", + "allowRequestOverride", true); + } + + @Test + void allowSessionOverrideCanBeCustomizedOnServletViewResolver() { + assertViewResolverProperty(ViewResolverKind.SERVLET, "spring.mustache.allow-session-override=true", + "allowSessionOverride", true); + } + + @Test + void cacheCanBeCustomizedOnServletViewResolver() { + assertViewResolverProperty(ViewResolverKind.SERVLET, "spring.mustache.cache=true", "cache", true); + } + + @ParameterizedTest + @EnumSource(ViewResolverKind.class) + void charsetCanBeCustomizedOnViewResolver(ViewResolverKind kind) { + assertViewResolverProperty(kind, "spring.mustache.charset=UTF-16", "charset", "UTF-16"); + } + + @Test + void exposeRequestAttributesCanBeCustomizedOnServletViewResolver() { + assertViewResolverProperty(ViewResolverKind.SERVLET, "spring.mustache.expose-request-attributes=true", + "exposeRequestAttributes", true); + } + + @Test + void exposeSessionAttributesCanBeCustomizedOnServletViewResolver() { + assertViewResolverProperty(ViewResolverKind.SERVLET, "spring.mustache.expose-session-attributes=true", + "exposeSessionAttributes", true); + } + + @Test + void exposeSpringMacroHelpersCanBeCustomizedOnServletViewResolver() { + assertViewResolverProperty(ViewResolverKind.SERVLET, "spring.mustache.expose-spring-macro-helpers=true", + "exposeSpringMacroHelpers", true); + } + + @ParameterizedTest + @EnumSource(ViewResolverKind.class) + void prefixCanBeCustomizedOnViewResolver(ViewResolverKind kind) { + assertViewResolverProperty(kind, "spring.mustache.prefix=classpath:/mustache-templates/", "prefix", + "classpath:/mustache-templates/"); + } + + @ParameterizedTest + @EnumSource(ViewResolverKind.class) + void requestContextAttributeCanBeCustomizedOnViewResolver(ViewResolverKind kind) { + assertViewResolverProperty(kind, "spring.mustache.request-context-attribute=test", "requestContextAttribute", + "test"); + } + + @ParameterizedTest + @EnumSource(ViewResolverKind.class) + void suffixCanBeCustomizedOnViewResolver(ViewResolverKind kind) { + assertViewResolverProperty(kind, "spring.mustache.suffix=.tache", "suffix", ".tache"); + } + + private void assertViewResolverProperty(ViewResolverKind kind, String property, String field, + Object expectedValue) { + kind.runner().withConfiguration(AutoConfigurations.of(MustacheAutoConfiguration.class)) + .withPropertyValues(property).run((context) -> assertThat(context.getBean(kind.viewResolverClass())) + .extracting(field).isEqualTo(expectedValue)); + } + private > T configure(T runner) { + return runner.withConfiguration(AutoConfigurations.of(MustacheAutoConfiguration.class)); } @Configuration(proxyBeanMethods = false) @@ -116,4 +218,36 @@ Mustache.Compiler compiler(Mustache.TemplateLoader mustacheTemplateLoader) { } + private enum ViewResolverKind { + + /** + * Servlet MustacheViewResolver + */ + SERVLET(WebApplicationContextRunner::new, MustacheViewResolver.class), + + /** + * Reactive MustacheViewResolver + */ + REACTIVE(ReactiveWebApplicationContextRunner::new, + org.springframework.boot.web.reactive.result.view.MustacheViewResolver.class); + + private final Supplier> runner; + + private final Class viewResolverClass; + + ViewResolverKind(Supplier> runner, Class viewResolverClass) { + this.runner = runner; + this.viewResolverClass = viewResolverClass; + } + + private AbstractApplicationContextRunner runner() { + return this.runner.get(); + } + + private Class viewResolverClass() { + return this.viewResolverClass; + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mustache/MustacheAutoConfigurationWithoutWebMvcTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mustache/MustacheAutoConfigurationWithoutWebMvcTests.java new file mode 100644 index 000000000000..be2a47669ef0 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mustache/MustacheAutoConfigurationWithoutWebMvcTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.mustache; + +import com.samskivert.mustache.Mustache; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.testsupport.classpath.ClassPathExclusions; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MustacheAutoConfiguration} without Spring MVC on the class path. + * + * @author Andy Wilkinson + */ +@ClassPathExclusions("spring-webmvc-*.jar") +class MustacheAutoConfigurationWithoutWebMvcTests { + + @Test + void registerBeansForServletAppWithoutMvc() { + new WebApplicationContextRunner().withConfiguration(AutoConfigurations.of(MustacheAutoConfiguration.class)) + .run((context) -> { + assertThat(context).hasSingleBean(Mustache.Compiler.class); + assertThat(context).hasSingleBean(MustacheResourceTemplateLoader.class); + }); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mustache/MustacheStandaloneIntegrationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mustache/MustacheStandaloneIntegrationTests.java index 85bf79322e4e..2fc1dc8fdce6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mustache/MustacheStandaloneIntegrationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mustache/MustacheStandaloneIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationIntegrationTests.java new file mode 100644 index 000000000000..074db2bce1f0 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationIntegrationTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.neo4j; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Result; +import org.neo4j.driver.Session; +import org.neo4j.driver.Transaction; +import org.testcontainers.containers.Neo4jContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link Neo4jAutoConfiguration}. + * + * @author Michael J. Simons + * @author Stephane Nicoll + */ +@SpringBootTest +@Testcontainers(disabledWithoutDocker = true) +class Neo4jAutoConfigurationIntegrationTests { + + @Container + private static final Neo4jContainer neo4jServer = new Neo4jContainer<>(DockerImageNames.neo4j()) + .withStartupAttempts(5).withStartupTimeout(Duration.ofMinutes(10)); + + @DynamicPropertySource + static void neo4jProperties(DynamicPropertyRegistry registry) { + registry.add("spring.neo4j.uri", neo4jServer::getBoltUrl); + registry.add("spring.neo4j.authentication.username", () -> "neo4j"); + registry.add("spring.neo4j.authentication.password", neo4jServer::getAdminPassword); + } + + @Autowired + private Driver driver; + + @Test + void driverCanHandleRequest() { + try (Session session = this.driver.session(); Transaction tx = session.beginTransaction()) { + Result statementResult = tx.run("MATCH (n:Thing) RETURN n LIMIT 1"); + assertThat(statementResult.hasNext()).isFalse(); + tx.commit(); + } + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration(Neo4jAutoConfiguration.class) + static class TestConfiguration { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationTests.java new file mode 100644 index 000000000000..3c892cc48802 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationTests.java @@ -0,0 +1,337 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.neo4j; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.time.Duration; +import java.util.Arrays; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.neo4j.driver.AuthToken; +import org.neo4j.driver.AuthTokens; +import org.neo4j.driver.Config; +import org.neo4j.driver.Config.ConfigBuilder; +import org.neo4j.driver.Driver; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.neo4j.Neo4jProperties.Authentication; +import org.springframework.boot.autoconfigure.neo4j.Neo4jProperties.Security.TrustStrategy; +import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.core.env.Environment; +import org.springframework.mock.env.MockEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link Neo4jAutoConfiguration}. + * + * @author Michael J. Simons + * @author Stephane Nicoll + */ +class Neo4jAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(Neo4jAutoConfiguration.class)); + + @Test + void driverNotConfiguredWithoutDriverApi() { + this.contextRunner.withPropertyValues("spring.neo4j.uri=bolt://localhost:4711") + .withClassLoader(new FilteredClassLoader(Driver.class)) + .run((ctx) -> assertThat(ctx).doesNotHaveBean(Driver.class)); + } + + @Test + void driverShouldNotRequireUri() { + this.contextRunner.run((ctx) -> assertThat(ctx).hasSingleBean(Driver.class)); + } + + @Test + void driverShouldInvokeConfigBuilderCustomizers() { + this.contextRunner.withPropertyValues("spring.neo4j.uri=bolt://localhost:4711") + .withBean(ConfigBuilderCustomizer.class, () -> ConfigBuilder::withEncryption) + .run((ctx) -> assertThat(ctx.getBean(Driver.class).isEncrypted()).isTrue()); + } + + @ParameterizedTest + @ValueSource(strings = { "bolt", "neo4j" }) + void uriWithSimpleSchemeAreDetected(String scheme) { + this.contextRunner.withPropertyValues("spring.neo4j.uri=" + scheme + "://localhost:4711").run((ctx) -> { + assertThat(ctx).hasSingleBean(Driver.class); + assertThat(ctx.getBean(Driver.class).isEncrypted()).isFalse(); + }); + } + + @ParameterizedTest + @ValueSource(strings = { "bolt+s", "bolt+ssc", "neo4j+s", "neo4j+ssc" }) + void uriWithAdvancedSchemesAreDetected(String scheme) { + this.contextRunner.withPropertyValues("spring.neo4j.uri=" + scheme + "://localhost:4711").run((ctx) -> { + assertThat(ctx).hasSingleBean(Driver.class); + Driver driver = ctx.getBean(Driver.class); + assertThat(driver.isEncrypted()).isTrue(); + }); + } + + @ParameterizedTest + @ValueSource(strings = { "bolt+routing", "bolt+x", "neo4j+wth" }) + void uriWithInvalidSchemesAreDetected(String invalidScheme) { + this.contextRunner.withPropertyValues("spring.neo4j.uri=" + invalidScheme + "://localhost:4711") + .run((ctx) -> assertThat(ctx).hasFailed().getFailure() + .hasMessageContaining("'%s' is not a supported scheme.", invalidScheme)); + } + + @Test + void connectionTimeout() { + Neo4jProperties properties = new Neo4jProperties(); + properties.setConnectionTimeout(Duration.ofMillis(500)); + assertThat(mapDriverConfig(properties).connectionTimeoutMillis()).isEqualTo(500); + } + + @Test + void maxTransactionRetryTime() { + Neo4jProperties properties = new Neo4jProperties(); + properties.setMaxTransactionRetryTime(Duration.ofSeconds(2)); + assertThat(mapDriverConfig(properties)).extracting("retrySettings") + .hasFieldOrPropertyWithValue("maxRetryTimeMs", 2000L); + } + + @Test + void determineServerUriShouldDefaultToLocalhost() { + assertThat(determineServerUri(new Neo4jProperties(), new MockEnvironment())) + .isEqualTo(URI.create("bolt://localhost:7687")); + } + + @Test + void determineServerUriWithCustomUriShouldOverrideDefault() { + URI customUri = URI.create("bolt://localhost:4242"); + Neo4jProperties properties = new Neo4jProperties(); + properties.setUri(customUri); + assertThat(determineServerUri(properties, new MockEnvironment())).isEqualTo(customUri); + } + + @Test + @Deprecated + void determineServerUriWithDeprecatedPropertyShouldOverrideDefault() { + URI customUri = URI.create("bolt://localhost:4242"); + MockEnvironment environment = new MockEnvironment().withProperty("spring.data.neo4j.uri", customUri.toString()); + assertThat(determineServerUri(new Neo4jProperties(), environment)).isEqualTo(customUri); + } + + @Test + @Deprecated + void determineServerUriWithCustoUriShouldTakePrecedenceOverDeprecatedProperty() { + URI customUri = URI.create("bolt://localhost:4242"); + URI anotherCustomURI = URI.create("bolt://localhost:2424"); + Neo4jProperties properties = new Neo4jProperties(); + properties.setUri(customUri); + MockEnvironment environment = new MockEnvironment().withProperty("spring.data.neo4j.uri", + anotherCustomURI.toString()); + assertThat(determineServerUri(properties, environment)).isEqualTo(customUri); + } + + @Test + void authenticationShouldDefaultToNone() { + assertThat(mapAuthToken(new Authentication())).isEqualTo(AuthTokens.none()); + } + + @Test + void authenticationWithUsernameShouldEnableBasicAuth() { + Authentication authentication = new Authentication(); + authentication.setUsername("Farin"); + authentication.setPassword("Urlaub"); + assertThat(mapAuthToken(authentication)).isEqualTo(AuthTokens.basic("Farin", "Urlaub")); + } + + @Test + void authenticationWithUsernameAndRealmShouldEnableBasicAuth() { + Authentication authentication = new Authentication(); + authentication.setUsername("Farin"); + authentication.setPassword("Urlaub"); + authentication.setRealm("Test Realm"); + assertThat(mapAuthToken(authentication)).isEqualTo(AuthTokens.basic("Farin", "Urlaub", "Test Realm")); + } + + @Test + @Deprecated + void authenticationWithUsernameUsingDeprecatedPropertiesShouldEnableBasicAuth() { + MockEnvironment environment = new MockEnvironment().withProperty("spring.data.neo4j.username", "user") + .withProperty("spring.data.neo4j.password", "secret"); + assertThat(mapAuthToken(new Authentication(), environment)).isEqualTo(AuthTokens.basic("user", "secret")); + } + + @Test + @Deprecated + void authenticationWithUsernameShouldTakePrecedenceOverDeprecatedPropertiesAndEnableBasicAuth() { + MockEnvironment environment = new MockEnvironment().withProperty("spring.data.neo4j.username", "user") + .withProperty("spring.data.neo4j.password", "secret"); + Authentication authentication = new Authentication(); + authentication.setUsername("Farin"); + authentication.setPassword("Urlaub"); + assertThat(mapAuthToken(authentication, environment)).isEqualTo(AuthTokens.basic("Farin", "Urlaub")); + } + + @Test + void authenticationWithKerberosTicketShouldEnableKerberos() { + Authentication authentication = new Authentication(); + authentication.setKerberosTicket("AABBCCDDEE"); + assertThat(mapAuthToken(authentication)).isEqualTo(AuthTokens.kerberos("AABBCCDDEE")); + } + + @Test + void authenticationWithBothUsernameAndKerberosShouldNotBeAllowed() { + Authentication authentication = new Authentication(); + authentication.setUsername("Farin"); + authentication.setKerberosTicket("AABBCCDDEE"); + assertThatIllegalStateException().isThrownBy(() -> mapAuthToken(authentication)) + .withMessage("Cannot specify both username ('Farin') and kerberos ticket ('AABBCCDDEE')"); + } + + @Test + void poolWithMetricsEnabled() { + Neo4jProperties properties = new Neo4jProperties(); + properties.getPool().setMetricsEnabled(true); + assertThat(mapDriverConfig(properties).isMetricsEnabled()).isTrue(); + } + + @Test + void poolWithLogLeakedSessions() { + Neo4jProperties properties = new Neo4jProperties(); + properties.getPool().setLogLeakedSessions(true); + assertThat(mapDriverConfig(properties).logLeakedSessions()).isTrue(); + } + + @Test + void poolWithMaxConnectionPoolSize() { + Neo4jProperties properties = new Neo4jProperties(); + properties.getPool().setMaxConnectionPoolSize(4711); + assertThat(mapDriverConfig(properties).maxConnectionPoolSize()).isEqualTo(4711); + } + + @Test + void poolWithIdleTimeBeforeConnectionTest() { + Neo4jProperties properties = new Neo4jProperties(); + properties.getPool().setIdleTimeBeforeConnectionTest(Duration.ofSeconds(23)); + assertThat(mapDriverConfig(properties).idleTimeBeforeConnectionTest()).isEqualTo(23000); + } + + @Test + void poolWithMaxConnectionLifetime() { + Neo4jProperties properties = new Neo4jProperties(); + properties.getPool().setMaxConnectionLifetime(Duration.ofSeconds(30)); + assertThat(mapDriverConfig(properties).maxConnectionLifetimeMillis()).isEqualTo(30000); + } + + @Test + void poolWithConnectionAcquisitionTimeout() { + Neo4jProperties properties = new Neo4jProperties(); + properties.getPool().setConnectionAcquisitionTimeout(Duration.ofSeconds(5)); + assertThat(mapDriverConfig(properties).connectionAcquisitionTimeoutMillis()).isEqualTo(5000); + } + + @Test + void securityWithEncrypted() { + Neo4jProperties properties = new Neo4jProperties(); + properties.getSecurity().setEncrypted(true); + assertThat(mapDriverConfig(properties).encrypted()).isTrue(); + } + + @Test + void securityWithTrustSignedCertificates() { + Neo4jProperties properties = new Neo4jProperties(); + properties.getSecurity().setTrustStrategy(TrustStrategy.TRUST_SYSTEM_CA_SIGNED_CERTIFICATES); + assertThat(mapDriverConfig(properties).trustStrategy().strategy()) + .isEqualTo(Config.TrustStrategy.Strategy.TRUST_SYSTEM_CA_SIGNED_CERTIFICATES); + } + + @Test + void securityWithTrustAllCertificates() { + Neo4jProperties properties = new Neo4jProperties(); + properties.getSecurity().setTrustStrategy(TrustStrategy.TRUST_ALL_CERTIFICATES); + assertThat(mapDriverConfig(properties).trustStrategy().strategy()) + .isEqualTo(Config.TrustStrategy.Strategy.TRUST_ALL_CERTIFICATES); + } + + @Test + void securityWitHostnameVerificationEnabled() { + Neo4jProperties properties = new Neo4jProperties(); + properties.getSecurity().setTrustStrategy(TrustStrategy.TRUST_ALL_CERTIFICATES); + properties.getSecurity().setHostnameVerificationEnabled(true); + assertThat(mapDriverConfig(properties).trustStrategy().isHostnameVerificationEnabled()).isTrue(); + } + + @Test + void securityWithCustomCertificates(@TempDir File directory) throws IOException { + File certFile = new File(directory, "neo4j-driver.cert"); + assertThat(certFile.createNewFile()).isTrue(); + + Neo4jProperties properties = new Neo4jProperties(); + properties.getSecurity().setTrustStrategy(TrustStrategy.TRUST_CUSTOM_CA_SIGNED_CERTIFICATES); + properties.getSecurity().setCertFile(certFile); + Config.TrustStrategy trustStrategy = mapDriverConfig(properties).trustStrategy(); + assertThat(trustStrategy.strategy()) + .isEqualTo(Config.TrustStrategy.Strategy.TRUST_CUSTOM_CA_SIGNED_CERTIFICATES); + assertThat(trustStrategy.certFile()).isEqualTo(certFile); + } + + @Test + void securityWithCustomCertificatesShouldFailWithoutCertificate() { + Neo4jProperties properties = new Neo4jProperties(); + properties.getSecurity().setTrustStrategy(TrustStrategy.TRUST_CUSTOM_CA_SIGNED_CERTIFICATES); + assertThatExceptionOfType(InvalidConfigurationPropertyValueException.class) + .isThrownBy(() -> mapDriverConfig(properties)).withMessage( + "Property spring.neo4j.security.trust-strategy with value 'TRUST_CUSTOM_CA_SIGNED_CERTIFICATES' is invalid: Configured trust strategy requires a certificate file."); + } + + @Test + void securityWithTrustSystemCertificates() { + Neo4jProperties properties = new Neo4jProperties(); + properties.getSecurity().setTrustStrategy(TrustStrategy.TRUST_SYSTEM_CA_SIGNED_CERTIFICATES); + assertThat(mapDriverConfig(properties).trustStrategy().strategy()) + .isEqualTo(Config.TrustStrategy.Strategy.TRUST_SYSTEM_CA_SIGNED_CERTIFICATES); + } + + @Test + void driverConfigShouldBeConfiguredToUseUseSpringJclLogging() { + assertThat(mapDriverConfig(new Neo4jProperties()).logging()).isNotNull() + .isInstanceOf(Neo4jSpringJclLogging.class); + } + + private URI determineServerUri(Neo4jProperties properties, Environment environment) { + return new Neo4jAutoConfiguration().determineServerUri(properties, environment); + } + + private AuthToken mapAuthToken(Authentication authentication, Environment environment) { + return new Neo4jAutoConfiguration().mapAuthToken(authentication, environment); + } + + private AuthToken mapAuthToken(Authentication authentication) { + return mapAuthToken(authentication, new MockEnvironment()); + } + + private Config mapDriverConfig(Neo4jProperties properties, ConfigBuilderCustomizer... customizers) { + return new Neo4jAutoConfiguration().mapDriverConfig(properties, Arrays.asList(customizers)); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jPropertiesTests.java new file mode 100644 index 000000000000..07bdc1227572 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jPropertiesTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.neo4j; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; +import org.neo4j.driver.Config; +import org.neo4j.driver.internal.retry.RetrySettings; + +import org.springframework.boot.autoconfigure.neo4j.Neo4jProperties.Pool; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Neo4jProperties}. + * + * @author Michael J. Simons + * @author Stephane Nicoll + */ +class Neo4jPropertiesTests { + + @Test + void poolSettingsHaveConsistentDefaults() { + Config defaultConfig = Config.defaultConfig(); + Pool pool = new Neo4jProperties().getPool(); + assertThat(pool.isMetricsEnabled()).isEqualTo(defaultConfig.isMetricsEnabled()); + assertThat(pool.isLogLeakedSessions()).isEqualTo(defaultConfig.logLeakedSessions()); + assertThat(pool.getMaxConnectionPoolSize()).isEqualTo(defaultConfig.maxConnectionPoolSize()); + assertDuration(pool.getIdleTimeBeforeConnectionTest(), defaultConfig.idleTimeBeforeConnectionTest()); + assertDuration(pool.getMaxConnectionLifetime(), defaultConfig.maxConnectionLifetimeMillis()); + assertDuration(pool.getConnectionAcquisitionTimeout(), defaultConfig.connectionAcquisitionTimeoutMillis()); + } + + @Test + void securitySettingsHaveConsistentDefaults() { + Config defaultConfig = Config.defaultConfig(); + Neo4jProperties properties = new Neo4jProperties(); + assertThat(properties.getSecurity().isEncrypted()).isEqualTo(defaultConfig.encrypted()); + assertThat(properties.getSecurity().getTrustStrategy().name()) + .isEqualTo(defaultConfig.trustStrategy().strategy().name()); + assertThat(properties.getSecurity().isHostnameVerificationEnabled()) + .isEqualTo(defaultConfig.trustStrategy().isHostnameVerificationEnabled()); + } + + @Test + void driverSettingsHaveConsistentDefaults() { + Config defaultConfig = Config.defaultConfig(); + Neo4jProperties properties = new Neo4jProperties(); + assertDuration(properties.getConnectionTimeout(), defaultConfig.connectionTimeoutMillis()); + assertDuration(properties.getMaxTransactionRetryTime(), RetrySettings.DEFAULT.maxRetryTimeMs()); + } + + private static void assertDuration(Duration duration, long expectedValueInMillis) { + if (expectedValueInMillis == org.neo4j.driver.internal.async.pool.PoolSettings.NOT_CONFIGURED) { + assertThat(duration).isNull(); + } + else { + assertThat(duration.toMillis()).isEqualTo(expectedValueInMillis); + } + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/netty/NettyAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/netty/NettyAutoConfigurationTests.java new file mode 100644 index 000000000000..821fd4cba92d --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/netty/NettyAutoConfigurationTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.netty; + +import io.netty.util.ResourceLeakDetector; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link NettyAutoConfiguration}. + * + * @author Brian Clozel + */ +class NettyAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(NettyAutoConfiguration.class)); + + @Test + void leakDetectionShouldBeConfigured() { + this.contextRunner.withPropertyValues("spring.netty.leak-detection=paranoid").run((context) -> { + assertThat(ResourceLeakDetector.getLevel()).isEqualTo(ResourceLeakDetector.Level.PARANOID); + // reset configuration for the following tests. + ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.DISABLED); + }); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/netty/NettyPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/netty/NettyPropertiesTests.java new file mode 100644 index 000000000000..df23a8ecb18d --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/netty/NettyPropertiesTests.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.netty; + +import io.netty.util.ResourceLeakDetector; +import io.netty.util.ResourceLeakDetector.Level; +import org.junit.jupiter.api.Test; + +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link NettyProperties} + * + * @author Brian Clozel + */ +class NettyPropertiesTests { + + @Test + void defaultValueShouldMatchNettys() { + NettyProperties properties = new NettyProperties(); + ResourceLeakDetector.Level defaultLevel = (Level) ReflectionTestUtils.getField(ResourceLeakDetector.class, + "DEFAULT_LEVEL"); + assertThat(ResourceLeakDetector.Level.valueOf(properties.getLeakDetection().name())).isEqualTo(defaultLevel); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/AbstractJpaAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/AbstractJpaAutoConfigurationTests.java index c061ec339239..64408fb98e41 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/AbstractJpaAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/AbstractJpaAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,13 @@ package org.springframework.boot.autoconfigure.orm.jpa; +import java.io.File; import java.util.HashMap; import java.util.Map; import java.util.UUID; import javax.persistence.EntityManagerFactory; +import javax.persistence.spi.PersistenceUnitInfo; import javax.sql.DataSource; import org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform; @@ -31,12 +33,14 @@ import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.test.City; +import org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration; import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.testsupport.BuildOutput; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -49,6 +53,7 @@ import org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter; import org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor; import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionManager; import static org.assertj.core.api.Assertions.assertThat; @@ -68,9 +73,12 @@ abstract class AbstractJpaAutoConfigurationTests { protected AbstractJpaAutoConfigurationTests(Class autoConfiguredClass) { this.autoConfiguredClass = autoConfiguredClass; this.contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.datasource.generate-unique-name=true") - .withUserConfiguration(TestConfiguration.class).withConfiguration(AutoConfigurations.of( - DataSourceAutoConfiguration.class, TransactionAutoConfiguration.class, autoConfiguredClass)); + .withPropertyValues("spring.datasource.generate-unique-name=true", + "spring.jta.log-dir=" + + new File(new BuildOutput(getClass()).getRootLocation(), "transaction-logs")) + .withUserConfiguration(TestConfiguration.class).withConfiguration( + AutoConfigurations.of(DataSourceAutoConfiguration.class, TransactionAutoConfiguration.class, + SqlInitializationAutoConfiguration.class, autoConfiguredClass)); } protected ApplicationContextRunner contextRunner() { @@ -93,7 +101,7 @@ protected ContextConsumer assertJpaIsNotAutoConfig return (context) -> { assertThat(context).hasNotFailed(); assertThat(context).hasSingleBean(JpaProperties.class); - assertThat(context).doesNotHaveBean(PlatformTransactionManager.class); + assertThat(context).doesNotHaveBean(TransactionManager.class); assertThat(context).doesNotHaveBean(EntityManagerFactory.class); }; } @@ -117,7 +125,7 @@ void configuredWithSingleCandidateDataSource() { } @Test - void jtaTransactionManagerTakesPrecedence() { + void jpaTransactionManagerTakesPrecedenceOverSimpleDataSourceOne() { this.contextRunner.withConfiguration(AutoConfigurations.of(DataSourceTransactionManagerAutoConfiguration.class)) .run((context) -> { assertThat(context).hasSingleBean(DataSource.class); @@ -211,7 +219,8 @@ void usesManuallyDefinedEntityManagerFactoryIfAvailable() { @Test void usesManuallyDefinedTransactionManagerBeanIfAvailable() { this.contextRunner.withUserConfiguration(TestConfigurationWithTransactionManager.class).run((context) -> { - PlatformTransactionManager txManager = context.getBean(PlatformTransactionManager.class); + assertThat(context).hasSingleBean(TransactionManager.class); + TransactionManager txManager = context.getBean(TransactionManager.class); assertThat(txManager).isInstanceOf(CustomJpaTransactionManager.class); }); } @@ -227,6 +236,19 @@ void customPersistenceUnitManager() { }); } + @Test + void customPersistenceUnitPostProcessors() { + this.contextRunner.withUserConfiguration(TestConfigurationWithCustomPersistenceUnitPostProcessors.class) + .run((context) -> { + LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = context + .getBean(LocalContainerEntityManagerFactoryBean.class); + PersistenceUnitInfo persistenceUnitInfo = entityManagerFactoryBean.getPersistenceUnitInfo(); + assertThat(persistenceUnitInfo).isNotNull(); + assertThat(persistenceUnitInfo.getManagedClassNames()) + .contains("customized.attribute.converter.class.name"); + }); + } + @Configuration(proxyBeanMethods = false) static class TestTwoDataSourcesConfiguration { @@ -360,7 +382,7 @@ PlatformTransactionManager transactionManager(EntityManagerFactory emf) { static class TestConfigurationWithTransactionManager { @Bean - PlatformTransactionManager transactionManager() { + TransactionManager testTransactionManager() { return new CustomJpaTransactionManager(); } @@ -386,7 +408,18 @@ PersistenceUnitManager persistenceUnitManager() { } - @SuppressWarnings("serial") + @Configuration(proxyBeanMethods = false) + @TestAutoConfigurationPackage(AbstractJpaAutoConfigurationTests.class) + static class TestConfigurationWithCustomPersistenceUnitPostProcessors { + + @Bean + EntityManagerFactoryBuilderCustomizer entityManagerFactoryBuilderCustomizer() { + return (builder) -> builder.setPersistenceUnitPostProcessors( + (pui) -> pui.addManagedClassName("customized.attribute.converter.class.name")); + } + + } + static class CustomJpaTransactionManager extends JpaTransactionManager { } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/DatabaseLookupTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/DatabaseLookupTests.java deleted file mode 100644 index 9de0b98ad209..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/DatabaseLookupTests.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.orm.jpa; - -import java.sql.Connection; -import java.sql.DatabaseMetaData; - -import javax.sql.DataSource; - -import org.junit.jupiter.api.Test; - -import org.springframework.orm.jpa.vendor.Database; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link DatabaseLookup}. - * - * @author Eddú Meléndez - * @author Phillip Webb - */ -class DatabaseLookupTests { - - @Test - void getDatabaseWhenDataSourceIsNullShouldReturnDefault() { - assertThat(DatabaseLookup.getDatabase(null)).isEqualTo(Database.DEFAULT); - } - - @Test - void getDatabaseWhenDataSourceIsUnknownShouldReturnDefault() throws Exception { - testGetDatabase("jdbc:idontexist:", Database.DEFAULT); - } - - @Test - void getDatabaseWhenDerbyShouldReturnDerby() throws Exception { - testGetDatabase("jdbc:derby:", Database.DERBY); - } - - @Test - void getDatabaseWhenH2ShouldReturnH2() throws Exception { - testGetDatabase("jdbc:h2:", Database.H2); - } - - @Test - void getDatabaseWhenHsqldbShouldReturnHsqldb() throws Exception { - testGetDatabase("jdbc:hsqldb:", Database.HSQL); - } - - @Test - void getDatabaseWhenMysqlShouldReturnMysql() throws Exception { - testGetDatabase("jdbc:mysql:", Database.MYSQL); - } - - @Test - void getDatabaseWhenOracleShouldReturnOracle() throws Exception { - testGetDatabase("jdbc:oracle:", Database.ORACLE); - } - - @Test - void getDatabaseWhenPostgresShouldReturnPostgres() throws Exception { - testGetDatabase("jdbc:postgresql:", Database.POSTGRESQL); - } - - @Test - void getDatabaseWhenSqlserverShouldReturnSqlserver() throws Exception { - testGetDatabase("jdbc:sqlserver:", Database.SQL_SERVER); - } - - @Test - void getDatabaseWhenDb2ShouldReturnDb2() throws Exception { - testGetDatabase("jdbc:db2:", Database.DB2); - } - - @Test - void getDatabaseWhenInformixShouldReturnInformix() throws Exception { - testGetDatabase("jdbc:informix-sqli:", Database.INFORMIX); - } - - @Test - void getDatabaseWhenSapShouldReturnHana() throws Exception { - testGetDatabase("jdbc:sap:", Database.HANA); - } - - private void testGetDatabase(String url, Database expected) throws Exception { - DataSource dataSource = mock(DataSource.class); - Connection connection = mock(Connection.class); - DatabaseMetaData metaData = mock(DatabaseMetaData.class); - given(dataSource.getConnection()).willReturn(connection); - given(connection.getMetaData()).willReturn(metaData); - given(metaData.getURL()).willReturn(url); - Database database = DatabaseLookup.getDatabase(dataSource); - assertThat(database).isEqualTo(expected); - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/Hibernate2ndLevelCacheIntegrationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/Hibernate2ndLevelCacheIntegrationTests.java index fe83c4483302..049ae5abe9b8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/Hibernate2ndLevelCacheIntegrationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/Hibernate2ndLevelCacheIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,6 @@ class Hibernate2ndLevelCacheIntegrationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(CacheAutoConfiguration.class, DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class)) - .withPropertyValues("spring.datasource.initialization-mode=never") .withUserConfiguration(TestConfiguration.class); @Test diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java index 23fd60c49c18..36813a7f1f96 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ import java.io.IOException; import java.net.URL; import java.net.URLClassLoader; -import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -28,7 +27,6 @@ import java.util.List; import java.util.Map; import java.util.function.Consumer; -import java.util.stream.Collectors; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; @@ -39,7 +37,6 @@ import javax.transaction.UserTransaction; import com.zaxxer.hikari.HikariDataSource; -import org.awaitility.Awaitility; import org.hibernate.boot.model.naming.ImplicitNamingStrategy; import org.hibernate.boot.model.naming.PhysicalNamingStrategy; import org.hibernate.cfg.AvailableSettings; @@ -55,7 +52,6 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceSchemaCreatedEvent; import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration; @@ -66,12 +62,14 @@ import org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy; import org.springframework.boot.orm.jpa.hibernate.SpringJtaPlatform; import org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy; +import org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitialization; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.JpaVendorAdapter; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; @@ -79,8 +77,8 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.entry; -import static org.hamcrest.Matchers.hasSize; import static org.mockito.Mockito.mock; /** @@ -91,6 +89,7 @@ * @author Andy Wilkinson * @author Kazuki Shimizu * @author Stephane Nicoll + * @author Chris Bono */ class HibernateJpaAutoConfigurationTests extends AbstractJpaAutoConfigurationTests { @@ -99,13 +98,23 @@ class HibernateJpaAutoConfigurationTests extends AbstractJpaAutoConfigurationTes } @Test - void testDataScriptWithMissingDdl() { + @Deprecated + void testDataScriptWithDeprecatedMissingDdl() { contextRunner().withPropertyValues("spring.datasource.data:classpath:/city.sql", // Missing: "spring.datasource.schema:classpath:/ddl.sql").run((context) -> { assertThat(context).hasFailed(); assertThat(context.getStartupFailure()).hasMessageContaining("ddl.sql"); - assertThat(context.getStartupFailure()).hasMessageContaining("spring.datasource.schema"); + }); + } + + @Test + void testDmlScriptWithMissingDdl() { + contextRunner().withPropertyValues("spring.sql.init.data-locations:classpath:/city.sql", + // Missing: + "spring.sql.init.schema-locations:classpath:/ddl.sql").run((context) -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()).hasMessageContaining("ddl.sql"); }); } @@ -120,19 +129,37 @@ void testDataScript() { } @Test + void testDmlScript() { + // This can't succeed because the data SQL is executed immediately after the + // schema and Hibernate hasn't initialized yet at that point + contextRunner().withPropertyValues("spring.sql.init.data-locations:/city.sql").run((context) -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()).isInstanceOf(BeanCreationException.class); + }); + } + + @Test + @Deprecated void testDataScriptRunsEarly() { contextRunner().withUserConfiguration(TestInitializedJpaConfiguration.class) .withClassLoader(new HideDataScriptClassLoader()) .withPropertyValues("spring.jpa.show-sql=true", "spring.jpa.hibernate.ddl-auto:create-drop", - "spring.datasource.data:classpath:/city.sql") + "spring.datasource.data:classpath:/city.sql", "spring.jpa.defer-datasource-initialization=true") + .run((context) -> assertThat(context.getBean(TestInitializedJpaConfiguration.class).called).isTrue()); + } + + @Test + void testDmlScriptRunsEarly() { + contextRunner().withUserConfiguration(TestInitializedJpaConfiguration.class) + .withClassLoader(new HideDataScriptClassLoader()) + .withPropertyValues("spring.jpa.show-sql=true", "spring.jpa.hibernate.ddl-auto:create-drop", + "spring.sql.init.data-locations:/city.sql", "spring.jpa.defer-datasource-initialization=true") .run((context) -> assertThat(context.getBean(TestInitializedJpaConfiguration.class).called).isTrue()); } @Test void testFlywaySwitchOffDdlAuto() { - contextRunner() - .withPropertyValues("spring.datasource.initialization-mode:never", - "spring.flyway.locations:classpath:db/city") + contextRunner().withPropertyValues("spring.sql.init.mode:never", "spring.flyway.locations:classpath:db/city") .withConfiguration(AutoConfigurations.of(FlywayAutoConfiguration.class)) .run((context) -> assertThat(context).hasNotFailed()); } @@ -140,8 +167,8 @@ void testFlywaySwitchOffDdlAuto() { @Test void testFlywayPlusValidation() { contextRunner() - .withPropertyValues("spring.datasource.initialization-mode:never", - "spring.flyway.locations:classpath:db/city", "spring.jpa.hibernate.ddl-auto:validate") + .withPropertyValues("spring.sql.init.mode:never", "spring.flyway.locations:classpath:db/city", + "spring.jpa.hibernate.ddl-auto:validate") .withConfiguration(AutoConfigurations.of(FlywayAutoConfiguration.class)) .run((context) -> assertThat(context).hasNotFailed()); } @@ -149,8 +176,7 @@ void testFlywayPlusValidation() { @Test void testLiquibasePlusValidation() { contextRunner() - .withPropertyValues("spring.datasource.initialization-mode:never", - "spring.liquibase.changeLog:classpath:db/changelog/db.changelog-city.yaml", + .withPropertyValues("spring.liquibase.changeLog:classpath:db/changelog/db.changelog-city.yaml", "spring.jpa.hibernate.ddl-auto:validate") .withConfiguration(AutoConfigurations.of(LiquibaseAutoConfiguration.class)) .run((context) -> assertThat(context).hasNotFailed()); @@ -285,8 +311,9 @@ void providerDisablesAutoCommitIsNotConfiguredWithJta() { @Test void customResourceMapping() { contextRunner().withClassLoader(new HideDataScriptClassLoader()) - .withPropertyValues("spring.datasource.data:classpath:/db/non-annotated-data.sql", - "spring.jpa.mapping-resources=META-INF/mappings/non-annotated.xml") + .withPropertyValues("spring.sql.init.data-locations:classpath:/db/non-annotated-data.sql", + "spring.jpa.mapping-resources=META-INF/mappings/non-annotated.xml", + "spring.jpa.defer-datasource-initialization=true") .run((context) -> { EntityManager em = context.getBean(EntityManagerFactory.class).createEntityManager(); NonAnnotatedEntity found = em.find(NonAnnotatedEntity.class, 2000L); @@ -356,8 +383,10 @@ void hibernatePropertiesCustomizerTakesPrecedenceOverStrategyInstancesAndNamingS @Test void eventListenerCanBeRegisteredAsBeans() { contextRunner().withUserConfiguration(TestInitializedJpaConfiguration.class) - .withClassLoader(new HideDataScriptClassLoader()).withPropertyValues("spring.jpa.show-sql=true", - "spring.jpa.hibernate.ddl-auto:create-drop", "spring.datasource.data:classpath:/city.sql") + .withClassLoader(new HideDataScriptClassLoader()) + .withPropertyValues("spring.jpa.show-sql=true", "spring.jpa.hibernate.ddl-auto:create-drop", + "spring.sql.init.data-locations:classpath:/city.sql", + "spring.jpa.defer-datasource-initialization=true") .run((context) -> { // See CityListener assertThat(context).hasSingleBean(City.class); @@ -371,32 +400,95 @@ void hibernatePropertiesCustomizerCanDisableBeanContainer() { .run((context) -> assertThat(context).doesNotHaveBean(City.class)); } + @Test + void vendorPropertiesWithEmbeddedDatabaseAndNoDdlProperty() { + contextRunner().run(vendorProperties((vendorProperties) -> { + assertThat(vendorProperties).doesNotContainKeys(AvailableSettings.HBM2DDL_DATABASE_ACTION); + assertThat(vendorProperties.get(AvailableSettings.HBM2DDL_AUTO)).isEqualTo("create-drop"); + })); + } + + @Test + void vendorPropertiesWhenDdlAutoPropertyIsSet() { + contextRunner().withPropertyValues("spring.jpa.hibernate.ddl-auto=update") + .run(vendorProperties((vendorProperties) -> { + assertThat(vendorProperties).doesNotContainKeys(AvailableSettings.HBM2DDL_DATABASE_ACTION); + assertThat(vendorProperties.get(AvailableSettings.HBM2DDL_AUTO)).isEqualTo("update"); + })); + } + + @Test + void vendorPropertiesWhenDdlAutoPropertyAndHibernatePropertiesAreSet() { + contextRunner() + .withPropertyValues("spring.jpa.hibernate.ddl-auto=update", + "spring.jpa.properties.hibernate.hbm2ddl.auto=create-drop") + .run(vendorProperties((vendorProperties) -> { + assertThat(vendorProperties).doesNotContainKeys(AvailableSettings.HBM2DDL_DATABASE_ACTION); + assertThat(vendorProperties.get(AvailableSettings.HBM2DDL_AUTO)).isEqualTo("create-drop"); + })); + } + + @Test + void vendorPropertiesWhenDdlAutoPropertyIsSetToNone() { + contextRunner().withPropertyValues("spring.jpa.hibernate.ddl-auto=none") + .run(vendorProperties((vendorProperties) -> assertThat(vendorProperties).doesNotContainKeys( + AvailableSettings.HBM2DDL_DATABASE_ACTION, AvailableSettings.HBM2DDL_AUTO))); + } + + @Test + void vendorPropertiesWhenJpaDdlActionIsSet() { + contextRunner() + .withPropertyValues("spring.jpa.properties.javax.persistence.schema-generation.database.action=create") + .run(vendorProperties((vendorProperties) -> { + assertThat(vendorProperties.get(AvailableSettings.HBM2DDL_DATABASE_ACTION)).isEqualTo("create"); + assertThat(vendorProperties).doesNotContainKeys(AvailableSettings.HBM2DDL_AUTO); + })); + } + + @Test + void vendorPropertiesWhenBothDdlAutoPropertiesAreSet() { + contextRunner() + .withPropertyValues("spring.jpa.properties.javax.persistence.schema-generation.database.action=create", + "spring.jpa.hibernate.ddl-auto=create-only") + .run(vendorProperties((vendorProperties) -> { + assertThat(vendorProperties.get(AvailableSettings.HBM2DDL_DATABASE_ACTION)).isEqualTo("create"); + assertThat(vendorProperties.get(AvailableSettings.HBM2DDL_AUTO)).isEqualTo("create-only"); + })); + } + + private ContextConsumer vendorProperties( + Consumer> vendorProperties) { + return (context) -> vendorProperties + .accept(context.getBean(HibernateJpaConfiguration.class).getVendorProperties()); + } + @Test void withSyncBootstrappingAnApplicationListenerThatUsesJpaDoesNotTriggerABeanCurrentlyInCreationException() { - contextRunner().withUserConfiguration(JpaUsingApplicationListenerConfiguration.class) - .withPropertyValues("spring.datasource.initialization-mode=never").run((context) -> { - assertThat(context).hasNotFailed(); - assertThat(context.getBean(EventCapturingApplicationListener.class).events.stream() - .filter(DataSourceSchemaCreatedEvent.class::isInstance)).hasSize(1); - }); + contextRunner().withUserConfiguration(JpaUsingApplicationListenerConfiguration.class).run((context) -> { + assertThat(context).hasNotFailed(); + EventCapturingApplicationListener listener = context.getBean(EventCapturingApplicationListener.class); + assertThat(listener.events).hasSize(1); + assertThat(listener.events).hasOnlyElementsOfType(ContextRefreshedEvent.class); + }); } @Test void withAsyncBootstrappingAnApplicationListenerThatUsesJpaDoesNotTriggerABeanCurrentlyInCreationException() { - contextRunner() - .withUserConfiguration(AsyncBootstrappingConfiguration.class, - JpaUsingApplicationListenerConfiguration.class) - .withPropertyValues("spring.datasource.initialization-mode=never").run((context) -> { + contextRunner().withUserConfiguration(AsyncBootstrappingConfiguration.class, + JpaUsingApplicationListenerConfiguration.class).run((context) -> { assertThat(context).hasNotFailed(); EventCapturingApplicationListener listener = context .getBean(EventCapturingApplicationListener.class); - Awaitility.waitAtMost(Duration.ofSeconds(30)) - .until(() -> dataSourceSchemaCreatedEventsReceivedBy(listener), hasSize(1)); + assertThat(listener.events).hasSize(1); + assertThat(listener.events).hasOnlyElementsOfType(ContextRefreshedEvent.class); + // createEntityManager requires Hibernate bootstrapping to be complete + assertThatNoException() + .isThrownBy(() -> context.getBean(EntityManagerFactory.class).createEntityManager()); }); } @Test - void whenLocalContanerEntityManagerFactoryBeanHasNoJpaVendorAdapterAutoConfigurationSucceeds() { + void whenLocalContainerEntityManagerFactoryBeanHasNoJpaVendorAdapterAutoConfigurationSucceeds() { contextRunner() .withUserConfiguration( TestConfigurationWithLocalContainerEntityManagerFactoryBeanWithNoJpaVendorAdapter.class) @@ -407,13 +499,9 @@ void whenLocalContanerEntityManagerFactoryBeanHasNoJpaVendorAdapterAutoConfigura }); } - private List dataSourceSchemaCreatedEventsReceivedBy(EventCapturingApplicationListener listener) { - return listener.events.stream().filter(DataSourceSchemaCreatedEvent.class::isInstance) - .collect(Collectors.toList()); - } - @Configuration(proxyBeanMethods = false) @TestAutoConfigurationPackage(City.class) + @DependsOnDatabaseInitialization static class TestInitializedJpaConfiguration { private boolean called; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernatePropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernatePropertiesTests.java index b58da5f8d63d..f98a1e41fb4c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernatePropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernatePropertiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,10 +21,10 @@ import java.util.function.Supplier; import org.hibernate.cfg.AvailableSettings; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy; @@ -36,15 +36,17 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; /** * Tests for {@link HibernateProperties}. * * @author Stephane Nicoll * @author Artsiom Yudovin + * @author Chris Bono */ +@ExtendWith(MockitoExtension.class) class HibernatePropertiesTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() @@ -53,11 +55,6 @@ class HibernatePropertiesTests { @Mock private Supplier ddlAutoSupplier; - @BeforeEach - void setup() { - MockitoAnnotations.initMocks(this); - } - @Test void noCustomNamingStrategy() { this.contextRunner.run(assertHibernateProperties((hibernateProperties) -> { @@ -135,10 +132,23 @@ void defaultDdlAutoIsNotInvokedIfHibernateSpecificPropertyIsSet() { .run(assertDefaultDdlAutoNotInvoked("create")); } + @Test + void defaultDdlAutoIsNotInvokedAndDdlAutoIsNotSetIfJpaDbActionPropertyIsSet() { + this.contextRunner + .withPropertyValues( + "spring.jpa.properties.javax.persistence.schema-generation.database.action=drop-and-create") + .run(assertHibernateProperties((hibernateProperties) -> { + assertThat(hibernateProperties).doesNotContainKey(AvailableSettings.HBM2DDL_AUTO); + assertThat(hibernateProperties).containsEntry(AvailableSettings.HBM2DDL_DATABASE_ACTION, + "drop-and-create"); + then(this.ddlAutoSupplier).should(never()).get(); + })); + } + private ContextConsumer assertDefaultDdlAutoNotInvoked(String expectedDdlAuto) { return assertHibernateProperties((hibernateProperties) -> { assertThat(hibernateProperties).containsEntry(AvailableSettings.HBM2DDL_AUTO, expectedDdlAuto); - verify(this.ddlAutoSupplier, never()).get(); + then(this.ddlAutoSupplier).should(never()).get(); }); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/JpaPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/JpaPropertiesTests.java deleted file mode 100644 index 2453e9d985db..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/JpaPropertiesTests.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.autoconfigure.orm.jpa; - -import java.sql.Connection; -import java.sql.DatabaseMetaData; -import java.sql.SQLException; -import java.util.function.Consumer; - -import javax.sql.DataSource; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.test.context.assertj.AssertableApplicationContext; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.boot.test.context.runner.ContextConsumer; -import org.springframework.context.annotation.Configuration; -import org.springframework.orm.jpa.vendor.Database; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; - -/** - * Tests for {@link JpaProperties}. - * - * @author Stephane Nicoll - */ -class JpaPropertiesTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withUserConfiguration(TestConfiguration.class); - - @Test - @Deprecated - @SuppressWarnings("deprecation") - void determineDatabaseNoCheckIfDatabaseIsSet() { - this.contextRunner.withPropertyValues("spring.jpa.database=postgresql") - .run(assertJpaProperties((properties) -> { - DataSource dataSource = mockStandaloneDataSource(); - Database database = properties.determineDatabase(dataSource); - assertThat(database).isEqualTo(Database.POSTGRESQL); - try { - verify(dataSource, never()).getConnection(); - } - catch (SQLException ex) { - throw new IllegalStateException("Should not happen", ex); - } - })); - } - - @Test - @Deprecated - @SuppressWarnings("deprecation") - void determineDatabaseWithKnownUrl() { - this.contextRunner.run(assertJpaProperties((properties) -> { - Database database = properties.determineDatabase(mockDataSource("jdbc:h2:mem:testdb")); - assertThat(database).isEqualTo(Database.H2); - })); - } - - @Test - @Deprecated - @SuppressWarnings("deprecation") - void determineDatabaseWithKnownUrlAndUserConfig() { - this.contextRunner.withPropertyValues("spring.jpa.database=mysql").run(assertJpaProperties((properties) -> { - Database database = properties.determineDatabase(mockDataSource("jdbc:h2:mem:testdb")); - assertThat(database).isEqualTo(Database.MYSQL); - })); - } - - @Test - @Deprecated - @SuppressWarnings("deprecation") - void determineDatabaseWithUnknownUrl() { - this.contextRunner.run(assertJpaProperties((properties) -> { - Database database = properties.determineDatabase(mockDataSource("jdbc:unknown://localhost")); - assertThat(database).isEqualTo(Database.DEFAULT); - })); - } - - private DataSource mockStandaloneDataSource() { - try { - DataSource ds = mock(DataSource.class); - given(ds.getConnection()).willThrow(SQLException.class); - return ds; - } - catch (SQLException ex) { - throw new IllegalStateException("Should not happen", ex); - } - } - - private DataSource mockDataSource(String jdbcUrl) { - DataSource ds = mock(DataSource.class); - try { - DatabaseMetaData metadata = mock(DatabaseMetaData.class); - given(metadata.getURL()).willReturn(jdbcUrl); - Connection connection = mock(Connection.class); - given(connection.getMetaData()).willReturn(metadata); - given(ds.getConnection()).willReturn(connection); - } - catch (SQLException ex) { - // Do nothing - } - return ds; - } - - private ContextConsumer assertJpaProperties(Consumer consumer) { - return (context) -> { - assertThat(context).hasSingleBean(JpaProperties.class); - consumer.accept(context.getBean(JpaProperties.class)); - }; - } - - @Configuration(proxyBeanMethods = false) - @EnableConfigurationProperties(JpaProperties.class) - static class TestConfiguration { - - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/packagestest/one/FirstConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/packagestest/one/FirstConfiguration.java index 5a2286d4bd06..c7b828f0d4d9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/packagestest/one/FirstConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/packagestest/one/FirstConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,8 @@ package org.springframework.boot.autoconfigure.packagestest.one; -import org.springframework.boot.autoconfigure.AutoConfigurationPackagesTests.TestRegistrar; +import org.springframework.boot.autoconfigure.AutoConfigurationPackage; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; /** * Sample configuration used in {@code AutoConfigurationPackagesTests}. @@ -26,7 +25,7 @@ * @author Oliver Gierke */ @Configuration(proxyBeanMethods = false) -@Import(TestRegistrar.class) +@AutoConfigurationPackage public class FirstConfiguration { } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/packagestest/two/SecondConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/packagestest/two/SecondConfiguration.java index dbe32ebbbd57..66a1bf074208 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/packagestest/two/SecondConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/packagestest/two/SecondConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,18 +16,16 @@ package org.springframework.boot.autoconfigure.packagestest.two; -import org.springframework.boot.autoconfigure.AutoConfigurationPackagesTests; -import org.springframework.boot.autoconfigure.AutoConfigurationPackagesTests.TestRegistrar; +import org.springframework.boot.autoconfigure.AutoConfigurationPackage; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; /** - * Sample configuration used in {@link AutoConfigurationPackagesTests}. + * Sample configuration used in {@code AutoConfigurationPackagesTests}. * * @author Oliver Gierke */ @Configuration(proxyBeanMethods = false) -@Import(TestRegistrar.class) +@AutoConfigurationPackage public class SecondConfiguration { } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/quartz/QuartzAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/quartz/QuartzAutoConfigurationTests.java index cb677e5ba6cb..d54f769ec93b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/quartz/QuartzAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/quartz/QuartzAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,10 +19,12 @@ import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Duration; import java.util.concurrent.Executor; import javax.sql.DataSource; +import org.awaitility.Awaitility; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.io.TempDir; @@ -60,14 +62,16 @@ import org.springframework.core.env.Environment; import org.springframework.core.io.ClassPathResource; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.scheduling.quartz.LocalDataSourceJobStore; import org.springframework.scheduling.quartz.QuartzJobBean; import org.springframework.scheduling.quartz.SchedulerFactoryBean; +import org.springframework.transaction.PlatformTransactionManager; import org.springframework.util.Assert; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verifyNoInteractions; /** * Tests for {@link QuartzAutoConfiguration}. @@ -136,6 +140,17 @@ void dataSourceWithQuartzDataSourceQualifierUsedWhenMultiplePresent() { .run(assertDataSourceJobStore("quartzDataSource")); } + @Test + void transactionManagerWithQuartzTransactionManagerUsedWhenMultiplePresent() { + this.contextRunner + .withUserConfiguration(QuartzJobsConfiguration.class, MultipleTransactionManagersConfiguration.class) + .withPropertyValues("spring.quartz.job-store-type=jdbc").run((context) -> { + SchedulerFactoryBean schedulerFactoryBean = context.getBean(SchedulerFactoryBean.class); + assertThat(schedulerFactoryBean).extracting("transactionManager") + .isEqualTo(context.getBean("quartzTransactionManager")); + }); + } + private ContextConsumer assertDataSourceJobStore(String datasourceName) { return (context) -> { assertThat(context).hasSingleBean(Scheduler.class); @@ -157,7 +172,7 @@ void withTaskExecutor() { Scheduler scheduler = context.getBean(Scheduler.class); assertThat(scheduler.getMetaData().getThreadPoolSize()).isEqualTo(50); Executor executor = context.getBean(Executor.class); - verifyNoInteractions(executor); + then(executor).shouldHaveNoInteractions(); }); } @@ -181,8 +196,8 @@ void withConfiguredJobAndTrigger(CapturedOutput output) { Scheduler scheduler = context.getBean(Scheduler.class); assertThat(scheduler.getJobDetail(JobKey.jobKey("fooJob"))).isNotNull(); assertThat(scheduler.getTrigger(TriggerKey.triggerKey("fooTrigger"))).isNotNull(); - Thread.sleep(1000L); - assertThat(output).contains("withConfiguredJobAndTrigger").contains("jobDataValue"); + Awaitility.waitAtMost(Duration.ofSeconds(5)).untilAsserted( + () -> assertThat(output).contains("withConfiguredJobAndTrigger").contains("jobDataValue")); }); } @@ -431,6 +446,51 @@ private DataSource createTestDataSource() throws Exception { } + @Configuration(proxyBeanMethods = false) + static class MultipleTransactionManagersConfiguration extends BaseQuartzConfiguration { + + private final DataSource primaryDataSource = createTestDataSource(); + + private final DataSource quartzDataSource = createTestDataSource(); + + @Bean + @Primary + DataSource applicationDataSource() { + return this.primaryDataSource; + } + + @Bean + @QuartzDataSource + DataSource quartzDataSource() { + return this.quartzDataSource; + } + + @Bean + @Primary + PlatformTransactionManager applicationTransactionManager() { + return new DataSourceTransactionManager(this.primaryDataSource); + } + + @Bean + @QuartzTransactionManager + PlatformTransactionManager quartzTransactionManager() { + return new DataSourceTransactionManager(this.quartzDataSource); + } + + private DataSource createTestDataSource() { + DataSourceProperties properties = new DataSourceProperties(); + properties.setGenerateUniqueName(true); + try { + properties.afterPropertiesSet(); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + return properties.initializeDataSourceBuilder().build(); + } + + } + static class ComponentThatUsesScheduler { ComponentThatUsesScheduler(Scheduler scheduler) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/quartz/QuartzDataSourceInitializerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/quartz/QuartzDataSourceInitializerTests.java index 6a4d980e0269..2fb2eca62a12 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/quartz/QuartzDataSourceInitializerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/quartz/QuartzDataSourceInitializerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,10 +30,13 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.ResourceLoader; import org.springframework.jdbc.core.JdbcTemplate; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; /** * Tests for {@link QuartzDataSourceInitializer}. @@ -48,6 +51,17 @@ class QuartzDataSourceInitializerTests { .withPropertyValues("spring.datasource.url=" + String.format( "jdbc:h2:mem:test-%s;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE", UUID.randomUUID().toString())); + @Test + void getDatabaseNameWithPlatformDoesNotTouchDataSource() { + DataSource dataSource = mock(DataSource.class); + QuartzProperties properties = new QuartzProperties(); + properties.getJdbc().setPlatform("test"); + QuartzDataSourceInitializer initializer = new QuartzDataSourceInitializer(dataSource, + new DefaultResourceLoader(), properties); + assertThat(initializer.getDatabaseName()).isEqualTo("test"); + then(dataSource).shouldHaveNoInteractions(); + } + @Test void hashIsUsedAsACommentPrefixByDefault() { this.contextRunner.withUserConfiguration(TestConfiguration.class).withPropertyValues( diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBeanCreationFailureAnalyzerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBeanCreationFailureAnalyzerTests.java new file mode 100644 index 000000000000..2faacabe3783 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBeanCreationFailureAnalyzerTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.r2dbc; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.diagnostics.FailureAnalysis; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.mock.env.MockEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConnectionFactoryBeanCreationFailureAnalyzer}. + * + * @author Mark Paluch + */ +class ConnectionFactoryBeanCreationFailureAnalyzerTests { + + private final MockEnvironment environment = new MockEnvironment(); + + @Test + void failureAnalysisIsPerformed() { + FailureAnalysis failureAnalysis = performAnalysis(TestConfiguration.class); + assertThat(failureAnalysis.getDescription()).contains("'url' attribute is not specified", + "no embedded database could be configured"); + assertThat(failureAnalysis.getAction()).contains( + "If you want an embedded database (H2), please put it on the classpath", + "If you have database settings to be loaded from a particular profile you may need to activate it", + "(no profiles are currently active)"); + } + + @Test + void failureAnalysisIsPerformedWithActiveProfiles() { + this.environment.setActiveProfiles("first", "second"); + FailureAnalysis failureAnalysis = performAnalysis(TestConfiguration.class); + assertThat(failureAnalysis.getAction()).contains("(the profiles first,second are currently active)"); + } + + private FailureAnalysis performAnalysis(Class configuration) { + BeanCreationException failure = createFailure(configuration); + assertThat(failure).isNotNull(); + ConnectionFactoryBeanCreationFailureAnalyzer failureAnalyzer = new ConnectionFactoryBeanCreationFailureAnalyzer(); + failureAnalyzer.setEnvironment(this.environment); + return failureAnalyzer.analyze(failure); + } + + private BeanCreationException createFailure(Class configuration) { + try { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.setClassLoader(new FilteredClassLoader("io.r2dbc.h2", "io.r2dbc.pool")); + context.setEnvironment(this.environment); + context.register(configuration); + context.refresh(); + context.close(); + return null; + } + catch (BeanCreationException ex) { + return ex; + } + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration(R2dbcAutoConfiguration.class) + static class TestConfiguration { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBuilderTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBuilderTests.java new file mode 100644 index 000000000000..61af160d5950 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryBuilderTests.java @@ -0,0 +1,214 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.r2dbc; + +import io.r2dbc.spi.ConnectionFactoryOptions; +import io.r2dbc.spi.Option; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.r2dbc.ConnectionFactoryOptionsInitializer.ConnectionFactoryBeanCreationException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; + +/** + * Tests for {@link ConnectionFactoryBuilder}. + * + * @author Mark Paluch + * @author Tadaya Tsuyukubo + * @author Stephane Nicoll + */ +@Deprecated +class ConnectionFactoryBuilderTests { + + @Test + void propertiesWithoutUrlAndNoAvailableEmbeddedConnectionShouldFail() { + R2dbcProperties properties = new R2dbcProperties(); + assertThatThrownBy(() -> ConnectionFactoryBuilder.of(properties, () -> EmbeddedDatabaseConnection.NONE)) + .isInstanceOf(ConnectionFactoryBeanCreationException.class) + .hasMessage("Failed to determine a suitable R2DBC Connection URL"); + } + + @Test + void connectionFactoryBeanCreationProvidesConnectionAndProperties() { + R2dbcProperties properties = new R2dbcProperties(); + try { + ConnectionFactoryBuilder.of(properties, () -> EmbeddedDatabaseConnection.NONE); + fail("Should have thrown a " + ConnectionFactoryBeanCreationException.class.getName()); + } + catch (ConnectionFactoryBeanCreationException ex) { + assertThat(ex.getEmbeddedDatabaseConnection()) + .isEqualTo(org.springframework.boot.r2dbc.EmbeddedDatabaseConnection.NONE); + assertThat(ex.getProperties()).isSameAs(properties); + } + } + + @Test + void regularConnectionIsConfiguredAutomaticallyWithUrl() { + R2dbcProperties properties = new R2dbcProperties(); + properties.setUrl("r2dbc:simple://:pool:"); + ConnectionFactoryOptions options = ConnectionFactoryBuilder + .of(properties, () -> EmbeddedDatabaseConnection.NONE).buildOptions(); + assertThat(options.hasOption(ConnectionFactoryOptions.USER)).isFalse(); + assertThat(options.hasOption(ConnectionFactoryOptions.PASSWORD)).isFalse(); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.DRIVER)).isEqualTo("simple"); + } + + @Test + void regularConnectionShouldInitializeUrlOptions() { + R2dbcProperties properties = new R2dbcProperties(); + properties.setUrl("r2dbc:simple:proto://user:password@myhost:4711/mydatabase"); + ConnectionFactoryOptions options = buildOptions(properties); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.DRIVER)).isEqualTo("simple"); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.PROTOCOL)).isEqualTo("proto"); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.USER)).isEqualTo("user"); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.PASSWORD)).isEqualTo("password"); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.HOST)).isEqualTo("myhost"); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.PORT)).isEqualTo(4711); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.DATABASE)).isEqualTo("mydatabase"); + } + + @Test + void regularConnectionShouldUseUrlOptionsOverProperties() { + R2dbcProperties properties = new R2dbcProperties(); + properties.setUrl("r2dbc:simple://user:password@myhost/mydatabase"); + properties.setUsername("another-user"); + properties.setPassword("another-password"); + properties.setName("another-database"); + ConnectionFactoryOptions options = buildOptions(properties); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.USER)).isEqualTo("user"); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.PASSWORD)).isEqualTo("password"); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.DATABASE)).isEqualTo("mydatabase"); + } + + @Test + void regularConnectionShouldUseDatabaseNameOverRandomName() { + R2dbcProperties properties = new R2dbcProperties(); + properties.setUrl("r2dbc:simple://user:password@myhost/mydatabase"); + properties.setGenerateUniqueName(true); + ConnectionFactoryOptions options = buildOptions(properties); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.DATABASE)).isEqualTo("mydatabase"); + } + + @Test + void regularConnectionWithRandomNameShouldIgnoreNameFromProperties() { + R2dbcProperties properties = new R2dbcProperties(); + properties.setUrl("r2dbc:h2://host"); + properties.setName("test-database"); + properties.setGenerateUniqueName(true); + ConnectionFactoryOptions options = buildOptions(properties); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.DATABASE)).isNotEqualTo("test-database") + .isNotEmpty(); + } + + @Test + void regularConnectionShouldSetCustomDriverProperties() { + R2dbcProperties properties = new R2dbcProperties(); + properties.setUrl("r2dbc:simple://user:password@myhost"); + properties.getProperties().put("simpleOne", "one"); + properties.getProperties().put("simpleTwo", "two"); + ConnectionFactoryOptions options = buildOptions(properties); + assertThat(options.getRequiredValue(Option.valueOf("simpleOne"))).isEqualTo("one"); + assertThat(options.getRequiredValue(Option.valueOf("simpleTwo"))).isEqualTo("two"); + } + + @Test + void regularConnectionShouldUseBuilderValuesOverProperties() { + R2dbcProperties properties = new R2dbcProperties(); + properties.setUrl("r2dbc:simple://user:password@myhost:47111/mydatabase"); + properties.setUsername("user"); + properties.setPassword("password"); + ConnectionFactoryOptions options = ConnectionFactoryBuilder + .of(properties, () -> EmbeddedDatabaseConnection.NONE).username("another-user") + .password("another-password").hostname("another-host").port(1234).database("another-database") + .buildOptions(); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.USER)).isEqualTo("another-user"); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.PASSWORD)).isEqualTo("another-password"); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.HOST)).isEqualTo("another-host"); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.PORT)).isEqualTo(1234); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.DATABASE)).isEqualTo("another-database"); + } + + @Test + void embeddedConnectionIsConfiguredAutomaticallyWithoutUrl() { + ConnectionFactoryOptions options = ConnectionFactoryBuilder + .of(new R2dbcProperties(), () -> EmbeddedDatabaseConnection.H2).buildOptions(); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.USER)).isEqualTo("sa"); + assertThat(options.hasOption(ConnectionFactoryOptions.PASSWORD)).isFalse(); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.DRIVER)).isEqualTo("h2"); + } + + @Test + void embeddedConnectionWithUsernameAndPassword() { + R2dbcProperties properties = new R2dbcProperties(); + properties.setUsername("embedded"); + properties.setPassword("secret"); + ConnectionFactoryOptions options = ConnectionFactoryBuilder.of(properties, () -> EmbeddedDatabaseConnection.H2) + .buildOptions(); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.USER)).isEqualTo("embedded"); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.PASSWORD)).isEqualTo("secret"); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.DRIVER)).isEqualTo("h2"); + } + + @Test + void embeddedConnectionUseDefaultDatabaseName() { + ConnectionFactoryOptions options = ConnectionFactoryBuilder + .of(new R2dbcProperties(), () -> EmbeddedDatabaseConnection.H2).buildOptions(); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.DATABASE)).isEqualTo("testdb"); + } + + @Test + void embeddedConnectionUseNameIfSet() { + R2dbcProperties properties = new R2dbcProperties(); + properties.setName("test-database"); + ConnectionFactoryOptions options = buildOptions(properties); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.DATABASE)).isEqualTo("test-database"); + } + + @Test + void embeddedConnectionCanGenerateUniqueDatabaseName() { + R2dbcProperties firstProperties = new R2dbcProperties(); + firstProperties.setGenerateUniqueName(true); + ConnectionFactoryOptions options11 = buildOptions(firstProperties); + ConnectionFactoryOptions options12 = buildOptions(firstProperties); + assertThat(options11.getRequiredValue(ConnectionFactoryOptions.DATABASE)) + .isEqualTo(options12.getRequiredValue(ConnectionFactoryOptions.DATABASE)); + R2dbcProperties secondProperties = new R2dbcProperties(); + firstProperties.setGenerateUniqueName(true); + ConnectionFactoryOptions options21 = buildOptions(secondProperties); + ConnectionFactoryOptions options22 = buildOptions(secondProperties); + assertThat(options21.getRequiredValue(ConnectionFactoryOptions.DATABASE)) + .isEqualTo(options22.getRequiredValue(ConnectionFactoryOptions.DATABASE)); + assertThat(options11.getRequiredValue(ConnectionFactoryOptions.DATABASE)) + .isNotEqualTo(options21.getRequiredValue(ConnectionFactoryOptions.DATABASE)); + } + + @Test + void embeddedConnectionShouldIgnoreNameIfRandomNameIsRequired() { + R2dbcProperties properties = new R2dbcProperties(); + properties.setGenerateUniqueName(true); + properties.setName("test-database"); + ConnectionFactoryOptions options = buildOptions(properties); + assertThat(options.getRequiredValue(ConnectionFactoryOptions.DATABASE)).isNotEqualTo("test-database"); + } + + private ConnectionFactoryOptions buildOptions(R2dbcProperties properties) { + return ConnectionFactoryBuilder.of(properties, () -> EmbeddedDatabaseConnection.H2).buildOptions(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcAutoConfigurationTests.java new file mode 100644 index 000000000000..f959a8e3aa07 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcAutoConfigurationTests.java @@ -0,0 +1,323 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.r2dbc; + +import java.net.URL; +import java.net.URLClassLoader; +import java.time.Duration; +import java.util.UUID; +import java.util.function.Function; + +import javax.sql.DataSource; + +import io.r2dbc.h2.H2ConnectionFactory; +import io.r2dbc.pool.ConnectionPool; +import io.r2dbc.pool.PoolMetrics; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.Option; +import io.r2dbc.spi.Wrapped; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.assertj.core.api.InstanceOfAssertFactory; +import org.assertj.core.api.ObjectAssert; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.r2dbc.SimpleConnectionFactoryProvider.SimpleTestConnectionFactory; +import org.springframework.boot.r2dbc.EmbeddedDatabaseConnection; +import org.springframework.boot.r2dbc.OptionsCapableConnectionFactory; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.r2dbc.core.DatabaseClient; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link R2dbcAutoConfiguration}. + * + * @author Mark Paluch + * @author Stephane Nicoll + */ +class R2dbcAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class)); + + @Test + void configureWithUrlCreateConnectionPoolByDefault() { + this.contextRunner.withPropertyValues("spring.r2dbc.url:r2dbc:h2:mem:///" + randomDatabaseName()) + .run((context) -> { + assertThat(context).hasSingleBean(ConnectionFactory.class).hasSingleBean(ConnectionPool.class); + assertThat(context.getBean(ConnectionPool.class)).extracting(ConnectionPool::unwrap) + .satisfies((connectionFactory) -> assertThat(connectionFactory) + .asInstanceOf(type(OptionsCapableConnectionFactory.class)) + .extracting(Wrapped::unwrap) + .isExactlyInstanceOf(H2ConnectionFactory.class)); + }); + } + + @Test + void configureWithUrlAndPoolPropertiesApplyProperties() { + this.contextRunner.withPropertyValues("spring.r2dbc.url:r2dbc:h2:mem:///" + randomDatabaseName(), + "spring.r2dbc.pool.max-size=15", "spring.r2dbc.pool.max-acquire-time=3m").run((context) -> { + assertThat(context).hasSingleBean(ConnectionFactory.class).hasSingleBean(ConnectionPool.class) + .hasSingleBean(R2dbcProperties.class); + ConnectionPool connectionPool = context.getBean(ConnectionPool.class); + PoolMetrics poolMetrics = connectionPool.getMetrics().get(); + assertThat(poolMetrics.getMaxAllocatedSize()).isEqualTo(15); + assertThat(connectionPool).hasFieldOrPropertyWithValue("maxAcquireTime", Duration.ofMinutes(3)); + }); + } + + @Test + void configureWithUrlAndDefaultDoNotOverrideDefaultTimeouts() { + this.contextRunner.withPropertyValues("spring.r2dbc.url:r2dbc:h2:mem:///" + randomDatabaseName()) + .run((context) -> { + assertThat(context).hasSingleBean(ConnectionFactory.class).hasSingleBean(ConnectionPool.class) + .hasSingleBean(R2dbcProperties.class); + ConnectionPool connectionPool = context.getBean(ConnectionPool.class); + assertThat(connectionPool).hasFieldOrPropertyWithValue("maxAcquireTime", Duration.ZERO); + }); + } + + @Test + void configureWithUrlPoolAndPoolPropertiesApplyUrlPoolOptions() { + this.contextRunner + .withPropertyValues("spring.r2dbc.url:r2dbc:pool:h2:mem:///" + randomDatabaseName() + "?maxSize=12", + "spring.r2dbc.pool.max-size=15") + .run((context) -> { + assertThat(context).hasSingleBean(ConnectionFactory.class).hasSingleBean(ConnectionPool.class); + PoolMetrics poolMetrics = context.getBean(ConnectionPool.class).getMetrics().get(); + assertThat(poolMetrics.getMaxAllocatedSize()).isEqualTo(12); + }); + } + + @Test + void configureWithPoolEnabledCreateConnectionPool() { + this.contextRunner + .withPropertyValues("spring.r2dbc.pool.enabled=true", + "spring.r2dbc.url:r2dbc:h2:mem:///" + randomDatabaseName() + + "?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE") + .run((context) -> assertThat(context).hasSingleBean(ConnectionFactory.class) + .hasSingleBean(ConnectionPool.class)); + } + + @Test + void configureWithPoolDisabledCreateGenericConnectionFactory() { + this.contextRunner.withPropertyValues("spring.r2dbc.pool.enabled=false", "spring.r2dbc.url:r2dbc:h2:mem:///" + + randomDatabaseName() + "?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE").run((context) -> { + assertThat(context).hasSingleBean(ConnectionFactory.class).doesNotHaveBean(ConnectionPool.class); + assertThat(context.getBean(ConnectionFactory.class)) + .asInstanceOf(type(OptionsCapableConnectionFactory.class)) + .extracting(Wrapped::unwrap) + .isExactlyInstanceOf(H2ConnectionFactory.class); + }); + } + + @Test + void configureWithoutR2dbcPoolCreateGenericConnectionFactory() { + this.contextRunner.with(hideConnectionPool()).withPropertyValues("spring.r2dbc.url:r2dbc:h2:mem:///" + + randomDatabaseName() + "?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE").run((context) -> { + assertThat(context).hasSingleBean(ConnectionFactory.class); + assertThat(context.getBean(ConnectionFactory.class)) + .asInstanceOf(type(OptionsCapableConnectionFactory.class)) + .extracting(Wrapped::unwrap) + .isExactlyInstanceOf(H2ConnectionFactory.class); + }); + } + + @Test + void configureWithoutR2dbcPoolAndPoolEnabledDoesNotCreateConnectionFactory() { + this.contextRunner.with(hideConnectionPool()) + .withPropertyValues("spring.r2dbc.pool.enabled=true", + "spring.r2dbc.url:r2dbc:h2:mem:///" + randomDatabaseName() + + "?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE") + .run((context) -> assertThat(context).doesNotHaveBean(ConnectionFactory.class)); + } + + @Test + void configureWithoutPoolInvokeOptionCustomizer() { + this.contextRunner + .withPropertyValues("spring.r2dbc.pool.enabled=false", "spring.r2dbc.url:r2dbc:simple://host/database") + .withUserConfiguration(CustomizerConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(ConnectionFactory.class).doesNotHaveBean(ConnectionPool.class); + ConnectionFactory connectionFactory = context.getBean(ConnectionFactory.class); + assertThat(connectionFactory).asInstanceOf(type(OptionsCapableConnectionFactory.class)) + .extracting(OptionsCapableConnectionFactory::getOptions).satisfies((options) -> assertThat( + options.getRequiredValue(Option.valueOf("customized"))).isTrue()); + }); + } + + @Test + void configureWithPoolInvokeOptionCustomizer() { + this.contextRunner.withPropertyValues("spring.r2dbc.url:r2dbc:simple://host/database") + .withUserConfiguration(CustomizerConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(ConnectionFactory.class).hasSingleBean(ConnectionPool.class); + ConnectionFactory pool = context.getBean(ConnectionFactory.class); + ConnectionFactory connectionFactory = ((ConnectionPool) pool).unwrap(); + assertThat(connectionFactory).asInstanceOf(type(OptionsCapableConnectionFactory.class)) + .extracting(OptionsCapableConnectionFactory::getOptions).satisfies((options) -> assertThat( + options.getRequiredValue(Option.valueOf("customized"))).isTrue()); + }); + } + + @Test + void configureWithInvalidUrlThrowsAppropriateException() { + this.contextRunner.withPropertyValues("spring.r2dbc.url:r2dbc:not-going-to-work") + .run((context) -> assertThat(context).getFailure().isInstanceOf(BeanCreationException.class)); + } + + @Test + void configureWithoutSpringJdbcCreateConnectionFactory() { + this.contextRunner.withPropertyValues("spring.r2dbc.pool.enabled=false", "spring.r2dbc.url:r2dbc:simple://foo") + .withClassLoader(new FilteredClassLoader("org.springframework.jdbc")).run((context) -> { + assertThat(context).hasSingleBean(ConnectionFactory.class); + assertThat(context.getBean(ConnectionFactory.class)) + .asInstanceOf(type(OptionsCapableConnectionFactory.class)) + .extracting(Wrapped::unwrap) + .isExactlyInstanceOf(SimpleTestConnectionFactory.class); + }); + } + + @Test + void configureWithoutPoolShouldApplyAdditionalProperties() { + this.contextRunner.withPropertyValues("spring.r2dbc.pool.enabled=false", "spring.r2dbc.url:r2dbc:simple://foo", + "spring.r2dbc.properties.test=value", "spring.r2dbc.properties.another=2").run((context) -> { + ConnectionFactory connectionFactory = context.getBean(ConnectionFactory.class); + assertThat(connectionFactory).asInstanceOf(type(OptionsCapableConnectionFactory.class)) + .extracting(OptionsCapableConnectionFactory::getOptions).satisfies((options) -> { + assertThat(options.getRequiredValue(Option.valueOf("test"))).isEqualTo("value"); + assertThat(options.getRequiredValue(Option.valueOf("another"))).isEqualTo("2"); + }); + }); + } + + @Test + void configureWithPoolShouldApplyAdditionalProperties() { + this.contextRunner.withPropertyValues("spring.r2dbc.url:r2dbc:simple://foo", + "spring.r2dbc.properties.test=value", "spring.r2dbc.properties.another=2").run((context) -> { + assertThat(context).hasSingleBean(ConnectionFactory.class).hasSingleBean(ConnectionPool.class); + ConnectionFactory connectionFactory = context.getBean(ConnectionPool.class).unwrap(); + assertThat(connectionFactory).asInstanceOf(type(OptionsCapableConnectionFactory.class)) + .extracting(OptionsCapableConnectionFactory::getOptions).satisfies((options) -> { + assertThat(options.getRequiredValue(Option.valueOf("test"))).isEqualTo("value"); + assertThat(options.getRequiredValue(Option.valueOf("another"))).isEqualTo("2"); + }); + }); + } + + @Test + void configureWithoutUrlShouldCreateEmbeddedConnectionPoolByDefault() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ConnectionFactory.class) + .hasSingleBean(ConnectionPool.class)); + } + + @Test + void configureWithoutUrlAndPollPoolDisabledCreateGenericConnectionFactory() { + this.contextRunner.withPropertyValues("spring.r2dbc.pool.enabled=false").run((context) -> { + assertThat(context).hasSingleBean(ConnectionFactory.class).doesNotHaveBean(ConnectionPool.class); + assertThat(context.getBean(ConnectionFactory.class)) + .asInstanceOf(type(OptionsCapableConnectionFactory.class)) + .extracting(Wrapped::unwrap).isExactlyInstanceOf(H2ConnectionFactory.class); + }); + } + + @Test + void configureWithoutUrlAndSprigJdbcCreateEmbeddedConnectionFactory() { + this.contextRunner.withClassLoader(new FilteredClassLoader("org.springframework.jdbc")) + .run((context) -> assertThat(context).hasSingleBean(ConnectionFactory.class) + .hasSingleBean(ConnectionPool.class)); + } + + @Test + void configureWithoutUrlAndEmbeddedCandidateFails() { + this.contextRunner.withClassLoader(new DisableEmbeddedDatabaseClassLoader()).run((context) -> { + assertThat(context).hasFailed(); + assertThat(context).getFailure().isInstanceOf(BeanCreationException.class) + .hasMessageContaining("Failed to determine a suitable R2DBC Connection URL"); + }); + } + + @Test + void configureWithDataSourceAutoConfigurationDoesNotCreateDataSource() { + this.contextRunner.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) + .run((context) -> assertThat(context).hasSingleBean(ConnectionFactory.class) + .doesNotHaveBean(DataSource.class)); + } + + @Test + void databaseClientIsConfigured() { + this.contextRunner.withPropertyValues("spring.r2dbc.url:r2dbc:h2:mem:///" + randomDatabaseName()) + .run((context) -> { + assertThat(context).hasSingleBean(ConnectionFactory.class).hasSingleBean(DatabaseClient.class); + assertThat(context.getBean(DatabaseClient.class).getConnectionFactory()) + .isSameAs(context.getBean(ConnectionFactory.class)); + }); + } + + @Test + void databaseClientBacksOffIfSpringR2dbcIsNotAvailable() { + this.contextRunner.withClassLoader(new FilteredClassLoader("org.springframework.r2dbc")) + .withPropertyValues("spring.r2dbc.url:r2dbc:h2:mem:///" + randomDatabaseName()) + .run((context) -> assertThat(context).hasSingleBean(ConnectionFactory.class) + .doesNotHaveBean(DatabaseClient.class)); + } + + private InstanceOfAssertFactory> type(Class type) { + return InstanceOfAssertFactories.type(type); + } + + private String randomDatabaseName() { + return "testdb-" + UUID.randomUUID(); + } + + private Function hideConnectionPool() { + return (runner) -> runner.withClassLoader(new FilteredClassLoader("io.r2dbc.pool")); + } + + private static class DisableEmbeddedDatabaseClassLoader extends URLClassLoader { + + DisableEmbeddedDatabaseClassLoader() { + super(new URL[0], DisableEmbeddedDatabaseClassLoader.class.getClassLoader()); + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + for (EmbeddedDatabaseConnection candidate : EmbeddedDatabaseConnection.values()) { + if (name.equals(candidate.getDriverClassName())) { + throw new ClassNotFoundException(); + } + } + return super.loadClass(name, resolve); + } + + } + + @Configuration(proxyBeanMethods = false) + private static class CustomizerConfiguration { + + @Bean + ConnectionFactoryOptionsBuilderCustomizer customizer() { + return (builder) -> builder.option(Option.valueOf("customized"), true); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcTransactionManagerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcTransactionManagerAutoConfigurationTests.java new file mode 100644 index 000000000000..7b1cdf0dd678 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/R2dbcTransactionManagerAutoConfigurationTests.java @@ -0,0 +1,116 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.r2dbc; + +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.ConnectionFactory; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.ReactiveTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.reactive.TransactionSynchronizationManager; +import org.springframework.transaction.reactive.TransactionalOperator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link R2dbcTransactionManagerAutoConfiguration}. + * + * @author Mark Paluch + * @author Oliver Drotbohm + */ +class R2dbcTransactionManagerAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration( + AutoConfigurations.of(R2dbcTransactionManagerAutoConfiguration.class, TransactionAutoConfiguration.class)); + + @Test + void noTransactionManager() { + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(ReactiveTransactionManager.class)); + } + + @Test + void singleTransactionManager() { + this.contextRunner.withUserConfiguration(SingleConnectionFactoryConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(TransactionalOperator.class) + .hasSingleBean(ReactiveTransactionManager.class)); + } + + @Test + void transactionManagerEnabled() { + this.contextRunner.withUserConfiguration(SingleConnectionFactoryConfiguration.class, BaseConfiguration.class) + .run((context) -> { + TransactionalService bean = context.getBean(TransactionalService.class); + bean.isTransactionActive().as(StepVerifier::create).expectNext(true).verifyComplete(); + }); + } + + @Configuration(proxyBeanMethods = false) + static class SingleConnectionFactoryConfiguration { + + @Bean + ConnectionFactory connectionFactory() { + ConnectionFactory connectionFactory = mock(ConnectionFactory.class); + Connection connection = mock(Connection.class); + given(connectionFactory.create()).willAnswer((invocation) -> Mono.just(connection)); + given(connection.beginTransaction()).willReturn(Mono.empty()); + given(connection.commitTransaction()).willReturn(Mono.empty()); + given(connection.close()).willReturn(Mono.empty()); + return connectionFactory; + } + + } + + @Configuration(proxyBeanMethods = false) + @EnableTransactionManagement + static class BaseConfiguration { + + @Bean + TransactionalService transactionalService() { + return new TransactionalServiceImpl(); + } + + } + + interface TransactionalService { + + @Transactional + Mono isTransactionActive(); + + } + + static class TransactionalServiceImpl implements TransactionalService { + + @Override + public Mono isTransactionActive() { + return TransactionSynchronizationManager.forCurrentTransaction() + .map(TransactionSynchronizationManager::isActualTransactionActive); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/SimpleBindMarkerFactoryProvider.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/SimpleBindMarkerFactoryProvider.java new file mode 100644 index 000000000000..84287a62a05d --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/SimpleBindMarkerFactoryProvider.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.r2dbc; + +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.Wrapped; + +import org.springframework.boot.autoconfigure.r2dbc.SimpleConnectionFactoryProvider.SimpleTestConnectionFactory; +import org.springframework.r2dbc.core.binding.BindMarkersFactory; +import org.springframework.r2dbc.core.binding.BindMarkersFactoryResolver.BindMarkerFactoryProvider; + +/** + * Simple {@link BindMarkerFactoryProvider} for {@link SimpleConnectionFactoryProvider}. + * + * @author Stephane Nicoll + */ +public class SimpleBindMarkerFactoryProvider implements BindMarkerFactoryProvider { + + @Override + public BindMarkersFactory getBindMarkers(ConnectionFactory connectionFactory) { + if (unwrapIfNecessary(connectionFactory) instanceof SimpleTestConnectionFactory) { + return BindMarkersFactory.anonymous("?"); + } + return null; + } + + @SuppressWarnings("unchecked") + private ConnectionFactory unwrapIfNecessary(ConnectionFactory connectionFactory) { + if (connectionFactory instanceof Wrapped) { + return unwrapIfNecessary(((Wrapped) connectionFactory).unwrap()); + } + return connectionFactory; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/SimpleConnectionFactoryProvider.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/SimpleConnectionFactoryProvider.java new file mode 100644 index 000000000000..40222ce1d4d6 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/r2dbc/SimpleConnectionFactoryProvider.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.r2dbc; + +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.ConnectionFactoryMetadata; +import io.r2dbc.spi.ConnectionFactoryOptions; +import io.r2dbc.spi.ConnectionFactoryProvider; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; + +/** + * Simple driver for testing. + * + * @author Mark Paluch + * @author Andy Wilkinson + */ +public class SimpleConnectionFactoryProvider implements ConnectionFactoryProvider { + + @Override + public ConnectionFactory create(ConnectionFactoryOptions connectionFactoryOptions) { + return new SimpleTestConnectionFactory(); + } + + @Override + public boolean supports(ConnectionFactoryOptions connectionFactoryOptions) { + return connectionFactoryOptions.getRequiredValue(ConnectionFactoryOptions.DRIVER).equals("simple"); + } + + @Override + public String getDriver() { + return "simple"; + } + + public static class SimpleTestConnectionFactory implements ConnectionFactory { + + @Override + public Publisher create() { + return Mono.error(new UnsupportedOperationException()); + } + + @Override + public ConnectionFactoryMetadata getMetadata() { + return SimpleConnectionFactoryProvider.class::getName; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/rsocket/RSocketMessagingAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/rsocket/RSocketMessagingAutoConfigurationTests.java index 41b7bc24a0aa..aee5e0f7058e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/rsocket/RSocketMessagingAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/rsocket/RSocketMessagingAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import org.springframework.core.codec.StringDecoder; import org.springframework.messaging.rsocket.RSocketStrategies; import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler; +import org.springframework.util.MimeType; import static org.assertj.core.api.Assertions.assertThat; @@ -33,6 +34,7 @@ * Tests for {@link RSocketMessagingAutoConfiguration}. * * @author Brian Clozel + * @author Madhura Bhave */ class RSocketMessagingAutoConfigurationTests { @@ -61,6 +63,14 @@ void shouldUseCustomSocketAcceptor() { .getBeanNames(RSocketMessageHandler.class).containsOnly("customMessageHandler")); } + @Test + void shouldApplyMessageHandlerCustomizers() { + this.contextRunner.withUserConfiguration(CustomizerConfiguration.class).run((context) -> { + RSocketMessageHandler handler = context.getBean(RSocketMessageHandler.class); + assertThat(handler.getDefaultDataMimeType()).isEqualTo(MimeType.valueOf("application/json")); + }); + } + @Configuration(proxyBeanMethods = false) static class BaseConfiguration { @@ -86,4 +96,14 @@ RSocketMessageHandler customMessageHandler() { } + @Configuration(proxyBeanMethods = false) + static class CustomizerConfiguration { + + @Bean + RSocketMessageHandlerCustomizer customizer() { + return (messageHandler) -> messageHandler.setDefaultDataMimeType(MimeType.valueOf("application/json")); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfigurationTests.java index f03c09c9fa96..91bbe8a37d39 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,8 +21,9 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer; import org.springframework.boot.rsocket.context.RSocketServerBootstrap; +import org.springframework.boot.rsocket.server.RSocketServerCustomizer; import org.springframework.boot.rsocket.server.RSocketServerFactory; -import org.springframework.boot.rsocket.server.ServerRSocketFactoryProcessor; +import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.boot.web.server.WebServerFactoryCustomizer; @@ -30,8 +31,10 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.codec.CharSequenceEncoder; import org.springframework.core.codec.StringDecoder; +import org.springframework.http.client.reactive.ReactorResourceFactory; import org.springframework.messaging.rsocket.RSocketStrategies; import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler; +import org.springframework.util.unit.DataSize; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -78,8 +81,7 @@ void shouldCreateDefaultBeansForReactiveWebApp() { void shouldCreateDefaultBeansForRSocketServerWhenPortIsSet() { reactiveWebContextRunner().withPropertyValues("spring.rsocket.server.port=0") .run((context) -> assertThat(context).hasSingleBean(RSocketServerFactory.class) - .hasSingleBean(RSocketServerBootstrap.class) - .hasSingleBean(ServerRSocketFactoryProcessor.class)); + .hasSingleBean(RSocketServerBootstrap.class).hasSingleBean(RSocketServerCustomizer.class)); } @Test @@ -87,12 +89,45 @@ void shouldSetLocalServerPortWhenRSocketServerPortIsSet() { reactiveWebContextRunner().withPropertyValues("spring.rsocket.server.port=0") .withInitializer(new RSocketPortInfoApplicationContextInitializer()).run((context) -> { assertThat(context).hasSingleBean(RSocketServerFactory.class) - .hasSingleBean(RSocketServerBootstrap.class) - .hasSingleBean(ServerRSocketFactoryProcessor.class); + .hasSingleBean(RSocketServerBootstrap.class).hasSingleBean(RSocketServerCustomizer.class); assertThat(context.getEnvironment().getProperty("local.rsocket.server.port")).isNotNull(); }); } + @Test + void shouldSetFragmentWhenRSocketServerFragmentSizeIsSet() { + reactiveWebContextRunner() + .withPropertyValues("spring.rsocket.server.port=0", "spring.rsocket.server.fragment-size=12KB") + .run((context) -> { + assertThat(context).hasSingleBean(RSocketServerFactory.class); + RSocketServerFactory factory = context.getBean(RSocketServerFactory.class); + assertThat(factory).hasFieldOrPropertyWithValue("fragmentSize", DataSize.ofKilobytes(12)); + }); + } + + @Test + void shouldFailToSetFragmentWhenRSocketServerFragmentSizeIsBelow64() { + reactiveWebContextRunner() + .withPropertyValues("spring.rsocket.server.port=0", "spring.rsocket.server.fragment-size=60B") + .run((context) -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()) + .hasMessageContaining("The smallest allowed mtu size is 64 bytes, provided: 60"); + }); + } + + @Test + void shouldUseSslWhenRocketServerSslIsConfigured() { + reactiveWebContextRunner() + .withPropertyValues("spring.rsocket.server.ssl.keyStore=classpath:rsocket/test.jks", + "spring.rsocket.server.ssl.keyPassword=password", "spring.rsocket.server.port=0") + .run((context) -> assertThat(context).hasSingleBean(RSocketServerFactory.class) + .hasSingleBean(RSocketServerBootstrap.class).hasSingleBean(RSocketServerCustomizer.class) + .getBean(RSocketServerFactory.class) + .hasFieldOrPropertyWithValue("ssl.keyStore", "classpath:rsocket/test.jks") + .hasFieldOrPropertyWithValue("ssl.keyPassword", "password")); + } + @Test void shouldUseCustomServerBootstrap() { contextRunner().withUserConfiguration(CustomServerBootstrapConfig.class).run((context) -> assertThat(context) @@ -108,6 +143,13 @@ void shouldUseCustomNettyRouteProvider() { .containsExactly("customNettyRouteProvider")); } + @Test + void whenSpringWebIsNotPresentThenEmbeddedServerConfigurationBacksOff() { + contextRunner().withClassLoader(new FilteredClassLoader(ReactorResourceFactory.class)) + .withPropertyValues("spring.rsocket.server.port=0") + .run((context) -> assertThat(context).doesNotHaveBean(RSocketServerFactory.class)); + } + private ApplicationContextRunner contextRunner() { return new ApplicationContextRunner().withUserConfiguration(BaseConfiguration.class) .withConfiguration(AutoConfigurations.of(RSocketServerAutoConfiguration.class)); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/rsocket/RSocketWebSocketNettyRouteProviderTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/rsocket/RSocketWebSocketNettyRouteProviderTests.java index b6ea6c9bb6b9..e5272b4ef328 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/rsocket/RSocketWebSocketNettyRouteProviderTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/rsocket/RSocketWebSocketNettyRouteProviderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,6 @@ import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration; import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; import org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration; -import org.springframework.boot.rsocket.server.ServerRSocketFactoryProcessor; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory; import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext; @@ -55,7 +54,7 @@ class RSocketWebSocketNettyRouteProviderTests { @Test - void webEndpointsShouldWork() throws Exception { + void webEndpointsShouldWork() { new ReactiveWebApplicationContextRunner(AnnotationConfigReactiveWebServerApplicationContext::new) .withConfiguration( AutoConfigurations.of(HttpHandlerAutoConfiguration.class, WebFluxAutoConfiguration.class, @@ -76,26 +75,24 @@ void webEndpointsShouldWork() throws Exception { WebTestClient client = createWebTestClient(serverContext.getWebServer()); client.get().uri("/protocol").exchange().expectStatus().isOk().expectBody().jsonPath("name", "http"); - assertThat(WebConfiguration.processorCallCount).isEqualTo(1); }); } private WebTestClient createWebTestClient(WebServer server) { - return WebTestClient.bindToServer().baseUrl("http://localhost:" + server.getPort()).build(); + return WebTestClient.bindToServer().baseUrl("http://localhost:" + server.getPort()) + .responseTimeout(Duration.ofMinutes(5)).build(); } private RSocketRequester createRSocketRequester(ApplicationContext context, WebServer server) { int port = server.getPort(); RSocketRequester.Builder builder = context.getBean(RSocketRequester.Builder.class); return builder.dataMimeType(MediaType.APPLICATION_CBOR) - .connectWebSocket(URI.create("ws://localhost:" + port + "/rsocket")).block(); + .websocket(URI.create("ws://localhost:" + port + "/rsocket")); } @Configuration(proxyBeanMethods = false) static class WebConfiguration { - static int processorCallCount = 0; - @Bean WebController webController() { return new WebController(); @@ -108,14 +105,6 @@ NettyReactiveWebServerFactory customServerFactory(RSocketWebSocketNettyRouteProv return serverFactory; } - @Bean - ServerRSocketFactoryProcessor myRSocketFactoryProcessor() { - return (server) -> { - processorCallCount++; - return server; - }; - } - } @Controller diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapterTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapterTests.java index cda74fae7259..ec888e0c6eab 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapterTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -86,10 +86,10 @@ void getClientRegistrationsWhenUsingDefinedProviderShouldAdapt() { assertThat(adapted.getClientId()).isEqualTo("clientId"); assertThat(adapted.getClientSecret()).isEqualTo("clientSecret"); assertThat(adapted.getClientAuthenticationMethod()) - .isEqualTo(org.springframework.security.oauth2.core.ClientAuthenticationMethod.POST); + .isEqualTo(org.springframework.security.oauth2.core.ClientAuthenticationMethod.CLIENT_SECRET_POST); assertThat(adapted.getAuthorizationGrantType()) .isEqualTo(org.springframework.security.oauth2.core.AuthorizationGrantType.AUTHORIZATION_CODE); - assertThat(adapted.getRedirectUriTemplate()).isEqualTo("https://example.com/redirect"); + assertThat(adapted.getRedirectUri()).isEqualTo("https://example.com/redirect"); assertThat(adapted.getScopes()).containsExactly("user"); assertThat(adapted.getClientName()).isEqualTo("clientName"); } @@ -116,10 +116,10 @@ void getClientRegistrationsWhenUsingCommonProviderShouldAdapt() { assertThat(adapted.getClientId()).isEqualTo("clientId"); assertThat(adapted.getClientSecret()).isEqualTo("clientSecret"); assertThat(adapted.getClientAuthenticationMethod()) - .isEqualTo(org.springframework.security.oauth2.core.ClientAuthenticationMethod.BASIC); + .isEqualTo(org.springframework.security.oauth2.core.ClientAuthenticationMethod.CLIENT_SECRET_BASIC); assertThat(adapted.getAuthorizationGrantType()) .isEqualTo(org.springframework.security.oauth2.core.AuthorizationGrantType.AUTHORIZATION_CODE); - assertThat(adapted.getRedirectUriTemplate()).isEqualTo("{baseUrl}/{action}/oauth2/code/{registrationId}"); + assertThat(adapted.getRedirectUri()).isEqualTo("{baseUrl}/{action}/oauth2/code/{registrationId}"); assertThat(adapted.getScopes()).containsExactly("openid", "profile", "email"); assertThat(adapted.getClientName()).isEqualTo("Google"); } @@ -146,10 +146,10 @@ void getClientRegistrationsWhenUsingCommonProviderWithOverrideShouldAdapt() { assertThat(adapted.getClientId()).isEqualTo("clientId"); assertThat(adapted.getClientSecret()).isEqualTo("clientSecret"); assertThat(adapted.getClientAuthenticationMethod()) - .isEqualTo(org.springframework.security.oauth2.core.ClientAuthenticationMethod.POST); + .isEqualTo(org.springframework.security.oauth2.core.ClientAuthenticationMethod.CLIENT_SECRET_POST); assertThat(adapted.getAuthorizationGrantType()) .isEqualTo(org.springframework.security.oauth2.core.AuthorizationGrantType.AUTHORIZATION_CODE); - assertThat(adapted.getRedirectUriTemplate()).isEqualTo("https://example.com/redirect"); + assertThat(adapted.getRedirectUri()).isEqualTo("https://example.com/redirect"); assertThat(adapted.getScopes()).containsExactly("user"); assertThat(adapted.getClientName()).isEqualTo("clientName"); } @@ -187,10 +187,10 @@ void getClientRegistrationsWhenProviderNotSpecifiedShouldUseRegistrationId() { assertThat(adapted.getClientId()).isEqualTo("clientId"); assertThat(adapted.getClientSecret()).isEqualTo("clientSecret"); assertThat(adapted.getClientAuthenticationMethod()) - .isEqualTo(org.springframework.security.oauth2.core.ClientAuthenticationMethod.BASIC); + .isEqualTo(org.springframework.security.oauth2.core.ClientAuthenticationMethod.CLIENT_SECRET_BASIC); assertThat(adapted.getAuthorizationGrantType()) .isEqualTo(org.springframework.security.oauth2.core.AuthorizationGrantType.AUTHORIZATION_CODE); - assertThat(adapted.getRedirectUriTemplate()).isEqualTo("{baseUrl}/{action}/oauth2/code/{registrationId}"); + assertThat(adapted.getRedirectUri()).isEqualTo("{baseUrl}/{action}/oauth2/code/{registrationId}"); assertThat(adapted.getScopes()).containsExactly("openid", "profile", "email"); assertThat(adapted.getClientName()).isEqualTo("Google"); } @@ -254,12 +254,12 @@ void oidcProviderConfigurationWithCustomConfigurationOverridesProviderDefaults() .getClientRegistrations(properties); ClientRegistration adapted = registrations.get("okta"); ProviderDetails providerDetails = adapted.getProviderDetails(); - assertThat(adapted.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.POST); + assertThat(adapted.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_POST); assertThat(adapted.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); assertThat(adapted.getRegistrationId()).isEqualTo("okta"); assertThat(adapted.getClientName()).isEqualTo(issuer); assertThat(adapted.getScopes()).containsOnly("user"); - assertThat(adapted.getRedirectUriTemplate()).isEqualTo("https://example.com/redirect"); + assertThat(adapted.getRedirectUri()).isEqualTo("https://example.com/redirect"); assertThat(providerDetails.getAuthorizationUri()).isEqualTo("https://example.com/auth"); assertThat(providerDetails.getTokenUri()).isEqualTo("https://example.com/token"); assertThat(providerDetails.getJwkSetUri()).isEqualTo("https://example.com/jwk"); @@ -283,7 +283,7 @@ private OAuth2ClientProperties.Registration createRegistration(String provider) registration.setProvider(provider); registration.setClientId("clientId"); registration.setClientSecret("clientSecret"); - registration.setClientAuthenticationMethod("post"); + registration.setClientAuthenticationMethod("client_secret_post"); registration.setRedirectUri("https://example.com/redirect"); registration.setScope(Collections.singleton("user")); registration.setAuthorizationGrantType("authorization_code"); @@ -305,11 +305,11 @@ private void testIssuerConfiguration(OAuth2ClientProperties.Registration registr .getClientRegistrations(properties); ClientRegistration adapted = registrations.get("okta"); ProviderDetails providerDetails = adapted.getProviderDetails(); - assertThat(adapted.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC); + assertThat(adapted.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC); assertThat(adapted.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); assertThat(adapted.getRegistrationId()).isEqualTo("okta"); assertThat(adapted.getClientName()).isEqualTo(issuer); - assertThat(adapted.getScopes()).containsOnly("openid"); + assertThat(adapted.getScopes()).isNull(); assertThat(providerDetails.getAuthorizationUri()).isEqualTo("https://example.com/o/oauth2/v2/auth"); assertThat(providerDetails.getTokenUri()).isEqualTo("https://example.com/oauth2/v4/token"); assertThat(providerDetails.getJwkSetUri()).isEqualTo("https://example.com/oauth2/v3/certs"); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientAutoConfigurationTests.java index 8898e3c97bb7..684d46f06cc5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.autoconfigure.security.oauth2.client.reactive; import java.time.Duration; @@ -22,7 +23,6 @@ import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; -import org.springframework.beans.BeansException; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration; import org.springframework.boot.test.context.FilteredClassLoader; @@ -62,8 +62,9 @@ */ class ReactiveOAuth2ClientAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration(AutoConfigurations - .of(ReactiveOAuth2ClientAutoConfiguration.class, ReactiveSecurityAutoConfiguration.class)); + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReactiveOAuth2ClientAutoConfiguration.class, + ReactiveSecurityAutoConfiguration.class)); private static final String REGISTRATION_PREFIX = "spring.security.oauth2.client.registration"; @@ -205,9 +206,9 @@ private ClientRegistration getClientRegistration(String id, String userInfoUri) ClientRegistration.Builder builder = ClientRegistration.withRegistrationId(id); builder.clientName("foo").clientId("foo") .clientAuthenticationMethod( - org.springframework.security.oauth2.core.ClientAuthenticationMethod.BASIC) + org.springframework.security.oauth2.core.ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE).scope("read") - .clientSecret("secret").redirectUriTemplate("https://redirect-uri.com") + .clientSecret("secret").redirectUri("https://redirect-uri.com") .authorizationUri("https://authorization-uri.com").tokenUri("https://token-uri.com") .userInfoUri(userInfoUri).userNameAttributeName("login"); return builder.build(); @@ -251,7 +252,7 @@ ServerHttpSecurity http() { static class TestServerHttpSecurity extends ServerHttpSecurity implements ApplicationContextAware { @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + public void setApplicationContext(ApplicationContext applicationContext) { super.setApplicationContext(applicationContext); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/servlet/OAuth2WebSecurityConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/servlet/OAuth2WebSecurityConfigurationTests.java index 3ebb0ebc94df..338c828707e9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/servlet/OAuth2WebSecurityConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/servlet/OAuth2WebSecurityConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; @@ -31,6 +32,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.security.config.BeanIds; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService; @@ -115,7 +117,7 @@ void configurationRegistersAuthorizedClientRepositoryBean() { } @Test - void securityConfigurerBacksOffWhenOtherWebSecurityAdapterPresent() { + void securityFilterChainConfigBacksOffWhenOtherWebSecurityAdapterPresent() { this.contextRunner .withUserConfiguration(TestWebSecurityConfigurerConfig.class, OAuth2WebSecurityConfiguration.class) .run((context) -> { @@ -125,6 +127,28 @@ void securityConfigurerBacksOffWhenOtherWebSecurityAdapterPresent() { }); } + @Test + void securityFilterChainConfigBacksOffWhenOtherSecurityFilterChainBeanPresent() { + this.contextRunner + .withUserConfiguration(TestSecurityFilterChainConfig.class, OAuth2WebSecurityConfiguration.class) + .run((context) -> { + assertThat(getFilters(context, OAuth2LoginAuthenticationFilter.class)).isEmpty(); + assertThat(getFilters(context, OAuth2AuthorizationCodeGrantFilter.class)).isEmpty(); + assertThat(context).getBean(OAuth2AuthorizedClientService.class).isNotNull(); + }); + } + + @Test + void securityFilterChainConfigConditionalOnSecurityFilterChainClass() { + this.contextRunner + .withUserConfiguration(ClientRegistrationRepositoryConfiguration.class, + OAuth2WebSecurityConfiguration.class) + .withClassLoader(new FilteredClassLoader(SecurityFilterChain.class)).run((context) -> { + assertThat(getFilters(context, OAuth2LoginAuthenticationFilter.class)).isEmpty(); + assertThat(getFilters(context, OAuth2AuthorizationCodeGrantFilter.class)).isEmpty(); + }); + } + @Test void authorizedClientServiceBeanIsConditionalOnMissingBean() { this.contextRunner.withUserConfiguration(OAuth2AuthorizedClientServiceConfiguration.class, @@ -155,7 +179,7 @@ private boolean isEqual(ClientRegistration reg1, ClientRegistration reg2) { result = result && ObjectUtils.nullSafeEquals(reg1.getClientName(), reg2.getClientName()); result = result && ObjectUtils.nullSafeEquals(reg1.getClientSecret(), reg2.getClientSecret()); result = result && ObjectUtils.nullSafeEquals(reg1.getScopes(), reg2.getScopes()); - result = result && ObjectUtils.nullSafeEquals(reg1.getRedirectUriTemplate(), reg2.getRedirectUriTemplate()); + result = result && ObjectUtils.nullSafeEquals(reg1.getRedirectUri(), reg2.getRedirectUri()); result = result && ObjectUtils.nullSafeEquals(reg1.getRegistrationId(), reg2.getRegistrationId()); result = result && ObjectUtils.nullSafeEquals(reg1.getAuthorizationGrantType(), reg2.getAuthorizationGrantType()); @@ -195,9 +219,9 @@ private ClientRegistration getClientRegistration(String id, String userInfoUri) ClientRegistration.Builder builder = ClientRegistration.withRegistrationId(id); builder.clientName("foo").clientId("foo") .clientAuthenticationMethod( - org.springframework.security.oauth2.core.ClientAuthenticationMethod.BASIC) + org.springframework.security.oauth2.core.ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE).scope("read") - .clientSecret("secret").redirectUriTemplate("https://redirect-uri.com") + .clientSecret("secret").redirectUri("https://redirect-uri.com") .authorizationUri("https://authorization-uri.com").tokenUri("https://token-uri.com") .userInfoUri(userInfoUri).userNameAttributeName("login"); return builder.build(); @@ -211,6 +235,19 @@ static class TestWebSecurityConfigurerConfig extends WebSecurityConfigurerAdapte } + @Configuration(proxyBeanMethods = false) + @Import(ClientRegistrationRepositoryConfiguration.class) + static class TestSecurityFilterChainConfig { + + @Bean + SecurityFilterChain testSecurityFilterChain(HttpSecurity http) throws Exception { + return http.antMatcher("/**").authorizeRequests((authorize) -> authorize.anyRequest().authenticated()) + .build(); + + } + + } + @Configuration(proxyBeanMethods = false) @Import(ClientRegistrationRepositoryConfiguration.class) static class OAuth2AuthorizedClientServiceConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java index 6a3cacf0aa9c..4fc7b29b8214 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.autoconfigure.security.oauth2.resource.reactive; import java.io.IOException; @@ -20,10 +21,12 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Set; import java.util.stream.Stream; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.nimbusds.jose.JWSAlgorithm; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import org.junit.jupiter.api.AfterEach; @@ -68,15 +71,22 @@ * @author Madhura Bhave * @author Artsiom Yudovin * @author HaiTao Zhang + * @author Anastasiia Losieva */ class ReactiveOAuth2ResourceServerAutoConfigurationTests { - private ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() + private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(ReactiveOAuth2ResourceServerAutoConfiguration.class)) .withUserConfiguration(TestConfig.class); private MockWebServer server; + private static final String JWK_SET = "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"use\":\"sig\"," + + "\"kid\":\"one\",\"n\":\"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGm" + + "uLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtd" + + "F4rIe7u-977QwHuh7yRPBQ3sII-cVoOUMgaXB9SHcGF2iZCtPzL_IffDUcfhLQteGebhW8A6eUHgpD5A1PQ-JCw_G7UOzZAj" + + "jDjtNM2eqm8j-Ms_gqnm4MiCZ4E-9pDN77CAAPVN7kuX6ejs9KBXpk01z48i9fORYk9u7rAkh1HuQw\"}]}"; + @AfterEach void cleanup() throws Exception { if (this.server != null) { @@ -94,6 +104,30 @@ void autoConfigurationShouldConfigureResourceServer() { }); } + @SuppressWarnings("unchecked") + @Test + void autoConfigurationUsingJwkSetUriShouldConfigureResourceServerUsingJwsAlgorithm() { + this.contextRunner + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com", + "spring.security.oauth2.resourceserver.jwt.jws-algorithm=RS512") + .run((context) -> { + NimbusReactiveJwtDecoder nimbusReactiveJwtDecoder = context.getBean(NimbusReactiveJwtDecoder.class); + assertThat(nimbusReactiveJwtDecoder).extracting("jwtProcessor.arg$2.arg$1.jwsAlgs") + .matches((algorithms) -> ((Set) algorithms).contains(JWSAlgorithm.RS512)); + }); + } + + @Test + void autoConfigurationUsingPublicKeyValueShouldConfigureResourceServerUsingJwsAlgorithm() { + this.contextRunner.withPropertyValues( + "spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location", + "spring.security.oauth2.resourceserver.jwt.jws-algorithm=RS384").run((context) -> { + NimbusReactiveJwtDecoder nimbusReactiveJwtDecoder = context.getBean(NimbusReactiveJwtDecoder.class); + assertThat(nimbusReactiveJwtDecoder).extracting("jwtProcessor.arg$1.jwsKeySelector.expectedJWSAlg") + .isEqualTo(JWSAlgorithm.RS384); + }); + } + @Test void autoConfigurationShouldConfigureResourceServerUsingOidcIssuerUri() throws IOException { this.server = new MockWebServer(); @@ -108,7 +142,8 @@ void autoConfigurationShouldConfigureResourceServerUsingOidcIssuerUri() throws I assertFilterConfiguredWithJwtAuthenticationManager(context); assertThat(context.containsBean("jwtDecoderByIssuerUri")).isTrue(); }); - assertThat(this.server.getRequestCount()).isEqualTo(1); + // The last request is to the JWK Set endpoint to look up the algorithm + assertThat(this.server.getRequestCount()).isEqualTo(2); } @Test @@ -124,7 +159,8 @@ void autoConfigurationShouldConfigureResourceServerUsingOidcRfc8414IssuerUri() t assertFilterConfiguredWithJwtAuthenticationManager(context); assertThat(context.containsBean("jwtDecoderByIssuerUri")).isTrue(); }); - assertThat(this.server.getRequestCount()).isEqualTo(2); + // The last request is to the JWK Set endpoint to look up the algorithm + assertThat(this.server.getRequestCount()).isEqualTo(3); } @Test @@ -140,7 +176,8 @@ void autoConfigurationShouldConfigureResourceServerUsingOAuthIssuerUri() throws assertFilterConfiguredWithJwtAuthenticationManager(context); assertThat(context.containsBean("jwtDecoderByIssuerUri")).isTrue(); }); - assertThat(this.server.getRequestCount()).isEqualTo(3); + // The last request is to the JWK Set endpoint to look up the algorithm + assertThat(this.server.getRequestCount()).isEqualTo(4); } @Test @@ -260,6 +297,20 @@ void autoConfigurationWhenIntrospectionUriAvailableShouldConfigureIntrospectionC }); } + @Test + void autoConfigurationWhenJwkSetUriAndIntrospectionUriAvailable() { + this.contextRunner + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com", + "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com", + "spring.security.oauth2.resourceserver.opaquetoken.client-id=my-client-id", + "spring.security.oauth2.resourceserver.opaquetoken.client-secret=my-client-secret") + .run((context) -> { + assertThat(context).hasSingleBean(ReactiveOpaqueTokenIntrospector.class); + assertThat(context).hasSingleBean(ReactiveJwtDecoder.class); + assertFilterConfiguredWithJwtAuthenticationManager(context); + }); + } + @Test void opaqueTokenIntrospectorIsConditionalOnMissingBean() { this.contextRunner @@ -292,36 +343,6 @@ void autoConfigurationWhenIntrospectionUriAvailableShouldBeConditionalOnClass() .run((context) -> assertThat(context).doesNotHaveBean(ReactiveOpaqueTokenIntrospector.class)); } - @Test - void autoConfigurationWhenBothJwkSetUriAndTokenIntrospectionUriSetShouldFail() { - this.contextRunner - .withPropertyValues( - "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com", - "spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com") - .run((context) -> assertThat(context).hasFailed().getFailure().hasMessageContaining( - "Only one of jwt.jwk-set-uri and opaquetoken.introspection-uri should be configured.")); - } - - @Test - void autoConfigurationWhenBothJwtIssuerUriAndTokenIntrospectionUriSetShouldFail() { - this.contextRunner - .withPropertyValues( - "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com", - "spring.security.oauth2.resourceserver.jwt.issuer-uri=https://jwk-oidc-issuer-location.com") - .run((context) -> assertThat(context).hasFailed().getFailure().hasMessageContaining( - "Only one of jwt.issuer-uri and opaquetoken.introspection-uri should be configured.")); - } - - @Test - void autoConfigurationWhenBothJwtKeyLocationAndTokenIntrospectionUriSetShouldFail() { - this.contextRunner - .withPropertyValues( - "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com", - "spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location") - .run((context) -> assertThat(context).hasFailed().getFailure().hasMessageContaining( - "Only one of jwt.public-key-location and opaquetoken.introspection-uri should be configured.")); - } - @SuppressWarnings("unchecked") @Test void autoConfigurationShouldConfigureResourceServerUsingJwkSetUriAndIssuerUri() throws Exception { @@ -338,7 +359,7 @@ void autoConfigurationShouldConfigureResourceServerUsingJwkSetUriAndIssuerUri() .run((context) -> { assertThat(context).hasSingleBean(ReactiveJwtDecoder.class); ReactiveJwtDecoder reactiveJwtDecoder = context.getBean(ReactiveJwtDecoder.class); - DelegatingOAuth2TokenValidator jwtValidator = (DelegatingOAuth2TokenValidator) ReflectionTestUtils + DelegatingOAuth2TokenValidator jwtValidator = (DelegatingOAuth2TokenValidator) ReflectionTestUtils .getField(reactiveJwtDecoder, "jwtValidator"); Collection> tokenValidators = (Collection>) ReflectionTestUtils .getField(jwtValidator, "tokenValidators"); @@ -383,6 +404,8 @@ private void setupMockResponse(String issuer) throws JsonProcessingException { .setBody(new ObjectMapper().writeValueAsString(getResponse(issuer))) .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); this.server.enqueue(mockResponse); + this.server.enqueue( + new MockResponse().setResponseCode(200).setHeader("Content-Type", "application/json").setBody(JWK_SET)); } private void setupMockResponsesWithErrors(String issuer, int errorResponseCount) throws JsonProcessingException { @@ -400,7 +423,7 @@ private Map getResponse(String issuer) { response.put("code_challenge_methods_supported", Collections.emptyList()); response.put("id_token_signing_alg_values_supported", Collections.emptyList()); response.put("issuer", issuer); - response.put("jwks_uri", "https://example.com/oauth2/v3/certs"); + response.put("jwks_uri", issuer + "/.well-known/jwks.json"); response.put("response_types_supported", Collections.emptyList()); response.put("revocation_endpoint", "https://example.com/o/oauth2/revoke"); response.put("scopes_supported", Collections.singletonList("openid")); @@ -446,7 +469,7 @@ ReactiveOpaqueTokenIntrospector decoder() { static class SecurityWebFilterChainConfig { @Bean - SecurityWebFilterChain testSpringSecurityFilterChain(ServerHttpSecurity http) throws Exception { + SecurityWebFilterChain testSpringSecurityFilterChain(ServerHttpSecurity http) { http.authorizeExchange((exchanges) -> { exchanges.pathMatchers("/message/**").hasRole("ADMIN"); exchanges.anyExchange().authenticated(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java index dc37b27f46c9..288832babfbf 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.autoconfigure.security.oauth2.resource.servlet; import java.util.Collection; @@ -41,6 +42,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.config.BeanIds; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; import org.springframework.security.oauth2.core.OAuth2TokenValidator; @@ -48,7 +50,7 @@ import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtIssuerValidator; import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; -import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter; import org.springframework.security.web.FilterChainProxy; @@ -73,6 +75,12 @@ class OAuth2ResourceServerAutoConfigurationTests { private MockWebServer server; + private static final String JWK_SET = "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"use\":\"sig\"," + + "\"kid\":\"one\",\"n\":\"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGm" + + "uLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtd" + + "F4rIe7u-977QwHuh7yRPBQ3sII-cVoOUMgaXB9SHcGF2iZCtPzL_IffDUcfhLQteGebhW8A6eUHgpD5A1PQ-JCw_G7UOzZAj" + + "jDjtNM2eqm8j-Ms_gqnm4MiCZ4E-9pDN77CAAPVN7kuX6ejs9KBXpk01z48i9fORYk9u7rAkh1HuQw\"}]}"; + @AfterEach void cleanup() throws Exception { if (this.server != null) { @@ -98,7 +106,8 @@ void autoConfigurationShouldMatchDefaultJwsAlgorithm() { JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class); Object processor = ReflectionTestUtils.getField(jwtDecoder, "jwtProcessor"); Object keySelector = ReflectionTestUtils.getField(processor, "jwsKeySelector"); - assertThat(keySelector).hasFieldOrPropertyWithValue("jwsAlg", JWSAlgorithm.RS256); + assertThat(keySelector).hasFieldOrPropertyWithValue("jwsAlgs", + Collections.singleton(JWSAlgorithm.RS256)); }); } @@ -111,7 +120,8 @@ void autoConfigurationShouldConfigureResourceServerWithJwsAlgorithm() { JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class); Object processor = ReflectionTestUtils.getField(jwtDecoder, "jwtProcessor"); Object keySelector = ReflectionTestUtils.getField(processor, "jwsKeySelector"); - assertThat(keySelector).hasFieldOrPropertyWithValue("jwsAlg", JWSAlgorithm.RS384); + assertThat(keySelector).hasFieldOrPropertyWithValue("jwsAlgs", + Collections.singleton(JWSAlgorithm.RS384)); assertThat(getBearerTokenFilter(context)).isNotNull(); }); } @@ -129,7 +139,8 @@ void autoConfigurationShouldConfigureResourceServerUsingOidcIssuerUri() throws E assertThat(context).hasSingleBean(JwtDecoder.class); assertThat(context.containsBean("jwtDecoderByIssuerUri")).isTrue(); }); - assertThat(this.server.getRequestCount()).isEqualTo(1); + // The last request is to the JWK Set endpoint to look up the algorithm + assertThat(this.server.getRequestCount()).isEqualTo(2); } @Test @@ -145,7 +156,8 @@ void autoConfigurationShouldConfigureResourceServerUsingOidcRfc8414IssuerUri() t assertThat(context).hasSingleBean(JwtDecoder.class); assertThat(context.containsBean("jwtDecoderByIssuerUri")).isTrue(); }); - assertThat(this.server.getRequestCount()).isEqualTo(2); + // The last request is to the JWK Set endpoint to look up the algorithm + assertThat(this.server.getRequestCount()).isEqualTo(3); } @Test @@ -161,7 +173,8 @@ void autoConfigurationShouldConfigureResourceServerUsingOAuthIssuerUri() throws assertThat(context).hasSingleBean(JwtDecoder.class); assertThat(context.containsBean("jwtDecoderByIssuerUri")).isTrue(); }); - assertThat(this.server.getRequestCount()).isEqualTo(3); + // The last request is to the JWK Set endpoint to look up the algorithm + assertThat(this.server.getRequestCount()).isEqualTo(4); } @Test @@ -190,6 +203,16 @@ void autoConfigurationShouldFailIfPublicKeyLocationDoesNotExist() { .hasMessageContaining("Public key location does not exist")); } + @Test + void autoConfigurationShouldFailIfAlgorithmIsInvalid() { + this.contextRunner + .withPropertyValues( + "spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location", + "spring.security.oauth2.resourceserver.jwt.jws-algorithm=NOT_VALID") + .run((context) -> assertThat(context).hasFailed().getFailure() + .hasMessageContaining("signatureAlgorithm cannot be null")); + } + @Test void autoConfigurationWhenSetUriKeyLocationAndIssuerUriPresentShouldUseSetUri() { this.contextRunner @@ -246,83 +269,97 @@ void jwtDecoderByOidcIssuerUriIsConditionalOnMissingBean() { } @Test - void autoConfigurationShouldBeConditionalOnJwtAuthenticationTokenClass() { + void autoConfigurationShouldBeConditionalOnResourceServerClass() { this.contextRunner .withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com") .withUserConfiguration(JwtDecoderConfig.class) - .withClassLoader(new FilteredClassLoader(JwtAuthenticationToken.class)) - .run((context) -> assertThat(getBearerTokenFilter(context)).isNull()); + .withClassLoader(new FilteredClassLoader(BearerTokenAuthenticationToken.class)).run((context) -> { + assertThat(context).doesNotHaveBean(OAuth2ResourceServerAutoConfiguration.class); + assertThat(getBearerTokenFilter(context)).isNull(); + }); } @Test - void autoConfigurationShouldBeConditionalOnJwtDecoderClass() { + void autoConfigurationForJwtShouldBeConditionalOnJwtDecoderClass() { this.contextRunner .withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com") .withUserConfiguration(JwtDecoderConfig.class) - .withClassLoader(new FilteredClassLoader(JwtDecoder.class)) - .run((context) -> assertThat(getBearerTokenFilter(context)).isNull()); + .withClassLoader(new FilteredClassLoader(JwtDecoder.class)).run((context) -> { + assertThat(context).hasSingleBean(OAuth2ResourceServerAutoConfiguration.class); + assertThat(getBearerTokenFilter(context)).isNull(); + }); } @Test - void autoConfigurationWhenIntrospectionUriAvailableShouldConfigureIntrospectionClient() { + void jwtSecurityFilterShouldBeConditionalOnSecurityFilterChainClass() { this.contextRunner - .withPropertyValues( - "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com", - "spring.security.oauth2.resourceserver.opaquetoken.client-id=my-client-id", - "spring.security.oauth2.resourceserver.opaquetoken.client-secret=my-client-secret") - .run((context) -> { - assertThat(context).hasSingleBean(OpaqueTokenIntrospector.class); - assertThat(getBearerTokenFilter(context)).isNotNull(); + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com") + .withUserConfiguration(JwtDecoderConfig.class) + .withClassLoader(new FilteredClassLoader(SecurityFilterChain.class)).run((context) -> { + assertThat(context).hasSingleBean(OAuth2ResourceServerAutoConfiguration.class); + assertThat(getBearerTokenFilter(context)).isNull(); }); } @Test - void opaqueTokenIntrospectorIsConditionalOnMissingBean() { + void opaqueTokenSecurityFilterShouldBeConditionalOnSecurityFilterChainClass() { this.contextRunner .withPropertyValues( - "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com") - .withUserConfiguration(OpaqueTokenIntrospectorConfig.class) - .run((context) -> assertThat(getBearerTokenFilter(context)).isNotNull()); + "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com", + "spring.security.oauth2.resourceserver.opaquetoken.client-id=my-client-id", + "spring.security.oauth2.resourceserver.opaquetoken.client-secret=my-client-secret") + .withClassLoader(new FilteredClassLoader(SecurityFilterChain.class)).run((context) -> { + assertThat(context).hasSingleBean(OAuth2ResourceServerAutoConfiguration.class); + assertThat(getBearerTokenFilter(context)).isNull(); + }); } @Test - void autoConfigurationWhenIntrospectionUriAvailableShouldBeConditionalOnClass() { - this.contextRunner.withClassLoader(new FilteredClassLoader(BearerTokenAuthenticationToken.class)) - .withPropertyValues( + void autoConfigurationWhenJwkSetUriAndIntrospectionUriAvailable() { + this.contextRunner + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com", "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com", "spring.security.oauth2.resourceserver.opaquetoken.client-id=my-client-id", "spring.security.oauth2.resourceserver.opaquetoken.client-secret=my-client-secret") - .run((context) -> assertThat(context).doesNotHaveBean(OpaqueTokenIntrospector.class)); + .run((context) -> { + assertThat(context).hasSingleBean(OpaqueTokenIntrospector.class); + assertThat(context).hasSingleBean(JwtDecoder.class); + assertThat(getBearerTokenFilter(context)) + .extracting("authenticationManagerResolver.arg$1.providers").asList() + .hasAtLeastOneElementOfType(JwtAuthenticationProvider.class); + }); } @Test - void autoConfigurationWhenBothJwkSetUriAndTokenIntrospectionUriSetShouldFail() { + void autoConfigurationWhenIntrospectionUriAvailableShouldConfigureIntrospectionClient() { this.contextRunner .withPropertyValues( "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com", - "spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com") - .run((context) -> assertThat(context).hasFailed().getFailure().hasMessageContaining( - "Only one of jwt.jwk-set-uri and opaquetoken.introspection-uri should be configured.")); + "spring.security.oauth2.resourceserver.opaquetoken.client-id=my-client-id", + "spring.security.oauth2.resourceserver.opaquetoken.client-secret=my-client-secret") + .run((context) -> { + assertThat(context).hasSingleBean(OpaqueTokenIntrospector.class); + assertThat(getBearerTokenFilter(context)).isNotNull(); + }); } @Test - void autoConfigurationWhenBothJwtIssuerUriAndTokenIntrospectionUriSetShouldFail() { + void opaqueTokenIntrospectorIsConditionalOnMissingBean() { this.contextRunner .withPropertyValues( - "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com", - "spring.security.oauth2.resourceserver.jwt.issuer-uri=https://jwk-oidc-issuer-location.com") - .run((context) -> assertThat(context).hasFailed().getFailure().hasMessageContaining( - "Only one of jwt.issuer-uri and opaquetoken.introspection-uri should be configured.")); + "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com") + .withUserConfiguration(OpaqueTokenIntrospectorConfig.class) + .run((context) -> assertThat(getBearerTokenFilter(context)).isNotNull()); } @Test - void autoConfigurationWhenBothJwtKeyLocationAndTokenIntrospectionUriSetShouldFail() { - this.contextRunner + void autoConfigurationWhenIntrospectionUriAvailableShouldBeConditionalOnClass() { + this.contextRunner.withClassLoader(new FilteredClassLoader(BearerTokenAuthenticationToken.class)) .withPropertyValues( "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com", - "spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location") - .run((context) -> assertThat(context).hasFailed().getFailure().hasMessageContaining( - "Only one of jwt.public-key-location and opaquetoken.introspection-uri should be configured.")); + "spring.security.oauth2.resourceserver.opaquetoken.client-id=my-client-id", + "spring.security.oauth2.resourceserver.opaquetoken.client-secret=my-client-secret") + .run((context) -> assertThat(context).doesNotHaveBean(OpaqueTokenIntrospector.class)); } @SuppressWarnings("unchecked") @@ -341,7 +378,7 @@ void autoConfigurationShouldConfigureResourceServerUsingJwkSetUriAndIssuerUri() .run((context) -> { assertThat(context).hasSingleBean(JwtDecoder.class); JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class); - DelegatingOAuth2TokenValidator jwtValidator = (DelegatingOAuth2TokenValidator) ReflectionTestUtils + DelegatingOAuth2TokenValidator jwtValidator = (DelegatingOAuth2TokenValidator) ReflectionTestUtils .getField(jwtDecoder, "jwtValidator"); Collection> tokenValidators = (Collection>) ReflectionTestUtils .getField(jwtValidator, "tokenValidators"); @@ -349,6 +386,24 @@ void autoConfigurationShouldConfigureResourceServerUsingJwkSetUriAndIssuerUri() }); } + @Test + void jwtSecurityConfigurerBacksOffWhenSecurityFilterChainBeanIsPresent() { + this.contextRunner + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com") + .withUserConfiguration(JwtDecoderConfig.class, TestSecurityFilterChainConfig.class) + .run((context) -> assertThat(context).hasSingleBean(SecurityFilterChain.class)); + } + + @Test + void opaqueTokenSecurityConfigurerBacksOffWhenSecurityFilterChainBeanIsPresent() { + this.contextRunner.withUserConfiguration(TestSecurityFilterChainConfig.class) + .withPropertyValues( + "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com", + "spring.security.oauth2.resourceserver.opaquetoken.client-id=my-client-id", + "spring.security.oauth2.resourceserver.opaquetoken.client-secret=my-client-secret") + .run((context) -> assertThat(context).hasSingleBean(SecurityFilterChain.class)); + } + private Filter getBearerTokenFilter(AssertableWebApplicationContext context) { FilterChainProxy filterChain = (FilterChainProxy) context.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN); List filterChains = filterChain.getFilterChains(); @@ -368,6 +423,8 @@ private void setupMockResponse(String issuer) throws JsonProcessingException { .setBody(new ObjectMapper().writeValueAsString(getResponse(issuer))) .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); this.server.enqueue(mockResponse); + this.server.enqueue( + new MockResponse().setResponseCode(200).setHeader("Content-Type", "application/json").setBody(JWK_SET)); } private void setupMockResponsesWithErrors(String issuer, int errorResponseCount) throws JsonProcessingException { @@ -385,7 +442,7 @@ private Map getResponse(String issuer) { response.put("code_challenge_methods_supported", Collections.emptyList()); response.put("id_token_signing_alg_values_supported", Collections.emptyList()); response.put("issuer", issuer); - response.put("jwks_uri", "https://example.com/oauth2/v3/certs"); + response.put("jwks_uri", issuer + "/.well-known/jwks.json"); response.put("response_types_supported", Collections.emptyList()); response.put("revocation_endpoint", "https://example.com/o/oauth2/revoke"); response.put("scopes_supported", Collections.singletonList("openid")); @@ -425,4 +482,16 @@ OpaqueTokenIntrospector decoder() { } + @Configuration(proxyBeanMethods = false) + @EnableWebSecurity + static class TestSecurityFilterChainConfig { + + @Bean + SecurityFilterChain testSecurityFilterChain(HttpSecurity http) throws Exception { + return http.antMatcher("/**").authorizeRequests((authorize) -> authorize.anyRequest().authenticated()) + .build(); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfigurationTests.java index 5f3bff879b07..59ce29e81543 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,7 @@ */ class ReactiveSecurityAutoConfigurationTests { - private ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner(); + private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner(); @Test void backsOffWhenWebFilterChainProxyBeanPresent() { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/StaticResourceRequestTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/StaticResourceRequestTests.java index 7df939f9478e..556a1d94e6c3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/StaticResourceRequestTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/StaticResourceRequestTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,7 +54,9 @@ void atCommonLocationsShouldMatchCommonLocations() { assertMatcher(matcher).matches("/js/file.js"); assertMatcher(matcher).matches("/images/file.css"); assertMatcher(matcher).matches("/webjars/file.css"); - assertMatcher(matcher).matches("/foo/favicon.ico"); + assertMatcher(matcher).matches("/favicon.ico"); + assertMatcher(matcher).matches("/favicon.png"); + assertMatcher(matcher).matches("/icons/icon-48x48.png"); assertMatcher(matcher).doesNotMatch("/bar"); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/rsocket/RSocketSecurityAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/rsocket/RSocketSecurityAutoConfigurationTests.java index 584782e04e6f..8b6f2411dc25 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/rsocket/RSocketSecurityAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/rsocket/RSocketSecurityAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,34 +16,33 @@ package org.springframework.boot.autoconfigure.security.rsocket; -import io.rsocket.RSocketFactory; +import io.rsocket.core.RSocketServer; import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration; import org.springframework.boot.autoconfigure.rsocket.RSocketStrategiesAutoConfiguration; import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration; -import org.springframework.boot.rsocket.server.ServerRSocketFactoryProcessor; +import org.springframework.boot.rsocket.server.RSocketServerCustomizer; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.security.config.annotation.rsocket.RSocketSecurity; import org.springframework.security.rsocket.core.SecuritySocketAcceptorInterceptor; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link RSocketSecurityAutoConfiguration}. * * @author Madhura Bhave + * @author Brian Clozel */ class RSocketSecurityAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration(AutoConfigurations - .of(ReactiveUserDetailsServiceAutoConfiguration.class, RSocketSecurityAutoConfiguration.class, - RSocketMessagingAutoConfiguration.class, RSocketStrategiesAutoConfiguration.class)); + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReactiveUserDetailsServiceAutoConfiguration.class, + RSocketSecurityAutoConfiguration.class, RSocketMessagingAutoConfiguration.class, + RSocketStrategiesAutoConfiguration.class)); @Test void autoConfigurationEnablesRSocketSecurity() { @@ -58,14 +57,15 @@ void autoConfigurationIsConditionalOnSecuritySocketAcceptorInterceptorClass() { @Test void autoConfigurationAddsCustomizerForServerRSocketFactory() { - RSocketFactory.ServerRSocketFactory factory = mock(RSocketFactory.ServerRSocketFactory.class); - ArgumentCaptor captor = ArgumentCaptor - .forClass(SecuritySocketAcceptorInterceptor.class); + RSocketServer server = RSocketServer.create(); this.contextRunner.run((context) -> { - ServerRSocketFactoryProcessor customizer = context.getBean(ServerRSocketFactoryProcessor.class); - customizer.process(factory); - verify(factory).addSocketAcceptorPlugin(captor.capture()); - assertThat(captor.getValue()).isInstanceOf(SecuritySocketAcceptorInterceptor.class); + RSocketServerCustomizer customizer = context.getBean(RSocketServerCustomizer.class); + customizer.customize(server); + server.interceptors((registry) -> registry.forSocketAcceptor((interceptors) -> { + assertThat(interceptors).isNotEmpty(); + assertThat(interceptors) + .anyMatch((interceptor) -> interceptor instanceof SecuritySocketAcceptorInterceptor); + })); }); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyAutoConfigurationTests.java index 297a36c2a925..da95d6308259 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,14 @@ package org.springframework.boot.autoconfigure.security.saml2; +import java.io.InputStream; import java.util.List; import javax.servlet.Filter; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okio.Buffer; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -30,10 +34,15 @@ import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; import org.springframework.security.config.BeanIds; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter; import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.SecurityFilterChain; @@ -46,11 +55,11 @@ * * @author Madhura Bhave */ -public class Saml2RelyingPartyAutoConfigurationTests { +class Saml2RelyingPartyAutoConfigurationTests { private static final String PREFIX = "spring.security.saml2.relyingparty.registration"; - private WebApplicationContextRunner contextRunner = new WebApplicationContextRunner().withConfiguration( + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner().withConfiguration( AutoConfigurations.of(Saml2RelyingPartyAutoConfiguration.class, SecurityAutoConfiguration.class)); @Test @@ -79,14 +88,94 @@ void relyingPartyRegistrationRepositoryBeanShouldBeCreatedWhenPropertiesPresent( this.contextRunner.withPropertyValues(getPropertyValues()).run((context) -> { RelyingPartyRegistrationRepository repository = context.getBean(RelyingPartyRegistrationRepository.class); RelyingPartyRegistration registration = repository.findByRegistrationId("foo"); - assertThat(registration.getIdpWebSsoUrl()) + + assertThat(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation()) .isEqualTo("https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php"); - assertThat(registration.getRemoteIdpEntityId()) + assertThat(registration.getAssertingPartyDetails().getEntityId()) .isEqualTo("https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php"); - assertThat(registration.getAssertionConsumerServiceUrlTemplate()) - .isEqualTo("{baseUrl}" + Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI); - assertThat(registration.getSigningCredentials()).isNotNull(); - assertThat(registration.getVerificationCredentials()).isNotNull(); + assertThat(registration.getAssertionConsumerServiceLocation()) + .isEqualTo("{baseUrl}/login/saml2/foo-entity-id"); + assertThat(registration.getAssertionConsumerServiceBinding()).isEqualTo(Saml2MessageBinding.REDIRECT); + assertThat(registration.getAssertingPartyDetails().getSingleSignOnServiceBinding()) + .isEqualTo(Saml2MessageBinding.POST); + assertThat(registration.getAssertingPartyDetails().getWantAuthnRequestsSigned()).isEqualTo(false); + assertThat(registration.getSigningX509Credentials()).hasSize(1); + assertThat(registration.getDecryptionX509Credentials()).hasSize(1); + assertThat(registration.getAssertingPartyDetails().getVerificationX509Credentials()).isNotNull(); + assertThat(registration.getEntityId()).isEqualTo("{baseUrl}/saml2/foo-entity-id"); + }); + } + + @Test + void autoConfigurationWhenSignRequestsTrueAndNoSigningCredentialsShouldThrowException() { + this.contextRunner.withPropertyValues(getPropertyValuesWithoutSigningCredentials(true)).run((context) -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()).hasMessageContaining( + "Signing credentials must not be empty when authentication requests require signing."); + }); + } + + @Test + void autoConfigurationWhenSignRequestsFalseAndNoSigningCredentialsShouldNotThrowException() { + this.contextRunner.withPropertyValues(getPropertyValuesWithoutSigningCredentials(false)) + .run((context) -> assertThat(context).hasSingleBean(RelyingPartyRegistrationRepository.class)); + } + + @Test + void autoconfigurationShouldQueryIdentityProviderMetadataWhenMetadataUrlIsPresent() throws Exception { + try (MockWebServer server = new MockWebServer()) { + server.start(); + String metadataUrl = server.url("").toString(); + setupMockResponse(server, new ClassPathResource("saml/idp-metadata")); + this.contextRunner.withPropertyValues(PREFIX + ".foo.identityprovider.metadata-uri=" + metadataUrl) + .run((context) -> { + assertThat(context).hasSingleBean(RelyingPartyRegistrationRepository.class); + assertThat(server.getRequestCount()).isEqualTo(1); + }); + } + } + + @Test + void autoconfigurationShouldUseBindingFromMetadataUrlIfPresent() throws Exception { + try (MockWebServer server = new MockWebServer()) { + server.start(); + String metadataUrl = server.url("").toString(); + setupMockResponse(server, new ClassPathResource("saml/idp-metadata")); + this.contextRunner.withPropertyValues(PREFIX + ".foo.identityprovider.metadata-uri=" + metadataUrl) + .run((context) -> { + RelyingPartyRegistrationRepository repository = context + .getBean(RelyingPartyRegistrationRepository.class); + RelyingPartyRegistration registration = repository.findByRegistrationId("foo"); + assertThat(registration.getAssertingPartyDetails().getSingleSignOnServiceBinding()) + .isEqualTo(Saml2MessageBinding.POST); + }); + } + } + + @Test + void autoconfigurationWhenMetadataUrlAndPropertyPresentShouldUseBindingFromProperty() throws Exception { + try (MockWebServer server = new MockWebServer()) { + server.start(); + String metadataUrl = server.url("").toString(); + setupMockResponse(server, new ClassPathResource("saml/idp-metadata")); + this.contextRunner.withPropertyValues(PREFIX + ".foo.identityprovider.metadata-uri=" + metadataUrl, + PREFIX + ".foo.identityprovider.singlesignon.binding=redirect").run((context) -> { + RelyingPartyRegistrationRepository repository = context + .getBean(RelyingPartyRegistrationRepository.class); + RelyingPartyRegistration registration = repository.findByRegistrationId("foo"); + assertThat(registration.getAssertingPartyDetails().getSingleSignOnServiceBinding()) + .isEqualTo(Saml2MessageBinding.REDIRECT); + }); + } + } + + @Test + void autoconfigurationWhenNoMetadataUrlOrPropertyPresentShouldUseRedirectBinding() { + this.contextRunner.withPropertyValues(getPropertyValuesWithoutSsoBinding()).run((context) -> { + RelyingPartyRegistrationRepository repository = context.getBean(RelyingPartyRegistrationRepository.class); + RelyingPartyRegistration registration = repository.findByRegistrationId("foo"); + assertThat(registration.getAssertingPartyDetails().getSingleSignOnServiceBinding()) + .isEqualTo(Saml2MessageBinding.REDIRECT); }); } @@ -112,13 +201,51 @@ void samlLoginShouldBackOffWhenAWebSecurityConfigurerAdapterIsDefined() { .run((context) -> assertThat(hasFilter(context, Saml2WebSsoAuthenticationFilter.class)).isFalse()); } + @Test + void samlLoginShouldBackOffWhenASecurityFilterChainBeanIsPresent() { + this.contextRunner.withUserConfiguration(TestSecurityFilterChainConfig.class) + .withPropertyValues(getPropertyValues()) + .run((context) -> assertThat(hasFilter(context, Saml2WebSsoAuthenticationFilter.class)).isFalse()); + } + + @Test + void samlLoginShouldShouldBeConditionalOnSecurityWebFilterClass() { + this.contextRunner.withClassLoader(new FilteredClassLoader(SecurityFilterChain.class)) + .withPropertyValues(getPropertyValues()) + .run((context) -> assertThat(context).doesNotHaveBean(SecurityFilterChain.class)); + } + + private String[] getPropertyValuesWithoutSigningCredentials(boolean signRequests) { + return new String[] { PREFIX + + ".foo.identityprovider.singlesignon.url=https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php", + PREFIX + ".foo.identityprovider.singlesignon.binding=post", + PREFIX + ".foo.identityprovider.singlesignon.sign-request=" + signRequests, + PREFIX + ".foo.identityprovider.entity-id=https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php", + PREFIX + ".foo.identityprovider.verification.credentials[0].certificate-location=classpath:saml/certificate-location" }; + } + + private String[] getPropertyValuesWithoutSsoBinding() { + return new String[] { PREFIX + + ".foo.identityprovider.singlesignon.url=https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php", + PREFIX + ".foo.identityprovider.singlesignon.sign-request=false", + PREFIX + ".foo.identityprovider.entity-id=https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php", + PREFIX + ".foo.identityprovider.verification.credentials[0].certificate-location=classpath:saml/certificate-location" }; + } + private String[] getPropertyValues() { return new String[] { PREFIX + ".foo.signing.credentials[0].private-key-location=classpath:saml/private-key-location", PREFIX + ".foo.signing.credentials[0].certificate-location=classpath:saml/certificate-location", - PREFIX + ".foo.identityprovider.sso-url=https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php", + PREFIX + ".foo.decryption.credentials[0].private-key-location=classpath:saml/private-key-location", + PREFIX + ".foo.decryption.credentials[0].certificate-location=classpath:saml/certificate-location", + PREFIX + ".foo.identityprovider.singlesignon.url=https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php", + PREFIX + ".foo.identityprovider.singlesignon.binding=post", + PREFIX + ".foo.identityprovider.singlesignon.sign-request=false", PREFIX + ".foo.identityprovider.entity-id=https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php", - PREFIX + ".foo.identityprovider.verification.credentials[0].certificate-location=classpath:saml/certificate-location" }; + PREFIX + ".foo.identityprovider.verification.credentials[0].certificate-location=classpath:saml/certificate-location", + PREFIX + ".foo.entity-id={baseUrl}/saml2/foo-entity-id", + PREFIX + ".foo.acs.location={baseUrl}/login/saml2/foo-entity-id", + PREFIX + ".foo.acs.binding=redirect" }; } private boolean hasFilter(AssertableWebApplicationContext context, Class filter) { @@ -128,6 +255,14 @@ private boolean hasFilter(AssertableWebApplicationContext context, Class authorize.anyRequest().authenticated()) + .build(); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyPropertiesTests.java new file mode 100644 index 000000000000..04f16e9f9618 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyPropertiesTests.java @@ -0,0 +1,105 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security.saml2; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Saml2RelyingPartyProperties}. + * + * @author Madhura Bhave + */ +class Saml2RelyingPartyPropertiesTests { + + private final Saml2RelyingPartyProperties properties = new Saml2RelyingPartyProperties(); + + @Test + void customizeSsoUrl() { + bind("spring.security.saml2.relyingparty.registration.simplesamlphp.identity-provider.single-sign-on.url", + "https://simplesaml-for-spring-saml/SSOService.php"); + assertThat( + this.properties.getRegistration().get("simplesamlphp").getIdentityprovider().getSinglesignon().getUrl()) + .isEqualTo("https://simplesaml-for-spring-saml/SSOService.php"); + } + + @Test + void customizeSsoBinding() { + bind("spring.security.saml2.relyingparty.registration.simplesamlphp.identity-provider.single-sign-on.binding", + "post"); + assertThat(this.properties.getRegistration().get("simplesamlphp").getIdentityprovider().getSinglesignon() + .getBinding()).isEqualTo(Saml2MessageBinding.POST); + } + + @Test + void customizeSsoSignRequests() { + bind("spring.security.saml2.relyingparty.registration.simplesamlphp.identity-provider.single-sign-on.sign-request", + "false"); + assertThat(this.properties.getRegistration().get("simplesamlphp").getIdentityprovider().getSinglesignon() + .isSignRequest()).isEqualTo(false); + } + + @Test + void customizeSsoSignRequestsIsTrueByDefault() { + this.properties.getRegistration().put("simplesamlphp", new Saml2RelyingPartyProperties.Registration()); + assertThat(this.properties.getRegistration().get("simplesamlphp").getIdentityprovider().getSinglesignon() + .isSignRequest()).isEqualTo(true); + } + + @Test + void customizeRelyingPartyEntityId() { + bind("spring.security.saml2.relyingparty.registration.simplesamlphp.entity-id", + "{baseUrl}/saml2/custom-entity-id"); + assertThat(this.properties.getRegistration().get("simplesamlphp").getEntityId()) + .isEqualTo("{baseUrl}/saml2/custom-entity-id"); + } + + @Test + void customizeRelyingPartyEntityIdDefaultsToServiceProviderMetadata() { + assertThat(RelyingPartyRegistration.withRegistrationId("id")).extracting("entityId") + .isEqualTo(new Saml2RelyingPartyProperties.Registration().getEntityId()); + } + + @Test + void customizeIdentityProviderMetadataUri() { + bind("spring.security.saml2.relyingparty.registration.simplesamlphp.identityprovider.metadata-uri", + "https://idp.example.org/metadata"); + assertThat(this.properties.getRegistration().get("simplesamlphp").getIdentityprovider().getMetadataUri()) + .isEqualTo("https://idp.example.org/metadata"); + } + + private void bind(String name, String value) { + bind(Collections.singletonMap(name, value)); + } + + private void bind(Map map) { + ConfigurationPropertySource source = new MapConfigurationPropertySource(map); + new Binder(source).bind("spring.security.saml2.relyingparty", Bindable.ofInstance(this.properties)); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/SecurityAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/SecurityAutoConfigurationTests.java index cb8510a52608..810c0b340340 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/SecurityAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/SecurityAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,11 @@ package org.springframework.boot.autoconfigure.security.servlet; -import java.util.EnumSet; +import java.security.interfaces.RSAPublicKey; import javax.servlet.DispatcherType; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -28,23 +29,30 @@ import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.test.City; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConfigurationPropertiesBinding; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.convert.ApplicationConversionService; +import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean; import org.springframework.boot.web.servlet.filter.OrderedFilter; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.security.authentication.AuthenticationEventPublisher; import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.data.repository.query.SecurityEvaluationContextExtension; import org.springframework.security.web.FilterChainProxy; -import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.security.web.SecurityFilterChain; import static org.assertj.core.api.Assertions.assertThat; @@ -58,7 +66,7 @@ */ class SecurityAutoConfigurationTests { - private WebApplicationContextRunner contextRunner = new WebApplicationContextRunner().withConfiguration( + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner().withConfiguration( AutoConfigurations.of(SecurityAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class)); @Test @@ -69,6 +77,34 @@ void testWebConfiguration() { }); } + @Test + void enableWebSecurityIsConditionalOnClass() { + this.contextRunner.withClassLoader(new FilteredClassLoader("org.springframework.security.config")) + .run((context) -> assertThat(context).doesNotHaveBean("springSecurityFilterChain")); + } + + @Test + void filterChainBeanIsConditionalOnClassSecurityFilterChain() { + this.contextRunner.withClassLoader(new FilteredClassLoader(SecurityFilterChain.class)) + .run((context) -> assertThat(context).doesNotHaveBean(SecurityFilterChain.class)); + } + + @Test + void securityConfigurerBacksOffWhenOtherSecurityFilterChainBeanPresent() { + this.contextRunner.withUserConfiguration(TestSecurityFilterChainConfig.class).run((context) -> { + assertThat(context.getBeansOfType(SecurityFilterChain.class).size()).isEqualTo(1); + assertThat(context.containsBean("testSecurityFilterChain")).isTrue(); + }); + } + + @Test + void securityConfigurerBacksOffWhenOtherWebSecurityAdapterBeanPresent() { + this.contextRunner.withUserConfiguration(WebSecurity.class).run((context) -> { + assertThat(context.getBeansOfType(WebSecurityConfigurerAdapter.class).size()).isEqualTo(1); + assertThat(context.containsBean("securityAutoConfigurationTests.WebSecurity")).isTrue(); + }); + } + @Test void testDefaultFilterOrderWithSecurityAdapter() { this.contextRunner @@ -120,9 +156,7 @@ void testCustomFilterOrder() { @Test void testJpaCoexistsHappily() { - this.contextRunner - .withPropertyValues("spring.datasource.url:jdbc:hsqldb:mem:testsecdb", - "spring.datasource.initialization-mode:never") + this.contextRunner.withPropertyValues("spring.datasource.url:jdbc:hsqldb:mem:testsecdb") .withUserConfiguration(EntityConfiguration.class) .withConfiguration( AutoConfigurations.of(HibernateJpaAutoConfiguration.class, DataSourceAutoConfiguration.class)) @@ -143,11 +177,9 @@ void defaultFilterDispatcherTypes() { .run((context) -> { DelegatingFilterProxyRegistrationBean bean = context.getBean("securityFilterChainRegistration", DelegatingFilterProxyRegistrationBean.class); - @SuppressWarnings("unchecked") - EnumSet dispatcherTypes = (EnumSet) ReflectionTestUtils - .getField(bean, "dispatcherTypes"); - assertThat(dispatcherTypes).containsOnly(DispatcherType.ASYNC, DispatcherType.ERROR, - DispatcherType.REQUEST); + assertThat(bean) + .extracting("dispatcherTypes", InstanceOfAssertFactories.iterable(DispatcherType.class)) + .containsOnly(DispatcherType.ASYNC, DispatcherType.ERROR, DispatcherType.REQUEST); }); } @@ -157,13 +189,41 @@ void customFilterDispatcherTypes() { .withConfiguration(AutoConfigurations.of(SecurityFilterAutoConfiguration.class)).run((context) -> { DelegatingFilterProxyRegistrationBean bean = context.getBean("securityFilterChainRegistration", DelegatingFilterProxyRegistrationBean.class); - @SuppressWarnings("unchecked") - EnumSet dispatcherTypes = (EnumSet) ReflectionTestUtils - .getField(bean, "dispatcherTypes"); - assertThat(dispatcherTypes).containsOnly(DispatcherType.INCLUDE, DispatcherType.ERROR); + assertThat(bean) + .extracting("dispatcherTypes", InstanceOfAssertFactories.iterable(DispatcherType.class)) + .containsOnly(DispatcherType.INCLUDE, DispatcherType.ERROR); }); } + @Test + void emptyFilterDispatcherTypesDoNotThrowException() { + this.contextRunner.withPropertyValues("spring.security.filter.dispatcher-types:") + .withConfiguration(AutoConfigurations.of(SecurityFilterAutoConfiguration.class)).run((context) -> { + DelegatingFilterProxyRegistrationBean bean = context.getBean("securityFilterChainRegistration", + DelegatingFilterProxyRegistrationBean.class); + assertThat(bean) + .extracting("dispatcherTypes", InstanceOfAssertFactories.iterable(DispatcherType.class)) + .isEmpty(); + }); + } + + @Test + void whenAConfigurationPropertyBindingConverterIsDefinedThenBindingToAnRsaKeySucceeds() { + this.contextRunner.withUserConfiguration(ConverterConfiguration.class, PropertiesConfiguration.class) + .withPropertyValues("jwt.public-key=classpath:public-key-location") + .run((context) -> assertThat(context.getBean(JwtProperties.class).getPublicKey()).isNotNull()); + } + + @Test + void whenTheBeanFactoryHasAConversionServiceAndAConfigurationPropertyBindingConverterIsDefinedThenBindingToAnRsaKeySucceeds() { + this.contextRunner + .withInitializer( + (context) -> context.getBeanFactory().setConversionService(new ApplicationConversionService())) + .withUserConfiguration(ConverterConfiguration.class, PropertiesConfiguration.class) + .withPropertyValues("jwt.public-key=classpath:public-key-location") + .run((context) -> assertThat(context.getBean(JwtProperties.class).getPublicKey()).isNotNull()); + } + @Configuration(proxyBeanMethods = false) @TestAutoConfigurationPackage(City.class) static class EntityConfiguration { @@ -200,4 +260,59 @@ static class WebSecurity extends WebSecurityConfigurerAdapter { } + @Configuration(proxyBeanMethods = false) + static class TestSecurityFilterChainConfig { + + @Bean + SecurityFilterChain testSecurityFilterChain(HttpSecurity http) throws Exception { + return http.antMatcher("/**").authorizeRequests((authorize) -> authorize.anyRequest().authenticated()) + .build(); + + } + + } + + @Configuration(proxyBeanMethods = false) + static class ConverterConfiguration { + + @Bean + @ConfigurationPropertiesBinding + Converter targetTypeConverter() { + return new Converter() { + + @Override + public TargetType convert(String input) { + return new TargetType(); + } + + }; + } + + } + + @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties(JwtProperties.class) + static class PropertiesConfiguration { + + } + + @ConfigurationProperties("jwt") + static class JwtProperties { + + private RSAPublicKey publicKey; + + RSAPublicKey getPublicKey() { + return this.publicKey; + } + + void setPublicKey(RSAPublicKey publicKey) { + this.publicKey = publicKey; + } + + } + + static class TargetType { + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/StaticResourceRequestTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/StaticResourceRequestTests.java index 60335af7e0d6..26babb807dec 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/StaticResourceRequestTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/StaticResourceRequestTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,7 +48,9 @@ void atCommonLocationsShouldMatchCommonLocations() { assertMatcher(matcher).matches("/js/file.js"); assertMatcher(matcher).matches("/images/file.css"); assertMatcher(matcher).matches("/webjars/file.css"); - assertMatcher(matcher).matches("/foo/favicon.ico"); + assertMatcher(matcher).matches("/favicon.ico"); + assertMatcher(matcher).matches("/favicon.png"); + assertMatcher(matcher).matches("/icons/icon-48x48.png"); assertMatcher(matcher).doesNotMatch("/bar"); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/sendgrid/SendGridAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/sendgrid/SendGridAutoConfigurationTests.java index aaf91b9a4feb..5cd87fe1a8b5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/sendgrid/SendGridAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/sendgrid/SendGridAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,7 +52,7 @@ void close() { void expectedSendGridBeanCreatedApiKey() { loadContext("spring.sendgrid.api-key:SG.SECRET-API-KEY"); SendGrid sendGrid = this.context.getBean(SendGrid.class); - assertThat(sendGrid).extracting("apiKey").isEqualTo("SG.SECRET-API-KEY"); + assertThat(sendGrid.getRequestHeaders().get("Authorization")).isEqualTo("Bearer SG.SECRET-API-KEY"); } @Test @@ -66,7 +66,7 @@ void autoConfigurationNotFiredWhenPropertiesNotSet() { void autoConfigurationNotFiredWhenBeanAlreadyCreated() { loadContext(ManualSendGridConfiguration.class, "spring.sendgrid.api-key:SG.SECRET-API-KEY"); SendGrid sendGrid = this.context.getBean(SendGrid.class); - assertThat(sendGrid).extracting("apiKey").isEqualTo("SG.CUSTOM_API_KEY"); + assertThat(sendGrid.getRequestHeaders().get("Authorization")).isEqualTo("Bearer SG.CUSTOM_API_KEY"); } @Test @@ -79,7 +79,7 @@ void expectedSendGridBeanWithProxyCreated() { } private void loadContext(String... environment) { - this.loadContext(null, environment); + loadContext(null, environment); } private void loadContext(Class additionalConfiguration, String... environment) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/JdbcSessionDataSourceInitializerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/JdbcSessionDataSourceInitializerTests.java new file mode 100644 index 000000000000..5d6cf3865c49 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/JdbcSessionDataSourceInitializerTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.session; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.io.DefaultResourceLoader; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link JdbcSessionDataSourceInitializer}. + * + * @author Stephane Nicoll + */ +class JdbcSessionDataSourceInitializerTests { + + @Test + void getDatabaseNameWithPlatformDoesNotTouchDataSource() { + DataSource dataSource = mock(DataSource.class); + JdbcSessionProperties properties = new JdbcSessionProperties(); + properties.setPlatform("test"); + JdbcSessionDataSourceInitializer initializer = new JdbcSessionDataSourceInitializer(dataSource, + new DefaultResourceLoader(), properties); + assertThat(initializer.getDatabaseName()).isEqualTo("test"); + then(dataSource).shouldHaveNoInteractions(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/NonUniqueSessionRepositoryFailureAnalyzerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/NonUniqueSessionRepositoryFailureAnalyzerTests.java index 52b3e16d82d7..09f304758807 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/NonUniqueSessionRepositoryFailureAnalyzerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/NonUniqueSessionRepositoryFailureAnalyzerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,6 +49,7 @@ void failureAnalysisWithMultipleCandidates() { } @SafeVarargs + @SuppressWarnings("varargs") private final Exception createFailure(Class>... candidates) { return new NonUniqueSessionRepositoryException(Arrays.asList(candidates)); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/ReactiveSessionAutoConfigurationMongoTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/ReactiveSessionAutoConfigurationMongoTests.java index bff4724409d7..f9deeb9c0959 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/ReactiveSessionAutoConfigurationMongoTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/ReactiveSessionAutoConfigurationMongoTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,18 +16,23 @@ package org.springframework.boot.autoconfigure.session; +import java.time.Duration; + import org.junit.jupiter.api.Test; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration; import org.springframework.boot.autoconfigure.data.mongo.MongoReactiveDataAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration; -import org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext; import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; import org.springframework.session.data.mongo.ReactiveMongoSessionRepository; import org.springframework.session.data.redis.ReactiveRedisSessionRepository; @@ -38,16 +43,22 @@ * * @author Andy Wilkinson */ +@Testcontainers(disabledWithoutDocker = true) class ReactiveSessionAutoConfigurationMongoTests extends AbstractSessionAutoConfigurationTests { + @Container + static final MongoDBContainer mongoDb = new MongoDBContainer(DockerImageNames.mongo()).withStartupAttempts(5) + .withStartupTimeout(Duration.ofMinutes(5)); + private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(SessionAutoConfiguration.class)); @Test void defaultConfig() { - this.contextRunner.withPropertyValues("spring.session.store-type=mongodb") - .withConfiguration(AutoConfigurations.of(EmbeddedMongoAutoConfiguration.class, - MongoAutoConfiguration.class, MongoDataAutoConfiguration.class, + this.contextRunner + .withPropertyValues("spring.session.store-type=mongodb", + "spring.data.mongodb.uri=" + mongoDb.getReplicaSetUrl()) + .withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class, MongoDataAutoConfiguration.class, MongoReactiveAutoConfiguration.class, MongoReactiveDataAutoConfiguration.class)) .run(validateSpringSessionUsesMongo("sessions")); } @@ -55,19 +66,33 @@ void defaultConfig() { @Test void defaultConfigWithUniqueStoreImplementation() { this.contextRunner.withClassLoader(new FilteredClassLoader(ReactiveRedisSessionRepository.class)) - .withConfiguration(AutoConfigurations.of(EmbeddedMongoAutoConfiguration.class, - MongoAutoConfiguration.class, MongoDataAutoConfiguration.class, + .withPropertyValues("spring.data.mongodb.uri=" + mongoDb.getReplicaSetUrl()) + .withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class, MongoDataAutoConfiguration.class, MongoReactiveAutoConfiguration.class, MongoReactiveDataAutoConfiguration.class)) .run(validateSpringSessionUsesMongo("sessions")); } + @Test + void defaultConfigWithCustomTimeout() { + this.contextRunner + .withPropertyValues("spring.session.store-type=mongodb", "spring.session.timeout=1m", + "spring.data.mongodb.uri=" + mongoDb.getReplicaSetUrl()) + .withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class, MongoDataAutoConfiguration.class, + MongoReactiveAutoConfiguration.class, MongoReactiveDataAutoConfiguration.class)) + .run((context) -> { + ReactiveMongoSessionRepository repository = validateSessionRepository(context, + ReactiveMongoSessionRepository.class); + assertThat(repository).hasFieldOrPropertyWithValue("maxInactiveIntervalInSeconds", 60); + }); + } + @Test void mongoSessionStoreWithCustomizations() { this.contextRunner - .withConfiguration(AutoConfigurations.of(EmbeddedMongoAutoConfiguration.class, - MongoAutoConfiguration.class, MongoDataAutoConfiguration.class, + .withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class, MongoDataAutoConfiguration.class, MongoReactiveAutoConfiguration.class, MongoReactiveDataAutoConfiguration.class)) - .withPropertyValues("spring.session.store-type=mongodb", "spring.session.mongodb.collection-name=foo") + .withPropertyValues("spring.session.store-type=mongodb", "spring.session.mongodb.collection-name=foo", + "spring.data.mongodb.uri=" + mongoDb.getReplicaSetUrl()) .run(validateSpringSessionUsesMongo("foo")); } @@ -77,6 +102,8 @@ private ContextConsumer validateSpringS ReactiveMongoSessionRepository repository = validateSessionRepository(context, ReactiveMongoSessionRepository.class); assertThat(repository.getCollectionName()).isEqualTo(collectionName); + assertThat(repository).hasFieldOrPropertyWithValue("maxInactiveIntervalInSeconds", + ReactiveMongoSessionRepository.DEFAULT_INACTIVE_INTERVAL); }; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/ReactiveSessionAutoConfigurationRedisTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/ReactiveSessionAutoConfigurationRedisTests.java index 8ab4e9c58119..c85272dd151f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/ReactiveSessionAutoConfigurationRedisTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/ReactiveSessionAutoConfigurationRedisTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext; import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; +import org.springframework.session.MapSession; import org.springframework.session.SaveMode; import org.springframework.session.data.mongo.ReactiveMongoSessionRepository; import org.springframework.session.data.redis.ReactiveRedisSessionRepository; @@ -59,6 +60,18 @@ void defaultConfigWithUniqueStoreImplementation() { .run(validateSpringSessionUsesRedis("spring:session:", SaveMode.ON_SET_ATTRIBUTE)); } + @Test + void defaultConfigWithCustomTimeout() { + this.contextRunner.withPropertyValues("spring.session.store-type=redis", "spring.session.timeout=1m") + .withConfiguration( + AutoConfigurations.of(RedisAutoConfiguration.class, RedisReactiveAutoConfiguration.class)) + .run((context) -> { + ReactiveRedisSessionRepository repository = validateSessionRepository(context, + ReactiveRedisSessionRepository.class); + assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval", 60); + }); + } + @Test void redisSessionStoreWithCustomizations() { this.contextRunner @@ -74,6 +87,8 @@ private ContextConsumer validateSpringS return (context) -> { ReactiveRedisSessionRepository repository = validateSessionRepository(context, ReactiveRedisSessionRepository.class); + assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval", + MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS); assertThat(repository).hasFieldOrPropertyWithValue("namespace", namespace); assertThat(repository).hasFieldOrPropertyWithValue("saveMode", saveMode); }; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationHazelcast3Tests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationHazelcast3Tests.java new file mode 100644 index 000000000000..dad6a1251cc2 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationHazelcast3Tests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.session; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.testsupport.classpath.ClassPathExclusions; +import org.springframework.boot.testsupport.classpath.ClassPathOverrides; +import org.springframework.session.FlushMode; +import org.springframework.session.SaveMode; +import org.springframework.session.data.mongo.MongoIndexedSessionRepository; +import org.springframework.session.data.redis.RedisIndexedSessionRepository; +import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository; +import org.springframework.session.jdbc.JdbcIndexedSessionRepository; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Hazelcast 3 specific tests for {@link SessionAutoConfiguration}. + * + * @author Vedran Pavic + */ +@ClassPathExclusions("hazelcast*.jar") +@ClassPathOverrides("com.hazelcast:hazelcast:3.12.8") +class SessionAutoConfigurationHazelcast3Tests extends AbstractSessionAutoConfigurationTests { + + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(HazelcastAutoConfiguration.class, SessionAutoConfiguration.class)) + .withPropertyValues( + "spring.hazelcast.config=org/springframework/boot/autoconfigure/session/hazelcast-3.xml"); + + @Test + void defaultConfig() { + this.contextRunner.withPropertyValues("spring.session.store-type=hazelcast").run(this::validateDefaultConfig); + } + + @Test + void defaultConfigWithUniqueStoreImplementation() { + this.contextRunner + .withClassLoader(new FilteredClassLoader(JdbcIndexedSessionRepository.class, + RedisIndexedSessionRepository.class, MongoIndexedSessionRepository.class)) + .run(this::validateDefaultConfig); + } + + private void validateDefaultConfig(AssertableWebApplicationContext context) { + validateSessionRepository(context, HazelcastIndexedSessionRepository.class); + } + + @Test + void customMapName() { + this.contextRunner + .withPropertyValues("spring.session.store-type=hazelcast", + "spring.session.hazelcast.map-name=foo:bar:biz") + .run((context) -> validateSessionRepository(context, HazelcastIndexedSessionRepository.class)); + } + + @Test + void customFlushMode() { + this.contextRunner.withPropertyValues("spring.session.store-type=hazelcast", + "spring.session.hazelcast.flush-mode=immediate").run((context) -> { + HazelcastIndexedSessionRepository repository = validateSessionRepository(context, + HazelcastIndexedSessionRepository.class); + assertThat(repository).hasFieldOrPropertyWithValue("flushMode", FlushMode.IMMEDIATE); + }); + } + + @Test + void customSaveMode() { + this.contextRunner.withPropertyValues("spring.session.store-type=hazelcast", + "spring.session.hazelcast.save-mode=on-get-attribute").run((context) -> { + HazelcastIndexedSessionRepository repository = validateSessionRepository(context, + HazelcastIndexedSessionRepository.class); + assertThat(repository).hasFieldOrPropertyWithValue("saveMode", SaveMode.ON_GET_ATTRIBUTE); + }); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationHazelcastTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationHazelcastTests.java index 83bae517f4c2..857a04505abd 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationHazelcastTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationHazelcastTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,10 +17,11 @@ package org.springframework.boot.autoconfigure.session; import com.hazelcast.core.HazelcastInstance; -import com.hazelcast.core.IMap; +import com.hazelcast.map.IMap; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; @@ -30,14 +31,13 @@ import org.springframework.session.SaveMode; import org.springframework.session.data.mongo.MongoIndexedSessionRepository; import org.springframework.session.data.redis.RedisIndexedSessionRepository; -import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository; +import org.springframework.session.hazelcast.Hazelcast4IndexedSessionRepository; import org.springframework.session.jdbc.JdbcIndexedSessionRepository; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; /** * Hazelcast specific tests for {@link SessionAutoConfiguration}. @@ -63,19 +63,32 @@ void defaultConfigWithUniqueStoreImplementation() { .run(this::validateDefaultConfig); } + @Test + void defaultConfigWithCustomTimeout() { + this.contextRunner.withPropertyValues("spring.session.store-type=hazelcast", "spring.session.timeout=1m") + .run((context) -> { + Hazelcast4IndexedSessionRepository repository = validateSessionRepository(context, + Hazelcast4IndexedSessionRepository.class); + assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval", 60); + }); + } + private void validateDefaultConfig(AssertableWebApplicationContext context) { - validateSessionRepository(context, HazelcastIndexedSessionRepository.class); + Hazelcast4IndexedSessionRepository repository = validateSessionRepository(context, + Hazelcast4IndexedSessionRepository.class); + assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval", + (int) new ServerProperties().getServlet().getSession().getTimeout().getSeconds()); HazelcastInstance hazelcastInstance = context.getBean(HazelcastInstance.class); - verify(hazelcastInstance, times(1)).getMap("spring:session:sessions"); + then(hazelcastInstance).should().getMap("spring:session:sessions"); } @Test void customMapName() { this.contextRunner.withPropertyValues("spring.session.store-type=hazelcast", "spring.session.hazelcast.map-name=foo:bar:biz").run((context) -> { - validateSessionRepository(context, HazelcastIndexedSessionRepository.class); + validateSessionRepository(context, Hazelcast4IndexedSessionRepository.class); HazelcastInstance hazelcastInstance = context.getBean(HazelcastInstance.class); - verify(hazelcastInstance, times(1)).getMap("foo:bar:biz"); + then(hazelcastInstance).should().getMap("foo:bar:biz"); }); } @@ -83,8 +96,8 @@ void customMapName() { void customFlushMode() { this.contextRunner.withPropertyValues("spring.session.store-type=hazelcast", "spring.session.hazelcast.flush-mode=immediate").run((context) -> { - HazelcastIndexedSessionRepository repository = validateSessionRepository(context, - HazelcastIndexedSessionRepository.class); + Hazelcast4IndexedSessionRepository repository = validateSessionRepository(context, + Hazelcast4IndexedSessionRepository.class); assertThat(repository).hasFieldOrPropertyWithValue("flushMode", FlushMode.IMMEDIATE); }); } @@ -93,8 +106,8 @@ void customFlushMode() { void customSaveMode() { this.contextRunner.withPropertyValues("spring.session.store-type=hazelcast", "spring.session.hazelcast.save-mode=on-get-attribute").run((context) -> { - HazelcastIndexedSessionRepository repository = validateSessionRepository(context, - HazelcastIndexedSessionRepository.class); + Hazelcast4IndexedSessionRepository repository = validateSessionRepository(context, + Hazelcast4IndexedSessionRepository.class); assertThat(repository).hasFieldOrPropertyWithValue("saveMode", SaveMode.ON_GET_ATTRIBUTE); }); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationIntegrationTests.java index d4d1287b8645..186f76020990 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationIntegrationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,7 @@ package org.springframework.boot.autoconfigure.session; import com.hazelcast.core.HazelcastInstance; -import com.hazelcast.core.IMap; +import com.hazelcast.map.IMap; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -48,7 +48,7 @@ class SessionAutoConfigurationIntegrationTests extends AbstractSessionAutoConfig void severalCandidatesWithNoSessionStore() { this.contextRunner.withUserConfiguration(HazelcastConfiguration.class).run((context) -> { assertThat(context).hasFailed(); - assertThat(context).getFailure().hasCauseInstanceOf(NonUniqueSessionRepositoryException.class); + assertThat(context).getFailure().hasRootCauseInstanceOf(NonUniqueSessionRepositoryException.class); assertThat(context).getFailure() .hasMessageContaining("Multiple session repository candidates are available"); assertThat(context).getFailure() diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationJdbcTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationJdbcTests.java index fd8470c32173..487a224ea67d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationJdbcTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationJdbcTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,18 +16,28 @@ package org.springframework.boot.autoconfigure.session; +import javax.sql.DataSource; + +import org.apache.commons.dbcp2.BasicDataSource; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration; +import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration; import org.springframework.boot.autoconfigure.session.JdbcSessionConfiguration.SpringBootJdbcHttpSessionConfiguration; +import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.jdbc.DataSourceInitializationMode; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import org.springframework.jdbc.BadSqlGrammarException; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.session.FlushMode; @@ -36,6 +46,7 @@ import org.springframework.session.data.redis.RedisIndexedSessionRepository; import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository; import org.springframework.session.jdbc.JdbcIndexedSessionRepository; +import org.springframework.session.jdbc.config.annotation.SpringSessionDataSource; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -70,6 +81,8 @@ void defaultConfigWithUniqueStoreImplementation() { private void validateDefaultConfig(AssertableWebApplicationContext context) { JdbcIndexedSessionRepository repository = validateSessionRepository(context, JdbcIndexedSessionRepository.class); + assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval", + (int) new ServerProperties().getServlet().getSession().getTimeout().getSeconds()); assertThat(repository).hasFieldOrPropertyWithValue("tableName", "SPRING_SESSION"); assertThat(context.getBean(JdbcSessionProperties.class).getInitializeSchema()) .isEqualTo(DataSourceInitializationMode.EMBEDDED); @@ -104,6 +117,16 @@ void disableDataSourceInitializer() { }); } + @Test + void customTimeout() { + this.contextRunner.withPropertyValues("spring.session.store-type=jdbc", "spring.session.timeout=1m") + .run((context) -> { + JdbcIndexedSessionRepository repository = validateSessionRepository(context, + JdbcIndexedSessionRepository.class); + assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval", 60); + }); + } + @Test void customTableName() { this.contextRunner @@ -157,4 +180,87 @@ void customSaveMode() { }); } + @Test + void sessionDataSourceIsUsedWhenAvailable() { + this.contextRunner.withUserConfiguration(SessionDataSourceConfiguration.class) + .withPropertyValues("spring.session.store-type=jdbc").run((context) -> { + JdbcIndexedSessionRepository repository = validateSessionRepository(context, + JdbcIndexedSessionRepository.class); + assertThat(repository).extracting("jdbcOperations").extracting("dataSource") + .isEqualTo(context.getBean("sessionDataSource")); + assertThatExceptionOfType(BadSqlGrammarException.class).isThrownBy( + () -> context.getBean(JdbcOperations.class).queryForList("select * from SPRING_SESSION")); + }); + } + + @Test + void sessionRepositoryBeansDependOnJdbcSessionDataSourceInitializer() { + this.contextRunner.withPropertyValues("spring.session.store-type=jdbc").run((context) -> { + ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); + String[] sessionRepositoryNames = beanFactory.getBeanNamesForType(JdbcIndexedSessionRepository.class); + assertThat(sessionRepositoryNames).isNotEmpty(); + for (String sessionRepositoryName : sessionRepositoryNames) { + assertThat(beanFactory.getBeanDefinition(sessionRepositoryName).getDependsOn()) + .contains("jdbcSessionDataSourceInitializer"); + } + }); + } + + @Test + void sessionRepositoryBeansDependOnFlyway() { + this.contextRunner.withConfiguration(AutoConfigurations.of(FlywayAutoConfiguration.class)) + .withPropertyValues("spring.session.store-type=jdbc", "spring.session.jdbc.initialize-schema=never") + .run((context) -> { + ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); + String[] sessionRepositoryNames = beanFactory + .getBeanNamesForType(JdbcIndexedSessionRepository.class); + assertThat(sessionRepositoryNames).isNotEmpty(); + for (String sessionRepositoryName : sessionRepositoryNames) { + assertThat(beanFactory.getBeanDefinition(sessionRepositoryName).getDependsOn()) + .contains("flyway", "flywayInitializer"); + } + }); + } + + @Test + void sessionRepositoryBeansDependOnLiquibase() { + this.contextRunner.withConfiguration(AutoConfigurations.of(LiquibaseAutoConfiguration.class)) + .withPropertyValues("spring.session.store-type=jdbc", "spring.session.jdbc.initialize-schema=never") + .run((context) -> { + ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); + String[] sessionRepositoryNames = beanFactory + .getBeanNamesForType(JdbcIndexedSessionRepository.class); + assertThat(sessionRepositoryNames).isNotEmpty(); + for (String sessionRepositoryName : sessionRepositoryNames) { + assertThat(beanFactory.getBeanDefinition(sessionRepositoryName).getDependsOn()) + .contains("liquibase"); + } + }); + } + + @Configuration + static class SessionDataSourceConfiguration { + + @Bean + @SpringSessionDataSource + DataSource sessionDataSource() { + BasicDataSource dataSource = new BasicDataSource(); + dataSource.setDriverClassName("org.hsqldb.jdbcDriver"); + dataSource.setUrl("jdbc:hsqldb:mem:sessiondb"); + dataSource.setUsername("sa"); + return dataSource; + } + + @Bean + @Primary + DataSource mainDataSource() { + BasicDataSource dataSource = new BasicDataSource(); + dataSource.setDriverClassName("org.hsqldb.jdbcDriver"); + dataSource.setUrl("jdbc:hsqldb:mem:maindb"); + dataSource.setUsername("sa"); + return dataSource; + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationMongoTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationMongoTests.java index 982bbb8a7e21..78dae04d9352 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationMongoTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationMongoTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,22 @@ package org.springframework.boot.autoconfigure.session; +import java.time.Duration; + import org.junit.jupiter.api.Test; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; -import org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration; +import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; import org.springframework.session.data.mongo.MongoIndexedSessionRepository; import org.springframework.session.data.redis.RedisIndexedSessionRepository; import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository; @@ -38,16 +44,21 @@ * * @author Andy Wilkinson */ +@Testcontainers(disabledWithoutDocker = true) class SessionAutoConfigurationMongoTests extends AbstractSessionAutoConfigurationTests { + @Container + static final MongoDBContainer mongoDB = new MongoDBContainer(DockerImageNames.mongo()).withStartupAttempts(5) + .withStartupTimeout(Duration.ofMinutes(5)); + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(SessionAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class, MongoDataAutoConfiguration.class, + SessionAutoConfiguration.class)) + .withPropertyValues("spring.data.mongodb.uri=" + mongoDB.getReplicaSetUrl()); @Test void defaultConfig() { this.contextRunner.withPropertyValues("spring.session.store-type=mongodb") - .withConfiguration(AutoConfigurations.of(EmbeddedMongoAutoConfiguration.class, - MongoAutoConfiguration.class, MongoDataAutoConfiguration.class)) .run(validateSpringSessionUsesMongo("sessions")); } @@ -56,25 +67,35 @@ void defaultConfigWithUniqueStoreImplementation() { this.contextRunner .withClassLoader(new FilteredClassLoader(HazelcastIndexedSessionRepository.class, JdbcIndexedSessionRepository.class, RedisIndexedSessionRepository.class)) - .withConfiguration(AutoConfigurations.of(EmbeddedMongoAutoConfiguration.class, - MongoAutoConfiguration.class, MongoDataAutoConfiguration.class)) .run(validateSpringSessionUsesMongo("sessions")); } + @Test + void defaultConfigWithCustomTimeout() { + this.contextRunner.withPropertyValues("spring.session.store-type=mongodb", "spring.session.timeout=1m") + .run(validateSpringSessionUsesMongo("sessions", Duration.ofMinutes(1))); + } + @Test void mongoSessionStoreWithCustomizations() { this.contextRunner - .withConfiguration(AutoConfigurations.of(EmbeddedMongoAutoConfiguration.class, - MongoAutoConfiguration.class, MongoDataAutoConfiguration.class)) .withPropertyValues("spring.session.store-type=mongodb", "spring.session.mongodb.collection-name=foo") .run(validateSpringSessionUsesMongo("foo")); } private ContextConsumer validateSpringSessionUsesMongo(String collectionName) { + return validateSpringSessionUsesMongo(collectionName, + new ServerProperties().getServlet().getSession().getTimeout()); + } + + private ContextConsumer validateSpringSessionUsesMongo(String collectionName, + Duration timeout) { return (context) -> { MongoIndexedSessionRepository repository = validateSessionRepository(context, MongoIndexedSessionRepository.class); assertThat(repository).hasFieldOrPropertyWithValue("collectionName", collectionName); + assertThat(repository).hasFieldOrPropertyWithValue("maxInactiveIntervalInSeconds", + (int) timeout.getSeconds()); }; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationRedisTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationRedisTests.java index 7fc3a3ac697f..418fd7f88bb6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationRedisTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationRedisTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.session.RedisSessionConfiguration.SpringBootRedisHttpSessionConfiguration; +import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; import org.springframework.boot.test.context.runner.ContextConsumer; @@ -56,7 +57,7 @@ class SessionAutoConfigurationRedisTests extends AbstractSessionAutoConfiguratio @Container public static RedisContainer redis = new RedisContainer().withStartupAttempts(5) - .withStartupTimeout(Duration.ofMinutes(2)); + .withStartupTimeout(Duration.ofMinutes(10)); protected final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(SessionAutoConfiguration.class)); @@ -64,7 +65,7 @@ class SessionAutoConfigurationRedisTests extends AbstractSessionAutoConfiguratio @Test void defaultConfig() { this.contextRunner - .withPropertyValues("spring.session.store-type=redis", + .withPropertyValues("spring.session.store-type=redis", "spring.redis.host=" + redis.getHost(), "spring.redis.port=" + redis.getFirstMappedPort()) .withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class)) .run(validateSpringSessionUsesRedis("spring:session:event:0:created:", FlushMode.ON_SAVE, @@ -77,17 +78,30 @@ void defaultConfigWithUniqueStoreImplementation() { .withClassLoader(new FilteredClassLoader(HazelcastIndexedSessionRepository.class, JdbcIndexedSessionRepository.class, MongoIndexedSessionRepository.class)) .withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class)) - .withPropertyValues("spring.redis.port=" + redis.getFirstMappedPort()) + .withPropertyValues("spring.redis.host=" + redis.getHost(), + "spring.redis.port=" + redis.getFirstMappedPort()) .run(validateSpringSessionUsesRedis("spring:session:event:0:created:", FlushMode.ON_SAVE, SaveMode.ON_SET_ATTRIBUTE, "0 * * * * *")); } + @Test + void defaultConfigWithCustomTimeout() { + this.contextRunner + .withPropertyValues("spring.session.store-type=redis", "spring.redis.host=" + redis.getHost(), + "spring.redis.port=" + redis.getFirstMappedPort(), "spring.session.timeout=1m") + .withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class)).run((context) -> { + RedisIndexedSessionRepository repository = validateSessionRepository(context, + RedisIndexedSessionRepository.class); + assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval", 60); + }); + } + @Test void redisSessionStoreWithCustomizations() { this.contextRunner.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class)) .withPropertyValues("spring.session.store-type=redis", "spring.session.redis.namespace=foo", "spring.session.redis.flush-mode=immediate", "spring.session.redis.save-mode=on-get-attribute", - "spring.session.redis.cleanup-cron=0 0 12 * * *", + "spring.session.redis.cleanup-cron=0 0 12 * * *", "spring.redis.host=" + redis.getHost(), "spring.redis.port=" + redis.getFirstMappedPort()) .run(validateSpringSessionUsesRedis("foo:event:0:created:", FlushMode.IMMEDIATE, SaveMode.ON_GET_ATTRIBUTE, "0 0 12 * * *")); @@ -97,14 +111,14 @@ void redisSessionStoreWithCustomizations() { void redisSessionWithConfigureActionNone() { this.contextRunner.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class)) .withPropertyValues("spring.session.store-type=redis", "spring.session.redis.configure-action=none", - "spring.redis.port=" + redis.getFirstMappedPort()) + "spring.redis.host=" + redis.getHost(), "spring.redis.port=" + redis.getFirstMappedPort()) .run(validateStrategy(ConfigureRedisAction.NO_OP.getClass())); } @Test void redisSessionWithDefaultConfigureActionNone() { this.contextRunner.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class)) - .withPropertyValues("spring.session.store-type=redis", + .withPropertyValues("spring.session.store-type=redis", "spring.redis.host=" + redis.getHost(), "spring.redis.port=" + redis.getFirstMappedPort()) .run(validateStrategy(ConfigureNotifyKeyspaceEventsAction.class, entry("notify-keyspace-events", "gxE"))); @@ -114,7 +128,7 @@ void redisSessionWithDefaultConfigureActionNone() { void redisSessionWithCustomConfigureRedisActionBean() { this.contextRunner.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class)) .withUserConfiguration(MaxEntriesRedisAction.class) - .withPropertyValues("spring.session.store-type=redis", + .withPropertyValues("spring.session.store-type=redis", "spring.redis.host=" + redis.getHost(), "spring.redis.port=" + redis.getFirstMappedPort()) .run(validateStrategy(MaxEntriesRedisAction.class, entry("set-max-intset-entries", "1024"))); @@ -125,6 +139,8 @@ private ContextConsumer validateSpringSessionUs return (context) -> { RedisIndexedSessionRepository repository = validateSessionRepository(context, RedisIndexedSessionRepository.class); + assertThat(repository).hasFieldOrPropertyWithValue("defaultMaxInactiveInterval", + (int) new ServerProperties().getServlet().getSession().getTimeout().getSeconds()); assertThat(repository.getSessionCreatedChannelPrefix()).isEqualTo(sessionCreatedChannelPrefix); assertThat(repository).hasFieldOrPropertyWithValue("flushMode", flushMode); SpringBootRedisHttpSessionConfiguration configuration = context diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationTests.java index 10735e3961c2..25233f614497 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,13 @@ package org.springframework.boot.autoconfigure.session; -import java.time.Duration; import java.util.Collections; -import java.util.EnumSet; import javax.servlet.DispatcherType; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; +import org.mockito.InOrder; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.web.ServerProperties; @@ -31,6 +31,7 @@ import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; import org.springframework.session.MapSessionRepository; import org.springframework.session.SessionRepository; import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession; @@ -40,9 +41,10 @@ import org.springframework.session.web.http.HeaderHttpSessionIdResolver; import org.springframework.session.web.http.HttpSessionIdResolver; import org.springframework.session.web.http.SessionRepositoryFilter; -import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; /** @@ -62,7 +64,7 @@ class SessionAutoConfigurationTests extends AbstractSessionAutoConfigurationTest void contextFailsIfMultipleStoresAreAvailable() { this.contextRunner.run((context) -> { assertThat(context).hasFailed(); - assertThat(context).getFailure().hasCauseInstanceOf(NonUniqueSessionRepositoryException.class); + assertThat(context).getFailure().hasRootCauseInstanceOf(NonUniqueSessionRepositoryException.class); assertThat(context).getFailure() .hasMessageContaining("Multiple session repository candidates are available"); }); @@ -93,31 +95,13 @@ void backOffIfSessionRepositoryIsPresent() { }); } - @Test - void autoConfigWhenSpringSessionTimeoutIsSetShouldUseThat() { - this.contextRunner - .withUserConfiguration(ServerPropertiesConfiguration.class, SessionRepositoryConfiguration.class) - .withPropertyValues("server.servlet.session.timeout=1", "spring.session.timeout=3") - .run((context) -> assertThat(context.getBean(SessionProperties.class).getTimeout()) - .isEqualTo(Duration.ofSeconds(3))); - } - - @Test - void autoConfigWhenSpringSessionTimeoutIsNotSetShouldUseServerSessionTimeout() { - this.contextRunner - .withUserConfiguration(ServerPropertiesConfiguration.class, SessionRepositoryConfiguration.class) - .withPropertyValues("server.servlet.session.timeout=3") - .run((context) -> assertThat(context.getBean(SessionProperties.class).getTimeout()) - .isEqualTo(Duration.ofSeconds(3))); - } - - @SuppressWarnings("unchecked") @Test void filterIsRegisteredWithAsyncErrorAndRequestDispatcherTypes() { this.contextRunner.withUserConfiguration(SessionRepositoryConfiguration.class).run((context) -> { FilterRegistrationBean registration = context.getBean(FilterRegistrationBean.class); assertThat(registration.getFilter()).isSameAs(context.getBean(SessionRepositoryFilter.class)); - assertThat((EnumSet) ReflectionTestUtils.getField(registration, "dispatcherTypes")) + assertThat(registration) + .extracting("dispatcherTypes", InstanceOfAssertFactories.iterable(DispatcherType.class)) .containsOnly(DispatcherType.ASYNC, DispatcherType.ERROR, DispatcherType.REQUEST); }); } @@ -131,17 +115,28 @@ void filterOrderCanBeCustomizedWithCustomStore() { }); } - @SuppressWarnings("unchecked") @Test void filterDispatcherTypesCanBeCustomized() { this.contextRunner.withUserConfiguration(SessionRepositoryConfiguration.class) .withPropertyValues("spring.session.servlet.filter-dispatcher-types=error, request").run((context) -> { FilterRegistrationBean registration = context.getBean(FilterRegistrationBean.class); - assertThat((EnumSet) ReflectionTestUtils.getField(registration, "dispatcherTypes")) + assertThat(registration) + .extracting("dispatcherTypes", InstanceOfAssertFactories.iterable(DispatcherType.class)) .containsOnly(DispatcherType.ERROR, DispatcherType.REQUEST); }); } + @Test + void emptyFilterDispatcherTypesDoNotThrowException() { + this.contextRunner.withUserConfiguration(SessionRepositoryConfiguration.class) + .withPropertyValues("spring.session.servlet.filter-dispatcher-types=").run((context) -> { + FilterRegistrationBean registration = context.getBean(FilterRegistrationBean.class); + assertThat(registration) + .extracting("dispatcherTypes", InstanceOfAssertFactories.iterable(DispatcherType.class)) + .isEmpty(); + }); + } + @Test void sessionCookieConfigurationIsAppliedToAutoConfiguredCookieSerializer() { this.contextRunner.withUserConfiguration(SessionRepositoryConfiguration.class) @@ -165,11 +160,8 @@ void autoConfiguredCookieSerializerIsUsedBySessionRepositoryFilter() { this.contextRunner.withUserConfiguration(SessionRepositoryConfiguration.class) .withPropertyValues("server.port=0").run((context) -> { SessionRepositoryFilter filter = context.getBean(SessionRepositoryFilter.class); - CookieHttpSessionIdResolver sessionIdResolver = (CookieHttpSessionIdResolver) ReflectionTestUtils - .getField(filter, "httpSessionIdResolver"); - DefaultCookieSerializer cookieSerializer = (DefaultCookieSerializer) ReflectionTestUtils - .getField(sessionIdResolver, "cookieSerializer"); - assertThat(cookieSerializer).isSameAs(context.getBean(DefaultCookieSerializer.class)); + assertThat(filter).extracting("httpSessionIdResolver").extracting("cookieSerializer") + .isSameAs(context.getBean(DefaultCookieSerializer.class)); }); } @@ -208,6 +200,16 @@ void autoConfiguredCookieSerializerIsConfiguredWithRememberMeRequestAttribute() }); } + @Test + void cookieSerializerCustomization() { + this.contextRunner.withBean(CookieSerializerCustomization.class).run((context) -> { + CookieSerializerCustomization customization = context.getBean(CookieSerializerCustomization.class); + InOrder inOrder = inOrder(customization.customizer1, customization.customizer2); + inOrder.verify(customization.customizer1).customize(any()); + inOrder.verify(customization.customizer2).customize(any()); + }); + } + @Configuration(proxyBeanMethods = false) @EnableSpringHttpSession static class SessionRepositoryConfiguration { @@ -279,4 +281,26 @@ SpringSessionRememberMeServices rememberMeServices() { } + @Configuration(proxyBeanMethods = false) + @EnableSpringHttpSession + static class CookieSerializerCustomization extends SessionRepositoryConfiguration { + + private final DefaultCookieSerializerCustomizer customizer1 = mock(DefaultCookieSerializerCustomizer.class); + + private final DefaultCookieSerializerCustomizer customizer2 = mock(DefaultCookieSerializerCustomizer.class); + + @Bean + @Order(1) + DefaultCookieSerializerCustomizer customizer1() { + return this.customizer1; + } + + @Bean + @Order(2) + DefaultCookieSerializerCustomizer customizer2() { + return this.customizer2; + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionPropertiesTests.java new file mode 100644 index 000000000000..4dc24f60aa47 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionPropertiesTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.session; + +import java.time.Duration; +import java.util.function.Supplier; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link SessionProperties}. + * + * @author Stephane Nicoll + */ +class SessionPropertiesTests { + + @Test + @SuppressWarnings("unchecked") + void determineTimeoutWithTimeoutIgnoreFallback() { + SessionProperties properties = new SessionProperties(); + properties.setTimeout(Duration.ofMinutes(1)); + Supplier fallback = mock(Supplier.class); + assertThat(properties.determineTimeout(fallback)).isEqualTo(Duration.ofMinutes(1)); + then(fallback).shouldHaveNoInteractions(); + } + + @Test + void determineTimeoutWithNoTimeoutUseFallback() { + SessionProperties properties = new SessionProperties(); + properties.setTimeout(null); + Duration fallback = Duration.ofMinutes(2); + assertThat(properties.determineTimeout(() -> fallback)).isSameAs(fallback); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/sql/init/SqlInitializationAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/sql/init/SqlInitializationAutoConfigurationTests.java new file mode 100644 index 000000000000..857d763d37a6 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/sql/init/SqlInitializationAutoConfigurationTests.java @@ -0,0 +1,199 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.sql.init; + +import java.nio.charset.Charset; +import java.util.List; + +import javax.sql.DataSource; + +import io.r2dbc.spi.ConnectionFactory; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; +import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer; +import org.springframework.boot.logging.LogLevel; +import org.springframework.boot.r2dbc.init.R2dbcScriptDatabaseInitializer; +import org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer; +import org.springframework.boot.sql.init.DatabaseInitializationSettings; +import org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitialization; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; +import org.springframework.jdbc.datasource.init.DatabasePopulator; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SqlInitializationAutoConfiguration}. + * + * @author Andy Wilkinson + */ +class SqlInitializationAutoConfigurationTests { + + private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(SqlInitializationAutoConfiguration.class)).withPropertyValues( + "spring.datasource.generate-unique-name:true", "spring.r2dbc.generate-unique-name:true"); + + @Test + void whenNoDataSourceOrConnectionFactoryIsAvailableThenAutoConfigurationBacksOff() { + this.contextRunner + .run((context) -> assertThat(context).doesNotHaveBean(AbstractScriptDatabaseInitializer.class)); + } + + @Test + void whenConnectionFactoryIsAvailableThenR2dbcInitializerIsAutoConfigured() { + this.contextRunner.withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class)) + .run((context) -> assertThat(context).hasSingleBean(R2dbcScriptDatabaseInitializer.class)); + } + + @Test + @Deprecated + void whenConnectionFactoryIsAvailableAndInitializationIsDisabledThenInitializerIsNotAutoConfigured() { + this.contextRunner.withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class)) + .withPropertyValues("spring.sql.init.enabled:false") + .run((context) -> assertThat(context).doesNotHaveBean(AbstractScriptDatabaseInitializer.class)); + } + + @Test + void whenConnectionFactoryIsAvailableAndModeIsNeverThenInitializerIsNotAutoConfigured() { + this.contextRunner.withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class)) + .withInitializer(new ConditionEvaluationReportLoggingListener(LogLevel.INFO)) + .withPropertyValues("spring.sql.init.mode:never") + .run((context) -> assertThat(context).doesNotHaveBean(AbstractScriptDatabaseInitializer.class)); + } + + @Test + void whenDataSourceIsAvailableThenDataSourceInitializerIsAutoConfigured() { + this.contextRunner.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) + .run((context) -> assertThat(context).hasSingleBean(DataSourceScriptDatabaseInitializer.class)); + } + + @Test + @Deprecated + void whenDataSourceIsAvailableAndInitializationIsDisabledThenInitializerIsNotAutoConfigured() { + this.contextRunner.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) + .withPropertyValues("spring.sql.init.enabled:false") + .run((context) -> assertThat(context).doesNotHaveBean(AbstractScriptDatabaseInitializer.class)); + } + + @Test + void whenDataSourceIsAvailableAndModeIsNeverThenThenInitializerIsNotAutoConfigured() { + this.contextRunner.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) + .withPropertyValues("spring.sql.init.mode:never") + .run((context) -> assertThat(context).doesNotHaveBean(AbstractScriptDatabaseInitializer.class)); + } + + @Test + void whenDataSourceAndConnectionFactoryAreAvailableThenOnlyR2dbcInitializerIsAutoConfigured() { + this.contextRunner.withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class)) + .withUserConfiguration(DataSourceAutoConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(ConnectionFactory.class) + .hasSingleBean(DataSource.class).hasSingleBean(R2dbcScriptDatabaseInitializer.class) + .doesNotHaveBean(DataSourceScriptDatabaseInitializer.class)); + } + + @Test + void whenAnInitializerIsDefinedThenInitializerIsNotAutoConfigured() { + this.contextRunner.withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class)) + .withUserConfiguration(DataSourceAutoConfiguration.class, DatabaseInitializerConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(AbstractScriptDatabaseInitializer.class) + .hasBean("customInitializer")); + } + + @Test + void whenBeanIsAnnotatedAsDependingOnDatabaseInitializationThenItDependsOnR2dbcScriptDatabaseInitializer() { + this.contextRunner.withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class)) + .withUserConfiguration(DependsOnInitializedDatabaseConfiguration.class).run((context) -> { + BeanDefinition beanDefinition = context.getBeanFactory().getBeanDefinition( + "sqlInitializationAutoConfigurationTests.DependsOnInitializedDatabaseConfiguration"); + assertThat(beanDefinition.getDependsOn()) + .containsExactlyInAnyOrder("r2dbcScriptDatabaseInitializer"); + }); + } + + @Test + void whenBeanIsAnnotatedAsDependingOnDatabaseInitializationThenItDependsOnDataSourceScriptDatabaseInitializer() { + this.contextRunner.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) + .withUserConfiguration(DependsOnInitializedDatabaseConfiguration.class).run((context) -> { + BeanDefinition beanDefinition = context.getBeanFactory().getBeanDefinition( + "sqlInitializationAutoConfigurationTests.DependsOnInitializedDatabaseConfiguration"); + assertThat(beanDefinition.getDependsOn()) + .containsExactlyInAnyOrder("dataSourceScriptDatabaseInitializer"); + }); + } + + @Test + void whenADataSourceIsAvailableAndSpringJdbcIsNotThenAutoConfigurationBacksOff() { + this.contextRunner.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) + .withClassLoader(new FilteredClassLoader(DatabasePopulator.class)).run((context) -> { + assertThat(context).hasSingleBean(DataSource.class); + assertThat(context).doesNotHaveBean(AbstractScriptDatabaseInitializer.class); + }); + } + + @Test + void whenAConnectionFactoryIsAvailableAndSpringR2dbcIsNotThenAutoConfigurationBacksOff() { + this.contextRunner.withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class)) + .withClassLoader( + new FilteredClassLoader(org.springframework.r2dbc.connection.init.DatabasePopulator.class)) + .run((context) -> { + assertThat(context).hasSingleBean(ConnectionFactory.class); + assertThat(context).doesNotHaveBean(AbstractScriptDatabaseInitializer.class); + }); + } + + @Configuration(proxyBeanMethods = false) + static class DatabaseInitializerConfiguration { + + @Bean + AbstractScriptDatabaseInitializer customInitializer() { + return new AbstractScriptDatabaseInitializer(new DatabaseInitializationSettings()) { + + @Override + protected void runScripts(List resources, boolean continueOnError, String separator, + Charset encoding) { + // No-op + } + + @Override + protected boolean isEmbeddedDatabase() { + return true; + } + + }; + } + + } + + @Configuration(proxyBeanMethods = false) + @DependsOnDatabaseInitialization + static class DependsOnInitializedDatabaseConfiguration { + + DependsOnInitializedDatabaseConfiguration() { + + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/ScheduledBeanLazyInitializationExcludeFilterTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/ScheduledBeanLazyInitializationExcludeFilterTests.java new file mode 100644 index 000000000000..f53b86497141 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/ScheduledBeanLazyInitializationExcludeFilterTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.task; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.scheduling.annotation.Schedules; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ScheduledBeanLazyInitializationExcludeFilter}. + * + * @author Stephane Nicoll + */ +class ScheduledBeanLazyInitializationExcludeFilterTests { + + private final ScheduledBeanLazyInitializationExcludeFilter filter = new ScheduledBeanLazyInitializationExcludeFilter(); + + @Test + void beanWithScheduledMethodIsDetected() { + assertThat(isExcluded(TestBean.class)).isTrue(); + } + + @Test + void beanWithSchedulesMethodIsDetected() { + assertThat(isExcluded(AnotherTestBean.class)).isTrue(); + } + + @Test + void beanWithoutScheduledMethodIsDetected() { + assertThat(isExcluded(ScheduledBeanLazyInitializationExcludeFilterTests.class)).isFalse(); + } + + private boolean isExcluded(Class type) { + return this.filter.isExcluded("test", new RootBeanDefinition(type), type); + } + + private static class TestBean { + + @Scheduled + void doStuff() { + } + + } + + private static class AnotherTestBean { + + @Schedules({ @Scheduled(fixedRate = 5000), @Scheduled(fixedRate = 2500) }) + void doStuff() { + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java index 8f75dd89be12..5b1c97250b97 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,13 +23,13 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.task.TaskExecutorBuilder; import org.springframework.boot.task.TaskExecutorCustomizer; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ContextConsumer; -import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -41,11 +41,10 @@ import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; -import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link TaskExecutionAutoConfiguration}. @@ -73,7 +72,7 @@ void taskExecutorBuilderShouldApplyCustomSettings() { assertThat(taskExecutor).hasFieldOrPropertyWithValue("allowCoreThreadTimeOut", true); assertThat(taskExecutor.getKeepAliveSeconds()).isEqualTo(5); assertThat(taskExecutor).hasFieldOrPropertyWithValue("waitForTasksToCompleteOnShutdown", true); - assertThat(taskExecutor).hasFieldOrPropertyWithValue("awaitTerminationSeconds", 30); + assertThat(taskExecutor).hasFieldOrPropertyWithValue("awaitTerminationMillis", 30000L); assertThat(taskExecutor.getThreadNamePrefix()).isEqualTo("mytest-"); })); } @@ -92,19 +91,18 @@ void taskExecutorBuilderShouldUseTaskDecorator() { this.contextRunner.withUserConfiguration(TaskDecoratorConfig.class).run((context) -> { assertThat(context).hasSingleBean(TaskExecutorBuilder.class); ThreadPoolTaskExecutor executor = context.getBean(TaskExecutorBuilder.class).build(); - assertThat(ReflectionTestUtils.getField(executor, "taskDecorator")) - .isSameAs(context.getBean(TaskDecorator.class)); + assertThat(executor).extracting("taskDecorator").isSameAs(context.getBean(TaskDecorator.class)); }); } @Test - void taskExecutorAutoConfigured(CapturedOutput output) { + void taskExecutorAutoConfiguredIsLazy() { this.contextRunner.run((context) -> { - assertThat(output).doesNotContain("Initializing ExecutorService"); - assertThat(context).hasSingleBean(Executor.class); - assertThat(context).hasBean("applicationTaskExecutor"); + assertThat(context).hasSingleBean(Executor.class).hasBean("applicationTaskExecutor"); + BeanDefinition beanDefinition = context.getSourceApplicationContext().getBeanFactory() + .getBeanDefinition("applicationTaskExecutor"); + assertThat(beanDefinition.isLazyInit()).isTrue(); assertThat(context).getBean("applicationTaskExecutor").isInstanceOf(ThreadPoolTaskExecutor.class); - assertThat(output).contains("Initializing ExecutorService"); }); } @@ -121,7 +119,7 @@ void taskExecutorBuilderShouldApplyCustomizer() { this.contextRunner.withUserConfiguration(TaskExecutorCustomizerConfig.class).run((context) -> { TaskExecutorCustomizer customizer = context.getBean(TaskExecutorCustomizer.class); ThreadPoolTaskExecutor executor = context.getBean(TaskExecutorBuilder.class).build(); - verify(customizer).customize(executor); + then(customizer).should().customize(executor); }); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfigurationTests.java index 3dd530e16c56..65257736d086 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,9 @@ package org.springframework.boot.autoconfigure.task; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; @@ -23,8 +26,10 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import org.awaitility.Awaitility; import org.junit.jupiter.api.Test; +import org.springframework.boot.LazyInitializationBeanFactoryPostProcessor; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.task.TaskSchedulerCustomizer; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -47,7 +52,7 @@ */ class TaskSchedulingAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withUserConfiguration(TestConfiguration.class) .withConfiguration(AutoConfigurations.of(TaskSchedulingAutoConfiguration.class)); @@ -68,7 +73,7 @@ void enableSchedulingWithNoTaskExecutorAutoConfiguresOne() { TestBean bean = context.getBean(TestBean.class); assertThat(bean.latch.await(30, TimeUnit.SECONDS)).isTrue(); assertThat(taskExecutor).hasFieldOrPropertyWithValue("waitForTasksToCompleteOnShutdown", true); - assertThat(taskExecutor).hasFieldOrPropertyWithValue("awaitTerminationSeconds", 30); + assertThat(taskExecutor).hasFieldOrPropertyWithValue("awaitTerminationMillis", 30000L); assertThat(bean.threadNames).allMatch((name) -> name.contains("scheduling-test-")); }); } @@ -121,6 +126,22 @@ void enableSchedulingWithConfigurerBacksOff() { }); } + @Test + void enableSchedulingWithLazyInitializationInvokeScheduledMethods() { + List threadNames = new ArrayList<>(); + new ApplicationContextRunner() + .withInitializer((context) -> context + .addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor())) + .withPropertyValues("spring.task.scheduling.thread-name-prefix=scheduling-test-") + .withBean(LazyTestBean.class, () -> new LazyTestBean(threadNames)) + .withUserConfiguration(SchedulingConfiguration.class) + .withConfiguration(AutoConfigurations.of(TaskSchedulingAutoConfiguration.class)).run((context) -> { + // No lazy lookup. + Awaitility.waitAtMost(Duration.ofSeconds(3)).until(() -> !threadNames.isEmpty()); + assertThat(threadNames).allMatch((name) -> name.contains("scheduling-test-")); + }); + } + @Configuration(proxyBeanMethods = false) @EnableScheduling static class SchedulingConfiguration { @@ -193,6 +214,21 @@ void accumulate() { } + static class LazyTestBean { + + private final List threadNames; + + LazyTestBean(List threadNames) { + this.threadNames = threadNames; + } + + @Scheduled(fixedRate = 2000) + void accumulate() { + this.threadNames.add(Thread.currentThread().getName()); + } + + } + static class TestTaskScheduler extends ThreadPoolTaskScheduler { TestTaskScheduler() { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/template/TemplateAvailabilityProvidersTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/template/TemplateAvailabilityProvidersTests.java index 0dc8ac0f4e95..903e04463f63 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/template/TemplateAvailabilityProvidersTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/template/TemplateAvailabilityProvidersTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,8 +21,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.context.ApplicationContext; import org.springframework.core.io.ResourceLoader; @@ -31,15 +32,16 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; /** * Tests for {@link TemplateAvailabilityProviders}. * * @author Phillip Webb */ +@ExtendWith(MockitoExtension.class) class TemplateAvailabilityProvidersTests { private TemplateAvailabilityProviders providers; @@ -58,7 +60,6 @@ class TemplateAvailabilityProvidersTests { @BeforeEach void setup() { - MockitoAnnotations.initMocks(this); this.providers = new TemplateAvailabilityProviders(Collections.singleton(this.provider)); } @@ -75,7 +76,7 @@ void createWhenUsingApplicationContextShouldLoadProviders() { given(applicationContext.getClassLoader()).willReturn(this.classLoader); TemplateAvailabilityProviders providers = new TemplateAvailabilityProviders(applicationContext); assertThat(providers.getProviders()).isNotEmpty(); - verify(applicationContext).getClassLoader(); + then(applicationContext).should().getClassLoader(); } @Test @@ -144,7 +145,8 @@ void getProviderWhenNoneMatchShouldReturnNull() { TemplateAvailabilityProvider found = this.providers.getProvider(this.view, this.environment, this.classLoader, this.resourceLoader); assertThat(found).isNull(); - verify(this.provider).isTemplateAvailable(this.view, this.environment, this.classLoader, this.resourceLoader); + then(this.provider).should().isTemplateAvailable(this.view, this.environment, this.classLoader, + this.resourceLoader); } @Test @@ -163,7 +165,7 @@ void getProviderShouldCacheMatchResult() { .willReturn(true); this.providers.getProvider(this.view, this.environment, this.classLoader, this.resourceLoader); this.providers.getProvider(this.view, this.environment, this.classLoader, this.resourceLoader); - verify(this.provider, times(1)).isTemplateAvailable(this.view, this.environment, this.classLoader, + then(this.provider).should().isTemplateAvailable(this.view, this.environment, this.classLoader, this.resourceLoader); } @@ -171,7 +173,7 @@ void getProviderShouldCacheMatchResult() { void getProviderShouldCacheNoMatchResult() { this.providers.getProvider(this.view, this.environment, this.classLoader, this.resourceLoader); this.providers.getProvider(this.view, this.environment, this.classLoader, this.resourceLoader); - verify(this.provider, times(1)).isTemplateAvailable(this.view, this.environment, this.classLoader, + then(this.provider).should().isTemplateAvailable(this.view, this.environment, this.classLoader, this.resourceLoader); } @@ -182,7 +184,7 @@ void getProviderWhenCacheDisabledShouldNotUseCache() { this.environment.setProperty("spring.template.provider.cache", "false"); this.providers.getProvider(this.view, this.environment, this.classLoader, this.resourceLoader); this.providers.getProvider(this.view, this.environment, this.classLoader, this.resourceLoader); - verify(this.provider, times(2)).isTemplateAvailable(this.view, this.environment, this.classLoader, + then(this.provider).should(times(2)).isTemplateAvailable(this.view, this.environment, this.classLoader, this.resourceLoader); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/thymeleaf/ThymeleafReactiveAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/thymeleaf/ThymeleafReactiveAutoConfigurationTests.java index 6b7ca6923989..c4e2f8d2c384 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/thymeleaf/ThymeleafReactiveAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/thymeleaf/ThymeleafReactiveAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -63,7 +63,7 @@ class ThymeleafReactiveAutoConfigurationTests { private final BuildOutput buildOutput = new BuildOutput(getClass()); - private ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() + private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(ThymeleafAutoConfiguration.class)); @Test @@ -87,11 +87,24 @@ void overrideCharacterEncoding() { }); } + @Test + void defaultMediaTypes() { + this.contextRunner.run( + (context) -> assertThat(context.getBean(ThymeleafReactiveViewResolver.class).getSupportedMediaTypes()) + .containsExactly(MediaType.TEXT_HTML, MediaType.APPLICATION_XHTML_XML, + MediaType.APPLICATION_XML, MediaType.TEXT_XML, MediaType.APPLICATION_RSS_XML, + MediaType.APPLICATION_ATOM_XML, new MediaType("application", "javascript"), + new MediaType("application", "ecmascript"), new MediaType("text", "javascript"), + new MediaType("text", "ecmascript"), MediaType.APPLICATION_JSON, + new MediaType("text", "css"), MediaType.TEXT_PLAIN, MediaType.TEXT_EVENT_STREAM) + .satisfies(System.out::println)); + } + @Test void overrideMediaTypes() { this.contextRunner.withPropertyValues("spring.thymeleaf.reactive.media-types:text/html,text/plain").run( (context) -> assertThat(context.getBean(ThymeleafReactiveViewResolver.class).getSupportedMediaTypes()) - .contains(MediaType.TEXT_HTML, MediaType.TEXT_PLAIN)); + .containsExactly(MediaType.TEXT_HTML, MediaType.TEXT_PLAIN)); } @Test diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/TransactionAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/TransactionAutoConfigurationTests.java index ba889f9762cc..a4bb93a15734 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/TransactionAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/TransactionAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.boot.autoconfigure.transaction; +import java.util.UUID; + import javax.sql.DataSource; import org.junit.jupiter.api.Test; @@ -81,7 +83,7 @@ void whenThereAreBothReactiveAndPlatformTransactionManagersATemplateAndAnOperato DataSourceTransactionManagerAutoConfiguration.class)) .withUserConfiguration(SinglePlatformTransactionManagerConfiguration.class, SingleReactiveTransactionManagerConfiguration.class) - .run((context) -> { + .withPropertyValues("spring.datasource.url:jdbc:h2:mem:" + UUID.randomUUID()).run((context) -> { PlatformTransactionManager platformTransactionManager = context .getBean(PlatformTransactionManager.class); TransactionTemplate transactionTemplate = context.getBean(TransactionTemplate.class); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/jta/JtaAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/jta/JtaAutoConfigurationTests.java index 06573d140731..f25bcdcf70be 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/jta/JtaAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/jta/JtaAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,16 +17,19 @@ package org.springframework.boot.autoconfigure.transaction.jta; import java.io.File; -import java.io.IOException; -import java.net.InetAddress; -import java.net.UnknownHostException; import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; import javax.jms.ConnectionFactory; import javax.jms.TemporaryQueue; import javax.jms.XAConnection; import javax.jms.XAConnectionFactory; import javax.jms.XASession; +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; import javax.sql.DataSource; import javax.sql.XADataSource; import javax.transaction.TransactionManager; @@ -38,7 +41,19 @@ import com.atomikos.jms.AtomikosConnectionFactoryBean; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.osjava.sj.loader.JndiLoader; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration; @@ -47,15 +62,13 @@ import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean; import org.springframework.boot.jta.atomikos.AtomikosDependsOnBeanFactoryPostProcessor; import org.springframework.boot.jta.atomikos.AtomikosProperties; -import org.springframework.boot.jta.bitronix.BitronixDependentBeanFactoryPostProcessor; -import org.springframework.boot.jta.bitronix.PoolingConnectionFactoryBean; -import org.springframework.boot.jta.bitronix.PoolingDataSourceBean; import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.boot.testsupport.BuildOutput; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.jta.JtaTransactionManager; +import org.springframework.transaction.jta.UserTransactionAdapter; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -73,6 +86,9 @@ */ class JtaAutoConfigurationTests { + private final File atomikosLogs = new File(new BuildOutput(JtaAutoConfigurationTests.class).getRootLocation(), + "atomikos-logs"); + private AnnotationConfigApplicationContext context; @AfterEach @@ -80,10 +96,35 @@ void closeContext() { if (this.context != null) { this.context.close(); } + + } + + @ParameterizedTest + @ExtendWith(JndiExtension.class) + @MethodSource("transactionManagerJndiEntries") + void transactionManagerFromJndi(JndiEntry jndiEntry, InitialContext initialContext) throws NamingException { + jndiEntry.register(initialContext); + this.context = new AnnotationConfigApplicationContext(JtaAutoConfiguration.class); + JtaTransactionManager transactionManager = this.context.getBean(JtaTransactionManager.class); + if (jndiEntry.value instanceof UserTransaction) { + assertThat(transactionManager.getUserTransaction()).isEqualTo(jndiEntry.value); + assertThat(transactionManager.getTransactionManager()).isNull(); + } + else { + assertThat(transactionManager.getUserTransaction()).isInstanceOf(UserTransactionAdapter.class); + assertThat(transactionManager.getTransactionManager()).isEqualTo(jndiEntry.value); + } + } + + static List transactionManagerJndiEntries() { + return Arrays.asList(Arguments.of(new JndiEntry("java:comp/UserTransaction", UserTransaction.class)), + Arguments.of(new JndiEntry("java:appserver/TransactionManager", TransactionManager.class)), + Arguments.of(new JndiEntry("java:pm/TransactionManager", TransactionManager.class)), + Arguments.of(new JndiEntry("java:/TransactionManager", TransactionManager.class))); } @Test - void customPlatformTransactionManager() { + void customTransactionManager() { this.context = new AnnotationConfigApplicationContext(CustomTransactionManagerConfig.class, JtaAutoConfiguration.class); assertThatExceptionOfType(NoSuchBeanDefinitionException.class) @@ -103,7 +144,10 @@ void disableJtaSupport() { @Test void atomikosSanityCheck() { - this.context = new AnnotationConfigApplicationContext(JtaProperties.class, AtomikosJtaConfiguration.class); + this.context = new AnnotationConfigApplicationContext(); + TestPropertyValues.of("spring.jta.log-dir:" + this.atomikosLogs).applyTo(this.context); + this.context.register(JtaProperties.class, AtomikosJtaConfiguration.class); + this.context.refresh(); this.context.getBean(AtomikosProperties.class); this.context.getBean(UserTransactionService.class); this.context.getBean(UserTransactionManager.class); @@ -115,35 +159,7 @@ void atomikosSanityCheck() { } @Test - void bitronixSanityCheck() { - this.context = new AnnotationConfigApplicationContext(JtaProperties.class, BitronixJtaConfiguration.class); - this.context.getBean(bitronix.tm.Configuration.class); - this.context.getBean(TransactionManager.class); - this.context.getBean(XADataSourceWrapper.class); - this.context.getBean(XAConnectionFactoryWrapper.class); - this.context.getBean(BitronixDependentBeanFactoryPostProcessor.class); - this.context.getBean(JtaTransactionManager.class); - } - - @Test - void defaultBitronixServerId() throws UnknownHostException { - this.context = new AnnotationConfigApplicationContext(BitronixJtaConfiguration.class); - String serverId = this.context.getBean(bitronix.tm.Configuration.class).getServerId(); - assertThat(serverId).isEqualTo(InetAddress.getLocalHost().getHostAddress()); - } - - @Test - void customBitronixServerId() { - this.context = new AnnotationConfigApplicationContext(); - TestPropertyValues.of("spring.jta.transactionManagerId:custom").applyTo(this.context); - this.context.register(BitronixJtaConfiguration.class); - this.context.refresh(); - String serverId = this.context.getBean(bitronix.tm.Configuration.class).getServerId(); - assertThat(serverId).isEqualTo("custom"); - } - - @Test - void defaultAtomikosTransactionManagerName(@TempDir Path dir) throws IOException { + void defaultAtomikosTransactionManagerName(@TempDir Path dir) { this.context = new AnnotationConfigApplicationContext(); File logs = new File(dir.toFile(), "jta"); TestPropertyValues.of("spring.jta.logDir:" + logs.getAbsolutePath()).applyTo(this.context); @@ -158,7 +174,8 @@ void defaultAtomikosTransactionManagerName(@TempDir Path dir) throws IOException void atomikosConnectionFactoryPoolConfiguration() { this.context = new AnnotationConfigApplicationContext(); TestPropertyValues.of("spring.jta.atomikos.connectionfactory.minPoolSize:5", - "spring.jta.atomikos.connectionfactory.maxPoolSize:10").applyTo(this.context); + "spring.jta.atomikos.connectionfactory.maxPoolSize:10", "spring.jta.log-dir:" + this.atomikosLogs) + .applyTo(this.context); this.context.register(AtomikosJtaConfiguration.class, PoolConfiguration.class); this.context.refresh(); AtomikosConnectionFactoryBean connectionFactory = this.context.getBean(AtomikosConnectionFactoryBean.class); @@ -166,23 +183,11 @@ void atomikosConnectionFactoryPoolConfiguration() { assertThat(connectionFactory.getMaxPoolSize()).isEqualTo(10); } - @Test - void bitronixConnectionFactoryPoolConfiguration() { - this.context = new AnnotationConfigApplicationContext(); - TestPropertyValues.of("spring.jta.bitronix.connectionfactory.minPoolSize:5", - "spring.jta.bitronix.connectionfactory.maxPoolSize:10").applyTo(this.context); - this.context.register(BitronixJtaConfiguration.class, PoolConfiguration.class); - this.context.refresh(); - PoolingConnectionFactoryBean connectionFactory = this.context.getBean(PoolingConnectionFactoryBean.class); - assertThat(connectionFactory.getMinPoolSize()).isEqualTo(5); - assertThat(connectionFactory.getMaxPoolSize()).isEqualTo(10); - } - @Test void atomikosDataSourcePoolConfiguration() { this.context = new AnnotationConfigApplicationContext(); - TestPropertyValues - .of("spring.jta.atomikos.datasource.minPoolSize:5", "spring.jta.atomikos.datasource.maxPoolSize:10") + TestPropertyValues.of("spring.jta.atomikos.datasource.minPoolSize:5", + "spring.jta.atomikos.datasource.maxPoolSize:10", "spring.jta.log-dir:" + this.atomikosLogs) .applyTo(this.context); this.context.register(AtomikosJtaConfiguration.class, PoolConfiguration.class); this.context.refresh(); @@ -191,24 +196,11 @@ void atomikosDataSourcePoolConfiguration() { assertThat(dataSource.getMaxPoolSize()).isEqualTo(10); } - @Test - void bitronixDataSourcePoolConfiguration() { - this.context = new AnnotationConfigApplicationContext(); - TestPropertyValues - .of("spring.jta.bitronix.datasource.minPoolSize:5", "spring.jta.bitronix.datasource.maxPoolSize:10") - .applyTo(this.context); - this.context.register(BitronixJtaConfiguration.class, PoolConfiguration.class); - this.context.refresh(); - PoolingDataSourceBean dataSource = this.context.getBean(PoolingDataSourceBean.class); - assertThat(dataSource.getMinPoolSize()).isEqualTo(5); - assertThat(dataSource.getMaxPoolSize()).isEqualTo(10); - } - @Test void atomikosCustomizeJtaTransactionManagerUsingProperties() { this.context = new AnnotationConfigApplicationContext(); - TestPropertyValues - .of("spring.transaction.default-timeout:30", "spring.transaction.rollback-on-commit-failure:true") + TestPropertyValues.of("spring.transaction.default-timeout:30", + "spring.transaction.rollback-on-commit-failure:true", "spring.jta.log-dir:" + this.atomikosLogs) .applyTo(this.context); this.context.register(AtomikosJtaConfiguration.class, TransactionAutoConfiguration.class); this.context.refresh(); @@ -217,25 +209,12 @@ void atomikosCustomizeJtaTransactionManagerUsingProperties() { assertThat(transactionManager.isRollbackOnCommitFailure()).isTrue(); } - @Test - void bitronixCustomizeJtaTransactionManagerUsingProperties() { - this.context = new AnnotationConfigApplicationContext(); - TestPropertyValues - .of("spring.transaction.default-timeout:30", "spring.transaction.rollback-on-commit-failure:true") - .applyTo(this.context); - this.context.register(BitronixJtaConfiguration.class, TransactionAutoConfiguration.class); - this.context.refresh(); - JtaTransactionManager transactionManager = this.context.getBean(JtaTransactionManager.class); - assertThat(transactionManager.getDefaultTimeout()).isEqualTo(30); - assertThat(transactionManager.isRollbackOnCommitFailure()).isTrue(); - } - @Configuration(proxyBeanMethods = false) static class CustomTransactionManagerConfig { @Bean - PlatformTransactionManager transactionManager() { - return mock(PlatformTransactionManager.class); + org.springframework.transaction.TransactionManager testTransactionManager() { + return mock(org.springframework.transaction.TransactionManager.class); } } @@ -265,4 +244,79 @@ DataSource pooledDataSource(XADataSourceWrapper wrapper) throws Exception { } + private static final class JndiEntry { + + private final String name; + + private final Class type; + + private final Object value; + + private JndiEntry(String name, Class type) { + this.name = name; + this.type = type; + this.value = mock(type); + } + + private void register(InitialContext initialContext) throws NamingException { + String[] components = this.name.split("/"); + String subcontextName = components[0]; + String entryName = components[1]; + Context javaComp = initialContext.createSubcontext(subcontextName); + JndiLoader loader = new JndiLoader(initialContext.getEnvironment()); + Properties properties = new Properties(); + properties.setProperty(entryName + "/type", this.type.getName()); + properties.put(entryName + "/valueToConvert", this.value); + loader.load(properties, javaComp); + } + + @Override + public String toString() { + return this.name; + } + + } + + private static final class JndiExtension implements BeforeEachCallback, AfterEachCallback, ParameterResolver { + + @Override + public void beforeEach(ExtensionContext context) throws Exception { + Namespace namespace = Namespace.create(getClass(), context.getUniqueId()); + context.getStore(namespace).getOrComputeIfAbsent(InitialContext.class, (k) -> createInitialContext(), + InitialContext.class); + } + + private InitialContext createInitialContext() { + try { + return new InitialContext(); + } + catch (Exception ex) { + throw new RuntimeException(); + } + } + + @Override + public void afterEach(ExtensionContext context) throws Exception { + Namespace namespace = Namespace.create(getClass(), context.getUniqueId()); + InitialContext initialContext = context.getStore(namespace).remove(InitialContext.class, + InitialContext.class); + initialContext.removeFromEnvironment("org.osjava.sj.jndi.ignoreClose"); + initialContext.close(); + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return InitialContext.class.isAssignableFrom(parameterContext.getParameter().getType()); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + Namespace namespace = Namespace.create(getClass(), extensionContext.getUniqueId()); + return extensionContext.getStore(namespace).get(InitialContext.class); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfigurationTests.java index fcc05056b446..30ce1a4bddb2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,13 +24,16 @@ import javax.validation.constraints.Min; import javax.validation.constraints.Size; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.validation.ValidationAutoConfigurationTests.CustomValidatorConfiguration.TestBeanPostProcessor; -import org.springframework.boot.test.util.TestPropertyValues; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.validation.beanvalidation.MethodValidationExcludeFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; @@ -43,6 +46,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.mockito.Mockito.mock; /** @@ -53,162 +57,187 @@ */ class ValidationAutoConfigurationTests { - private AnnotationConfigApplicationContext context; - - @AfterEach - void close() { - if (this.context != null) { - this.context.close(); - } - } + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ValidationAutoConfiguration.class)); @Test void validationAutoConfigurationShouldConfigureDefaultValidator() { - load(Config.class); - String[] jsrValidatorNames = this.context.getBeanNamesForType(Validator.class); - String[] springValidatorNames = this.context - .getBeanNamesForType(org.springframework.validation.Validator.class); - assertThat(jsrValidatorNames).containsExactly("defaultValidator"); - assertThat(springValidatorNames).containsExactly("defaultValidator"); - Validator jsrValidator = this.context.getBean(Validator.class); - org.springframework.validation.Validator springValidator = this.context - .getBean(org.springframework.validation.Validator.class); - assertThat(jsrValidator).isInstanceOf(LocalValidatorFactoryBean.class); - assertThat(jsrValidator).isEqualTo(springValidator); - assertThat(isPrimaryBean("defaultValidator")).isTrue(); + this.contextRunner.run((context) -> { + assertThat(context.getBeanNamesForType(Validator.class)).containsExactly("defaultValidator"); + assertThat(context.getBeanNamesForType(org.springframework.validation.Validator.class)) + .containsExactly("defaultValidator"); + assertThat(context.getBean(Validator.class)).isInstanceOf(LocalValidatorFactoryBean.class) + .isEqualTo(context.getBean(org.springframework.validation.Validator.class)); + assertThat(isPrimaryBean(context, "defaultValidator")).isTrue(); + }); } @Test void validationAutoConfigurationWhenUserProvidesValidatorShouldBackOff() { - load(UserDefinedValidatorConfig.class); - String[] jsrValidatorNames = this.context.getBeanNamesForType(Validator.class); - String[] springValidatorNames = this.context - .getBeanNamesForType(org.springframework.validation.Validator.class); - assertThat(jsrValidatorNames).containsExactly("customValidator"); - assertThat(springValidatorNames).containsExactly("customValidator"); - org.springframework.validation.Validator springValidator = this.context - .getBean(org.springframework.validation.Validator.class); - Validator jsrValidator = this.context.getBean(Validator.class); - assertThat(jsrValidator).isInstanceOf(OptionalValidatorFactoryBean.class); - assertThat(jsrValidator).isEqualTo(springValidator); - assertThat(isPrimaryBean("customValidator")).isFalse(); + this.contextRunner.withUserConfiguration(UserDefinedValidatorConfig.class).run((context) -> { + assertThat(context.getBeanNamesForType(Validator.class)).containsExactly("customValidator"); + assertThat(context.getBeanNamesForType(org.springframework.validation.Validator.class)) + .containsExactly("customValidator"); + assertThat(context.getBean(Validator.class)).isInstanceOf(OptionalValidatorFactoryBean.class) + .isEqualTo(context.getBean(org.springframework.validation.Validator.class)); + assertThat(isPrimaryBean(context, "customValidator")).isFalse(); + }); } @Test void validationAutoConfigurationWhenUserProvidesDefaultValidatorShouldNotEnablePrimary() { - load(UserDefinedDefaultValidatorConfig.class); - String[] jsrValidatorNames = this.context.getBeanNamesForType(Validator.class); - String[] springValidatorNames = this.context - .getBeanNamesForType(org.springframework.validation.Validator.class); - assertThat(jsrValidatorNames).containsExactly("defaultValidator"); - assertThat(springValidatorNames).containsExactly("defaultValidator"); - assertThat(isPrimaryBean("defaultValidator")).isFalse(); + this.contextRunner.withUserConfiguration(UserDefinedDefaultValidatorConfig.class).run((context) -> { + assertThat(context.getBeanNamesForType(Validator.class)).containsExactly("defaultValidator"); + assertThat(context.getBeanNamesForType(org.springframework.validation.Validator.class)) + .containsExactly("defaultValidator"); + assertThat(isPrimaryBean(context, "defaultValidator")).isFalse(); + }); } @Test void validationAutoConfigurationWhenUserProvidesJsrValidatorShouldBackOff() { - load(UserDefinedJsrValidatorConfig.class); - String[] jsrValidatorNames = this.context.getBeanNamesForType(Validator.class); - String[] springValidatorNames = this.context - .getBeanNamesForType(org.springframework.validation.Validator.class); - assertThat(jsrValidatorNames).containsExactly("customValidator"); - assertThat(springValidatorNames).isEmpty(); - assertThat(isPrimaryBean("customValidator")).isFalse(); + this.contextRunner.withUserConfiguration(UserDefinedJsrValidatorConfig.class).run((context) -> { + assertThat(context.getBeanNamesForType(Validator.class)).containsExactly("customValidator"); + assertThat(context.getBeanNamesForType(org.springframework.validation.Validator.class)).isEmpty(); + assertThat(isPrimaryBean(context, "customValidator")).isFalse(); + }); } @Test void validationAutoConfigurationWhenUserProvidesSpringValidatorShouldCreateJsrValidator() { - load(UserDefinedSpringValidatorConfig.class); - String[] jsrValidatorNames = this.context.getBeanNamesForType(Validator.class); - String[] springValidatorNames = this.context - .getBeanNamesForType(org.springframework.validation.Validator.class); - assertThat(jsrValidatorNames).containsExactly("defaultValidator"); - assertThat(springValidatorNames).containsExactly("customValidator", "anotherCustomValidator", - "defaultValidator"); - Validator jsrValidator = this.context.getBean(Validator.class); - org.springframework.validation.Validator springValidator = this.context - .getBean(org.springframework.validation.Validator.class); - assertThat(jsrValidator).isInstanceOf(LocalValidatorFactoryBean.class); - assertThat(jsrValidator).isEqualTo(springValidator); - assertThat(isPrimaryBean("defaultValidator")).isTrue(); + this.contextRunner.withUserConfiguration(UserDefinedSpringValidatorConfig.class).run((context) -> { + assertThat(context.getBeanNamesForType(Validator.class)).containsExactly("defaultValidator"); + assertThat(context.getBeanNamesForType(org.springframework.validation.Validator.class)) + .containsExactly("customValidator", "anotherCustomValidator", "defaultValidator"); + assertThat(context.getBean(Validator.class)).isInstanceOf(LocalValidatorFactoryBean.class) + .isEqualTo(context.getBean(org.springframework.validation.Validator.class)); + assertThat(isPrimaryBean(context, "defaultValidator")).isTrue(); + }); } @Test void validationAutoConfigurationWhenUserProvidesPrimarySpringValidatorShouldRemovePrimaryFlag() { - load(UserDefinedPrimarySpringValidatorConfig.class); - String[] jsrValidatorNames = this.context.getBeanNamesForType(Validator.class); - String[] springValidatorNames = this.context - .getBeanNamesForType(org.springframework.validation.Validator.class); - assertThat(jsrValidatorNames).containsExactly("defaultValidator"); - assertThat(springValidatorNames).containsExactly("customValidator", "anotherCustomValidator", - "defaultValidator"); - Validator jsrValidator = this.context.getBean(Validator.class); - org.springframework.validation.Validator springValidator = this.context - .getBean(org.springframework.validation.Validator.class); - assertThat(jsrValidator).isInstanceOf(LocalValidatorFactoryBean.class); - assertThat(springValidator).isEqualTo(this.context.getBean("anotherCustomValidator")); - assertThat(isPrimaryBean("defaultValidator")).isFalse(); + this.contextRunner.withUserConfiguration(UserDefinedPrimarySpringValidatorConfig.class).run((context) -> { + assertThat(context.getBeanNamesForType(Validator.class)).containsExactly("defaultValidator"); + assertThat(context.getBeanNamesForType(org.springframework.validation.Validator.class)) + .containsExactly("customValidator", "anotherCustomValidator", "defaultValidator"); + assertThat(context.getBean(Validator.class)).isInstanceOf(LocalValidatorFactoryBean.class); + assertThat(context.getBean(org.springframework.validation.Validator.class)) + .isEqualTo(context.getBean("anotherCustomValidator")); + assertThat(isPrimaryBean(context, "defaultValidator")).isFalse(); + }); + } + + @Test + void whenUserProvidesSpringValidatorInParentContextThenAutoConfiguredValidatorIsPrimary() { + new ApplicationContextRunner().withUserConfiguration(UserDefinedSpringValidatorConfig.class).run((parent) -> { + this.contextRunner.withParent(parent).run((context) -> { + assertThat(context.getBeanNamesForType(Validator.class)).containsExactly("defaultValidator"); + assertThat(context.getBeanNamesForType(org.springframework.validation.Validator.class)) + .containsExactly("defaultValidator"); + assertThat(BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context.getBeanFactory(), + org.springframework.validation.Validator.class)).containsExactly("defaultValidator", + "customValidator", "anotherCustomValidator"); + assertThat(isPrimaryBean(context, "defaultValidator")).isTrue(); + }); + }); + } + + @Test + void whenUserProvidesPrimarySpringValidatorInParentContextThenAutoConfiguredValidatorIsPrimary() { + new ApplicationContextRunner().withUserConfiguration(UserDefinedPrimarySpringValidatorConfig.class) + .run((parent) -> { + this.contextRunner.withParent(parent).run((context) -> { + assertThat(context.getBeanNamesForType(Validator.class)).containsExactly("defaultValidator"); + assertThat(context.getBeanNamesForType(org.springframework.validation.Validator.class)) + .containsExactly("defaultValidator"); + assertThat(BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context.getBeanFactory(), + org.springframework.validation.Validator.class)).containsExactly("defaultValidator", + "customValidator", "anotherCustomValidator"); + assertThat(isPrimaryBean(context, "defaultValidator")).isTrue(); + }); + }); } @Test void validationIsEnabled() { - load(SampleService.class); - assertThat(this.context.getBeansOfType(Validator.class)).hasSize(1); - SampleService service = this.context.getBean(SampleService.class); - service.doSomething("Valid"); - assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy(() -> service.doSomething("KO")); + this.contextRunner.withUserConfiguration(SampleService.class).run((context) -> { + assertThat(context.getBeansOfType(Validator.class)).hasSize(1); + SampleService service = context.getBean(SampleService.class); + service.doSomething("Valid"); + assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy(() -> service.doSomething("KO")); + }); + } + + @Test + void classCanBeExcludedFromValidation() { + this.contextRunner.withUserConfiguration(ExcludedServiceConfiguration.class).run((context) -> { + assertThat(context.getBeansOfType(Validator.class)).hasSize(1); + ExcludedService service = context.getBean(ExcludedService.class); + service.doSomething("Valid"); + assertThatNoException().isThrownBy(() -> service.doSomething("KO")); + }); } @Test void validationUsesCglibProxy() { - load(DefaultAnotherSampleService.class); - assertThat(this.context.getBeansOfType(Validator.class)).hasSize(1); - DefaultAnotherSampleService service = this.context.getBean(DefaultAnotherSampleService.class); - service.doSomething(42); - assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy(() -> service.doSomething(2)); + this.contextRunner.withUserConfiguration(DefaultAnotherSampleService.class).run((context) -> { + assertThat(context.getBeansOfType(Validator.class)).hasSize(1); + DefaultAnotherSampleService service = context.getBean(DefaultAnotherSampleService.class); + service.doSomething(42); + assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy(() -> service.doSomething(2)); + }); } @Test void validationCanBeConfiguredToUseJdkProxy() { - load(AnotherSampleServiceConfiguration.class, "spring.aop.proxy-target-class=false"); - assertThat(this.context.getBeansOfType(Validator.class)).hasSize(1); - assertThat(this.context.getBeansOfType(DefaultAnotherSampleService.class)).isEmpty(); - AnotherSampleService service = this.context.getBean(AnotherSampleService.class); - service.doSomething(42); - assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy(() -> service.doSomething(2)); + this.contextRunner.withUserConfiguration(AnotherSampleServiceConfiguration.class) + .withPropertyValues("spring.aop.proxy-target-class=false").run((context) -> { + assertThat(context.getBeansOfType(Validator.class)).hasSize(1); + assertThat(context.getBeansOfType(DefaultAnotherSampleService.class)).isEmpty(); + AnotherSampleService service = context.getBean(AnotherSampleService.class); + service.doSomething(42); + assertThatExceptionOfType(ConstraintViolationException.class) + .isThrownBy(() -> service.doSomething(2)); + }); } @Test void userDefinedMethodValidationPostProcessorTakesPrecedence() { - load(SampleConfiguration.class); - assertThat(this.context.getBeansOfType(Validator.class)).hasSize(1); - Object userMethodValidationPostProcessor = this.context.getBean("testMethodValidationPostProcessor"); - assertThat(this.context.getBean(MethodValidationPostProcessor.class)) - .isSameAs(userMethodValidationPostProcessor); - assertThat(this.context.getBeansOfType(MethodValidationPostProcessor.class)).hasSize(1); - assertThat(this.context.getBean(Validator.class)) - .isNotSameAs(ReflectionTestUtils.getField(userMethodValidationPostProcessor, "validator")); + this.contextRunner.withUserConfiguration(SampleConfiguration.class).run((context) -> { + assertThat(context.getBeansOfType(Validator.class)).hasSize(1); + Object userMethodValidationPostProcessor = context.getBean("testMethodValidationPostProcessor"); + assertThat(context.getBean(MethodValidationPostProcessor.class)) + .isSameAs(userMethodValidationPostProcessor); + assertThat(context.getBeansOfType(MethodValidationPostProcessor.class)).hasSize(1); + assertThat(context.getBean(Validator.class)) + .isNotSameAs(ReflectionTestUtils.getField(userMethodValidationPostProcessor, "validator")); + }); } @Test void methodValidationPostProcessorValidatorDependencyDoesNotTriggerEarlyInitialization() { - load(CustomValidatorConfiguration.class); - assertThat(this.context.getBean(TestBeanPostProcessor.class).postProcessed).contains("someService"); + this.contextRunner.withUserConfiguration(CustomValidatorConfiguration.class) + .run((context) -> assertThat(context.getBean(TestBeanPostProcessor.class).postProcessed) + .contains("someService")); } - private boolean isPrimaryBean(String beanName) { - return this.context.getBeanDefinition(beanName).isPrimary(); + @Test + void validationIsEnabledInChildContext() { + this.contextRunner.run((parent) -> new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ValidationAutoConfiguration.class)) + .withUserConfiguration(SampleService.class).withParent(parent).run((context) -> { + assertThat(context.getBeansOfType(Validator.class)).hasSize(0); + assertThat(parent.getBeansOfType(Validator.class)).hasSize(1); + SampleService service = context.getBean(SampleService.class); + service.doSomething("Valid"); + assertThatExceptionOfType(ConstraintViolationException.class) + .isThrownBy(() -> service.doSomething("KO")); + })); } - private void load(Class config, String... environment) { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); - TestPropertyValues.of(environment).applyTo(ctx); - if (config != null) { - ctx.register(config); - } - ctx.register(ValidationAutoConfiguration.class); - ctx.refresh(); - this.context = ctx; + private boolean isPrimaryBean(AssertableApplicationContext context, String beanName) { + return ((BeanDefinitionRegistry) context.getSourceApplicationContext()).getBeanDefinition(beanName).isPrimary(); } @Configuration(proxyBeanMethods = false) @@ -285,6 +314,29 @@ void doSomething(@Size(min = 3, max = 10) String name) { } + @Configuration(proxyBeanMethods = false) + static final class ExcludedServiceConfiguration { + + @Bean + ExcludedService excludedService() { + return new ExcludedService(); + } + + @Bean + MethodValidationExcludeFilter exclusionFilter() { + return (type) -> type.equals(ExcludedService.class); + } + + } + + @Validated + static final class ExcludedService { + + void doSomething(@Size(min = 3, max = 10) String name) { + } + + } + interface AnotherSampleService { void doSomething(@Min(42) Integer counter); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/ValidatorAdapterTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/ValidatorAdapterTests.java index 25db63573428..f11535abfec8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/ValidatorAdapterTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/ValidatorAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,10 +33,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; /** * Tests for {@link ValidatorAdapter}. @@ -46,7 +45,7 @@ */ class ValidatorAdapterTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner(); + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner(); @Test void wrapLocalValidatorFactoryBean() { @@ -63,11 +62,11 @@ void wrapLocalValidatorFactoryBean() { void wrapperInvokesCallbackOnNonManagedBean() { this.contextRunner.withUserConfiguration(NonManagedBeanConfig.class).run((context) -> { LocalValidatorFactoryBean validator = context.getBean(NonManagedBeanConfig.class).validator; - verify(validator, times(1)).setApplicationContext(any(ApplicationContext.class)); - verify(validator, times(1)).afterPropertiesSet(); - verify(validator, never()).destroy(); + then(validator).should().setApplicationContext(any(ApplicationContext.class)); + then(validator).should().afterPropertiesSet(); + then(validator).should(never()).destroy(); context.close(); - verify(validator, times(1)).destroy(); + then(validator).should().destroy(); }); } @@ -75,11 +74,11 @@ void wrapperInvokesCallbackOnNonManagedBean() { void wrapperDoesNotInvokeCallbackOnManagedBean() { this.contextRunner.withUserConfiguration(ManagedBeanConfig.class).run((context) -> { LocalValidatorFactoryBean validator = context.getBean(ManagedBeanConfig.class).validator; - verify(validator, never()).setApplicationContext(any(ApplicationContext.class)); - verify(validator, never()).afterPropertiesSet(); - verify(validator, never()).destroy(); + then(validator).should(never()).setApplicationContext(any(ApplicationContext.class)); + then(validator).should(never()).afterPropertiesSet(); + then(validator).should(never()).destroy(); context.close(); - verify(validator, never()).destroy(); + then(validator).should(never()).destroy(); }); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ConditionalOnEnabledResourceChainTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ConditionalOnEnabledResourceChainTests.java index e056962f8aa2..9c8d73855333 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ConditionalOnEnabledResourceChainTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ConditionalOnEnabledResourceChainTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,8 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @@ -46,27 +48,31 @@ void disabledByDefault() { assertThat(this.context.containsBean("foo")).isFalse(); } - @Test - void disabledExplicitly() { - load("spring.resources.chain.enabled:false"); + @ParameterizedTest + @ValueSource(strings = { "spring.resources.", "spring.web.resources." }) + void disabledExplicitly(String prefix) { + load(prefix + "chain.enabled:false"); assertThat(this.context.containsBean("foo")).isFalse(); } - @Test - void enabledViaMainEnabledFlag() { - load("spring.resources.chain.enabled:true"); + @ParameterizedTest + @ValueSource(strings = { "spring.resources.", "spring.web.resources." }) + void enabledViaMainEnabledFlag(String prefix) { + load(prefix + "chain.enabled:true"); assertThat(this.context.containsBean("foo")).isTrue(); } - @Test - void enabledViaFixedStrategyFlag() { - load("spring.resources.chain.strategy.fixed.enabled:true"); + @ParameterizedTest + @ValueSource(strings = { "spring.resources.", "spring.web.resources." }) + void enabledViaFixedStrategyFlag(String prefix) { + load(prefix + "chain.strategy.fixed.enabled:true"); assertThat(this.context.containsBean("foo")).isTrue(); } - @Test - void enabledViaContentStrategyFlag() { - load("spring.resources.chain.strategy.content.enabled:true"); + @ParameterizedTest + @ValueSource(strings = { "spring.resources.", "spring.web.resources." }) + void enabledViaContentStrategyFlag(String prefix) { + load(prefix + "chain.strategy.content.enabled:true"); assertThat(this.context.containsBean("foo")).isTrue(); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ResourcePropertiesBindingTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ResourcePropertiesBindingTests.java index 9ce3fdc37fe2..97c66c4207f9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ResourcePropertiesBindingTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ResourcePropertiesBindingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,9 +33,10 @@ * * @author Stephane Nicoll */ +@Deprecated class ResourcePropertiesBindingTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withUserConfiguration(TestConfiguration.class); @Test diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ResourcePropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ResourcePropertiesTests.java index 4172490aef2d..91518700c7c2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ResourcePropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ResourcePropertiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import org.junit.jupiter.api.Test; -import org.springframework.boot.autoconfigure.web.ResourceProperties.Cache; import org.springframework.http.CacheControl; import static org.assertj.core.api.Assertions.assertThat; @@ -31,6 +30,7 @@ * @author Stephane Nicoll * @author Kristine Jetzke */ +@Deprecated class ResourcePropertiesTests { private final ResourceProperties properties = new ResourceProperties(); @@ -78,7 +78,7 @@ void emptyCacheControl() { @Test void cacheControlAllPropertiesSet() { - Cache.Cachecontrol properties = this.properties.getCache().getCachecontrol(); + ResourceProperties.Cache.Cachecontrol properties = this.properties.getCache().getCachecontrol(); properties.setMaxAge(Duration.ofSeconds(4)); properties.setCachePrivate(true); properties.setCachePublic(true); @@ -96,7 +96,7 @@ void cacheControlAllPropertiesSet() { @Test void invalidCacheControlCombination() { - Cache.Cachecontrol properties = this.properties.getCache().getCachecontrol(); + ResourceProperties.Cache.Cachecontrol properties = this.properties.getCache().getCachecontrol(); properties.setMaxAge(Duration.ofSeconds(4)); properties.setNoStore(true); CacheControl cacheControl = properties.toHttpCacheControl(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java index 17b82914c925..bd8afc972d67 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,9 +38,14 @@ import org.apache.catalina.valves.AccessLogValve; import org.apache.catalina.valves.RemoteIpValve; import org.apache.coyote.AbstractProtocol; +import org.apache.tomcat.util.net.AbstractEndpoint; import org.eclipse.jetty.server.HttpChannel; import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.junit.jupiter.api.Test; +import reactor.netty.http.HttpDecoderSpec; +import reactor.netty.http.server.HttpRequestDecoderSpec; import org.springframework.boot.autoconfigure.web.ServerProperties.Tomcat.Accesslog; import org.springframework.boot.context.properties.bind.Bindable; @@ -55,6 +60,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.client.ClientHttpResponse; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.unit.DataSize; @@ -76,6 +82,8 @@ * @author Andrew McGhie * @author HaiTao Zhang * @author Rafiullah Hamedy + * @author Chris Bono + * @author Parviz Rozikov */ class ServerPropertiesTests { @@ -104,12 +112,6 @@ void testServerHeader() { assertThat(this.properties.getServerHeader()).isEqualTo("Custom Server"); } - @Test - void testConnectionTimeout() { - bind("server.connection-timeout", "60s"); - assertThat(this.properties.getConnectionTimeout()).isEqualTo(Duration.ofMillis(60000)); - } - @Test void testTomcatBinding() { Map map = new HashMap<>(); @@ -125,12 +127,13 @@ void testTomcatBinding() { map.put("server.tomcat.accesslog.rename-on-rotate", "true"); map.put("server.tomcat.accesslog.ipv6Canonical", "true"); map.put("server.tomcat.accesslog.request-attributes-enabled", "true"); - map.put("server.tomcat.protocol-header", "X-Forwarded-Protocol"); - map.put("server.tomcat.remote-ip-header", "Remote-Ip"); - map.put("server.tomcat.internal-proxies", "10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}"); + map.put("server.tomcat.remoteip.protocol-header", "X-Forwarded-Protocol"); + map.put("server.tomcat.remoteip.remote-ip-header", "Remote-Ip"); + map.put("server.tomcat.remoteip.internal-proxies", "10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}"); map.put("server.tomcat.background-processor-delay", "10"); map.put("server.tomcat.relaxed-path-chars", "|,<"); map.put("server.tomcat.relaxed-query-chars", "^ , | "); + map.put("server.tomcat.use-relative-redirects", "true"); bind(map); ServerProperties.Tomcat tomcat = this.properties.getTomcat(); Accesslog accesslog = tomcat.getAccesslog(); @@ -146,12 +149,13 @@ void testTomcatBinding() { assertThat(accesslog.isRenameOnRotate()).isTrue(); assertThat(accesslog.isIpv6Canonical()).isTrue(); assertThat(accesslog.isRequestAttributesEnabled()).isTrue(); - assertThat(tomcat.getRemoteIpHeader()).isEqualTo("Remote-Ip"); - assertThat(tomcat.getProtocolHeader()).isEqualTo("X-Forwarded-Protocol"); - assertThat(tomcat.getInternalProxies()).isEqualTo("10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}"); - assertThat(tomcat.getBackgroundProcessorDelay()).isEqualTo(Duration.ofSeconds(10)); + assertThat(tomcat.getRemoteip().getRemoteIpHeader()).isEqualTo("Remote-Ip"); + assertThat(tomcat.getRemoteip().getProtocolHeader()).isEqualTo("X-Forwarded-Protocol"); + assertThat(tomcat.getRemoteip().getInternalProxies()).isEqualTo("10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}"); + assertThat(tomcat.getBackgroundProcessorDelay()).hasSeconds(10); assertThat(tomcat.getRelaxedPathChars()).containsExactly('|', '<'); assertThat(tomcat.getRelaxedQueryChars()).containsExactly('^', '|'); + assertThat(tomcat.isUseRelativeRedirects()).isTrue(); } @Test @@ -208,34 +212,76 @@ void testCustomizeHeaderSizeUseBytesByDefault() { assertThat(this.properties.getMaxHttpHeaderSize()).isEqualTo(DataSize.ofKilobytes(1)); } + @Test + void testCustomizeTomcatMaxThreads() { + bind("server.tomcat.threads.max", "10"); + assertThat(this.properties.getTomcat().getThreads().getMax()).isEqualTo(10); + } + + @Test + void testCustomizeTomcatKeepAliveTimeout() { + bind("server.tomcat.keep-alive-timeout", "30s"); + assertThat(this.properties.getTomcat().getKeepAliveTimeout()).hasSeconds(30); + } + + @Test + void testCustomizeTomcatKeepAliveTimeoutWithInfinite() { + bind("server.tomcat.keep-alive-timeout", "-1"); + assertThat(this.properties.getTomcat().getKeepAliveTimeout()).hasMillis(-1); + } + + @Test + void customizeMaxKeepAliveRequests() { + bind("server.tomcat.max-keep-alive-requests", "200"); + assertThat(this.properties.getTomcat().getMaxKeepAliveRequests()).isEqualTo(200); + } + + @Test + void customizeMaxKeepAliveRequestsWithInfinite() { + bind("server.tomcat.max-keep-alive-requests", "-1"); + assertThat(this.properties.getTomcat().getMaxKeepAliveRequests()).isEqualTo(-1); + } + + @Test + void testCustomizeTomcatMinSpareThreads() { + bind("server.tomcat.threads.min-spare", "10"); + assertThat(this.properties.getTomcat().getThreads().getMinSpare()).isEqualTo(10); + } + @Test void testCustomizeJettyAcceptors() { - bind("server.jetty.acceptors", "10"); - assertThat(this.properties.getJetty().getAcceptors()).isEqualTo(10); + bind("server.jetty.threads.acceptors", "10"); + assertThat(this.properties.getJetty().getThreads().getAcceptors()).isEqualTo(10); } @Test void testCustomizeJettySelectors() { - bind("server.jetty.selectors", "10"); - assertThat(this.properties.getJetty().getSelectors()).isEqualTo(10); + bind("server.jetty.threads.selectors", "10"); + assertThat(this.properties.getJetty().getThreads().getSelectors()).isEqualTo(10); } @Test void testCustomizeJettyMaxThreads() { - bind("server.jetty.max-threads", "10"); - assertThat(this.properties.getJetty().getMaxThreads()).isEqualTo(10); + bind("server.jetty.threads.max", "10"); + assertThat(this.properties.getJetty().getThreads().getMax()).isEqualTo(10); } @Test void testCustomizeJettyMinThreads() { - bind("server.jetty.min-threads", "10"); - assertThat(this.properties.getJetty().getMinThreads()).isEqualTo(10); + bind("server.jetty.threads.min", "10"); + assertThat(this.properties.getJetty().getThreads().getMin()).isEqualTo(10); } @Test void testCustomizeJettyIdleTimeout() { - bind("server.jetty.thread-idle-timeout", "10s"); - assertThat(this.properties.getJetty().getThreadIdleTimeout()).isEqualTo(Duration.ofSeconds(10)); + bind("server.jetty.threads.idle-timeout", "10s"); + assertThat(this.properties.getJetty().getThreads().getIdleTimeout()).isEqualTo(Duration.ofSeconds(10)); + } + + @Test + void testCustomizeJettyMaxQueueCapacity() { + bind("server.jetty.threads.max-queue-capacity", "5150"); + assertThat(this.properties.getJetty().getThreads().getMaxQueueCapacity()).isEqualTo(5150); } @Test @@ -252,6 +298,18 @@ void testCustomizeUndertowSocketOption() { "true"); } + @Test + void testCustomizeUndertowIoThreads() { + bind("server.undertow.threads.io", "4"); + assertThat(this.properties.getUndertow().getThreads().getIo()).isEqualTo(4); + } + + @Test + void testCustomizeUndertowWorkerThreads() { + bind("server.undertow.threads.worker", "10"); + assertThat(this.properties.getUndertow().getThreads().getWorker()).isEqualTo(10); + } + @Test void testCustomizeJettyAccessLog() { Map map = new HashMap<>(); @@ -290,25 +348,25 @@ void tomcatMaxConnectionsMatchesProtocolDefault() throws Exception { @Test void tomcatMaxThreadsMatchesProtocolDefault() throws Exception { - assertThat(this.properties.getTomcat().getMaxThreads()).isEqualTo(getDefaultProtocol().getMaxThreads()); + assertThat(this.properties.getTomcat().getThreads().getMax()).isEqualTo(getDefaultProtocol().getMaxThreads()); } @Test void tomcatMinSpareThreadsMatchesProtocolDefault() throws Exception { - assertThat(this.properties.getTomcat().getMinSpareThreads()) + assertThat(this.properties.getTomcat().getThreads().getMinSpare()) .isEqualTo(getDefaultProtocol().getMinSpareThreads()); } @Test void tomcatMaxHttpPostSizeMatchesConnectorDefault() throws Exception { - assertThat(this.properties.getTomcat().getMaxHttpPostSize().toBytes()) + assertThat(this.properties.getTomcat().getMaxHttpFormPostSize().toBytes()) .isEqualTo(getDefaultConnector().getMaxPostSize()); } @Test void tomcatBackgroundProcessorDelayMatchesEngineDefault() { assertThat(this.properties.getTomcat().getBackgroundProcessorDelay()) - .isEqualTo(Duration.ofSeconds((new StandardEngine().getBackgroundProcessorDelay()))); + .hasSeconds((new StandardEngine().getBackgroundProcessorDelay())); } @Test @@ -343,12 +401,39 @@ void tomcatAccessLogRequestAttributesEnabledMatchesDefault() { @Test void tomcatInternalProxiesMatchesDefault() { - assertThat(this.properties.getTomcat().getInternalProxies()) + assertThat(this.properties.getTomcat().getRemoteip().getInternalProxies()) .isEqualTo(new RemoteIpValve().getInternalProxies()); } @Test - void jettyMaxHttpFormPostSizeMatchesDefault() throws Exception { + void tomcatUseRelativeRedirectsDefaultsToFalse() { + assertThat(this.properties.getTomcat().isUseRelativeRedirects()).isFalse(); + } + + @Test + void tomcatMaxKeepAliveRequestsDefault() throws Exception { + AbstractEndpoint endpoint = (AbstractEndpoint) ReflectionTestUtils.getField(getDefaultProtocol(), + "endpoint"); + int defaultMaxKeepAliveRequests = (int) ReflectionTestUtils.getField(endpoint, "maxKeepAliveRequests"); + assertThat(this.properties.getTomcat().getMaxKeepAliveRequests()).isEqualTo(defaultMaxKeepAliveRequests); + } + + @Test + void jettyThreadPoolPropertyDefaultsShouldMatchServerDefault() { + JettyServletWebServerFactory jettyFactory = new JettyServletWebServerFactory(0); + JettyWebServer jetty = (JettyWebServer) jettyFactory.getWebServer(); + Server server = jetty.getServer(); + QueuedThreadPool threadPool = (QueuedThreadPool) server.getThreadPool(); + int idleTimeout = threadPool.getIdleTimeout(); + int maxThreads = threadPool.getMaxThreads(); + int minThreads = threadPool.getMinThreads(); + assertThat(this.properties.getJetty().getThreads().getIdleTimeout().toMillis()).isEqualTo(idleTimeout); + assertThat(this.properties.getJetty().getThreads().getMax()).isEqualTo(maxThreads); + assertThat(this.properties.getJetty().getThreads().getMin()).isEqualTo(minThreads); + } + + @Test + void jettyMaxHttpFormPostSizeMatchesDefault() { JettyServletWebServerFactory jettyFactory = new JettyServletWebServerFactory(0); JettyWebServer jetty = (JettyWebServer) jettyFactory .getWebServer((ServletContextInitializer) (servletContext) -> servletContext @@ -399,7 +484,7 @@ public void handleError(ClientHttpResponse response) throws IOException { template.postForEntity(URI.create("http://localhost:" + jetty.getPort() + "/form"), entity, Void.class); assertThat(failure.get()).isNotNull(); String message = failure.get().getCause().getMessage(); - int defaultMaxPostSize = Integer.valueOf(message.substring(message.lastIndexOf(' ')).trim()); + int defaultMaxPostSize = Integer.parseInt(message.substring(message.lastIndexOf(' ')).trim()); assertThat(this.properties.getJetty().getMaxHttpFormPostSize().toBytes()).isEqualTo(defaultMaxPostSize); } finally { @@ -413,12 +498,42 @@ void undertowMaxHttpPostSizeMatchesDefault() { .isEqualTo(UndertowOptions.DEFAULT_MAX_ENTITY_SIZE); } - private Connector getDefaultConnector() throws Exception { + @Test + void nettyMaxChunkSizeMatchesHttpDecoderSpecDefault() { + assertThat(this.properties.getNetty().getMaxChunkSize().toBytes()) + .isEqualTo(HttpDecoderSpec.DEFAULT_MAX_CHUNK_SIZE); + } + + @Test + void nettyMaxInitialLineLengthMatchesHttpDecoderSpecDefault() { + assertThat(this.properties.getNetty().getMaxInitialLineLength().toBytes()) + .isEqualTo(HttpDecoderSpec.DEFAULT_MAX_INITIAL_LINE_LENGTH); + } + + @Test + void nettyValidateHeadersMatchesHttpDecoderSpecDefault() { + assertThat(this.properties.getNetty().isValidateHeaders()).isEqualTo(HttpDecoderSpec.DEFAULT_VALIDATE_HEADERS); + } + + @Test + void nettyH2cMaxContentLengthMatchesHttpDecoderSpecDefault() { + assertThat(this.properties.getNetty().getH2cMaxContentLength().toBytes()) + .isEqualTo(HttpRequestDecoderSpec.DEFAULT_H2C_MAX_CONTENT_LENGTH); + } + + @Test + void nettyInitialBufferSizeMatchesHttpDecoderSpecDefault() { + assertThat(this.properties.getNetty().getInitialBufferSize().toBytes()) + .isEqualTo(HttpDecoderSpec.DEFAULT_INITIAL_BUFFER_SIZE); + } + + private Connector getDefaultConnector() { return new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL); } private AbstractProtocol getDefaultProtocol() throws Exception { - return (AbstractProtocol) Class.forName(TomcatServletWebServerFactory.DEFAULT_PROTOCOL).newInstance(); + return (AbstractProtocol) Class.forName(TomcatServletWebServerFactory.DEFAULT_PROTOCOL) + .getDeclaredConstructor().newInstance(); } private void bind(String name, String value) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebPropertiesResourcesBindingTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebPropertiesResourcesBindingTests.java new file mode 100644 index 000000000000..ce8d79428537 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebPropertiesResourcesBindingTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.web; + +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.web.WebProperties.Resources; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.ContextConsumer; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Binding tests for {@link WebProperties.Resources}. + * + * @author Stephane Nicoll + */ +class WebPropertiesResourcesBindingTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withUserConfiguration(TestConfiguration.class); + + @Test + void staticLocationsExpandArray() { + this.contextRunner + .withPropertyValues("spring.web.resources.static-locations[0]=classpath:/one/", + "spring.web.resources.static-locations[1]=classpath:/two", + "spring.web.resources.static-locations[2]=classpath:/three/", + "spring.web.resources.static-locations[3]=classpath:/four", + "spring.web.resources.static-locations[4]=classpath:/five/", + "spring.web.resources.static-locations[5]=classpath:/six") + .run(assertResourceProperties((properties) -> assertThat(properties.getStaticLocations()).contains( + "classpath:/one/", "classpath:/two/", "classpath:/three/", "classpath:/four/", + "classpath:/five/", "classpath:/six/"))); + } + + private ContextConsumer assertResourceProperties(Consumer consumer) { + return (context) -> { + assertThat(context).hasSingleBean(WebProperties.class); + consumer.accept(context.getBean(WebProperties.class).getResources()); + }; + } + + @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties(WebProperties.class) + static class TestConfiguration { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebPropertiesResourcesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebPropertiesResourcesTests.java new file mode 100644 index 000000000000..6606ca73e1ef --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/WebPropertiesResourcesTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.web; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.web.WebProperties.Resources; +import org.springframework.boot.autoconfigure.web.WebProperties.Resources.Cache; +import org.springframework.http.CacheControl; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link WebProperties.Resources}. + * + * @author Stephane Nicoll + * @author Kristine Jetzke + */ +@Deprecated +class WebPropertiesResourcesTests { + + private final Resources properties = new WebProperties().getResources(); + + @Test + void resourceChainNoCustomization() { + assertThat(this.properties.getChain().getEnabled()).isNull(); + } + + @Test + void resourceChainStrategyEnabled() { + this.properties.getChain().getStrategy().getFixed().setEnabled(true); + assertThat(this.properties.getChain().getEnabled()).isTrue(); + } + + @Test + void resourceChainEnabled() { + this.properties.getChain().setEnabled(true); + assertThat(this.properties.getChain().getEnabled()).isTrue(); + } + + @Test + void resourceChainDisabled() { + this.properties.getChain().setEnabled(false); + assertThat(this.properties.getChain().getEnabled()).isFalse(); + } + + @Test + void defaultStaticLocationsAllEndWithTrailingSlash() { + assertThat(this.properties.getStaticLocations()).allMatch((location) -> location.endsWith("/")); + } + + @Test + void customStaticLocationsAreNormalizedToEndWithTrailingSlash() { + this.properties.setStaticLocations(new String[] { "/foo", "/bar", "/baz/" }); + String[] actual = this.properties.getStaticLocations(); + assertThat(actual).containsExactly("/foo/", "/bar/", "/baz/"); + } + + @Test + void emptyCacheControl() { + CacheControl cacheControl = this.properties.getCache().getCachecontrol().toHttpCacheControl(); + assertThat(cacheControl).isNull(); + } + + @Test + void cacheControlAllPropertiesSet() { + Cache.Cachecontrol properties = this.properties.getCache().getCachecontrol(); + properties.setMaxAge(Duration.ofSeconds(4)); + properties.setCachePrivate(true); + properties.setCachePublic(true); + properties.setMustRevalidate(true); + properties.setNoTransform(true); + properties.setProxyRevalidate(true); + properties.setSMaxAge(Duration.ofSeconds(5)); + properties.setStaleIfError(Duration.ofSeconds(6)); + properties.setStaleWhileRevalidate(Duration.ofSeconds(7)); + CacheControl cacheControl = properties.toHttpCacheControl(); + assertThat(cacheControl.getHeaderValue()) + .isEqualTo("max-age=4, must-revalidate, no-transform, public, private, proxy-revalidate," + + " s-maxage=5, stale-if-error=6, stale-while-revalidate=7"); + } + + @Test + void invalidCacheControlCombination() { + Cache.Cachecontrol properties = this.properties.getCache().getCachecontrol(); + properties.setMaxAge(Duration.ofSeconds(4)); + properties.setNoStore(true); + CacheControl cacheControl = properties.toHttpCacheControl(); + assertThat(cacheControl.getHeaderValue()).isEqualTo("no-store"); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfigurationTests.java index 995f39d56c84..72b592b971e8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,8 +44,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link RestTemplateAutoConfiguration} @@ -58,6 +58,19 @@ class RestTemplateAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(RestTemplateAutoConfiguration.class)); + @Test + void restTemplateBuilderConfigurerShouldBeLazilyDefined() { + this.contextRunner.run((context) -> assertThat( + context.getBeanFactory().getBeanDefinition("restTemplateBuilderConfigurer").isLazyInit()).isTrue()); + } + + @Test + void restTemplateBuilderShouldBeLazilyDefined() { + this.contextRunner.run( + (context) -> assertThat(context.getBeanFactory().getBeanDefinition("restTemplateBuilder").isLazyInit()) + .isTrue()); + } + @Test void restTemplateWhenMessageConvertersDefinedShouldHaveMessageConverters() { this.contextRunner.withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class)) @@ -95,25 +108,41 @@ void restTemplateWhenHasCustomMessageConvertersShouldHaveMessageConverters() { } @Test - void restTemplateWhenHasCustomBuilderShouldUseCustomBuilder() { - this.contextRunner.withUserConfiguration(RestTemplateConfig.class, CustomRestTemplateBuilderConfig.class) + void restTemplateShouldApplyCustomizer() { + this.contextRunner.withUserConfiguration(RestTemplateConfig.class, RestTemplateCustomizerConfig.class) .run((context) -> { + assertThat(context).hasSingleBean(RestTemplate.class); + RestTemplate restTemplate = context.getBean(RestTemplate.class); + RestTemplateCustomizer customizer = context.getBean(RestTemplateCustomizer.class); + then(customizer).should().customize(restTemplate); + }); + } + + @Test + void restTemplateWhenHasCustomBuilderShouldUseCustomBuilder() { + this.contextRunner.withUserConfiguration(RestTemplateConfig.class, CustomRestTemplateBuilderConfig.class, + RestTemplateCustomizerConfig.class).run((context) -> { assertThat(context).hasSingleBean(RestTemplate.class); RestTemplate restTemplate = context.getBean(RestTemplate.class); assertThat(restTemplate.getMessageConverters()).hasSize(1); assertThat(restTemplate.getMessageConverters().get(0)) .isInstanceOf(CustomHttpMessageConverter.class); + then(context.getBean(RestTemplateCustomizer.class)).shouldHaveNoInteractions(); }); } @Test - void restTemplateShouldApplyCustomizer() { - this.contextRunner.withUserConfiguration(RestTemplateConfig.class, RestTemplateCustomizerConfig.class) + void restTemplateWhenHasCustomBuilderCouldReuseBuilderConfigurer() { + this.contextRunner.withUserConfiguration(RestTemplateConfig.class, + CustomRestTemplateBuilderWithConfigurerConfig.class, RestTemplateCustomizerConfig.class) .run((context) -> { assertThat(context).hasSingleBean(RestTemplate.class); RestTemplate restTemplate = context.getBean(RestTemplate.class); + assertThat(restTemplate.getMessageConverters()).hasSize(1); + assertThat(restTemplate.getMessageConverters().get(0)) + .isInstanceOf(CustomHttpMessageConverter.class); RestTemplateCustomizer customizer = context.getBean(RestTemplateCustomizer.class); - verify(customizer).customize(restTemplate); + then(customizer).should().customize(restTemplate); }); } @@ -140,14 +169,16 @@ void builderShouldBeFreshForEachUse() { @Test void whenServletWebApplicationRestTemplateBuilderIsConfigured() { new WebApplicationContextRunner().withConfiguration(AutoConfigurations.of(RestTemplateAutoConfiguration.class)) - .run((context) -> assertThat(context).hasSingleBean(RestTemplateBuilder.class)); + .run((context) -> assertThat(context).hasSingleBean(RestTemplateBuilder.class) + .hasSingleBean(RestTemplateBuilderConfigurer.class)); } @Test void whenReactiveWebApplicationRestTemplateBuilderIsNotConfigured() { new ReactiveWebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(RestTemplateAutoConfiguration.class)) - .run((context) -> assertThat(context).doesNotHaveBean(RestTemplateBuilder.class)); + .run((context) -> assertThat(context).doesNotHaveBean(RestTemplateBuilder.class) + .doesNotHaveBean(RestTemplateBuilderConfigurer.class)); } @Configuration(proxyBeanMethods = false) @@ -201,6 +232,16 @@ RestTemplateBuilder restTemplateBuilder() { } + @Configuration(proxyBeanMethods = false) + static class CustomRestTemplateBuilderWithConfigurerConfig { + + @Bean + RestTemplateBuilder restTemplateBuilder(RestTemplateBuilderConfigurer configurer) { + return configurer.configure(new RestTemplateBuilder()).messageConverters(new CustomHttpMessageConverter()); + } + + } + @Configuration(proxyBeanMethods = false) static class RestTemplateCustomizerConfig { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizerTests.java index 118dd8bf3c21..db30924210a1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,9 +18,12 @@ import java.io.File; import java.io.IOException; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.SynchronousQueue; import java.util.stream.Collectors; import org.eclipse.jetty.server.AbstractConnector; @@ -30,11 +33,16 @@ import org.eclipse.jetty.server.HttpConfiguration.ConnectionFactory; import org.eclipse.jetty.server.RequestLog; import org.eclipse.jetty.server.RequestLogWriter; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.BlockingArrayQueue; import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.eclipse.jetty.util.thread.ThreadPool; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.autoconfigure.web.ServerProperties.ForwardHeadersStrategy; +import org.springframework.boot.autoconfigure.web.ServerProperties.Jetty; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.context.properties.source.ConfigurationPropertySources; @@ -43,10 +51,11 @@ import org.springframework.boot.web.embedded.jetty.JettyWebServer; import org.springframework.mock.env.MockEnvironment; import org.springframework.test.context.support.TestPropertySourceUtils; +import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link JettyWebServerFactoryCustomizer}. @@ -76,14 +85,31 @@ void deduceUseForwardHeaders() { this.environment.setProperty("DYNO", "-"); ConfigurableJettyWebServerFactory factory = mock(ConfigurableJettyWebServerFactory.class); this.customizer.customize(factory); - verify(factory).setUseForwardHeaders(true); + then(factory).should().setUseForwardHeaders(true); } @Test void defaultUseForwardHeaders() { ConfigurableJettyWebServerFactory factory = mock(ConfigurableJettyWebServerFactory.class); this.customizer.customize(factory); - verify(factory).setUseForwardHeaders(false); + then(factory).should().setUseForwardHeaders(false); + } + + @Test + void forwardHeadersWhenStrategyIsNativeShouldConfigureValve() { + this.serverProperties.setForwardHeadersStrategy(ServerProperties.ForwardHeadersStrategy.NATIVE); + ConfigurableJettyWebServerFactory factory = mock(ConfigurableJettyWebServerFactory.class); + this.customizer.customize(factory); + then(factory).should().setUseForwardHeaders(true); + } + + @Test + void forwardHeadersWhenStrategyIsNoneShouldNotConfigureValve() { + this.environment.setProperty("DYNO", "-"); + this.serverProperties.setForwardHeadersStrategy(ServerProperties.ForwardHeadersStrategy.NONE); + ConfigurableJettyWebServerFactory factory = mock(ConfigurableJettyWebServerFactory.class); + this.customizer.customize(factory); + then(factory).should().setUseForwardHeaders(false); } @Test @@ -118,29 +144,101 @@ void accessLogCanBeEnabled() { } @Test - void maxThreadsCanBeCustomized() { - bind("server.jetty.max-threads=100"); + void threadPoolMatchesJettyDefaults() { + ThreadPool defaultThreadPool = new Server(0).getThreadPool(); + ThreadPool configuredThreadPool = customizeAndGetServer().getServer().getThreadPool(); + assertThat(defaultThreadPool).isInstanceOf(QueuedThreadPool.class); + assertThat(configuredThreadPool).isInstanceOf(QueuedThreadPool.class); + QueuedThreadPool defaultQueuedThreadPool = (QueuedThreadPool) defaultThreadPool; + QueuedThreadPool configuredQueuedThreadPool = (QueuedThreadPool) configuredThreadPool; + assertThat(configuredQueuedThreadPool.getMinThreads()).isEqualTo(defaultQueuedThreadPool.getMinThreads()); + assertThat(configuredQueuedThreadPool.getMaxThreads()).isEqualTo(defaultQueuedThreadPool.getMaxThreads()); + assertThat(configuredQueuedThreadPool.getIdleTimeout()).isEqualTo(defaultQueuedThreadPool.getIdleTimeout()); + BlockingQueue defaultQueue = getQueue(defaultThreadPool); + BlockingQueue configuredQueue = getQueue(configuredThreadPool); + assertThat(defaultQueue).isInstanceOf(BlockingArrayQueue.class); + assertThat(configuredQueue).isInstanceOf(BlockingArrayQueue.class); + assertThat(((BlockingArrayQueue) defaultQueue).getMaxCapacity()) + .isEqualTo(((BlockingArrayQueue) configuredQueue).getMaxCapacity()); + } + + @Test + void threadPoolMaxThreadsCanBeCustomized() { + bind("server.jetty.threads.max=100"); JettyWebServer server = customizeAndGetServer(); QueuedThreadPool threadPool = (QueuedThreadPool) server.getServer().getThreadPool(); assertThat(threadPool.getMaxThreads()).isEqualTo(100); } @Test - void minThreadsCanBeCustomized() { - bind("server.jetty.min-threads=100"); + void threadPoolMinThreadsCanBeCustomized() { + bind("server.jetty.threads.min=100"); JettyWebServer server = customizeAndGetServer(); QueuedThreadPool threadPool = (QueuedThreadPool) server.getServer().getThreadPool(); assertThat(threadPool.getMinThreads()).isEqualTo(100); } @Test - void threadIdleTimeoutCanBeCustomized() { - bind("server.jetty.thread-idle-timeout=100s"); + void threadPoolIdleTimeoutCanBeCustomized() { + bind("server.jetty.threads.idle-timeout=100s"); JettyWebServer server = customizeAndGetServer(); QueuedThreadPool threadPool = (QueuedThreadPool) server.getServer().getThreadPool(); assertThat(threadPool.getIdleTimeout()).isEqualTo(100000); } + @Test + void threadPoolWithMaxQueueCapacityEqualToZeroCreateSynchronousQueue() { + bind("server.jetty.threads.max-queue-capacity=0"); + JettyWebServer server = customizeAndGetServer(); + ThreadPool threadPool = server.getServer().getThreadPool(); + BlockingQueue queue = getQueue(threadPool); + assertThat(queue).isInstanceOf(SynchronousQueue.class); + assertDefaultThreadPoolSettings(threadPool); + } + + @Test + void threadPoolWithMaxQueueCapacityEqualToZeroCustomizesThreadPool() { + bind("server.jetty.threads.max-queue-capacity=0", "server.jetty.threads.min=100", + "server.jetty.threads.max=100", "server.jetty.threads.idle-timeout=6s"); + JettyWebServer server = customizeAndGetServer(); + QueuedThreadPool threadPool = (QueuedThreadPool) server.getServer().getThreadPool(); + assertThat(threadPool.getMinThreads()).isEqualTo(100); + assertThat(threadPool.getMaxThreads()).isEqualTo(100); + assertThat(threadPool.getIdleTimeout()).isEqualTo(Duration.ofSeconds(6).toMillis()); + } + + @Test + void threadPoolWithMaxQueueCapacityPositiveCreateBlockingArrayQueue() { + bind("server.jetty.threads.max-queue-capacity=1234"); + JettyWebServer server = customizeAndGetServer(); + ThreadPool threadPool = server.getServer().getThreadPool(); + BlockingQueue queue = getQueue(threadPool); + assertThat(queue).isInstanceOf(BlockingArrayQueue.class); + assertThat(((BlockingArrayQueue) queue).getMaxCapacity()).isEqualTo(1234); + assertDefaultThreadPoolSettings(threadPool); + } + + @Test + void threadPoolWithMaxQueueCapacityPositiveCustomizesThreadPool() { + bind("server.jetty.threads.max-queue-capacity=1234", "server.jetty.threads.min=10", + "server.jetty.threads.max=150", "server.jetty.threads.idle-timeout=3s"); + JettyWebServer server = customizeAndGetServer(); + QueuedThreadPool threadPool = (QueuedThreadPool) server.getServer().getThreadPool(); + assertThat(threadPool.getMinThreads()).isEqualTo(10); + assertThat(threadPool.getMaxThreads()).isEqualTo(150); + assertThat(threadPool.getIdleTimeout()).isEqualTo(Duration.ofSeconds(3).toMillis()); + } + + private void assertDefaultThreadPoolSettings(ThreadPool threadPool) { + assertThat(threadPool).isInstanceOf(QueuedThreadPool.class); + QueuedThreadPool queuedThreadPool = (QueuedThreadPool) threadPool; + Jetty defaultProperties = new Jetty(); + assertThat(queuedThreadPool.getMinThreads()).isEqualTo(defaultProperties.getThreads().getMin()); + assertThat(queuedThreadPool.getMaxThreads()).isEqualTo(defaultProperties.getThreads().getMax()); + assertThat(queuedThreadPool.getIdleTimeout()) + .isEqualTo(defaultProperties.getThreads().getIdleTimeout().toMillis()); + } + private CustomRequestLog getRequestLog(JettyWebServer server) { RequestLog requestLog = server.getServer().getRequestLog(); assertThat(requestLog).isInstanceOf(CustomRequestLog.class); @@ -155,10 +253,10 @@ private RequestLogWriter getLogWriter(CustomRequestLog requestLog) { @Test void setUseForwardHeaders() { - this.serverProperties.setUseForwardHeaders(true); + this.serverProperties.setForwardHeadersStrategy(ForwardHeadersStrategy.NATIVE); ConfigurableJettyWebServerFactory factory = mock(ConfigurableJettyWebServerFactory.class); this.customizer.customize(factory); - verify(factory).setUseForwardHeaders(true); + then(factory).should().setUseForwardHeaders(true); } @Test @@ -219,6 +317,10 @@ private List getRequestHeaderSizes(JettyWebServer server) { return requestHeaderSizes; } + private BlockingQueue getQueue(ThreadPool threadPool) { + return ReflectionTestUtils.invokeMethod(threadPool, "getQueue"); + } + private void bind(String... inlinedProperties) { TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment, inlinedProperties); new Binder(ConfigurationPropertySources.get(this.environment)).bind("server", diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizerTests.java index 36a5a5b0cbe9..960acbeba78e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,29 +19,30 @@ import java.time.Duration; import java.util.Map; -import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelOption; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; +import reactor.netty.http.server.HttpRequestDecoderSpec; import reactor.netty.http.server.HttpServer; -import reactor.netty.tcp.TcpServer; import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.autoconfigure.web.ServerProperties.ForwardHeadersStrategy; import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory; import org.springframework.boot.web.embedded.netty.NettyServerCustomizer; import org.springframework.mock.env.MockEnvironment; -import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.util.unit.DataSize; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; /** * Tests for {@link NettyWebServerFactoryCustomizer}. @@ -49,6 +50,7 @@ * @author Brian Clozel * @author Artsiom Yudovin */ +@ExtendWith(MockitoExtension.class) class NettyWebServerFactoryCustomizerTests { private MockEnvironment environment; @@ -62,7 +64,6 @@ class NettyWebServerFactoryCustomizerTests { @BeforeEach void setup() { - MockitoAnnotations.initMocks(this); this.environment = new MockEnvironment(); this.serverProperties = new ServerProperties(); ConfigurationPropertySources.attach(this.environment); @@ -74,79 +75,76 @@ void deduceUseForwardHeaders() { this.environment.setProperty("DYNO", "-"); NettyReactiveWebServerFactory factory = mock(NettyReactiveWebServerFactory.class); this.customizer.customize(factory); - verify(factory).setUseForwardHeaders(true); + then(factory).should().setUseForwardHeaders(true); } @Test void defaultUseForwardHeaders() { NettyReactiveWebServerFactory factory = mock(NettyReactiveWebServerFactory.class); this.customizer.customize(factory); - verify(factory).setUseForwardHeaders(false); + then(factory).should().setUseForwardHeaders(false); } @Test - void setUseForwardHeaders() { - this.serverProperties.setUseForwardHeaders(true); + void forwardHeadersWhenStrategyIsNativeShouldConfigureValve() { + this.serverProperties.setForwardHeadersStrategy(ServerProperties.ForwardHeadersStrategy.NATIVE); NettyReactiveWebServerFactory factory = mock(NettyReactiveWebServerFactory.class); this.customizer.customize(factory); - verify(factory).setUseForwardHeaders(true); + then(factory).should().setUseForwardHeaders(true); } @Test - void setServerConnectionTimeoutAsZero() { - setupServerConnectionTimeout(Duration.ZERO); - NettyReactiveWebServerFactory factory = mock(NettyReactiveWebServerFactory.class); - this.customizer.customize(factory); - verifyConnectionTimeout(factory, null); - } - - @Test - void setServerConnectionTimeoutAsMinusOne() { - setupServerConnectionTimeout(Duration.ofNanos(-1)); + void forwardHeadersWhenStrategyIsNoneShouldNotConfigureValve() { + this.environment.setProperty("DYNO", "-"); + this.serverProperties.setForwardHeadersStrategy(ServerProperties.ForwardHeadersStrategy.NONE); NettyReactiveWebServerFactory factory = mock(NettyReactiveWebServerFactory.class); this.customizer.customize(factory); - verifyConnectionTimeout(factory, 0); + then(factory).should().setUseForwardHeaders(false); } @Test - void setServerConnectionTimeout() { - setupServerConnectionTimeout(Duration.ofSeconds(1)); + void setConnectionTimeout() { + setupConnectionTimeout(Duration.ofSeconds(1)); NettyReactiveWebServerFactory factory = mock(NettyReactiveWebServerFactory.class); this.customizer.customize(factory); verifyConnectionTimeout(factory, 1000); } @Test - void setConnectionTimeout() { - setupConnectionTimeout(Duration.ofSeconds(1)); + void configureHttpRequestDecoder() { + ServerProperties.Netty nettyProperties = this.serverProperties.getNetty(); + nettyProperties.setValidateHeaders(false); + nettyProperties.setInitialBufferSize(DataSize.ofBytes(512)); + nettyProperties.setH2cMaxContentLength(DataSize.ofKilobytes(1)); + nettyProperties.setMaxChunkSize(DataSize.ofKilobytes(16)); + nettyProperties.setMaxInitialLineLength(DataSize.ofKilobytes(32)); NettyReactiveWebServerFactory factory = mock(NettyReactiveWebServerFactory.class); this.customizer.customize(factory); - verifyConnectionTimeout(factory, 1000); + then(factory).should().addServerCustomizers(this.customizerCaptor.capture()); + NettyServerCustomizer serverCustomizer = this.customizerCaptor.getValue(); + HttpServer httpServer = serverCustomizer.apply(HttpServer.create()); + HttpRequestDecoderSpec decoder = httpServer.configuration().decoder(); + assertThat(decoder.validateHeaders()).isFalse(); + assertThat(decoder.initialBufferSize()).isEqualTo(nettyProperties.getInitialBufferSize().toBytes()); + assertThat(decoder.h2cMaxContentLength()).isEqualTo(nettyProperties.getH2cMaxContentLength().toBytes()); + assertThat(decoder.maxChunkSize()).isEqualTo(nettyProperties.getMaxChunkSize().toBytes()); + assertThat(decoder.maxInitialLineLength()).isEqualTo(nettyProperties.getMaxInitialLineLength().toBytes()); } - @SuppressWarnings("unchecked") private void verifyConnectionTimeout(NettyReactiveWebServerFactory factory, Integer expected) { if (expected == null) { - verify(factory, never()).addServerCustomizers(any(NettyServerCustomizer.class)); + then(factory).should(never()).addServerCustomizers(any(NettyServerCustomizer.class)); return; } - verify(factory, times(1)).addServerCustomizers(this.customizerCaptor.capture()); - NettyServerCustomizer serverCustomizer = this.customizerCaptor.getValue(); + then(factory).should(times(2)).addServerCustomizers(this.customizerCaptor.capture()); + NettyServerCustomizer serverCustomizer = this.customizerCaptor.getAllValues().get(0); HttpServer httpServer = serverCustomizer.apply(HttpServer.create()); - TcpServer tcpConfiguration = ReflectionTestUtils.invokeMethod(httpServer, "tcpConfiguration"); - ServerBootstrap bootstrap = tcpConfiguration.configure(); - Map options = (Map) ReflectionTestUtils.getField(bootstrap, "options"); - assertThat(options).containsEntry(ChannelOption.CONNECT_TIMEOUT_MILLIS, expected); - } - - private void setupServerConnectionTimeout(Duration connectionTimeout) { - this.serverProperties.setUseForwardHeaders(null); - this.serverProperties.setMaxHttpHeaderSize(null); - this.serverProperties.setConnectionTimeout(connectionTimeout); + Map, ?> options = httpServer.configuration().options(); + assertThat(options.get(ChannelOption.CONNECT_TIMEOUT_MILLIS)).isEqualTo(expected); } private void setupConnectionTimeout(Duration connectionTimeout) { - this.serverProperties.setUseForwardHeaders(null); + this.serverProperties.setForwardHeadersStrategy(ForwardHeadersStrategy.NONE); this.serverProperties.setMaxHttpHeaderSize(null); this.serverProperties.getNetty().setConnectionTimeout(connectionTimeout); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizerTests.java index bde0378c9cd7..5e3015b81114 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,11 +26,14 @@ import org.apache.catalina.valves.ErrorReportValve; import org.apache.catalina.valves.RemoteIpValve; import org.apache.coyote.AbstractProtocol; +import org.apache.coyote.ajp.AbstractAjpProtocol; import org.apache.coyote.http11.AbstractHttp11Protocol; +import org.apache.coyote.http2.Http2Protocol; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.autoconfigure.web.ServerProperties.ForwardHeadersStrategy; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.context.properties.source.ConfigurationPropertySources; @@ -53,6 +56,8 @@ * @author Stephane Nicoll * @author Andrew McGhie * @author Rafiullah Hamedy + * @author Victor Mandujano + * @author Parviz Rozikov */ class TomcatWebServerFactoryCustomizerTests { @@ -94,6 +99,45 @@ void customProcessorCache() { .isEqualTo(100)); } + @Test + void customKeepAliveTimeout() { + bind("server.tomcat.keep-alive-timeout=30ms"); + customizeAndRunServer((server) -> assertThat( + ((AbstractProtocol) server.getTomcat().getConnector().getProtocolHandler()).getKeepAliveTimeout()) + .isEqualTo(30)); + } + + @Test + void defaultKeepAliveTimeoutWithHttp2() { + bind("server.http2.enabled=true"); + customizeAndRunServer((server) -> assertThat( + ((Http2Protocol) server.getTomcat().getConnector().findUpgradeProtocols()[0]).getKeepAliveTimeout()) + .isEqualTo(20000L)); + } + + @Test + void customKeepAliveTimeoutWithHttp2() { + bind("server.tomcat.keep-alive-timeout=30s", "server.http2.enabled=true"); + customizeAndRunServer((server) -> assertThat( + ((Http2Protocol) server.getTomcat().getConnector().findUpgradeProtocols()[0]).getKeepAliveTimeout()) + .isEqualTo(30000L)); + } + + @Test + void customMaxKeepAliveRequests() { + bind("server.tomcat.max-keep-alive-requests=-1"); + customizeAndRunServer((server) -> assertThat( + ((AbstractHttp11Protocol) server.getTomcat().getConnector().getProtocolHandler()) + .getMaxKeepAliveRequests()).isEqualTo(-1)); + } + + @Test + void defaultMaxKeepAliveRequests() { + customizeAndRunServer((server) -> assertThat( + ((AbstractHttp11Protocol) server.getTomcat().getConnector().getProtocolHandler()) + .getMaxKeepAliveRequests()).isEqualTo(100)); + } + @Test void unlimitedProcessorCache() { bind("server.tomcat.processor-cache=-1"); @@ -109,12 +153,6 @@ void customBackgroundProcessorDelay() { assertThat(server.getTomcat().getEngine().getBackgroundProcessorDelay()).isEqualTo(5); } - @Test - void customDisableMaxHttpPostSize() { - bind("server.tomcat.max-http-post-size=-1"); - customizeAndRunServer((server) -> assertThat(server.getTomcat().getConnector().getMaxPostSize()).isEqualTo(-1)); - } - @Test void customDisableMaxHttpFormPostSize() { bind("server.tomcat.max-http-form-post-size=-1"); @@ -129,13 +167,6 @@ void customMaxConnections() { .isEqualTo(5)); } - @Test - void customMaxHttpPostSize() { - bind("server.tomcat.max-http-post-size=10000"); - customizeAndRunServer( - (server) -> assertThat(server.getTomcat().getConnector().getMaxPostSize()).isEqualTo(10000)); - } - @Test void customMaxHttpFormPostSize() { bind("server.tomcat.max-http-form-post-size=10000"); @@ -177,9 +208,12 @@ void customMaxSwallowSize() { @Test void customRemoteIpValve() { - bind("server.tomcat.remote-ip-header=x-my-remote-ip-header", - "server.tomcat.protocol-header=x-my-protocol-header", "server.tomcat.internal-proxies=192.168.0.1", - "server.tomcat.port-header=x-my-forward-port", "server.tomcat.protocol-header-https-value=On"); + bind("server.tomcat.remoteip.remote-ip-header=x-my-remote-ip-header", + "server.tomcat.remoteip.protocol-header=x-my-protocol-header", + "server.tomcat.remoteip.internal-proxies=192.168.0.1", + "server.tomcat.remoteip.host-header=x-my-forward-host", + "server.tomcat.remoteip.port-header=x-my-forward-port", + "server.tomcat.remoteip.protocol-header-https-value=On"); TomcatServletWebServerFactory factory = customizeAndGetFactory(); assertThat(factory.getEngineValves()).hasSize(1); Valve valve = factory.getEngineValves().iterator().next(); @@ -188,6 +222,7 @@ void customRemoteIpValve() { assertThat(remoteIpValve.getProtocolHeader()).isEqualTo("x-my-protocol-header"); assertThat(remoteIpValve.getProtocolHeaderHttpsValue()).isEqualTo("On"); assertThat(remoteIpValve.getRemoteIpHeader()).isEqualTo("x-my-remote-ip-header"); + assertThat(remoteIpValve.getHostHeader()).isEqualTo("x-my-forward-host"); assertThat(remoteIpValve.getPortHeader()).isEqualTo("x-my-forward-port"); assertThat(remoteIpValve.getInternalProxies()).isEqualTo("192.168.0.1"); } @@ -234,17 +269,37 @@ void deduceUseForwardHeaders() { testRemoteIpValveConfigured(); } + @Test + void defaultUseForwardHeaders() { + TomcatServletWebServerFactory factory = customizeAndGetFactory(); + assertThat(factory.getEngineValves()).hasSize(0); + } + + @Test + void forwardHeadersWhenStrategyIsNativeShouldConfigureValve() { + this.serverProperties.setForwardHeadersStrategy(ServerProperties.ForwardHeadersStrategy.NATIVE); + testRemoteIpValveConfigured(); + } + + @Test + void forwardHeadersWhenStrategyIsNoneShouldNotConfigureValve() { + this.environment.setProperty("DYNO", "-"); + this.serverProperties.setForwardHeadersStrategy(ServerProperties.ForwardHeadersStrategy.NONE); + TomcatServletWebServerFactory factory = customizeAndGetFactory(); + assertThat(factory.getEngineValves()).hasSize(0); + } + @Test void defaultRemoteIpValve() { // Since 1.1.7 you need to specify at least the protocol - bind("server.tomcat.protocol-header=X-Forwarded-Proto", "server.tomcat.remote-ip-header=X-Forwarded-For"); + bind("server.tomcat.remoteip.protocol-header=X-Forwarded-Proto", + "server.tomcat.remoteip.remote-ip-header=X-Forwarded-For"); testRemoteIpValveConfigured(); } @Test - void setUseForwardHeaders() { - // Since 1.3.0 no need to explicitly set header names if use-forward-header=true - this.serverProperties.setUseForwardHeaders(true); + void setUseNativeForwardHeadersStrategy() { + this.serverProperties.setForwardHeadersStrategy(ForwardHeadersStrategy.NATIVE); testRemoteIpValveConfigured(); } @@ -277,7 +332,7 @@ void defaultBackgroundProcessorDelay() { @Test void disableRemoteIpValve() { - bind("server.tomcat.remote-ip-header=", "server.tomcat.protocol-header="); + bind("server.tomcat.remoteip.remote-ip-header=", "server.tomcat.remoteip.protocol-header="); TomcatServletWebServerFactory factory = customizeAndGetFactory(); assertThat(factory.getEngineValves()).isEmpty(); } @@ -298,8 +353,8 @@ void errorReportValveIsConfiguredToNotReportStackTraces() { @Test void testCustomizeMinSpareThreads() { - bind("server.tomcat.min-spare-threads=10"); - assertThat(this.serverProperties.getTomcat().getMinSpareThreads()).isEqualTo(10); + bind("server.tomcat.threads.min-spare=10"); + assertThat(this.serverProperties.getTomcat().getThreads().getMinSpare()).isEqualTo(10); } @Test @@ -434,7 +489,7 @@ void accessLogDoesNotUseIpv6CanonicalFormatByDefault() { } @Test - void accessLogwithIpv6CanonicalSet() { + void accessLogWithIpv6CanonicalSet() { bind("server.tomcat.accesslog.enabled=true", "server.tomcat.accesslog.ipv6-canonical=true"); TomcatServletWebServerFactory factory = customizeAndGetFactory(); assertThat(((AccessLogValve) factory.getEngineValves().iterator().next()).getIpv6Canonical()).isTrue(); @@ -444,6 +499,8 @@ void accessLogwithIpv6CanonicalSet() { void ajpConnectorCanBeCustomized() { TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(0); factory.setProtocol("AJP/1.3"); + factory.addConnectorCustomizers( + (connector) -> ((AbstractAjpProtocol) connector.getProtocolHandler()).setSecretRequired(false)); this.customizer.customize(factory); WebServer server = factory.getWebServer(); server.start(); @@ -474,6 +531,7 @@ private TomcatWebServer customizeAndGetServer() { private TomcatServletWebServerFactory customizeAndGetFactory() { TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(0); + factory.setHttp2(this.serverProperties.getHttp2()); this.customizer.customize(factory); return factory; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizerTests.java index 5fe8abc3ddcd..7b544fb3e6ba 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import org.junit.jupiter.api.Test; import org.xnio.Option; import org.xnio.OptionMap; +import org.xnio.Options; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.context.properties.bind.Bindable; @@ -40,9 +41,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.then; import static org.mockito.BDDMockito.willAnswer; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link UndertowWebServerFactoryCustomizer}. @@ -76,12 +77,12 @@ void customizeUndertowAccessLog() { "server.undertow.accesslog.dir=test-logs", "server.undertow.accesslog.rotate=false"); ConfigurableUndertowWebServerFactory factory = mock(ConfigurableUndertowWebServerFactory.class); this.customizer.customize(factory); - verify(factory).setAccessLogEnabled(true); - verify(factory).setAccessLogPattern("foo"); - verify(factory).setAccessLogPrefix("test_log"); - verify(factory).setAccessLogSuffix("txt"); - verify(factory).setAccessLogDirectory(new File("test-logs")); - verify(factory).setAccessLogRotate(false); + then(factory).should().setAccessLogEnabled(true); + then(factory).should().setAccessLogPattern("foo"); + then(factory).should().setAccessLogPrefix("test_log"); + then(factory).should().setAccessLogSuffix("txt"); + then(factory).should().setAccessLogDirectory(new File("test-logs")); + then(factory).should().setAccessLogRotate(false); } @Test @@ -132,6 +133,22 @@ void customMaxCookies() { assertThat(boundServerOption(UndertowOptions.MAX_COOKIES)).isEqualTo(4); } + @Test + void customizeIoThreads() { + bind("server.undertow.threads.io=4"); + ConfigurableUndertowWebServerFactory factory = mock(ConfigurableUndertowWebServerFactory.class); + this.customizer.customize(factory); + then(factory).should().setIoThreads(4); + } + + @Test + void customizeWorkerThreads() { + bind("server.undertow.threads.worker=10"); + ConfigurableUndertowWebServerFactory factory = mock(ConfigurableUndertowWebServerFactory.class); + this.customizer.customize(factory); + then(factory).should().setWorkerThreads(10); + } + @Test void allowEncodedSlashes() { bind("server.undertow.allow-encoded-slash=true"); @@ -170,13 +187,14 @@ void customServerOptionShouldBeRelaxed() { @Test void customSocketOption() { - bind("server.undertow.options.socket.ALWAYS_SET_KEEP_ALIVE=false"); - assertThat(boundSocketOption(UndertowOptions.ALWAYS_SET_KEEP_ALIVE)).isFalse(); + bind("server.undertow.options.socket.CONNECTION_LOW_WATER=8"); + assertThat(boundSocketOption(Options.CONNECTION_LOW_WATER)).isEqualTo(8); } + @Test void customSocketOptionShouldBeRelaxed() { - bind("server.undertow.options.socket.always-set-keep-alive=false"); - assertThat(boundSocketOption(UndertowOptions.ALWAYS_SET_KEEP_ALIVE)).isFalse(); + bind("server.undertow.options.socket.connection-low-water=8"); + assertThat(boundSocketOption(Options.CONNECTION_LOW_WATER)).isEqualTo(8); } @Test @@ -184,22 +202,31 @@ void deduceUseForwardHeaders() { this.environment.setProperty("DYNO", "-"); ConfigurableUndertowWebServerFactory factory = mock(ConfigurableUndertowWebServerFactory.class); this.customizer.customize(factory); - verify(factory).setUseForwardHeaders(true); + then(factory).should().setUseForwardHeaders(true); } @Test void defaultUseForwardHeaders() { ConfigurableUndertowWebServerFactory factory = mock(ConfigurableUndertowWebServerFactory.class); this.customizer.customize(factory); - verify(factory).setUseForwardHeaders(false); + then(factory).should().setUseForwardHeaders(false); + } + + @Test + void forwardHeadersWhenStrategyIsNativeShouldConfigureValve() { + this.serverProperties.setForwardHeadersStrategy(ServerProperties.ForwardHeadersStrategy.NATIVE); + ConfigurableUndertowWebServerFactory factory = mock(ConfigurableUndertowWebServerFactory.class); + this.customizer.customize(factory); + then(factory).should().setUseForwardHeaders(true); } @Test - void setUseForwardHeaders() { - this.serverProperties.setUseForwardHeaders(true); + void forwardHeadersWhenStrategyIsNoneShouldNotConfigureValve() { + this.environment.setProperty("DYNO", "-"); + this.serverProperties.setForwardHeadersStrategy(ServerProperties.ForwardHeadersStrategy.NONE); ConfigurableUndertowWebServerFactory factory = mock(ConfigurableUndertowWebServerFactory.class); this.customizer.customize(factory); - verify(factory).setUseForwardHeaders(true); + then(factory).should().setUseForwardHeaders(false); } private T boundServerOption(Option option) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/format/WebConversionServiceTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/format/WebConversionServiceTests.java index 7b4724122d38..dd6d51c340b4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/format/WebConversionServiceTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/format/WebConversionServiceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,19 @@ package org.springframework.boot.autoconfigure.web.format; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.OffsetTime; import java.time.ZoneId; +import java.time.ZoneOffset; import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.util.Calendar; import java.util.Date; -import org.joda.time.DateTime; -import org.joda.time.LocalDate; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -31,18 +38,29 @@ * * @author Brian Clozel * @author Madhura Bhave + * @author Gaurav Pareek */ class WebConversionServiceTests { @Test - void customDateFormatWithJavaUtilDate() { - customDateFormat(Date.from(ZonedDateTime.of(2018, 1, 1, 20, 30, 0, 0, ZoneId.systemDefault()).toInstant())); + void defaultDateFormat() { + WebConversionService conversionService = new WebConversionService(new DateTimeFormatters()); + LocalDate date = LocalDate.of(2020, 4, 26); + assertThat(conversionService.convert(date, String.class)) + .isEqualTo(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).format(date)); } @Test - @Deprecated - void customDateFormatWithJodaTime() { - customDateFormat(LocalDate.fromDateFields(new DateTime(2018, 1, 1, 20, 30).toDate())); + void isoDateFormat() { + WebConversionService conversionService = new WebConversionService(new DateTimeFormatters().dateFormat("iso")); + LocalDate date = LocalDate.of(2020, 4, 26); + assertThat(conversionService.convert(date, String.class)) + .isEqualTo(DateTimeFormatter.ISO_LOCAL_DATE.format(date)); + } + + @Test + void customDateFormatWithJavaUtilDate() { + customDateFormat(Date.from(ZonedDateTime.of(2018, 1, 1, 20, 30, 0, 0, ZoneId.systemDefault()).toInstant())); } @Test @@ -50,16 +68,120 @@ void customDateFormatWithJavaTime() { customDateFormat(java.time.LocalDate.of(2018, 1, 1)); } - private void customDateFormat(Object input) { - WebConversionService conversionService = new WebConversionService("dd*MM*yyyy"); - assertThat(conversionService.convert(input, String.class)).isEqualTo("01*01*2018"); + @Test + void defaultTimeFormat() { + WebConversionService conversionService = new WebConversionService(new DateTimeFormatters()); + LocalTime time = LocalTime.of(12, 45, 23); + assertThat(conversionService.convert(time, String.class)) + .isEqualTo(DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).format(time)); + } + + @Test + void isoTimeFormat() { + WebConversionService conversionService = new WebConversionService(new DateTimeFormatters().timeFormat("iso")); + LocalTime time = LocalTime.of(12, 45, 23); + assertThat(conversionService.convert(time, String.class)) + .isEqualTo(DateTimeFormatter.ISO_LOCAL_TIME.format(time)); + } + + @Test + void isoOffsetTimeFormat() { + isoOffsetTimeFormat(new DateTimeFormatters().timeFormat("isooffset")); + } + + @Test + void hyphenatedIsoOffsetTimeFormat() { + isoOffsetTimeFormat(new DateTimeFormatters().timeFormat("iso-offset")); + } + + private void isoOffsetTimeFormat(DateTimeFormatters formatters) { + WebConversionService conversionService = new WebConversionService(formatters); + OffsetTime offsetTime = OffsetTime.of(LocalTime.of(12, 45, 23), ZoneOffset.ofHoursMinutes(1, 30)); + assertThat(conversionService.convert(offsetTime, String.class)) + .isEqualTo(DateTimeFormatter.ISO_OFFSET_TIME.format(offsetTime)); + } + + @Test + void customTimeFormat() { + WebConversionService conversionService = new WebConversionService( + new DateTimeFormatters().timeFormat("HH*mm*ss")); + LocalTime time = LocalTime.of(12, 45, 23); + assertThat(conversionService.convert(time, String.class)).isEqualTo("12*45*23"); + } + + @Test + void defaultDateTimeFormat() { + WebConversionService conversionService = new WebConversionService(new DateTimeFormatters()); + LocalDateTime dateTime = LocalDateTime.of(2020, 4, 26, 12, 45, 23); + assertThat(conversionService.convert(dateTime, String.class)) + .isEqualTo(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT).format(dateTime)); + } + + @Test + void isoDateTimeFormat() { + WebConversionService conversionService = new WebConversionService( + new DateTimeFormatters().dateTimeFormat("iso")); + LocalDateTime dateTime = LocalDateTime.of(2020, 4, 26, 12, 45, 23); + assertThat(conversionService.convert(dateTime, String.class)) + .isEqualTo(DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(dateTime)); + } + + @Test + void isoOffsetDateTimeFormat() { + isoOffsetDateTimeFormat(new DateTimeFormatters().dateTimeFormat("isooffset")); + } + + @Test + void hyphenatedIsoOffsetDateTimeFormat() { + isoOffsetDateTimeFormat(new DateTimeFormatters().dateTimeFormat("iso-offset")); + } + + private void isoOffsetDateTimeFormat(DateTimeFormatters formatters) { + WebConversionService conversionService = new WebConversionService(formatters); + OffsetDateTime offsetDateTime = OffsetDateTime.of(LocalDate.of(2020, 4, 26), LocalTime.of(12, 45, 23), + ZoneOffset.ofHoursMinutes(1, 30)); + assertThat(conversionService.convert(offsetDateTime, String.class)) + .isEqualTo(DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(offsetDateTime)); + } + + @Test + void customDateTimeFormat() { + WebConversionService conversionService = new WebConversionService( + new DateTimeFormatters().dateTimeFormat("dd*MM*yyyy HH*mm*ss")); + LocalDateTime dateTime = LocalDateTime.of(2020, 4, 26, 12, 45, 23); + assertThat(conversionService.convert(dateTime, String.class)).isEqualTo("26*04*2020 12*45*23"); } @Test - void convertFromStringToDate() { - WebConversionService conversionService = new WebConversionService("yyyy-MM-dd"); - java.time.LocalDate date = conversionService.convert("2018-01-01", java.time.LocalDate.class); + void convertFromStringToLocalDate() { + WebConversionService conversionService = new WebConversionService( + new DateTimeFormatters().dateFormat("yyyy-MM-dd")); + LocalDate date = conversionService.convert("2018-01-01", LocalDate.class); assertThat(date).isEqualTo(java.time.LocalDate.of(2018, 1, 1)); } + @Test + void convertFromStringToLocalDateWithIsoFormatting() { + WebConversionService conversionService = new WebConversionService(new DateTimeFormatters().dateFormat("iso")); + LocalDate date = conversionService.convert("2018-01-01", LocalDate.class); + assertThat(date).isEqualTo(java.time.LocalDate.of(2018, 1, 1)); + } + + @Test + void convertFromStringToDateWithIsoFormatting() { + WebConversionService conversionService = new WebConversionService(new DateTimeFormatters().dateFormat("iso")); + Date date = conversionService.convert("2018-01-01", Date.class); + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + assertThat(calendar.get(Calendar.YEAR)).isEqualTo(2018); + assertThat(calendar.get(Calendar.MONTH)).isEqualTo(Calendar.JANUARY); + assertThat(calendar.get(Calendar.DAY_OF_MONTH)).isEqualTo(1); + } + + 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"); + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/HttpHandlerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/HttpHandlerAutoConfigurationTests.java index a3f97f777a5b..4c44adc55699 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/HttpHandlerAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/HttpHandlerAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,15 +16,19 @@ package org.springframework.boot.autoconfigure.web.reactive; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.server.reactive.ContextPathCompositeHandler; import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.server.WebHandler; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.web.reactive.function.server.RequestPredicates.GET; @@ -56,6 +60,24 @@ void shouldConfigureHttpHandlerAnnotation() { .run((context) -> assertThat(context).hasSingleBean(HttpHandler.class)); } + @Test + void shouldConfigureHttpHandlerWithoutWebFluxAutoConfiguration() { + this.contextRunner.withUserConfiguration(CustomWebHandler.class) + .run((context) -> assertThat(context).hasSingleBean(HttpHandler.class)); + } + + @Test + void shouldConfigureBasePathCompositeHandler() { + this.contextRunner.withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class)) + .withPropertyValues("spring.webflux.base-path=/something").run((context) -> { + assertThat(context).hasSingleBean(HttpHandler.class); + HttpHandler httpHandler = context.getBean(HttpHandler.class); + assertThat(httpHandler).isInstanceOf(ContextPathCompositeHandler.class) + .extracting("handlerMap", InstanceOfAssertFactories.map(String.class, HttpHandler.class)) + .containsKey("/something"); + }); + } + @Configuration(proxyBeanMethods = false) static class CustomHttpHandler { @@ -71,4 +93,14 @@ RouterFunction routerFunction() { } + @Configuration(proxyBeanMethods = false) + static class CustomWebHandler { + + @Bean + WebHandler webHandler() { + return new DispatcherHandler(); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/MockReactiveWebServerFactory.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/MockReactiveWebServerFactory.java index 027b1f2e71b0..706fac198b2b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/MockReactiveWebServerFactory.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/MockReactiveWebServerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,6 @@ import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory; import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory; import org.springframework.boot.web.server.WebServer; -import org.springframework.boot.web.server.WebServerException; import org.springframework.http.server.reactive.HttpHandler; import static org.mockito.Mockito.spy; @@ -72,12 +71,12 @@ Map getHttpHandlerMap() { } @Override - public void start() throws WebServerException { + public void start() { } @Override - public void stop() throws WebServerException { + public void stop() { } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryAutoConfigurationTests.java index 112c2efb4f80..4b549494d604 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -53,9 +53,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; /** * Tests for {@link ReactiveWebServerFactoryAutoConfiguration}. @@ -66,7 +65,7 @@ */ class ReactiveWebServerFactoryAutoConfigurationTests { - private ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner( + private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner( AnnotationConfigReactiveWebServerApplicationContext::new) .withConfiguration(AutoConfigurations.of(ReactiveWebServerFactoryAutoConfiguration.class)); @@ -128,7 +127,7 @@ void tomcatConnectorCustomizerBeanIsAddedToFactory() { TomcatConnectorCustomizer customizer = context.getBean("connectorCustomizer", TomcatConnectorCustomizer.class); assertThat(factory.getTomcatConnectorCustomizers()).contains(customizer); - verify(customizer, times(1)).customize(any(Connector.class)); + then(customizer).should().customize(any(Connector.class)); }); } @@ -145,7 +144,7 @@ void tomcatConnectorCustomizerRegisteredAsBeanAndViaFactoryIsOnlyCalledOnce() { TomcatConnectorCustomizer customizer = context.getBean("connectorCustomizer", TomcatConnectorCustomizer.class); assertThat(factory.getTomcatConnectorCustomizers()).contains(customizer); - verify(customizer, times(1)).customize(any(Connector.class)); + then(customizer).should().customize(any(Connector.class)); }); } @@ -161,7 +160,7 @@ void tomcatContextCustomizerBeanIsAddedToFactory() { TomcatReactiveWebServerFactory factory = context.getBean(TomcatReactiveWebServerFactory.class); TomcatContextCustomizer customizer = context.getBean("contextCustomizer", TomcatContextCustomizer.class); assertThat(factory.getTomcatContextCustomizers()).contains(customizer); - verify(customizer, times(1)).customize(any(Context.class)); + then(customizer).should().customize(any(Context.class)); }); } @@ -177,7 +176,7 @@ void tomcatContextCustomizerRegisteredAsBeanAndViaFactoryIsOnlyCalledOnce() { TomcatReactiveWebServerFactory factory = context.getBean(TomcatReactiveWebServerFactory.class); TomcatContextCustomizer customizer = context.getBean("contextCustomizer", TomcatContextCustomizer.class); assertThat(factory.getTomcatContextCustomizers()).contains(customizer); - verify(customizer, times(1)).customize(any(Context.class)); + then(customizer).should().customize(any(Context.class)); }); } @@ -194,7 +193,7 @@ void tomcatProtocolHandlerCustomizerBeanIsAddedToFactory() { TomcatProtocolHandlerCustomizer customizer = context.getBean("protocolHandlerCustomizer", TomcatProtocolHandlerCustomizer.class); assertThat(factory.getTomcatProtocolHandlerCustomizers()).contains(customizer); - verify(customizer, times(1)).customize(any()); + then(customizer).should().customize(any()); }); } @@ -211,7 +210,7 @@ void tomcatProtocolHandlerCustomizerRegisteredAsBeanAndViaFactoryIsOnlyCalledOnc TomcatProtocolHandlerCustomizer customizer = context.getBean("protocolHandlerCustomizer", TomcatProtocolHandlerCustomizer.class); assertThat(factory.getTomcatProtocolHandlerCustomizers()).contains(customizer); - verify(customizer, times(1)).customize(any()); + then(customizer).should().customize(any()); }); } @@ -238,7 +237,7 @@ void jettyServerCustomizerRegisteredAsBeanAndViaFactoryIsOnlyCalledOnce() { JettyReactiveWebServerFactory factory = context.getBean(JettyReactiveWebServerFactory.class); JettyServerCustomizer customizer = context.getBean("serverCustomizer", JettyServerCustomizer.class); assertThat(factory.getServerCustomizers()).contains(customizer); - verify(customizer, times(1)).customize(any(Server.class)); + then(customizer).should().customize(any(Server.class)); }); } @@ -266,7 +265,7 @@ void undertowBuilderCustomizerRegisteredAsBeanAndViaFactoryIsOnlyCalledOnce() { UndertowBuilderCustomizer customizer = context.getBean("builderCustomizer", UndertowBuilderCustomizer.class); assertThat(factory.getBuilderCustomizers()).contains(customizer); - verify(customizer, times(1)).customize(any(Builder.class)); + then(customizer).should().customize(any(Builder.class)); }); } @@ -293,7 +292,7 @@ void nettyServerCustomizerRegisteredAsBeanAndViaFactoryIsOnlyCalledOnce() { NettyReactiveWebServerFactory factory = context.getBean(NettyReactiveWebServerFactory.class); NettyServerCustomizer customizer = context.getBean("serverCustomizer", NettyServerCustomizer.class); assertThat(factory.getServerCustomizers()).contains(customizer); - verify(customizer, times(1)).apply(any(HttpServer.class)); + then(customizer).should().apply(any(HttpServer.class)); }); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryCustomizerTests.java index e3c676f4fd7d..63a7344911f8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryCustomizerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryCustomizerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,13 +20,16 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.web.reactive.server.ConfigurableReactiveWebServerFactory; +import org.springframework.boot.web.server.Shutdown; import org.springframework.boot.web.server.Ssl; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link ReactiveWebServerFactoryCustomizer}. @@ -50,7 +53,7 @@ void testCustomizeServerPort() { ConfigurableReactiveWebServerFactory factory = mock(ConfigurableReactiveWebServerFactory.class); this.properties.setPort(9000); this.customizer.customize(factory); - verify(factory).setPort(9000); + then(factory).should().setPort(9000); } @Test @@ -59,7 +62,7 @@ void testCustomizeServerAddress() { InetAddress address = mock(InetAddress.class); this.properties.setAddress(address); this.customizer.customize(factory); - verify(factory).setAddress(address); + then(factory).should().setAddress(address); } @Test @@ -68,7 +71,17 @@ void testCustomizeServerSsl() { Ssl ssl = mock(Ssl.class); this.properties.setSsl(ssl); this.customizer.customize(factory); - verify(factory).setSsl(ssl); + then(factory).should().setSsl(ssl); + } + + @Test + void whenShutdownPropertyIsSetThenShutdownIsCustomized() { + this.properties.setShutdown(Shutdown.GRACEFUL); + ConfigurableReactiveWebServerFactory factory = mock(ConfigurableReactiveWebServerFactory.class); + this.customizer.customize(factory); + ArgumentCaptor shutdownCaptor = ArgumentCaptor.forClass(Shutdown.class); + then(factory).should().setShutdown(shutdownCaptor.capture()); + assertThat(shutdownCaptor.getValue()).isEqualTo(Shutdown.GRACEFUL); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java index 463fb25de0c0..56dfbd022c86 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,12 @@ package org.springframework.boot.autoconfigure.web.reactive; +import java.time.LocalDateTime; +import java.time.LocalTime; import java.time.ZoneId; import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; import java.util.Collections; import java.util.Date; import java.util.List; @@ -29,10 +33,13 @@ import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.autoconfigure.validation.ValidatorAdapter; +import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration.WebFluxConfig; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.boot.web.codec.CodecCustomizer; import org.springframework.boot.web.reactive.filter.OrderedHiddenHttpMethodFilter; @@ -40,6 +47,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.context.i18n.LocaleContext; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.core.convert.ConversionService; @@ -50,14 +58,19 @@ import org.springframework.http.CacheControl; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.util.StringUtils; import org.springframework.validation.Validator; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.web.filter.reactive.HiddenHttpMethodFilter; import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; +import org.springframework.web.reactive.config.DelegatingWebFluxConfiguration; import org.springframework.web.reactive.config.WebFluxConfigurationSupport; import org.springframework.web.reactive.config.WebFluxConfigurer; +import org.springframework.web.reactive.function.server.support.RouterFunctionMapping; import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping; import org.springframework.web.reactive.resource.CachingResourceResolver; import org.springframework.web.reactive.resource.CachingResourceTransformer; @@ -68,12 +81,19 @@ import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.reactive.result.view.ViewResolutionResultHandler; import org.springframework.web.reactive.result.view.ViewResolver; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebSession; +import org.springframework.web.server.adapter.WebHttpHandlerBuilder; +import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver; +import org.springframework.web.server.i18n.FixedLocaleContextResolver; +import org.springframework.web.server.i18n.LocaleContextResolver; +import org.springframework.web.server.session.WebSessionManager; import org.springframework.web.util.pattern.PathPattern; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link WebFluxAutoConfiguration}. @@ -86,7 +106,7 @@ class WebFluxAutoConfigurationTests { private static final MockReactiveWebServerFactory mockReactiveWebServerFactory = new MockReactiveWebServerFactory(); - private ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() + private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class)) .withUserConfiguration(Config.class); @@ -104,6 +124,9 @@ void shouldCreateDefaultBeans() { assertThat(context).getBeans(RequestMappingHandlerMapping.class).hasSize(1); assertThat(context).getBeans(RequestMappingHandlerAdapter.class).hasSize(1); assertThat(context).getBeans(RequestedContentTypeResolver.class).hasSize(1); + assertThat(context).getBeans(RouterFunctionMapping.class).hasSize(1); + assertThat(context.getBean(WebHttpHandlerBuilder.WEB_SESSION_MANAGER_BEAN_NAME, WebSessionManager.class)) + .isNotNull(); assertThat(context.getBean("resourceHandlerMapping", HandlerMapping.class)).isNotNull(); }); } @@ -125,7 +148,7 @@ void shouldCustomizeCodecs() { this.contextRunner.withUserConfiguration(CustomCodecCustomizers.class).run((context) -> { CodecCustomizer codecCustomizer = context.getBean("firstCodecCustomizer", CodecCustomizer.class); assertThat(codecCustomizer).isNotNull(); - verify(codecCustomizer).customize(any(ServerCodecConfigurer.class)); + then(codecCustomizer).should().customize(any(ServerCodecConfigurer.class)); }); } @@ -150,20 +173,22 @@ void shouldMapResourcesToCustomPath() { SimpleUrlHandlerMapping hm = context.getBean("resourceHandlerMapping", SimpleUrlHandlerMapping.class); assertThat(hm.getUrlMap().get("/static/**")).isInstanceOf(ResourceWebHandler.class); ResourceWebHandler staticHandler = (ResourceWebHandler) hm.getUrlMap().get("/static/**"); - assertThat(staticHandler.getLocations()).hasSize(4); + assertThat(staticHandler).extracting("locationValues").asList().hasSize(4); }); } - @Test - void shouldNotMapResourcesWhenDisabled() { - this.contextRunner.withPropertyValues("spring.resources.add-mappings:false") + @ParameterizedTest + @ValueSource(strings = { "spring.resources.", "spring.web.resources." }) + void shouldNotMapResourcesWhenDisabled(String prefix) { + this.contextRunner.withPropertyValues(prefix + ".add-mappings:false") .run((context) -> assertThat(context.getBean("resourceHandlerMapping")) .isNotInstanceOf(SimpleUrlHandlerMapping.class)); } - @Test - void resourceHandlerChainEnabled() { - this.contextRunner.withPropertyValues("spring.resources.chain.enabled:true").run((context) -> { + @ParameterizedTest + @ValueSource(strings = { "spring.resources.", "spring.web.resources." }) + void resourceHandlerChainEnabled(String prefix) { + this.contextRunner.withPropertyValues(prefix + "chain.enabled:true").run((context) -> { SimpleUrlHandlerMapping hm = context.getBean("resourceHandlerMapping", SimpleUrlHandlerMapping.class); assertThat(hm.getUrlMap().get("/**")).isInstanceOf(ResourceWebHandler.class); ResourceWebHandler staticHandler = (ResourceWebHandler) hm.getUrlMap().get("/**"); @@ -185,7 +210,7 @@ void shouldRegisterViewResolvers() { } @Test - void noDateFormat() { + void defaultDateFormat() { this.contextRunner.run((context) -> { FormattingConversionService conversionService = context.getBean(FormattingConversionService.class); Date date = Date.from(ZonedDateTime.of(1988, 6, 25, 20, 30, 0, 0, ZoneId.systemDefault()).toInstant()); @@ -195,14 +220,52 @@ void noDateFormat() { } @Test - void overrideDateFormat() { - this.contextRunner.withPropertyValues("spring.webflux.date-format:dd*MM*yyyy").run((context) -> { + void customDateFormat() { + this.contextRunner.withPropertyValues("spring.webflux.format.date:dd*MM*yyyy").run((context) -> { FormattingConversionService conversionService = context.getBean(FormattingConversionService.class); Date date = Date.from(ZonedDateTime.of(1988, 6, 25, 20, 30, 0, 0, ZoneId.systemDefault()).toInstant()); assertThat(conversionService.convert(date, String.class)).isEqualTo("25*06*1988"); }); } + @Test + void defaultTimeFormat() { + this.contextRunner.run((context) -> { + FormattingConversionService conversionService = context.getBean(FormattingConversionService.class); + LocalTime time = LocalTime.of(11, 43, 10); + assertThat(conversionService.convert(time, String.class)) + .isEqualTo(DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).format(time)); + }); + } + + @Test + void customTimeFormat() { + this.contextRunner.withPropertyValues("spring.webflux.format.time=HH:mm:ss").run((context) -> { + FormattingConversionService conversionService = context.getBean(FormattingConversionService.class); + LocalTime time = LocalTime.of(11, 43, 10); + assertThat(conversionService.convert(time, String.class)).isEqualTo("11:43:10"); + }); + } + + @Test + void defaultDateTimeFormat() { + this.contextRunner.run((context) -> { + FormattingConversionService conversionService = context.getBean(FormattingConversionService.class); + LocalDateTime dateTime = LocalDateTime.of(2020, 4, 28, 11, 43, 10); + assertThat(conversionService.convert(dateTime, String.class)) + .isEqualTo(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT).format(dateTime)); + }); + } + + @Test + void customDateTimeTimeFormat() { + this.contextRunner.withPropertyValues("spring.webflux.format.date-time=yyyy-MM-dd HH:mm:ss").run((context) -> { + FormattingConversionService conversionService = context.getBean(FormattingConversionService.class); + LocalDateTime dateTime = LocalDateTime.of(2020, 4, 28, 11, 43, 10); + assertThat(conversionService.convert(dateTime, String.class)).isEqualTo("2020-04-28 11:43:10"); + }); + } + @Test void validatorWhenNoValidatorShouldUseDefault() { this.contextRunner.run((context) -> { @@ -324,16 +387,20 @@ void hiddenHttpMethodFilterCanBeEnabled() { @Test void customRequestMappingHandlerMapping() { - this.contextRunner.withUserConfiguration(CustomRequestMappingHandlerMapping.class) - .run((context) -> assertThat(context).getBean(RequestMappingHandlerMapping.class) - .isInstanceOf(MyRequestMappingHandlerMapping.class)); + this.contextRunner.withUserConfiguration(CustomRequestMappingHandlerMapping.class).run((context) -> { + assertThat(context).getBean(RequestMappingHandlerMapping.class) + .isInstanceOf(MyRequestMappingHandlerMapping.class); + assertThat(context.getBean(CustomRequestMappingHandlerMapping.class).handlerMappings).isEqualTo(1); + }); } @Test void customRequestMappingHandlerAdapter() { - this.contextRunner.withUserConfiguration(CustomRequestMappingHandlerAdapter.class) - .run((context) -> assertThat(context).getBean(RequestMappingHandlerAdapter.class) - .isInstanceOf(MyRequestMappingHandlerAdapter.class)); + this.contextRunner.withUserConfiguration(CustomRequestMappingHandlerAdapter.class).run((context) -> { + assertThat(context).getBean(RequestMappingHandlerAdapter.class) + .isInstanceOf(MyRequestMappingHandlerAdapter.class); + assertThat(context.getBean(CustomRequestMappingHandlerAdapter.class).handlerAdapters).isEqualTo(1); + }); } @Test @@ -346,39 +413,54 @@ void multipleWebFluxRegistrations() { }); } - @Test - void cachePeriod() { + @ParameterizedTest + @ValueSource(strings = { "spring.resources.", "spring.web.resources." }) + void cachePeriod(String prefix) { Assertions.setExtractBareNamePropertyMethods(false); - this.contextRunner.withPropertyValues("spring.resources.cache.period:5").run((context) -> { + this.contextRunner.withPropertyValues(prefix + "cache.period:5").run((context) -> { Map handlerMap = getHandlerMap(context); assertThat(handlerMap).hasSize(2); for (Object handler : handlerMap.values()) { if (handler instanceof ResourceWebHandler) { - assertThat(((ResourceWebHandler) handler).getCacheControl()) - .isEqualToComparingFieldByField(CacheControl.maxAge(5, TimeUnit.SECONDS)); + assertThat(((ResourceWebHandler) handler).getCacheControl()).usingRecursiveComparison() + .isEqualTo(CacheControl.maxAge(5, TimeUnit.SECONDS)); } } }); Assertions.setExtractBareNamePropertyMethods(true); } - @Test - void cacheControl() { + @ParameterizedTest + @ValueSource(strings = { "spring.resources.", "spring.web.resources." }) + void cacheControl(String prefix) { Assertions.setExtractBareNamePropertyMethods(false); - this.contextRunner.withPropertyValues("spring.resources.cache.cachecontrol.max-age:5", - "spring.resources.cache.cachecontrol.proxy-revalidate:true").run((context) -> { + this.contextRunner.withPropertyValues(prefix + "cache.cachecontrol.max-age:5", + prefix + "cache.cachecontrol.proxy-revalidate:true").run((context) -> { Map handlerMap = getHandlerMap(context); assertThat(handlerMap).hasSize(2); for (Object handler : handlerMap.values()) { if (handler instanceof ResourceWebHandler) { - assertThat(((ResourceWebHandler) handler).getCacheControl()).isEqualToComparingFieldByField( - CacheControl.maxAge(5, TimeUnit.SECONDS).proxyRevalidate()); + assertThat(((ResourceWebHandler) handler).getCacheControl()).usingRecursiveComparison() + .isEqualTo(CacheControl.maxAge(5, TimeUnit.SECONDS).proxyRevalidate()); } } }); Assertions.setExtractBareNamePropertyMethods(true); } + @Test + void useLastModified() { + this.contextRunner.withPropertyValues("spring.web.resources.cache.use-last-modified=false").run((context) -> { + Map handlerMap = getHandlerMap(context); + assertThat(handlerMap).hasSize(2); + for (Object handler : handlerMap.values()) { + if (handler instanceof ResourceWebHandler) { + assertThat(((ResourceWebHandler) handler).isUseLastModified()).isFalse(); + } + } + }); + } + @Test void customPrinterAndParserShouldBeRegisteredAsConverters() { this.contextRunner.withUserConfiguration(ParserConfiguration.class, PrinterConfiguration.class) @@ -389,6 +471,111 @@ void customPrinterAndParserShouldBeRegisteredAsConverters() { }); } + @ParameterizedTest + @ValueSource(strings = { "spring.resources.", "spring.web.resources." }) + void welcomePageHandlerMapping(String prefix) { + this.contextRunner.withPropertyValues(prefix + "static-locations=classpath:/welcome-page/").run((context) -> { + assertThat(context).getBeans(RouterFunctionMapping.class).hasSize(2); + assertThat(context.getBean("welcomePageRouterFunctionMapping", HandlerMapping.class)).isNotNull() + .extracting("order").isEqualTo(1); + }); + } + + @Test + void defaultLocaleContextResolver() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(LocaleContextResolver.class); + LocaleContextResolver resolver = context.getBean(LocaleContextResolver.class); + assertThat(((AcceptHeaderLocaleContextResolver) resolver).getDefaultLocale()).isNull(); + }); + } + + @Test + void whenFixedLocalContextResolverIsUsedThenAcceptLanguagesHeaderIsIgnored() { + this.contextRunner.withPropertyValues("spring.web.locale:en_UK", "spring.web.locale-resolver=fixed") + .run((context) -> { + MockServerHttpRequest request = MockServerHttpRequest.get("/") + .acceptLanguageAsLocales(StringUtils.parseLocaleString("nl_NL")).build(); + MockServerWebExchange exchange = MockServerWebExchange.from(request); + LocaleContextResolver localeContextResolver = context.getBean(LocaleContextResolver.class); + assertThat(localeContextResolver).isInstanceOf(FixedLocaleContextResolver.class); + LocaleContext localeContext = localeContextResolver.resolveLocaleContext(exchange); + assertThat(localeContext.getLocale()).isEqualTo(StringUtils.parseLocaleString("en_UK")); + }); + } + + @Test + void whenAcceptHeaderLocaleContextResolverIsUsedThenAcceptLanguagesHeaderIsHonoured() { + this.contextRunner.withPropertyValues("spring.web.locale:en_UK").run((context) -> { + MockServerHttpRequest request = MockServerHttpRequest.get("/") + .acceptLanguageAsLocales(StringUtils.parseLocaleString("nl_NL")).build(); + MockServerWebExchange exchange = MockServerWebExchange.from(request); + LocaleContextResolver localeContextResolver = context.getBean(LocaleContextResolver.class); + assertThat(localeContextResolver).isInstanceOf(AcceptHeaderLocaleContextResolver.class); + LocaleContext localeContext = localeContextResolver.resolveLocaleContext(exchange); + assertThat(localeContext.getLocale()).isEqualTo(StringUtils.parseLocaleString("nl_NL")); + }); + } + + @Test + void whenAcceptHeaderLocaleContextResolverIsUsedAndHeaderIsAbsentThenConfiguredLocaleIsUsed() { + this.contextRunner.withPropertyValues("spring.web.locale:en_UK").run((context) -> { + MockServerHttpRequest request = MockServerHttpRequest.get("/").build(); + MockServerWebExchange exchange = MockServerWebExchange.from(request); + LocaleContextResolver localeContextResolver = context.getBean(LocaleContextResolver.class); + assertThat(localeContextResolver).isInstanceOf(AcceptHeaderLocaleContextResolver.class); + LocaleContext localeContext = localeContextResolver.resolveLocaleContext(exchange); + assertThat(localeContext.getLocale()).isEqualTo(StringUtils.parseLocaleString("en_UK")); + }); + } + + @Test + void customLocaleContextResolverWithMatchingNameReplacedAutoConfiguredLocaleContextResolver() { + this.contextRunner + .withBean("localeContextResolver", CustomLocaleContextResolver.class, CustomLocaleContextResolver::new) + .run((context) -> { + assertThat(context).hasSingleBean(LocaleContextResolver.class); + assertThat(context.getBean("localeContextResolver")) + .isInstanceOf(CustomLocaleContextResolver.class); + }); + } + + @Test + void customLocaleContextResolverWithDifferentNameDoesNotReplaceAutoConfiguredLocaleContextResolver() { + this.contextRunner.withBean("customLocaleContextResolver", CustomLocaleContextResolver.class, + CustomLocaleContextResolver::new).run((context) -> { + assertThat(context.getBean("customLocaleContextResolver")) + .isInstanceOf(CustomLocaleContextResolver.class); + assertThat(context.getBean("localeContextResolver")) + .isInstanceOf(AcceptHeaderLocaleContextResolver.class); + }); + } + + @Test + @SuppressWarnings("rawtypes") + void userConfigurersCanBeOrderedBeforeOrAfterTheAutoConfiguredConfigurer() { + this.contextRunner.withBean(HighPrecedenceConfigurer.class, HighPrecedenceConfigurer::new) + .withBean(LowPrecedenceConfigurer.class, LowPrecedenceConfigurer::new) + .run((context) -> assertThat(context.getBean(DelegatingWebFluxConfiguration.class)) + .extracting("configurers.delegates").asList() + .extracting((configurer) -> (Class) configurer.getClass()).containsExactly( + HighPrecedenceConfigurer.class, WebFluxConfig.class, LowPrecedenceConfigurer.class)); + } + + @Test + void customSameSteConfigurationShouldBeApplied() { + this.contextRunner.withPropertyValues("spring.webflux.session.cookie.same-site:strict").run((context) -> { + MockServerHttpRequest request = MockServerHttpRequest.get("/").build(); + MockServerWebExchange exchange = MockServerWebExchange.from(request); + WebSessionManager webSessionManager = context.getBean(WebSessionManager.class); + WebSession webSession = webSessionManager.getSession(exchange).block(); + webSession.start(); + exchange.getResponse().setComplete().block(); + assertThat(exchange.getResponse().getCookies().get("SESSION")).isNotEmpty() + .allMatch((cookie) -> cookie.getSameSite().equals("Strict")); + }); + } + private Map getHandlerMap(ApplicationContext context) { HandlerMapping mapping = context.getBean("resourceHandlerMapping", HandlerMapping.class); if (mapping instanceof SimpleUrlHandlerMapping) { @@ -515,12 +702,15 @@ HiddenHttpMethodFilter customHiddenHttpMethodFilter() { @Configuration(proxyBeanMethods = false) static class CustomRequestMappingHandlerAdapter { + private int handlerAdapters = 0; + @Bean WebFluxRegistrations webFluxRegistrationsHandlerAdapter() { return new WebFluxRegistrations() { @Override public RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() { + CustomRequestMappingHandlerAdapter.this.handlerAdapters++; return new WebFluxAutoConfigurationTests.MyRequestMappingHandlerAdapter(); } @@ -543,12 +733,15 @@ static class MultipleWebFluxRegistrations { @Configuration(proxyBeanMethods = false) static class CustomRequestMappingHandlerMapping { + private int handlerMappings = 0; + @Bean WebFluxRegistrations webFluxRegistrationsHandlerMapping() { return new WebFluxRegistrations() { @Override public RequestMappingHandlerMapping getRequestMappingHandlerMapping() { + CustomRequestMappingHandlerMapping.this.handlerMappings++; return new MyRequestMappingHandlerMapping(); } @@ -613,4 +806,27 @@ public Example parse(String source, Locale locale) { } + static class CustomLocaleContextResolver implements LocaleContextResolver { + + @Override + public LocaleContext resolveLocaleContext(ServerWebExchange exchange) { + return () -> Locale.ENGLISH; + } + + @Override + public void setLocaleContext(ServerWebExchange exchange, LocaleContext localeContext) { + } + + } + + @Order(-100) + static class HighPrecedenceConfigurer implements WebFluxConfigurer { + + } + + @Order(100) + static class LowPrecedenceConfigurer implements WebFluxConfigurer { + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxPropertiesTests.java new file mode 100644 index 000000000000..83eb6b20b789 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxPropertiesTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.web.reactive; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link WebFluxProperties} + * + * @author Brian Clozel + */ +class WebFluxPropertiesTests { + + private final WebFluxProperties properties = new WebFluxProperties(); + + @Test + void shouldPrefixBasePathWithMissingSlash() { + bind("spring.webflux.base-path", "something"); + assertThat(this.properties.getBasePath()).isEqualTo("/something"); + } + + @Test + void shouldRemoveTrailingSlashFromBasePath() { + bind("spring.webflux.base-path", "/something/"); + assertThat(this.properties.getBasePath()).isEqualTo("/something"); + } + + private void bind(String name, String value) { + bind(Collections.singletonMap(name, value)); + } + + private void bind(Map map) { + ConfigurationPropertySource source = new MapConfigurationPropertySource(map); + new Binder(source).bind("spring.webflux", Bindable.ofInstance(this.properties)); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WelcomePageRouterFunctionFactoryTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WelcomePageRouterFunctionFactoryTests.java new file mode 100644 index 000000000000..c6c84990833a --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WelcomePageRouterFunctionFactoryTests.java @@ -0,0 +1,191 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.web.reactive; + +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Locale; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider; +import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders; +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.reactive.function.server.HandlerStrategies; +import org.springframework.web.reactive.result.view.View; +import org.springframework.web.reactive.result.view.ViewResolver; +import org.springframework.web.server.ServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link WelcomePageRouterFunctionFactory} + * + * @author Brian Clozel + */ +class WelcomePageRouterFunctionFactoryTests { + + private StaticApplicationContext applicationContext; + + private final String[] noIndexLocations = { "classpath:/" }; + + private final String[] indexLocations = { "classpath:/public/", "classpath:/welcome-page/" }; + + @BeforeEach + void setup() { + this.applicationContext = new StaticApplicationContext(); + this.applicationContext.refresh(); + } + + @Test + void handlesRequestForStaticPageThatAcceptsTextHtml() { + WebTestClient client = withStaticIndex(); + client.get().uri("/").accept(MediaType.TEXT_HTML).exchange().expectStatus().isOk().expectBody(String.class) + .isEqualTo("welcome-page-static"); + } + + @Test + void handlesRequestForStaticPageThatAcceptsAll() { + WebTestClient client = withStaticIndex(); + client.get().uri("/").accept(MediaType.ALL).exchange().expectStatus().isOk().expectBody(String.class) + .isEqualTo("welcome-page-static"); + } + + @Test + void doesNotHandleRequestThatDoesNotAcceptTextHtml() { + WebTestClient client = withStaticIndex(); + client.get().uri("/").accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isNotFound(); + } + + @Test + void handlesRequestWithNoAcceptHeader() { + WebTestClient client = withStaticIndex(); + client.get().uri("/").exchange().expectStatus().isOk().expectBody(String.class) + .isEqualTo("welcome-page-static"); + } + + @Test + void handlesRequestWithEmptyAcceptHeader() { + WebTestClient client = withStaticIndex(); + client.get().uri("/").header(HttpHeaders.ACCEPT, "").exchange().expectStatus().isOk().expectBody(String.class) + .isEqualTo("welcome-page-static"); + } + + @Test + void producesNotFoundResponseWhenThereIsNoWelcomePage() { + WelcomePageRouterFunctionFactory factory = factoryWithoutTemplateSupport(this.noIndexLocations, "/**"); + assertThat(factory.createRouterFunction()).isNull(); + } + + @Test + void handlesRequestForTemplateThatAcceptsTextHtml() { + WebTestClient client = withTemplateIndex(); + client.get().uri("/").accept(MediaType.TEXT_HTML).exchange().expectStatus().isOk().expectBody(String.class) + .isEqualTo("welcome-page-template"); + } + + @Test + void handlesRequestForTemplateThatAcceptsAll() { + WebTestClient client = withTemplateIndex(); + client.get().uri("/").accept(MediaType.ALL).exchange().expectStatus().isOk().expectBody(String.class) + .isEqualTo("welcome-page-template"); + } + + @Test + void prefersAStaticResourceToATemplate() { + WebTestClient client = withStaticAndTemplateIndex(); + client.get().uri("/").accept(MediaType.ALL).exchange().expectStatus().isOk().expectBody(String.class) + .isEqualTo("welcome-page-static"); + } + + private WebTestClient withStaticIndex() { + WelcomePageRouterFunctionFactory factory = factoryWithoutTemplateSupport(this.indexLocations, "/**"); + return WebTestClient.bindToRouterFunction(factory.createRouterFunction()).build(); + } + + private WebTestClient withTemplateIndex() { + WelcomePageRouterFunctionFactory factory = factoryWithTemplateSupport(this.noIndexLocations); + TestViewResolver testViewResolver = new TestViewResolver(); + return WebTestClient.bindToRouterFunction(factory.createRouterFunction()) + .handlerStrategies(HandlerStrategies.builder().viewResolver(testViewResolver).build()).build(); + } + + private WebTestClient withStaticAndTemplateIndex() { + WelcomePageRouterFunctionFactory factory = factoryWithTemplateSupport(this.indexLocations); + TestViewResolver testViewResolver = new TestViewResolver(); + return WebTestClient.bindToRouterFunction(factory.createRouterFunction()) + .handlerStrategies(HandlerStrategies.builder().viewResolver(testViewResolver).build()).build(); + } + + private WelcomePageRouterFunctionFactory factoryWithoutTemplateSupport(String[] locations, + String staticPathPattern) { + return new WelcomePageRouterFunctionFactory(new TestTemplateAvailabilityProviders(), this.applicationContext, + locations, staticPathPattern); + } + + private WelcomePageRouterFunctionFactory factoryWithTemplateSupport(String[] locations) { + return new WelcomePageRouterFunctionFactory(new TestTemplateAvailabilityProviders("index"), + this.applicationContext, locations, "/**"); + } + + static class TestTemplateAvailabilityProviders extends TemplateAvailabilityProviders { + + TestTemplateAvailabilityProviders() { + super(Collections.emptyList()); + } + + TestTemplateAvailabilityProviders(String viewName) { + this((view, environment, classLoader, resourceLoader) -> view.equals(viewName)); + } + + TestTemplateAvailabilityProviders(TemplateAvailabilityProvider provider) { + super(Collections.singletonList(provider)); + } + + } + + static class TestViewResolver implements ViewResolver { + + @Override + public Mono resolveViewName(String viewName, Locale locale) { + return Mono.just(new TestView()); + } + + } + + static class TestView implements View { + + private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory(); + + @Override + public Mono render(Map model, MediaType contentType, ServerWebExchange exchange) { + DataBuffer buffer = this.bufferFactory.wrap("welcome-page-template".getBytes(StandardCharsets.UTF_8)); + return exchange.getResponse().writeWith(Mono.just(buffer)); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandlerIntegrationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandlerIntegrationTests.java index ede71b38ba6c..576935cbd704 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandlerIntegrationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandlerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,8 @@ package org.springframework.boot.autoconfigure.web.reactive.error; import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; import javax.validation.Valid; @@ -34,15 +36,21 @@ import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.boot.web.error.ErrorAttributeOptions; +import org.springframework.boot.web.reactive.error.DefaultErrorAttributes; +import org.springframework.boot.web.reactive.error.ErrorAttributes; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.HttpHandlerConnector.FailureAfterResponseCompletedException; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; @@ -55,6 +63,7 @@ * Integration tests for {@link DefaultErrorWebExceptionHandler} * * @author Brian Clozel + * @author Scott Frederick */ @ExtendWith(OutputCaptureExtension.class) class DefaultErrorWebExceptionHandlerIntegrationTests { @@ -63,7 +72,7 @@ class DefaultErrorWebExceptionHandlerIntegrationTests { private final LogIdFilter logIdFilter = new LogIdFilter(); - private ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() + private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(ReactiveWebServerFactoryAutoConfiguration.class, HttpHandlerAutoConfiguration.class, WebFluxAutoConfiguration.class, ErrorWebFluxAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class, @@ -78,7 +87,7 @@ void jsonError(CapturedOutput output) { client.get().uri("/").exchange().expectStatus().isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR).expectBody() .jsonPath("status").isEqualTo("500").jsonPath("error") .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()).jsonPath("path").isEqualTo(("/")) - .jsonPath("message").isEqualTo("Expected!").jsonPath("exception").doesNotExist().jsonPath("trace") + .jsonPath("message").doesNotExist().jsonPath("exception").doesNotExist().jsonPath("trace") .doesNotExist().jsonPath("requestId").isEqualTo(this.logIdFilter.getLogId()); assertThat(output).contains("500 Server Error for HTTP GET \"/\"") .contains("java.lang.IllegalStateException: Expected!"); @@ -98,7 +107,7 @@ void notFound() { @Test void htmlError() { - this.contextRunner.run((context) -> { + this.contextRunner.withPropertyValues("server.error.include-message=always").run((context) -> { WebTestClient client = getWebClient(context); String body = client.get().uri("/").accept(MediaType.TEXT_HTML).exchange().expectStatus() .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR).expectHeader().contentType(TEXT_HTML_UTF8) @@ -114,15 +123,30 @@ void bindingResultError() { client.post().uri("/bind").contentType(MediaType.APPLICATION_JSON).bodyValue("{}").exchange().expectStatus() .isBadRequest().expectBody().jsonPath("status").isEqualTo("400").jsonPath("error") .isEqualTo(HttpStatus.BAD_REQUEST.getReasonPhrase()).jsonPath("path").isEqualTo(("/bind")) - .jsonPath("exception").doesNotExist().jsonPath("errors").isArray().jsonPath("message").isNotEmpty() - .jsonPath("requestId").isEqualTo(this.logIdFilter.getLogId()); + .jsonPath("exception").doesNotExist().jsonPath("errors").doesNotExist().jsonPath("message") + .doesNotExist().jsonPath("requestId").isEqualTo(this.logIdFilter.getLogId()); }); } + @Test + void bindingResultErrorIncludeMessageAndErrors() { + this.contextRunner.withPropertyValues("server.error.include-message=on-param", + "server.error.include-binding-errors=on-param").run((context) -> { + WebTestClient client = getWebClient(context); + client.post().uri("/bind?message=true&errors=true").contentType(MediaType.APPLICATION_JSON) + .bodyValue("{}").exchange().expectStatus().isBadRequest().expectBody().jsonPath("status") + .isEqualTo("400").jsonPath("error").isEqualTo(HttpStatus.BAD_REQUEST.getReasonPhrase()) + .jsonPath("path").isEqualTo(("/bind")).jsonPath("exception").doesNotExist() + .jsonPath("errors").isArray().jsonPath("message").isNotEmpty().jsonPath("requestId") + .isEqualTo(this.logIdFilter.getLogId()); + }); + } + @Test void includeStackTraceOnParam() { - this.contextRunner.withPropertyValues("server.error.include-exception=true", - "server.error.include-stacktrace=on-trace-param").run((context) -> { + this.contextRunner + .withPropertyValues("server.error.include-exception=true", "server.error.include-stacktrace=on-param") + .run((context) -> { WebTestClient client = getWebClient(context); client.get().uri("/?trace=true").exchange().expectStatus() .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR).expectBody().jsonPath("status") @@ -163,6 +187,51 @@ void neverIncludeStackTrace() { }); } + @Test + void includeMessageOnParam() { + this.contextRunner + .withPropertyValues("server.error.include-exception=true", "server.error.include-message=on-param") + .run((context) -> { + WebTestClient client = getWebClient(context); + client.get().uri("/?message=true").exchange().expectStatus() + .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR).expectBody().jsonPath("status") + .isEqualTo("500").jsonPath("error") + .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()).jsonPath("exception") + .isEqualTo(IllegalStateException.class.getName()).jsonPath("message").isNotEmpty() + .jsonPath("requestId").isEqualTo(this.logIdFilter.getLogId()); + }); + } + + @Test + void alwaysIncludeMessage() { + this.contextRunner + .withPropertyValues("server.error.include-exception=true", "server.error.include-message=always") + .run((context) -> { + WebTestClient client = getWebClient(context); + client.get().uri("/?trace=false").exchange().expectStatus() + .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR).expectBody().jsonPath("status") + .isEqualTo("500").jsonPath("error") + .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()).jsonPath("exception") + .isEqualTo(IllegalStateException.class.getName()).jsonPath("message").isNotEmpty() + .jsonPath("requestId").isEqualTo(this.logIdFilter.getLogId()); + }); + } + + @Test + void neverIncludeMessage() { + this.contextRunner + .withPropertyValues("server.error.include-exception=true", "server.error.include-message=never") + .run((context) -> { + WebTestClient client = getWebClient(context); + client.get().uri("/?trace=true").exchange().expectStatus() + .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR).expectBody().jsonPath("status") + .isEqualTo("500").jsonPath("error") + .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()).jsonPath("exception") + .isEqualTo(IllegalStateException.class.getName()).jsonPath("message").doesNotExist() + .jsonPath("requestId").isEqualTo(this.logIdFilter.getLogId()); + }); + } + @Test void statusException() { this.contextRunner.withPropertyValues("server.error.include-exception=true").run((context) -> { @@ -176,8 +245,10 @@ void statusException() { @Test void defaultErrorView() { - this.contextRunner.withPropertyValues("spring.mustache.prefix=classpath:/unknown/", - "server.error.include-stacktrace=always").run((context) -> { + this.contextRunner + .withPropertyValues("spring.mustache.prefix=classpath:/unknown/", + "server.error.include-stacktrace=always", "server.error.include-message=always") + .run((context) -> { WebTestClient client = getWebClient(context); String body = client.get().uri("/").accept(MediaType.TEXT_HTML).exchange().expectStatus() .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR).expectHeader().contentType(TEXT_HTML_UTF8) @@ -190,14 +261,16 @@ void defaultErrorView() { @Test void escapeHtmlInDefaultErrorView() { - this.contextRunner.withPropertyValues("spring.mustache.prefix=classpath:/unknown/").run((context) -> { - WebTestClient client = getWebClient(context); - String body = client.get().uri("/html").accept(MediaType.TEXT_HTML).exchange().expectStatus() - .isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR).expectHeader().contentType(TEXT_HTML_UTF8) - .expectBody(String.class).returnResult().getResponseBody(); - assertThat(body).contains("Whitelabel Error Page").contains(this.logIdFilter.getLogId()) - .doesNotContain(" - - - - - - - - org.codehaus.groovy - groovy - ${groovy.version} - - - org.codehaus.groovy - groovy-ant - ${groovy.version} - - - org.springframework - spring-core - ${spring-framework.version} - - - - - org.codehaus.mojo - properties-maven-plugin - 1.0.0 - - - read-flattenedpom-versions - prepare-package - - read-project-properties - - false - - - ${project.build.directory}/generated-resources/.flattened-pom.properties - - - - - - - org.asciidoctor - asciidoctor-maven-plugin - - ${refdocs.build.directory} - - - ${refdocs.build.directory} - - **/*.*adoc - - - - - ${spring-boot-artifactory-repo} - ${github-tag} - ${project.basedir}/src - ${project.basedir}/src/main/asciidoc - ${project.basedir}/target/generated-resources - ${jetty.version} - ${jooq.version} - ${revision} - ${spring-amqp.version} - ${flattenedpom.version.org.springframework.data.spring-data-couchbase} - ${flattenedpom.version.org.springframework.data.spring-data-commons} - ${flattenedpom.version.org.springframework.data.spring-data-jpa} - ${flattenedpom.version.org.springframework.data.spring-data-jdbc} - ${flattenedpom.version.org.springframework.data.spring-data-mongodb} - ${flattenedpom.version.org.springframework.data.spring-data-neo4j} - ${flattenedpom.version.org.springframework.data.spring-data-rest-core} - ${flattenedpom.version.org.springframework.data.spring-data-solr} - ${spring-framework.version} - ${spring-integration.version} - ${spring-security.version} - ${spring-ws.version} - - - - - io.spring.asciidoctor - spring-asciidoctor-extensions-spring-boot - ${spring-asciidoctor-extensions.version} - - - org.springframework.boot - spring-boot-actuator-autoconfigure - ${revision} - - - org.springframework.boot - spring-boot-autoconfigure - ${revision} - - - org.springframework.boot - spring-boot-devtools - ${revision} - - - - - generate-html-documentation - prepare-package - - process-asciidoc - - - html5 - - .adoc - .htmladoc - - ${project.build.directory}/generated-docs/reference/html - highlight.js - book - - js/highlight - github - true - ./images - font - css/ - spring.css - warn - - - false - - INFO - - - - - - generate-htmlsingle-documentation - prepare-package - - process-asciidoc - - - html5 - - .htmlsingleadoc - - ${project.build.directory}/generated-docs/reference/htmlsingle - highlight.js - book - - js/highlight - github - true - ./images - font - css/ - spring.css - - - false - - INFO - - - - - - generate-pdf-documentation - prepare-package - - process-asciidoc - - - pdf - - pdfadoc - - ${project.build.directory}/generated-docs/reference/pdf - - - ${refdocs.build.directory} - - **/* - - - - - - - - - org.apache.maven.plugins - maven-antrun-plugin - - - ant-contrib - ant-contrib - 1.0b3 - - - ant - ant - - - - - org.apache.ant - ant-nodeps - 1.8.1 - - - org.tigris.antelope - antelopetasks - 3.2.10 - - - - - package-and-attach-docs-zip - package - - run - - - - - - - - - - - - - setup-maven-properties - validate - - run - - - true - - - - - - - - - - - - - - - - - - - - org.codehaus.mojo - build-helper-maven-plugin - - - attach-zip - - attach-artifact - - - - - ${project.build.directory}/${project.artifactId}-${project.version}.zip - zip - - - - - - - - - - - diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator.adoc new file mode 100644 index 000000000000..f9e97e30b274 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator.adoc @@ -0,0 +1,33 @@ +[[actuator]] += Spring Boot Actuator: Production-ready Features +include::attributes.adoc[] + + + +Spring Boot includes a number of additional features to help you monitor and manage your application when you push it to production. +You can choose to manage and monitor your application by using HTTP endpoints or with JMX. +Auditing, health, and metrics gathering can also be automatically applied to your application. + + + +include::actuator/enabling.adoc[] + +include::actuator/endpoints.adoc[] + +include::actuator/monitoring.adoc[] + +include::actuator/jmx.adoc[] + +include::actuator/loggers.adoc[] + +include::actuator/metrics.adoc[] + +include::actuator/auditing.adoc[] + +include::actuator/tracing.adoc[] + +include::actuator/process-monitoring.adoc[] + +include::actuator/cloud-foundry.adoc[] + +include::actuator/whats-next.adoc[] diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/auditing.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/auditing.adoc new file mode 100644 index 000000000000..2a18952ed172 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/auditing.adoc @@ -0,0 +1,18 @@ +[[actuator.auditing]] +== Auditing +Once Spring Security is in play, Spring Boot Actuator has a flexible audit framework that publishes events (by default, "`authentication success`", "`failure`" and "`access denied`" exceptions). +This feature can be very useful for reporting and for implementing a lock-out policy based on authentication failures. + +Auditing can be enabled by providing a bean of type `AuditEventRepository` in your application's configuration. +For convenience, Spring Boot offers an `InMemoryAuditEventRepository`. +`InMemoryAuditEventRepository` has limited capabilities and we recommend using it only for development environments. +For production environments, consider creating your own alternative `AuditEventRepository` implementation. + + + +[[actuator.auditing.custom]] +=== Custom Auditing +To customize published security events, you can provide your own implementations of `AbstractAuthenticationAuditListener` and `AbstractAuthorizationAuditListener`. + +You can also use the audit services for your own business events. +To do so, either inject the `AuditEventRepository` bean into your own components and use that directly or publish an `AuditApplicationEvent` with the Spring `ApplicationEventPublisher` (by implementing `ApplicationEventPublisherAware`). diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/cloud-foundry.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/cloud-foundry.adoc new file mode 100644 index 000000000000..cf44dca874fc --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/cloud-foundry.adoc @@ -0,0 +1,54 @@ +[[actuator.cloud-foundry]] +== Cloud Foundry Support +Spring Boot's actuator module includes additional support that is activated when you deploy to a compatible Cloud Foundry instance. +The `/cloudfoundryapplication` path provides an alternative secured route to all `@Endpoint` beans. + +The extended support lets Cloud Foundry management UIs (such as the web application that you can use to view deployed applications) be augmented with Spring Boot actuator information. +For example, an application status page may include full health information instead of the typical "`running`" or "`stopped`" status. + +NOTE: The `/cloudfoundryapplication` path is not directly accessible to regular users. +In order to use the endpoint, a valid UAA token must be passed with the request. + + + +[[actuator.cloud-foundry.disable]] +=== Disabling Extended Cloud Foundry Actuator Support +If you want to fully disable the `/cloudfoundryapplication` endpoints, you can add the following setting to your `application.properties` file: + + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + cloudfoundry: + enabled: false +---- + + + +[[actuator.cloud-foundry.ssl]] +=== Cloud Foundry Self-signed Certificates +By default, the security verification for `/cloudfoundryapplication` endpoints makes SSL calls to various Cloud Foundry services. +If your Cloud Foundry UAA or Cloud Controller services use self-signed certificates, you need to set the following property: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + cloudfoundry: + skip-ssl-validation: true +---- + + + +[[actuator.cloud-foundry.custom-context-path]] +=== Custom Context Path +If the server's context-path has been configured to anything other than `/`, the Cloud Foundry endpoints will not be available at the root of the application. +For example, if `server.servlet.context-path=/app`, Cloud Foundry endpoints will be available at `/app/cloudfoundryapplication/*`. + +If you expect the Cloud Foundry endpoints to always be available at `/cloudfoundryapplication/*`, regardless of the server's context-path, you will need to explicitly configure that in your application. +The configuration will differ depending on the web server in use. +For Tomcat, the following configuration can be added: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/actuator/cloudfoundry/customcontextpath/MyCloudFoundryConfiguration.java[] +---- diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/enabling.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/enabling.adoc new file mode 100644 index 000000000000..a962b0ce4558 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/enabling.adoc @@ -0,0 +1,31 @@ +[[actuator.enabling]] +== Enabling Production-ready Features +The {spring-boot-code}/spring-boot-project/spring-boot-actuator[`spring-boot-actuator`] module provides all of Spring Boot's production-ready features. +The recommended way to enable the features is to add a dependency on the `spring-boot-starter-actuator` '`Starter`'. + +.Definition of Actuator +**** +An actuator is a manufacturing term that refers to a mechanical device for moving or controlling something. +Actuators can generate a large amount of motion from a small change. +**** + +To add the actuator to a Maven based project, add the following '`Starter`' dependency: + +[source,xml,indent=0,subs="verbatim"] +---- + + + org.springframework.boot + spring-boot-starter-actuator + + +---- + +For Gradle, use the following declaration: + +[source,gradle,indent=0,subs="verbatim"] +---- + dependencies { + implementation 'org.springframework.boot:spring-boot-starter-actuator' + } +---- diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/endpoints.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/endpoints.adoc new file mode 100644 index 000000000000..8b0c34bc2469 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/endpoints.adoc @@ -0,0 +1,1203 @@ +[[actuator.endpoints]] +== Endpoints +Actuator endpoints let you monitor and interact with your application. +Spring Boot includes a number of built-in endpoints and lets you add your own. +For example, the `health` endpoint provides basic application health information. + +Each individual endpoint can be <> and <>. +An endpoint is considered to be available when it is both enabled and exposed. +The built-in endpoints will only be auto-configured when they are available. +Most applications choose exposure via HTTP, where the ID of the endpoint along with a prefix of `/actuator` is mapped to a URL. +For example, by default, the `health` endpoint is mapped to `/actuator/health`. + +TIP: To learn more about the Actuator's endpoints and their request and response formats, please refer to the separate API documentation ({spring-boot-actuator-restapi-docs}[HTML] or {spring-boot-actuator-restapi-pdfdocs}[PDF]). + +The following technology-agnostic endpoints are available: + +[cols="2,5"] +|=== +| ID | Description + +| `auditevents` +| Exposes audit events information for the current application. + Requires an `AuditEventRepository` bean. + +| `beans` +| Displays a complete list of all the Spring beans in your application. + +| `caches` +| Exposes available caches. + +| `conditions` +| Shows the conditions that were evaluated on configuration and auto-configuration classes and the reasons why they did or did not match. + +| `configprops` +| Displays a collated list of all `@ConfigurationProperties`. + +| `env` +| Exposes properties from Spring's `ConfigurableEnvironment`. + +| `flyway` +| Shows any Flyway database migrations that have been applied. + Requires one or more `Flyway` beans. + +| `health` +| Shows application health information. + +| `httptrace` +| Displays HTTP trace information (by default, the last 100 HTTP request-response exchanges). + Requires an `HttpTraceRepository` bean. + +| `info` +| Displays arbitrary application info. + +| `integrationgraph` +| Shows the Spring Integration graph. + Requires a dependency on `spring-integration-core`. + +| `loggers` +| Shows and modifies the configuration of loggers in the application. + +| `liquibase` +| Shows any Liquibase database migrations that have been applied. + Requires one or more `Liquibase` beans. + +| `metrics` +| Shows '`metrics`' information for the current application. + +| `mappings` +| Displays a collated list of all `@RequestMapping` paths. + +|`quartz` +|Shows information about Quartz Scheduler jobs. + +| `scheduledtasks` +| Displays the scheduled tasks in your application. + +| `sessions` +| Allows retrieval and deletion of user sessions from a Spring Session-backed session store. + Requires a Servlet-based web application using Spring Session. + +| `shutdown` +| Lets the application be gracefully shutdown. + Disabled by default. + +| `startup` +| Shows the <> collected by the `ApplicationStartup`. + Requires the `SpringApplication` to be configured with a `BufferingApplicationStartup`. + +| `threaddump` +| Performs a thread dump. +|=== + +If your application is a web application (Spring MVC, Spring WebFlux, or Jersey), you can use the following additional endpoints: + +[cols="2,5"] +|=== +| ID | Description + +| `heapdump` +| Returns an `hprof` heap dump file. + Requires a HotSpot JVM. + +| `jolokia` +| Exposes JMX beans over HTTP (when Jolokia is on the classpath, not available for WebFlux). + Requires a dependency on `jolokia-core`. + +| `logfile` +| Returns the contents of the logfile (if `logging.file.name` or `logging.file.path` properties have been set). + Supports the use of the HTTP `Range` header to retrieve part of the log file's content. + +| `prometheus` +| Exposes metrics in a format that can be scraped by a Prometheus server. + Requires a dependency on `micrometer-registry-prometheus`. +|=== + + + +[[actuator.endpoints.enabling]] +=== Enabling Endpoints +By default, all endpoints except for `shutdown` are enabled. +To configure the enablement of an endpoint, use its `management.endpoint..enabled` property. +The following example enables the `shutdown` endpoint: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + endpoint: + shutdown: + enabled: true +---- + +If you prefer endpoint enablement to be opt-in rather than opt-out, set the configprop:management.endpoints.enabled-by-default[] property to `false` and use individual endpoint `enabled` properties to opt back in. +The following example enables the `info` endpoint and disables all other endpoints: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + endpoints: + enabled-by-default: false + endpoint: + info: + enabled: true +---- + +NOTE: Disabled endpoints are removed entirely from the application context. +If you want to change only the technologies over which an endpoint is exposed, use the <> instead. + + + +[[actuator.endpoints.exposing]] +=== Exposing Endpoints +Since Endpoints may contain sensitive information, careful consideration should be given about when to expose them. +The following table shows the default exposure for the built-in endpoints: + +[cols="1,1,1"] +|=== +| ID | JMX | Web + +| `auditevents` +| Yes +| No + +| `beans` +| Yes +| No + +| `caches` +| Yes +| No + +| `conditions` +| Yes +| No + +| `configprops` +| Yes +| No + +| `env` +| Yes +| No + +| `flyway` +| Yes +| No + +| `health` +| Yes +| Yes + +| `heapdump` +| N/A +| No + +| `httptrace` +| Yes +| No + +| `info` +| Yes +| No + +| `integrationgraph` +| Yes +| No + +| `jolokia` +| N/A +| No + +| `logfile` +| N/A +| No + +| `loggers` +| Yes +| No + +| `liquibase` +| Yes +| No + +| `metrics` +| Yes +| No + +| `mappings` +| Yes +| No + +| `prometheus` +| N/A +| No + +| `quartz` +| Yes +| No + +| `scheduledtasks` +| Yes +| No + +| `sessions` +| Yes +| No + +| `shutdown` +| Yes +| No + +| `startup` +| Yes +| No + +| `threaddump` +| Yes +| No +|=== + +To change which endpoints are exposed, use the following technology-specific `include` and `exclude` properties: + +[cols="3,1"] +|=== +| Property | Default + +| configprop:management.endpoints.jmx.exposure.exclude[] +| + +| configprop:management.endpoints.jmx.exposure.include[] +| `*` + +| configprop:management.endpoints.web.exposure.exclude[] +| + +| configprop:management.endpoints.web.exposure.include[] +| `health` +|=== + +The `include` property lists the IDs of the endpoints that are exposed. +The `exclude` property lists the IDs of the endpoints that should not be exposed. +The `exclude` property takes precedence over the `include` property. +Both `include` and `exclude` properties can be configured with a list of endpoint IDs. + +For example, to stop exposing all endpoints over JMX and only expose the `health` and `info` endpoints, use the following property: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + endpoints: + jmx: + exposure: + include: "health,info" +---- + +`*` can be used to select all endpoints. +For example, to expose everything over HTTP except the `env` and `beans` endpoints, use the following properties: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + endpoints: + web: + exposure: + include: "*" + exclude: "env,beans" +---- + +NOTE: `*` has a special meaning in YAML, so be sure to add quotes if you want to include (or exclude) all endpoints. + +NOTE: If your application is exposed publicly, we strongly recommend that you also <>. + +TIP: If you want to implement your own strategy for when endpoints are exposed, you can register an `EndpointFilter` bean. + + + +[[actuator.endpoints.security]] +=== Securing HTTP Endpoints +You should take care to secure HTTP endpoints in the same way that you would any other sensitive URL. +If Spring Security is present, endpoints are secured by default using Spring Security’s content-negotiation strategy. +If you wish to configure custom security for HTTP endpoints, for example, only allow users with a certain role to access them, Spring Boot provides some convenient `RequestMatcher` objects that can be used in combination with Spring Security. + +A typical Spring Security configuration might look something like the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/actuator/endpoints/security/typical/MySecurityConfiguration.java[] +---- + +The preceding example uses `EndpointRequest.toAnyEndpoint()` to match a request to any endpoint and then ensures that all have the `ENDPOINT_ADMIN` role. +Several other matcher methods are also available on `EndpointRequest`. +See the API documentation ({spring-boot-actuator-restapi-docs}[HTML] or {spring-boot-actuator-restapi-pdfdocs}[PDF]) for details. + +If you deploy applications behind a firewall, you may prefer that all your actuator endpoints can be accessed without requiring authentication. +You can do so by changing the configprop:management.endpoints.web.exposure.include[] property, as follows: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + endpoints: + web: + exposure: + include: "*" +---- + +Additionally, if Spring Security is present, you would need to add custom security configuration that allows unauthenticated access to the endpoints as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/actuator/endpoints/security/exposeall/MySecurityConfiguration.java[] +---- + +NOTE: In both the examples above, the configuration applies only to the actuator endpoints. +Since Spring Boot's security configuration backs off completely in the presence of any `SecurityFilterChain` bean, you will need to configure an additional `SecurityFilterChain` bean with rules that apply to the rest of the application. + + + +[[actuator.endpoints.caching]] +=== Configuring Endpoints +Endpoints automatically cache responses to read operations that do not take any parameters. +To configure the amount of time for which an endpoint will cache a response, use its `cache.time-to-live` property. +The following example sets the time-to-live of the `beans` endpoint's cache to 10 seconds: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + endpoint: + beans: + cache: + time-to-live: "10s" +---- + +NOTE: The prefix `management.endpoint.` is used to uniquely identify the endpoint that is being configured. + + + +[[actuator.endpoints.hypermedia]] +=== Hypermedia for Actuator Web Endpoints +A "`discovery page`" is added with links to all the endpoints. +The "`discovery page`" is available on `/actuator` by default. + +To disable the "`discovery page`", add the following property to your application properties: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + endpoints: + web: + discovery: + enabled: false +---- + +When a custom management context path is configured, the "`discovery page`" automatically moves from `/actuator` to the root of the management context. +For example, if the management context path is `/management`, then the discovery page is available from `/management`. +When the management context path is set to `/`, the discovery page is disabled to prevent the possibility of a clash with other mappings. + + + +[[actuator.endpoints.cors]] +=== CORS Support +https://en.wikipedia.org/wiki/Cross-origin_resource_sharing[Cross-origin resource sharing] (CORS) is a https://www.w3.org/TR/cors/[W3C specification] that lets you specify in a flexible way what kind of cross-domain requests are authorized. +If you use Spring MVC or Spring WebFlux, Actuator's web endpoints can be configured to support such scenarios. + +CORS support is disabled by default and is only enabled once the configprop:management.endpoints.web.cors.allowed-origins[] property has been set. +The following configuration permits `GET` and `POST` calls from the `example.com` domain: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + endpoints: + web: + cors: + allowed-origins: "https://example.com" + allowed-methods: "GET,POST" +---- + +TIP: See {spring-boot-actuator-autoconfigure-module-code}/endpoint/web/CorsEndpointProperties.java[CorsEndpointProperties] for a complete list of options. + + + +[[actuator.endpoints.implementing-custom]] +=== Implementing Custom Endpoints +If you add a `@Bean` annotated with `@Endpoint`, any methods annotated with `@ReadOperation`, `@WriteOperation`, or `@DeleteOperation` are automatically exposed over JMX and, in a web application, over HTTP as well. +Endpoints can be exposed over HTTP using Jersey, Spring MVC, or Spring WebFlux. +If both Jersey and Spring MVC are available, Spring MVC will be used. + +The following example exposes a read operation that returns a custom object: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/actuator/endpoints/implementingcustom/MyEndpoint.java[tag=read] +---- + +You can also write technology-specific endpoints by using `@JmxEndpoint` or `@WebEndpoint`. +These endpoints are restricted to their respective technologies. +For example, `@WebEndpoint` is exposed only over HTTP and not over JMX. + +You can write technology-specific extensions by using `@EndpointWebExtension` and `@EndpointJmxExtension`. +These annotations let you provide technology-specific operations to augment an existing endpoint. + +Finally, if you need access to web-framework-specific functionality, you can implement Servlet or Spring `@Controller` and `@RestController` endpoints at the cost of them not being available over JMX or when using a different web framework. + + + +[[actuator.endpoints.implementing-custom.input]] +==== Receiving Input +Operations on an endpoint receive input via their parameters. +When exposed via the web, the values for these parameters are taken from the URL's query parameters and from the JSON request body. +When exposed via JMX, the parameters are mapped to the parameters of the MBean's operations. +Parameters are required by default. +They can be made optional by annotating them with either `@javax.annotation.Nullable` or `@org.springframework.lang.Nullable`. + +Each root property in the JSON request body can be mapped to a parameter of the endpoint. +Consider the following JSON request body: + +[source,json,indent=0,subs="verbatim"] +---- + { + "name": "test", + "counter": 42 + } +---- + +This can be used to invoke a write operation that takes `String name` and `int counter` parameters, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/actuator/endpoints/implementingcustom/MyEndpoint.java[tag=write] +---- + +TIP: Because endpoints are technology agnostic, only simple types can be specified in the method signature. +In particular declaring a single parameter with a `CustomData` type defining a `name` and `counter` properties is not supported. + +NOTE: To allow the input to be mapped to the operation method's parameters, Java code implementing an endpoint should be compiled with `-parameters`, and Kotlin code implementing an endpoint should be compiled with `-java-parameters`. +This will happen automatically if you are using Spring Boot's Gradle plugin or if you are using Maven and `spring-boot-starter-parent`. + + + +[[actuator.endpoints.implementing-custom.input.conversion]] +===== Input Type Conversion +The parameters passed to endpoint operation methods are, if necessary, automatically converted to the required type. +Before calling an operation method, the input received via JMX or an HTTP request is converted to the required types using an instance of `ApplicationConversionService` as well as any `Converter` or `GenericConverter` beans qualified with `@EndpointConverter`. + + + +[[actuator.endpoints.implementing-custom.web]] +==== Custom Web Endpoints +Operations on an `@Endpoint`, `@WebEndpoint`, or `@EndpointWebExtension` are automatically exposed over HTTP using Jersey, Spring MVC, or Spring WebFlux. +If both Jersey and Spring MVC are available, Spring MVC will be used. + + + +[[actuator.endpoints.implementing-custom.web.request-predicates]] +===== Web Endpoint Request Predicates +A request predicate is automatically generated for each operation on a web-exposed endpoint. + + + +[[actuator.endpoints.implementing-custom.web.path-predicates]] +===== Path +The path of the predicate is determined by the ID of the endpoint and the base path of web-exposed endpoints. +The default base path is `/actuator`. +For example, an endpoint with the ID `sessions` will use `/actuator/sessions` as its path in the predicate. + +The path can be further customized by annotating one or more parameters of the operation method with `@Selector`. +Such a parameter is added to the path predicate as a path variable. +The variable's value is passed into the operation method when the endpoint operation is invoked. +If you want to capture all remaining path elements, you can add `@Selector(Match=ALL_REMAINING)` to the last parameter and make it a type that is conversion compatible with a `String[]`. + + + +[[actuator.endpoints.implementing-custom.web.method-predicates]] +===== HTTP method +The HTTP method of the predicate is determined by the operation type, as shown in the following table: + +[cols="3, 1"] +|=== +| Operation | HTTP method + +| `@ReadOperation` +| `GET` + +| `@WriteOperation` +| `POST` + +| `@DeleteOperation` +| `DELETE` +|=== + + + +[[actuator.endpoints.implementing-custom.web.consumes-predicates]] +===== Consumes +For a `@WriteOperation` (HTTP `POST`) that uses the request body, the consumes clause of the predicate is `application/vnd.spring-boot.actuator.v2+json, application/json`. +For all other operations the consumes clause is empty. + + + +[[actuator.endpoints.implementing-custom.web.produces-predicates]] +===== Produces +The produces clause of the predicate can be determined by the `produces` attribute of the `@DeleteOperation`, `@ReadOperation`, and `@WriteOperation` annotations. +The attribute is optional. +If it is not used, the produces clause is determined automatically. + +If the operation method returns `void` or `Void` the produces clause is empty. +If the operation method returns a `org.springframework.core.io.Resource`, the produces clause is `application/octet-stream`. +For all other operations the produces clause is `application/vnd.spring-boot.actuator.v2+json, application/json`. + + + +[[actuator.endpoints.implementing-custom.web.response-status]] +===== Web Endpoint Response Status +The default response status for an endpoint operation depends on the operation type (read, write, or delete) and what, if anything, the operation returns. + +A `@ReadOperation` returns a value, the response status will be 200 (OK). +If it does not return a value, the response status will be 404 (Not Found). + +If a `@WriteOperation` or `@DeleteOperation` returns a value, the response status will be 200 (OK). +If it does not return a value the response status will be 204 (No Content). + +If an operation is invoked without a required parameter, or with a parameter that cannot be converted to the required type, the operation method will not be called and the response status will be 400 (Bad Request). + + + +[[actuator.endpoints.implementing-custom.web.range-requests]] +===== Web Endpoint Range Requests +An HTTP range request can be used to request part of an HTTP resource. +When using Spring MVC or Spring Web Flux, operations that return a `org.springframework.core.io.Resource` automatically support range requests. + +NOTE: Range requests are not supported when using Jersey. + + + +[[actuator.endpoints.implementing-custom.web.security]] +===== Web Endpoint Security +An operation on a web endpoint or a web-specific endpoint extension can receive the current `java.security.Principal` or `org.springframework.boot.actuate.endpoint.SecurityContext` as a method parameter. +The former is typically used in conjunction with `@Nullable` to provide different behavior for authenticated and unauthenticated users. +The latter is typically used to perform authorization checks using its `isUserInRole(String)` method. + + + +[[actuator.endpoints.implementing-custom.servlet]] +==== Servlet Endpoints +A `Servlet` can be exposed as an endpoint by implementing a class annotated with `@ServletEndpoint` that also implements `Supplier`. +Servlet endpoints provide deeper integration with the Servlet container but at the expense of portability. +They are intended to be used to expose an existing `Servlet` as an endpoint. +For new endpoints, the `@Endpoint` and `@WebEndpoint` annotations should be preferred whenever possible. + + + +[[actuator.endpoints.implementing-custom.controller]] +==== Controller Endpoints +`@ControllerEndpoint` and `@RestControllerEndpoint` can be used to implement an endpoint that is only exposed by Spring MVC or Spring WebFlux. +Methods are mapped using the standard annotations for Spring MVC and Spring WebFlux such as `@RequestMapping` and `@GetMapping`, with the endpoint's ID being used as a prefix for the path. +Controller endpoints provide deeper integration with Spring's web frameworks but at the expense of portability. +The `@Endpoint` and `@WebEndpoint` annotations should be preferred whenever possible. + + + +[[actuator.endpoints.health]] +=== Health Information +You can use health information to check the status of your running application. +It is often used by monitoring software to alert someone when a production system goes down. +The information exposed by the `health` endpoint depends on the configprop:management.endpoint.health.show-details[] and configprop:management.endpoint.health.show-components[] properties which can be configured with one of the following values: + +[cols="1, 3"] +|=== +| Name | Description + +| `never` +| Details are never shown. + +| `when-authorized` +| Details are only shown to authorized users. + Authorized roles can be configured using `management.endpoint.health.roles`. + +| `always` +| Details are shown to all users. +|=== + +The default value is `never`. +A user is considered to be authorized when they are in one or more of the endpoint's roles. +If the endpoint has no configured roles (the default) all authenticated users are considered to be authorized. +The roles can be configured using the configprop:management.endpoint.health.roles[] property. + +NOTE: If you have secured your application and wish to use `always`, your security configuration must permit access to the health endpoint for both authenticated and unauthenticated users. + +Health information is collected from the content of a {spring-boot-actuator-module-code}/health/HealthContributorRegistry.java[`HealthContributorRegistry`] (by default all {spring-boot-actuator-module-code}/health/HealthContributor.java[`HealthContributor`] instances defined in your `ApplicationContext`). +Spring Boot includes a number of auto-configured `HealthContributors` and you can also write your own. + +A `HealthContributor` can either be a `HealthIndicator` or a `CompositeHealthContributor`. +A `HealthIndicator` provides actual health information, including a `Status`. +A `CompositeHealthContributor` provides a composite of other `HealthContributors`. +Taken together, contributors form a tree structure to represent the overall system health. + +By default, the final system health is derived by a `StatusAggregator` which sorts the statuses from each `HealthIndicator` based on an ordered list of statuses. +The first status in the sorted list is used as the overall health status. +If no `HealthIndicator` returns a status that is known to the `StatusAggregator`, an `UNKNOWN` status is used. + +TIP: The `HealthContributorRegistry` can be used to register and unregister health indicators at runtime. + + + +[[actuator.endpoints.health.auto-configured-health-indicators]] +==== Auto-configured HealthIndicators +The following `HealthIndicators` are auto-configured by Spring Boot when appropriate. +You can also enable/disable selected indicators by configuring `management.health.key.enabled`, +with the `key` listed in the table below. + +[cols="2,4,6"] +|=== +| Key | Name | Description + +| `cassandra` +| {spring-boot-actuator-module-code}/cassandra/CassandraDriverHealthIndicator.java[`CassandraDriverHealthIndicator`] +| Checks that a Cassandra database is up. + +| `couchbase` +| {spring-boot-actuator-module-code}/couchbase/CouchbaseHealthIndicator.java[`CouchbaseHealthIndicator`] +| Checks that a Couchbase cluster is up. + +| `db` +| {spring-boot-actuator-module-code}/jdbc/DataSourceHealthIndicator.java[`DataSourceHealthIndicator`] +| Checks that a connection to `DataSource` can be obtained. + +| `diskspace` +| {spring-boot-actuator-module-code}/system/DiskSpaceHealthIndicator.java[`DiskSpaceHealthIndicator`] +| Checks for low disk space. + +| `elasticsearch` +| {spring-boot-actuator-module-code}/elasticsearch/ElasticsearchRestHealthIndicator.java[`ElasticsearchRestHealthIndicator`] +| Checks that an Elasticsearch cluster is up. + +| `hazelcast` +| {spring-boot-actuator-module-code}/hazelcast/HazelcastHealthIndicator.java[`HazelcastHealthIndicator`] +| Checks that a Hazelcast server is up. + +| `influxdb` +| {spring-boot-actuator-module-code}/influx/InfluxDbHealthIndicator.java[`InfluxDbHealthIndicator`] +| Checks that an InfluxDB server is up. + +| `jms` +| {spring-boot-actuator-module-code}/jms/JmsHealthIndicator.java[`JmsHealthIndicator`] +| Checks that a JMS broker is up. + +| `ldap` +| {spring-boot-actuator-module-code}/ldap/LdapHealthIndicator.java[`LdapHealthIndicator`] +| Checks that an LDAP server is up. + +| `mail` +| {spring-boot-actuator-module-code}/mail/MailHealthIndicator.java[`MailHealthIndicator`] +| Checks that a mail server is up. + +| `mongo` +| {spring-boot-actuator-module-code}/mongo/MongoHealthIndicator.java[`MongoHealthIndicator`] +| Checks that a Mongo database is up. + +| `neo4j` +| {spring-boot-actuator-module-code}/neo4j/Neo4jHealthIndicator.java[`Neo4jHealthIndicator`] +| Checks that a Neo4j database is up. + +| `ping` +| {spring-boot-actuator-module-code}/health/PingHealthIndicator.java[`PingHealthIndicator`] +| Always responds with `UP`. + +| `rabbit` +| {spring-boot-actuator-module-code}/amqp/RabbitHealthIndicator.java[`RabbitHealthIndicator`] +| Checks that a Rabbit server is up. + +| `redis` +| {spring-boot-actuator-module-code}/redis/RedisHealthIndicator.java[`RedisHealthIndicator`] +| Checks that a Redis server is up. + +| `solr` +| {spring-boot-actuator-module-code}/solr/SolrHealthIndicator.java[`SolrHealthIndicator`] +| Checks that a Solr server is up. +|=== + +TIP: You can disable them all by setting the configprop:management.health.defaults.enabled[] property. + +Additional `HealthIndicators` are available but not enabled by default: + +[cols="3,4,6"] +|=== +| Key | Name | Description + +| `livenessstate` +| {spring-boot-actuator-module-code}/availability/LivenessStateHealthIndicator.java[`LivenessStateHealthIndicator`] +| Exposes the "Liveness" application availability state. + +| `readinessstate` +| {spring-boot-actuator-module-code}/availability/ReadinessStateHealthIndicator.java[`ReadinessStateHealthIndicator`] +| Exposes the "Readiness" application availability state. +|=== + + + +[[actuator.endpoints.health.writing-custom-health-indicators]] +==== Writing Custom HealthIndicators +To provide custom health information, you can register Spring beans that implement the {spring-boot-actuator-module-code}/health/HealthIndicator.java[`HealthIndicator`] interface. +You need to provide an implementation of the `health()` method and return a `Health` response. +The `Health` response should include a status and can optionally include additional details to be displayed. +The following code shows a sample `HealthIndicator` implementation: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/actuator/endpoints/health/writingcustomhealthindicators/MyHealthIndicator.java[] +---- + +NOTE: The identifier for a given `HealthIndicator` is the name of the bean without the `HealthIndicator` suffix, if it exists. +In the preceding example, the health information is available in an entry named `my`. + +In addition to Spring Boot's predefined {spring-boot-actuator-module-code}/health/Status.java[`Status`] types, it is also possible for `Health` to return a custom `Status` that represents a new system state. +In such cases, a custom implementation of the {spring-boot-actuator-module-code}/health/StatusAggregator.java[`StatusAggregator`] interface also needs to be provided, or the default implementation has to be configured by using the configprop:management.endpoint.health.status.order[] configuration property. + +For example, assume a new `Status` with code `FATAL` is being used in one of your `HealthIndicator` implementations. +To configure the severity order, add the following property to your application properties: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + endpoint: + health: + status: + order: "fatal,down,out-of-service,unknown,up" +---- + +The HTTP status code in the response reflects the overall health status. +By default, `OUT_OF_SERVICE` and `DOWN` map to 503. +Any unmapped health statuses, including `UP`, map to 200. +You might also want to register custom status mappings if you access the health endpoint over HTTP. +Configuring a custom mapping disables the defaults mappings for `DOWN` and `OUT_OF_SERVICE`. +If you want to retain the default mappings they must be configured explicitly alongside any custom mappings. +For example, the following property maps `FATAL` to 503 (service unavailable) and retains the default mappings for `DOWN` and `OUT_OF_SERVICE`: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + endpoint: + health: + status: + http-mapping: + down: 503 + fatal: 503 + out-of-service: 503 +---- + +TIP: If you need more control, you can define your own `HttpCodeStatusMapper` bean. + +The following table shows the default status mappings for the built-in statuses: + +[cols="1,3"] +|=== +| Status | Mapping + +| `DOWN` +| `SERVICE_UNAVAILABLE` (`503`) + +| `OUT_OF_SERVICE` +| `SERVICE_UNAVAILABLE` (`503`) + +| `UP` +| No mapping by default, so HTTP status is `200` + +| `UNKNOWN` +| No mapping by default, so HTTP status is `200` +|=== + + + +[[actuator.endpoints.health.reactive-health-indicators]] +==== Reactive Health Indicators +For reactive applications, such as those using Spring WebFlux, `ReactiveHealthContributor` provides a non-blocking contract for getting application health. +Similar to a traditional `HealthContributor`, health information is collected from the content of a {spring-boot-actuator-module-code}/health/ReactiveHealthContributorRegistry.java[`ReactiveHealthContributorRegistry`] (by default all {spring-boot-actuator-module-code}/health/HealthContributor.java[`HealthContributor`] and {spring-boot-actuator-module-code}/health/ReactiveHealthContributor.java[`ReactiveHealthContributor`] instances defined in your `ApplicationContext`). +Regular `HealthContributors` that do not check against a reactive API are executed on the elastic scheduler. + +TIP: In a reactive application, The `ReactiveHealthContributorRegistry` should be used to register and unregister health indicators at runtime. +If you need to register a regular `HealthContributor`, you should wrap it using `ReactiveHealthContributor#adapt`. + +To provide custom health information from a reactive API, you can register Spring beans that implement the {spring-boot-actuator-module-code}/health/ReactiveHealthIndicator.java[`ReactiveHealthIndicator`] interface. +The following code shows a sample `ReactiveHealthIndicator` implementation: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/actuator/endpoints/health/reactivehealthindicators/MyReactiveHealthIndicator.java[] +---- + +TIP: To handle the error automatically, consider extending from `AbstractReactiveHealthIndicator`. + + + +[[actuator.endpoints.health.auto-configured-reactive-health-indicators]] +==== Auto-configured ReactiveHealthIndicators +The following `ReactiveHealthIndicators` are auto-configured by Spring Boot when appropriate: + +[cols="2,4,6"] +|=== +| Key | Name | Description + +| `cassandra` +| {spring-boot-actuator-module-code}/cassandra/CassandraDriverReactiveHealthIndicator.java[`CassandraDriverReactiveHealthIndicator`] +| Checks that a Cassandra database is up. + +| `couchbase` +| {spring-boot-actuator-module-code}/couchbase/CouchbaseReactiveHealthIndicator.java[`CouchbaseReactiveHealthIndicator`] +| Checks that a Couchbase cluster is up. + +| `elasticsearch` +| {spring-boot-actuator-module-code}/elasticsearch/ElasticsearchReactiveHealthIndicator.java[`ElasticsearchReactiveHealthIndicator`] +| Checks that an Elasticsearch cluster is up. + +| `mongo` +| {spring-boot-actuator-module-code}/mongo/MongoReactiveHealthIndicator.java[`MongoReactiveHealthIndicator`] +| Checks that a Mongo database is up. + +| `neo4j` +| {spring-boot-actuator-module-code}/neo4j/Neo4jReactiveHealthIndicator.java[`Neo4jReactiveHealthIndicator`] +| Checks that a Neo4j database is up. + +| `redis` +| {spring-boot-actuator-module-code}/redis/RedisReactiveHealthIndicator.java[`RedisReactiveHealthIndicator`] +| Checks that a Redis server is up. +|=== + +TIP: If necessary, reactive indicators replace the regular ones. +Also, any `HealthIndicator` that is not handled explicitly is wrapped automatically. + + + +[[actuator.endpoints.health.groups]] +==== Health Groups +It's sometimes useful to organize health indicators into groups that can be used for different purposes. + +To create a health indicator group you can use the `management.endpoint.health.group.` property and specify a list of health indicator IDs to `include` or `exclude`. +For example, to create a group that includes only database indicators you can define the following: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + endpoint: + health: + group: + custom: + include: "db" +---- + +You can then check the result by hitting `http://localhost:8080/actuator/health/custom`. + +Similarly, to create a group that excludes the database indicators from the group and includes all the other indicators, you can define the following: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + endpoint: + health: + group: + custom: + exclude: "db" +---- + +By default groups will inherit the same `StatusAggregator` and `HttpCodeStatusMapper` settings as the system health, however, these can also be defined on a per-group basis. +It's also possible to override the `show-details` and `roles` properties if required: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + endpoint: + health: + group: + custom: + show-details: "when-authorized" + roles: "admin" + status: + order: "fatal,up" + http-mapping: + fatal: 500 + out-of-service: 500 +---- + +TIP: You can use `@Qualifier("groupname")` if you need to register custom `StatusAggregator` or `HttpCodeStatusMapper` beans for use with the group. + + + +[[actuator.endpoints.health.datasource]] +==== DataSource Health +The `DataSource` health indicator shows the health of both standard data source and routing data source beans. +The health of a routing data source includes the health of each of its target data sources. +In the health endpoint's response, each of a routing data source's targets is named using its routing key. +If you prefer not to include routing data sources in the indicator's output, set configprop:management.health.db.ignore-routing-data-sources[] to `true`. + + + +[[actuator.endpoints.kubernetes-probes]] +=== Kubernetes Probes +Applications deployed on Kubernetes can provide information about their internal state with https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-probes[Container Probes]. +Depending on https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/[your Kubernetes configuration], the kubelet will call those probes and react to the result. + +Spring Boot manages your <> out-of-the-box. +If deployed in a Kubernetes environment, actuator will gather the "Liveness" and "Readiness" information from the `ApplicationAvailability` interface and use that information in dedicated <>: `LivenessStateHealthIndicator` and `ReadinessStateHealthIndicator`. +These indicators will be shown on the global health endpoint (`"/actuator/health"`). +They will also be exposed as separate HTTP Probes using <>: `"/actuator/health/liveness"` and `"/actuator/health/readiness"`. + +You can then configure your Kubernetes infrastructure with the following endpoint information: + +[source,yml,indent=0,subs="verbatim"] +---- +livenessProbe: + httpGet: + path: /actuator/health/liveness + port: + failureThreshold: ... + periodSeconds: ... + +readinessProbe: + httpGet: + path: /actuator/health/readiness + port: + failureThreshold: ... + periodSeconds: ... +---- + +NOTE: `` should be set to the port that the actuator endpoints are available on. +It could be the main web server port, or a separate management port if the `"management.server.port"` property has been set. + +These health groups are only enabled automatically if the application is <>. +You can enable them in any environment using the configprop:management.endpoint.health.probes.enabled[] configuration property. + +NOTE: If an application takes longer to start than the configured liveness period, Kubernetes mention the `"startupProbe"` as a possible solution. +The `"startupProbe"` is not necessarily needed here as the `"readinessProbe"` fails until all startup tasks are done, see <>. + +WARNING: If your Actuator endpoints are deployed on a separate management context, be aware that endpoints are then not using the same web infrastructure (port, connection pools, framework components) as the main application. +In this case, a probe check could be successful even if the main application does not work properly (for example, it cannot accept new connections). + + + +[[actuator.endpoints.kubernetes-probes.external-state]] +==== Checking External State with Kubernetes Probes +Actuator configures the "liveness" and "readiness" probes as Health Groups; this means that all the <> are available for them. +You can, for example, configure additional Health Indicators: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + endpoint: + health: + group: + readiness: + include: "readinessState,customCheck" +---- + +By default, Spring Boot does not add other Health Indicators to these groups. + +The "`liveness`" Probe should not depend on health checks for external systems. +If the <> is broken, Kubernetes will try to solve that problem by restarting the application instance. +This means that if an external system fails (e.g. a database, a Web API, an external cache), Kubernetes might restart all application instances and create cascading failures. + +As for the "`readiness`" Probe, the choice of checking external systems must be made carefully by the application developers, i.e. Spring Boot does not include any additional health checks in the readiness probe. +If the <> is unready, Kubernetes will not route traffic to that instance. +Some external systems might not be shared by application instances, in which case they could quite naturally be included in a readiness probe. +Other external systems might not be essential to the application (the application could have circuit breakers and fallbacks), in which case they definitely should not be included. +Unfortunately, an external system that is shared by all application instances is common, and you have to make a judgement call: include it in the readiness probe and expect that the application is taken out of service when the external service is down, or leave it out and deal with failures higher up the stack, e.g. using a circuit breaker in the caller. + +NOTE: If all instances of an application are unready, a Kubernetes Service with `type=ClusterIP` or `NodePort` will not accept any incoming connections. +There is no HTTP error response (503 etc.) since there is no connection. +A Service with `type=LoadBalancer` might or might not accept connections, depending on the provider. +A Service that has an explicit https://kubernetes.io/docs/concepts/services-networking/ingress/[Ingress] will also respond in a way that depends on the implementation - the ingress service itself will have to decide how to handle the "connection refused" from downstream. +HTTP 503 is quite likely in the case of both load balancer and ingress. + +Also, if an application is using Kubernetes https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/[autoscaling] it may react differently to applications being taken out of the load-balancer, depending on its autoscaler configuration. + + + +[[actuator.endpoints.kubernetes-probes.lifecycle]] +==== Application Lifecycle and Probe States +An important aspect of the Kubernetes Probes support is its consistency with the application lifecycle. +There is a significant difference between the `AvailabilityState` which is the in-memory, internal state of the application +and the actual Probe which exposes that state: depending on the phase of application lifecycle, the Probe might not be available. + +Spring Boot publishes <>, +and Probes can listen to such events and expose the `AvailabilityState` information. + +The following tables show the `AvailabilityState` and the state of HTTP connectors at different stages. + +When a Spring Boot application starts: + +[cols="2,2,2,3,5"] +|=== +|Startup phase |LivenessState |ReadinessState |HTTP server |Notes + +|Starting +|`BROKEN` +|`REFUSING_TRAFFIC` +|Not started +|Kubernetes checks the "liveness" Probe and restarts the application if it takes too long. + +|Started +|`CORRECT` +|`REFUSING_TRAFFIC` +|Refuses requests +|The application context is refreshed. The application performs startup tasks and does not receive traffic yet. + +|Ready +|`CORRECT` +|`ACCEPTING_TRAFFIC` +|Accepts requests +|Startup tasks are finished. The application is receiving traffic. +|=== + +When a Spring Boot application shuts down: + +[cols="2,2,2,3,5"] +|=== +|Shutdown phase |Liveness State |Readiness State |HTTP server |Notes + +|Running +|`CORRECT` +|`ACCEPTING_TRAFFIC` +|Accepts requests +|Shutdown has been requested. + +|Graceful shutdown +|`CORRECT` +|`REFUSING_TRAFFIC` +|New requests are rejected +|If enabled, <>. + +|Shutdown complete +|N/A +|N/A +|Server is shut down +|The application context is closed and the application is shut down. +|=== + +TIP: Check out the <> for more information about Kubernetes deployment. + + + +[[actuator.endpoints.info]] +=== Application Information +Application information exposes various information collected from all {spring-boot-actuator-module-code}/info/InfoContributor.java[`InfoContributor`] beans defined in your `ApplicationContext`. +Spring Boot includes a number of auto-configured `InfoContributor` beans, and you can write your own. + + + +[[actuator.endpoints.info.auto-configured-info-contributors]] +==== Auto-configured InfoContributors +The following `InfoContributor` beans are auto-configured by Spring Boot, when appropriate: + +[cols="1,4"] +|=== +| Name | Description + +| {spring-boot-actuator-module-code}/info/EnvironmentInfoContributor.java[`EnvironmentInfoContributor`] +| Exposes any key from the `Environment` under the `info` key. + +| {spring-boot-actuator-module-code}/info/GitInfoContributor.java[`GitInfoContributor`] +| Exposes git information if a `git.properties` file is available. + +| {spring-boot-actuator-module-code}/info/BuildInfoContributor.java[`BuildInfoContributor`] +| Exposes build information if a `META-INF/build-info.properties` file is available. +|=== + +TIP: It is possible to disable them all by setting the configprop:management.info.defaults.enabled[] property. + + + +[[actuator.endpoints.info.custom-application-information]] +==== Custom Application Information +You can customize the data exposed by the `info` endpoint by setting `+info.*+` Spring properties. +All `Environment` properties under the `info` key are automatically exposed. +For example, you could add the following settings to your `application.properties` file: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + info: + app: + encoding: "UTF-8" + java: + source: "11" + target: "11" +---- + +[TIP] +==== +Rather than hardcoding those values, you could also <>. + +Assuming you use Maven, you could rewrite the preceding example as follows: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + info: + app: + encoding: "@project.build.sourceEncoding@" + java: + source: "@java.version@" + target: "@java.version@" +---- +==== + + + +[[actuator.endpoints.info.git-commit-information]] +==== Git Commit Information +Another useful feature of the `info` endpoint is its ability to publish information about the state of your `git` source code repository when the project was built. +If a `GitProperties` bean is available, the `info` endpoint can be used to expose these properties. + +TIP: A `GitProperties` bean is auto-configured if a `git.properties` file is available at the root of the classpath. +See "<>" for more details. + +By default, the endpoint exposes `git.branch`, `git.commit.id`, and `git.commit.time` properties, if present. +If you don't want any of these properties in the endpoint response, they need to be excluded from the `git.properties` file. +If you want to display the full git information (that is, the full content of `git.properties`), use the configprop:management.info.git.mode[] property, as follows: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + info: + git: + mode: "full" +---- + +To disable the git commit information from the `info` endpoint completely, set the configprop:management.info.git.enabled[] property to `false`, as follows: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + info: + git: + enabled: false +---- + + + +[[actuator.endpoints.info.build-information]] +==== Build Information +If a `BuildProperties` bean is available, the `info` endpoint can also publish information about your build. +This happens if a `META-INF/build-info.properties` file is available in the classpath. + +TIP: The Maven and Gradle plugins can both generate that file. +See "<>" for more details. + + + +[[actuator.endpoints.info.writing-custom-info-contributors]] +==== Writing Custom InfoContributors +To provide custom application information, you can register Spring beans that implement the {spring-boot-actuator-module-code}/info/InfoContributor.java[`InfoContributor`] interface. + +The following example contributes an `example` entry with a single value: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/actuator/endpoints/info/writingcustominfocontributors/MyInfoContributor.java[] +---- + +If you reach the `info` endpoint, you should see a response that contains the following additional entry: + +[source,json,indent=0,subs="verbatim"] +---- + { + "example": { + "key" : "value" + } + } +---- diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/from-1x.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/from-1x.adoc new file mode 100644 index 000000000000..10b440cac895 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/from-1x.adoc @@ -0,0 +1,5 @@ +[[upgrading.from-1x]] +== Upgrading from 1.x + +If you are upgrading from the `1.x` release of Spring Boot, check the {github-wiki}/Spring-Boot-2.0-Migration-Guide["`migration guide`" on the project wiki] that provides detailed upgrade instructions. +Check also the {github-wiki}["`release notes`"] for a list of "`new and noteworthy`" features for each release. \ No newline at end of file diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/jmx.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/jmx.adoc new file mode 100644 index 000000000000..16b0936b80ec --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/jmx.adoc @@ -0,0 +1,99 @@ +[[actuator.jmx]] +== Monitoring and Management over JMX +Java Management Extensions (JMX) provide a standard mechanism to monitor and manage applications. +By default, this feature is not enabled and can be turned on by setting the configuration property configprop:spring.jmx.enabled[] to `true`. +Spring Boot exposes management endpoints as JMX MBeans under the `org.springframework.boot` domain by default. +To Take full control over endpoints registration in the JMX domain, consider registering your own `EndpointObjectNameFactory` implementation. + + + +[[actuator.jmx.custom-mbean-names]] +=== Customizing MBean Names +The name of the MBean is usually generated from the `id` of the endpoint. +For example, the `health` endpoint is exposed as `org.springframework.boot:type=Endpoint,name=Health`. + +If your application contains more than one Spring `ApplicationContext`, you may find that names clash. +To solve this problem, you can set the configprop:spring.jmx.unique-names[] property to `true` so that MBean names are always unique. + +You can also customize the JMX domain under which endpoints are exposed. +The following settings show an example of doing so in `application.properties`: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + jmx: + unique-names: true + management: + endpoints: + jmx: + domain: "com.example.myapp" +---- + + + +[[actuator.jmx.disable-jmx-endpoints]] +=== Disabling JMX Endpoints +If you do not want to expose endpoints over JMX, you can set the configprop:management.endpoints.jmx.exposure.exclude[] property to `*`, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + endpoints: + jmx: + exposure: + exclude: "*" +---- + + + +[[actuator.jmx.jolokia]] +=== Using Jolokia for JMX over HTTP +Jolokia is a JMX-HTTP bridge that provides an alternative method of accessing JMX beans. +To use Jolokia, include a dependency to `org.jolokia:jolokia-core`. +For example, with Maven, you would add the following dependency: + +[source,xml,indent=0,subs="verbatim"] +---- + + org.jolokia + jolokia-core + +---- + +The Jolokia endpoint can then be exposed by adding `jolokia` or `*` to the configprop:management.endpoints.web.exposure.include[] property. +You can then access it by using `/actuator/jolokia` on your management HTTP server. + +NOTE: The Jolokia endpoint exposes Jolokia's servlet as an actuator endpoint. +As a result, it is specific to servlet environments such as Spring MVC and Jersey. +The endpoint will not be available in a WebFlux application. + + + +[[actuator.jmx.jolokia.customizing]] +==== Customizing Jolokia +Jolokia has a number of settings that you would traditionally configure by setting servlet parameters. +With Spring Boot, you can use your `application.properties` file. +To do so, prefix the parameter with `management.endpoint.jolokia.config.`, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + endpoint: + jolokia: + config: + debug: true +---- + + + +[[actuator.jmx.jolokia.disabling]] +==== Disabling Jolokia +If you use Jolokia but do not want Spring Boot to configure it, set the configprop:management.endpoint.jolokia.enabled[] property to `false`, as follows: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + endpoint: + jolokia: + enabled: false +---- diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/loggers.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/loggers.adoc new file mode 100644 index 000000000000..8ada3ab66d5e --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/loggers.adoc @@ -0,0 +1,31 @@ +[[actuator.loggers]] +== Loggers +Spring Boot Actuator includes the ability to view and configure the log levels of your application at runtime. +You can view either the entire list or an individual logger's configuration, which is made up of both the explicitly configured logging level as well as the effective logging level given to it by the logging framework. +These levels can be one of: + +* `TRACE` +* `DEBUG` +* `INFO` +* `WARN` +* `ERROR` +* `FATAL` +* `OFF` +* `null` + +`null` indicates that there is no explicit configuration. + + + +[[actuator.loggers.configure]] +=== Configure a Logger +To configure a given logger, `POST` a partial entity to the resource's URI, as shown in the following example: + +[source,json,indent=0,subs="verbatim"] +---- + { + "configuredLevel": "DEBUG" + } +---- + +TIP: To "`reset`" the specific level of the logger (and use the default configuration instead), you can pass a value of `null` as the `configuredLevel`. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc new file mode 100644 index 000000000000..07e78fd51467 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc @@ -0,0 +1,1106 @@ +[[actuator.metrics]] +== Metrics +Spring Boot Actuator provides dependency management and auto-configuration for https://micrometer.io[Micrometer], an application metrics facade that supports {micrometer-docs}[numerous monitoring systems], including: + +- <> +- <> +- <> +- <> +- <> +- <> +- <> +- <> +- <> +- <> +- <> +- <> +- <> +- <> +- <> +- <> +- <> +- <> + +TIP: To learn more about Micrometer's capabilities, please refer to its https://micrometer.io/docs[reference documentation], in particular the {micrometer-concepts-docs}[concepts section]. + + + +[[actuator.metrics.getting-started]] +=== Getting started +Spring Boot auto-configures a composite `MeterRegistry` and adds a registry to the composite for each of the supported implementations that it finds on the classpath. +Having a dependency on `micrometer-registry-\{system}` in your runtime classpath is enough for Spring Boot to configure the registry. + +Most registries share common features. +For instance, you can disable a particular registry even if the Micrometer registry implementation is on the classpath. +For example, to disable Datadog: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + export: + datadog: + enabled: false +---- + +You can also disable all registries unless stated otherwise by the registry-specific property, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + export: + defaults: + enabled: false +---- + +Spring Boot will also add any auto-configured registries to the global static composite registry on the `Metrics` class unless you explicitly tell it not to: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + use-global-registry: false +---- + +You can register any number of `MeterRegistryCustomizer` beans to further configure the registry, such as applying common tags, before any meters are registered with the registry: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/actuator/metrics/gettingstarted/commontags/MyMeterRegistryConfiguration.java[] +---- + +You can apply customizations to particular registry implementations by being more specific about the generic type: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/actuator/metrics/gettingstarted/specifictype/MyMeterRegistryConfiguration.java[] +---- + +Spring Boot also <> that you can control via configuration or dedicated annotation markers. + + + +[[actuator.metrics.export]] +=== Supported Monitoring Systems + + + +[[actuator.metrics.export.appoptics]] +==== AppOptics +By default, the AppOptics registry pushes metrics to `https://api.appoptics.com/v1/measurements` periodically. +To export metrics to SaaS {micrometer-registry-docs}/appOptics[AppOptics], your API token must be provided: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + export: + appoptics: + api-token: "YOUR_TOKEN" +---- + + + +[[actuator.metrics.export.atlas]] +==== Atlas +By default, metrics are exported to {micrometer-registry-docs}/atlas[Atlas] running on your local machine. +The location of the https://github.com/Netflix/atlas[Atlas server] to use can be provided using: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + export: + atlas: + uri: "https://atlas.example.com:7101/api/v1/publish" +---- + + + +[[actuator.metrics.export.datadog]] +==== Datadog +Datadog registry pushes metrics to https://www.datadoghq.com[datadoghq] periodically. +To export metrics to {micrometer-registry-docs}/datadog[Datadog], your API key must be provided: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + export: + datadog: + api-key: "YOUR_KEY" +---- + +You can also change the interval at which metrics are sent to Datadog: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + export: + datadog: + step: "30s" +---- + + + +[[actuator.metrics.export.dynatrace]] +==== Dynatrace +Dynatrace registry pushes metrics to the configured URI periodically. +To export metrics to {micrometer-registry-docs}/dynatrace[Dynatrace], your API token, device ID, and URI must be provided: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + export: + dynatrace: + api-token: "YOUR_TOKEN" + device-id: "YOUR_DEVICE_ID" + uri: "YOUR_URI" +---- + +You can also change the interval at which metrics are sent to Dynatrace: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + export: + dynatrace: + step: "30s" +---- + + + +[[actuator.metrics.export.elastic]] +==== Elastic +By default, metrics are exported to {micrometer-registry-docs}/elastic[Elastic] running on your local machine. +The location of the Elastic server to use can be provided using the following property: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + export: + elastic: + host: "https://elastic.example.com:8086" +---- + + + +[[actuator.metrics.export.ganglia]] +==== Ganglia +By default, metrics are exported to {micrometer-registry-docs}/ganglia[Ganglia] running on your local machine. +The http://ganglia.sourceforge.net[Ganglia server] host and port to use can be provided using: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + export: + ganglia: + host: "ganglia.example.com" + port: 9649 +---- + + + +[[actuator.metrics.export.graphite]] +==== Graphite +By default, metrics are exported to {micrometer-registry-docs}/graphite[Graphite] running on your local machine. +The https://graphiteapp.org[Graphite server] host and port to use can be provided using: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + export: + graphite: + host: "graphite.example.com" + port: 9004 +---- + +Micrometer provides a default `HierarchicalNameMapper` that governs how a dimensional meter id is {micrometer-registry-docs}/graphite#_hierarchical_name_mapping[mapped to flat hierarchical names]. + +TIP: To take control over this behavior, define your `GraphiteMeterRegistry` and supply your own `HierarchicalNameMapper`. +An auto-configured `GraphiteConfig` and `Clock` beans are provided unless you define your own: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/actuator/metrics/export/graphite/MyGraphiteConfiguration.java[] +---- + + + +[[actuator.metrics.export.humio]] +==== Humio +By default, the Humio registry pushes metrics to https://cloud.humio.com periodically. +To export metrics to SaaS {micrometer-registry-docs}/humio[Humio], your API token must be provided: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + export: + humio: + api-token: "YOUR_TOKEN" +---- + +You should also configure one or more tags to identify the data source to which metrics will be pushed: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + export: + humio: + tags: + alpha: "a" + bravo: "b" +---- + + + +[[actuator.metrics.export.influx]] +==== Influx +By default, metrics are exported to an {micrometer-registry-docs}/influx[Influx] v1 instance running on your local machine with the default configuration. +To export metrics to InfluxDB v2, configure the `org`, `bucket`, and authentication `token` for writing metrics. +The location of the https://www.influxdata.com[Influx server] to use can be provided using: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + export: + influx: + uri: "https://influx.example.com:8086" +---- + + + +[[actuator.metrics.export.jmx]] +==== JMX +Micrometer provides a hierarchical mapping to {micrometer-registry-docs}/jmx[JMX], primarily as a cheap and portable way to view metrics locally. +By default, metrics are exported to the `metrics` JMX domain. +The domain to use can be provided using: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + export: + jmx: + domain: "com.example.app.metrics" +---- + +Micrometer provides a default `HierarchicalNameMapper` that governs how a dimensional meter id is {micrometer-registry-docs}/jmx#_hierarchical_name_mapping[mapped to flat hierarchical names]. + +TIP: To take control over this behavior, define your `JmxMeterRegistry` and supply your own `HierarchicalNameMapper`. +An auto-configured `JmxConfig` and `Clock` beans are provided unless you define your own: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/actuator/metrics/export/jmx/MyJmxConfiguration.java[] +---- + + + +[[actuator.metrics.export.kairos]] +==== KairosDB +By default, metrics are exported to {micrometer-registry-docs}/kairos[KairosDB] running on your local machine. +The location of the https://kairosdb.github.io/[KairosDB server] to use can be provided using: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + export: + kairos: + uri: "https://kairosdb.example.com:8080/api/v1/datapoints" +---- + + + +[[actuator.metrics.export.newrelic]] +==== New Relic +New Relic registry pushes metrics to {micrometer-registry-docs}/new-relic[New Relic] periodically. +To export metrics to https://newrelic.com[New Relic], your API key and account id must be provided: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + export: + newrelic: + api-key: "YOUR_KEY" + account-id: "YOUR_ACCOUNT_ID" +---- + +You can also change the interval at which metrics are sent to New Relic: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + export: + newrelic: + step: "30s" +---- + +By default, metrics are published via REST calls but it is also possible to use the Java Agent API if you have it on the classpath: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + export: + newrelic: + client-provider-type: "insights-agent" +---- + +Finally, you can take full control by defining your own `NewRelicClientProvider` bean. + + + +[[actuator.metrics.export.prometheus]] +==== Prometheus +{micrometer-registry-docs}/prometheus[Prometheus] expects to scrape or poll individual app instances for metrics. +Spring Boot provides an actuator endpoint available at `/actuator/prometheus` to present a https://prometheus.io[Prometheus scrape] with the appropriate format. + +TIP: The endpoint is not available by default and must be exposed, see <> for more details. + +Here is an example `scrape_config` to add to `prometheus.yml`: + +[source,yaml,indent=0,subs="verbatim"] +---- + scrape_configs: + - job_name: 'spring' + metrics_path: '/actuator/prometheus' + static_configs: + - targets: ['HOST:PORT'] +---- + +For ephemeral or batch jobs which may not exist long enough to be scraped, https://github.com/prometheus/pushgateway[Prometheus Pushgateway] support can be used to expose their metrics to Prometheus. +To enable Prometheus Pushgateway support, add the following dependency to your project: + +[source,xml,indent=0,subs="verbatim"] +---- + + io.prometheus + simpleclient_pushgateway + +---- + +When the Prometheus Pushgateway dependency is present on the classpath and the configprop:management.metrics.export.prometheus.pushgateway.enabled[] property is set to `true`, a `PrometheusPushGatewayManager` bean is auto-configured. +This manages the pushing of metrics to a Prometheus Pushgateway. + +The `PrometheusPushGatewayManager` can be tuned using properties under `management.metrics.export.prometheus.pushgateway`. +For advanced configuration, you can also provide your own `PrometheusPushGatewayManager` bean. + + + +[[actuator.metrics.export.signalfx]] +==== SignalFx +SignalFx registry pushes metrics to {micrometer-registry-docs}/signalFx[SignalFx] periodically. +To export metrics to https://www.signalfx.com[SignalFx], your access token must be provided: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + export: + signalfx: + access-token: "YOUR_ACCESS_TOKEN" +---- + +You can also change the interval at which metrics are sent to SignalFx: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + export: + signalfx: + step: "30s" +---- + + + +[[actuator.metrics.export.simple]] +==== Simple +Micrometer ships with a simple, in-memory backend that is automatically used as a fallback if no other registry is configured. +This allows you to see what metrics are collected in the <>. + +The in-memory backend disables itself as soon as you're using any of the other available backend. +You can also disable it explicitly: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + export: + simple: + enabled: false +---- + + + +[[actuator.metrics.export.stackdriver]] +==== Stackdriver +Stackdriver registry pushes metrics to https://cloud.google.com/stackdriver/[Stackdriver] periodically. +To export metrics to SaaS {micrometer-registry-docs}/stackdriver[Stackdriver], your Google Cloud project id must be provided: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + export: + stackdriver: + project-id: "my-project" +---- + +You can also change the interval at which metrics are sent to Stackdriver: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + export: + stackdriver: + step: "30s" +---- + + + +[[actuator.metrics.export.statsd]] +==== StatsD +The StatsD registry pushes metrics over UDP to a StatsD agent eagerly. +By default, metrics are exported to a {micrometer-registry-docs}/statsD[StatsD] agent running on your local machine. +The StatsD agent host, port, and protocol to use can be provided using: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + export: + statsd: + host: "statsd.example.com" + port: 9125 + protocol: "udp" +---- + +You can also change the StatsD line protocol to use (default to Datadog): + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + export: + statsd: + flavor: "etsy" +---- + + + +[[actuator.metrics.export.wavefront]] +==== Wavefront +Wavefront registry pushes metrics to {micrometer-registry-docs}/wavefront[Wavefront] periodically. +If you are exporting metrics to https://www.wavefront.com/[Wavefront] directly, your API token must be provided: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + export: + wavefront: + api-token: "YOUR_API_TOKEN" +---- + +Alternatively, you may use a Wavefront sidecar or an internal proxy set up in your environment that forwards metrics data to the Wavefront API host: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + export: + wavefront: + uri: "proxy://localhost:2878" +---- + +TIP: If publishing metrics to a Wavefront proxy (as described in https://docs.wavefront.com/proxies_installing.html[the documentation]), the host must be in the `proxy://HOST:PORT` format. + +You can also change the interval at which metrics are sent to Wavefront: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + export: + wavefront: + step: "30s" +---- + + + +[[actuator.metrics.supported]] +=== Supported Metrics and Meters +Spring Boot provides automatic meter registration for a wide variety of technologies. +In most situations, the out-of-the-box defaults will provide sensible metrics that can be published to any of the supported monioring systems. + + + +[[actuator.metrics.supported.jvm]] +==== JVM Metrics +Auto-configuration will enable JVM Metrics using core Micrometer classes. +JVM metrics are published under the `jvm.` meter name. + +The following JVM metrics are provided: + +* Various memory and buffer pool details +* Statistics related to garbage collection +* Threads utilization +* The Number of classes loaded/unloaded + + + +[[actuator.metrics.supported.system]] +==== System Metrics +Auto-configuration will enable system metrics using core Micrometer classes. +System metrics are published under the `system.` and `process.` meter names. + +The following system metrics are provided: + +* CPU metrics +* File descriptor metrics +* Uptime metrics (both the amount of time the application has been running as well as a fixed gauge of the absolute start time) + + + +[[actuator.metrics.supported.logger]] +==== Logger Metrics +Auto-configuration enables the event metrics for both Logback and Log4J2. +Details are published under the `log4j2.events.` or `logback.events.` meter names. + + + +[[actuator.metrics.supported.spring-mvc]] +==== Spring MVC Metrics +Auto-configuration enables the instrumentation of all requests handled by Spring MVC controllers and functional handlers. +By default, metrics are generated with the name, `http.server.requests`. +The name can be customized by setting the configprop:management.metrics.web.server.request.metric-name[] property. + +`@Timed` annotations are supported on `@Controller` classes and `@RequestMapping` methods (see <> for details). +If you don't want to record metrics for all Spring MVC requests, you can set configprop:management.metrics.web.server.request.autotime.enabled[] to `false` and exclusively use `@Timed` annotations instead. + +By default, Spring MVC related metrics are tagged with the following information: + +|=== +| Tag | Description + +| `exception` +| Simple class name of any exception that was thrown while handling the request. + +| `method` +| Request's method (for example, `GET` or `POST`) + +| `outcome` +| Request's outcome based on the status code of the response. + 1xx is `INFORMATIONAL`, 2xx is `SUCCESS`, 3xx is `REDIRECTION`, 4xx `CLIENT_ERROR`, and 5xx is `SERVER_ERROR` + +| `status` +| Response's HTTP status code (for example, `200` or `500`) + +| `uri` +| Request's URI template prior to variable substitution, if possible (for example, `/api/person/\{id}`) +|=== + +To add to the default tags, provide one or more ``@Bean``s that implement `WebMvcTagsContributor`. +To replace the default tags, provide a `@Bean` that implements `WebMvcTagsProvider`. + +TIP: In some cases, exceptions handled in Web controllers are not recorded as request metrics tags. +Applications can opt-in and record exceptions by <>. + + + +[[actuator.metrics.supported.spring-webflux]] +==== Spring WebFlux Metrics +Auto-configuration enables the instrumentation of all requests handled by Spring WebFlux controllers and functional handlers. +By default, metrics are generated with the name, `http.server.requests`. +The name can be customized by setting the configprop:management.metrics.web.server.request.metric-name[] property. + +`@Timed` annotations are supported on `@Controller` classes and `@RequestMapping` methods (see <> for details). +If you don't want to record metrics for all Spring WebFlux requests, you can set configprop:management.metrics.web.server.request.autotime.enabled[] to `false` and exclusively use `@Timed` annotations instead. + +By default, WebFlux related metrics are tagged with the following information: + +|=== +| Tag | Description + +| `exception` +| Simple class name of any exception that was thrown while handling the request. + +| `method` +| Request's method (for example, `GET` or `POST`) + +| `outcome` +| Request's outcome based on the status code of the response. + 1xx is `INFORMATIONAL`, 2xx is `SUCCESS`, 3xx is `REDIRECTION`, 4xx `CLIENT_ERROR`, and 5xx is `SERVER_ERROR` + +| `status` +| Response's HTTP status code (for example, `200` or `500`) + +| `uri` +| Request's URI template prior to variable substitution, if possible (for example, `/api/person/\{id}`) +|=== + +To add to the default tags, provide one or more ``@Bean``s that implement `WebFluxTagsContributor`. +To replace the default tags, provide a `@Bean` that implements `WebFluxTagsProvider`. + +TIP: In some cases, exceptions handled in controllers and handler functions are not recorded as request metrics tags. +Applications can opt-in and record exceptions by <>. + + + +[[actuator.metrics.supported.jersey]] +==== Jersey Server Metrics +Auto-configuration enables the instrumentation of all requests handled by the Jersey JAX-RS implementation whenever Micrometer's `micrometer-jersey2` module is on the classpath. +By default, metrics are generated with the name, `http.server.requests`. +The name can be customized by setting the configprop:management.metrics.web.server.request.metric-name[] property. + +`@Timed` annotations are supported on request-handling classes and methods (see <> for details). +If you don't want to record metrics for all Jersey requests, you can set configprop:management.metrics.web.server.request.autotime.enabled[] to `false` and exclusively use `@Timed` annotations instead. + +By default, Jersey server metrics are tagged with the following information: + +|=== +| Tag | Description + +| `exception` +| Simple class name of any exception that was thrown while handling the request. + +| `method` +| Request's method (for example, `GET` or `POST`) + +| `outcome` +| Request's outcome based on the status code of the response. + 1xx is `INFORMATIONAL`, 2xx is `SUCCESS`, 3xx is `REDIRECTION`, 4xx `CLIENT_ERROR`, and 5xx is `SERVER_ERROR` + +| `status` +| Response's HTTP status code (for example, `200` or `500`) + +| `uri` +| Request's URI template prior to variable substitution, if possible (for example, `/api/person/\{id}`) +|=== + +To customize the tags, provide a `@Bean` that implements `JerseyTagsProvider`. + + + +[[actuator.metrics.supported.http-clients]] +==== HTTP Client Metrics +Spring Boot Actuator manages the instrumentation of both `RestTemplate` and `WebClient`. +For that, you have to inject the auto-configured builder and use it to create instances: + +* `RestTemplateBuilder` for `RestTemplate` +* `WebClient.Builder` for `WebClient` + +It is also possible to apply manually the customizers responsible for this instrumentation, namely `MetricsRestTemplateCustomizer` and `MetricsWebClientCustomizer`. + +By default, metrics are generated with the name, `http.client.requests`. +The name can be customized by setting the configprop:management.metrics.web.client.request.metric-name[] property. + +By default, metrics generated by an instrumented client are tagged with the following information: + +|=== +| Tag | Description + +| `clientName` +| Host portion of the URI + +| `method` +| Request's method (for example, `GET` or `POST`) + +| `outcome` +| Request's outcome based on the status code of the response. + 1xx is `INFORMATIONAL`, 2xx is `SUCCESS`, 3xx is `REDIRECTION`, 4xx `CLIENT_ERROR`, and 5xx is `SERVER_ERROR`, `UNKNOWN` otherwise + +| `status` +| Response's HTTP status code if available (for example, `200` or `500`), or `IO_ERROR` in case of I/O issues, `CLIENT_ERROR` otherwise + +| `uri` +| Request's URI template prior to variable substitution, if possible (for example, `/api/person/\{id}`) +|=== + +To customize the tags, and depending on your choice of client, you can provide a `@Bean` that implements `RestTemplateExchangeTagsProvider` or `WebClientExchangeTagsProvider`. +There are convenience static functions in `RestTemplateExchangeTags` and `WebClientExchangeTags`. + + + +[[actuator.metrics.supported.tomcat]] +==== Tomcat Metrics +Auto-configuration will enable the instrumentation of Tomcat only when an `MBeanRegistry` is enabled. +By default, the `MBeanRegistry` is disabled, but you can enable it by setting configprop:server.tomcat.mbeanregistry.enabled[] to `true`. + +Tomcat metrics are published under the `tomcat.` meter name. + + + +[[actuator.metrics.supported.cache]] +==== Cache Metrics +Auto-configuration enables the instrumentation of all available ``Cache``s on startup with metrics prefixed with `cache`. +Cache instrumentation is standardized for a basic set of metrics. +Additional, cache-specific metrics are also available. + +The following cache libraries are supported: + +* Caffeine +* EhCache 2 +* Hazelcast +* Any compliant JCache (JSR-107) implementation +* Redis + +Metrics are tagged by the name of the cache and by the name of the `CacheManager` that is derived from the bean name. + +NOTE: Only caches that are configured on startup are bound to the registry. +For caches not defined in the cache’s configuration, e.g. caches created on-the-fly or programmatically after the startup phase, an explicit registration is required. +A `CacheMetricsRegistrar` bean is made available to make that process easier. + + + +[[actuator.metrics.supported.jdbc]] +==== DataSource Metrics +Auto-configuration enables the instrumentation of all available `DataSource` objects with metrics prefixed with `jdbc.connections`. +Data source instrumentation results in gauges representing the currently active, idle, maximum allowed, and minimum allowed connections in the pool. + +Metrics are also tagged by the name of the `DataSource` computed based on the bean name. + +TIP: By default, Spring Boot provides metadata for all supported data sources; you can add additional `DataSourcePoolMetadataProvider` beans if your favorite data source isn't supported out of the box. +See `DataSourcePoolMetadataProvidersConfiguration` for examples. + +Also, Hikari-specific metrics are exposed with a `hikaricp` prefix. +Each metric is tagged by the name of the Pool (can be controlled with `spring.datasource.name`). + + + +[[actuator.metrics.supported.hibernate]] +==== Hibernate Metrics +If `org.hibernate:hibernate-micrometer` is on the classpath, all available Hibernate `EntityManagerFactory` instances that have statistics enabled are instrumented with a metric named `hibernate`. + +Metrics are also tagged by the name of the `EntityManagerFactory` that is derived from the bean name. + +To enable statistics, the standard JPA property `hibernate.generate_statistics` must be set to `true`. +You can enable that on the auto-configured `EntityManagerFactory` as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + jpa: + properties: + "[hibernate.generate_statistics]": true +---- + + + +[[actuator.metrics.supported.spring-data-repository]] +==== Spring Data Repository Metrics +Auto-configuration enables the instrumentation of all Spring Data `Repository` method invocations. +By default, metrics are generated with the name, `spring.data.repository.invocations`. +The name can be customized by setting the configprop:management.metrics.data.repository.metric-name[] property. + +`@Timed` annotations are supported on `Repository` classes and methods (see <> for details). +If you don't want to record metrics for all `Repository` invocations, you can set configprop:management.metrics.data.repository.autotime.enabled[] to `false` and exclusively use `@Timed` annotations instead. + +By default, repository invocation related metrics are tagged with the following information: + +|=== +| Tag | Description + +| `repository` +| Simple class name of the source `Repository`. + +| `method` +| The name of the `Repository` method that was invoked. + +| `state` +| The result state (`SUCCESS`, `ERROR`, `CANCELED` or `RUNNING`). + +| `exception` +| Simple class name of any exception that was thrown from the invocation. +|=== + +To replace the default tags, provide a `@Bean` that implements `RepositoryTagsProvider`. + + + +[[actuator.metrics.supported.rabbitmq]] +==== RabbitMQ Metrics +Auto-configuration will enable the instrumentation of all available RabbitMQ connection factories with a metric named `rabbitmq`. + + + +[[actuator.metrics.supported.spring-integration]] +==== Spring Integration Metrics +Spring Integration provides {spring-integration-docs}system-management.html#micrometer-integration[Micrometer support] automatically whenever a `MeterRegistry` bean is available. +Metrics are published under the `spring.integration.` meter name. + + + +[[actuator.metrics.supported.kafka]] +==== Kafka Metrics +Auto-configuration will register a `MicrometerConsumerListener` and `MicrometerProducerListener` for the auto-configured consumer factory and producer factory respectively. +It will also register a `KafkaStreamsMicrometerListener` for `StreamsBuilderFactoryBean`. +For more details refer to {spring-kafka-docs}#micrometer-native[Micrometer Native Metrics] section of the Spring Kafka documentation. + + + +[[actuator.metrics.supported.mongodb]] +==== MongoDB Metrics + + + +[[actuator.metrics.supported.mongodb.command]] +===== Command Metrics +Auto-configuration will register a `MongoMetricsCommandListener` with the auto-configured `MongoClient`. + +A timer metric with the name `mongodb.driver.commands` is created for each command issued to the underlying MongoDB driver. +Each metric is tagged with the following information by default: +|=== +| Tag | Description + +| `command` +| Name of the command issued + +| `cluster.id` +| Identifier of the cluster the command was sent to + +| `server.address` +| Address of the server the command was sent to + +| `status` +| Outcome of the command - one of (`SUCCESS`, `FAILED`) +|=== + +To replace the default metric tags, define a `MongoCommandTagsProvider` bean, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/actuator/metrics/supported/mongodb/command/MyCommandTagsProviderConfiguration.java[] +---- + +To disable the auto-configured command metrics, set the following property: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + mongo: + command: + enabled: false +---- + + + +[[actuator.metrics.supported.mongodb.connection-pool]] +===== Connection Pool Metrics +Auto-configuration will register a `MongoMetricsConnectionPoolListener` with the auto-configured `MongoClient`. + +The following gauge metrics are created for the connection pool: + +* `mongodb.driver.pool.size` that reports the current size of the connection pool, including idle and and in-use members +* `mongodb.driver.pool.checkedout` that reports the count of connections that are currently in use +* `mongodb.driver.pool.waitqueuesize` that reports the current size of the wait queue for a connection from the pool + +Each metric is tagged with the following information by default: +|=== +| Tag | Description + +| `cluster.id` +| Identifier of the cluster the connection pool corresponds to + +| `server.address` +| Address of the server the connection pool corresponds to +|=== + +To replace the default metric tags, define a `MongoConnectionPoolTagsProvider` bean, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/actuator/metrics/supported/mongodb/connectionpool/MyConnectionPoolTagsProviderConfiguration.java[] +---- + +To disable the auto-configured connection pool metrics, set the following property: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + mongo: + connectionpool: + enabled: false +---- + + + +[[actuator.metrics.supported.jetty]] +==== Jetty Metrics +Auto-configuration will bind metrics for Jetty's `ThreadPool` using Micrometer's `JettyServerThreadPoolMetrics`. + + + +[[actuator.metrics.supported.timed-annotation]] +==== @Timed Annotation Support +The `@Timed` annotation from the `io.micrometer.core.annotation` package can be used with several of the supported technologies listed above. +If supported, the annotation can be used either at the class-level or the method-level. + +For example, the following code shows how the annotation can be used to instrument all request mappings in a `@RestController`: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/actuator/metrics/supported/timedannotation/all/MyController.java[] +---- + +If you only want to instrument a single mapping, you can use the annotation on the method instead of the class: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/actuator/metrics/supported/timedannotation/single/MyController.java[] +---- + +You can also combine class-level and method-level annotations if you want to change timing details for a specific method: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/actuator/metrics/supported/timedannotation/change/MyController.java[] +---- + +NOTE: A `@Timed` annotation with `longTask = true` will enable a long task timer for the method. +Long task timers require a separate metric name, and can be stacked with a short task timer. + + + +[[actuator.metrics.registering-custom]] +=== Registering Custom Metrics +To register custom metrics, inject `MeterRegistry` into your component, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/actuator/metrics/registeringcustom/MyBean.java[] +---- + +If your metrics depend on other beans, it is recommended that you use a `MeterBinder` to register them, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/actuator/metrics/registeringcustom/MyMeterBinderConfiguration.java[] +---- + +Using a `MeterBinder` ensures that the correct dependency relationships are set up and that the bean is available when the metric's value is retrieved. +A `MeterBinder` implementation can also be useful if you find that you repeatedly instrument a suite of metrics across components or applications. + +NOTE: By default, metrics from all `MeterBinder` beans will be automatically bound to the Spring-managed `MeterRegistry`. + + + +[[actuator.metrics.customizing]] +=== Customizing Individual Metrics +If you need to apply customizations to specific `Meter` instances you can use the `io.micrometer.core.instrument.config.MeterFilter` interface. + +For example, if you want to rename the `mytag.region` tag to `mytag.area` for all meter IDs beginning with `com.example`, you can do the following: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/actuator/metrics/customizing/MyMetricsFilterConfiguration.java[] +---- + +NOTE: By default, all `MeterFilter` beans will be automatically bound to the Spring-managed `MeterRegistry`. +Make sure to register your metrics using the Spring-managed `MeterRegistry` and not any of the static methods on `Metrics`. +These use the global registry that is not Spring-managed. + + + +[[actuator.metrics.customizing.common-tags]] +==== Common Tags +Common tags are generally used for dimensional drill-down on the operating environment like host, instance, region, stack, etc. +Commons tags are applied to all meters and can be configured as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + tags: + region: "us-east-1" + stack: "prod" +---- + +The example above adds `region` and `stack` tags to all meters with a value of `us-east-1` and `prod` respectively. + +NOTE: The order of common tags is important if you are using Graphite. +As the order of common tags cannot be guaranteed using this approach, Graphite users are advised to define a custom `MeterFilter` instead. + + + +[[actuator.metrics.customizing.per-meter-properties]] +==== Per-meter Properties +In addition to `MeterFilter` beans, it's also possible to apply a limited set of customization on a per-meter basis using properties. +Per-meter customizations apply to any all meter IDs that start with the given name. +For example, the following will disable any meters that have an ID starting with `example.remote` + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + enable: + example: + remote: false +---- + +The following properties allow per-meter customization: + +.Per-meter customizations +|=== +| Property | Description + +| configprop:management.metrics.enable[] +| Whether to deny meters from emitting any metrics. + +| configprop:management.metrics.distribution.percentiles-histogram[] +| Whether to publish a histogram suitable for computing aggregable (across dimension) percentile approximations. + +| configprop:management.metrics.distribution.minimum-expected-value[], configprop:management.metrics.distribution.maximum-expected-value[] +| Publish less histogram buckets by clamping the range of expected values. + +| configprop:management.metrics.distribution.percentiles[] +| Publish percentile values computed in your application + +| configprop:management.metrics.distribution.slo[] +| Publish a cumulative histogram with buckets defined by your service-level objectives. +|=== + +For more details on concepts behind `percentiles-histogram`, `percentiles` and `slo` refer to the {micrometer-concepts-docs}#_histograms_and_percentiles["Histograms and percentiles" section] of the micrometer documentation. + + + +[[actuator.metrics.endpoint]] +=== Metrics Endpoint +Spring Boot provides a `metrics` endpoint that can be used diagnostically to examine the metrics collected by an application. +The endpoint is not available by default and must be exposed, see <> for more details. + +Navigating to `/actuator/metrics` displays a list of available meter names. +You can drill down to view information about a particular meter by providing its name as a selector, e.g. `/actuator/metrics/jvm.memory.max`. + +[TIP] +==== +The name you use here should match the name used in the code, not the name after it has been naming-convention normalized for a monitoring system it is shipped to. +In other words, if `jvm.memory.max` appears as `jvm_memory_max` in Prometheus because of its snake case naming convention, you should still use `jvm.memory.max` as the selector when inspecting the meter in the `metrics` endpoint. +==== + +You can also add any number of `tag=KEY:VALUE` query parameters to the end of the URL to dimensionally drill down on a meter, e.g. `/actuator/metrics/jvm.memory.max?tag=area:nonheap`. + +[TIP] +==== +The reported measurements are the _sum_ of the statistics of all meters matching the meter name and any tags that have been applied. +So in the example above, the returned "Value" statistic is the sum of the maximum memory footprints of "Code Cache", "Compressed Class Space", and "Metaspace" areas of the heap. +If you only wanted to see the maximum size for the "Metaspace", you could add an additional `tag=id:Metaspace`, i.e. `/actuator/metrics/jvm.memory.max?tag=area:nonheap&tag=id:Metaspace`. +==== diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/monitoring.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/monitoring.adoc new file mode 100644 index 000000000000..3e1e5c4824b5 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/monitoring.adoc @@ -0,0 +1,148 @@ +[[actuator.monitoring]] +== Monitoring and Management over HTTP +If you are developing a web application, Spring Boot Actuator auto-configures all enabled endpoints to be exposed over HTTP. +The default convention is to use the `id` of the endpoint with a prefix of `/actuator` as the URL path. +For example, `health` is exposed as `/actuator/health`. + +TIP: Actuator is supported natively with Spring MVC, Spring WebFlux, and Jersey. +If both Jersey and Spring MVC are available, Spring MVC will be used. + +NOTE: Jackson is a required dependency in order to get the correct JSON responses as documented in the API documentation ({spring-boot-actuator-restapi-docs}[HTML] or {spring-boot-actuator-restapi-pdfdocs}[PDF]). + + + +[[actuator.monitoring.customizing-management-server-context-path]] +=== Customizing the Management Endpoint Paths +Sometimes, it is useful to customize the prefix for the management endpoints. +For example, your application might already use `/actuator` for another purpose. +You can use the configprop:management.endpoints.web.base-path[] property to change the prefix for your management endpoint, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + endpoints: + web: + base-path: "/manage" +---- + +The preceding `application.properties` example changes the endpoint from `/actuator/\{id}` to `/manage/\{id}` (for example, `/manage/info`). + +NOTE: Unless the management port has been configured to <>, `management.endpoints.web.base-path` is relative to `server.servlet.context-path` (Servlet web applications) or `spring.webflux.base-path` (reactive web applications). +If `management.server.port` is configured, `management.endpoints.web.base-path` is relative to `management.server.base-path`. + +If you want to map endpoints to a different path, you can use the configprop:management.endpoints.web.path-mapping[] property. + +The following example remaps `/actuator/health` to `/healthcheck`: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + endpoints: + web: + base-path: "/" + path-mapping: + health: "healthcheck" +---- + + + +[[actuator.monitoring.customizing-management-server-port]] +=== Customizing the Management Server Port +Exposing management endpoints by using the default HTTP port is a sensible choice for cloud-based deployments. +If, however, your application runs inside your own data center, you may prefer to expose endpoints by using a different HTTP port. + +You can set the configprop:management.server.port[] property to change the HTTP port, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + server: + port: 8081 +---- + +NOTE: On Cloud Foundry, applications only receive requests on port 8080 for both HTTP and TCP routing, by default. +If you want to use a custom management port on Cloud Foundry, you will need to explicitly set up the application's routes to forward traffic to the custom port. + + + +[[actuator.monitoring.management-specific-ssl]] +=== Configuring Management-specific SSL +When configured to use a custom port, the management server can also be configured with its own SSL by using the various `management.server.ssl.*` properties. +For example, doing so lets a management server be available over HTTP while the main application uses HTTPS, as shown in the following property settings: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + server: + port: 8443 + ssl: + enabled: true + key-store: "classpath:store.jks" + key-password: secret + management: + server: + port: 8080 + ssl: + enabled: false +---- + +Alternatively, both the main server and the management server can use SSL but with different key stores, as follows: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + server: + port: 8443 + ssl: + enabled: true + key-store: "classpath:main.jks" + key-password: "secret" + management: + server: + port: 8080 + ssl: + enabled: true + key-store: "classpath:management.jks" + key-password: "secret" +---- + + + +[[actuator.monitoring.customizing-management-server-address]] +=== Customizing the Management Server Address +You can customize the address that the management endpoints are available on by setting the configprop:management.server.address[] property. +Doing so can be useful if you want to listen only on an internal or ops-facing network or to listen only for connections from `localhost`. + +NOTE: You can listen on a different address only when the port differs from the main server port. + +The following example `application.properties` does not allow remote management connections: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + server: + port: 8081 + address: "127.0.0.1" +---- + + + +[[actuator.monitoring.disabling-http-endpoints]] +=== Disabling HTTP Endpoints +If you do not want to expose endpoints over HTTP, you can set the management port to `-1`, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + server: + port: -1 +---- + +This can be achieved using the configprop:management.endpoints.web.exposure.exclude[] property as well, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + endpoints: + web: + exposure: + exclude: "*" +---- diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/process-monitoring.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/process-monitoring.adoc new file mode 100644 index 000000000000..365d3c185357 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/process-monitoring.adoc @@ -0,0 +1,31 @@ +[[actuator.process-monitoring]] +== Process Monitoring +In the `spring-boot` module, you can find two classes to create files that are often useful for process monitoring: + +* `ApplicationPidFileWriter` creates a file containing the application PID (by default, in the application directory with a file name of `application.pid`). +* `WebServerPortFileWriter` creates a file (or files) containing the ports of the running web server (by default, in the application directory with a file name of `application.port`). + +By default, these writers are not activated, but you can enable: + +* <> +* <> + + + +[[actuator.process-monitoring.configuration]] +=== Extending Configuration +In the `META-INF/spring.factories` file, you can activate the listener(s) that writes a PID file, as shown in the following example: + +[indent=0] +---- + org.springframework.context.ApplicationListener=\ + org.springframework.boot.context.ApplicationPidFileWriter,\ + org.springframework.boot.web.context.WebServerPortFileWriter +---- + + + +[[actuator.process-monitoring.programmatically]] +=== Programmatically +You can also activate a listener by invoking the `SpringApplication.addListeners(...)` method and passing the appropriate `Writer` object. +This method also lets you customize the file name and path in the `Writer` constructor. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/tracing.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/tracing.adoc new file mode 100644 index 000000000000..df0adbbdd9fd --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/tracing.adoc @@ -0,0 +1,16 @@ +[[actuator.tracing]] +== HTTP Tracing +HTTP Tracing can be enabled by providing a bean of type `HttpTraceRepository` in your application's configuration. +For convenience, Spring Boot offers an `InMemoryHttpTraceRepository` that stores traces for the last 100 request-response exchanges, by default. +`InMemoryHttpTraceRepository` is limited compared to other tracing solutions and we recommend using it only for development environments. +For production environments, use of a production-ready tracing or observability solution, such as Zipkin or Spring Cloud Sleuth, is recommended. +Alternatively, create your own `HttpTraceRepository` that meets your needs. + +The `httptrace` endpoint can be used to obtain information about the request-response exchanges that are stored in the `HttpTraceRepository`. + + + +[[actuator.tracing.custom]] +=== Custom HTTP tracing +To customize the items that are included in each trace, use the configprop:management.trace.http.include[] configuration property. +For advanced customization, consider registering your own `HttpExchangeTracer` implementation. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/whats-next.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/whats-next.adoc new file mode 100644 index 000000000000..652c0d147a2e --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/whats-next.adoc @@ -0,0 +1,5 @@ +[[actuator.whats-next]] +== What to Read Next +You might want to read about graphing tools such as https://graphiteapp.org[Graphite]. + +Otherwise, you can continue on, to read about <> or jump ahead for some in-depth information about Spring Boot's _<>_. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/anchor-rewrite.properties b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/anchor-rewrite.properties new file mode 100644 index 000000000000..589cc04cb84b --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/anchor-rewrite.properties @@ -0,0 +1,849 @@ +# Spring Boot 2.x - 2.4 Migrations +#--------------------------------------------------- +spring-boot-reference-documentation=index +legal=legal +boot-documentation=documentation +boot-documentation-about=documentation.about +boot-documentation-getting-help=documentation.getting-help +boot-documentation-upgrading=documentation.upgrading +boot-documentation-first-steps=documentation.first-steps +boot-documentation-workingwith=documentation.using +boot-documentation-learning=documentation.features +boot-documentation-production=documentation.actuator +boot-documentation-advanced=documentation.advanced +getting-started=getting-started +getting-started-introducing-spring-boot=getting-started.introducing-spring-boot +getting-started-system-requirements=getting-started.system-requirements +getting-started-system-requirements-servlet-containers=getting-started.system-requirements.servlet-containers +getting-started-installing-spring-boot=getting-started.installing +getting-started-installation-instructions-for-java=getting-started.installing.java +getting-started-maven-installation=getting-started.installing.java.maven +getting-started-gradle-installation=getting-started.installing.java.gradle +getting-started-installing-the-cli=getting-started.installing.cli +getting-started-manual-cli-installation=getting-started.installing.cli.manual-installation +getting-started-sdkman-cli-installation=getting-started.installing.cli.sdkman +getting-started-homebrew-cli-installation=getting-started.installing.cli.homebrew +getting-started-macports-cli-installation=getting-started.installing.cli.macports +getting-started-cli-command-line-completion=getting-started.installing.cli.completion +getting-started-scoop-cli-installation=getting-started.installing.cli.scoop +getting-started-cli-example=getting-started.installing.cli.quick-start +getting-started-upgrading-from-an-earlier-version=getting-started.installing.upgrading +getting-started-first-application=getting-started.first-application +getting-started-first-application-pom=getting-started.first-application.pom +getting-started-first-application-dependencies=getting-started.first-application.dependencies +getting-started-first-application-code=getting-started.first-application.code +getting-started-first-application-annotations=getting-started.first-application.code.mvc-annotations +getting-started-first-application-auto-configuration=getting-started.first-application.code.enable-auto-configuration +getting-started-first-application-main-method=getting-started.first-application.code.main-method +getting-started-first-application-run=getting-started.first-application.run +getting-started-first-application-executable-jar=getting-started.first-application.executable-jar +getting-started-whats-next=getting-started.whats-next +using-boot=using +using-boot-build-systems=using.build-systems +using-boot-dependency-management=using.build-systems.dependency-management +using-boot-maven=using.build-systems.maven +using-boot-gradle=using.build-systems.gradle +using-boot-ant=using.build-systems.ant +using-boot-starter=using.build-systems.starters +using-boot-structuring-your-code=using.structuring-your-code +using-boot-using-the-default-package=using.structuring-your-code.using-the-default-package +using-boot-locating-the-main-class=using.structuring-your-code.locating-the-main-class +using-boot-configuration-classes=using.configuration-classes +using-boot-importing-configuration=using.configuration-classes.importing-additional-configuration +using-boot-importing-xml-configuration=using.configuration-classes.importing-xml-configuration +using-boot-auto-configuration=using.auto-configuration +using-boot-replacing-auto-configuration=using.auto-configuration.replacing +using-boot-disabling-specific-auto-configuration=using.auto-configuration.disabling-specific +using-boot-spring-beans-and-dependency-injection=using.spring-beans-and-dependency-injection +using-boot-using-springbootapplication-annotation=using.using-the-springbootapplication-annotation +using-boot-running-your-application=using.running-your-application +using-boot-running-from-an-ide=using.running-your-application.from-an-ide +using-boot-running-as-a-packaged-application=using.running-your-application.as-a-packaged-application +using-boot-running-with-the-maven-plugin=using.running-your-application.with-the-maven-plugin +using-boot-running-with-the-gradle-plugin=using.running-your-application.with-the-gradle-plugin +using-boot-hot-swapping=using.running-your-application.hot-swapping +using-boot-devtools=using.devtools +using-boot-devtools-property-defaults=using.devtools.property-defaults +using-boot-devtools-restart=using.devtools.restart +using-boot-devtools-restart-logging-condition-delta=using.devtools.restart.logging-condition-delta +using-boot-devtools-restart-exclude=using.devtools.restart.excluding-resources +using-boot-devtools-restart-additional-paths=using.devtools.restart.watching-additional-paths +using-boot-devtools-restart-disable=using.devtools.restart.disable +using-boot-devtools-restart-triggerfile=using.devtools.restart.triggerfile +using-boot-devtools-customizing-classload=using.devtools.restart.customizing-the-classload +using-boot-devtools-known-restart-limitations=using.devtools.restart.limitations +using-boot-devtools-livereload=using.devtools.livereload +using-boot-devtools-globalsettings=using.devtools.globalsettings +configuring-file-system-watcher=using.devtools.globalsettings.configuring-file-system-watcher +using-boot-devtools-remote=using.devtools.remote-applications +running-remote-client-application=using.devtools.remote-applications.client +using-boot-devtools-remote-update=using.devtools.remote-applications.update +using-boot-packaging-for-production=using.packaging-for-production +using-boot-whats-next=using.whats-next +boot-features=features +boot-features-spring-application=features.spring-application +boot-features-startup-failure=features.spring-application.startup-failure +boot-features-lazy-initialization=features.spring-application.lazy-initialization +boot-features-banner=features.spring-application.banner +boot-features-customizing-spring-application=features.spring-application.customizing-spring-application +boot-features-fluent-builder-api=features.spring-application.fluent-builder-api +boot-features-application-availability=features.spring-application.application-availability +boot-features-application-availability-liveness-state=features.spring-application.application-availability.liveness +boot-features-application-availability-readiness-state=features.spring-application.application-availability.readiness +boot-features-application-availability-managing=features.spring-application.application-availability.managing +boot-features-application-events-and-listeners=features.spring-application.application-events-and-listeners +boot-features-web-environment=features.spring-application.web-environment +boot-features-application-arguments=features.spring-application.application-arguments +boot-features-command-line-runner=features.spring-application.command-line-runner +boot-features-application-exit=features.spring-application.application-exit +boot-features-application-admin=features.spring-application.admin +boot-features-application-startup-tracking=features.spring-application.startup-tracking +boot-features-external-config=features.external-config +boot-features-external-config-command-line-args=features.external-config.command-line-args +boot-features-external-config-application-json=features.external-config.application-json +boot-features-external-config-files=features.external-config.files +boot-features-external-config-application-property-files=features.external-config.files +boot-features-external-config-optional-prefix=features.external-config.files.optional-prefix +boot-features-external-config-files-wildcards=features.external-config.files.wildcard-locations +boot-features-external-config-files-profile-specific=features.external-config.files.profile-specific +boot-features-external-config-files-importing=features.external-config.files.importing +boot-features-external-config-files-importing-extensionless=features.external-config.file.importing-extensionless +boot-features-external-config-files-configtree=features.external-config.files.configtree +boot-features-external-config-placeholders-in-properties=features.external-config.files.property-placeholders +boot-features-external-config-files-multi-document=features.external-config.files.multi-document +boot-features-external-config-file-activation-properties=features.external-config.files.activation-properties +boot-features-encrypting-properties=features.external-config.encrypting +boot-features-external-config-yaml=features.external-config.yaml +boot-features-external-config-yaml-to-properties=features.external-config.yaml.mapping-to-properties +boot-features-external-config-exposing-yaml-to-spring=features.external-config.yaml.directly-loading +boot-features-external-config-loading-yaml=features.external-config.yaml.directly-loading +boot-features-external-config-random-values=features.external-config.random-values +boot-features-external-config-system-environment=features.external-config.system-environment +boot-features-external-config-typesafe-configuration-properties=features.external-config.typesafe-configuration-properties +boot-features-external-config-java-bean-binding=features.external-config.typesafe-configuration-properties.java-bean-binding +boot-features-external-config-constructor-binding=features.external-config.typesafe-configuration-properties.constructor-binding +boot-features-external-config-enabling=features.external-config.typesafe-configuration-properties.enabling-annotated-types +boot-features-external-config-using=features.external-config.typesafe-configuration-properties.using-annotated-types +boot-features-external-config-3rd-party-configuration=features.external-config.typesafe-configuration-properties.third-party-configuration +boot-features-external-config-relaxed-binding=features.external-config.typesafe-configuration-properties.relaxed-binding +boot-features-external-config-relaxed-binding-maps=features.external-config.typesafe-configuration-properties.relaxed-binding.maps +boot-features-external-config-relaxed-binding-from-environment-variables=features.external-config.typesafe-configuration-properties.relaxed-binding.environment-variables +boot-features-external-config-complex-type-merge=features.external-config.typesafe-configuration-properties.merging-complex-types +boot-features-external-config-conversion=features.external-config.typesafe-configuration-properties.conversion +boot-features-external-config-conversion-duration=features.external-config.typesafe-configuration-properties.conversion.durations +boot-features-external-config-conversion-period=features.external-config.typesafe-configuration-properties.conversion.periods +boot-features-external-config-conversion-datasize=features.external-config.typesafe-configuration-properties.conversion.data-sizes +boot-features-external-config-validation=features.external-config.typesafe-configuration-properties.validation +boot-features-external-config-vs-value=features.external-config.typesafe-configuration-properties.vs-value-annotation +boot-features-profiles=features.profiles +boot-features-adding-active-profiles=features.profiles.adding-active-profiles +boot-features-profiles-groups=features.profiles.groups +boot-features-programmatically-setting-profiles=features.profiles.programmatically-setting-profiles +boot-features-profile-specific-configuration=features.profiles.profile-specific-configuration-files +boot-features-logging=features.logging +boot-features-logging-format=features.logging.log-format +boot-features-logging-console-output=features.logging.console-output +boot-features-logging-color-coded-output=features.logging.console-output.color-coded +boot-features-logging-file-output=features.logging.file-output +boot-features-logging-file-rotation=features.logging.file-rotation +boot-features-custom-log-levels=features.logging.log-levels +boot-features-custom-log-groups=features.logging.log-groups +boot-features-log-shutdown-hook=features.logging.shutdown-hook +boot-features-custom-log-configuration=features.logging.custom-log-configuration +boot-features-logback-extensions=features.logging.logback-extensions +boot-features-logback-extensions-profile-specific=features.logging.logback-extensions.profile-specific +boot-features-logback-environment-properties=features.logging.logback-extensions.environment-properties +boot-features-internationalization=features.internationalization +boot-features-json=features.json +boot-features-json-jackson=features.json.jackson +boot-features-json-gson=features.json.gson +boot-features-json-json-b=features.json.json-b +boot-features-developing-web-applications=features.developing-web-applications +boot-features-spring-mvc=features.developing-web-applications.spring-mvc +boot-features-spring-mvc-auto-configuration=features.developing-web-applications.spring-mvc.auto-configuration +boot-features-spring-mvc-message-converters=features.developing-web-applications.spring-mvc.message-converters +boot-features-json-components=features.developing-web-applications.spring-mvc.json +boot-features-spring-message-codes=features.developing-web-applications.spring-mvc.message-codes +boot-features-spring-mvc-static-content=features.developing-web-applications.spring-mvc.static-content +boot-features-spring-mvc-welcome-page=features.developing-web-applications.spring-mvc.welcome-page +boot-features-spring-mvc-pathmatch=features.developing-web-applications.spring-mvc.content-negotiation +boot-features-spring-mvc-web-binding-initializer=features.developing-web-applications.spring-mvc.binding-initializer +boot-features-spring-mvc-template-engines=features.developing-web-applications.spring-mvc.template-engines +boot-features-error-handling=features.developing-web-applications.spring-mvc.error-handling +boot-features-error-handling-custom-error-pages=features.developing-web-applications.spring-mvc.error-handling.error-pages +boot-features-error-handling-mapping-error-pages-without-mvc=features.developing-web-applications.spring-mvc.error-handling.error-pages-without-spring-mvc +boot-features-error-handling-war-deployment=features.developing-web-applications.spring-mvc.error-handling.in-a-war-deployment +boot-features-spring-hateoas=features.developing-web-applications.spring-mvc.spring-hateoas +boot-features-cors=features.developing-web-applications.spring-mvc.cors +boot-features-webflux=features.developing-web-applications.spring-webflux +boot-features-webflux-auto-configuration=features.developing-web-applications.spring-webflux.auto-configuration +boot-features-webflux-httpcodecs=features.developing-web-applications.spring-webflux.httpcodecs +boot-features-webflux-static-content=features.developing-web-applications.spring-webflux.static-content +boot-features-webflux-welcome-page=features.developing-web-applications.spring-webflux.welcome-page +boot-features-webflux-template-engines=features.developing-web-applications.spring-webflux.template-engines +boot-features-webflux-error-handling=features.developing-web-applications.spring-webflux.error-handling +boot-features-webflux-error-handling-custom-error-pages=features.developing-web-applications.spring-webflux.error-handling.error-pages +boot-features-webflux-web-filters=features.developing-web-applications.spring-webflux.web-filters +boot-features-jersey=features.developing-web-applications.jersey +boot-features-embedded-container=features.developing-web-applications.embedded-container +boot-features-embedded-container-servlets-filters-listeners=features.developing-web-applications.embedded-container.servlets-filters-listeners +boot-features-embedded-container-servlets-filters-listeners-beans=features.developing-web-applications.embedded-container.servlets-filters-listeners.beans +boot-features-embedded-container-context-initializer=features.developing-web-applications.embedded-container.context-initializer +boot-features-embedded-container-servlets-filters-listeners-scanning=features.developing-web-applications.embedded-container.context-initializer.scanning +boot-features-embedded-container-application-context=features.developing-web-applications.embedded-container.application-context +boot-features-customizing-embedded-containers=features.developing-web-applications.embedded-container.customizing +boot-features-programmatic-embedded-container-customization=features.developing-web-applications.embedded-container.customizing.programmatic +boot-features-customizing-configurableservletwebserverfactory-directly=features.developing-web-applications.embedded-container.customizing.direct +boot-features-jsp-limitations=features.developing-web-applications.embedded-container.jsp-limitations +boot-features-reactive-server=features.developing-web-applications.reactive-server +boot-features-reactive-server-resources=features.developing-web-applications.reactive-server-resources-configuration +boot-features-graceful-shutdown=features.graceful-shutdown +boot-features-rsocket=features.rsocket +boot-features-rsocket-strategies-auto-configuration=features.rsocket.strategies-auto-configuration +boot-features-rsocket-server-auto-configuration=features.rsocket.server-auto-configuration +boot-features-rsocket-messaging=features.rsocket.messaging +boot-features-rsocket-requester=features.rsocket.requester +boot-features-security=features.security +boot-features-security-mvc=features.security.spring-mvc +boot-features-security-webflux=features.security.spring-webflux +boot-features-security-oauth2=features.security.oauth2 +boot-features-security-oauth2-client=features.security.oauth2.client +boot-features-security-oauth2-common-providers=features.security.oauth2.client.common-providers +boot-features-security-oauth2-server=features.security.oauth2.server +boot-features-security-authorization-server=features.security.oauth2.authorization-server +boot-features-security-saml=features.security.saml2 +boot-features-security-saml2-relying-party=features.security.saml2.relying-party +boot-features-security-actuator=features.security.actuator +boot-features-security-csrf=features.security.actuator.csrf +boot-features-sql=features.sql +boot-features-configure-datasource=features.sql.datasource +boot-features-embedded-database-support=features.sql.datasource.embedded +boot-features-connect-to-production-database=features.sql.datasource.production +boot-features-connect-to-production-database-configuration=features.sql.datasource.configuration +boot-features-connect-to-production-database-connection-pool=features.sql.datasource.connection-pool +boot-features-connecting-to-a-jndi-datasource=features.sql.datasource.jndi +boot-features-using-jdbc-template=features.sql.jdbc-template +boot-features-jpa-and-spring-data=features.sql.jpa-and-spring-data +boot-features-entity-classes=features.sql.jpa-and-spring-data.entity-classes +boot-features-spring-data-jpa-repositories=features.sql.jpa-and-spring-data.repositories +boot-features-creating-and-dropping-jpa-databases=features.sql.jpa-and-spring-data.creating-and-dropping +boot-features-jpa-in-web-environment=features.sql.jpa-and-spring-data.open-entity-manager-in-view +boot-features-data-jdbc=features.sql.jdbc +boot-features-sql-h2-console=features.sql.h2-web-console +boot-features-sql-h2-console-custom-path=features.sql.h2-web-console.custom-path +boot-features-jooq=features.sql.jooq +boot-features-jooq-codegen=features.sql.jooq.codegen +boot-features-jooq-dslcontext=features.sql.jooq.dslcontext +boot-features-jooq-sqldialect=features.sql.jooq.sqldialect +boot-features-jooq-customizing=features.sql.jooq.customizing +boot-features-r2dbc=features.sql.r2dbc +boot-features-r2dbc-embedded-database=features.sql.r2dbc.embedded +boot-features-r2dbc-using-database-client=features.sql.r2dbc.using-database-client +boot-features-spring-data-r2dbc-repositories=features.sql.r2dbc.repositories +boot-features-nosql=features.nosql +boot-features-redis=features.nosql.redis +boot-features-connecting-to-redis=features.nosql.redis.connecting +boot-features-mongodb=features.nosql.mongodb +boot-features-connecting-to-mongodb=features.nosql.mongodb.connecting +boot-features-mongo-template=features.nosql.mongodb.template +boot-features-spring-data-mongodb-repositories=features.nosql.mongodb.repositories +boot-features-spring-data-mongo-repositories=features.nosql.mongodb.repositories +boot-features-mongo-embedded=features.nosql.mongodb.embedded +boot-features-neo4j=features.nosql.neo4j +boot-features-connecting-to-neo4j=features.nosql.neo4j.connecting +boot-features-spring-data-neo4j-repositories=features.nosql.neo4j.repositories +boot-features-solr=features.nosql.solr +boot-features-connecting-to-solr=features.nosql.solr.connecting +boot-features-elasticsearch=features.nosql.elasticsearch +boot-features-connecting-to-elasticsearch-rest=features.nosql.elasticsearch.connecting-using-rest +boot-features-connecting-to-elasticsearch-reactive-rest=features.nosql.elasticsearch.connecting-using-reactive-rest +boot-features-connecting-to-elasticsearch-spring-data=features.nosql.elasticsearch.connecting-using-spring-data +boot-features-spring-data-elasticsearch-repositories=features.nosql.elasticsearch.repositories +boot-features-cassandra=features.nosql.cassandra +boot-features-connecting-to-cassandra=features.nosql.cassandra.connecting +boot-features-spring-data-cassandra-repositories=features.nosql.cassandra.repositories +boot-features-couchbase=features.nosql.couchbase +boot-features-connecting-to-couchbase=features.nosql.couchbase.connecting +boot-features-spring-data-couchbase-repositories=features.nosql.couchbase.repositories +boot-features-ldap=features.nosql.ldap +boot-features-ldap-connecting=features.nosql.ldap.connecting +boot-features-ldap-spring-data-repositories=features.nosql.ldap.repositories +boot-features-ldap-embedded=features.nosql.ldap.embedded +boot-features-influxdb=features.nosql.influxdb +boot-features-connecting-to-influxdb=features.nosql.influxdb.connecting +boot-features-caching=features.caching +boot-features-caching-provider=features.caching.provider +boot-features-caching-provider-generic=features.caching.provider.generic +boot-features-caching-provider-jcache=features.caching.provider.jcache +boot-features-caching-provider-ehcache2=features.caching.provider.ehcache2 +boot-features-caching-provider-hazelcast=features.caching.provider.hazelcast +boot-features-caching-provider-infinispan=features.caching.provider.infinispan +boot-features-caching-provider-couchbase=features.caching.provider.couchbase +boot-features-caching-provider-redis=features.caching.provider.redis +boot-features-caching-provider-caffeine=features.caching.provider.caffeine +boot-features-caching-provider-simple=features.caching.provider.simple +boot-features-caching-provider-none=features.caching.provider.none +boot-features-messaging=features.messaging +boot-features-jms=features.messaging.jms +boot-features-activemq=features.messaging.jms.activemq +boot-features-artemis=features.messaging.jms.artemis +boot-features-jms-jndi=features.messaging.jms.jndi +boot-features-using-jms-sending=features.messaging.jms.sending +boot-features-using-jms-receiving=features.messaging.jms.receiving +boot-features-amqp=features.messaging.amqp +boot-features-rabbitmq=features.messaging.amqp.rabbitmq +boot-features-using-amqp-sending=features.messaging.amqp.sending +boot-features-using-amqp-receiving=features.messaging.amqp.receiving +boot-features-kafka=features.messaging.kafka +boot-features-kafka-sending-a-message=features.messaging.kafka.sending +boot-features-kafka-receiving-a-message=features.messaging.kafka.receiving +boot-features-kafka-streams=features.messaging.kafka.streams +boot-features-kafka-extra-props=features.messaging.kafka.additional-properties +boot-features-embedded-kafka=features.messaging.kafka.embedded +boot-features-resttemplate=features.resttemplate +boot-features-resttemplate-customization=features.resttemplate.customization +boot-features-webclient=features.webclient +boot-features-webclient-runtime=features.webclient.runtime +boot-features-webclient-customization=features.webclient.customization +boot-features-validation=features.validation +boot-features-email=features.email +boot-features-jta=features.jta +boot-features-jta-atomikos=features.jta.atomikos +boot-features-jta-javaee=features.jta.javaee +boot-features-jta-mixed-jms=features.jta.mixing-xa-and-non-xa-connections +boot-features-jta-supporting-alternative-embedded=features.jta.supporting-alternative-embedded-transaction-manager +boot-features-hazelcast=features.hazelcast +boot-features-quartz=features.quartz +boot-features-task-execution-scheduling=features.task-execution-and-scheduling +boot-features-integration=features.spring-integration +boot-features-session=features.spring-session +boot-features-jmx=features.jmx +boot-features-testing=features.testing +boot-features-test-scope-dependencies=features.testing.test-scope-dependencies +boot-features-testing-spring-applications=features.testing.spring-applications +boot-features-testing-spring-boot-applications=features.testing.spring-boot-applications +boot-features-testing-spring-boot-applications-detecting-web-app-type=features.testing.spring-boot-applications.detecting-web-app-type +boot-features-testing-spring-boot-applications-detecting-config=features.testing.spring-boot-applications.detecting-configuration +boot-features-testing-spring-boot-applications-excluding-config=features.testing.spring-boot-applications.excluding-configuration +boot-features-testing-spring-boot-application-arguments=features.testing.spring-boot-applications.using-application-arguments +boot-features-testing-spring-boot-applications-testing-with-mock-environment=features.testing.spring-boot-applications.with-mock-environment +boot-features-testing-spring-boot-applications-testing-with-running-server=features.testing.spring-boot-applications.with-running-server +boot-features-testing-spring-boot-applications-customizing-web-test-client=features.testing.spring-boot-applications.customizing-web-test-client +boot-features-testing-spring-boot-applications-jmx=features.testing.spring-boot-applications.jmx +boot-features-testing-spring-boot-applications-metrics=features.testing.spring-boot-applications.metrics +boot-features-testing-spring-boot-applications-mocking-beans=features.testing.spring-boot-applications.mocking-beans +boot-features-testing-spring-boot-applications-testing-autoconfigured-tests=features.testing.spring-boot-applications.autoconfigured-tests +boot-features-testing-spring-boot-applications-testing-autoconfigured-json-tests=features.testing.spring-boot-applications.json-tests +boot-features-testing-spring-boot-applications-testing-autoconfigured-mvc-tests=features.testing.spring-boot-applications.spring-mvc-tests +boot-features-testing-spring-boot-applications-testing-autoconfigured-webflux-tests=features.testing.spring-boot-applications.spring-webflux-tests +boot-features-testing-spring-boot-applications-testing-autoconfigured-cassandra-test=features.testing.spring-boot-applications.autoconfigured-spring-data-cassandra +boot-features-testing-spring-boot-applications-testing-autoconfigured-jpa-test=features.testing.spring-boot-applications.autoconfigured-spring-data-jpa +boot-features-testing-spring-boot-applications-testing-autoconfigured-jdbc-test=features.testing.spring-boot-applications.autoconfigured-jdbc +boot-features-testing-spring-boot-applications-testing-autoconfigured-data-jdbc-test=features.testing.spring-boot-applications.autoconfigured-spring-data-jdbc +boot-features-testing-spring-boot-applications-testing-autoconfigured-jooq-test=features.testing.spring-boot-applications.autoconfigured-jooq +boot-features-testing-spring-boot-applications-testing-autoconfigured-mongo-test=features.testing.spring-boot-applications.autoconfigured-spring-data-mongodb +boot-features-testing-spring-boot-applications-testing-autoconfigured-neo4j-test=features.testing.spring-boot-applications.autoconfigured-spring-data-neo4j +boot-features-testing-spring-boot-applications-testing-autoconfigured-redis-test=features.testing.spring-boot-applications.autoconfigured-spring-data-redis +boot-features-testing-spring-boot-applications-testing-autoconfigured-ldap-test=features.testing.spring-boot-applications.autoconfigured-spring-data-ldap +boot-features-testing-spring-boot-applications-testing-autoconfigured-rest-client=features.testing.spring-boot-applications.autoconfigured-rest-client +boot-features-testing-spring-boot-applications-testing-autoconfigured-rest-docs=features.testing.spring-boot-applications.autoconfigured-spring-restdocs +boot-features-testing-spring-boot-applications-testing-autoconfigured-rest-docs-mock-mvc=features.testing.spring-boot-applications.autoconfigured-spring-restdocs.with-mock-mvc +boot-features-testing-spring-boot-applications-testing-autoconfigured-rest-docs-web-test-client=features.testing.spring-boot-applications.autoconfigured-spring-restdocs.with-web-test-client +boot-features-testing-spring-boot-applications-testing-autoconfigured-rest-docs-rest-assured=features.testing.spring-boot-applications.autoconfigured-spring-restdocs.with-rest-assured +boot-features-testing-spring-boot-applications-testing-autoconfigured-webservices=features.testing.spring-boot-applications.autoconfigured-webservices +boot-features-testing-spring-boot-applications-testing-auto-configured-additional-auto-config=features.testing.spring-boot-applications.additional-autoconfiguration-and-slicing +boot-features-testing-spring-boot-applications-testing-user-configuration=features.testing.spring-boot-applications.user-configuration-and-slicing +boot-features-testing-spring-boot-applications-with-spock=features.testing.spring-boot-applications.spock +boot-features-test-utilities=features.testing.utilities +boot-features-configfileapplicationcontextinitializer-test-utility=features.testing.utilities.config-data-application-context-initializer +boot-features-test-property-values=features.testing.utilities.test-property-values +boot-features-output-capture-test-utility=features.testing.utilities.output-capture +boot-features-rest-templates-test-utility=features.testing.utilities.test-rest-template +boot-features-websockets=features.websockets +boot-features-webservices=features.webservices +boot-features-webservices-template=features.webservices.template +boot-features-developing-auto-configuration=features.developing-auto-configuration +boot-features-understanding-auto-configured-beans=features.developing-auto-configuration.understanding-auto-configured-beans +boot-features-locating-auto-configuration-candidates=features.developing-auto-configuration.locating-auto-configuration-candidates +boot-features-condition-annotations=features.developing-auto-configuration.condition-annotations +boot-features-class-conditions=features.developing-auto-configuration.condition-annotations.class-conditions +boot-features-bean-conditions=features.developing-auto-configuration.condition-annotations.bean-conditions +boot-features-property-conditions=features.developing-auto-configuration.condition-annotations.property-conditions +boot-features-resource-conditions=features.developing-auto-configuration.condition-annotations.resource-conditions +boot-features-web-application-conditions=features.developing-auto-configuration.condition-annotations.web-application-conditions +boot-features-spel-conditions=features.developing-auto-configuration.condition-annotations.spel-conditions +boot-features-test-autoconfig=features.developing-auto-configuration.testing +boot-features-test-autoconfig-simulating-web-context=features.developing-auto-configuration.testing.simulating-a-web-context +boot-features-test-autoconfig-overriding-classpath=features.developing-auto-configuration.testing.overriding-classpath +boot-features-custom-starter=features.developing-auto-configuration.custom-starter +boot-features-custom-starter-naming=features.developing-auto-configuration.custom-starter.naming +boot-features-custom-starter-configuration-keys=features.developing-auto-configuration.custom-starter.configuration-keys +boot-features-custom-starter-module-autoconfigure=features.developing-auto-configuration.custom-starter.autoconfigure-module +boot-features-custom-starter-module-starter=features.developing-auto-configuration.custom-starter.starter-module +boot-features-kotlin=features.kotlin +boot-features-kotlin-requirements=features.kotlin.requirements +boot-features-kotlin-null-safety=features.kotlin.null-safety +boot-features-kotlin-api=features.kotlin.api +boot-features-kotlin-api-runapplication=features.kotlin.api.run-application +boot-features-kotlin-api-extensions=features.kotlin.api.extensions +boot-features-kotlin-dependency-management=features.kotlin.dependency-management +boot-features-kotlin-configuration-properties=features.kotlin.configuration-properties +boot-features-kotlin-testing=features.kotlin.testing +boot-features-kotlin-resources=features.kotlin.resources +boot-features-kotlin-resources-further-reading=features.kotlin.resources.further-reading +boot-features-kotlin-resources-examples=features.kotlin.resources.examples +boot-features-container-images=features.container-images +boot-layering-docker-images=features.container-images.layering +boot-features-container-images-building=features.container-images.building +boot-features-container-images-docker=features.container-images.building.dockerfiles +boot-features-container-images-buildpacks=features.container-images.building.buildpacks +boot-features-whats-next=features.whats-next +production-ready=actuator +production-ready-enabling=actuator.enabling +production-ready-endpoints=actuator.endpoints +production-ready-endpoints-enabling-endpoints=actuator.endpoints.enabling +production-ready-endpoints-exposing-endpoints=actuator.endpoints.exposing +production-ready-endpoints-security=actuator.endpoints.security +production-ready-endpoints-caching=actuator.endpoints.caching +production-ready-endpoints-hypermedia=actuator.endpoints.hypermedia +production-ready-endpoints-cors=actuator.endpoints.cors +production-ready-endpoints-custom=actuator.endpoints.implementing-custom +production-ready-endpoints-custom-input=actuator.endpoints.implementing-custom.input +production-ready-endpoints-custom-input-conversion=actuator.endpoints.implementing-custom.input.conversion +production-ready-endpoints-custom-web=actuator.endpoints.implementing-custom.web +production-ready-endpoints-custom-web-predicate=actuator.endpoints.implementing-custom.web.request-predicates +production-ready-endpoints-custom-web-predicate-path=actuator.endpoints.implementing-custom.web.path-predicates +production-ready-endpoints-custom-web-predicate-http-method=actuator.endpoints.implementing-custom.web.method-predicates +production-ready-endpoints-custom-web-predicate-consumes=actuator.endpoints.implementing-custom.web.consumes-predicates +production-ready-endpoints-custom-web-predicate-produces=actuator.endpoints.implementing-custom.web.produces-predicates +production-ready-endpoints-custom-web-response-status=actuator.endpoints.implementing-custom.web.response-status +production-ready-endpoints-custom-web-range-requests=actuator.endpoints.implementing-custom.web.range-requests +production-ready-endpoints-custom-web-security=actuator.endpoints.implementing-custom.web.security +production-ready-endpoints-custom-servlet=actuator.endpoints.implementing-custom.servlet +production-ready-endpoints-custom-controller=actuator.endpoints.implementing-custom.controller +production-ready-health=actuator.endpoints.health +production-ready-health-indicators=actuator.endpoints.health.auto-configured-health-indicators +production-ready-health-indicators-writing=actuator.endpoints.health.writing-custom-health-indicators +reactive-health-indicators=actuator.endpoints.health.reactive-health-indicators +reactive-health-indicators-autoconfigured=actuator.endpoints.health.auto-configured-reactive-health-indicators +production-ready-health-groups=actuator.endpoints.health.groups +production-ready-health-datasource=actuator.endpoints.health.datasource +production-ready-kubernetes-probes=actuator.endpoints.kubernetes-probes +production-ready-kubernetes-probes-external-state=actuator.endpoints.kubernetes-probes.external-state +production-ready-kubernetes-probes-lifecycle=actuator.endpoints.kubernetes-probes.lifecycle +production-ready-application-info=actuator.endpoints.info +production-ready-application-info-autoconfigure=actuator.endpoints.info.auto-configured-info-contributors +production-ready-application-info-env=actuator.endpoints.info.custom-application-information +production-ready-application-info-git=actuator.endpoints.info.git-commit-information +production-ready-application-info-build=actuator.endpoints.info.build-information +production-ready-application-info-custom=actuator.endpoints.info.writing-custom-info-contributors +production-ready-monitoring=actuator.monitoring +production-ready-customizing-management-server-context-path=actuator.monitoring.customizing-management-server-context-path +production-ready-customizing-management-server-port=actuator.monitoring.customizing-management-server-port +production-ready-management-specific-ssl=actuator.monitoring.management-specific-ssl +production-ready-customizing-management-server-address=actuator.monitoring.customizing-management-server-address +production-ready-disabling-http-endpoints=actuator.monitoring.disabling-http-endpoints +production-ready-jmx=actuator.jmx +production-ready-custom-mbean-names=actuator.jmx.custom-mbean-names +production-ready-disable-jmx-endpoints=actuator.jmx.disable-jmx-endpoints +production-ready-jolokia=actuator.jmx.jolokia +production-ready-customizing-jolokia=actuator.jmx.jolokia.customizing +production-ready-disabling-jolokia=actuator.jmx.jolokia.disabling +production-ready-loggers=actuator.loggers +production-ready-logger-configuration=actuator.loggers.configure +production-ready-metrics=actuator.metrics +production-ready-metrics-getting-started=actuator.metrics.getting-started +production-ready-metrics-export=actuator.metrics.export +production-ready-metrics-export-appoptics=actuator.metrics.export.appoptics +production-ready-metrics-export-atlas=actuator.metrics.export.atlas +production-ready-metrics-export-datadog=actuator.metrics.export.datadog +production-ready-metrics-export-dynatrace=actuator.metrics.export.dynatrace +production-ready-metrics-export-elastic=actuator.metrics.export.elastic +production-ready-metrics-export-ganglia=actuator.metrics.export.ganglia +production-ready-metrics-export-graphite=actuator.metrics.export.graphite +production-ready-metrics-export-humio=actuator.metrics.export.humio +production-ready-metrics-export-influx=actuator.metrics.export.influx +production-ready-metrics-export-jmx=actuator.metrics.export.jmx +production-ready-metrics-export-kairos=actuator.metrics.export.kairos +production-ready-metrics-export-newrelic=actuator.metrics.export.newrelic +production-ready-metrics-export-prometheus=actuator.metrics.export.prometheus +production-ready-metrics-export-signalfx=actuator.metrics.export.signalfx +production-ready-metrics-export-simple=actuator.metrics.export.simple +production-ready-metrics-export-stackdriver=actuator.metrics.export.stackdriver +production-ready-metrics-export-statsd=actuator.metrics.export.statsd +production-ready-metrics-export-wavefront=actuator.metrics.export.wavefront +production-ready-metrics-meter=actuator.metrics.supported +production-ready-metrics-jvm=actuator.metrics.supported.jvm +production-ready-metrics-system=actuator.metrics.supported.system +production-ready-metrics-logger=actuator.metrics.supported.logger +production-ready-metrics-spring-mvc=actuator.metrics.supported.spring-mvc +production-ready-metrics-web-flux=actuator.metrics.supported.spring-webflux +production-ready-metrics-jersey-server=actuator.metrics.supported.jersey +production-ready-metrics-http-clients=actuator.metrics.supported.http-clients +production-ready-metrics-tomcat=actuator.metrics.supported.tomcat +production-ready-metrics-cache=actuator.metrics.supported.cache +production-ready-metrics-jdbc=actuator.metrics.supported.jdbc +production-ready-metrics-hibernate=actuator.metrics.supported.hibernate +production-ready-metrics-data-repository=actuator.metrics.supported.spring-data-repository +production-ready-metrics-rabbitmq=actuator.metrics.supported.rabbitmq +production-ready-metrics-integration=actuator.metrics.supported.spring-integration +production-ready-metrics-kafka=actuator.metrics.supported.kafka +production-ready-metrics-mongodb=actuator.metrics.supported.mongodb +production-ready-metrics-mongodb-command=actuator.metrics.supported.mongodb.command +production-ready-metrics-mongodb-connectionpool=actuator.metrics.supported.mongodb.connection-pool +production-ready-metrics-timed-annotation=actuator.metrics.supported.timed-annotation +production-ready-metrics-custom=actuator.metrics.registering-custom +production-ready-metrics-customizing=actuator.metrics.customizing +production-ready-metrics-common-tags=actuator.metrics.customizing.common-tags +production-ready-metrics-per-meter-properties=actuator.metrics.customizing.per-meter-properties +production-ready-metrics-endpoint=actuator.metrics.endpoint +production-ready-auditing=actuator.auditing +production-ready-auditing-custom=actuator.auditing.custom +production-ready-http-tracing=actuator.tracing +production-ready-http-tracing-custom=actuator.tracing.custom +production-ready-process-monitoring=actuator.process-monitoring +production-ready-process-monitoring-configuration=actuator.process-monitoring.configuration +production-ready-process-monitoring-programmatically=actuator.process-monitoring.programmatically +production-ready-cloudfoundry=actuator.cloud-foundry +production-ready-cloudfoundry-disable=actuator.cloud-foundry.disable +production-ready-cloudfoundry-ssl=actuator.cloud-foundry.ssl +production-ready-custom-context-path=actuator.cloud-foundry.custom-context-path +production-ready-whats-next=actuator.whats-next +deployment=deployment +containers-deployment=deployment.containers +cloud-deployment=deployment.cloud +cloud-deployment-cloud-foundry=deployment.cloud.cloud-foundry +cloud-deployment-cloud-foundry-services=deployment.cloud.cloud-foundry.binding-to-services +cloud-deployment-kubernetes=deployment.cloud.kubernetes +cloud-deployment-kubernetes-container-lifecycle=deployment.cloud.kubernetes.container-lifecycle +cloud-deployment-heroku=deployment.cloud.heroku +cloud-deployment-openshift=deployment.cloud.openshift +cloud-deployment-aws=deployment.cloud.aws +cloud-deployment-aws-beanstalk=deployment.cloud.aws.beanstalk +cloud-deployment-aws-tomcat-platform=deployment.cloud.aws.beanstalk.tomcat-platform +cloud-deployment-aws-java-se-platform=deployment.cloud.aws.beanstalk.java-se-platform +cloud-deployment-aws-summary=deployment.cloud.aws.summary +cloud-deployment-boxfuse=deployment.cloud.boxfuse +cloud-deployment-gae=deployment.cloud.google +deployment-install=deployment.installing +deployment-install-supported-operating-systems=deployment.installing.supported-operating-systems +deployment-service=deployment.installing.nix-services +deployment-initd-service=deployment.installing.nix-services.init-d +deployment-initd-service-securing=deployment.installing.nix-services.init-d.securing +deployment-systemd-service=deployment.installing.nix-services.system-d +deployment-script-customization=deployment.installing.nix-services.script-customization +deployment-script-customization-when-it-written=deployment.installing.nix-services.script-customization.when-written +deployment-script-customization-when-it-runs=deployment.installing.nix-services.script-customization.when-running +deployment-windows=deployment.installing.windows-services +deployment-whats-next=deployment.whats-next +cli=cli +cli-installation=cli.installation +cli-using-the-cli=cli.using-the-cli +cli-run=cli.using-the-cli.run +cli-deduced-grab-annotations=cli.using-the-cli.run.deduced-grab-annotations +cli-default-grab-deduced-coordinates=cli.using-the-cli.run.deduced-grab-coordinates +cli-default-import-statements=cli.using-the-cli.run.default-import-statements +cli-automatic-main-method=cli.using-the-cli.run.automatic-main-method +cli-default-grab-deduced-coordinates-custom-dependency-management=cli.using-the-cli.run.custom-dependency-management +cli-multiple-source-files=cli.using-the-cli.multiple-source-files +cli-jar=cli.using-the-cli.packaging +cli-init=cli.using-the-cli.initialize-new-project +cli-shell=cli.using-the-cli.embedded-shell +cli-install-uninstall=cli.using-the-cli.extensions +cli-groovy-beans-dsl=cli.groovy-beans-dsl +cli-maven-settings=cli.maven-setting +cli-whats-next=cli.whats-next +build-tool-plugins=build-tool-plugins +build-tool-plugins-maven-plugin=build-tool-plugins.maven +build-tool-plugins-gradle-plugin=build-tool-plugins.gradle +build-tool-plugins-antlib=build-tool-plugins.antlib +spring-boot-ant-tasks=build-tool-plugins.antlib.tasks +spring-boot-ant-exejar=build-tool-plugins.antlib.tasks.exejar +spring-boot-ant-exejar-examples=build-tool-plugins.antlib.tasks.examples +spring-boot-ant-findmainclass=build-tool-plugins.antlib.findmainclass +spring-boot-ant-findmainclass-examples=build-tool-plugins.antlib.findmainclass.examples +build-tool-plugins-other-build-systems=build-tool-plugins.other-build-systems +build-tool-plugins-repackaging-archives=build-tool-plugins.other-build-systems.repackaging-archives +build-tool-plugins-nested-libraries=build-tool-plugins.other-build-systems.nested-libraries +build-tool-plugins-find-a-main-class=build-tool-plugins.other-build-systems.finding-main-class +build-tool-plugins-repackage-implementation=build-tool-plugins.other-build-systems.example-repackage-implementation +build-tool-plugins-whats-next=build-tool-plugins.whats-next +howto=howto +howto-spring-boot-application=howto.application +howto-failure-analyzer=howto.application.failure-analyzer +howto-troubleshoot-auto-configuration=howto.application.troubleshoot-auto-configuration +howto-customize-the-environment-or-application-context=howto.application.customize-the-environment-or-application-context +howto-build-an-application-context-hierarchy=howto.application.context-hierarchy +howto-create-a-non-web-application=howto.application.non-web-application +howto-properties-and-configuration=howto.properties-and-configuration +howto-automatic-expansion=howto.properties-and-configuration.expand-properties +howto-automatic-expansion-maven=howto.properties-and-configuration.expand-properties.maven +howto-automatic-expansion-gradle=howto.properties-and-configuration.expand-properties.gradle +howto-externalize-configuration=howto.properties-and-configuration.externalize-configuration +howto-change-the-location-of-external-properties=howto.properties-and-configuration.external-properties-location +howto-use-short-command-line-arguments=howto.properties-and-configuration.short-command-line-arguments +howto-use-yaml-for-external-properties=howto.properties-and-configuration.yaml +howto-set-active-spring-profiles=howto.properties-and-configuration.set-active-spring-profiles +howto-change-configuration-depending-on-the-environment=howto.properties-and-configuration.change-configuration-depending-on-the-environment +howto-discover-build-in-options-for-external-properties=howto.properties-and-configuration.discover-build-in-options-for-external-properties +howto-embedded-web-servers=howto.webserver +howto-use-another-web-server=howto.webserver.use-another +howto-disable-web-server=howto.webserver.disable +howto-change-the-http-port=howto.webserver.change-port +howto-user-a-random-unassigned-http-port=howto.webserver.use-random-port +howto-discover-the-http-port-at-runtime=howto.webserver.discover-port +how-to-enable-http-response-compression=howto.webserver.enable-response-compression +howto-configure-ssl=howto.webserver.configure-ssl +howto-configure-http2=howto.webserver.configure-http2 +howto-configure-http2-tomcat=howto.webserver.configure-http2.tomcat +howto-configure-http2-jetty=howto.webserver.configure-http2.jetty +howto-configure-http2-netty=howto.webserver.configure-http2.netty +howto-configure-http2-undertow=howto.webserver.configure-http2.undertow +howto-configure-webserver=howto.webserver.configure +howto-add-a-servlet-filter-or-listener=howto.webserver.add-servlet-filter-listener +howto-add-a-servlet-filter-or-listener-as-spring-bean=howto.webserver.add-servlet-filter-listener.spring-bean +howto-disable-registration-of-a-servlet-or-filter=howto.webserver.add-servlet-filter-listener.spring-bean.disable +howto-add-a-servlet-filter-or-listener-using-scanning=howto.webserver.add-servlet-filter-listener.using-scanning +howto-configure-accesslogs=howto.webserver.configure-access-logs +howto-use-behind-a-proxy-server=howto.webserver.use-behind-a-proxy-server +howto-customize-tomcat-behind-a-proxy-server=howto.webserver.use-behind-a-proxy-server.tomcat +howto-enable-multiple-connectors-in-tomcat=howto.webserver.enable-multiple-connectors-in-tomcat +howto-use-tomcat-legacycookieprocessor=howto.webserver.use-tomcat-legacycookieprocessor +howto-enable-tomcat-mbean-registry=howto.webserver.enable-tomcat-mbean-registry +howto-enable-multiple-listeners-in-undertow=howto.webserver.enable-multiple-listeners-in-undertow +howto-create-websocket-endpoints-using-serverendpoint=howto.webserver.create-websocket-endpoints-using-serverendpoint +howto-spring-mvc=howto.spring-mvc +howto-write-a-json-rest-service=howto.spring-mvc.write-json-rest-service +howto-write-an-xml-rest-service=howto.spring-mvc.write-xml-rest-service +howto-customize-the-jackson-objectmapper=howto.spring-mvc.customize-jackson-objectmapper +howto-customize-the-responsebody-rendering=howto.spring-mvc.customize-responsebody-rendering +howto-multipart-file-upload-configuration=howto.spring-mvc.multipart-file-uploads +howto-switch-off-the-spring-mvc-dispatcherservlet=howto.spring-mvc.switch-off-dispatcherservlet +howto-switch-off-default-mvc-configuration=howto.spring-mvc.switch-off-default-configuration +howto-customize-view-resolvers=howto.spring-mvc.customize-view-resolvers +howto-use-test-with-spring-security=howto.spring-mvc.testing.with-spring-security +howto-jersey=howto.jersey +howto-jersey-spring-security=howto.jersey.spring-security +howto-jersey-alongside-another-web-framework=howto.jersey.alongside-another-web-framework +howto-http-clients=howto.http-clients +howto-http-clients-proxy-configuration=howto.http-clients.rest-template-proxy-configuration +howto-webclient-reactor-netty-customization=howto.http-clients.webclient-reactor-netty-customization +howto-logging=howto.logging +howto-configure-logback-for-logging=howto.logging.logback +howto-configure-logback-for-logging-fileonly=howto.logging.logback.file-only-output +howto-configure-log4j-for-logging=howto.logging.log4j +howto-configure-log4j-for-logging-yaml-or-json-config=howto.logging.log4j.yaml-or-json-config +howto-data-access=howto.data-access +howto-configure-a-datasource=howto.data-access.configure-custom-datasource +howto-two-datasources=howto.data-access.configure-two-datasources +howto-use-spring-data-repositories=howto.data-access.spring-data-repositories +howto-separate-entity-definitions-from-spring-configuration=howto.data-access.separate-entity-definitions-from-spring-configuration +howto-configure-jpa-properties=howto.data-access.jpa-properties +howto-configure-hibernate-naming-strategy=howto.data-access.configure-hibernate-naming-strategy +howto-configure-hibernate-second-level-caching=howto.data-access.configure-hibernate-second-level-caching +howto-use-dependency-injection-hibernate-components=howto.data-access.dependency-injection-in-hibernate-components +howto-use-custom-entity-manager=howto.data-access.use-custom-entity-manager +howto-use-multiple-entity-managers=howto.data-access.use-multiple-entity-managers +howto-use-two-entity-managers=howto.data-access.use-multiple-entity-managers +howto-use-traditional-persistence-xml=howto.data-access.use-traditional-persistence-xml +howto-use-spring-data-jpa--and-mongo-repositories=howto.data-access.use-spring-data-jpa-and-mongo-repositories +howto-use-customize-spring-datas-web-support=howto.data-access.customize-spring-data-web-support +howto-use-exposing-spring-data-repositories-rest-endpoint=howto.data-access.exposing-spring-data-repositories-as-rest +howto-configure-a-component-that-is-used-by-JPA=howto.data-access.configure-a-component-that-is-used-by-jpa +howto-configure-jOOQ-with-multiple-datasources=howto.data-access.configure-jooq-with-multiple-datasources +howto-database-initialization=howto.data-initialization +howto-initialize-a-database-using-jpa=howto.data-initialization.using-jpa +howto-initialize-a-database-using-hibernate=howto.data-initialization.using-hibernate +howto-initialize-a-database-using-basic-scripts=howto.data-initialization.using-basic-sql-scripts +howto-initialize-a-spring-batch-database=howto.data-initialization.batch +howto-use-a-higher-level-database-migration-tool=howto.data-initialization.migration-tool +howto-execute-flyway-database-migrations-on-startup=howto.data-initialization.migration-tool.flyway +howto-execute-liquibase-database-migrations-on-startup=howto.data-initialization.migration-tool.liquibase +howto-initialize-a-database-configuring-dependencies=howto.data-initialization.dependencies +howto-initialize-a-database-configuring-dependencies-initializer-detection=howto.data-initialization.dependencies.initializer-detection +howto-initialize-a-database-configuring-dependencies-depends-on-initialization-detection=howto.data-initialization.dependencies.depends-on-initialization-detection +howto-messaging=howto.messaging +howto-jms-disable-transaction=howto.messaging.disable-transacted-jms-session +howto-batch-applications=howto.batch +howto-spring-batch-specifying-a-data-source=howto.batch.specifying-a-data-source +howto-spring-batch-running-jobs-on-startup=howto.batch.running-jobs-on-startup +howto-spring-batch-running-command-line=howto.batch.running-from-the-command-line +howto-spring-batch-storing-job-repository=howto.batch.storing-job-repository +howto-actuator=howto.actuator +howto-change-the-http-port-or-address-of-the-actuator-endpoints=howto.actuator.change-http-port-or-address +howto-customize-the-whitelabel-error-page=howto.actuator.customize-whitelabel-error-page +howto-sanitize-sensitive-values=howto.actuator.sanitize-sensitive-values +howto-sanitize-sensible-values=howto.actuator.sanitize-sensitive-values +howto-map-health-indicators-to-metrics=howto.actuator.map-health-indicators-to-metrics +howto-security=howto.security +howto-switch-off-spring-boot-security-configuration=howto.security.switch-off-spring-boot-configuration +howto-change-the-user-details-service-and-add-user-accounts=howto.security.change-user-details-service-and-add-user-accounts +howto-enable-https=howto.security.enable-https +howto-hotswapping=howto.hotswapping +howto-reload-static-content=howto.hotswapping.reload-static-content +howto-reload-thymeleaf-template-content=howto.hotswapping.reload-templates +howto-reload-thymeleaf-content=howto.hotswapping.reload-templates.thymeleaf +howto-reload-freemarker-content=howto.hotswapping.reload-templates.freemarker +howto-reload-groovy-template-content=howto.hotswapping.reload-templates.groovy +howto-reload-fast-restart=howto.hotswapping.fast-application-restarts +howto-reload-java-classes-without-restarting=howto.hotswapping.reload-java-classes-without-restarting +howto-build=howto.build +howto-build-info=howto.build.generate-info +howto-git-info=howto.build.generate-git-info +howto-customize-dependency-versions=howto.build.customize-dependency-versions +howto-create-an-executable-jar-with-maven=howto.build.create-an-executable-jar-with-maven +howto-create-an-additional-executable-jar=howto.build.use-a-spring-boot-application-as-dependency +howto-extract-specific-libraries-when-an-executable-jar-runs=howto.build.extract-specific-libraries-when-an-executable-jar-runs +howto-create-a-nonexecutable-jar=howto.build.create-a-nonexecutable-jar +howto-remote-debug-maven-run=howto.build.remote-debug-maven +howto-build-an-executable-archive-with-ant=howto.build.build-an-executable-archive-with-ant-without-using-spring-boot-antlib +howto-traditional-deployment=howto.traditional-deployment +howto-create-a-deployable-war-file=howto.traditional-deployment.war +howto-convert-an-existing-application-to-spring-boot=howto.traditional-deployment.convert-existing-application +howto-weblogic=howto.traditional-deployment.weblogic +howto-use-jedis-instead-of-lettuce=howto.nosql.jedis-instead-of-lettuce +howto-testcontainers=howto.testing.testcontainers + +# Appendix restructuring, see gh-27003 +common-application-properties=appendix.application-properties +common-application-properties-core=appendix.application-properties.core +common-application-properties-cache=appendix.application-properties.cache +common-application-properties-mail=appendix.application-properties.mail +common-application-properties-json=appendix.application-properties.json +common-application-properties-data=appendix.application-properties.data +common-application-properties-transaction=appendix.application-properties.transaction +common-application-properties-data-migration=appendix.application-properties.data-migration +common-application-properties-integration=appendix.application-properties.integration +common-application-properties-web=appendix.application-properties.web +common-application-properties-templating=appendix.application-properties.templating +common-application-properties-server=appendix.application-properties.server +common-application-properties-security=appendix.application-properties.security +common-application-properties-rsocket=appendix.application-properties.rsocket +common-application-properties-actuator=appendix.application-properties.actuator +common-application-properties-devtools=appendix.application-properties.devtools +common-application-properties-testing=appendix.application-properties.testing + +application-properties=appendix.application-properties +application-properties.core=appendix.application-properties.core +application-properties.cache=appendix.application-properties.cache +application-properties.mail=appendix.application-properties.mail +application-properties.json=appendix.application-properties.json +application-properties.data=appendix.application-properties.data +application-properties.transaction=appendix.application-properties.transaction +application-properties.data-migration=appendix.application-properties.data-migration +application-properties.integration=appendix.application-properties.integration +application-properties.web=appendix.application-properties.web +application-properties.templating=appendix.application-properties.templating +application-properties.server=appendix.application-properties.server +application-properties.security=appendix.application-properties.security +application-properties.rsocket=appendix.application-properties.rsocket +application-properties.actuator=appendix.application-properties.actuator +application-properties.devtools=appendix.application-properties.devtools +application-properties.testing=appendix.application-properties.testing + +core-properties=appendix.application-properties.core +cache-properties=appendix.application-properties.cache +mail-properties=appendix.application-properties.mail +json-properties=appendix.application-properties.json +data-properties=appendix.application-properties.data +transaction-properties=appendix.application-properties.transaction +data-migration-properties=appendix.application-properties.data-migration +integration-properties=appendix.application-properties.integration +web-properties=appendix.application-properties.web +templating-properties=appendix.application-properties.templating +server-properties=appendix.application-properties.server +security-properties=appendix.application-properties.security +rsocket-properties=appendix.application-properties.rsocket +actuator-properties=appendix.application-properties.actuator +devtools-properties=appendix.application-properties.devtools +testing-properties=appendix.application-properties.testing + +configuration-metadata=appendix.configuration-metadata +configuration-metadata-format=appendix.configuration-metadata.format +configuration-metadata-group-attributes=appendix.configuration-metadata.format.group +configuration-metadata-property-attributes=appendix.configuration-metadata.format.property +configuration-metadata-hints-attributes=appendix.configuration-metadata.format.hints +configuration-metadata-repeated-items=appendix.configuration-metadata.format.repeated-items +configuration-metadata-providing-manual-hints=appendix.configuration-metadata.manual-hints +configuration-metadata-providing-manual-hints-value-hint=appendix.configuration-metadata.manual-hints.value-hint +configuration-metadata-providing-manual-hints-value-providers=appendix.configuration-metadata.manual-hints.value-providers +configuration-metadata-providing-manual-hints-any=appendix.configuration-metadata.manual-hints.value-providers.any +configuration-metadata-providing-manual-hints-class-reference=appendix.configuration-metadata.manual-hints.value-providers.class-reference +configuration-metadata-providing-manual-hints-handle-as=appendix.configuration-metadata.manual-hints.value-providers.handle-as +configuration-metadata-providing-manual-hints-logger-name=appendix.configuration-metadata.manual-hints.value-providers.logger-name +configuration-metadata-providing-manual-hints-spring-bean-reference=appendix.configuration-metadata.manual-hints.value-providers.spring-bean-reference +configuration-metadata-providing-manual-hints-spring-profile-name=appendix.configuration-metadata.manual-hints.value-providers.spring-profile-name +configuration-metadata-annotation-processor=appendix.configuration-metadata.annotation-processor +configuration-metadata-annotation-processor-setup=appendix.configuration-metadata.annotation-processor.configuring +configuration-metadata-annotation-processor-metadata-generation=appendix.configuration-metadata.annotation-processor.automatic-metadata-generation +configuration-metadata-annotation-processor-metadata-generation-nested=appendix.configuration-metadata.annotation-processor.automatic-metadata-generation.nested-properties +configuration-metadata-additional-metadata=appendix.configuration-metadata.annotation-processor.adding-additional-metadata + +configuration-metadata.format=appendix.configuration-metadata.format +configuration-metadata.format.group=appendix.configuration-metadata.format.group +configuration-metadata.format.property=appendix.configuration-metadata.format.property +configuration-metadata.format.hints=appendix.configuration-metadata.format.hints +configuration-metadata.format.repeated-items=appendix.configuration-metadata.format.repeated-items +configuration-metadata.manual-hints=appendix.configuration-metadata.manual-hints +configuration-metadata.manual-hints.value-hint=appendix.configuration-metadata.manual-hints.value-hint +configuration-metadata.manual-hints.value-providers=appendix.configuration-metadata.manual-hints.value-providers +configuration-metadata.manual-hints.value-providers.any=appendix.configuration-metadata.manual-hints.value-providers.any +configuration-metadata.manual-hints.value-providers.class-reference=appendix.configuration-metadata.manual-hints.value-providers.class-reference +configuration-metadata.manual-hints.value-providers.handle-as=appendix.configuration-metadata.manual-hints.value-providers.handle-as +configuration-metadata.manual-hints.value-providers.logger-name=appendix.configuration-metadata.manual-hints.value-providers.logger-name +configuration-metadata.manual-hints.value-providers.spring-bean-reference=appendix.configuration-metadata.manual-hints.value-providers.spring-bean-reference +configuration-metadata.manual-hints.value-providers.spring-profile-name=appendix.configuration-metadata.manual-hints.value-providers.spring-profile-name +configuration-metadata.annotation-processor=appendix.configuration-metadata.annotation-processor +configuration-metadata.annotation-processor.configuring=appendix.configuration-metadata.annotation-processor.configuring +configuration-metadata.annotation-processor.automatic-metadata-generation=appendix.configuration-metadata.annotation-processor.automatic-metadata-generation +configuration-metadata.annotation-processor.automatic-metadata-generation.nested-properties=appendix.configuration-metadata.annotation-processor.automatic-metadata-generation.nested-properties +configuration-metadata.annotation-processor.adding-additional-metadata=appendix.configuration-metadata.annotation-processor.adding-additional-metadata + +auto-configuration-classes=appendix.auto-configuration-classes +auto-configuration-classes-from-autoconfigure-module=appendix.auto-configuration-classes.core +auto-configuration-classes-from-actuator=appendix.auto-configuration-classes.actuator + +auto-configuration-classes.core=appendix.auto-configuration-classes.core +auto-configuration-classes.actuator=appendix.auto-configuration-classes.actuator + +test-auto-configuration=appendix.test-auto-configuration +test-auto-configuration-slices=appendix.test-auto-configuration.slices + +test-auto-configuration.slices=appendix.test-auto-configuration.slices + +executable-jar=appendix.executable-jar +executable-jar-nested-jars=appendix.executable-jar.nested-jars +executable-jar-jar-file-structure=appendix.executable-jar.nested-jars.jar-structure +executable-jar-war-file-structure=appendix.executable-jar.nested-jars.war-structure +executable-jar-war-index-files=appendix.executable-jar.nested-jars.index-files +executable-jar-war-index-files-classpath=appendix.executable-jar.nested-jars.classpath-index +executable-jar-war-index-files-layers=appendix.executable-jar.nested-jars.layer-index +executable-jar-jarfile=appendix.executable-jar.jarfile-class +executable-jar-jarfile-compatibility=appendix.executable-jar.jarfile-class.compatibility +executable-jar-launching=appendix.executable-jar.launching +executable-jar-launcher-manifest=appendix.executable-jar.launching.manifest +executable-jar-property-launcher-features=appendix.executable-jar.property-launcher +executable-jar-restrictions=appendix.executable-jar.restrictions +executable-jar-alternatives=appendix.executable-jar.alternatives + +executable-jar.nested-jars=appendix.executable-jar.nested-jars +executable-jar.nested-jars.jar-structure=appendix.executable-jar.nested-jars.jar-structure +executable-jar.nested-jars.war-structure=appendix.executable-jar.nested-jars.war-structure +executable-jar.nested-jars.index-files=appendix.executable-jar.nested-jars.index-files +executable-jar.nested-jars.classpath-index=appendix.executable-jar.nested-jars.classpath-index +executable-jar.nested-jars.layer-index=appendix.executable-jar.nested-jars.layer-index +executable-jar.jarfile-class=appendix.executable-jar.jarfile-class +executable-jar.jarfile-class.compatibility=appendix.executable-jar.jarfile-class.compatibility +executable-jar.launching=appendix.executable-jar.launching +executable-jar.launching.manifest=appendix.executable-jar.launching.manifest +executable-jar.property-launcher=appendix.executable-jar.property-launcher +executable-jar.restrictions=appendix.executable-jar.restrictions +executable-jar.alternatives=appendix.executable-jar.alternatives + +dependency-versions=appendix.dependency-versions +dependency-versions-coordinates=appendix.dependency-versions.coordinates +dependency-versions-properties=appendix.dependency-versions.properties + +dependency-versions.coordinates=appendix.dependency-versions.coordinates +dependency-versions.properties=appendix.dependency-versions.properties + diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/application-properties.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/application-properties.adoc new file mode 100644 index 000000000000..74adc6cc4152 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/application-properties.adoc @@ -0,0 +1,48 @@ +[appendix] +[[appendix.application-properties]] += Common Application Properties +include::attributes.adoc[] + + + +Various properties can be specified inside your `application.properties` file, inside your `application.yml` file, or as command line switches. +This appendix provides a list of common Spring Boot properties and references to the underlying classes that consume them. + +TIP: Spring Boot provides various conversion mechanism with advanced value formatting, make sure to review <>. + +NOTE: Property contributions can come from additional jar files on your classpath, so you should not consider this an exhaustive list. +Also, you can define your own properties. + + + +include::application-properties/core.adoc[] + +include::application-properties/cache.adoc[] + +include::application-properties/mail.adoc[] + +include::application-properties/json.adoc[] + +include::application-properties/data.adoc[] + +include::application-properties/transaction.adoc[] + +include::application-properties/data-migration.adoc[] + +include::application-properties/integration.adoc[] + +include::application-properties/web.adoc[] + +include::application-properties/templating.adoc[] + +include::application-properties/server.adoc[] + +include::application-properties/security.adoc[] + +include::application-properties/rsocket.adoc[] + +include::application-properties/actuator.adoc[] + +include::application-properties/devtools.adoc[] + +include::application-properties/testing.adoc[] diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc new file mode 100644 index 000000000000..954b2efb593c --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc @@ -0,0 +1,104 @@ +:doctype: book +:idprefix: +:idseparator: - +:toc: left +:toclevels: 4 +:tabsize: 4 +:numbered: +:sectanchors: +:sectnums: +:hide-uri-scheme: +:docinfo: shared,private +:attribute-missing: warn +:chomp: default headers packages +:spring-boot-artifactory-repo: snapshot +:github-tag: main +:spring-boot-version: current +:github-repo: spring-projects/spring-boot +:github-raw: https://raw.githubusercontent.com/{github-repo}/{github-tag} +:github-issues: https://github.com/{github-repo}/issues/ +:github-wiki: https://github.com/{github-repo}/wiki +:docs-java: ../../main/java/org/springframework/boot/docs +:docs-groovy: ../../main/groovy/org/springframework/boot/docs +:spring-boot-code: https://github.com/{github-repo}/tree/{github-tag} +:spring-boot-api: https://docs.spring.io/spring-boot/docs/{spring-boot-version}/api +:spring-boot-docs: https://docs.spring.io/spring-boot/docs/{spring-boot-version}/reference +:spring-boot-latest-code: https://github.com/{github-repo}/tree/main +:spring-boot-current-docs: https://docs.spring.io/spring-boot/docs/current/reference/ +:spring-boot-actuator-restapi-docs: https://docs.spring.io/spring-boot/docs/{spring-boot-version}/actuator-api/htmlsingle +:spring-boot-actuator-restapi-pdfdocs: https://docs.spring.io/spring-boot/docs/{spring-boot-version}/actuator-api/pdf/spring-boot-actuator-web-api.pdf +:spring-boot-maven-plugin-docs: https://docs.spring.io/spring-boot/docs/{spring-boot-version}/maven-plugin/reference/htmlsingle/ +:spring-boot-maven-plugin-pdfdocs: https://docs.spring.io/spring-boot/docs/{spring-boot-version}/maven-plugin/reference/pdf/spring-boot-maven-plugin-reference.pdf +:spring-boot-maven-plugin-api: https://docs.spring.io/spring-boot/docs/{spring-boot-version}/maven-plugin/api/ +:spring-boot-gradle-plugin-docs: https://docs.spring.io/spring-boot/docs/{spring-boot-version}/gradle-plugin/reference/htmlsingle/ +:spring-boot-gradle-plugin-pdfdocs: https://docs.spring.io/spring-boot/docs/{spring-boot-version}/gradle-plugin/reference/pdf/spring-boot-gradle-plugin-reference.pdf +:spring-boot-gradle-plugin-api: https://docs.spring.io/spring-boot/docs/{spring-boot-version}/gradle-plugin/api/ +:spring-boot-module-code: {spring-boot-code}/spring-boot-project/spring-boot/src/main/java/org/springframework/boot +:spring-boot-module-api: {spring-boot-api}/org/springframework/boot +:spring-boot-autoconfigure-module-code: {spring-boot-code}/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure +:spring-boot-autoconfigure-module-api: {spring-boot-api}/org/springframework/boot/autoconfigure +:spring-boot-actuator-module-code: {spring-boot-code}/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate +:spring-boot-actuator-module-api: {spring-boot-api}/org/springframework/boot/actuate +:spring-boot-actuator-autoconfigure-module-code: {spring-boot-code}/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure +:spring-boot-actuator-autoconfigure-module-api: : {spring-boot-api}/org/springframework/boot/actuate/autoconfigure +:spring-boot-cli-module-code: {spring-boot-code}/spring-boot-project/spring-boot-cli/src/main/java/org/springframework/boot/cli +:spring-boot-cli-module-api: {spring-boot-api}/org/springframework/boot/cli +:spring-boot-devtools-module-code: {spring-boot-code}/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools +:spring-boot-devtools-module-api: {spring-boot-api}/org/springframework/boot/devtools +:spring-boot-for-apache-geode: https://github.com/spring-projects/spring-boot-data-geode +:spring-boot-for-apache-geode-docs: https://docs.spring.io/spring-boot-data-geode-build/1.5.x/reference/html5/ +:spring-boot-test-module-code: {spring-boot-code}/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test +:spring-boot-test-module-api: {spring-boot-api}/org/springframework/boot/test +:spring-boot-test-autoconfigure-module-code: {spring-boot-code}/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure +:spring-boot-test-autoconfigure-module-api: {spring-boot-api}/org/springframework/boot/test/autoconfigure +:spring-amqp-api: https://docs.spring.io/spring-amqp/docs/{spring-amqp-version}/api/org/springframework/amqp +:spring-batch: https://spring.io/projects/spring-batch +:spring-batch-api: https://docs.spring.io/spring-batch/docs/{spring-batch-version}/api/org/springframework/batch +:spring-batch-docs: https://docs.spring.io/spring-batch/docs/{spring-batch-version}/reference/html/ +:spring-data: https://spring.io/projects/spring-data +:spring-data-cassandra: https://spring.io/projects/spring-data-cassandra +:spring-data-commons-api: https://docs.spring.io/spring-data/commons/docs/{spring-data-commons-version}/api/org/springframework/data +:spring-data-couchbase: https://spring.io/projects/spring-data-couchbase +:spring-data-couchbase-docs: https://docs.spring.io/spring-data/couchbase/docs/{spring-data-couchbase-version}/reference/html/ +:spring-data-elasticsearch: https://spring.io/projects/spring-data-elasticsearch +:spring-data-elasticsearch-docs: https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/ +:spring-data-gemfire: https://spring.io/projects/spring-data-gemfire +:spring-data-geode: https://spring.io/projects/spring-data-geode +:spring-data-jpa: https://spring.io/projects/spring-data-jpa +:spring-data-jpa-api: https://docs.spring.io/spring-data/jpa/docs/{spring-data-jpa-version}/api/org/springframework/data/jpa +:spring-data-jpa-docs: https://docs.spring.io/spring-data/jpa/docs/{spring-data-jpa-version}/reference/html +:spring-data-jdbc-docs: https://docs.spring.io/spring-data/jdbc/docs/{spring-data-jdbc-version}/reference/html/ +:spring-data-ldap: https://spring.io/projects/spring-data-ldap +:spring-data-mongodb: https://spring.io/projects/spring-data-mongodb +:spring-data-mongodb-api: https://docs.spring.io/spring-data/mongodb/docs/{spring-data-mongodb-version}/api/org/springframework/data/mongodb +:spring-data-neo4j: https://spring.io/projects/spring-data-neo4j +:spring-data-neo4j-docs: https://docs.spring.io/spring-data/neo4j/docs/{spring-data-neo4j-version}/reference/html/ +:spring-data-r2dbc-api: https://docs.spring.io/spring-data/r2dbc/docs/{spring-data-r2dbc-version}/api/org/springframework/data/r2dbc +:spring-data-r2dbc-docs: https://docs.spring.io/spring-data/r2dbc/docs/{spring-data-r2dbc-version}/reference/html/ +:spring-data-redis: https://spring.io/projects/spring-data-redis +:spring-data-rest-api: https://docs.spring.io/spring-data/rest/docs/{spring-data-rest-version}/api/org/springframework/data/rest +:spring-framework: https://spring.io/projects/spring-framework +:spring-framework-api: https://docs.spring.io/spring-framework/docs/{spring-framework-version}/javadoc-api/org/springframework +:spring-framework-docs: https://docs.spring.io/spring-framework/docs/{spring-framework-version}/reference/html +:spring-integration: https://spring.io/projects/spring-integration +:spring-integration-docs: https://docs.spring.io/spring-integration/docs/{spring-integration-version}/reference/html/ +:spring-kafka-docs: https://docs.spring.io/spring-kafka/docs/{spring-kafka-version}/reference/html/ +:spring-restdocs: https://spring.io/projects/spring-restdocs +:spring-security: https://spring.io/projects/spring-security +:spring-security-docs: https://docs.spring.io/spring-security/site/docs/{spring-security-version}/reference/html5/ +:spring-security-oauth2: https://spring.io/projects/spring-security-oauth +:spring-security-oauth2-docs: https://projects.spring.io/spring-security-oauth/docs/oauth2.html +:spring-session: https://spring.io/projects/spring-session +:spring-webservices-docs: https://docs.spring.io/spring-ws/docs/{spring-webservices-version}/reference/html/ +:ant-docs: https://ant.apache.org/manual +:dependency-management-plugin-code: https://github.com/spring-gradle-plugins/dependency-management-plugin +:gradle-docs: https://docs.gradle.org/current/userguide +:hibernate-docs: https://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html +:java-api: https://docs.oracle.com/javase/8/docs/api +:jooq-docs: https://www.jooq.org/doc/{jooq-version}/manual-single-page +:junit5-docs: https://junit.org/junit5/docs/current/user-guide +:kotlin-docs: https://kotlinlang.org/docs/reference/ +:micrometer-docs: https://micrometer.io/docs +:micrometer-concepts-docs: {micrometer-docs}/concepts +:micrometer-registry-docs: {micrometer-docs}/registry +:tomcat-docs: https://tomcat.apache.org/tomcat-9.0-doc diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/authors.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/authors.adoc new file mode 100644 index 000000000000..88a716a43846 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/authors.adoc @@ -0,0 +1 @@ +Phillip Webb; Dave Syer; Josh Long; Stéphane Nicoll; Rob Winch; Andy Wilkinson; Marcel Overdijk; Christian Dupuis; Sébastien Deleuze; Michael Simons; Vedran Pavić; Jay Bryant; Madhura Bhave; Eddú Meléndez; Scott Frederick diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/auto-configuration-classes.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/auto-configuration-classes.adoc new file mode 100644 index 000000000000..13252737c083 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/auto-configuration-classes.adoc @@ -0,0 +1,16 @@ +[appendix] +[[appendix.auto-configuration-classes]] += Auto-configuration Classes +include::attributes.adoc[] + + + +This appendix contains details of all of the auto-configuration classes provided by Spring Boot, with links to documentation and source code. +Remember to also look at the conditions report in your application for more details of which features are switched on. +(To do so, start the app with `--debug` or `-Ddebug` or, in an Actuator application, use the `conditions` endpoint). + + + +include::auto-configuration-classes/core.adoc[] + +include::auto-configuration-classes/actuator.adoc[] diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/auto-configuration-classes/actuator.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/auto-configuration-classes/actuator.adoc new file mode 100644 index 000000000000..517465ea7bf5 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/auto-configuration-classes/actuator.adoc @@ -0,0 +1,5 @@ +[[appendix.auto-configuration-classes.actuator]] +== spring-boot-actuator-autoconfigure +The following auto-configuration classes are from the `spring-boot-actuator-autoconfigure` module: + +include::documented-auto-configuration-classes/spring-boot-actuator-autoconfigure.adoc[] diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/auto-configuration-classes/core.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/auto-configuration-classes/core.adoc new file mode 100644 index 000000000000..0b70c76a13cd --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/auto-configuration-classes/core.adoc @@ -0,0 +1,5 @@ +[[appendix.auto-configuration-classes.core]] +== spring-boot-autoconfigure +The following auto-configuration classes are from the `spring-boot-autoconfigure` module: + +include::documented-auto-configuration-classes/spring-boot-autoconfigure.adoc[] diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/build-tool-plugins.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/build-tool-plugins.adoc new file mode 100644 index 000000000000..1fff3b6bf05c --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/build-tool-plugins.adoc @@ -0,0 +1,22 @@ +[[build-tool-plugins]] += Build Tool Plugins +include::attributes.adoc[] + + + +Spring Boot provides build tool plugins for Maven and Gradle. +The plugins offer a variety of features, including the packaging of executable jars. +This section provides more details on both plugins as well as some help should you need to extend an unsupported build system. +If you are just getting started, you might want to read "`<>`" from the "`<>`" section first. + + + +include::build-tool-plugins/maven.adoc[] + +include::build-tool-plugins/gradle.adoc[] + +include::build-tool-plugins/antlib.adoc[] + +include::build-tool-plugins/other-build-systems.adoc[] + +include::build-tool-plugins/whats-next.adoc[] diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/build-tool-plugins/antlib.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/build-tool-plugins/antlib.adoc new file mode 100644 index 000000000000..a7be882b6be6 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/build-tool-plugins/antlib.adoc @@ -0,0 +1,148 @@ +[[build-tool-plugins.antlib]] +== Spring Boot AntLib Module +The Spring Boot AntLib module provides basic Spring Boot support for Apache Ant. +You can use the module to create executable jars. +To use the module, you need to declare an additional `spring-boot` namespace in your `build.xml`, as shown in the following example: + +[source,xml,indent=0,subs="verbatim"] +---- + + ... + +---- + +You need to remember to start Ant using the `-lib` option, as shown in the following example: + +[source,shell,indent=0,subs="verbatim,attributes"] +---- + $ ant -lib +---- + +TIP: The "`Using Spring Boot`" section includes a more complete example of <>. + + + +[[build-tool-plugins.antlib.tasks]] +=== Spring Boot Ant Tasks +Once the `spring-boot-antlib` namespace has been declared, the following additional tasks are available: + +* <> +* <> + + + +[[build-tool-plugins.antlib.tasks.exejar]] +==== Using the "`exejar`" Task +You can use the `exejar` task to create a Spring Boot executable jar. +The following attributes are supported by the task: + +[cols="1,2,2"] +|==== +| Attribute | Description | Required + +| `destfile` +| The destination jar file to create +| Yes + +| `classes` +| The root directory of Java class files +| Yes + +| `start-class` +| The main application class to run +| No _(the default is the first class found that declares a `main` method)_ +|==== + +The following nested elements can be used with the task: + +[cols="1,4"] +|==== +| Element | Description + +| `resources` +| One or more {ant-docs}/Types/resources.html#collection[Resource Collections] describing a set of {ant-docs}/Types/resources.html[Resources] that should be added to the content of the created +jar+ file. + +| `lib` +| One or more {ant-docs}/Types/resources.html#collection[Resource Collections] that should be added to the set of jar libraries that make up the runtime dependency classpath of the application. +|==== + + + +[[build-tool-plugins.antlib.tasks.examples]] +==== Examples +This section shows two examples of Ant tasks. + +.Specify +start-class+ +[source,xml,indent=0,subs="verbatim"] +---- + + + + + + + + +---- + +.Detect +start-class+ +[source,xml,indent=0,subs="verbatim"] +---- + + + + + +---- + + + +[[build-tool-plugins.antlib.findmainclass]] +=== Using the "`findmainclass`" Task +The `findmainclass` task is used internally by `exejar` to locate a class declaring a `main`. +If necessary, you can also use this task directly in your build. +The following attributes are supported: + +[cols="1,2,2"] +|==== +| Attribute | Description | Required + +| `classesroot` +| The root directory of Java class files +| Yes _(unless `mainclass` is specified)_ + +| `mainclass` +| Can be used to short-circuit the `main` class search +| No + +| `property` +| The Ant property that should be set with the result +| No _(result will be logged if unspecified)_ +|==== + + + +[[build-tool-plugins.antlib.findmainclass.examples]] +==== Examples +This section contains three examples of using `findmainclass`. + +.Find and log +[source,xml,indent=0,subs="verbatim"] +---- + +---- + +.Find and set +[source,xml,indent=0,subs="verbatim"] +---- + +---- + +.Override and set +[source,xml,indent=0,subs="verbatim"] +---- + +---- diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/build-tool-plugins/gradle.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/build-tool-plugins/gradle.adoc new file mode 100644 index 000000000000..0281939c142f --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/build-tool-plugins/gradle.adoc @@ -0,0 +1,8 @@ +[[build-tool-plugins.gradle]] +== Spring Boot Gradle Plugin +The Spring Boot Gradle Plugin provides Spring Boot support in Gradle, letting you package executable jar or war archives, run Spring Boot applications, and use the dependency management provided by `spring-boot-dependencies`. +It requires Gradle 6.8, 6.9, or 7.x. +Please refer to the plugin's documentation to learn more: + +* Reference ({spring-boot-gradle-plugin-docs}[HTML] and {spring-boot-gradle-plugin-pdfdocs}[PDF]) +* {spring-boot-gradle-plugin-api}[API] diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/build-tool-plugins/maven.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/build-tool-plugins/maven.adoc new file mode 100644 index 000000000000..452f039101df --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/build-tool-plugins/maven.adoc @@ -0,0 +1,9 @@ +[[build-tool-plugins.maven]] +== Spring Boot Maven Plugin +The Spring Boot Maven Plugin provides Spring Boot support in Maven, letting you package executable jar or war archives and run an application "`in-place`". +To use it, you must use Maven 3.2 (or later). + +Please refer to the plugin's documentation to learn more: + +* Reference ({spring-boot-maven-plugin-docs}[HTML] and {spring-boot-maven-plugin-pdfdocs}[PDF]) +* {spring-boot-maven-plugin-api}[API] diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/build-tool-plugins/other-build-systems.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/build-tool-plugins/other-build-systems.adoc new file mode 100644 index 000000000000..bc560fc168f2 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/build-tool-plugins/other-build-systems.adoc @@ -0,0 +1,43 @@ +[[build-tool-plugins.other-build-systems]] +== Supporting Other Build Systems +If you want to use a build tool other than Maven, Gradle, or Ant, you likely need to develop your own plugin. +Executable jars need to follow a specific format and certain entries need to be written in an uncompressed form (see the "`<>`" section in the appendix for details). + +The Spring Boot Maven and Gradle plugins both make use of `spring-boot-loader-tools` to actually generate jars. +If you need to, you may use this library directly. + + + +[[build-tool-plugins.other-build-systems.repackaging-archives]] +=== Repackaging Archives +To repackage an existing archive so that it becomes a self-contained executable archive, use `org.springframework.boot.loader.tools.Repackager`. +The `Repackager` class takes a single constructor argument that refers to an existing jar or war archive. +Use one of the two available `repackage()` methods to either replace the original file or write to a new destination. +Various settings can also be configured on the repackager before it is run. + + + +[[build-tool-plugins.other-build-systems.nested-libraries]] +=== Nested Libraries +When repackaging an archive, you can include references to dependency files by using the `org.springframework.boot.loader.tools.Libraries` interface. +We do not provide any concrete implementations of `Libraries` here as they are usually build-system-specific. + +If your archive already includes libraries, you can use `Libraries.NONE`. + + + +[[build-tool-plugins.other-build-systems.finding-main-class]] +=== Finding a Main Class +If you do not use `Repackager.setMainClass()` to specify a main class, the repackager uses https://asm.ow2.io/[ASM] to read class files and tries to find a suitable class with a `public static void main(String[] args)` method. +An exception is thrown if more than one candidate is found. + + + +[[build-tool-plugins.other-build-systems.example-repackage-implementation]] +=== Example Repackage Implementation +The following example shows a typical repackage implementation: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/buildtoolplugins/otherbuildsystems/examplerepackageimplementation/MyBuildTool.java[] +---- diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/build-tool-plugins/whats-next.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/build-tool-plugins/whats-next.adoc new file mode 100644 index 000000000000..8bd05a7dba8e --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/build-tool-plugins/whats-next.adoc @@ -0,0 +1,6 @@ +[[build-tool-plugins.whats-next]] +== What to Read Next +If you are interested in how the build tool plugins work, you can look at the {spring-boot-code}/spring-boot-project/spring-boot-tools[`spring-boot-tools`] module on GitHub. +More technical details of the executable jar format are covered in <>. + +If you have specific build-related questions, you can check out the "`<>`" guides. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/cli.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/cli.adoc new file mode 100644 index 000000000000..e573303d9c67 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/cli.adoc @@ -0,0 +1,20 @@ +[[cli]] += Spring Boot CLI +include::attributes.adoc[] + + +The Spring Boot CLI is a command line tool that you can use if you want to quickly develop a Spring application. +It lets you run Groovy scripts, which means that you have a familiar Java-like syntax without so much boilerplate code. +You can also bootstrap a new project or write your own command for it. + + + +include::cli/installation.adoc[] + +include::cli/using-the-cli.adoc[] + +include::cli/groovy-beans-dsl.adoc[] + +include::cli/maven-setting.adoc[] + +include::cli/whats-next.adoc[] diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/cli/groovy-beans-dsl.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/cli/groovy-beans-dsl.adoc new file mode 100644 index 000000000000..b523e3d30b1c --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/cli/groovy-beans-dsl.adoc @@ -0,0 +1,30 @@ +[[cli.groovy-beans-dsl]] +== Developing Applications with the Groovy Beans DSL +Spring Framework 4.0 has native support for a `beans{}` "`DSL`" (borrowed from https://grails.org/[Grails]), and you can embed bean definitions in your Groovy application scripts by using the same format. +This is sometimes a good way to include external features like middleware declarations, as shown in the following example: + +[source,groovy,pending-extract=true,indent=0,subs="verbatim"] +---- + @Configuration(proxyBeanMethods = false) + class Application implements CommandLineRunner { + + @Autowired + SharedService service + + @Override + void run(String... args) { + println service.message + } + + } + + import my.company.SharedService + + beans { + service(SharedService) { + message = "Hello World" + } + } +---- + +You can mix class declarations with `beans{}` in the same file as long as they stay at the top level, or, if you prefer, you can put the beans DSL in a separate file. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/cli/installation.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/cli/installation.adoc new file mode 100644 index 000000000000..e4c1cdd9b1d9 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/cli/installation.adoc @@ -0,0 +1,4 @@ +[[cli.installation]] +== Installing the CLI +The Spring Boot CLI (Command-Line Interface) can be installed manually by using SDKMAN! (the SDK Manager) or by using Homebrew or MacPorts if you are an OSX user. +See _<>_ in the "`Getting started`" section for comprehensive installation instructions. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/cli/maven-setting.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/cli/maven-setting.adoc new file mode 100644 index 000000000000..6be011286e47 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/cli/maven-setting.adoc @@ -0,0 +1,16 @@ +[[cli.maven-setting]] +== Configuring the CLI with settings.xml +The Spring Boot CLI uses Maven Resolver, Maven's dependency resolution engine, to resolve dependencies. +The CLI makes use of the Maven configuration found in `~/.m2/settings.xml` to configure Maven Resolver. +The following configuration settings are honored by the CLI: + +* Offline +* Mirrors +* Servers +* Proxies +* Profiles +** Activation +** Repositories +* Active profiles + +See https://maven.apache.org/settings.html[Maven's settings documentation] for further information. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/cli/using-the-cli.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/cli/using-the-cli.adoc new file mode 100644 index 000000000000..066472a4767f --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/cli/using-the-cli.adoc @@ -0,0 +1,347 @@ +[[cli.using-the-cli]] +== Using the CLI +Once you have installed the CLI, you can run it by typing `spring` and pressing Enter at the command line. +If you run `spring` without any arguments, a help screen is displayed, as follows: + +[source,shell,indent=0,subs="verbatim"] +---- + $ spring + usage: spring [--help] [--version] + [] + + Available commands are: + + run [options] [--] [args] + Run a spring groovy script + + _... more command help is shown here_ +---- + +You can type `spring help` to get more details about any of the supported commands, as shown in the following example: + +[source,shell,indent=0,subs="verbatim"] +---- + $ spring help run + spring run - Run a spring groovy script + + usage: spring run [options] [--] [args] + + Option Description + ------ ----------- + --autoconfigure [Boolean] Add autoconfigure compiler + transformations (default: true) + --classpath, -cp Additional classpath entries + --no-guess-dependencies Do not attempt to guess dependencies + --no-guess-imports Do not attempt to guess imports + -q, --quiet Quiet logging + -v, --verbose Verbose logging of dependency + resolution + --watch Watch the specified file for changes +---- + +The `version` command provides a quick way to check which version of Spring Boot you are using, as follows: + +[source,shell,indent=0,subs="verbatim,attributes"] +---- + $ spring version + Spring CLI v{spring-boot-version} +---- + + + +[[cli.using-the-cli.run]] +=== Running Applications with the CLI +You can compile and run Groovy source code by using the `run` command. +The Spring Boot CLI is completely self-contained, so you do not need any external Groovy installation. + +The following example shows a "`hello world`" web application written in Groovy: + +.hello.groovy +[source,groovy,indent=0,subs="verbatim"] +---- +include::{docs-groovy}/cli/usingthecli/run/WebApplication.groovy[tag=*] +---- + +To compile and run the application, type the following command: + +[source,shell,indent=0,subs="verbatim"] +---- + $ spring run hello.groovy +---- + +To pass command-line arguments to the application, use `--` to separate the commands from the "`spring`" command arguments, as shown in the following example: + +[source,shell,indent=0,subs="verbatim"] +---- + $ spring run hello.groovy -- --server.port=9000 +---- + +To set JVM command line arguments, you can use the `JAVA_OPTS` environment variable, as shown in the following example: + +[source,shell,indent=0,subs="verbatim"] +---- + $ JAVA_OPTS=-Xmx1024m spring run hello.groovy +---- + +NOTE: When setting `JAVA_OPTS` on Microsoft Windows, make sure to quote the entire instruction, such as `set "JAVA_OPTS=-Xms256m -Xmx2048m"`. +Doing so ensures the values are properly passed to the process. + + + +[[cli.using-the-cli.run.deduced-grab-annotations]] +==== Deduced "`grab`" Dependencies +Standard Groovy includes a `@Grab` annotation, which lets you declare dependencies on third-party libraries. +This useful technique lets Groovy download jars in the same way as Maven or Gradle would but without requiring you to use a build tool. + +Spring Boot extends this technique further and tries to deduce which libraries to "`grab`" based on your code. +For example, since the `WebApplication` code shown previously uses `@RestController` annotations, Spring Boot grabs "Tomcat" and "Spring MVC". + +The following items are used as "`grab hints`": + +|=== +| Items | Grabs + +| `JdbcTemplate`, `NamedParameterJdbcTemplate`, `DataSource` +| JDBC Application. + +| `@EnableJms` +| JMS Application. + +| `@EnableCaching` +| Caching abstraction. + +| `@Test` +| JUnit. + +| `@EnableRabbit` +| RabbitMQ. + +| extends `Specification` +| Spock test. + +| `@EnableBatchProcessing` +| Spring Batch. + +| `@MessageEndpoint` `@EnableIntegration` +| Spring Integration. + +| `@Controller` `@RestController` `@EnableWebMvc` +| Spring MVC + Embedded Tomcat. + +| `@EnableWebSecurity` +| Spring Security. + +| `@EnableTransactionManagement` +| Spring Transaction Management. +|=== + +TIP: See subclasses of {spring-boot-cli-module-code}/compiler/CompilerAutoConfiguration.java[`CompilerAutoConfiguration`] in the Spring Boot CLI source code to understand exactly how customizations are applied. + + + +[[cli.using-the-cli.run.deduced-grab-coordinates]] +==== Deduced "`grab`" Coordinates +Spring Boot extends Groovy's standard `@Grab` support by letting you specify a dependency without a group or version (for example, `@Grab('freemarker')`). +Doing so consults Spring Boot's default dependency metadata to deduce the artifact's group and version. + +NOTE: The default metadata is tied to the version of the CLI that you use. +It changes only when you move to a new version of the CLI, putting you in control of when the versions of your dependencies may change. +A table showing the dependencies and their versions that are included in the default metadata can be found in the <>. + + + +[[cli.using-the-cli.run.default-import-statements]] +==== Default Import Statements +To help reduce the size of your Groovy code, several `import` statements are automatically included. +Notice how the preceding example refers to `@Component`, `@RestController`, and `@RequestMapping` without needing to use fully-qualified names or `import` statements. + +TIP: Many Spring annotations work without using `import` statements. +Try running your application to see what fails before adding imports. + + + +[[cli.using-the-cli.run.automatic-main-method]] +==== Automatic Main Method +Unlike the equivalent Java application, you do not need to include a `public static void main(String[] args)` method with your `Groovy` scripts. +A `SpringApplication` is automatically created, with your compiled code acting as the `source`. + + + +[[cli.using-the-cli.run.custom-dependency-management]] +==== Custom Dependency Management +By default, the CLI uses the dependency management declared in `spring-boot-dependencies` when resolving `@Grab` dependencies. +Additional dependency management, which overrides the default dependency management, can be configured by using the `@DependencyManagementBom` annotation. +The annotation's value should specify the coordinates (`groupId:artifactId:version`) of one or more Maven BOMs. + +For example, consider the following declaration: + +[source,groovy,indent=0,subs="verbatim"] +---- +include::{docs-groovy}/cli/usingthecli/run/customdependencymanagement/single/CustomDependencyManagement.groovy[tag=*] +---- + +The preceding declaration picks up `custom-bom-1.0.0.pom` in a Maven repository under `com/example/custom-versions/1.0.0/`. + +When you specify multiple BOMs, they are applied in the order in which you declare them, as shown in the following example: + +[source,groovy,indent=0,subs="verbatim"] +---- +include::{docs-groovy}/cli/usingthecli/run/customdependencymanagement/multiple/CustomDependencyManagement.groovy[tag=*] +---- + +The preceding example indicates that the dependency management in `another-bom` overrides the dependency management in `custom-bom`. + +You can use `@DependencyManagementBom` anywhere that you can use `@Grab`. +However, to ensure consistent ordering of the dependency management, you can use `@DependencyManagementBom` at most once in your application. + + + +[[cli.using-the-cli.multiple-source-files]] +=== Applications with Multiple Source Files +You can use "`shell globbing`" with all commands that accept file input. +Doing so lets you use multiple files from a single directory, as shown in the following example: + +[source,shell,indent=0,subs="verbatim"] +---- + $ spring run *.groovy +---- + + + +[[cli.using-the-cli.packaging]] +=== Packaging Your Application +You can use the `jar` command to package your application into a self-contained executable jar file, as shown in the following example: + +[source,shell,indent=0,subs="verbatim"] +---- + $ spring jar my-app.jar *.groovy +---- + +The resulting jar contains the classes produced by compiling the application and all of the application's dependencies so that it can then be run by using `java -jar`. +The jar file also contains entries from the application's classpath. +You can add and remove explicit paths to the jar by using `--include` and `--exclude`. +Both are comma-separated, and both accept prefixes, in the form of "`+`" and "`-`", to signify that they should be removed from the defaults. +The default includes are as follows: + +[indent=0] +---- + public/**, resources/**, static/**, templates/**, META-INF/**, * +---- + +The default excludes are as follows: + +[indent=0] +---- + .*, repository/**, build/**, target/**, **/*.jar, **/*.groovy +---- + +Type `spring help jar` on the command line for more information. + + + +[[cli.using-the-cli.initialize-new-project]] +=== Initialize a New Project +The `init` command lets you create a new project by using https://start.spring.io without leaving the shell, as shown in the following example: + +[source,shell,indent=0,subs="verbatim"] +---- + $ spring init --dependencies=web,data-jpa my-project + Using service at https://start.spring.io + Project extracted to '/Users/developer/example/my-project' +---- + +The preceding example creates a `my-project` directory with a Maven-based project that uses `spring-boot-starter-web` and `spring-boot-starter-data-jpa`. +You can list the capabilities of the service by using the `--list` flag, as shown in the following example: + +[source,shell,indent=0,subs="verbatim"] +---- + $ spring init --list + ======================================= + Capabilities of https://start.spring.io + ======================================= + + Available dependencies: + ----------------------- + actuator - Actuator: Production ready features to help you monitor and manage your application + ... + web - Web: Support for full-stack web development, including Tomcat and spring-webmvc + websocket - Websocket: Support for WebSocket development + ws - WS: Support for Spring Web Services + + Available project types: + ------------------------ + gradle-build - Gradle Config [format:build, build:gradle] + gradle-project - Gradle Project [format:project, build:gradle] + maven-build - Maven POM [format:build, build:maven] + maven-project - Maven Project [format:project, build:maven] (default) + + ... +---- + +The `init` command supports many options. +See the `help` output for more details. +For instance, the following command creates a Gradle project that uses Java 8 and `war` packaging: + +[source,shell,indent=0,subs="verbatim"] +---- + $ spring init --build=gradle --java-version=1.8 --dependencies=websocket --packaging=war sample-app.zip + Using service at https://start.spring.io + Content saved to 'sample-app.zip' +---- + + + +[[cli.using-the-cli.embedded-shell]] +=== Using the Embedded Shell +Spring Boot includes command-line completion scripts for the BASH and zsh shells. +If you do not use either of these shells (perhaps you are a Windows user), you can use the `shell` command to launch an integrated shell, as shown in the following example: + +[source,shell,indent=0,subs="verbatim,quotes,attributes"] +---- + $ spring shell + *Spring Boot* (v{spring-boot-version}) + Hit TAB to complete. Type \'help' and hit RETURN for help, and \'exit' to quit. +---- + +From inside the embedded shell, you can run other commands directly: + +[source,shell,indent=0,subs="verbatim,attributes"] +---- + $ version + Spring CLI v{spring-boot-version} +---- + +The embedded shell supports ANSI color output as well as `tab` completion. +If you need to run a native command, you can use the `!` prefix. +To exit the embedded shell, press `ctrl-c`. + + + +[[cli.using-the-cli.extensions]] +=== Adding Extensions to the CLI +You can add extensions to the CLI by using the `install` command. +The command takes one or more sets of artifact coordinates in the format `group:artifact:version`, as shown in the following example: + +[source,shell,indent=0,subs="verbatim"] +---- + $ spring install com.example:spring-boot-cli-extension:1.0.0.RELEASE +---- + +In addition to installing the artifacts identified by the coordinates you supply, all of the artifacts' dependencies are also installed. + +To uninstall a dependency, use the `uninstall` command. +As with the `install` command, it takes one or more sets of artifact coordinates in the format of `group:artifact:version`, as shown in the following example: + +[source,shell,indent=0,subs="verbatim"] +---- + $ spring uninstall com.example:spring-boot-cli-extension:1.0.0.RELEASE +---- + +It uninstalls the artifacts identified by the coordinates you supply and their dependencies. + +To uninstall all additional dependencies, you can use the `--all` option, as shown in the following example: + +[source,shell,indent=0,subs="verbatim"] +---- + $ spring uninstall --all +---- diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/cli/whats-next.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/cli/whats-next.adoc new file mode 100644 index 000000000000..f3d05acbfeda --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/cli/whats-next.adoc @@ -0,0 +1,7 @@ +[[cli.whats-next]] +== What to Read Next +There are some {spring-boot-code}/spring-boot-project/spring-boot-cli/samples[sample groovy scripts] available from the GitHub repository that you can use to try out the Spring Boot CLI. +There is also extensive Javadoc throughout the {spring-boot-cli-module-code}[source code]. + +If you find that you reach the limit of the CLI tool, you probably want to look at converting your application to a full Gradle or Maven built "`Groovy project`". +The next section covers Spring Boot's "<>", which you can use with Gradle or Maven. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/configuration-metadata.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/configuration-metadata.adoc new file mode 100644 index 000000000000..5a5fb7b43544 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/configuration-metadata.adoc @@ -0,0 +1,20 @@ +[appendix] +[[appendix.configuration-metadata]] += Configuration Metadata +include::attributes.adoc[] + + + +Spring Boot jars include metadata files that provide details of all supported configuration properties. +The files are designed to let IDE developers offer contextual help and "`code completion`" as users are working with `application.properties` or `application.yml` files. + +The majority of the metadata file is generated automatically at compile time by processing all items annotated with `@ConfigurationProperties`. +However, it is possible to <> for corner cases or more advanced use cases. + + + +include::configuration-metadata/format.adoc[] + +include::configuration-metadata/manual-hints.adoc[] + +include::configuration-metadata/annotation-processor.adoc[] diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/configuration-metadata/annotation-processor.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/configuration-metadata/annotation-processor.adoc new file mode 100644 index 000000000000..6aa025bf7347 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/configuration-metadata/annotation-processor.adoc @@ -0,0 +1,145 @@ +[[appendix.configuration-metadata.annotation-processor]] +== Generating Your Own Metadata by Using the Annotation Processor +You can easily generate your own configuration metadata file from items annotated with `@ConfigurationProperties` by using the `spring-boot-configuration-processor` jar. +The jar includes a Java annotation processor which is invoked as your project is compiled. + + + +[[appendix.configuration-metadata.annotation-processor.configuring]] +=== Configuring the Annotation Processor +To use the processor, include a dependency on `spring-boot-configuration-processor`. + +With Maven the dependency should be declared as optional, as shown in the following example: + +[source,xml,indent=0,subs="verbatim"] +---- + + org.springframework.boot + spring-boot-configuration-processor + true + +---- + +With Gradle, the dependency should be declared in the `annotationProcessor` configuration, as shown in the following example: + +[source,gradle,indent=0,subs="verbatim"] +---- + dependencies { + annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" + } +---- + +If you are using an `additional-spring-configuration-metadata.json` file, the `compileJava` task should be configured to depend on the `processResources` task, as shown in the following example: + +[source,gradle,indent=0,subs="verbatim"] +---- + tasks.named('compileJava') { + inputs.files(tasks.named('processResources')) + } +---- + +This dependency ensures that the additional metadata is available when the annotation processor runs during compilation. + +[NOTE] +==== +If you are using AspectJ in your project, you need to make sure that the annotation processor runs only once. +There are several ways to do this. +With Maven, you can configure the `maven-apt-plugin` explicitly and add the dependency to the annotation processor only there. +You could also let the AspectJ plugin run all the processing and disable annotation processing in the `maven-compiler-plugin` configuration, as follows: + +[source,xml,indent=0,subs="verbatim"] +---- + + org.apache.maven.plugins + maven-compiler-plugin + + none + + +---- +==== + + + +[[appendix.configuration-metadata.annotation-processor.automatic-metadata-generation]] +=== Automatic Metadata Generation +The processor picks up both classes and methods that are annotated with `@ConfigurationProperties`. + +If the class is also annotated with `@ConstructorBinding`, a single constructor is expected and one property is created per constructor parameter. +Otherwise, properties are discovered through the presence of standard getters and setters with special handling for collection and map types (that is detected even if only a getter is present). +The annotation processor also supports the use of the `@Data`, `@Getter`, and `@Setter` lombok annotations. + +Consider the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/configurationmetadata/annotationprocessor/automaticmetadatageneration/MyServerProperties.java[] +---- + +This exposes three properties where `my.server.name` has no default and `my.server.ip` and `my.server.port` defaults to `"127.0.0.1"` and `9797` respectively. +The Javadoc on fields is used to populate the `description` attribute. For instance, the description of `my.server.ip` is "IP address to listen to.". + +NOTE: You should only use plain text with `@ConfigurationProperties` field Javadoc, since they are not processed before being added to the JSON. + +The annotation processor applies a number of heuristics to extract the default value from the source model. +Default values have to be provided statically. In particular, do not refer to a constant defined in another class. +Also, the annotation processor cannot auto-detect default values for ``Enum``s and ``Collections``s. + +For cases where the default value could not be detected, <> should be provided. +Consider the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/configurationmetadata/annotationprocessor/automaticmetadatageneration/MyMessagingProperties.java[] +---- + +In order to document default values for properties in the class above, you could add the following content to <>: + +[source,json,indent=0,subs="verbatim"] +---- + {"properties": [ + { + "name": "my.messaging.addresses", + "defaultValue": ["a", "b"] + }, + { + "name": "my.messaging.container-type", + "defaultValue": "simple" + } + ]} +---- + +NOTE: Only the `name` of the property is required to document additional metadata for existing properties. + + + +[[appendix.configuration-metadata.annotation-processor.automatic-metadata-generation.nested-properties]] +==== Nested Properties +The annotation processor automatically considers inner classes as nested properties. +Rather than documenting the `ip` and `port` at the root of the namespace, we could create a sub-namespace for it. +Consider the updated example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/configurationmetadata/annotationprocessor/automaticmetadatageneration/nestedproperties/MyServerProperties.java[] +---- + +The preceding example produces metadata information for `my.server.name`, `my.server.host.ip`, and `my.server.host.port` properties. +You can use the `@NestedConfigurationProperty` annotation on a field to indicate that a regular (non-inner) class should be treated as if it were nested. + +TIP: This has no effect on collections and maps, as those types are automatically identified, and a single metadata property is generated for each of them. + + + +[[appendix.configuration-metadata.annotation-processor.adding-additional-metadata]] +=== Adding Additional Metadata +Spring Boot's configuration file handling is quite flexible, and it is often the case that properties may exist that are not bound to a `@ConfigurationProperties` bean. +You may also need to tune some attributes of an existing key. +To support such cases and let you provide custom "hints", the annotation processor automatically merges items from `META-INF/additional-spring-configuration-metadata.json` into the main metadata file. + +If you refer to a property that has been detected automatically, the description, default value, and deprecation information are overridden, if specified. +If the manual property declaration is not identified in the current module, it is added as a new property. + +The format of the `additional-spring-configuration-metadata.json` file is exactly the same as the regular `spring-configuration-metadata.json`. +The additional properties file is optional. +If you do not have any additional properties, do not add the file. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/configuration-metadata/format.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/configuration-metadata/format.adoc new file mode 100644 index 000000000000..ebc69d5089a5 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/configuration-metadata/format.adoc @@ -0,0 +1,293 @@ +[[appendix.configuration-metadata.format]] +== Metadata Format +Configuration metadata files are located inside jars under `META-INF/spring-configuration-metadata.json`. +They use a JSON format with items categorized under either "`groups`" or "`properties`" and additional values hints categorized under "hints", as shown in the following example: + +[source,json,indent=0,subs="verbatim"] +---- + {"groups": [ + { + "name": "server", + "type": "org.springframework.boot.autoconfigure.web.ServerProperties", + "sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties" + }, + { + "name": "spring.jpa.hibernate", + "type": "org.springframework.boot.autoconfigure.orm.jpa.JpaProperties$Hibernate", + "sourceType": "org.springframework.boot.autoconfigure.orm.jpa.JpaProperties", + "sourceMethod": "getHibernate()" + } + ... + ],"properties": [ + { + "name": "server.port", + "type": "java.lang.Integer", + "sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties" + }, + { + "name": "server.address", + "type": "java.net.InetAddress", + "sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties" + }, + { + "name": "spring.jpa.hibernate.ddl-auto", + "type": "java.lang.String", + "description": "DDL mode. This is actually a shortcut for the \"hibernate.hbm2ddl.auto\" property.", + "sourceType": "org.springframework.boot.autoconfigure.orm.jpa.JpaProperties$Hibernate" + } + ... + ],"hints": [ + { + "name": "spring.jpa.hibernate.ddl-auto", + "values": [ + { + "value": "none", + "description": "Disable DDL handling." + }, + { + "value": "validate", + "description": "Validate the schema, make no changes to the database." + }, + { + "value": "update", + "description": "Update the schema if necessary." + }, + { + "value": "create", + "description": "Create the schema and destroy previous data." + }, + { + "value": "create-drop", + "description": "Create and then destroy the schema at the end of the session." + } + ] + } + ]} +---- + +Each "`property`" is a configuration item that the user specifies with a given value. +For example, `server.port` and `server.address` might be specified in your `application.properties`/`application.yaml`, as follows: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + server: + port: 9090 + address: 127.0.0.1 +---- + +The "`groups`" are higher level items that do not themselves specify a value but instead provide a contextual grouping for properties. +For example, the `server.port` and `server.address` properties are part of the `server` group. + +NOTE: It is not required that every "`property`" has a "`group`". +Some properties might exist in their own right. + +Finally, "`hints`" are additional information used to assist the user in configuring a given property. +For example, when a developer is configuring the configprop:spring.jpa.hibernate.ddl-auto[] property, a tool can use the hints to offer some auto-completion help for the `none`, `validate`, `update`, `create`, and `create-drop` values. + + + +[[appendix.configuration-metadata.format.group]] +=== Group Attributes +The JSON object contained in the `groups` array can contain the attributes shown in the following table: + +[cols="1,1,4"] +|=== +| Name | Type | Purpose + +| `name` +| String +| The full name of the group. + This attribute is mandatory. + +| `type` +| String +| The class name of the data type of the group. + For example, if the group were based on a class annotated with `@ConfigurationProperties`, the attribute would contain the fully qualified name of that class. + If it were based on a `@Bean` method, it would be the return type of that method. + If the type is not known, the attribute may be omitted. + +| `description` +| String +| A short description of the group that can be displayed to users. + If no description is available, it may be omitted. + It is recommended that descriptions be short paragraphs, with the first line providing a concise summary. + The last line in the description should end with a period (`.`). + +| `sourceType` +| String +| The class name of the source that contributed this group. + For example, if the group were based on a `@Bean` method annotated with `@ConfigurationProperties`, this attribute would contain the fully qualified name of the `@Configuration` class that contains the method. + If the source type is not known, the attribute may be omitted. + +| `sourceMethod` +| String +| The full name of the method (include parenthesis and argument types) that contributed this group (for example, the name of a `@ConfigurationProperties` annotated `@Bean` method). + If the source method is not known, it may be omitted. +|=== + + + +[[appendix.configuration-metadata.format.property]] +=== Property Attributes +The JSON object contained in the `properties` array can contain the attributes described in the following table: + +[cols="1,1,4"] +|=== +| Name | Type | Purpose + +| `name` +| String +| The full name of the property. + Names are in lower-case period-separated form (for example, `server.address`). + This attribute is mandatory. + +| `type` +| String +| The full signature of the data type of the property (for example, `java.lang.String`) but also a full generic type (such as `java.util.Map`). + You can use this attribute to guide the user as to the types of values that they can enter. + For consistency, the type of a primitive is specified by using its wrapper counterpart (for example, `boolean` becomes `java.lang.Boolean`). + Note that this class may be a complex type that gets converted from a `String` as values are bound. + If the type is not known, it may be omitted. + +| `description` +| String +| A short description of the property that can be displayed to users. + If no description is available, it may be omitted. + It is recommended that descriptions be short paragraphs, with the first line providing a concise summary. + The last line in the description should end with a period (`.`). + +| `sourceType` +| String +| The class name of the source that contributed this property. + For example, if the property were from a class annotated with `@ConfigurationProperties`, this attribute would contain the fully qualified name of that class. + If the source type is unknown, it may be omitted. + +| `defaultValue` +| Object +| The default value, which is used if the property is not specified. + If the type of the property is an array, it can be an array of value(s). + If the default value is unknown, it may be omitted. + +| `deprecation` +| Deprecation +| Specify whether the property is deprecated. + If the field is not deprecated or if that information is not known, it may be omitted. + The next table offers more detail about the `deprecation` attribute. +|=== + +The JSON object contained in the `deprecation` attribute of each `properties` element can contain the following attributes: + +[cols="1,1,4"] +|=== +| Name | Type | Purpose + +| `level` +| String +| The level of deprecation, which can be either `warning` (the default) or `error`. + When a property has a `warning` deprecation level, it should still be bound in the environment. + However, when it has an `error` deprecation level, the property is no longer managed and is not bound. + +| `reason` +| String +| A short description of the reason why the property was deprecated. + If no reason is available, it may be omitted. + It is recommended that descriptions be short paragraphs, with the first line providing a concise summary. + The last line in the description should end with a period (`.`). + +| `replacement` +| String +| The full name of the property that _replaces_ this deprecated property. + If there is no replacement for this property, it may be omitted. +|=== + +NOTE: Prior to Spring Boot 1.3, a single `deprecated` boolean attribute can be used instead of the `deprecation` element. +This is still supported in a deprecated fashion and should no longer be used. +If no reason and replacement are available, an empty `deprecation` object should be set. + +Deprecation can also be specified declaratively in code by adding the `@DeprecatedConfigurationProperty` annotation to the getter exposing the deprecated property. +For instance, assume that the `my.app.target` property was confusing and was renamed to `my.app.name`. +The following example shows how to handle that situation: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/configurationmetadata/format/group/MyProperties.java[] +---- + +NOTE: There is no way to set a `level`. +`warning` is always assumed, since code is still handling the property. + +The preceding code makes sure that the deprecated property still works (delegating to the `name` property behind the scenes). +Once the `getTarget` and `setTarget` methods can be removed from your public API, the automatic deprecation hint in the metadata goes away as well. +If you want to keep a hint, adding manual metadata with an `error` deprecation level ensures that users are still informed about that property. +Doing so is particularly useful when a `replacement` is provided. + + + +[[appendix.configuration-metadata.format.hints]] +=== Hint Attributes +The JSON object contained in the `hints` array can contain the attributes shown in the following table: + +[cols="1,1,4"] +|=== +| Name | Type | Purpose + +| `name` +| String +| The full name of the property to which this hint refers. + Names are in lower-case period-separated form (such as `spring.mvc.servlet.path`). + If the property refers to a map (such as `system.contexts`), the hint either applies to the _keys_ of the map (`system.contexts.keys`) or the _values_ (`system.contexts.values`) of the map. + This attribute is mandatory. + +| `values` +| ValueHint[] +| A list of valid values as defined by the `ValueHint` object (described in the next table). + Each entry defines the value and may have a description. + +| `providers` +| ValueProvider[] +| A list of providers as defined by the `ValueProvider` object (described later in this document). + Each entry defines the name of the provider and its parameters, if any. +|=== + +The JSON object contained in the `values` attribute of each `hint` element can contain the attributes described in the following table: + +[cols="1,1,4"] +|=== +| Name | Type | Purpose + +| `value` +| Object +| A valid value for the element to which the hint refers. + If the type of the property is an array, it can also be an array of value(s). + This attribute is mandatory. + +| `description` +| String +| A short description of the value that can be displayed to users. + If no description is available, it may be omitted. + It is recommended that descriptions be short paragraphs, with the first line providing a concise summary. + The last line in the description should end with a period (`.`). +|=== + +The JSON object contained in the `providers` attribute of each `hint` element can contain the attributes described in the following table: + +[cols="1,1,4"] +|=== +|Name | Type |Purpose + +| `name` +| String +| The name of the provider to use to offer additional content assistance for the element to which the hint refers. + +| `parameters` +| JSON object +| Any additional parameter that the provider supports (check the documentation of the provider for more details). +|=== + + + +[[appendix.configuration-metadata.format.repeated-items]] +=== Repeated Metadata Items +Objects with the same "`property`" and "`group`" name can appear multiple times within a metadata file. +For example, you could bind two separate classes to the same prefix, with each having potentially overlapping property names. +While the same names appearing in the metadata multiple times should not be common, consumers of metadata should take care to ensure that they support it. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/configuration-metadata/manual-hints.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/configuration-metadata/manual-hints.adoc new file mode 100644 index 000000000000..1b4013c01adf --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/configuration-metadata/manual-hints.adoc @@ -0,0 +1,369 @@ +[[appendix.configuration-metadata.manual-hints]] +== Providing Manual Hints +To improve the user experience and further assist the user in configuring a given property, you can provide additional metadata that: + +* Describes the list of potential values for a property. +* Associates a provider, to attach a well defined semantic to a property, so that a tool can discover the list of potential values based on the project's context. + + + +[[appendix.configuration-metadata.manual-hints.value-hint]] +=== Value Hint +The `name` attribute of each hint refers to the `name` of a property. +In the <>, we provide five values for the `spring.jpa.hibernate.ddl-auto` property: `none`, `validate`, `update`, `create`, and `create-drop`. +Each value may have a description as well. + +If your property is of type `Map`, you can provide hints for both the keys and the values (but not for the map itself). +The special `.keys` and `.values` suffixes must refer to the keys and the values, respectively. + +Assume a `my.contexts` maps magic `String` values to an integer, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/configurationmetadata/manualhints/valuehint/MyProperties.java[] +---- + +The magic values are (in this example) are `sample1` and `sample2`. +In order to offer additional content assistance for the keys, you could add the following JSON to <>: + +[source,json,indent=0,subs="verbatim"] +---- + {"hints": [ + { + "name": "my.contexts.keys", + "values": [ + { + "value": "sample1" + }, + { + "value": "sample2" + } + ] + } + ]} +---- + +TIP: We recommend that you use an `Enum` for those two values instead. +If your IDE supports it, this is by far the most effective approach to auto-completion. + + + +[[appendix.configuration-metadata.manual-hints.value-providers]] +=== Value Providers +Providers are a powerful way to attach semantics to a property. +In this section, we define the official providers that you can use for your own hints. +However, your favorite IDE may implement some of these or none of them. +Also, it could eventually provide its own. + +NOTE: As this is a new feature, IDE vendors must catch up with how it works. +Adoption times naturally vary. + +The following table summarizes the list of supported providers: + +[cols="2,4"] +|=== +| Name | Description + +| `any` +| Permits any additional value to be provided. + +| `class-reference` +| Auto-completes the classes available in the project. + Usually constrained by a base class that is specified by the `target` parameter. + +| `handle-as` +| Handles the property as if it were defined by the type defined by the mandatory `target` parameter. + +| `logger-name` +| Auto-completes valid logger names and <>. + Typically, package and class names available in the current project can be auto-completed as well as defined groups. + +| `spring-bean-reference` +| Auto-completes the available bean names in the current project. + Usually constrained by a base class that is specified by the `target` parameter. + +| `spring-profile-name` +| Auto-completes the available Spring profile names in the project. +|=== + +TIP: Only one provider can be active for a given property, but you can specify several providers if they can all manage the property _in some way_. +Make sure to place the most powerful provider first, as the IDE must use the first one in the JSON section that it can handle. +If no provider for a given property is supported, no special content assistance is provided, either. + + + +[[appendix.configuration-metadata.manual-hints.value-providers.any]] +==== Any +The special **any** provider value permits any additional values to be provided. +Regular value validation based on the property type should be applied if this is supported. + +This provider is typically used if you have a list of values and any extra values should still be considered as valid. + +The following example offers `on` and `off` as auto-completion values for `system.state`: + +[source,json,indent=0,subs="verbatim"] +---- + {"hints": [ + { + "name": "system.state", + "values": [ + { + "value": "on" + }, + { + "value": "off" + } + ], + "providers": [ + { + "name": "any" + } + ] + } + ]} +---- + +Note that, in the preceding example, any other value is also allowed. + + + +[[appendix.configuration-metadata.manual-hints.value-providers.class-reference]] +==== Class Reference +The **class-reference** provider auto-completes classes available in the project. +This provider supports the following parameters: + +[cols="1,1,2,4"] +|=== +| Parameter | Type | Default value | Description + +| `target` +| `String` (`Class`) +| _none_ +| The fully qualified name of the class that should be assignable to the chosen value. + Typically used to filter out-non candidate classes. + Note that this information can be provided by the type itself by exposing a class with the appropriate upper bound. + +| `concrete` +| `boolean` +| true +| Specify whether only concrete classes are to be considered as valid candidates. +|=== + + +The following metadata snippet corresponds to the standard `server.servlet.jsp.class-name` property that defines the `JspServlet` class name to use: + +[source,json,indent=0,subs="verbatim"] +---- + {"hints": [ + { + "name": "server.servlet.jsp.class-name", + "providers": [ + { + "name": "class-reference", + "parameters": { + "target": "javax.servlet.http.HttpServlet" + } + } + ] + } + ]} +---- + + + +[[appendix.configuration-metadata.manual-hints.value-providers.handle-as]] +==== Handle As +The **handle-as** provider lets you substitute the type of the property to a more high-level type. +This typically happens when the property has a `java.lang.String` type, because you do not want your configuration classes to rely on classes that may not be on the classpath. +This provider supports the following parameters: + +[cols="1,1,2,4"] +|=== +| Parameter | Type | Default value | Description + +| **`target`** +| `String` (`Class`) +| _none_ +| The fully qualified name of the type to consider for the property. + This parameter is mandatory. +|=== + +The following types can be used: + +* Any `java.lang.Enum`: Lists the possible values for the property. + (We recommend defining the property with the `Enum` type, as no further hint should be required for the IDE to auto-complete the values) +* `java.nio.charset.Charset`: Supports auto-completion of charset/encoding values (such as `UTF-8`) +* `java.util.Locale`: auto-completion of locales (such as `en_US`) +* `org.springframework.util.MimeType`: Supports auto-completion of content type values (such as `text/plain`) +* `org.springframework.core.io.Resource`: Supports auto-completion of Spring’s Resource abstraction to refer to a file on the filesystem or on the classpath (such as `classpath:/sample.properties`) + +TIP: If multiple values can be provided, use a `Collection` or _Array_ type to teach the IDE about it. + +The following metadata snippet corresponds to the standard `spring.liquibase.change-log` property that defines the path to the changelog to use. +It is actually used internally as a `org.springframework.core.io.Resource` but cannot be exposed as such, because we need to keep the original String value to pass it to the Liquibase API. + +[source,json,indent=0,subs="verbatim"] +---- + {"hints": [ + { + "name": "spring.liquibase.change-log", + "providers": [ + { + "name": "handle-as", + "parameters": { + "target": "org.springframework.core.io.Resource" + } + } + ] + } + ]} +---- + + + +[[appendix.configuration-metadata.manual-hints.value-providers.logger-name]] +==== Logger Name +The **logger-name** provider auto-completes valid logger names and <>. +Typically, package and class names available in the current project can be auto-completed. +If groups are enabled (default) and if a custom logger group is identified in the configuration, auto-completion for it should be provided. +Specific frameworks may have extra magic logger names that can be supported as well. + +This provider supports the following parameters: + +[cols="1,1,2,4"] +|=== +| Parameter | Type | Default value | Description + +| `group` +| `boolean` +| `true` +| Specify whether known groups should be considered. +|=== + +Since a logger name can be any arbitrary name, this provider should allow any value but could highlight valid package and class names that are not available in the project's classpath. + +The following metadata snippet corresponds to the standard `logging.level` property. +Keys are _logger names_, and values correspond to the standard log levels or any custom level. +As Spring Boot defines a few logger groups out-of-the-box, dedicated value hints have been added for those. + +[source,json,indent=0,subs="verbatim"] +---- + {"hints": [ + { + "name": "logging.level.keys", + "values": [ + { + "value": "root", + "description": "Root logger used to assign the default logging level." + }, + { + "value": "sql", + "description": "SQL logging group including Hibernate SQL logger." + }, + { + "value": "web", + "description": "Web logging group including codecs." + } + ], + "providers": [ + { + "name": "logger-name" + } + ] + }, + { + "name": "logging.level.values", + "values": [ + { + "value": "trace" + }, + { + "value": "debug" + }, + { + "value": "info" + }, + { + "value": "warn" + }, + { + "value": "error" + }, + { + "value": "fatal" + }, + { + "value": "off" + } + + ], + "providers": [ + { + "name": "any" + } + ] + } + ]} +---- + + + +[[appendix.configuration-metadata.manual-hints.value-providers.spring-bean-reference]] +==== Spring Bean Reference +The **spring-bean-reference** provider auto-completes the beans that are defined in the configuration of the current project. +This provider supports the following parameters: + +[cols="1,1,2,4"] +|=== +| Parameter | Type | Default value | Description + +| `target` +| `String` (`Class`) +| _none_ +| The fully qualified name of the bean class that should be assignable to the candidate. + Typically used to filter out non-candidate beans. +|=== + +The following metadata snippet corresponds to the standard `spring.jmx.server` property that defines the name of the `MBeanServer` bean to use: + +[source,json,indent=0,subs="verbatim"] +---- + {"hints": [ + { + "name": "spring.jmx.server", + "providers": [ + { + "name": "spring-bean-reference", + "parameters": { + "target": "javax.management.MBeanServer" + } + } + ] + } + ]} +---- + +NOTE: The binder is not aware of the metadata. +If you provide that hint, you still need to transform the bean name into an actual Bean reference using by the `ApplicationContext`. + + + +[[appendix.configuration-metadata.manual-hints.value-providers.spring-profile-name]] +==== Spring Profile Name +The **spring-profile-name** provider auto-completes the Spring profiles that are defined in the configuration of the current project. + +The following metadata snippet corresponds to the standard `spring.profiles.active` property that defines the name of the Spring profile(s) to enable: + +[source,json,indent=0,subs="verbatim"] +---- + {"hints": [ + { + "name": "spring.profiles.active", + "providers": [ + { + "name": "spring-profile-name" + } + ] + } + ]} +---- diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/dependency-versions.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/dependency-versions.adoc new file mode 100644 index 000000000000..c2ca180dea02 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/dependency-versions.adoc @@ -0,0 +1,14 @@ +[appendix] +[[appendix.dependency-versions]] += Dependency Versions +include::attributes.adoc[] + + + +This appendix provides details of the dependencies that are managed by Spring Boot. + + + +include::dependency-versions/coordinates.adoc[] + +include::dependency-versions/properties.adoc[] diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/dependency-versions/coordinates.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/dependency-versions/coordinates.adoc new file mode 100644 index 000000000000..1643a9a6c2dc --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/dependency-versions/coordinates.adoc @@ -0,0 +1,7 @@ +[[appendix.dependency-versions.coordinates]] +== Managed Dependency Coordinates + +The following table provides details of all of the dependency versions that are provided by Spring Boot in its CLI (Command Line Interface), Maven dependency management, and Gradle plugin. +When you declare a dependency on one of these artifacts without declaring a version, the version listed in the table is used. + +include::documented-coordinates.adoc[] diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/dependency-versions/properties.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/dependency-versions/properties.adoc new file mode 100644 index 000000000000..5d8932917647 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/dependency-versions/properties.adoc @@ -0,0 +1,8 @@ +[[appendix.dependency-versions.properties]] +== Version Properties + +The following table provides all properties that can be used to override the versions managed by Spring Boot. +Browse the {spring-boot-code}/spring-boot-project/spring-boot-dependencies/build.gradle[`spring-boot-dependencies` build.gradle] for a complete list of dependencies. +You can learn how to customize these versions in your application in the <>. + +include::documented-properties.adoc[] diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment.adoc new file mode 100644 index 000000000000..9e3736de913f --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment.adoc @@ -0,0 +1,20 @@ +[[deployment]] += Deploying Spring Boot Applications +include::attributes.adoc[] + + + +Spring Boot's flexible packaging options provide a great deal of choice when it comes to deploying your application. +You can deploy Spring Boot applications to a variety of cloud platforms, to container images (such as Docker), or to virtual/real machines. + +This section covers some of the more common deployment scenarios. + + + +include::deployment/containers.adoc[] + +include::deployment/cloud.adoc[] + +include::deployment/installing.adoc[] + +include::deployment/whats-next.adoc[] \ No newline at end of file diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/cloud.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/cloud.adoc new file mode 100644 index 000000000000..6c2dc36341ea --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/cloud.adoc @@ -0,0 +1,418 @@ +[[deployment.cloud]] +== Deploying to the Cloud +Spring Boot's executable jars are ready-made for most popular cloud PaaS (Platform-as-a-Service) providers. +These providers tend to require that you "`bring your own container`". +They manage application processes (not Java applications specifically), so they need an intermediary layer that adapts _your_ application to the _cloud's_ notion of a running process. + +Two popular cloud providers, Heroku and Cloud Foundry, employ a "`buildpack`" approach. +The buildpack wraps your deployed code in whatever is needed to _start_ your application. +It might be a JDK and a call to `java`, an embedded web server, or a full-fledged application server. +A buildpack is pluggable, but ideally you should be able to get by with as few customizations to it as possible. +This reduces the footprint of functionality that is not under your control. +It minimizes divergence between development and production environments. + +Ideally, your application, like a Spring Boot executable jar, has everything that it needs to run packaged within it. + +In this section, we look at what it takes to get the <> in the "`Getting Started`" section up and running in the Cloud. + + + +[[deployment.cloud.cloud-foundry]] +=== Cloud Foundry +Cloud Foundry provides default buildpacks that come into play if no other buildpack is specified. +The Cloud Foundry https://github.com/cloudfoundry/java-buildpack[Java buildpack] has excellent support for Spring applications, including Spring Boot. +You can deploy stand-alone executable jar applications as well as traditional `.war` packaged applications. + +Once you have built your application (by using, for example, `mvn clean package`) and have https://docs.cloudfoundry.org/cf-cli/install-go-cli.html[installed the `cf` command line tool], deploy your application by using the `cf push` command, substituting the path to your compiled `.jar`. +Be sure to have https://docs.cloudfoundry.org/cf-cli/getting-started.html#login[logged in with your `cf` command line client] before pushing an application. +The following line shows using the `cf push` command to deploy an application: + +[source,shell,indent=0,subs="verbatim"] +---- + $ cf push acloudyspringtime -p target/demo-0.0.1-SNAPSHOT.jar +---- + +NOTE: In the preceding example, we substitute `acloudyspringtime` for whatever value you give `cf` as the name of your application. + +See the https://docs.cloudfoundry.org/cf-cli/getting-started.html#push[`cf push` documentation] for more options. +If there is a Cloud Foundry https://docs.cloudfoundry.org/devguide/deploy-apps/manifest.html[`manifest.yml`] file present in the same directory, it is considered. + +At this point, `cf` starts uploading your application, producing output similar to the following example: + +[indent=0,subs="verbatim,quotes"] +---- + Uploading acloudyspringtime... *OK* + Preparing to start acloudyspringtime... *OK* + -----> Downloaded app package (*8.9M*) + -----> Java Buildpack Version: v3.12 (offline) | https://github.com/cloudfoundry/java-buildpack.git#6f25b7e + -----> Downloading Open Jdk JRE 1.8.0_121 from https://java-buildpack.cloudfoundry.org/openjdk/trusty/x86_64/openjdk-1.8.0_121.tar.gz (found in cache) + Expanding Open Jdk JRE to .java-buildpack/open_jdk_jre (1.6s) + -----> Downloading Open JDK Like Memory Calculator 2.0.2_RELEASE from https://java-buildpack.cloudfoundry.org/memory-calculator/trusty/x86_64/memory-calculator-2.0.2_RELEASE.tar.gz (found in cache) + Memory Settings: -Xss349K -Xmx681574K -XX:MaxMetaspaceSize=104857K -Xms681574K -XX:MetaspaceSize=104857K + -----> Downloading Container Certificate Trust Store 1.0.0_RELEASE from https://java-buildpack.cloudfoundry.org/container-certificate-trust-store/container-certificate-trust-store-1.0.0_RELEASE.jar (found in cache) + Adding certificates to .java-buildpack/container_certificate_trust_store/truststore.jks (0.6s) + -----> Downloading Spring Auto Reconfiguration 1.10.0_RELEASE from https://java-buildpack.cloudfoundry.org/auto-reconfiguration/auto-reconfiguration-1.10.0_RELEASE.jar (found in cache) + Checking status of app 'acloudyspringtime'... + 0 of 1 instances running (1 starting) + ... + 0 of 1 instances running (1 starting) + ... + 0 of 1 instances running (1 starting) + ... + 1 of 1 instances running (1 running) + + App started +---- + +Congratulations! The application is now live! + +Once your application is live, you can verify the status of the deployed application by using the `cf apps` command, as shown in the following example: + +[source,shell,indent=0,subs="verbatim"] +---- + $ cf apps + Getting applications in ... + OK + + name requested state instances memory disk urls + ... + acloudyspringtime started 1/1 512M 1G acloudyspringtime.cfapps.io + ... +---- + +Once Cloud Foundry acknowledges that your application has been deployed, you should be able to find the application at the URI given. +In the preceding example, you could find it at `\https://acloudyspringtime.cfapps.io/`. + + + +[[deployment.cloud.cloud-foundry.binding-to-services]] +==== Binding to Services +By default, metadata about the running application as well as service connection information is exposed to the application as environment variables (for example: `$VCAP_SERVICES`). +This architecture decision is due to Cloud Foundry's polyglot (any language and platform can be supported as a buildpack) nature. +Process-scoped environment variables are language agnostic. + +Environment variables do not always make for the easiest API, so Spring Boot automatically extracts them and flattens the data into properties that can be accessed through Spring's `Environment` abstraction, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/deployment/cloud/cloudfoundry/bindingtoservices/MyBean.java[] +---- + +All Cloud Foundry properties are prefixed with `vcap`. +You can use `vcap` properties to access application information (such as the public URL of the application) and service information (such as database credentials). +See the {spring-boot-module-api}/cloud/CloudFoundryVcapEnvironmentPostProcessor.html['`CloudFoundryVcapEnvironmentPostProcessor`'] Javadoc for complete details. + +TIP: The https://github.com/pivotal-cf/java-cfenv/[Java CFEnv] project is a better fit for tasks such as configuring a DataSource. + + + +[[deployment.cloud.kubernetes]] +=== Kubernetes +Spring Boot auto-detects Kubernetes deployment environments by checking the environment for `"*_SERVICE_HOST"` and `"*_SERVICE_PORT"` variables. +You can override this detection with the configprop:spring.main.cloud-platform[] configuration property. + +Spring Boot helps you to <> and export it with <>. + + + +[[deployment.cloud.kubernetes.container-lifecycle]] +==== Kubernetes Container Lifecycle +When Kubernetes deletes an application instance, the shutdown process involves several subsystems concurrently: shutdown hooks, unregistering the service, removing the instance from the load-balancer... +Because this shutdown processing happens in parallel (and due to the nature of distributed systems), there is a window during which traffic can be routed to a pod that has also begun its shutdown processing. + +You can configure a sleep execution in a preStop handler to avoid requests being routed to a pod that has already begun shutting down. +This sleep should be long enough for new requests to stop being routed to the pod and its duration will vary from deployment to deployment. +The preStop handler can be configured via the PodSpec in the pod's configuration file as follows: + +[source,yml,indent=0,subs="verbatim"] +---- + spec: + containers: + - name: example-container + image: example-image + lifecycle: + preStop: + exec: + command: ["sh", "-c", "sleep 10"] +---- + +Once the pre-stop hook has completed, SIGTERM will be sent to the container and <> will begin, allowing any remaining in-flight requests to complete. + +NOTE: When Kubernetes sends a SIGTERM signal to the pod, it waits for a specified time called the termination grace period (the default for which is 30 seconds). +If the containers are still running after the grace period, they are sent the SIGKILL signal and forcibly removed. +If the pod takes longer than 30 seconds to shut down, which could be because you've increased configprop:spring.lifecycle.timeout-per-shutdown-phase[], make sure to increase the termination grace period by setting the `terminationGracePeriodSeconds` option in the Pod YAML. + + + +[[deployment.cloud.heroku]] +=== Heroku +Heroku is another popular PaaS platform. +To customize Heroku builds, you provide a `Procfile`, which provides the incantation required to deploy an application. +Heroku assigns a `port` for the Java application to use and then ensures that routing to the external URI works. + +You must configure your application to listen on the correct port. +The following example shows the `Procfile` for our starter REST application: + +[indent=0] +---- + web: java -Dserver.port=$PORT -jar target/demo-0.0.1-SNAPSHOT.jar +---- + +Spring Boot makes `-D` arguments available as properties accessible from a Spring `Environment` instance. +The `server.port` configuration property is fed to the embedded Tomcat, Jetty, or Undertow instance, which then uses the port when it starts up. +The `$PORT` environment variable is assigned to us by the Heroku PaaS. + +This should be everything you need. +The most common deployment workflow for Heroku deployments is to `git push` the code to production, as shown in the following example: + +[source,shell,indent=0,subs="verbatim"] +---- + $ git push heroku main +---- + +Which will result in the following: + +[indent=0,subs="verbatim,quotes"] +---- + Initializing repository, *done*. + Counting objects: 95, *done*. + Delta compression using up to 8 threads. + Compressing objects: 100% (78/78), *done*. + Writing objects: 100% (95/95), 8.66 MiB | 606.00 KiB/s, *done*. + Total 95 (delta 31), reused 0 (delta 0) + + -----> Java app detected + -----> Installing OpenJDK 1.8... *done* + -----> Installing Maven 3.3.1... *done* + -----> Installing settings.xml... *done* + -----> Executing: mvn -B -DskipTests=true clean install + + [INFO] Scanning for projects... + Downloading: https://repo.spring.io/... + Downloaded: https://repo.spring.io/... (818 B at 1.8 KB/sec) + .... + Downloaded: https://s3pository.heroku.com/jvm/... (152 KB at 595.3 KB/sec) + [INFO] Installing /tmp/build_0c35a5d2-a067-4abc-a232-14b1fb7a8229/target/... + [INFO] Installing /tmp/build_0c35a5d2-a067-4abc-a232-14b1fb7a8229/pom.xml ... + [INFO] ------------------------------------------------------------------------ + [INFO] *BUILD SUCCESS* + [INFO] ------------------------------------------------------------------------ + [INFO] Total time: 59.358s + [INFO] Finished at: Fri Mar 07 07:28:25 UTC 2014 + [INFO] Final Memory: 20M/493M + [INFO] ------------------------------------------------------------------------ + + -----> Discovering process types + Procfile declares types -> *web* + + -----> Compressing... *done*, 70.4MB + -----> Launching... *done*, v6 + https://agile-sierra-1405.herokuapp.com/ *deployed to Heroku* + + To git@heroku.com:agile-sierra-1405.git + * [new branch] main -> main +---- + +Your application should now be up and running on Heroku. +For more details, refer to https://devcenter.heroku.com/articles/deploying-spring-boot-apps-to-heroku[Deploying Spring Boot Applications to Heroku]. + + + +[[deployment.cloud.openshift]] +=== OpenShift +https://www.openshift.com/[OpenShift] has many resources describing how to deploy Spring Boot applications, including: + +* https://blog.openshift.com/using-openshift-enterprise-grade-spring-boot-deployments/[Using the S2I builder] +* https://access.redhat.com/documentation/en-us/reference_architectures/2017/html-single/spring_boot_microservices_on_red_hat_openshift_container_platform_3/[Architecture guide] +* https://blog.openshift.com/using-spring-boot-on-openshift/[Running as a traditional web application on Wildfly] +* https://blog.openshift.com/openshift-commons-briefing-96-cloud-native-applications-spring-rhoar/[OpenShift Commons Briefing] + + + +[[deployment.cloud.aws]] +=== Amazon Web Services (AWS) +Amazon Web Services offers multiple ways to install Spring Boot-based applications, either as traditional web applications (war) or as executable jar files with an embedded web server. +The options include: + +* AWS Elastic Beanstalk +* AWS Code Deploy +* AWS OPS Works +* AWS Cloud Formation +* AWS Container Registry + +Each has different features and pricing models. +In this document, we describe to approach using AWS Elastic Beanstalk. + + + +[[deployment.cloud.aws.beanstalk]] +==== AWS Elastic Beanstalk +As described in the official https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/create_deploy_Java.html[Elastic Beanstalk Java guide], there are two main options to deploy a Java application. +You can either use the "`Tomcat Platform`" or the "`Java SE platform`". + + + +[[deployment.cloud.aws.beanstalk.tomcat-platform]] +===== Using the Tomcat Platform +This option applies to Spring Boot projects that produce a war file. +No special configuration is required. +You need only follow the official guide. + + + +[[deployment.cloud.aws.beanstalk.java-se-platform]] +===== Using the Java SE Platform +This option applies to Spring Boot projects that produce a jar file and run an embedded web container. +Elastic Beanstalk environments run an nginx instance on port 80 to proxy the actual application, running on port 5000. +To configure it, add the following line to your `application.properties` file: + +[indent=0] +---- + server.port=5000 +---- + + +[TIP] +.Upload binaries instead of sources +==== +By default, Elastic Beanstalk uploads sources and compiles them in AWS. +However, it is best to upload the binaries instead. +To do so, add lines similar to the following to your `.elasticbeanstalk/config.yml` file: + +[source,xml,indent=0,subs="verbatim"] +---- + deploy: + artifact: target/demo-0.0.1-SNAPSHOT.jar +---- +==== + +[TIP] +.Reduce costs by setting the environment type +==== +By default an Elastic Beanstalk environment is load balanced. +The load balancer has a significant cost. +To avoid that cost, set the environment type to "`Single instance`", as described in https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/environments-create-wizard.html#environments-create-wizard-capacity[the Amazon documentation]. +You can also create single instance environments by using the CLI and the following command: + +[indent=0] +---- + eb create -s +---- +==== + + + +[[deployment.cloud.aws.summary]] +==== Summary +This is one of the easiest ways to get to AWS, but there are more things to cover, such as how to integrate Elastic Beanstalk into any CI / CD tool, use the Elastic Beanstalk Maven plugin instead of the CLI, and others. +There is a https://exampledriven.wordpress.com/2017/01/09/spring-boot-aws-elastic-beanstalk-example/[blog post] covering these topics more in detail. + + + +[[deployment.cloud.boxfuse]] +=== CloudCaptain and Amazon Web Services +https://cloudcaptain.sh/[CloudCaptain] works by turning your Spring Boot executable jar or war into a minimal VM image that can be deployed unchanged either on VirtualBox or on AWS. +CloudCaptain comes with deep integration for Spring Boot and uses the information from your Spring Boot configuration file to automatically configure ports and health check URLs. +CloudCaptain leverages this information both for the images it produces as well as for all the resources it provisions (instances, security groups, elastic load balancers, and so on). + +Once you have created a https://console.cloudcaptain.sh[CloudCaptain account], connected it to your AWS account, installed the latest version of the CloudCaptain Client, and ensured that the application has been built by Maven or Gradle (by using, for example, `mvn clean package`), you can deploy your Spring Boot application to AWS with a command similar to the following: + +[source,shell,indent=0,subs="verbatim"] +---- + $ boxfuse run myapp-1.0.jar -env=prod +---- + +See the https://cloudcaptain.sh/docs/commandline/run.html[`boxfuse run` documentation] for more options. +If there is a https://cloudcaptain.sh/docs/commandline/#configuration[`boxfuse.conf`] file present in the current directory, it is considered. + +TIP: By default, CloudCaptain activates a Spring profile named `boxfuse` on startup. +If your executable jar or war contains an https://cloudcaptain.sh/docs/payloads/springboot.html#configuration[`application-boxfuse.properties`] file, CloudCaptain bases its configuration on the properties it contains. + +At this point, CloudCaptain creates an image for your application, uploads it, and configures and starts the necessary resources on AWS, resulting in output similar to the following example: + +[indent=0,subs="verbatim"] +---- + Fusing Image for myapp-1.0.jar ... + Image fused in 00:06.838s (53937 K) -> axelfontaine/myapp:1.0 + Creating axelfontaine/myapp ... + Pushing axelfontaine/myapp:1.0 ... + Verifying axelfontaine/myapp:1.0 ... + Creating Elastic IP ... + Mapping myapp-axelfontaine.boxfuse.io to 52.28.233.167 ... + Waiting for AWS to create an AMI for axelfontaine/myapp:1.0 in eu-central-1 (this may take up to 50 seconds) ... + AMI created in 00:23.557s -> ami-d23f38cf + Creating security group boxfuse-sg_axelfontaine/myapp:1.0 ... + Launching t2.micro instance of axelfontaine/myapp:1.0 (ami-d23f38cf) in eu-central-1 ... + Instance launched in 00:30.306s -> i-92ef9f53 + Waiting for AWS to boot Instance i-92ef9f53 and Payload to start at https://52.28.235.61/ ... + Payload started in 00:29.266s -> https://52.28.235.61/ + Remapping Elastic IP 52.28.233.167 to i-92ef9f53 ... + Waiting 15s for AWS to complete Elastic IP Zero Downtime transition ... + Deployment completed successfully. axelfontaine/myapp:1.0 is up and running at https://myapp-axelfontaine.boxfuse.io/ +---- + +Your application should now be up and running on AWS. + +See the blog post on https://cloudcaptain.sh/blog/spring-boot-ec2.html[deploying Spring Boot apps on EC2] as well as the https://cloudcaptain.sh/docs/payloads/springboot.html[documentation for the CloudCaptain Spring Boot integration] to get started with a Maven build to run the app. + + + +[[deployment.cloud.azure]] +=== Azure +This https://spring.io/guides/gs/spring-boot-for-azure/[Getting Started guide] walks you through deploying your Spring Boot application to either https://azure.microsoft.com/en-us/services/spring-cloud/[Azure Spring Cloud] or https://docs.microsoft.com/en-us/azure/app-service/overview[Azure App Service]. + + + +[[deployment.cloud.google]] +=== Google Cloud +Google Cloud has several options that can be used to launch Spring Boot applications. +The easiest to get started with is probably App Engine, but you could also find ways to run Spring Boot in a container with Container Engine or on a virtual machine with Compute Engine. + +To run in App Engine, you can create a project in the UI first, which sets up a unique identifier for you and also sets up HTTP routes. +Add a Java app to the project and leave it empty and then use the https://cloud.google.com/sdk/install[Google Cloud SDK] to push your Spring Boot app into that slot from the command line or CI build. + +App Engine Standard requires you to use WAR packaging. +Follow https://github.com/GoogleCloudPlatform/java-docs-samples/tree/master/appengine-java8/springboot-helloworld/README.md[these steps] to deploy App Engine Standard application to Google Cloud. + +Alternatively, App Engine Flex requires you to create an `app.yaml` file to describe the resources your app requires. +Normally, you put this file in `src/main/appengine`, and it should resemble the following file: + +[source,yaml,indent=0,subs="verbatim"] +---- + service: default + + runtime: java + env: flex + + runtime_config: + jdk: openjdk8 + + handlers: + - url: /.* + script: this field is required, but ignored + + manual_scaling: + instances: 1 + + health_check: + enable_health_check: False + + env_variables: + ENCRYPT_KEY: your_encryption_key_here +---- + +You can deploy the app (for example, with a Maven plugin) by adding the project ID to the build configuration, as shown in the following example: + +[source,xml,indent=0,subs="verbatim"] +---- + + com.google.cloud.tools + appengine-maven-plugin + 1.3.0 + + myproject + + +---- + +Then deploy with `mvn appengine:deploy` (if you need to authenticate first, the build fails). diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/containers.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/containers.adoc new file mode 100644 index 000000000000..ee67b1c01174 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/containers.adoc @@ -0,0 +1,28 @@ +[[deployment.containers]] +== Deploying to Containers +If you are running your application from a container, you can use an executable jar, but it is also often an advantage to explode it and run it in a different way. +Certain PaaS implementations may also choose to unpack archives before they run. +For example, Cloud Foundry operates this way. +One way to run an unpacked archive is by starting the appropriate launcher, as follows: + +[source,shell,indent=0,subs="verbatim"] +---- + $ jar -xf myapp.jar + $ java org.springframework.boot.loader.JarLauncher +---- + +This is actually slightly faster on startup (depending on the size of the jar) than running from an unexploded archive. +At runtime you shouldn't expect any differences. + +Once you have unpacked the jar file, you can also get an extra boost to startup time by running the app with its "natural" main method instead of the `JarLauncher`. For example: + +[source,shell,indent=0,subs="verbatim"] +---- + $ jar -xf myapp.jar + $ java -cp BOOT-INF/classes:BOOT-INF/lib/* com.example.MyApplication +---- + +NOTE: Using the `JarLauncher` over the application's main method has the added benefit of a predictable classpath order. +The jar contains a `classpath.idx` file which is used by the `JarLauncher` when constructing the classpath. + +More efficient container images can also be created by <> for your dependencies and application classes and resources (which normally change more frequently). diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/installing.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/installing.adoc new file mode 100644 index 000000000000..ce221ed4f9f0 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/installing.adoc @@ -0,0 +1,389 @@ +[[deployment.installing]] +== Installing Spring Boot Applications +In addition to running Spring Boot applications by using `java -jar`, it is also possible to make fully executable applications for Unix systems. +A fully executable jar can be executed like any other executable binary or it can be <>. +This helps when installing and managing Spring Boot applications in common production environments. + +CAUTION: Fully executable jars work by embedding an extra script at the front of the file. +Currently, some tools do not accept this format, so you may not always be able to use this technique. +For example, `jar -xf` may silently fail to extract a jar or war that has been made fully executable. +It is recommended that you make your jar or war fully executable only if you intend to execute it directly, rather than running it with `java -jar` or deploying it to a servlet container. + +CAUTION: A zip64-format jar file cannot be made fully executable. +Attempting to do so will result in a jar file that is reported as corrupt when executed directly or with `java -jar`. +A standard-format jar file that contains one or more zip64-format nested jars can be fully executable. + +To create a '`fully executable`' jar with Maven, use the following plugin configuration: + +[source,xml,indent=0,subs="verbatim"] +---- + + org.springframework.boot + spring-boot-maven-plugin + + true + + +---- + +The following example shows the equivalent Gradle configuration: + +[source,gradle,indent=0,subs="verbatim"] +---- + tasks.named('bootJar') { + launchScript() + } +---- + +You can then run your application by typing `./my-application.jar` (where `my-application` is the name of your artifact). +The directory containing the jar is used as your application's working directory. + + + +[[deployment.installing.supported-operating-systems]] +=== Supported Operating Systems +The default script supports most Linux distributions and is tested on CentOS and Ubuntu. +Other platforms, such as OS X and FreeBSD, require the use of a custom `embeddedLaunchScript`. + + + +[[deployment.installing.nix-services]] +=== Unix/Linux Services +Spring Boot application can be easily started as Unix/Linux services by using either `init.d` or `systemd`. + + + +[[deployment.installing.nix-services.init-d]] +==== Installation as an init.d Service (System V) +If you configured Spring Boot's Maven or Gradle plugin to generate a <>, and you do not use a custom `embeddedLaunchScript`, your application can be used as an `init.d` service. +To do so, symlink the jar to `init.d` to support the standard `start`, `stop`, `restart`, and `status` commands. + +The script supports the following features: + +* Starts the services as the user that owns the jar file +* Tracks the application's PID by using `/var/run//.pid` +* Writes console logs to `/var/log/.log` + +Assuming that you have a Spring Boot application installed in `/var/myapp`, to install a Spring Boot application as an `init.d` service, create a symlink, as follows: + +[source,shell,indent=0,subs="verbatim"] +---- + $ sudo ln -s /var/myapp/myapp.jar /etc/init.d/myapp +---- + +Once installed, you can start and stop the service in the usual way. +For example, on a Debian-based system, you could start it with the following command: + +[source,shell,indent=0,subs="verbatim"] +---- + $ service myapp start +---- + +TIP: If your application fails to start, check the log file written to `/var/log/.log` for errors. + +You can also flag the application to start automatically by using your standard operating system tools. +For example, on Debian, you could use the following command: + +[source,shell,indent=0,subs="verbatim"] +---- + $ update-rc.d myapp defaults +---- + + + +[[deployment.installing.nix-services.init-d.securing]] +===== Securing an init.d Service +NOTE: The following is a set of guidelines on how to secure a Spring Boot application that runs as an init.d service. +It is not intended to be an exhaustive list of everything that should be done to harden an application and the environment in which it runs. + +When executed as root, as is the case when root is being used to start an init.d service, the default executable script runs the application as the user specified in the `RUN_AS_USER` environment variable. +When the environment variable is not set, the user who owns the jar file is used instead. +You should never run a Spring Boot application as `root`, so `RUN_AS_USER` should never be root and your application's jar file should never be owned by root. +Instead, create a specific user to run your application and set the `RUN_AS_USER` environment variable or use `chown` to make it the owner of the jar file, as shown in the following example: + +[source,shell,indent=0,subs="verbatim"] +---- + $ chown bootapp:bootapp your-app.jar +---- + +In this case, the default executable script runs the application as the `bootapp` user. + +TIP: To reduce the chances of the application's user account being compromised, you should consider preventing it from using a login shell. +For example, you can set the account's shell to `/usr/sbin/nologin`. + +You should also take steps to prevent the modification of your application's jar file. +Firstly, configure its permissions so that it cannot be written and can only be read or executed by its owner, as shown in the following example: + +[source,shell,indent=0,subs="verbatim"] +---- + $ chmod 500 your-app.jar +---- + +Second, you should also take steps to limit the damage if your application or the account that's running it is compromised. +If an attacker does gain access, they could make the jar file writable and change its contents. +One way to protect against this is to make it immutable by using `chattr`, as shown in the following example: + +[source,shell,indent=0,subs="verbatim"] +---- + $ sudo chattr +i your-app.jar +---- + +This will prevent any user, including root, from modifying the jar. + +If root is used to control the application's service and you <> to customize its startup, the `.conf` file is read and evaluated by the root user. +It should be secured accordingly. +Use `chmod` so that the file can only be read by the owner and use `chown` to make root the owner, as shown in the following example: + +[source,shell,indent=0,subs="verbatim"] +---- + $ chmod 400 your-app.conf + $ sudo chown root:root your-app.conf +---- + + + +[[deployment.installing.nix-services.system-d]] +==== Installation as a systemd Service +`systemd` is the successor of the System V init system and is now being used by many modern Linux distributions. +Although you can continue to use `init.d` scripts with `systemd`, it is also possible to launch Spring Boot applications by using `systemd` '`service`' scripts. + +Assuming that you have a Spring Boot application installed in `/var/myapp`, to install a Spring Boot application as a `systemd` service, create a script named `myapp.service` and place it in `/etc/systemd/system` directory. +The following script offers an example: + +[indent=0] +---- + [Unit] + Description=myapp + After=syslog.target + + [Service] + User=myapp + ExecStart=/var/myapp/myapp.jar + SuccessExitStatus=143 + + [Install] + WantedBy=multi-user.target +---- + +IMPORTANT: Remember to change the `Description`, `User`, and `ExecStart` fields for your application. + +NOTE: The `ExecStart` field does not declare the script action command, which means that the `run` command is used by default. + +Note that, unlike when running as an `init.d` service, the user that runs the application, the PID file, and the console log file are managed by `systemd` itself and therefore must be configured by using appropriate fields in the '`service`' script. +Consult the https://www.freedesktop.org/software/systemd/man/systemd.service.html[service unit configuration man page] for more details. + +To flag the application to start automatically on system boot, use the following command: + +[source,shell,indent=0,subs="verbatim"] +---- + $ systemctl enable myapp.service +---- + +Refer to `man systemctl` for more details. + + + +[[deployment.installing.nix-services.script-customization]] +==== Customizing the Startup Script +The default embedded startup script written by the Maven or Gradle plugin can be customized in a number of ways. +For most people, using the default script along with a few customizations is usually enough. +If you find you cannot customize something that you need to, use the `embeddedLaunchScript` option to write your own file entirely. + + + +[[deployment.installing.nix-services.script-customization.when-written]] +===== Customizing the Start Script When It Is Written +It often makes sense to customize elements of the start script as it is written into the jar file. +For example, init.d scripts can provide a "`description`". +Since you know the description up front (and it need not change), you may as well provide it when the jar is generated. + +To customize written elements, use the `embeddedLaunchScriptProperties` option of the Spring Boot Maven plugin or the {spring-boot-gradle-plugin-docs}#packaging-executable-configuring-launch-script[`properties` property of the Spring Boot Gradle plugin's `launchScript`]. + +The following property substitutions are supported with the default script: + +[cols="1,3,3,3"] +|=== +| Name | Description | Gradle default | Maven default + +| `mode` +| The script mode. +| `auto` +| `auto` + +| `initInfoProvides` +| The `Provides` section of "`INIT INFO`" +| `${task.baseName}` +| `${project.artifactId}` + +| `initInfoRequiredStart` +| `Required-Start` section of "`INIT INFO`". +| `$remote_fs $syslog $network` +| `$remote_fs $syslog $network` + +| `initInfoRequiredStop` +| `Required-Stop` section of "`INIT INFO`". +| `$remote_fs $syslog $network` +| `$remote_fs $syslog $network` + +| `initInfoDefaultStart` +| `Default-Start` section of "`INIT INFO`". +| `2 3 4 5` +| `2 3 4 5` + +| `initInfoDefaultStop` +| `Default-Stop` section of "`INIT INFO`". +| `0 1 6` +| `0 1 6` + +| `initInfoShortDescription` +| `Short-Description` section of "`INIT INFO`". +| Single-line version of `${project.description}` (falling back to `${task.baseName}`) +| `${project.name}` + +| `initInfoDescription` +| `Description` section of "`INIT INFO`". +| `${project.description}` (falling back to `${task.baseName}`) +| `${project.description}` (falling back to `${project.name}`) + +| `initInfoChkconfig` +| `chkconfig` section of "`INIT INFO`" +| `2345 99 01` +| `2345 99 01` + +| `confFolder` +| The default value for `CONF_FOLDER` +| Folder containing the jar +| Folder containing the jar + +| `inlinedConfScript` +| Reference to a file script that should be inlined in the default launch script. + This can be used to set environmental variables such as `JAVA_OPTS` before any external config files are loaded +| +| + +| `logFolder` +| Default value for `LOG_FOLDER`. + Only valid for an `init.d` service +| +| + +| `logFilename` +| Default value for `LOG_FILENAME`. + Only valid for an `init.d` service +| +| + +| `pidFolder` +| Default value for `PID_FOLDER`. + Only valid for an `init.d` service +| +| + +| `pidFilename` +| Default value for the name of the PID file in `PID_FOLDER`. + Only valid for an `init.d` service +| +| + +| `useStartStopDaemon` +| Whether the `start-stop-daemon` command, when it's available, should be used to control the process +| `true` +| `true` + +| `stopWaitTime` +| Default value for `STOP_WAIT_TIME` in seconds. + Only valid for an `init.d` service +| 60 +| 60 +|=== + + + +[[deployment.installing.nix-services.script-customization.when-running]] +===== Customizing a Script When It Runs +For items of the script that need to be customized _after_ the jar has been written, you can use environment variables or a <>. + +The following environment properties are supported with the default script: + +[cols="1,6"] +|=== +| Variable | Description + +| `MODE` +| The "`mode`" of operation. + The default depends on the way the jar was built but is usually `auto` (meaning it tries to guess if it is an init script by checking if it is a symlink in a directory called `init.d`). + You can explicitly set it to `service` so that the `stop\|start\|status\|restart` commands work or to `run` if you want to run the script in the foreground. + +| `RUN_AS_USER` +| The user that will be used to run the application. + When not set, the user that owns the jar file will be used. + +| `USE_START_STOP_DAEMON` +| Whether the `start-stop-daemon` command, when it's available, should be used to control the process. + Defaults to `true`. + +| `PID_FOLDER` +| The root name of the pid folder (`/var/run` by default). + +| `LOG_FOLDER` +| The name of the folder in which to put log files (`/var/log` by default). + +| `CONF_FOLDER` +| The name of the folder from which to read .conf files (same folder as jar-file by default). + +| `LOG_FILENAME` +| The name of the log file in the `LOG_FOLDER` (`.log` by default). + +| `APP_NAME` +| The name of the app. + If the jar is run from a symlink, the script guesses the app name. + If it is not a symlink or you want to explicitly set the app name, this can be useful. + +| `RUN_ARGS` +| The arguments to pass to the program (the Spring Boot app). + +| `JAVA_HOME` +| The location of the `java` executable is discovered by using the `PATH` by default, but you can set it explicitly if there is an executable file at `$JAVA_HOME/bin/java`. + +| `JAVA_OPTS` +| Options that are passed to the JVM when it is launched. + +| `JARFILE` +| The explicit location of the jar file, in case the script is being used to launch a jar that it is not actually embedded. + +| `DEBUG` +| If not empty, sets the `-x` flag on the shell process, allowing you to see the logic in the script. + +| `STOP_WAIT_TIME` +| The time in seconds to wait when stopping the application before forcing a shutdown (`60` by default). +|=== + +NOTE: The `PID_FOLDER`, `LOG_FOLDER`, and `LOG_FILENAME` variables are only valid for an `init.d` service. +For `systemd`, the equivalent customizations are made by using the '`service`' script. +See the https://www.freedesktop.org/software/systemd/man/systemd.service.html[service unit configuration man page] for more details. + + + +[[deployment.installing.nix-services.script-customization.when-running.conf-file]] +With the exception of `JARFILE` and `APP_NAME`, the settings listed in the preceding section can be configured by using a `.conf` file. +The file is expected to be next to the jar file and have the same name but suffixed with `.conf` rather than `.jar`. +For example, a jar named `/var/myapp/myapp.jar` uses the configuration file named `/var/myapp/myapp.conf`, as shown in the following example: + +.myapp.conf +[indent=0,subs="verbatim"] +---- + JAVA_OPTS=-Xmx1024M + LOG_FOLDER=/custom/log/folder +---- + +TIP: If you do not like having the config file next to the jar file, you can set a `CONF_FOLDER` environment variable to customize the location of the config file. + +To learn about securing this file appropriately, see <>. + + + +[[deployment.installing.windows-services]] +=== Microsoft Windows Services +A Spring Boot application can be started as a Windows service by using https://github.com/kohsuke/winsw[`winsw`]. + +A (https://github.com/snicoll/spring-boot-daemon[separately maintained sample]) describes step-by-step how you can create a Windows service for your Spring Boot application. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/whats-next.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/whats-next.adoc new file mode 100644 index 000000000000..f755bb566491 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/whats-next.adoc @@ -0,0 +1,7 @@ +[[deployment.whats-next]] +== What to Read Next +Check out the https://www.cloudfoundry.org/[Cloud Foundry], https://www.heroku.com/[Heroku], https://www.openshift.com[OpenShift], and https://boxfuse.com[Boxfuse] web sites for more information about the kinds of features that a PaaS can offer. +These are just four of the most popular Java PaaS providers. +Since Spring Boot is so amenable to cloud-based deployment, you can freely consider other providers as well. + +The next section goes on to cover the _<>_, or you can jump ahead to read about _<>_. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation.adoc new file mode 100644 index 000000000000..759efc3b0390 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation.adoc @@ -0,0 +1,27 @@ +include::attributes.adoc[] + + + +[[documentation]] += Documentation Overview +include::attributes.adoc[] + + + +This section provides a brief overview of Spring Boot reference documentation. +It serves as a map for the rest of the document. + +The latest copy of this document is available at {spring-boot-current-docs}. + + +include::documentation/first-steps.adoc[] + +include::documentation/upgrading.adoc[] + +include::documentation/using.adoc[] + +include::documentation/features.adoc[] + +include::documentation/actuator.adoc[] + +include::documentation/advanced.adoc[] \ No newline at end of file diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/actuator.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/actuator.adoc new file mode 100644 index 000000000000..9baf497527b8 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/actuator.adoc @@ -0,0 +1,7 @@ +[[documentation.actuator]] +== Moving to Production +When you are ready to push your Spring Boot application to production, we have <> that you might like: + +* *Management endpoints:* <> +* *Connection options:* <> | <> +* *Monitoring:* <> | <> | <> | <> diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/advanced.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/advanced.adoc new file mode 100644 index 000000000000..7617561d11cf --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/advanced.adoc @@ -0,0 +1,7 @@ +[[documentation.advanced]] +== Advanced Topics +Finally, we have a few topics for more advanced users: + +* *Spring Boot Applications Deployment:* <> | <> +* *Build tool plugins:* <> | <> +* *Appendix:* <> | <> | <> | <> | <> | <> diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/features.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/features.adoc new file mode 100644 index 000000000000..0e6a8f8657c1 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/features.adoc @@ -0,0 +1,11 @@ +[[documentation.features]] +== Learning About Spring Boot Features +Need more details about Spring Boot's core features? +<>: + +* *Core Features:* <> | <> | <> | <> +* *Web Applications:* <> | <> +* *Working with data:* <> | <> +* *Messaging:* <> | <> +* *Testing:* <> | <> | <> +* *Extending:* <> | <> diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/first-steps.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/first-steps.adoc new file mode 100644 index 000000000000..fb396ffd3408 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/first-steps.adoc @@ -0,0 +1,7 @@ +[[documentation.first-steps]] +== First Steps +If you are getting started with Spring Boot or 'Spring' in general, start with <>: + +* *From scratch:* <> | <> | <> +* *Tutorial:* <> | <> +* *Running your example:* <> | <> diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/upgrading.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/upgrading.adoc new file mode 100644 index 000000000000..314239c9ecdb --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/upgrading.adoc @@ -0,0 +1,11 @@ +[[documentation.upgrading]] +== Upgrading From an Earlier Version + +You should always ensure that you are running a {github-wiki}/Supported-Versions[supported version] of Spring Boot. + +Depending on the version that you are upgrading to, you can find some additional tips here: + +* *From 1.x:* <> +* *To a new feature release:* <> +* *Spring Boot CLI:* <> + diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/using.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/using.adoc new file mode 100644 index 000000000000..122466f852d3 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/using.adoc @@ -0,0 +1,9 @@ +[[documentation.using]] +== Developing with Spring Boot +Ready to actually start using Spring Boot? <>: + +* *Build systems:* <> | <> | <> | <> +* *Best practices:* <> | <> | <> | <> +* *Running your code:* <> | <> | <> | <> +* *Packaging your app:* <> +* *Spring Boot CLI:* <> diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar.adoc new file mode 100644 index 000000000000..49bb31a01af8 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar.adoc @@ -0,0 +1,25 @@ +[appendix] +[[appendix.executable-jar]] += The Executable Jar Format +include::attributes.adoc[] + + + +The `spring-boot-loader` modules lets Spring Boot support executable jar and war files. +If you use the Maven plugin or the Gradle plugin, executable jars are automatically generated, and you generally do not need to know the details of how they work. + +If you need to create executable jars from a different build system or if you are just curious about the underlying technology, this appendix provides some background. + + + +include::executable-jar/nested-jars.adoc[] + +include::executable-jar/jarfile-class.adoc[] + +include::executable-jar/launching.adoc[] + +include::executable-jar/property-launcher.adoc[] + +include::executable-jar/restrictions.adoc[] + +include::executable-jar/alternatives.adoc[] diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/alternatives.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/alternatives.adoc new file mode 100644 index 000000000000..30d4bbc6f2d6 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/alternatives.adoc @@ -0,0 +1,9 @@ +[[appendix.executable-jar.alternatives]] +== Alternative Single Jar Solutions +If the preceding restrictions mean that you cannot use Spring Boot Loader, consider the following alternatives: + +* https://maven.apache.org/plugins/maven-shade-plugin/[Maven Shade Plugin] +* http://www.jdotsoft.com/JarClassLoader.php[JarClassLoader] +* https://sourceforge.net/projects/one-jar/[OneJar] +* https://imperceptiblethoughts.com/shadow/[Gradle Shadow Plugin] + diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/jarfile-class.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/jarfile-class.adoc new file mode 100644 index 000000000000..65e3d3de9b5e --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/jarfile-class.adoc @@ -0,0 +1,32 @@ +[[appendix.executable-jar.jarfile-class]] +== Spring Boot's "`JarFile`" Class +The core class used to support loading nested jars is `org.springframework.boot.loader.jar.JarFile`. +It lets you load jar content from a standard jar file or from nested child jar data. +When first loaded, the location of each `JarEntry` is mapped to a physical file offset of the outer jar, as shown in the following example: + +[indent=0] +---- + myapp.jar + +-------------------+-------------------------+ + | /BOOT-INF/classes | /BOOT-INF/lib/mylib.jar | + |+-----------------+||+-----------+----------+| + || A.class ||| B.class | C.class || + |+-----------------+||+-----------+----------+| + +-------------------+-------------------------+ + ^ ^ ^ + 0063 3452 3980 +---- + +The preceding example shows how `A.class` can be found in `/BOOT-INF/classes` in `myapp.jar` at position `0063`. +`B.class` from the nested jar can actually be found in `myapp.jar` at position `3452`, and `C.class` is at position `3980`. + +Armed with this information, we can load specific nested entries by seeking to the appropriate part of the outer jar. +We do not need to unpack the archive, and we do not need to read all entry data into memory. + + + +[[appendix.executable-jar.jarfile-class.compatibility]] +=== Compatibility with the Standard Java "`JarFile`" +Spring Boot Loader strives to remain compatible with existing code and libraries. +`org.springframework.boot.loader.jar.JarFile` extends from `java.util.jar.JarFile` and should work as a drop-in replacement. +The `getURL()` method returns a `URL` that opens a connection compatible with `java.net.JarURLConnection` and can be used with Java's `URLClassLoader`. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/launching.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/launching.adoc new file mode 100644 index 000000000000..a672c6963495 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/launching.adoc @@ -0,0 +1,38 @@ +[[appendix.executable-jar.launching]] +== Launching Executable Jars +The `org.springframework.boot.loader.Launcher` class is a special bootstrap class that is used as an executable jar's main entry point. +It is the actual `Main-Class` in your jar file, and it is used to setup an appropriate `URLClassLoader` and ultimately call your `main()` method. + +There are three launcher subclasses (`JarLauncher`, `WarLauncher`, and `PropertiesLauncher`). +Their purpose is to load resources (`.class` files and so on) from nested jar files or war files in directories (as opposed to those explicitly on the classpath). +In the case of `JarLauncher` and `WarLauncher`, the nested paths are fixed. +`JarLauncher` looks in `BOOT-INF/lib/`, and `WarLauncher` looks in `WEB-INF/lib/` and `WEB-INF/lib-provided/`. +You can add extra jars in those locations if you want more. +The `PropertiesLauncher` looks in `BOOT-INF/lib/` in your application archive by default. +You can add additional locations by setting an environment variable called `LOADER_PATH` or `loader.path` in `loader.properties` (which is a comma-separated list of directories, archives, or directories within archives). + + + +[[appendix.executable-jar.launching.manifest]] +=== Launcher Manifest +You need to specify an appropriate `Launcher` as the `Main-Class` attribute of `META-INF/MANIFEST.MF`. +The actual class that you want to launch (that is, the class that contains a `main` method) should be specified in the `Start-Class` attribute. + +The following example shows a typical `MANIFEST.MF` for an executable jar file: + +[indent=0] +---- + Main-Class: org.springframework.boot.loader.JarLauncher + Start-Class: com.mycompany.project.MyApplication +---- + +For a war file, it would be as follows: + +[indent=0] +---- + Main-Class: org.springframework.boot.loader.WarLauncher + Start-Class: com.mycompany.project.MyApplication +---- + +NOTE: You need not specify `Class-Path` entries in your manifest file. +The classpath is deduced from the nested jars. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/nested-jars.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/nested-jars.adoc new file mode 100644 index 000000000000..b163b3974194 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/nested-jars.adoc @@ -0,0 +1,141 @@ +[[appendix.executable-jar.nested-jars]] +== Nested JARs +Java does not provide any standard way to load nested jar files (that is, jar files that are themselves contained within a jar). +This can be problematic if you need to distribute a self-contained application that can be run from the command line without unpacking. + +To solve this problem, many developers use "`shaded`" jars. +A shaded jar packages all classes, from all jars, into a single "`uber jar`". +The problem with shaded jars is that it becomes hard to see which libraries are actually in your application. +It can also be problematic if the same filename is used (but with different content) in multiple jars. +Spring Boot takes a different approach and lets you actually nest jars directly. + + + +[[appendix.executable-jar.nested-jars.jar-structure]] +=== The Executable Jar File Structure +Spring Boot Loader-compatible jar files should be structured in the following way: + +[indent=0] +---- + example.jar + | + +-META-INF + | +-MANIFEST.MF + +-org + | +-springframework + | +-boot + | +-loader + | +- + +-BOOT-INF + +-classes + | +-mycompany + | +-project + | +-YourClasses.class + +-lib + +-dependency1.jar + +-dependency2.jar +---- + +Application classes should be placed in a nested `BOOT-INF/classes` directory. +Dependencies should be placed in a nested `BOOT-INF/lib` directory. + + + +[[appendix.executable-jar.nested-jars.war-structure]] +=== The Executable War File Structure +Spring Boot Loader-compatible war files should be structured in the following way: + +[indent=0] +---- + example.war + | + +-META-INF + | +-MANIFEST.MF + +-org + | +-springframework + | +-boot + | +-loader + | +- + +-WEB-INF + +-classes + | +-com + | +-mycompany + | +-project + | +-YourClasses.class + +-lib + | +-dependency1.jar + | +-dependency2.jar + +-lib-provided + +-servlet-api.jar + +-dependency3.jar +---- + +Dependencies should be placed in a nested `WEB-INF/lib` directory. +Any dependencies that are required when running embedded but are not required when deploying to a traditional web container should be placed in `WEB-INF/lib-provided`. + + + +[[appendix.executable-jar.nested-jars.index-files]] +=== Index Files +Spring Boot Loader-compatible jar and war archives can include additional index files under the `BOOT-INF/` directory. +A `classpath.idx` file can be provided for both jars and wars, and it provides the ordering that jars should be added to the classpath. +The `layers.idx` file can be used only for jars, and it allows a jar to be split into logical layers for Docker/OCI image creation. + +Index files follow a YAML compatible syntax so that they can be easily parsed by third-party tools. +These files, however, are _not_ parsed internally as YAML and they must be written in exactly the formats described below in order to be used. + + + +[[appendix.executable-jar.nested-jars.classpath-index]] +=== Classpath Index +The classpath index file can be provided in `BOOT-INF/classpath.idx`. +It provides a list of jar names (including the directory) in the order that they should be added to the classpath. +Each line must start with dash space (`"-·"`) and names must be in double quotes. + +For example, given the following jar: + +[indent=0] +---- + example.jar + | + +-META-INF + | +-... + +-BOOT-INF + +-classes + | +... + +-lib + +-dependency1.jar + +-dependency2.jar +---- + +The index file would look like this: + +[indent=0] +---- + - "BOOT-INF/lib/dependency2.jar" + - "BOOT-INF/lib/dependency1.jar" +---- + + + +[[appendix.executable-jar.nested-jars.layer-index]] +=== Layer Index +The layers index file can be provided in `BOOT-INF/layers.idx`. +It provides a list of layers and the parts of the jar that should be contained within them. +Layers are written in the order that they should be added to the Docker/OCI image. +Layers names are written as quoted strings prefixed with dash space (`"-·"`) and with a colon (`":"`) suffix. +Layer content is either a file or directory name written as a quoted string prefixed by space space dash space (`"··-·"`). +A directory name ends with `/`, a file name does not. +When a directory name is used it means that all files inside that directory are in the same layer. + +A typical example of a layers index would be: + +[indent=0] +---- + - "dependencies": + - "BOOT-INF/lib/dependency1.jar" + - "BOOT-INF/lib/dependency2.jar" + - "application": + - "BOOT-INF/classes/" + - "META-INF/" +---- diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/property-launcher.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/property-launcher.adoc new file mode 100644 index 000000000000..675a2bc27801 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/property-launcher.adoc @@ -0,0 +1,81 @@ +[[appendix.executable-jar.property-launcher]] +== PropertiesLauncher Features +`PropertiesLauncher` has a few special features that can be enabled with external properties (System properties, environment variables, manifest entries, or `loader.properties`). +The following table describes these properties: + +|=== +| Key | Purpose + +| `loader.path` +| Comma-separated Classpath, such as `lib,$\{HOME}/app/lib`. + Earlier entries take precedence, like a regular `-classpath` on the `javac` command line. + +| `loader.home` +| Used to resolve relative paths in `loader.path`. + For example, given `loader.path=lib`, then `${loader.home}/lib` is a classpath location (along with all jar files in that directory). + This property is also used to locate a `loader.properties` file, as in the following example `file:///opt/app` It defaults to `${user.dir}`. + +| `loader.args` +| Default arguments for the main method (space separated). + +| `loader.main` +| Name of main class to launch (for example, `com.app.Application`). + +| `loader.config.name` +| Name of properties file (for example, `launcher`). + It defaults to `loader`. + +| `loader.config.location` +| Path to properties file (for example, `classpath:loader.properties`). + It defaults to `loader.properties`. + +| `loader.system` +| Boolean flag to indicate that all properties should be added to System properties. + It defaults to `false`. +|=== + +When specified as environment variables or manifest entries, the following names should be used: + +|=== +| Key | Manifest entry | Environment variable + +| `loader.path` +| `Loader-Path` +| `LOADER_PATH` + +| `loader.home` +| `Loader-Home` +| `LOADER_HOME` + +| `loader.args` +| `Loader-Args` +| `LOADER_ARGS` + +| `loader.main` +| `Start-Class` +| `LOADER_MAIN` + +| `loader.config.location` +| `Loader-Config-Location` +| `LOADER_CONFIG_LOCATION` + +| `loader.system` +| `Loader-System` +| `LOADER_SYSTEM` +|=== + +TIP: Build plugins automatically move the `Main-Class` attribute to `Start-Class` when the fat jar is built. +If you use that, specify the name of the class to launch by using the `Main-Class` attribute and leaving out `Start-Class`. + +The following rules apply to working with `PropertiesLauncher`: + +* `loader.properties` is searched for in `loader.home`, then in the root of the classpath, and then in `classpath:/BOOT-INF/classes`. + The first location where a file with that name exists is used. +* `loader.home` is the directory location of an additional properties file (overriding the default) only when `loader.config.location` is not specified. +* `loader.path` can contain directories (which are scanned recursively for jar and zip files), archive paths, a directory within an archive that is scanned for jar files (for example, `dependencies.jar!/lib`), or wildcard patterns (for the default JVM behavior). + Archive paths can be relative to `loader.home` or anywhere in the file system with a `jar:file:` prefix. +* `loader.path` (if empty) defaults to `BOOT-INF/lib` (meaning a local directory or a nested one if running from an archive). + Because of this, `PropertiesLauncher` behaves the same as `JarLauncher` when no additional configuration is provided. +* `loader.path` can not be used to configure the location of `loader.properties` (the classpath used to search for the latter is the JVM classpath when `PropertiesLauncher` is launched). +* Placeholder replacement is done from System and environment variables plus the properties file itself on all values before use. +* The search order for properties (where it makes sense to look in more than one place) is environment variables, system properties, `loader.properties`, the exploded archive manifest, and the archive manifest. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/restrictions.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/restrictions.adoc new file mode 100644 index 000000000000..92df8346fa18 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/restrictions.adoc @@ -0,0 +1,20 @@ +[[appendix.executable-jar.restrictions]] +== Executable Jar Restrictions +You need to consider the following restrictions when working with a Spring Boot Loader packaged application: + + + +[[appendix.executable-jar-zip-entry-compression]] +* Zip entry compression: +The `ZipEntry` for a nested jar must be saved by using the `ZipEntry.STORED` method. +This is required so that we can seek directly to individual content within the nested jar. +The content of the nested jar file itself can still be compressed, as can any other entries in the outer jar. + + + +[[appendix.executable-jar-system-classloader]] +* System classLoader: +Launched applications should use `Thread.getContextClassLoader()` when loading classes (most libraries and frameworks do so by default). +Trying to load nested jar classes with `ClassLoader.getSystemClassLoader()` fails. +`java.util.Logging` always uses the system classloader. +For this reason, you should consider a different logging implementation. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features.adoc new file mode 100644 index 000000000000..6d60adc5e5ff --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features.adoc @@ -0,0 +1,75 @@ +[[features]] += Spring Boot Features +include::attributes.adoc[] + + + +This section dives into the details of Spring Boot. +Here you can learn about the key features that you may want to use and customize. +If you have not already done so, you might want to read the "<>" and "<>" sections, so that you have a good grounding of the basics. + + + +include::features/spring-application.adoc[] + +include::features/external-config.adoc[] + +include::features/profiles.adoc[] + +include::features/logging.adoc[] + +include::features/internationalization.adoc[] + +include::features/json.adoc[] + +include::features/developing-web-applications.adoc[] + +include::features/graceful-shutdown.adoc[] + +include::features/rsocket.adoc[] + +include::features/security.adoc[] + +include::features/sql.adoc[] + +include::features/nosql.adoc[] + +include::features/caching.adoc[] + +include::features/messaging.adoc[] + +include::features/resttemplate.adoc[] + +include::features/webclient.adoc[] + +include::features/validation.adoc[] + +include::features/email.adoc[] + +include::features/jta.adoc[] + +include::features/hazelcast.adoc[] + +include::features/quartz.adoc[] + +include::features/task-execution-and-scheduling.adoc[] + +include::features/spring-integration.adoc[] + +include::features/spring-session.adoc[] + +include::features/jmx.adoc[] + +include::features/testing.adoc[] + +include::features/websockets.adoc[] + +include::features/webservices.adoc[] + +include::features/developing-auto-configuration.adoc[] + +include::features/kotlin.adoc[] + +include::features/container-images.adoc[] + +include::features/whats-next.adoc[] diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/caching.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/caching.adoc new file mode 100644 index 000000000000..e33f7286cbeb --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/caching.adoc @@ -0,0 +1,274 @@ +[[features.caching]] +== Caching +The Spring Framework provides support for transparently adding caching to an application. +At its core, the abstraction applies caching to methods, thus reducing the number of executions based on the information available in the cache. +The caching logic is applied transparently, without any interference to the invoker. +Spring Boot auto-configures the cache infrastructure as long as caching support is enabled via the `@EnableCaching` annotation. + +NOTE: Check the {spring-framework-docs}/integration.html#cache[relevant section] of the Spring Framework reference for more details. + +In a nutshell, to add caching to an operation of your service add the relevant annotation to its method, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/caching/MyMathService.java[] +---- + +This example demonstrates the use of caching on a potentially costly operation. +Before invoking `computePiDecimal`, the abstraction looks for an entry in the `piDecimals` cache that matches the `i` argument. +If an entry is found, the content in the cache is immediately returned to the caller, and the method is not invoked. +Otherwise, the method is invoked, and the cache is updated before returning the value. + +CAUTION: You can also use the standard JSR-107 (JCache) annotations (such as `@CacheResult`) transparently. +However, we strongly advise you to not mix and match the Spring Cache and JCache annotations. + +If you do not add any specific cache library, Spring Boot auto-configures a <> that uses concurrent maps in memory. +When a cache is required (such as `piDecimals` in the preceding example), this provider creates it for you. +The simple provider is not really recommended for production usage, but it is great for getting started and making sure that you understand the features. +When you have made up your mind about the cache provider to use, please make sure to read its documentation to figure out how to configure the caches that your application uses. +Nearly all providers require you to explicitly configure every cache that you use in the application. +Some offer a way to customize the default caches defined by the configprop:spring.cache.cache-names[] property. + +TIP: It is also possible to transparently {spring-framework-docs}/integration.html#cache-annotations-put[update] or {spring-framework-docs}/integration.html#cache-annotations-evict[evict] data from the cache. + + + +[[features.caching.provider]] +=== Supported Cache Providers +The cache abstraction does not provide an actual store and relies on abstraction materialized by the `org.springframework.cache.Cache` and `org.springframework.cache.CacheManager` interfaces. + +If you have not defined a bean of type `CacheManager` or a `CacheResolver` named `cacheResolver` (see {spring-framework-api}/cache/annotation/CachingConfigurer.html[`CachingConfigurer`]), Spring Boot tries to detect the following providers (in the indicated order): + +. <> +. <> (EhCache 3, Hazelcast, Infinispan, and others) +. <> +. <> +. <> +. <> +. <> +. <> +. <> + +Additionally, {spring-boot-for-apache-geode}[Spring Boot for Apache Geode] provides {spring-boot-for-apache-geode-docs}#geode-caching-provider[auto-configuration for using Apache Geode as a cache provider]. + +TIP: It is also possible to _force_ a particular cache provider by setting the configprop:spring.cache.type[] property. +Use this property if you need to <> in certain environment (such as tests). + +TIP: Use the `spring-boot-starter-cache` "`Starter`" to quickly add basic caching dependencies. +The starter brings in `spring-context-support`. +If you add dependencies manually, you must include `spring-context-support` in order to use the JCache, EhCache 2.x, or Caffeine support. + +If the `CacheManager` is auto-configured by Spring Boot, you can further tune its configuration before it is fully initialized by exposing a bean that implements the `CacheManagerCustomizer` interface. +The following example sets a flag to say that `null` values should not be passed down to the underlying map: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/caching/provider/MyCacheManagerConfiguration.java[] +---- + +NOTE: In the preceding example, an auto-configured `ConcurrentMapCacheManager` is expected. +If that is not the case (either you provided your own config or a different cache provider was auto-configured), the customizer is not invoked at all. +You can have as many customizers as you want, and you can also order them by using `@Order` or `Ordered`. + + + +[[features.caching.provider.generic]] +==== Generic +Generic caching is used if the context defines _at least_ one `org.springframework.cache.Cache` bean. +A `CacheManager` wrapping all beans of that type is created. + + + +[[features.caching.provider.jcache]] +==== JCache (JSR-107) +https://jcp.org/en/jsr/detail?id=107[JCache] is bootstrapped through the presence of a `javax.cache.spi.CachingProvider` on the classpath (that is, a JSR-107 compliant caching library exists on the classpath), and the `JCacheCacheManager` is provided by the `spring-boot-starter-cache` "`Starter`". +Various compliant libraries are available, and Spring Boot provides dependency management for Ehcache 3, Hazelcast, and Infinispan. +Any other compliant library can be added as well. + +It might happen that more than one provider is present, in which case the provider must be explicitly specified. +Even if the JSR-107 standard does not enforce a standardized way to define the location of the configuration file, Spring Boot does its best to accommodate setting a cache with implementation details, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + # Only necessary if more than one provider is present + spring: + cache: + jcache: + provider: "com.example.MyCachingProvider" + config: "classpath:example.xml" +---- + +NOTE: When a cache library offers both a native implementation and JSR-107 support, Spring Boot prefers the JSR-107 support, so that the same features are available if you switch to a different JSR-107 implementation. + +TIP: Spring Boot has <>. +If a single `HazelcastInstance` is available, it is automatically reused for the `CacheManager` as well, unless the configprop:spring.cache.jcache.config[] property is specified. + +There are two ways to customize the underlying `javax.cache.cacheManager`: + +* Caches can be created on startup by setting the configprop:spring.cache.cache-names[] property. + If a custom `javax.cache.configuration.Configuration` bean is defined, it is used to customize them. +* `org.springframework.boot.autoconfigure.cache.JCacheManagerCustomizer` beans are invoked with the reference of the `CacheManager` for full customization. + +TIP: If a standard `javax.cache.CacheManager` bean is defined, it is wrapped automatically in an `org.springframework.cache.CacheManager` implementation that the abstraction expects. +No further customization is applied to it. + + + +[[features.caching.provider.ehcache2]] +==== EhCache 2.x +https://www.ehcache.org/[EhCache] 2.x is used if a file named `ehcache.xml` can be found at the root of the classpath. +If EhCache 2.x is found, the `EhCacheCacheManager` provided by the `spring-boot-starter-cache` "`Starter`" is used to bootstrap the cache manager. +An alternate configuration file can be provided as well, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + cache: + ehcache: + config: "classpath:config/another-config.xml" +---- + + + +[[features.caching.provider.hazelcast]] +==== Hazelcast +Spring Boot has <>. +If a `HazelcastInstance` has been auto-configured, it is automatically wrapped in a `CacheManager`. + + + +[[features.caching.provider.infinispan]] +==== Infinispan +https://infinispan.org/[Infinispan] has no default configuration file location, so it must be specified explicitly. +Otherwise, the default bootstrap is used. + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + cache: + infinispan: + config: "infinispan.xml" +---- + +Caches can be created on startup by setting the configprop:spring.cache.cache-names[] property. +If a custom `ConfigurationBuilder` bean is defined, it is used to customize the caches. + +NOTE: The support of Infinispan in Spring Boot is restricted to the embedded mode and is quite basic. +If you want more options, you should use the official Infinispan Spring Boot starter instead. +See https://github.com/infinispan/infinispan-spring-boot[Infinispan's documentation] for more details. + + + +[[features.caching.provider.couchbase]] +==== Couchbase +If Spring Data Couchbase is available and Couchbase is <>, a `CouchbaseCacheManager` is auto-configured. +It is possible to create additional caches on startup by setting the configprop:spring.cache.cache-names[] property and cache defaults can be configured by using `spring.cache.couchbase.*` properties. +For instance, the following configuration creates `cache1` and `cache2` caches with an entry _expiration_ of 10 minutes: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + cache: + cache-names: "cache1,cache2" + couchbase: + expiration: "10m" +---- + +If you need more control over the configuration, consider registering a `CouchbaseCacheManagerBuilderCustomizer` bean. +The following example shows a customizer that configures a specific entry expiration for `cache1` and `cache2`: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/caching/provider/couchbase/MyCouchbaseCacheManagerConfiguration.java[] +---- + + + +[[features.caching.provider.redis]] +==== Redis +If https://redis.io/[Redis] is available and configured, a `RedisCacheManager` is auto-configured. +It is possible to create additional caches on startup by setting the configprop:spring.cache.cache-names[] property and cache defaults can be configured by using `spring.cache.redis.*` properties. +For instance, the following configuration creates `cache1` and `cache2` caches with a _time to live_ of 10 minutes: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + cache: + cache-names: "cache1,cache2" + redis: + time-to-live: "10m" +---- + +NOTE: By default, a key prefix is added so that, if two separate caches use the same key, Redis does not have overlapping keys and cannot return invalid values. +We strongly recommend keeping this setting enabled if you create your own `RedisCacheManager`. + +TIP: You can take full control of the default configuration by adding a `RedisCacheConfiguration` `@Bean` of your own. +This can be useful if you're looking for customizing the default serialization strategy. + +If you need more control over the configuration, consider registering a `RedisCacheManagerBuilderCustomizer` bean. +The following example shows a customizer that configures a specific time to live for `cache1` and `cache2`: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/caching/provider/redis/MyRedisCacheManagerConfiguration.java[] +---- + + + +[[features.caching.provider.caffeine]] +==== Caffeine +https://github.com/ben-manes/caffeine[Caffeine] is a Java 8 rewrite of Guava's cache that supersedes support for Guava. +If Caffeine is present, a `CaffeineCacheManager` (provided by the `spring-boot-starter-cache` "`Starter`") is auto-configured. +Caches can be created on startup by setting the configprop:spring.cache.cache-names[] property and can be customized by one of the following (in the indicated order): + +. A cache spec defined by `spring.cache.caffeine.spec` +. A `com.github.benmanes.caffeine.cache.CaffeineSpec` bean is defined +. A `com.github.benmanes.caffeine.cache.Caffeine` bean is defined + +For instance, the following configuration creates `cache1` and `cache2` caches with a maximum size of 500 and a _time to live_ of 10 minutes + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + cache: + cache-names: "cache1,cache2" + caffeine: + spec: "maximumSize=500,expireAfterAccess=600s" +---- + +If a `com.github.benmanes.caffeine.cache.CacheLoader` bean is defined, it is automatically associated to the `CaffeineCacheManager`. +Since the `CacheLoader` is going to be associated with _all_ caches managed by the cache manager, it must be defined as `CacheLoader`. +The auto-configuration ignores any other generic type. + + + +[[features.caching.provider.simple]] +==== Simple +If none of the other providers can be found, a simple implementation using a `ConcurrentHashMap` as the cache store is configured. +This is the default if no caching library is present in your application. +By default, caches are created as needed, but you can restrict the list of available caches by setting the `cache-names` property. +For instance, if you want only `cache1` and `cache2` caches, set the `cache-names` property as follows: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + cache: + cache-names: "cache1,cache2" +---- + +If you do so and your application uses a cache not listed, then it fails at runtime when the cache is needed, but not on startup. +This is similar to the way the "real" cache providers behave if you use an undeclared cache. + + + +[[features.caching.provider.none]] +==== None +When `@EnableCaching` is present in your configuration, a suitable cache configuration is expected as well. +If you need to disable caching altogether in certain environments, force the cache type to `none` to use a no-op implementation, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + cache: + type: "none" +---- diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/container-images.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/container-images.adoc new file mode 100644 index 000000000000..8fae91b84f89 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/container-images.adoc @@ -0,0 +1,141 @@ +[[features.container-images]] +== Container Images +It is easily possible to package a Spring Boot fat jar as a docker image. +However, there are various downsides to copying and running the fat jar as is in the docker image. +There’s always a certain amount of overhead when running a fat jar without unpacking it, and in a containerized environment this can be noticeable. +The other issue is that putting your application's code and all its dependencies in one layer in the Docker image is sub-optimal. +Since you probably recompile your code more often than you upgrade the version of Spring Boot you use, it’s often better to separate things a bit more. +If you put jar files in the layer before your application classes, Docker often only needs to change the very bottom layer and can pick others up from its cache. + + + +[[features.container-images.layering]] +=== Layering Docker Images +To make it easier to create optimized Docker images, Spring Boot supports adding a layer index file to the jar. +It provides a list of layers and the parts of the jar that should be contained within them. +The list of layers in the index is ordered based on the order in which the layers should be added to the Docker/OCI image. +Out-of-the-box, the following layers are supported: + +* `dependencies` (for regular released dependencies) +* `spring-boot-loader` (for everything under `org/springframework/boot/loader`) +* `snapshot-dependencies` (for snapshot dependencies) +* `application` (for application classes and resources) + +The following shows an example of a `layers.idx` file: + +[source,yaml,indent=0,subs="verbatim"] +---- + - "dependencies": + - BOOT-INF/lib/library1.jar + - BOOT-INF/lib/library2.jar + - "spring-boot-loader": + - org/springframework/boot/loader/JarLauncher.class + - org/springframework/boot/loader/jar/JarEntry.class + - "snapshot-dependencies": + - BOOT-INF/lib/library3-SNAPSHOT.jar + - "application": + - META-INF/MANIFEST.MF + - BOOT-INF/classes/a/b/C.class +---- + +This layering is designed to separate code based on how likely it is to change between application builds. +Library code is less likely to change between builds, so it is placed in its own layers to allow tooling to re-use the layers from cache. +Application code is more likely to change between builds so it is isolated in a separate layer. + +Spring Boot also supports layering for war files with the help of a `layers.idx`. + +For Maven, refer to the {spring-boot-maven-plugin-docs}#repackage-layers[packaging layered jar or war section] for more details on adding a layer index to the archive. +For Gradle, refer to the {spring-boot-gradle-plugin-docs}#packaging-layered-archives[packaging layered jar or war section] of the Gradle plugin documentation. + + + +[[features.container-images.building]] +=== Building Container Images +Spring Boot applications can be containerized <>, or by <>. + + + +[[features.container-images.building.dockerfiles]] +==== Dockerfiles +While it is possible to convert a Spring Boot fat jar into a docker image with just a few lines in the Dockerfile, we will use the <> to create an optimized docker image. +When you create a jar containing the layers index file, the `spring-boot-jarmode-layertools` jar will be added as a dependency to your jar. +With this jar on the classpath, you can launch your application in a special mode which allows the bootstrap code to run something entirely different from your application, for example, something that extracts the layers. + +CAUTION: The `layertools` mode can not be used with a <> that includes a launch script. +Disable launch script configuration when building a jar file that is intended to be used with `layertools`. + +Here’s how you can launch your jar with a `layertools` jar mode: + +[source,shell,indent=0,subs="verbatim"] +---- +$ java -Djarmode=layertools -jar my-app.jar +---- + +This will provide the following output: + +[subs="verbatim"] +---- +Usage: + java -Djarmode=layertools -jar my-app.jar + +Available commands: + list List layers from the jar that can be extracted + extract Extracts layers from the jar for image creation + help Help about any command +---- + +The `extract` command can be used to easily split the application into layers to be added to the dockerfile. +Here's an example of a Dockerfile using `jarmode`. + +[source,dockerfile,indent=0,subs="verbatim"] +---- +FROM adoptopenjdk:11-jre-hotspot as builder +WORKDIR application +ARG JAR_FILE=target/*.jar +COPY ${JAR_FILE} application.jar +RUN java -Djarmode=layertools -jar application.jar extract + +FROM adoptopenjdk:11-jre-hotspot +WORKDIR application +COPY --from=builder application/dependencies/ ./ +COPY --from=builder application/spring-boot-loader/ ./ +COPY --from=builder application/snapshot-dependencies/ ./ +COPY --from=builder application/application/ ./ +ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"] +---- + +Assuming the above `Dockerfile` is in the current directory, your docker image can be built with `docker build .`, or optionally specifying the path to your application jar, as shown in the following example: + +[source,shell,indent=0,subs="verbatim"] +---- + $ docker build --build-arg JAR_FILE=path/to/myapp.jar . +---- + +This is a multi-stage dockerfile. +The builder stage extracts the directories that are needed later. +Each of the `COPY` commands relates to the layers extracted by the jarmode. + +Of course, a Dockerfile can be written without using the jarmode. +You can use some combination of `unzip` and `mv` to move things to the right layer but jarmode simplifies that. + + + +[[features.container-images.building.buildpacks]] +==== Cloud Native Buildpacks +Dockerfiles are just one way to build docker images. +Another way to build docker images is directly from your Maven or Gradle plugin, using buildpacks. +If you’ve ever used an application platform such as Cloud Foundry or Heroku then you’ve probably used a buildpack. +Buildpacks are the part of the platform that takes your application and converts it into something that the platform can actually run. +For example, Cloud Foundry’s Java buildpack will notice that you’re pushing a `.jar` file and automatically add a relevant JRE. + +With Cloud Native Buildpacks, you can create Docker compatible images that you can run anywhere. +Spring Boot includes buildpack support directly for both Maven and Gradle. +This means you can just type a single command and quickly get a sensible image into your locally running Docker daemon. + +Refer to the individual plugin documentation on how to use buildpacks with {spring-boot-maven-plugin-docs}#build-image[Maven] and {spring-boot-gradle-plugin-docs}#build-image[Gradle]. + +NOTE: The https://github.com/paketo-buildpacks/spring-boot[Paketo Spring Boot buildpack] has also been updated to support the `layers.idx` file so any customization that is applied to it will be reflected in the image created by the buildpack. + +NOTE: In order to achieve reproducible builds and container image caching, Buildpacks can manipulate the application resources metadata (such as the file "last modified" information). +You should ensure that your application does not rely on that metadata at runtime. +Spring Boot can use that information when serving static resources, but this can be disabled with configprop:spring.web.resources.cache.use-last-modified[] diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/developing-auto-configuration.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/developing-auto-configuration.adoc new file mode 100644 index 000000000000..6703efb4f5f7 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/developing-auto-configuration.adoc @@ -0,0 +1,353 @@ +[[features.developing-auto-configuration]] +== Creating Your Own Auto-configuration +If you work in a company that develops shared libraries, or if you work on an open-source or commercial library, you might want to develop your own auto-configuration. +Auto-configuration classes can be bundled in external jars and still be picked-up by Spring Boot. + +Auto-configuration can be associated to a "`starter`" that provides the auto-configuration code as well as the typical libraries that you would use with it. +We first cover what you need to know to build your own auto-configuration and then we move on to the <>. + +TIP: A https://github.com/snicoll-demos/spring-boot-master-auto-configuration[demo project] is available to showcase how you can create a starter step-by-step. + + + +[[features.developing-auto-configuration.understanding-auto-configured-beans]] +=== Understanding Auto-configured Beans +Under the hood, auto-configuration is implemented with standard `@Configuration` classes. +Additional `@Conditional` annotations are used to constrain when the auto-configuration should apply. +Usually, auto-configuration classes use `@ConditionalOnClass` and `@ConditionalOnMissingBean` annotations. +This ensures that auto-configuration applies only when relevant classes are found and when you have not declared your own `@Configuration`. + +You can browse the source code of {spring-boot-autoconfigure-module-code}[`spring-boot-autoconfigure`] to see the `@Configuration` classes that Spring provides (see the {spring-boot-code}/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories[`META-INF/spring.factories`] file). + + + +[[features.developing-auto-configuration.locating-auto-configuration-candidates]] +=== Locating Auto-configuration Candidates +Spring Boot checks for the presence of a `META-INF/spring.factories` file within your published jar. +The file should list your configuration classes under the `EnableAutoConfiguration` key, as shown in the following example: + +[indent=0] +---- + org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.mycorp.libx.autoconfigure.LibXAutoConfiguration,\ + com.mycorp.libx.autoconfigure.LibXWebAutoConfiguration +---- + +NOTE: Auto-configurations must be loaded that way _only_. +Make sure that they are defined in a specific package space and that they are never the target of component scanning. +Furthermore, auto-configuration classes should not enable component scanning to find additional components. +Specific ``@Import``s should be used instead. + +You can use the {spring-boot-autoconfigure-module-code}/AutoConfigureAfter.java[`@AutoConfigureAfter`] or {spring-boot-autoconfigure-module-code}/AutoConfigureBefore.java[`@AutoConfigureBefore`] annotations if your configuration needs to be applied in a specific order. +For example, if you provide web-specific configuration, your class may need to be applied after `WebMvcAutoConfiguration`. + +If you want to order certain auto-configurations that should not have any direct knowledge of each other, you can also use `@AutoConfigureOrder`. +That annotation has the same semantic as the regular `@Order` annotation but provides a dedicated order for auto-configuration classes. + +As with standard `@Configuration` classes, the order in which auto-configuration classes are applied only affects the order in which their beans are defined. +The order in which those beans are subsequently created is unaffected and is determined by each bean's dependencies and any `@DependsOn` relationships. + + + +[[features.developing-auto-configuration.condition-annotations]] +=== Condition Annotations +You almost always want to include one or more `@Conditional` annotations on your auto-configuration class. +The `@ConditionalOnMissingBean` annotation is one common example that is used to allow developers to override auto-configuration if they are not happy with your defaults. + +Spring Boot includes a number of `@Conditional` annotations that you can reuse in your own code by annotating `@Configuration` classes or individual `@Bean` methods. +These annotations include: + +* <> +* <> +* <> +* <> +* <> +* <> + + + +[[features.developing-auto-configuration.condition-annotations.class-conditions]] +==== Class Conditions +The `@ConditionalOnClass` and `@ConditionalOnMissingClass` annotations let `@Configuration` classes be included based on the presence or absence of specific classes. +Due to the fact that annotation metadata is parsed by using https://asm.ow2.io/[ASM], you can use the `value` attribute to refer to the real class, even though that class might not actually appear on the running application classpath. +You can also use the `name` attribute if you prefer to specify the class name by using a `String` value. + +This mechanism does not apply the same way to `@Bean` methods where typically the return type is the target of the condition: before the condition on the method applies, the JVM will have loaded the class and potentially processed method references which will fail if the class is not present. + +To handle this scenario, a separate `@Configuration` class can be used to isolate the condition, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/developingautoconfiguration/conditionannotations/classconditions/MyAutoConfiguration.java[] +---- + +TIP: If you use `@ConditionalOnClass` or `@ConditionalOnMissingClass` as a part of a meta-annotation to compose your own composed annotations, you must use `name` as referring to the class in such a case is not handled. + + + +[[features.developing-auto-configuration.condition-annotations.bean-conditions]] +==== Bean Conditions +The `@ConditionalOnBean` and `@ConditionalOnMissingBean` annotations let a bean be included based on the presence or absence of specific beans. +You can use the `value` attribute to specify beans by type or `name` to specify beans by name. +The `search` attribute lets you limit the `ApplicationContext` hierarchy that should be considered when searching for beans. + +When placed on a `@Bean` method, the target type defaults to the return type of the method, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/developingautoconfiguration/conditionannotations/beanconditions/MyAutoConfiguration.java[] +---- + +In the preceding example, the `someService` bean is going to be created if no bean of type `SomeService` is already contained in the `ApplicationContext`. + +TIP: You need to be very careful about the order in which bean definitions are added, as these conditions are evaluated based on what has been processed so far. +For this reason, we recommend using only `@ConditionalOnBean` and `@ConditionalOnMissingBean` annotations on auto-configuration classes (since these are guaranteed to load after any user-defined bean definitions have been added). + +NOTE: `@ConditionalOnBean` and `@ConditionalOnMissingBean` do not prevent `@Configuration` classes from being created. +The only difference between using these conditions at the class level and marking each contained `@Bean` method with the annotation is that the former prevents registration of the `@Configuration` class as a bean if the condition does not match. + +TIP: When declaring a `@Bean` method, provide as much type information as possible in the method's return type. +For example, if your bean's concrete class implements an interface the bean method's return type should be the concrete class and not the interface. +Providing as much type information as possible in `@Bean` methods is particularly important when using bean conditions as their evaluation can only rely upon to type information that's available in the method signature. + + + +[[features.developing-auto-configuration.condition-annotations.property-conditions]] +==== Property Conditions +The `@ConditionalOnProperty` annotation lets configuration be included based on a Spring Environment property. +Use the `prefix` and `name` attributes to specify the property that should be checked. +By default, any property that exists and is not equal to `false` is matched. +You can also create more advanced checks by using the `havingValue` and `matchIfMissing` attributes. + + + +[[features.developing-auto-configuration.condition-annotations.resource-conditions]] +==== Resource Conditions +The `@ConditionalOnResource` annotation lets configuration be included only when a specific resource is present. +Resources can be specified by using the usual Spring conventions, as shown in the following example: `file:/home/user/test.dat`. + + + +[[features.developing-auto-configuration.condition-annotations.web-application-conditions]] +==== Web Application Conditions +The `@ConditionalOnWebApplication` and `@ConditionalOnNotWebApplication` annotations let configuration be included depending on whether the application is a "`web application`". +A servlet-based web application is any application that uses a Spring `WebApplicationContext`, defines a `session` scope, or has a `ConfigurableWebEnvironment`. +A reactive web application is any application that uses a `ReactiveWebApplicationContext`, or has a `ConfigurableReactiveWebEnvironment`. + +The `@ConditionalOnWarDeployment` annotation lets configuration be included depending on whether the application is a traditional WAR application that is deployed to a container. +This condition will not match for applications that are run with an embedded server. + + + +[[features.developing-auto-configuration.condition-annotations.spel-conditions]] +==== SpEL Expression Conditions +The `@ConditionalOnExpression` annotation lets configuration be included based on the result of a {spring-framework-docs}/core.html#expressions[SpEL expression]. + +NOTE: Referencing a bean in the expression will cause that bean to be initialized very early in context refresh processing. +As a result, the bean won't be eligible for post-processing (such as configuration properties binding) and its state may be incomplete. + + + +[[features.developing-auto-configuration.testing]] +=== Testing your Auto-configuration +An auto-configuration can be affected by many factors: user configuration (`@Bean` definition and `Environment` customization), condition evaluation (presence of a particular library), and others. +Concretely, each test should create a well defined `ApplicationContext` that represents a combination of those customizations. +`ApplicationContextRunner` provides a great way to achieve that. + +`ApplicationContextRunner` is usually defined as a field of the test class to gather the base, common configuration. +The following example makes sure that `MyServiceAutoConfiguration` is always invoked: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/developingautoconfiguration/testing/MyServiceAutoConfigurationTests.java[tag=runner] +---- + +TIP: If multiple auto-configurations have to be defined, there is no need to order their declarations as they are invoked in the exact same order as when running the application. + +Each test can use the runner to represent a particular use case. +For instance, the sample below invokes a user configuration (`UserConfiguration`) and checks that the auto-configuration backs off properly. +Invoking `run` provides a callback context that can be used with `AssertJ`. + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/developingautoconfiguration/testing/MyServiceAutoConfigurationTests.java[tag=test-user-config] +---- + +It is also possible to easily customize the `Environment`, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/developingautoconfiguration/testing/MyServiceAutoConfigurationTests.java[tag=test-env] +---- + +The runner can also be used to display the `ConditionEvaluationReport`. +The report can be printed at `INFO` or `DEBUG` level. +The following example shows how to use the `ConditionEvaluationReportLoggingListener` to print the report in auto-configuration tests. + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/developingautoconfiguration/testing/MyConditionEvaluationReportingTests.java[] +---- + + + +[[features.developing-auto-configuration.testing.simulating-a-web-context]] +==== Simulating a Web Context +If you need to test an auto-configuration that only operates in a Servlet or Reactive web application context, use the `WebApplicationContextRunner` or `ReactiveWebApplicationContextRunner` respectively. + + + +[[features.developing-auto-configuration.testing.overriding-classpath]] +==== Overriding the Classpath +It is also possible to test what happens when a particular class and/or package is not present at runtime. +Spring Boot ships with a `FilteredClassLoader` that can easily be used by the runner. +In the following example, we assert that if `MyService` is not present, the auto-configuration is properly disabled: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/developingautoconfiguration/testing/MyServiceAutoConfigurationTests.java[tag=test-classloader] +---- + + + +[[features.developing-auto-configuration.custom-starter]] +=== Creating Your Own Starter +A typical Spring Boot starter contains code to auto-configure and customize the infrastructure of a given technology, let's call that "acme". +To make it easily extensible, a number of configuration keys in a dedicated namespace can be exposed to the environment. +Finally, a single "starter" dependency is provided to help users get started as easily as possible. + +Concretely, a custom starter can contain the following: + +* The `autoconfigure` module that contains the auto-configuration code for "acme". +* The `starter` module that provides a dependency to the `autoconfigure` module as well as "acme" and any additional dependencies that are typically useful. +In a nutshell, adding the starter should provide everything needed to start using that library. + +This separation in two modules is in no way necessary. +If "acme" has several flavors, options or optional features, then it is better to separate the auto-configuration as you can clearly express the fact some features are optional. +Besides, you have the ability to craft a starter that provides an opinion about those optional dependencies. +At the same time, others can rely only on the `autoconfigure` module and craft their own starter with different opinions. + +If the auto-configuration is relatively straightforward and does not have optional feature, merging the two modules in the starter is definitely an option. + + + +[[features.developing-auto-configuration.custom-starter.naming]] +==== Naming +You should make sure to provide a proper namespace for your starter. +Do not start your module names with `spring-boot`, even if you use a different Maven `groupId`. +We may offer official support for the thing you auto-configure in the future. + +As a rule of thumb, you should name a combined module after the starter. +For example, assume that you are creating a starter for "acme" and that you name the auto-configure module `acme-spring-boot` and the starter `acme-spring-boot-starter`. +If you only have one module that combines the two, name it `acme-spring-boot-starter`. + + + +[[features.developing-auto-configuration.custom-starter.configuration-keys]] +==== Configuration keys +If your starter provides configuration keys, use a unique namespace for them. +In particular, do not include your keys in the namespaces that Spring Boot uses (such as `server`, `management`, `spring`, and so on). +If you use the same namespace, we may modify these namespaces in the future in ways that break your modules. +As a rule of thumb, prefix all your keys with a namespace that you own (e.g. `acme`). + +Make sure that configuration keys are documented by adding field javadoc for each property, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/developingautoconfiguration/customstarter/configurationkeys/AcmeProperties.java[] +---- + +NOTE: You should only use plain text with `@ConfigurationProperties` field Javadoc, since they are not processed before being added to the JSON. + +Here are some rules we follow internally to make sure descriptions are consistent: + +* Do not start the description by "The" or "A". +* For `boolean` types, start the description with "Whether" or "Enable". +* For collection-based types, start the description with "Comma-separated list" +* Use `java.time.Duration` rather than `long` and describe the default unit if it differs from milliseconds, e.g. "If a duration suffix is not specified, seconds will be used". +* Do not provide the default value in the description unless it has to be determined at runtime. + +Make sure to <> so that IDE assistance is available for your keys as well. +You may want to review the generated metadata (`META-INF/spring-configuration-metadata.json`) to make sure your keys are properly documented. +Using your own starter in a compatible IDE is also a good idea to validate that quality of the metadata. + + + +[[features.developing-auto-configuration.custom-starter.autoconfigure-module]] +==== The "`autoconfigure`" Module +The `autoconfigure` module contains everything that is necessary to get started with the library. +It may also contain configuration key definitions (such as `@ConfigurationProperties`) and any callback interface that can be used to further customize how the components are initialized. + +TIP: You should mark the dependencies to the library as optional so that you can include the `autoconfigure` module in your projects more easily. +If you do it that way, the library is not provided and, by default, Spring Boot backs off. + +Spring Boot uses an annotation processor to collect the conditions on auto-configurations in a metadata file (`META-INF/spring-autoconfigure-metadata.properties`). +If that file is present, it is used to eagerly filter auto-configurations that do not match, which will improve startup time. +It is recommended to add the following dependency in a module that contains auto-configurations: + +[source,xml,indent=0,subs="verbatim"] +---- + + org.springframework.boot + spring-boot-autoconfigure-processor + true + +---- + +If you have defined auto-configurations directly in your application, make sure to configure the `spring-boot-maven-plugin` to prevent the `repackage` goal from adding the dependency into the fat jar: + +[source,xml,indent=0,subs="verbatim"] +---- + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.springframework.boot + spring-boot-autoconfigure-processor + + + + + + + +---- + +With Gradle 4.5 and earlier, the dependency should be declared in the `compileOnly` configuration, as shown in the following example: + +[source,gradle,indent=0,subs="verbatim"] +---- + dependencies { + compileOnly "org.springframework.boot:spring-boot-autoconfigure-processor" + } +---- + +With Gradle 4.6 and later, the dependency should be declared in the `annotationProcessor` configuration, as shown in the following example: + +[source,gradle,indent=0,subs="verbatim"] +---- + dependencies { + annotationProcessor "org.springframework.boot:spring-boot-autoconfigure-processor" + } +---- + + + +[[features.developing-auto-configuration.custom-starter.starter-module]] +==== Starter Module +The starter is really an empty jar. +Its only purpose is to provide the necessary dependencies to work with the library. +You can think of it as an opinionated view of what is required to get started. + +Do not make assumptions about the project in which your starter is added. +If the library you are auto-configuring typically requires other starters, mention them as well. +Providing a proper set of _default_ dependencies may be hard if the number of optional dependencies is high, as you should avoid including dependencies that are unnecessary for a typical usage of the library. +In other words, you should not include optional dependencies. + +NOTE: Either way, your starter must reference the core Spring Boot starter (`spring-boot-starter`) directly or indirectly (i.e. no need to add it if your starter relies on another starter). +If a project is created with only your custom starter, Spring Boot's core features will be honoured by the presence of the core starter. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/developing-web-applications.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/developing-web-applications.adoc new file mode 100644 index 000000000000..6afd43e6d6b8 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/developing-web-applications.adoc @@ -0,0 +1,922 @@ +[[features.developing-web-applications]] +== Developing Web Applications +Spring Boot is well suited for web application development. +You can create a self-contained HTTP server by using embedded Tomcat, Jetty, Undertow, or Netty. +Most web applications use the `spring-boot-starter-web` module to get up and running quickly. +You can also choose to build reactive web applications by using the `spring-boot-starter-webflux` module. + +If you have not yet developed a Spring Boot web application, you can follow the "Hello World!" example in the _<>_ section. + + + +[[features.developing-web-applications.spring-mvc]] +=== The "`Spring Web MVC Framework`" +The {spring-framework-docs}/web.html#mvc[Spring Web MVC framework] (often referred to as "`Spring MVC`") is a rich "`model view controller`" web framework. +Spring MVC lets you create special `@Controller` or `@RestController` beans to handle incoming HTTP requests. +Methods in your controller are mapped to HTTP by using `@RequestMapping` annotations. + +The following code shows a typical `@RestController` that serves JSON data: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/developingwebapplications/springmvc/MyRestController.java[] +---- + +"`WebMvc.fn`", the functional variant, separates the routing configuration from the actual handling of the requests, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/developingwebapplications/springmvc/MyRoutingConfiguration.java[] +---- + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/developingwebapplications/springmvc/MyUserHandler.java[] +---- + +Spring MVC is part of the core Spring Framework, and detailed information is available in the {spring-framework-docs}/web.html#mvc[reference documentation]. +There are also several guides that cover Spring MVC available at https://spring.io/guides. + +TIP: You can define as many `RouterFunction` beans as you like to modularize the definition of the router. +Beans can be ordered if you need to apply a precedence. + + + +[[features.developing-web-applications.spring-mvc.auto-configuration]] +==== Spring MVC Auto-configuration +Spring Boot provides auto-configuration for Spring MVC that works well with most applications. + +The auto-configuration adds the following features on top of Spring's defaults: + +* Inclusion of `ContentNegotiatingViewResolver` and `BeanNameViewResolver` beans. +* Support for serving static resources, including support for WebJars (covered <>). +* Automatic registration of `Converter`, `GenericConverter`, and `Formatter` beans. +* Support for `HttpMessageConverters` (covered <>). +* Automatic registration of `MessageCodesResolver` (covered <>). +* Static `index.html` support. +* Automatic use of a `ConfigurableWebBindingInitializer` bean (covered <>). + +If you want to keep those Spring Boot MVC customizations and make more {spring-framework-docs}/web.html#mvc[MVC customizations] (interceptors, formatters, view controllers, and other features), you can add your own `@Configuration` class of type `WebMvcConfigurer` but *without* `@EnableWebMvc`. + +If you want to provide custom instances of `RequestMappingHandlerMapping`, `RequestMappingHandlerAdapter`, or `ExceptionHandlerExceptionResolver`, and still keep the Spring Boot MVC customizations, you can declare a bean of type `WebMvcRegistrations` and use it to provide custom instances of those components. + +If you want to take complete control of Spring MVC, you can add your own `@Configuration` annotated with `@EnableWebMvc`, or alternatively add your own `@Configuration`-annotated `DelegatingWebMvcConfiguration` as described in the Javadoc of `@EnableWebMvc`. + +[NOTE] +==== +Spring MVC uses a different `ConversionService` to the one used to convert values from your `application.properties` or `application.yaml` file. +It means that `Period`, `Duration` and `DataSize` converters are not available and that `@DurationUnit` and `@DataSizeUnit` annotations will be ignored. + +If you want to customize the `ConversionService` used by Spring MVC, you can provide a `WebMvcConfigurer` bean with an `addFormatters` method. +From this method you can register any converter that you like, or you can delegate to the static methods available on `ApplicationConversionService`. +==== + + + +[[features.developing-web-applications.spring-mvc.message-converters]] +==== HttpMessageConverters +Spring MVC uses the `HttpMessageConverter` interface to convert HTTP requests and responses. +Sensible defaults are included out of the box. +For example, objects can be automatically converted to JSON (by using the Jackson library) or XML (by using the Jackson XML extension, if available, or by using JAXB if the Jackson XML extension is not available). +By default, strings are encoded in `UTF-8`. + +If you need to add or customize converters, you can use Spring Boot's `HttpMessageConverters` class, as shown in the following listing: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/developingwebapplications/springmvc/messageconverters/MyHttpMessageConvertersConfiguration.java[] +---- + +Any `HttpMessageConverter` bean that is present in the context is added to the list of converters. +You can also override default converters in the same way. + + + +[[features.developing-web-applications.spring-mvc.json]] +==== Custom JSON Serializers and Deserializers +If you use Jackson to serialize and deserialize JSON data, you might want to write your own `JsonSerializer` and `JsonDeserializer` classes. +Custom serializers are usually https://github.com/FasterXML/jackson-docs/wiki/JacksonHowToCustomSerializers[registered with Jackson through a module], but Spring Boot provides an alternative `@JsonComponent` annotation that makes it easier to directly register Spring Beans. + +You can use the `@JsonComponent` annotation directly on `JsonSerializer`, `JsonDeserializer` or `KeyDeserializer` implementations. +You can also use it on classes that contain serializers/deserializers as inner classes, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/developingwebapplications/springmvc/json/MyJsonComponent.java[] +---- + +All `@JsonComponent` beans in the `ApplicationContext` are automatically registered with Jackson. +Because `@JsonComponent` is meta-annotated with `@Component`, the usual component-scanning rules apply. + +Spring Boot also provides {spring-boot-module-code}/jackson/JsonObjectSerializer.java[`JsonObjectSerializer`] and {spring-boot-module-code}/jackson/JsonObjectDeserializer.java[`JsonObjectDeserializer`] base classes that provide useful alternatives to the standard Jackson versions when serializing objects. +See {spring-boot-module-api}/jackson/JsonObjectSerializer.html[`JsonObjectSerializer`] and {spring-boot-module-api}/jackson/JsonObjectDeserializer.html[`JsonObjectDeserializer`] in the Javadoc for details. + +The example above can be rewritten to use `JsonObjectSerializer`/`JsonObjectDeserializer` as follows: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/developingwebapplications/springmvc/json/object/MyJsonComponent.java[] +---- + + + +[[features.developing-web-applications.spring-mvc.message-codes]] +==== MessageCodesResolver +Spring MVC has a strategy for generating error codes for rendering error messages from binding errors: `MessageCodesResolver`. +If you set the configprop:spring.mvc.message-codes-resolver-format[] property `PREFIX_ERROR_CODE` or `POSTFIX_ERROR_CODE`, Spring Boot creates one for you (see the enumeration in {spring-framework-api}/validation/DefaultMessageCodesResolver.Format.html[`DefaultMessageCodesResolver.Format`]). + + + +[[features.developing-web-applications.spring-mvc.static-content]] +==== Static Content +By default, Spring Boot serves static content from a directory called `/static` (or `/public` or `/resources` or `/META-INF/resources`) in the classpath or from the root of the `ServletContext`. +It uses the `ResourceHttpRequestHandler` from Spring MVC so that you can modify that behavior by adding your own `WebMvcConfigurer` and overriding the `addResourceHandlers` method. + +In a stand-alone web application, the default servlet from the container is also enabled and acts as a fallback, serving content from the root of the `ServletContext` if Spring decides not to handle it. +Most of the time, this does not happen (unless you modify the default MVC configuration), because Spring can always handle requests through the `DispatcherServlet`. + +By default, resources are mapped on `+/**+`, but you can tune that with the configprop:spring.mvc.static-path-pattern[] property. +For instance, relocating all resources to `/resources/**` can be achieved as follows: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + mvc: + static-path-pattern: "/resources/**" +---- + +You can also customize the static resource locations by using the configprop:spring.web.resources.static-locations[] property (replacing the default values with a list of directory locations). +The root Servlet context path, `"/"`, is automatically added as a location as well. + +In addition to the "`standard`" static resource locations mentioned earlier, a special case is made for https://www.webjars.org/[Webjars content]. +Any resources with a path in `+/webjars/**+` are served from jar files if they are packaged in the Webjars format. + +TIP: Do not use the `src/main/webapp` directory if your application is packaged as a jar. +Although this directory is a common standard, it works *only* with war packaging, and it is silently ignored by most build tools if you generate a jar. + +Spring Boot also supports the advanced resource handling features provided by Spring MVC, allowing use cases such as cache-busting static resources or using version agnostic URLs for Webjars. + +To use version agnostic URLs for Webjars, add the `webjars-locator-core` dependency. +Then declare your Webjar. +Using jQuery as an example, adding `"/webjars/jquery/jquery.min.js"` results in `"/webjars/jquery/x.y.z/jquery.min.js"` where `x.y.z` is the Webjar version. + +NOTE: If you use JBoss, you need to declare the `webjars-locator-jboss-vfs` dependency instead of the `webjars-locator-core`. +Otherwise, all Webjars resolve as a `404`. + +To use cache busting, the following configuration configures a cache busting solution for all static resources, effectively adding a content hash, such as ``, in URLs: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + web: + resources: + chain: + strategy: + content: + enabled: true + paths: "/**" +---- + +NOTE: Links to resources are rewritten in templates at runtime, thanks to a `ResourceUrlEncodingFilter` that is auto-configured for Thymeleaf and FreeMarker. +You should manually declare this filter when using JSPs. +Other template engines are currently not automatically supported but can be with custom template macros/helpers and the use of the {spring-framework-api}/web/servlet/resource/ResourceUrlProvider.html[`ResourceUrlProvider`]. + +When loading resources dynamically with, for example, a JavaScript module loader, renaming files is not an option. +That is why other strategies are also supported and can be combined. +A "fixed" strategy adds a static version string in the URL without changing the file name, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + web: + resources: + chain: + strategy: + content: + enabled: true + paths: "/**" + fixed: + enabled: true + paths: "/js/lib/" + version: "v12" +---- + +With this configuration, JavaScript modules located under `"/js/lib/"` use a fixed versioning strategy (`"/v12/js/lib/mymodule.js"`), while other resources still use the content one (``). + +See {spring-boot-autoconfigure-module-code}/web/ResourceProperties.java[`ResourceProperties`] for more supported options. + +[TIP] +==== +This feature has been thoroughly described in a dedicated https://spring.io/blog/2014/07/24/spring-framework-4-1-handling-static-web-resources[blog post] and in Spring Framework's {spring-framework-docs}/web.html#mvc-config-static-resources[reference documentation]. +==== + + + +[[features.developing-web-applications.spring-mvc.welcome-page]] +==== Welcome Page +Spring Boot supports both static and templated welcome pages. +It first looks for an `index.html` file in the configured static content locations. +If one is not found, it then looks for an `index` template. +If either is found, it is automatically used as the welcome page of the application. + + + +[[features.developing-web-applications.spring-mvc.content-negotiation]] +==== Path Matching and Content Negotiation +Spring MVC can map incoming HTTP requests to handlers by looking at the request path and matching it to the mappings defined in your application (for example, `@GetMapping` annotations on Controller methods). + +Spring Boot chooses to disable suffix pattern matching by default, which means that requests like `"GET /projects/spring-boot.json"` won't be matched to `@GetMapping("/projects/spring-boot")` mappings. +This is considered as a {spring-framework-docs}/web.html#mvc-ann-requestmapping-suffix-pattern-match[best practice for Spring MVC applications]. +This feature was mainly useful in the past for HTTP clients which did not send proper "Accept" request headers; we needed to make sure to send the correct Content Type to the client. +Nowadays, Content Negotiation is much more reliable. + +There are other ways to deal with HTTP clients that don't consistently send proper "Accept" request headers. +Instead of using suffix matching, we can use a query parameter to ensure that requests like `"GET /projects/spring-boot?format=json"` will be mapped to `@GetMapping("/projects/spring-boot")`: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + mvc: + contentnegotiation: + favor-parameter: true +---- + +Or if you prefer to use a different parameter name: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + mvc: + contentnegotiation: + favor-parameter: true + parameter-name: "myparam" +---- + +Most standard media types are supported out-of-the-box, but you can also define new ones: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + mvc: + contentnegotiation: + media-types: + markdown: "text/markdown" +---- + + + +Suffix pattern matching is deprecated and will be removed in a future release. +If you understand the caveats and would still like your application to use suffix pattern matching, the following configuration is required: + +[source,yaml,indent=0,subs="verbatim",configblocks] +---- + spring: + mvc: + contentnegotiation: + favor-path-extension: true + pathmatch: + use-suffix-pattern: true +---- + +Alternatively, rather than open all suffix patterns, it's more secure to only support registered suffix patterns: + +[source,yaml,indent=0,subs="verbatim",configblocks] +---- + spring: + mvc: + contentnegotiation: + favor-path-extension: true + pathmatch: + use-registered-suffix-pattern: true +---- + +As of Spring Framework 5.3, Spring MVC supports several implementation strategies for matching request paths to Controller handlers. +It was previously only supporting the `AntPathMatcher` strategy, but it now also offers `PathPatternParser`. +Spring Boot now provides a configuration property to choose and opt in the new strategy: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + mvc: + pathmatch: + matching-strategy: "path-pattern-parser" +---- + +For more details on why you should consider this new implementation, please check out the +https://spring.io/blog/2020/06/30/url-matching-with-pathpattern-in-spring-mvc[dedicated blog post]. + +NOTE: `PathPatternParser` is an optimized implementation but restricts usage of +{spring-framework-docs}/web.html#mvc-ann-requestmapping-uri-templates[some path patterns variants] +and is incompatible with suffix pattern matching (configprop:spring.mvc.pathmatch.use-suffix-pattern[deprecated], +configprop:spring.mvc.pathmatch.use-registered-suffix-pattern[deprecated]) or mapping the `DispatcherServlet` +with a Servlet prefix (configprop:spring.mvc.servlet.path[]). + + + +[[features.developing-web-applications.spring-mvc.binding-initializer]] +==== ConfigurableWebBindingInitializer +Spring MVC uses a `WebBindingInitializer` to initialize a `WebDataBinder` for a particular request. +If you create your own `ConfigurableWebBindingInitializer` `@Bean`, Spring Boot automatically configures Spring MVC to use it. + + + +[[features.developing-web-applications.spring-mvc.template-engines]] +==== Template Engines +As well as REST web services, you can also use Spring MVC to serve dynamic HTML content. +Spring MVC supports a variety of templating technologies, including Thymeleaf, FreeMarker, and JSPs. +Also, many other templating engines include their own Spring MVC integrations. + +Spring Boot includes auto-configuration support for the following templating engines: + + * https://freemarker.apache.org/docs/[FreeMarker] + * https://docs.groovy-lang.org/docs/next/html/documentation/template-engines.html#_the_markuptemplateengine[Groovy] + * https://www.thymeleaf.org[Thymeleaf] + * https://mustache.github.io/[Mustache] + +TIP: If possible, JSPs should be avoided. +There are several <> when using them with embedded servlet containers. + +When you use one of these templating engines with the default configuration, your templates are picked up automatically from `src/main/resources/templates`. + +TIP: Depending on how you run your application, your IDE may order the classpath differently. +Running your application in the IDE from its main method results in a different ordering than when you run your application by using Maven or Gradle or from its packaged jar. +This can cause Spring Boot to fail to find the expected template. +If you have this problem, you can reorder the classpath in the IDE to place the module's classes and resources first. + + + +[[features.developing-web-applications.spring-mvc.error-handling]] +==== Error Handling +By default, Spring Boot provides an `/error` mapping that handles all errors in a sensible way, and it is registered as a "`global`" error page in the servlet container. +For machine clients, it produces a JSON response with details of the error, the HTTP status, and the exception message. +For browser clients, there is a "`whitelabel`" error view that renders the same data in HTML format (to customize it, add a `View` that resolves to `error`). + +There are a number of `server.error` properties that can be set if you want to customize the default error handling behavior. +See the <> section of the Appendix. + +To replace the default behavior completely, you can implement `ErrorController` and register a bean definition of that type or add a bean of type `ErrorAttributes` to use the existing mechanism but replace the contents. + +TIP: The `BasicErrorController` can be used as a base class for a custom `ErrorController`. +This is particularly useful if you want to add a handler for a new content type (the default is to handle `text/html` specifically and provide a fallback for everything else). +To do so, extend `BasicErrorController`, add a public method with a `@RequestMapping` that has a `produces` attribute, and create a bean of your new type. + +You can also define a class annotated with `@ControllerAdvice` to customize the JSON document to return for a particular controller and/or exception type, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/developingwebapplications/springmvc/errorhandling/MyControllerAdvice.java[] +---- + +In the preceding example, if `YourException` is thrown by a controller defined in the same package as `SomeController`, a JSON representation of the `CustomErrorType` POJO is used instead of the `ErrorAttributes` representation. + +In some cases, errors handled at the controller level are not recorded by the <>. +Applications can ensure that such exceptions are recorded with the request metrics by setting the handled exception as a request attribute: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/developingwebapplications/springmvc/errorhandling/MyController.java[] +---- + + + +[[features.developing-web-applications.spring-mvc.error-handling.error-pages]] +===== Custom Error Pages +If you want to display a custom HTML error page for a given status code, you can add a file to an `/error` directory. +Error pages can either be static HTML (that is, added under any of the static resource directories) or be built by using templates. +The name of the file should be the exact status code or a series mask. + +For example, to map `404` to a static HTML file, your directory structure would be as follows: + +[indent=0,subs="verbatim"] +---- + src/ + +- main/ + +- java/ + | + + +- resources/ + +- public/ + +- error/ + | +- 404.html + +- +---- + +To map all `5xx` errors by using a FreeMarker template, your directory structure would be as follows: + +[indent=0,subs="verbatim"] +---- + src/ + +- main/ + +- java/ + | + + +- resources/ + +- templates/ + +- error/ + | +- 5xx.ftlh + +- +---- + +For more complex mappings, you can also add beans that implement the `ErrorViewResolver` interface, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/developingwebapplications/springmvc/errorhandling/errorpages/MyErrorViewResolver.java[] +---- + +You can also use regular Spring MVC features such as {spring-framework-docs}/web.html#mvc-exceptionhandlers[`@ExceptionHandler` methods] and {spring-framework-docs}/web.html#mvc-ann-controller-advice[`@ControllerAdvice`]. +The `ErrorController` then picks up any unhandled exceptions. + + + +[[features.developing-web-applications.spring-mvc.error-handling.error-pages-without-spring-mvc]] +===== Mapping Error Pages outside of Spring MVC +For applications that do not use Spring MVC, you can use the `ErrorPageRegistrar` interface to directly register `ErrorPages`. +This abstraction works directly with the underlying embedded servlet container and works even if you do not have a Spring MVC `DispatcherServlet`. + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/developingwebapplications/springmvc/errorhandling/errorpageswithoutspringmvc/MyErrorPagesConfiguration.java[] +---- + +NOTE: If you register an `ErrorPage` with a path that ends up being handled by a `Filter` (as is common with some non-Spring web frameworks, like Jersey and Wicket), then the `Filter` has to be explicitly registered as an `ERROR` dispatcher, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/developingwebapplications/springmvc/errorhandling/errorpageswithoutspringmvc/MyFilterConfiguration.java[] +---- + +Note that the default `FilterRegistrationBean` does not include the `ERROR` dispatcher type. + + + +[[features.developing-web-applications.spring-mvc.error-handling.in-a-war-deployment]] +===== Error handling in a war deployment +When deployed to a servlet container, Spring Boot uses its error page filter to forward a request with an error status to the appropriate error page. +This is necessary as the Servlet specification does not provide an API for registering error pages. +Depending on the container that you are deploying your war file to and the technologies that your application uses, some additional configuration may be required. + +The error page filter can only forward the request to the correct error page if the response has not already been committed. +By default, WebSphere Application Server 8.0 and later commits the response upon successful completion of a servlet's service method. +You should disable this behavior by setting `com.ibm.ws.webcontainer.invokeFlushAfterService` to `false`. + +If you are using Spring Security and want to access the principal in an error page, you must configure Spring Security's filter to be invoked on error dispatches. +To do so, set the `spring.security.filter.dispatcher-types` property to `async, error, forward, request`. + + + +[[features.developing-web-applications.spring-mvc.spring-hateoas]] +==== Spring HATEOAS +If you develop a RESTful API that makes use of hypermedia, Spring Boot provides auto-configuration for Spring HATEOAS that works well with most applications. +The auto-configuration replaces the need to use `@EnableHypermediaSupport` and registers a number of beans to ease building hypermedia-based applications, including a `LinkDiscoverers` (for client side support) and an `ObjectMapper` configured to correctly marshal responses into the desired representation. +The `ObjectMapper` is customized by setting the various `spring.jackson.*` properties or, if one exists, by a `Jackson2ObjectMapperBuilder` bean. + +You can take control of Spring HATEOAS's configuration by using `@EnableHypermediaSupport`. +Note that doing so disables the `ObjectMapper` customization described earlier. + +WARNING: `spring-boot-starter-hateoas` is specific to Spring MVC and should not be combined with Spring WebFlux. +In order to use Spring HATEOAS with Spring WebFlux, you can add a direct dependency on `org.springframework.hateoas:spring-hateoas` along with `spring-boot-starter-webflux`. + + + +[[features.developing-web-applications.spring-mvc.cors]] +==== CORS Support +https://en.wikipedia.org/wiki/Cross-origin_resource_sharing[Cross-origin resource sharing] (CORS) is a https://www.w3.org/TR/cors/[W3C specification] implemented by https://caniuse.com/#feat=cors[most browsers] that lets you specify in a flexible way what kind of cross-domain requests are authorized., instead of using some less secure and less powerful approaches such as IFRAME or JSONP. + +As of version 4.2, Spring MVC {spring-framework-docs}/web.html#mvc-cors[supports CORS]. +Using {spring-framework-docs}/web.html#mvc-cors-controller[controller method CORS configuration] with {spring-framework-api}/web/bind/annotation/CrossOrigin.html[`@CrossOrigin`] annotations in your Spring Boot application does not require any specific configuration. +{spring-framework-docs}/web.html#mvc-cors-global[Global CORS configuration] can be defined by registering a `WebMvcConfigurer` bean with a customized `addCorsMappings(CorsRegistry)` method, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/developingwebapplications/springmvc/cors/MyCorsConfiguration.java[] +---- + + + +[[features.developing-web-applications.spring-webflux]] +=== The "`Spring WebFlux Framework`" +Spring WebFlux is the new reactive web framework introduced in Spring Framework 5.0. +Unlike Spring MVC, it does not require the Servlet API, is fully asynchronous and non-blocking, and implements the https://www.reactive-streams.org/[Reactive Streams] specification through https://projectreactor.io/[the Reactor project]. + +Spring WebFlux comes in two flavors: functional and annotation-based. +The annotation-based one is quite close to the Spring MVC model, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/developingwebapplications/springwebflux/MyRestController.java[] +---- + +"`WebFlux.fn`", the functional variant, separates the routing configuration from the actual handling of the requests, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/developingwebapplications/springwebflux/MyRoutingConfiguration.java[] +---- + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/developingwebapplications/springwebflux/MyUserHandler.java[] +---- + +WebFlux is part of the Spring Framework and detailed information is available in its {spring-framework-docs}/web-reactive.html#webflux-fn[reference documentation]. + +TIP: You can define as many `RouterFunction` beans as you like to modularize the definition of the router. +Beans can be ordered if you need to apply a precedence. + +To get started, add the `spring-boot-starter-webflux` module to your application. + +NOTE: Adding both `spring-boot-starter-web` and `spring-boot-starter-webflux` modules in your application results in Spring Boot auto-configuring Spring MVC, not WebFlux. +This behavior has been chosen because many Spring developers add `spring-boot-starter-webflux` to their Spring MVC application to use the reactive `WebClient`. +You can still enforce your choice by setting the chosen application type to `SpringApplication.setWebApplicationType(WebApplicationType.REACTIVE)`. + + + +[[features.developing-web-applications.spring-webflux.auto-configuration]] +==== Spring WebFlux Auto-configuration +Spring Boot provides auto-configuration for Spring WebFlux that works well with most applications. + +The auto-configuration adds the following features on top of Spring's defaults: + +* Configuring codecs for `HttpMessageReader` and `HttpMessageWriter` instances (described <>). +* Support for serving static resources, including support for WebJars (described <>). + +If you want to keep Spring Boot WebFlux features and you want to add additional {spring-framework-docs}/web-reactive.html#webflux-config[WebFlux configuration], you can add your own `@Configuration` class of type `WebFluxConfigurer` but *without* `@EnableWebFlux`. + +If you want to take complete control of Spring WebFlux, you can add your own `@Configuration` annotated with `@EnableWebFlux`. + + + +[[features.developing-web-applications.spring-webflux.httpcodecs]] +==== HTTP Codecs with HttpMessageReaders and HttpMessageWriters +Spring WebFlux uses the `HttpMessageReader` and `HttpMessageWriter` interfaces to convert HTTP requests and responses. +They are configured with `CodecConfigurer` to have sensible defaults by looking at the libraries available in your classpath. + +Spring Boot provides dedicated configuration properties for codecs, `+spring.codec.*+`. +It also applies further customization by using `CodecCustomizer` instances. +For example, `+spring.jackson.*+` configuration keys are applied to the Jackson codec. + +If you need to add or customize codecs, you can create a custom `CodecCustomizer` component, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/developingwebapplications/springwebflux/httpcodecs/MyCodecsConfiguration.java[] +---- + +You can also leverage <>. + + + +[[features.developing-web-applications.spring-webflux.static-content]] +==== Static Content +By default, Spring Boot serves static content from a directory called `/static` (or `/public` or `/resources` or `/META-INF/resources`) in the classpath. +It uses the `ResourceWebHandler` from Spring WebFlux so that you can modify that behavior by adding your own `WebFluxConfigurer` and overriding the `addResourceHandlers` method. + +By default, resources are mapped on `+/**+`, but you can tune that by setting the configprop:spring.webflux.static-path-pattern[] property. +For instance, relocating all resources to `/resources/**` can be achieved as follows: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + webflux: + static-path-pattern: "/resources/**" +---- + +You can also customize the static resource locations by using `spring.web.resources.static-locations`. +Doing so replaces the default values with a list of directory locations. +If you do so, the default welcome page detection switches to your custom locations. +So, if there is an `index.html` in any of your locations on startup, it is the home page of the application. + +In addition to the "`standard`" static resource locations listed earlier, a special case is made for https://www.webjars.org/[Webjars content]. +Any resources with a path in `+/webjars/**+` are served from jar files if they are packaged in the Webjars format. + +TIP: Spring WebFlux applications do not strictly depend on the Servlet API, so they cannot be deployed as war files and do not use the `src/main/webapp` directory. + + + +[[features.developing-web-applications.spring-webflux.welcome-page]] +==== Welcome Page +Spring Boot supports both static and templated welcome pages. +It first looks for an `index.html` file in the configured static content locations. +If one is not found, it then looks for an `index` template. +If either is found, it is automatically used as the welcome page of the application. + + + +[[features.developing-web-applications.spring-webflux.template-engines]] +==== Template Engines +As well as REST web services, you can also use Spring WebFlux to serve dynamic HTML content. +Spring WebFlux supports a variety of templating technologies, including Thymeleaf, FreeMarker, and Mustache. + +Spring Boot includes auto-configuration support for the following templating engines: + + * https://freemarker.apache.org/docs/[FreeMarker] + * https://www.thymeleaf.org[Thymeleaf] + * https://mustache.github.io/[Mustache] + +When you use one of these templating engines with the default configuration, your templates are picked up automatically from `src/main/resources/templates`. + + + +[[features.developing-web-applications.spring-webflux.error-handling]] +==== Error Handling +Spring Boot provides a `WebExceptionHandler` that handles all errors in a sensible way. +Its position in the processing order is immediately before the handlers provided by WebFlux, which are considered last. +For machine clients, it produces a JSON response with details of the error, the HTTP status, and the exception message. +For browser clients, there is a "`whitelabel`" error handler that renders the same data in HTML format. +You can also provide your own HTML templates to display errors (see the <>). + +The first step to customizing this feature often involves using the existing mechanism but replacing or augmenting the error contents. +For that, you can add a bean of type `ErrorAttributes`. + +To change the error handling behavior, you can implement `ErrorWebExceptionHandler` and register a bean definition of that type. +Because a `ErrorWebExceptionHandler` is quite low-level, Spring Boot also provides a convenient `AbstractErrorWebExceptionHandler` to let you handle errors in a WebFlux functional way, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/developingwebapplications/springwebflux/errorhandling/MyErrorWebExceptionHandler.java[] +---- + +For a more complete picture, you can also subclass `DefaultErrorWebExceptionHandler` directly and override specific methods. + +In some cases, errors handled at the controller or handler function level are not recorded by the <>. +Applications can ensure that such exceptions are recorded with the request metrics by setting the handled exception as a request attribute: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/developingwebapplications/springwebflux/errorhandling/MyExceptionHandlingController.java[] +---- + + + +[[features.developing-web-applications.spring-webflux.error-handling.error-pages]] +===== Custom Error Pages +If you want to display a custom HTML error page for a given status code, you can add a file to an `/error` directory. +Error pages can either be static HTML (that is, added under any of the static resource directories) or built with templates. +The name of the file should be the exact status code or a series mask. + +For example, to map `404` to a static HTML file, your directory structure would be as follows: + +[source,indent=0,subs="verbatim"] +---- + src/ + +- main/ + +- java/ + | + + +- resources/ + +- public/ + +- error/ + | +- 404.html + +- +---- + +To map all `5xx` errors by using a Mustache template, your directory structure would be as follows: + +[source,indent=0,subs="verbatim"] +---- + src/ + +- main/ + +- java/ + | + + +- resources/ + +- templates/ + +- error/ + | +- 5xx.mustache + +- +---- + + + +[[features.developing-web-applications.spring-webflux.web-filters]] +==== Web Filters +Spring WebFlux provides a `WebFilter` interface that can be implemented to filter HTTP request-response exchanges. +`WebFilter` beans found in the application context will be automatically used to filter each exchange. + +Where the order of the filters is important they can implement `Ordered` or be annotated with `@Order`. +Spring Boot auto-configuration may configure web filters for you. +When it does so, the orders shown in the following table will be used: + +|=== +| Web Filter | Order + +| `MetricsWebFilter` +| `Ordered.HIGHEST_PRECEDENCE + 1` + +| `WebFilterChainProxy` (Spring Security) +| `-100` + +| `HttpTraceWebFilter` +| `Ordered.LOWEST_PRECEDENCE - 10` +|=== + + + +[[features.developing-web-applications.jersey]] +=== JAX-RS and Jersey +If you prefer the JAX-RS programming model for REST endpoints, you can use one of the available implementations instead of Spring MVC. +https://jersey.github.io/[Jersey] and https://cxf.apache.org/[Apache CXF] work quite well out of the box. +CXF requires you to register its `Servlet` or `Filter` as a `@Bean` in your application context. +Jersey has some native Spring support, so we also provide auto-configuration support for it in Spring Boot, together with a starter. + +To get started with Jersey, include the `spring-boot-starter-jersey` as a dependency and then you need one `@Bean` of type `ResourceConfig` in which you register all the endpoints, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/developingwebapplications/jersey/MyJerseyConfig.java[] +---- + +WARNING: Jersey's support for scanning executable archives is rather limited. +For example, it cannot scan for endpoints in a package found in a <> or in `WEB-INF/classes` when running an executable war file. +To avoid this limitation, the `packages` method should not be used, and endpoints should be registered individually by using the `register` method, as shown in the preceding example. + +For more advanced customizations, you can also register an arbitrary number of beans that implement `ResourceConfigCustomizer`. + +All the registered endpoints should be `@Components` with HTTP resource annotations (`@GET` and others), as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/developingwebapplications/jersey/MyEndpoint.java[] +---- + +Since the `Endpoint` is a Spring `@Component`, its lifecycle is managed by Spring and you can use the `@Autowired` annotation to inject dependencies and use the `@Value` annotation to inject external configuration. +By default, the Jersey servlet is registered and mapped to `/*`. +You can change the mapping by adding `@ApplicationPath` to your `ResourceConfig`. + +By default, Jersey is set up as a Servlet in a `@Bean` of type `ServletRegistrationBean` named `jerseyServletRegistration`. +By default, the servlet is initialized lazily, but you can customize that behavior by setting `spring.jersey.servlet.load-on-startup`. +You can disable or override that bean by creating one of your own with the same name. +You can also use a filter instead of a servlet by setting `spring.jersey.type=filter` (in which case, the `@Bean` to replace or override is `jerseyFilterRegistration`). +The filter has an `@Order`, which you can set with `spring.jersey.filter.order`. +When using Jersey as a filter, a Servlet that will handle any requests that are not intercepted by Jersey must be present. +If your application does not contain such a servlet, you may want to enable the default servlet by setting configprop:server.servlet.register-default-servlet[] to `true`. +Both the servlet and the filter registrations can be given init parameters by using `spring.jersey.init.*` to specify a map of properties. + + + +[[features.developing-web-applications.embedded-container]] +=== Embedded Servlet Container Support +Spring Boot includes support for embedded https://tomcat.apache.org/[Tomcat], https://www.eclipse.org/jetty/[Jetty], and https://github.com/undertow-io/undertow[Undertow] servers. +Most developers use the appropriate "`Starter`" to obtain a fully configured instance. +By default, the embedded server listens for HTTP requests on port `8080`. + + + +[[features.developing-web-applications.embedded-container.servlets-filters-listeners]] +==== Servlets, Filters, and listeners +When using an embedded servlet container, you can register servlets, filters, and all the listeners (such as `HttpSessionListener`) from the Servlet spec, either by using Spring beans or by scanning for Servlet components. + + + +[[features.developing-web-applications.embedded-container.servlets-filters-listeners.beans]] +===== Registering Servlets, Filters, and Listeners as Spring Beans +Any `Servlet`, `Filter`, or servlet `*Listener` instance that is a Spring bean is registered with the embedded container. +This can be particularly convenient if you want to refer to a value from your `application.properties` during configuration. + +By default, if the context contains only a single Servlet, it is mapped to `/`. +In the case of multiple servlet beans, the bean name is used as a path prefix. +Filters map to `+/*+`. + +If convention-based mapping is not flexible enough, you can use the `ServletRegistrationBean`, `FilterRegistrationBean`, and `ServletListenerRegistrationBean` classes for complete control. + +It is usually safe to leave Filter beans unordered. +If a specific order is required, you should annotate the `Filter` with `@Order` or make it implement `Ordered`. +You cannot configure the order of a `Filter` by annotating its bean method with `@Order`. +If you cannot change the `Filter` class to add `@Order` or implement `Ordered`, you must define a `FilterRegistrationBean` for the `Filter` and set the registration bean's order using the `setOrder(int)` method. +Avoid configuring a Filter that reads the request body at `Ordered.HIGHEST_PRECEDENCE`, since it might go against the character encoding configuration of your application. +If a Servlet filter wraps the request, it should be configured with an order that is less than or equal to `OrderedFilter.REQUEST_WRAPPER_FILTER_MAX_ORDER`. + +TIP: To see the order of every `Filter` in your application, enable debug level logging for the `web` <> (`logging.level.web=debug`). +Details of the registered filters, including their order and URL patterns, will then be logged at startup. + +WARNING: Take care when registering `Filter` beans since they are initialized very early in the application lifecycle. +If you need to register a `Filter` that interacts with other beans, consider using a {spring-boot-module-api}/web/servlet/DelegatingFilterProxyRegistrationBean.html[`DelegatingFilterProxyRegistrationBean`] instead. + + + +[[features.developing-web-applications.embedded-container.context-initializer]] +==== Servlet Context Initialization +Embedded servlet containers do not directly execute the Servlet 3.0+ `javax.servlet.ServletContainerInitializer` interface or Spring's `org.springframework.web.WebApplicationInitializer` interface. +This is an intentional design decision intended to reduce the risk that third party libraries designed to run inside a war may break Spring Boot applications. + +If you need to perform servlet context initialization in a Spring Boot application, you should register a bean that implements the `org.springframework.boot.web.servlet.ServletContextInitializer` interface. +The single `onStartup` method provides access to the `ServletContext` and, if necessary, can easily be used as an adapter to an existing `WebApplicationInitializer`. + + + +[[features.developing-web-applications.embedded-container.context-initializer.scanning]] +===== Scanning for Servlets, Filters, and listeners +When using an embedded container, automatic registration of classes annotated with `@WebServlet`, `@WebFilter`, and `@WebListener` can be enabled by using `@ServletComponentScan`. + +TIP: `@ServletComponentScan` has no effect in a standalone container, where the container's built-in discovery mechanisms are used instead. + + + +[[features.developing-web-applications.embedded-container.application-context]] +==== The ServletWebServerApplicationContext +Under the hood, Spring Boot uses a different type of `ApplicationContext` for embedded servlet container support. +The `ServletWebServerApplicationContext` is a special type of `WebApplicationContext` that bootstraps itself by searching for a single `ServletWebServerFactory` bean. +Usually a `TomcatServletWebServerFactory`, `JettyServletWebServerFactory`, or `UndertowServletWebServerFactory` has been auto-configured. + +NOTE: You usually do not need to be aware of these implementation classes. +Most applications are auto-configured, and the appropriate `ApplicationContext` and `ServletWebServerFactory` are created on your behalf. + +In an embedded container setup, the `ServletContext` is set as part of server startup which happens during application context initialization. +Because of this beans in the `ApplicationContext` cannot be reliably initialized with a `ServletContext`. +One way to get around this is to inject `ApplicationContext` as a dependency of the bean and access the `ServletContext` only when it is needed. +Another way is to use a callback once the server has started. +This can be done using an `ApplicationListener` which listens for the `ApplicationStartedEvent` as follows: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/developingwebapplications/embeddedcontainer/applicationcontext/MyDemoBean.java[] +---- + + + +[[features.developing-web-applications.embedded-container.customizing]] +==== Customizing Embedded Servlet Containers +Common servlet container settings can be configured by using Spring `Environment` properties. +Usually, you would define the properties in your `application.properties` or `application.yaml` file. + +Common server settings include: + +* Network settings: Listen port for incoming HTTP requests (`server.port`), interface address to bind to `server.address`, and so on. +* Session settings: Whether the session is persistent (`server.servlet.session.persistent`), session timeout (`server.servlet.session.timeout`), location of session data (`server.servlet.session.store-dir`), and session-cookie configuration (`server.servlet.session.cookie.*`). +* Error management: Location of the error page (`server.error.path`) and so on. +* <> +* <> + +Spring Boot tries as much as possible to expose common settings, but this is not always possible. +For those cases, dedicated namespaces offer server-specific customizations (see `server.tomcat` and `server.undertow`). +For instance, <> can be configured with specific features of the embedded servlet container. + +TIP: See the {spring-boot-autoconfigure-module-code}/web/ServerProperties.java[`ServerProperties`] class for a complete list. + + + +[[features.developing-web-applications.embedded-container.customizing.programmatic]] +===== Programmatic Customization +If you need to programmatically configure your embedded servlet container, you can register a Spring bean that implements the `WebServerFactoryCustomizer` interface. +`WebServerFactoryCustomizer` provides access to the `ConfigurableServletWebServerFactory`, which includes numerous customization setter methods. +The following example shows programmatically setting the port: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/developingwebapplications/embeddedcontainer/customizing/programmatic/MyWebServerFactoryCustomizer.java[] +---- + +`TomcatServletWebServerFactory`, `JettyServletWebServerFactory` and `UndertowServletWebServerFactory` are dedicated variants of `ConfigurableServletWebServerFactory` that have additional customization setter methods for Tomcat, Jetty and Undertow respectively. +The following example shows how to customize `TomcatServletWebServerFactory` that provides access to Tomcat-specific configuration options: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/developingwebapplications/embeddedcontainer/customizing/programmatic/MyTomcatWebServerFactoryCustomizer.java[] +---- + + + +[[features.developing-web-applications.embedded-container.customizing.direct]] +===== Customizing ConfigurableServletWebServerFactory Directly +For more advanced use cases that require you to extend from `ServletWebServerFactory`, you can expose a bean of such type yourself. + +Setters are provided for many configuration options. +Several protected method "`hooks`" are also provided should you need to do something more exotic. +See the {spring-boot-module-api}/web/servlet/server/ConfigurableServletWebServerFactory.html[source code documentation] for details. + +NOTE: Auto-configured customizers are still applied on your custom factory, so use that option carefully. + + + +[[features.developing-web-applications.embedded-container.jsp-limitations]] +==== JSP Limitations +When running a Spring Boot application that uses an embedded servlet container (and is packaged as an executable archive), there are some limitations in the JSP support. + +* With Jetty and Tomcat, it should work if you use war packaging. + An executable war will work when launched with `java -jar`, and will also be deployable to any standard container. + JSPs are not supported when using an executable jar. + +* Undertow does not support JSPs. + +* Creating a custom `error.jsp` page does not override the default view for <>. + <> should be used instead. + + + +[[features.developing-web-applications.reactive-server]] +=== Embedded Reactive Server Support +Spring Boot includes support for the following embedded reactive web servers: Reactor Netty, Tomcat, Jetty, and Undertow. +Most developers use the appropriate “Starter†to obtain a fully configured instance. +By default, the embedded server listens for HTTP requests on port 8080. + + + +[[features.developing-web-applications.reactive-server-resources-configuration]] +=== Reactive Server Resources Configuration +When auto-configuring a Reactor Netty or Jetty server, Spring Boot will create specific beans that will provide HTTP resources to the server instance: `ReactorResourceFactory` or `JettyResourceFactory`. + +By default, those resources will be also shared with the Reactor Netty and Jetty clients for optimal performances, given: + +* the same technology is used for server and client +* the client instance is built using the `WebClient.Builder` bean auto-configured by Spring Boot + +Developers can override the resource configuration for Jetty and Reactor Netty by providing a custom `ReactorResourceFactory` or `JettyResourceFactory` bean - this will be applied to both clients and servers. + +You can learn more about the resource configuration on the client side in the <>. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/email.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/email.adoc new file mode 100644 index 000000000000..6409ecb71847 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/email.adoc @@ -0,0 +1,32 @@ +[[features.email]] +== Sending Email +The Spring Framework provides an abstraction for sending email by using the `JavaMailSender` interface, and Spring Boot provides auto-configuration for it as well as a starter module. + +TIP: See the {spring-framework-docs}/integration.html#mail[reference documentation] for a detailed explanation of how you can use `JavaMailSender`. + +If `spring.mail.host` and the relevant libraries (as defined by `spring-boot-starter-mail`) are available, a default `JavaMailSender` is created if none exists. +The sender can be further customized by configuration items from the `spring.mail` namespace. +See {spring-boot-autoconfigure-module-code}/mail/MailProperties.java[`MailProperties`] for more details. + +In particular, certain default timeout values are infinite, and you may want to change that to avoid having a thread blocked by an unresponsive mail server, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + mail: + properties: + "[mail.smtp.connectiontimeout]": 5000 + "[mail.smtp.timeout]": 3000 + "[mail.smtp.writetimeout]": 5000 +---- + +It is also possible to configure a `JavaMailSender` with an existing `Session` from JNDI: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + mail: + jndi-name: "mail/Session" +---- + +When a `jndi-name` is set, it takes precedence over all other Session-related settings. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/external-config.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/external-config.adoc new file mode 100644 index 000000000000..6c4878a7c833 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/external-config.adoc @@ -0,0 +1,1234 @@ +[[features.external-config]] +== Externalized Configuration +Spring Boot lets you externalize your configuration so that you can work with the same application code in different environments. +You can use a variety of external configuration sources, include Java properties files, YAML files, environment variables, and command-line arguments. + +Property values can be injected directly into your beans by using the `@Value` annotation, accessed through Spring's `Environment` abstraction, or be <> through `@ConfigurationProperties`. + +Spring Boot uses a very particular `PropertySource` order that is designed to allow sensible overriding of values. +Properties are considered in the following order (with values from lower items overriding earlier ones): + +. Default properties (specified by setting `SpringApplication.setDefaultProperties`). +. {spring-framework-api}/context/annotation/PropertySource.html[`@PropertySource`] annotations on your `@Configuration` classes. + Please note that such property sources are not added to the `Environment` until the application context is being refreshed. + This is too late to configure certain properties such as `+logging.*+` and `+spring.main.*+` which are read before refresh begins. +. Config data (such as `application.properties` files) +. A `RandomValuePropertySource` that has properties only in `+random.*+`. +. OS environment variables. +. Java System properties (`System.getProperties()`). +. JNDI attributes from `java:comp/env`. +. `ServletContext` init parameters. +. `ServletConfig` init parameters. +. Properties from `SPRING_APPLICATION_JSON` (inline JSON embedded in an environment variable or system property). +. Command line arguments. +. `properties` attribute on your tests. + Available on {spring-boot-test-module-api}/context/SpringBootTest.html[`@SpringBootTest`] and the <>. +. {spring-framework-api}/test/context/TestPropertySource.html[`@TestPropertySource`] annotations on your tests. +. <> in the `$HOME/.config/spring-boot` directory when devtools is active. + +Config data files are considered in the following order: + +. <> packaged inside your jar (`application.properties` and YAML variants). +. <> packaged inside your jar (`application-\{profile}.properties` and YAML variants). +. <> outside of your packaged jar (`application.properties` and YAML variants). +. <> outside of your packaged jar (`application-\{profile}.properties` and YAML variants). + +NOTE: It is recommended to stick with one format for your entire application. +If you have configuration files with both `.properties` and `.yml` format in the same location, `.properties` takes precedence. + +To provide a concrete example, suppose you develop a `@Component` that uses a `name` property, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/externalconfig/MyBean.java[] +---- + +On your application classpath (for example, inside your jar) you can have an `application.properties` file that provides a sensible default property value for `name`. +When running in a new environment, an `application.properties` file can be provided outside of your jar that overrides the `name`. +For one-off testing, you can launch with a specific command line switch (for example, `java -jar app.jar --name="Spring"`). + +TIP: The `env` and `configprops` endpoints can be useful in determining why a property has a particular value. +You can use these two endpoints to diagnose unexpected property values. +See the "<>" section for details. + + + +[[features.external-config.command-line-args]] +=== Accessing Command Line Properties +By default, `SpringApplication` converts any command line option arguments (that is, arguments starting with `--`, such as `--server.port=9000`) to a `property` and adds them to the Spring `Environment`. +As mentioned previously, command line properties always take precedence over file based property sources. + +If you do not want command line properties to be added to the `Environment`, you can disable them by using `SpringApplication.setAddCommandLineProperties(false)`. + + + +[[features.external-config.application-json]] +=== JSON Application Properties +Environment variables and system properties often have restrictions that mean some property names cannot be used. +To help with this, Spring Boot allows you to encode a block of properties into a single JSON structure. + +When your application starts, any `spring.application.json` or `SPRING_APPLICATION_JSON` properties will be parsed and added to the `Environment`. + +For example, the `SPRING_APPLICATION_JSON` property can be supplied on the command line in a UN{asterisk}X shell as an environment variable: + +[source,shell,indent=0,subs="verbatim"] +---- + $ SPRING_APPLICATION_JSON='{"my":{"name":"test"}}' java -jar myapp.jar +---- + +In the preceding example, you end up with `my.name=test` in the Spring `Environment`. + +The same JSON can also be provided as a system property: + +[source,shell,indent=0,subs="verbatim"] +---- + $ java -Dspring.application.json='{"my":{"name":"test"}}' -jar myapp.jar +---- + +Or you could supply the JSON by using a command line argument: + +[source,shell,indent=0,subs="verbatim"] +---- + $ java -jar myapp.jar --spring.application.json='{"my":{"name":"test"}}' +---- + +If you are deploying to a classic Application Server, you could also use a JNDI variable named `java:comp/env/spring.application.json`. + +NOTE: Although `null` values from the JSON will be added to the resulting property source, the `PropertySourcesPropertyResolver` treats `null` properties as missing values. +This means that the JSON cannot override properties from lower order property sources with a `null` value. + + + +[[features.external-config.files]] +=== External Application Properties [[features.external-config.files]] +Spring Boot will automatically find and load `application.properties` and `application.yaml` files from the following locations when your application starts: + +. From the classpath +.. The classpath root +.. The classpath `/config` package +. From the current directory +.. The current directory +.. The `/config` subdirectory in the current directory +.. Immediate child directories of the `/config` subdirectory + +The list is ordered by precedence (with values from lower items overriding earlier ones). +Documents from the loaded files are added as `PropertySources` to the Spring `Environment`. + +If you do not like `application` as the configuration file name, you can switch to another file name by specifying a configprop:spring.config.name[] environment property. +For example, to look for `myproject.properties` and `myproject.yaml` files you can run your application as follows: + +[source,shell,indent=0,subs="verbatim"] +---- + $ java -jar myproject.jar --spring.config.name=myproject +---- + +You can also refer to an explicit location by using the configprop:spring.config.location[] environment property. +This property accepts a comma-separated list of one or more locations to check. + +The following example shows how to specify two distinct files: + +[source,shell,indent=0,subs="verbatim"] +---- + $ java -jar myproject.jar --spring.config.location=\ + optional:classpath:/default.properties,\ + optional:classpath:/override.properties +---- + +TIP: Use the prefix `optional:` if the <> and you don't mind if they don't exist. + +WARNING: `spring.config.name`, `spring.config.location`, and `spring.config.additional-location` are used very early to determine which files have to be loaded. +They must be defined as an environment property (typically an OS environment variable, a system property, or a command-line argument). + +If `spring.config.location` contains directories (as opposed to files), they should end in `/`. +At runtime they will be appended with the names generated from `spring.config.name` before being loaded. +Files specified in `spring.config.location` are imported directly. + +NOTE: Both directory and file location values are also expanded to check for <>. +For example, if you have a `spring.config.location` of `classpath:myconfig.properties`, you will also find appropriate `classpath:myconfig-.properties` files are loaded. + +In most situations, each configprop:spring.config.location[] item you add will reference a single file or directory. +Locations are processed in the order that they are defined and later ones can override the values of earlier ones. + +[[features.external-config.files.location-groups]] +If you have a complex location setup, and you use profile-specific configuration files, you may need to provide further hints so that Spring Boot knows how they should be grouped. +A location group is a collection of locations that are all considered at the same level. +For example, you might want to group all classpath locations, then all external locations. +Items within a location group should be separated with `;`. +See the example in the "`<>`" section for more details. + +Locations configured by using `spring.config.location` replace the default locations. +For example, if `spring.config.location` is configured with the value `optional:classpath:/custom-config/,optional:file:./custom-config/`, the complete set of locations considered is: + +. `optional:classpath:custom-config/` +. `optional:file:./custom-config/` + +If you prefer to add additional locations, rather than replacing them, you can use `spring.config.additional-location`. +Properties loaded from additional locations can override those in the default locations. +For example, if `spring.config.additional-location` is configured with the value `optional:classpath:/custom-config/,optional:file:./custom-config/`, the complete set of locations considered is: + +. `optional:classpath:/;optional:classpath:/config/` +. `optional:file:./;optional:file:./config/;optional:file:./config/*/` +. `optional:classpath:custom-config/` +. `optional:file:./custom-config/` + +This search ordering lets you specify default values in one configuration file and then selectively override those values in another. +You can provide default values for your application in `application.properties` (or whatever other basename you choose with `spring.config.name`) in one of the default locations. +These default values can then be overridden at runtime with a different file located in one of the custom locations. + +NOTE: If you use environment variables rather than system properties, most operating systems disallow period-separated key names, but you can use underscores instead (for example, configprop:spring.config.name[format=envvar] instead of configprop:spring.config.name[]). +See <> for details. + +NOTE: If your application runs in a servlet container or application server, then JNDI properties (in `java:comp/env`) or servlet context initialization parameters can be used instead of, or as well as, environment variables or system properties. + + + +[[features.external-config.files.optional-prefix]] +==== Optional Locations +By default, when a specified config data location does not exist, Spring Boot will throw a `ConfigDataLocationNotFoundException` and your application will not start. + +If you want to specify a location, but you don't mind if it doesn't always exist, you can use the `optional:` prefix. +You can use this prefix with the `spring.config.location` and `spring.config.additional-location` properties, as well as with <> declarations. + +For example, a `spring.config.import` value of `optional:file:./myconfig.properties` allows your application to start, even if the `myconfig.properties` file is missing. + +If you want to ignore all `ConfigDataLocationNotFoundExceptions` and always continue to start your application, you can use the `spring.config.on-not-found` property. +Set the value to `ignore` using `SpringApplication.setDefaultProperties(...)` or with a system/environment variable. + + + +[[features.external-config.files.wildcard-locations]] +==== Wildcard Locations +If a config file location includes the `{asterisk}` character for the last path segment, it is considered a wildcard location. +Wildcards are expanded when the config is loaded so that immediate subdirectories are also checked. +Wildcard locations are particularly useful in an environment such as Kubernetes when there are multiple sources of config properties. + +For example, if you have some Redis configuration and some MySQL configuration, you might want to keep those two pieces of configuration separate, while requiring that both those are present in an `application.properties` file. +This might result in two separate `application.properties` files mounted at different locations such as `/config/redis/application.properties` and `/config/mysql/application.properties`. +In such a case, having a wildcard location of `config/*/`, will result in both files being processed. + +By default, Spring Boot includes `config/*/` in the default search locations. +It means that all subdirectories of the `/config` directory outside of your jar will be searched. + +You can use wildcard locations yourself with the `spring.config.location` and `spring.config.additional-location` properties. + +NOTE: A wildcard location must contain only one `{asterisk}` and end with `{asterisk}/` for search locations that are directories or `*/` for search locations that are files. +Locations with wildcards are sorted alphabetically based on the absolute path of the file names. + +TIP: Wildcard locations only work with external directories. +You cannot use a wildcard in a `classpath:` location. + + + +[[features.external-config.files.profile-specific]] +==== Profile Specific Files +As well as `application` property files, Spring Boot will also attempt to load profile-specific files using the naming convention `application-\{profile}`. +For example, if your application activates a profile named `prod` and uses YAML files, then both `application.yml` and `application-prod.yml` will be considered. + +Profile-specific properties are loaded from the same locations as standard `application.properties`, with profile-specific files always overriding the non-specific ones. +If several profiles are specified, a last-wins strategy applies. +For example, if profiles `prod,live` are specified by the configprop:spring.profiles.active[] property, values in `application-prod.properties` can be overridden by those in `application-live.properties`. + +[NOTE] +==== +The last-wins strategy applies at the <> level. +A configprop:spring.config.location[] of `classpath:/cfg/,classpath:/ext/` will not have the same override rules as `classpath:/cfg/;classpath:/ext/`. + +For example, continuing our `prod,live` example above, we might have the following files: + +---- +/cfg + application-live.properties +/ext + application-live.properties + application-prod.properties +---- + +When we have a configprop:spring.config.location[] of `classpath:/cfg/,classpath:/ext/` we process all `/cfg` files before all `/ext` files: + +. `/cfg/application-live.properties` +. `/ext/application-prod.properties` +. `/ext/application-live.properties` + + +When we have `classpath:/cfg/;classpath:/ext/` instead (with a `;` delimiter) we process `/cfg` and `/ext` at the same level: + +. `/ext/application-prod.properties` +. `/cfg/application-live.properties` +. `/ext/application-live.properties` +==== + +The `Environment` has a set of default profiles (by default, `[default]`) that are used if no active profiles are set. +In other words, if no profiles are explicitly activated, then properties from `application-default` are considered. + +NOTE: Properties files are only ever loaded once. +If you've already directly <> a profile specific property files then it won't be imported a second time. + + + +[[features.external-config.files.importing]] +==== Importing Additional Data +Application properties may import further config data from other locations using the `spring.config.import` property. +Imports are processed as they are discovered, and are treated as additional documents inserted immediately below the one that declares the import. + +For example, you might have the following in your classpath `application.properties` file: + +[source,yaml,indent=0,subs="verbatim",configblocks] +---- + spring: + application: + name: "myapp" + config: + import: "optional:file:./dev.properties" +---- + +This will trigger the import of a `dev.properties` file in current directory (if such a file exists). +Values from the imported `dev.properties` will take precedence over the file that triggered the import. +In the above example, the `dev.properties` could redefine `spring.application.name` to a different value. + +An import will only be imported once no matter how many times it is declared. +The order an import is defined inside a single document within the properties/yaml file doesn't matter. +For instance, the two examples below produce the same result: + +[source,yaml,indent=0,subs="verbatim",configblocks] +---- + spring: + config: + import: my.properties + my: + property: value +---- + +[source,yaml,indent=0,subs="verbatim",configblocks] +---- + my: + property: value + spring: + config: + import: my.properties +---- + +In both of the above examples, the values from the `my.properties` file will take precedence over the file that triggered its import. + +Several locations can be specified under a single `spring.config.import` key. +Locations will be processed in the order that they are defined, with later imports taking precedence. + +NOTE: When appropriate, <> are also considered for import. +The example above would import both `my.properties` as well as any `my-.properties` variants. + +[TIP] +==== +Spring Boot includes pluggable API that allows various different location addresses to be supported. +By default you can import Java Properties, YAML and "`<>`". + +Third-party jars can offer support for additional technologies (there's no requirement for files to be local). +For example, you can imagine config data being from external stores such as Consul, Apache ZooKeeper or Netflix Archaius. + +If you want to support your own locations, see the `ConfigDataLocationResolver` and `ConfigDataLoader` classes in the `org.springframework.boot.context.config` package. +==== + + + +[[features.external-config.files.importing-extensionless]] +==== Importing Extensionless Files +Some cloud platforms cannot add a file extension to volume mounted files. +To import these extensionless files, you need to give Spring Boot a hint so that it knows how to load them. +You can do this by putting an extension hint in square brackets. + +For example, suppose you have a `/etc/config/myconfig` file that you wish to import as yaml. +You can import it from your `application.properties` using the following: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + config: + import: "file:/etc/config/myconfig[.yaml]" +---- + + + +[[features.external-config.files.configtree]] +==== Using Configuration Trees +When running applications on a cloud platform (such as Kubernetes) you often need to read config values that the platform supplies. +It's not uncommon to use environment variables for such purposes, but this can have drawbacks, especially if the value is supposed to be kept secret. + +As an alternative to environment variables, many cloud platforms now allow you to map configuration into mounted data volumes. +For example, Kubernetes can volume mount both https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#populate-a-volume-with-data-stored-in-a-configmap[`ConfigMaps`] and https://kubernetes.io/docs/concepts/configuration/secret/#using-secrets-as-files-from-a-pod[`Secrets`]. + +There are two common volume mount patterns that can be used: + +. A single file contains a complete set of properties (usually written as YAML). +. Multiple files are written to a directory tree, with the filename becoming the '`key`' and the contents becoming the '`value`'. + +For the first case, you can import the YAML or Properties file directly using `spring.config.import` as described <>. +For the second case, you need to use the `configtree:` prefix so that Spring Boot knows it needs to expose all the files as properties. + +As an example, let's imagine that Kubernetes has mounted the following volume: + +[indent=0] +---- + etc/ + config/ + myapp/ + username + password +---- + +The contents of the `username` file would be a config value, and the contents of `password` would be a secret. + +To import these properties, you can add the following to your `application.properties` or `application.yaml` file: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + config: + import: "optional:configtree:/etc/config/" +---- + +You can then access or inject `myapp.username` and `myapp.password` properties from the `Environment` in the usual way. + +TIP: The folders under the config tree form the property name. +In the above example, to access the properties as `username` and `password`, you can set `spring.config.import` to `optional:configtree:/etc/config/myapp`. + +NOTE: Filenames with dot notation are also correctly mapped. +For example, in the above example, a file named `myapp.username` in `/etc/config` would result in a `myapp.username` property in the `Environment`. + +TIP: Configuration tree values can be bound to both string `String` and `byte[]` types depending on the contents expected. + +If you have multiple config trees to import from the same parent folder you can use a wildcard shortcut. +Any `configtree:` location that ends with `/*/` will import all immediate children as config trees. + +For example, given the following volume: + +[indent=0] +---- + etc/ + config/ + dbconfig/ + db/ + username + password + mqconfig/ + mq/ + username + password +---- + +You can use `configtree:/etc/config/*/` as the import location: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + config: + import: "optional:configtree:/etc/config/*/" +---- + +This will add `db.username`, `db.password`, `mq.username` and `mq.password` properties. + +NOTE: Directories loaded using a wildcard are sorted alphabetically. +If you need a different order, then you should list each location as a separate import + + +Configuration trees can also be used for Docker secrets. +When a Docker swarm service is granted access to a secret, the secret gets mounted into the container. +For example, if a secret named `db.password` is mounted at location `/run/secrets/`, you can make `db.password` available to the Spring environment using the following: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + config: + import: "optional:configtree:/run/secrets/" +---- + + + +[[features.external-config.files.property-placeholders]] +==== Property Placeholders +The values in `application.properties` and `application.yml` are filtered through the existing `Environment` when they are used, so you can refer back to previously defined values (for example, from System properties or environment variables). +The standard `$\{name}` property-placeholder syntax can be used anywhere within a value. +Property placeholders can also specify a default value using a `:` to separate the default value from the property name, for example `${name:default}`. + +The use of placeholders with and without defaults is shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configblocks] +---- + app: + name: "MyApp" + description: "${app.name} is a Spring Boot application written by ${username:Unknown}" +---- + +Assuming that the `username` property has not be set elsewhere, `app.description` will have the value `MyApp is a Spring Boot application written by Unknown`. + +TIP: You can also use this technique to create "`short`" variants of existing Spring Boot properties. +See the _<>_ how-to for details. + + + +[[features.external-config.files.multi-document]] +==== Working with Multi-Document Files +Spring Boot allows you to split a single physical file into multiple logical documents which are each added independently. +Documents are processed in order, from top to bottom. +Later documents can override the properties defined in earlier ones. + +For `application.yml` files, the standard YAML multi-document syntax is used. +Three consecutive hyphens represent the end of one document, and the start of the next. + +For example, the following file has two logical documents: + +[source,yaml,indent=0,subs="verbatim"] +---- + spring: + application: + name: MyApp + --- + spring: + application: + name: MyCloudApp + config: + activate: + on-cloud-platform: kubernetes +---- + +For `application.properties` files a special `#---` comment is used to mark the document splits: + +[source,properties,indent=0,subs="verbatim"] +---- + spring.application.name=MyApp + #--- + spring.application.name=MyCloudApp + spring.config.activate.on-cloud-platform=kubernetes +---- + +NOTE: Property file separators must not have any leading whitespace and must have exactly three hyphen characters. +The lines immediately before and after the separator must not be comments. + +TIP: Multi-document property files are often used in conjunction with activation properties such as `spring.config.activate.on-profile`. +See the <> for details. + +WARNING: Multi-document property files cannot be loaded by using the `@PropertySource` or `@TestPropertySource` annotations. + + + +[[features.external-config.files.activation-properties]] +==== Activation Properties +It's sometimes useful to only activate a given set of properties when certain conditions are met. +For example, you might have properties that are only relevant when a specific profile is active. + +You can conditionally activate a properties document using `spring.config.activate.*`. + +The following activation properties are available: + +.activation properties +[cols="1,4"] +|=== +| Property | Note + +| `on-profile` +| A profile expression that must match for the document to be active. + +| `on-cloud-platform` +| The `CloudPlatform` that must be detected for the document to be active. +|=== + +For example, the following specifies that the second document is only active when running on Kubernetes, and only when either the "`prod`" or "`staging`" profiles are active: + +[source,yaml,indent=0,subs="verbatim",configblocks] +---- + myprop: + always-set + --- + spring: + config: + activate: + on-cloud-platform: "kubernetes" + on-profile: "prod | staging" + myotherprop: sometimes-set +---- + + + +[[features.external-config.encrypting]] +=== Encrypting Properties +Spring Boot does not provide any built in support for encrypting property values, however, it does provide the hook points necessary to modify values contained in the Spring `Environment`. +The `EnvironmentPostProcessor` interface allows you to manipulate the `Environment` before the application starts. +See <> for details. + +If you're looking for a secure way to store credentials and passwords, the https://cloud.spring.io/spring-cloud-vault/[Spring Cloud Vault] project provides support for storing externalized configuration in https://www.vaultproject.io/[HashiCorp Vault]. + + + +[[features.external-config.yaml]] +=== Working with YAML +https://yaml.org[YAML] is a superset of JSON and, as such, is a convenient format for specifying hierarchical configuration data. +The `SpringApplication` class automatically supports YAML as an alternative to properties whenever you have the https://bitbucket.org/asomov/snakeyaml[SnakeYAML] library on your classpath. + +NOTE: If you use "`Starters`", SnakeYAML is automatically provided by `spring-boot-starter`. + + + +[[features.external-config.yaml.mapping-to-properties]] +==== Mapping YAML to Properties +YAML documents need to be converted from their hierarchical format to a flat structure that can be used with the Spring `Environment`. +For example, consider the following YAML document: + +[source,yaml,indent=0,subs="verbatim"] +---- + environments: + dev: + url: https://dev.example.com + name: Developer Setup + prod: + url: https://another.example.com + name: My Cool App +---- + +In order to access these properties from the `Environment`, they would be flattened as follows: + +[source,properties,indent=0,subs="verbatim"] +---- + environments.dev.url=https://dev.example.com + environments.dev.name=Developer Setup + environments.prod.url=https://another.example.com + environments.prod.name=My Cool App +---- + +Likewise, YAML lists also need to be flattened. +They are represented as property keys with `[index]` dereferencers. +For example, consider the following YAML: + +[source,yaml,indent=0,subs="verbatim"] +---- + my: + servers: + - dev.example.com + - another.example.com +---- + +The preceding example would be transformed into these properties: + +[source,properties,indent=0,subs="verbatim"] +---- + my.servers[0]=dev.example.com + my.servers[1]=another.example.com +---- + +TIP: Properties that use the `[index]` notation can be bound to Java `List` or `Set` objects using Spring Boot's `Binder` class. +For more details see the "`<>`" section below. + +WARNING: YAML files cannot be loaded by using the `@PropertySource` or `@TestPropertySource` annotations. +So, in the case that you need to load values that way, you need to use a properties file. + + + +[[features.external-config.yaml.directly-loading]] +[[features.external-config.yaml.directly-loading]] +==== Directly Loading YAML +Spring Framework provides two convenient classes that can be used to load YAML documents. +The `YamlPropertiesFactoryBean` loads YAML as `Properties` and the `YamlMapFactoryBean` loads YAML as a `Map`. + +You can also use the `YamlPropertySourceLoader` class if you want to load YAML as a Spring `PropertySource`. + + + +[[features.external-config.random-values]] +=== Configuring Random Values +The `RandomValuePropertySource` is useful for injecting random values (for example, into secrets or test cases). +It can produce integers, longs, uuids, or strings, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configblocks] +---- + my: + secret: "${random.value}" + number: "${random.int}" + bignumber: "${random.long}" + uuid: "${random.uuid}" + number-less-than-ten: "${random.int(10)}" + number-in-range: "${random.int[1024,65536]}" +---- + +The `+random.int*+` syntax is `OPEN value (,max) CLOSE` where the `OPEN,CLOSE` are any character and `value,max` are integers. +If `max` is provided, then `value` is the minimum value and `max` is the maximum value (exclusive). + + + +[[features.external-config.system-environment]] +=== Configuring System Environment Properties +Spring Boot supports setting a prefix for environment properties. +This is useful if the system environment is shared by multiple Spring Boot applications with different configuration requirements. +The prefix for system environment properties can be set directly on `SpringApplication`. + +For example, if you set the prefix to `input`, a property such as `remote.timeout` will also be resolved as `input.remote.timeout` in the system environment. + + + +[[features.external-config.typesafe-configuration-properties]] +=== Type-safe Configuration Properties +Using the `@Value("$\{property}")` annotation to inject configuration properties can sometimes be cumbersome, especially if you are working with multiple properties or your data is hierarchical in nature. +Spring Boot provides an alternative method of working with properties that lets strongly typed beans govern and validate the configuration of your application. + +TIP: See also the <>. + + + +[[features.external-config.typesafe-configuration-properties.java-bean-binding]] +==== JavaBean properties binding +It is possible to bind a bean declaring standard JavaBean properties as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/externalconfig/typesafeconfigurationproperties/javabeanbinding/MyProperties.java[] +---- + +The preceding POJO defines the following properties: + +* `my.service.enabled`, with a value of `false` by default. +* `my.service.remote-address`, with a type that can be coerced from `String`. +* `my.service.security.username`, with a nested "security" object whose name is determined by the name of the property. + In particular, the return type is not used at all there and could have been `SecurityProperties`. +* `my.service.security.password`. +* `my.service.security.roles`, with a collection of `String` that defaults to `USER`. + +NOTE: The properties that map to `@ConfigurationProperties` classes available in Spring Boot, which are configured via properties files, YAML files, environment variables etc., are public API but the accessors (getters/setters) of the class itself are not meant to be used directly. + +[NOTE] +==== +Such arrangement relies on a default empty constructor and getters and setters are usually mandatory, since binding is through standard Java Beans property descriptors, just like in Spring MVC. +A setter may be omitted in the following cases: + +* Maps, as long as they are initialized, need a getter but not necessarily a setter, since they can be mutated by the binder. +* Collections and arrays can be accessed either through an index (typically with YAML) or by using a single comma-separated value (properties). + In the latter case, a setter is mandatory. + We recommend to always add a setter for such types. + If you initialize a collection, make sure it is not immutable (as in the preceding example). +* If nested POJO properties are initialized (like the `Security` field in the preceding example), a setter is not required. + If you want the binder to create the instance on the fly by using its default constructor, you need a setter. + +Some people use Project Lombok to add getters and setters automatically. +Make sure that Lombok does not generate any particular constructor for such a type, as it is used automatically by the container to instantiate the object. + +Finally, only standard Java Bean properties are considered and binding on static properties is not supported. +==== + + + +[[features.external-config.typesafe-configuration-properties.constructor-binding]] +==== Constructor binding +The example in the previous section can be rewritten in an immutable fashion as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/externalconfig/typesafeconfigurationproperties/constructorbinding/MyProperties.java[] +---- + +In this setup, the `@ConstructorBinding` annotation is used to indicate that constructor binding should be used. +This means that the binder will expect to find a constructor with the parameters that you wish to have bound. +If you are using Java 16 or later, constructor binding can be used with records. + +Nested members of a `@ConstructorBinding` class (such as `Security` in the example above) will also be bound via their constructor. + +Default values can be specified using `@DefaultValue` on a constructor parameter or, when using Java 16 or later, a record component. +The conversion service will be applied to coerce the `String` value to the target type of a missing property. + +Referring to the previous example, if no properties are bound to `Security`, the `MyProperties` instance will contain a `null` value for `security`. +If you wish you return a non-null instance of `Security` even when no properties are bound to it, you can use an empty `@DefaultValue` annotation to do so: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/externalconfig/typesafeconfigurationproperties/constructorbinding/nonnull/MyProperties.java[tag=*] +---- + + +NOTE: To use constructor binding the class must be enabled using `@EnableConfigurationProperties` or configuration property scanning. +You cannot use constructor binding with beans that are created by the regular Spring mechanisms (e.g. `@Component` beans, beans created via `@Bean` methods or beans loaded using `@Import`) + +TIP: If you have more than one constructor for your class you can also use `@ConstructorBinding` directly on the constructor that should be bound. + +NOTE: The use of `java.util.Optional` with `@ConfigurationProperties` is not recommended as it is primarily intended for use as a return type. +As such, it is not well-suited to configuration property injection. +For consistency with properties of other types, if you do declare an `Optional` property and it has no value, `null` rather than an empty `Optional` will be bound. + + + +[[features.external-config.typesafe-configuration-properties.enabling-annotated-types]] +==== Enabling @ConfigurationProperties-annotated types +Spring Boot provides infrastructure to bind `@ConfigurationProperties` types and register them as beans. +You can either enable configuration properties on a class-by-class basis or enable configuration property scanning that works in a similar manner to component scanning. + +Sometimes, classes annotated with `@ConfigurationProperties` might not be suitable for scanning, for example, if you're developing your own auto-configuration or you want to enable them conditionally. +In these cases, specify the list of types to process using the `@EnableConfigurationProperties` annotation. +This can be done on any `@Configuration` class, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/externalconfig/typesafeconfigurationproperties/enablingannotatedtypes/MyConfiguration.java[] +---- + +To use configuration property scanning, add the `@ConfigurationPropertiesScan` annotation to your application. +Typically, it is added to the main application class that is annotated with `@SpringBootApplication` but it can be added to any `@Configuration` class. +By default, scanning will occur from the package of the class that declares the annotation. +If you want to define specific packages to scan, you can do so as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/externalconfig/typesafeconfigurationproperties/enablingannotatedtypes/MyApplication.java[] +---- + +[NOTE] +==== +When the `@ConfigurationProperties` bean is registered using configuration property scanning or via `@EnableConfigurationProperties`, the bean has a conventional name: `-`, where `` is the environment key prefix specified in the `@ConfigurationProperties` annotation and `` is the fully qualified name of the bean. +If the annotation does not provide any prefix, only the fully qualified name of the bean is used. + +The bean name in the example above is `com.example.app-com.example.app.SomeProperties`. +==== + +We recommend that `@ConfigurationProperties` only deal with the environment and, in particular, does not inject other beans from the context. +For corner cases, setter injection can be used or any of the `*Aware` interfaces provided by the framework (such as `EnvironmentAware` if you need access to the `Environment`). +If you still want to inject other beans using the constructor, the configuration properties bean must be annotated with `@Component` and use JavaBean-based property binding. + + + +[[features.external-config.typesafe-configuration-properties.using-annotated-types]] +==== Using @ConfigurationProperties-annotated types +This style of configuration works particularly well with the `SpringApplication` external YAML configuration, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim"] +---- + my: + service: + remote-address: 192.168.1.1 + security: + username: admin + roles: + - USER + - ADMIN +---- + +To work with `@ConfigurationProperties` beans, you can inject them in the same way as any other bean, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/externalconfig/typesafeconfigurationproperties/usingannotatedtypes/MyService.java[] +---- + +TIP: Using `@ConfigurationProperties` also lets you generate metadata files that can be used by IDEs to offer auto-completion for your own keys. +See the <> for details. + + + +[[features.external-config.typesafe-configuration-properties.third-party-configuration]] +==== Third-party Configuration +As well as using `@ConfigurationProperties` to annotate a class, you can also use it on public `@Bean` methods. +Doing so can be particularly useful when you want to bind properties to third-party components that are outside of your control. + +To configure a bean from the `Environment` properties, add `@ConfigurationProperties` to its bean registration, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/externalconfig/typesafeconfigurationproperties/thirdpartyconfiguration/ThirdPartyConfiguration.java[] +---- + +Any JavaBean property defined with the `another` prefix is mapped onto that `AnotherComponent` bean in manner similar to the preceding `SomeProperties` example. + + + +[[features.external-config.typesafe-configuration-properties.relaxed-binding]] +==== Relaxed Binding +Spring Boot uses some relaxed rules for binding `Environment` properties to `@ConfigurationProperties` beans, so there does not need to be an exact match between the `Environment` property name and the bean property name. +Common examples where this is useful include dash-separated environment properties (for example, `context-path` binds to `contextPath`), and capitalized environment properties (for example, `PORT` binds to `port`). + +As an example, consider the following `@ConfigurationProperties` class: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/externalconfig/typesafeconfigurationproperties/relaxedbinding/MyPersonProperties.java[] +---- + +With the preceding code, the following properties names can all be used: + +.relaxed binding +[cols="1,4"] +|=== +| Property | Note + +| `my.main-project.person.first-name` +| Kebab case, which is recommended for use in `.properties` and `.yml` files. + +| `my.main-project.person.firstName` +| Standard camel case syntax. + +| `my.main-project.person.first_name` +| Underscore notation, which is an alternative format for use in `.properties` and `.yml` files. + +| `MY_MAINPROJECT_PERSON_FIRSTNAME` +| Upper case format, which is recommended when using system environment variables. +|=== + +NOTE: The `prefix` value for the annotation _must_ be in kebab case (lowercase and separated by `-`, such as `my.main-project.person`). + +.relaxed binding rules per property source +[cols="2,4,4"] +|=== +| Property Source | Simple | List + +| Properties Files +| Camel case, kebab case, or underscore notation +| Standard list syntax using `[ ]` or comma-separated values + +| YAML Files +| Camel case, kebab case, or underscore notation +| Standard YAML list syntax or comma-separated values + +| Environment Variables +| Upper case format with underscore as the delimiter (see <>). +| Numeric values surrounded by underscores (see <>) + +| System properties +| Camel case, kebab case, or underscore notation +| Standard list syntax using `[ ]` or comma-separated values +|=== + +TIP: We recommend that, when possible, properties are stored in lower-case kebab format, such as `my.person.first-name=Rod`. + + + +[[features.external-config.typesafe-configuration-properties.relaxed-binding.maps]] +===== Binding Maps +When binding to `Map` properties you may need to use a special bracket notation so that the original `key` value is preserved. +If the key is not surrounded by `[]`, any characters that are not alpha-numeric, `-` or `.` are removed. + +For example, consider binding the following properties to a `Map`: + + +[source,properties,indent=0,subs="verbatim",role="primary"] +.Properties +---- + my.map.[/key1]=value1 + my.map.[/key2]=value2 + my.map./key3=value3 +---- + +[source,yaml,indent=0,subs="verbatim",role="secondary"] +.Yaml +---- + my: + map: + "[/key1]": "value1" + "[/key2]": "value2" + "/key3": "value3" +---- + +NOTE: For YAML files, the brackets need to be surrounded by quotes for the keys to be parsed properly. + +The properties above will bind to a `Map` with `/key1`, `/key2` and `key3` as the keys in the map. +The slash has been removed from `key3` because it wasn't surrounded by square brackets. + +When binding to scalar values, keys with `.` in them do not need to be surrounded by `[]`. +Scalar values include enums and all types in the `java.lang` package except for `Object`. +Binding `a.b=c` to `Map` will preserve the `.` in the key and return a Map with the entry `{"a.b"="c"}`. +For any other types you need to use the bracket notation if your `key` contains a `.`. +For example, binding `a.b=c` to `Map` will return a Map with the entry `{"a"={"b"="c"}}` whereas `[a.b]=c` will return a Map with the entry `{"a.b"="c"}`. + + + +[[features.external-config.typesafe-configuration-properties.relaxed-binding.environment-variables]] +===== Binding from Environment Variables +Most operating systems impose strict rules around the names that can be used for environment variables. +For example, Linux shell variables can contain only letters (`a` to `z` or `A` to `Z`), numbers (`0` to `9`) or the underscore character (`_`). +By convention, Unix shell variables will also have their names in UPPERCASE. + +Spring Boot's relaxed binding rules are, as much as possible, designed to be compatible with these naming restrictions. + +To convert a property name in the canonical-form to an environment variable name you can follow these rules: + +* Replace dots (`.`) with underscores (`_`). +* Remove any dashes (`-`). +* Convert to uppercase. + +For example, the configuration property `spring.main.log-startup-info` would be an environment variable named `SPRING_MAIN_LOGSTARTUPINFO`. + +Environment variables can also be used when binding to object lists. +To bind to a `List`, the element number should be surrounded with underscores in the variable name. + +For example, the configuration property `my.service[0].other` would use an environment variable named `MY_SERVICE_0_OTHER`. + + + +[[features.external-config.typesafe-configuration-properties.merging-complex-types]] +==== Merging Complex Types +When lists are configured in more than one place, overriding works by replacing the entire list. + +For example, assume a `MyPojo` object with `name` and `description` attributes that are `null` by default. +The following example exposes a list of `MyPojo` objects from `MyProperties`: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/externalconfig/typesafeconfigurationproperties/mergingcomplextypes/list/MyProperties.java[] +---- + +Consider the following configuration: + +[source,yaml,indent=0,subs="verbatim",configblocks] +---- + my: + list: + - name: "my name" + description: "my description" + --- + spring: + config: + activate: + on-profile: "dev" + my: + list: + - name: "my another name" +---- + +If the `dev` profile is not active, `MyProperties.list` contains one `MyPojo` entry, as previously defined. +If the `dev` profile is enabled, however, the `list` _still_ contains only one entry (with a name of `my another name` and a description of `null`). +This configuration _does not_ add a second `MyPojo` instance to the list, and it does not merge the items. + +When a `List` is specified in multiple profiles, the one with the highest priority (and only that one) is used. +Consider the following example: + +[source,yaml,indent=0,subs="verbatim",configblocks] +---- + my: + list: + - name: "my name" + description: "my description" + - name: "another name" + description: "another description" + --- + spring: + config: + activate: + on-profile: "dev" + my: + list: + - name: "my another name" +---- + +In the preceding example, if the `dev` profile is active, `MyProperties.list` contains _one_ `MyPojo` entry (with a name of `my another name` and a description of `null`). +For YAML, both comma-separated lists and YAML lists can be used for completely overriding the contents of the list. + +For `Map` properties, you can bind with property values drawn from multiple sources. +However, for the same property in multiple sources, the one with the highest priority is used. +The following example exposes a `Map` from `MyProperties`: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/externalconfig/typesafeconfigurationproperties/mergingcomplextypes/map/MyProperties.java[] +---- + +Consider the following configuration: + +[source,yaml,indent=0,subs="verbatim",configblocks] +---- + my: + map: + key1: + name: "my name 1" + description: "my description 1" + --- + spring: + config: + activate: + on-profile: "dev" + my: + map: + key1: + name: "dev name 1" + key2: + name: "dev name 2" + description: "dev description 2" +---- + +If the `dev` profile is not active, `MyProperties.map` contains one entry with key `key1` (with a name of `my name 1` and a description of `my description 1`). +If the `dev` profile is enabled, however, `map` contains two entries with keys `key1` (with a name of `dev name 1` and a description of `my description 1`) and `key2` (with a name of `dev name 2` and a description of `dev description 2`). + +NOTE: The preceding merging rules apply to properties from all property sources, and not just files. + + + +[[features.external-config.typesafe-configuration-properties.conversion]] +==== Properties Conversion +Spring Boot attempts to coerce the external application properties to the right type when it binds to the `@ConfigurationProperties` beans. +If you need custom type conversion, you can provide a `ConversionService` bean (with a bean named `conversionService`) or custom property editors (through a `CustomEditorConfigurer` bean) or custom `Converters` (with bean definitions annotated as `@ConfigurationPropertiesBinding`). + +NOTE: As this bean is requested very early during the application lifecycle, make sure to limit the dependencies that your `ConversionService` is using. +Typically, any dependency that you require may not be fully initialized at creation time. +You may want to rename your custom `ConversionService` if it is not required for configuration keys coercion and only rely on custom converters qualified with `@ConfigurationPropertiesBinding`. + + + +[[features.external-config.typesafe-configuration-properties.conversion.durations]] +===== Converting Durations +Spring Boot has dedicated support for expressing durations. +If you expose a `java.time.Duration` property, the following formats in application properties are available: + +* A regular `long` representation (using milliseconds as the default unit unless a `@DurationUnit` has been specified) +* The standard ISO-8601 format {java-api}/java/time/Duration.html#parse-java.lang.CharSequence-[used by `java.time.Duration`] +* A more readable format where the value and the unit are coupled (e.g. `10s` means 10 seconds) + +Consider the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/externalconfig/typesafeconfigurationproperties/conversion/durations/javabeanbinding/MyProperties.java[] +---- + +To specify a session timeout of 30 seconds, `30`, `PT30S` and `30s` are all equivalent. +A read timeout of 500ms can be specified in any of the following form: `500`, `PT0.5S` and `500ms`. + +You can also use any of the supported units. +These are: + +* `ns` for nanoseconds +* `us` for microseconds +* `ms` for milliseconds +* `s` for seconds +* `m` for minutes +* `h` for hours +* `d` for days + +The default unit is milliseconds and can be overridden using `@DurationUnit` as illustrated in the sample above. + +If you prefer to use constructor binding, the same properties can be exposed, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/externalconfig/typesafeconfigurationproperties/conversion/durations/constructorbinding/MyProperties.java[] +---- + + +TIP: If you are upgrading a `Long` property, make sure to define the unit (using `@DurationUnit`) if it isn't milliseconds. +Doing so gives a transparent upgrade path while supporting a much richer format. + + + +[[features.external-config.typesafe-configuration-properties.conversion.periods]] +===== Converting periods +In addition to durations, Spring Boot can also work with `java.time.Period` type. +The following formats can be used in application properties: + +* An regular `int` representation (using days as the default unit unless a `@PeriodUnit` has been specified) +* The standard ISO-8601 format {java-api}/java/time/Period.html#parse-java.lang.CharSequence-[used by `java.time.Period`] +* A simpler format where the value and the unit pairs are coupled (e.g. `1y3d` means 1 year and 3 days) + +The following units are supported with the simple format: + +* `y` for years +* `m` for months +* `w` for weeks +* `d` for days + +NOTE: The `java.time.Period` type never actually stores the number of weeks, it is a shortcut that means "`7 days`". + + + +[[features.external-config.typesafe-configuration-properties.conversion.data-sizes]] +===== Converting Data Sizes +Spring Framework has a `DataSize` value type that expresses a size in bytes. +If you expose a `DataSize` property, the following formats in application properties are available: + +* A regular `long` representation (using bytes as the default unit unless a `@DataSizeUnit` has been specified) +* A more readable format where the value and the unit are coupled (e.g. `10MB` means 10 megabytes) + +Consider the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/externalconfig/typesafeconfigurationproperties/conversion/datasizes/javabeanbinding/MyProperties.java[] +---- + +To specify a buffer size of 10 megabytes, `10` and `10MB` are equivalent. +A size threshold of 256 bytes can be specified as `256` or `256B`. + +You can also use any of the supported units. +These are: + +* `B` for bytes +* `KB` for kilobytes +* `MB` for megabytes +* `GB` for gigabytes +* `TB` for terabytes + +The default unit is bytes and can be overridden using `@DataSizeUnit` as illustrated in the sample above. + +If you prefer to use constructor binding, the same properties can be exposed, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/externalconfig/typesafeconfigurationproperties/conversion/datasizes/constructorbinding/MyProperties.java[] +---- + +TIP: If you are upgrading a `Long` property, make sure to define the unit (using `@DataSizeUnit`) if it isn't bytes. +Doing so gives a transparent upgrade path while supporting a much richer format. + + + +[[features.external-config.typesafe-configuration-properties.validation]] +==== @ConfigurationProperties Validation +Spring Boot attempts to validate `@ConfigurationProperties` classes whenever they are annotated with Spring's `@Validated` annotation. +You can use JSR-303 `javax.validation` constraint annotations directly on your configuration class. +To do so, ensure that a compliant JSR-303 implementation is on your classpath and then add constraint annotations to your fields, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/externalconfig/typesafeconfigurationproperties/validate/MyProperties.java[] +---- + +TIP: You can also trigger validation by annotating the `@Bean` method that creates the configuration properties with `@Validated`. + +To ensure that validation is always triggered for nested properties, even when no properties are found, the associated field must be annotated with `@Valid`. +The following example builds on the preceding `MyProperties` example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/externalconfig/typesafeconfigurationproperties/validate/nested/MyProperties.java[] +---- + +You can also add a custom Spring `Validator` by creating a bean definition called `configurationPropertiesValidator`. +The `@Bean` method should be declared `static`. +The configuration properties validator is created very early in the application's lifecycle, and declaring the `@Bean` method as static lets the bean be created without having to instantiate the `@Configuration` class. +Doing so avoids any problems that may be caused by early instantiation. + +TIP: The `spring-boot-actuator` module includes an endpoint that exposes all `@ConfigurationProperties` beans. +Point your web browser to `/actuator/configprops` or use the equivalent JMX endpoint. +See the "<>" section for details. + + + +[[features.external-config.typesafe-configuration-properties.vs-value-annotation]] +==== @ConfigurationProperties vs. @Value +The `@Value` annotation is a core container feature, and it does not provide the same features as type-safe configuration properties. +The following table summarizes the features that are supported by `@ConfigurationProperties` and `@Value`: + +[cols="4,2,2"] +|=== +| Feature |`@ConfigurationProperties` |`@Value` + +| <> +| Yes +| Limited (see <>) + +| <> +| Yes +| No + +| `SpEL` evaluation +| No +| Yes +|=== + +[[features.external-config.typesafe-configuration-properties.vs-value-annotation.note]] +NOTE: If you do want to use `@Value`, we recommend that you refer to property names using their canonical form (kebab-case using only lowercase letters). +This will allow Spring Boot to use the same logic as it does when relaxed binding `@ConfigurationProperties`. +For example, `@Value("{demo.item-price}")` will pick up `demo.item-price` and `demo.itemPrice` forms from the `application.properties` file, as well as `DEMO_ITEMPRICE` from the system environment. +If you used `@Value("{demo.itemPrice}")` instead, `demo.item-price` and `DEMO_ITEMPRICE` would not be considered. + +If you define a set of configuration keys for your own components, we recommend you group them in a POJO annotated with `@ConfigurationProperties`. +Doing so will provide you with structured, type-safe object that you can inject into your own beans. + +`SpEL` expressions from <> are not processed at time of parsing these files and populating the environment. +However, it is possible to write a `SpEL` expression in `@Value`. +If the value of a property from an application property file is a `SpEL` expression, it will be evaluated when consumed via `@Value`. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/graceful-shutdown.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/graceful-shutdown.adoc new file mode 100644 index 000000000000..823005a0a3e6 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/graceful-shutdown.adoc @@ -0,0 +1,30 @@ +[[features.graceful-shutdown]] +== Graceful shutdown +Graceful shutdown is supported with all four embedded web servers (Jetty, Reactor Netty, Tomcat, and Undertow) and with both reactive and Servlet-based web applications. +It occurs as part of closing the application context and is performed in the earliest phase of stopping `SmartLifecycle` beans. +This stop processing uses a timeout which provides a grace period during which existing requests will be allowed to complete but no new requests will be permitted. +The exact way in which new requests are not permitted varies depending on the web server that is being used. +Jetty, Reactor Netty, and Tomcat will stop accepting requests at the network layer. +Undertow will accept requests but respond immediately with a service unavailable (503) response. + +NOTE: Graceful shutdown with Tomcat requires Tomcat 9.0.33 or later. + +To enable graceful shutdown, configure the configprop:server.shutdown[] property, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- +server: + shutdown: "graceful" +---- + +To configure the timeout period, configure the configprop:spring.lifecycle.timeout-per-shutdown-phase[] property, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- +spring: + lifecycle: + timeout-per-shutdown-phase: "20s" +---- + +IMPORTANT: Using graceful shutdown with your IDE may not work properly if it does not send a proper `SIGTERM` signal. +Refer to the documentation of your IDE for more details. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/hazelcast.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/hazelcast.adoc new file mode 100644 index 000000000000..4e8d8e5466a2 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/hazelcast.adoc @@ -0,0 +1,34 @@ +[[features.hazelcast]] +== Hazelcast +If https://hazelcast.com/[Hazelcast] is on the classpath and a suitable configuration is found, Spring Boot auto-configures a `HazelcastInstance` that you can inject in your application. + +Spring Boot first attempts to create a client by checking the following configuration options: + +* The presence of a `com.hazelcast.client.config.ClientConfig` bean. +* A configuration file defined by the configprop:spring.hazelcast.config[] property. +* The presence of the `hazelcast.client.config` system property. +* A `hazelcast-client.xml` in the working directory or at the root of the classpath. +* A `hazelcast-client.yaml` in the working directory or at the root of the classpath. + +NOTE: Spring Boot supports both Hazelcast 4 and Hazelcast 3. +If you downgrade to Hazelcast 3, `hazelcast-client` should be added to the classpath to configure a client. + +If a client can't be created, Spring Boot attempts to configure an embedded server. +If you define a `com.hazelcast.config.Config` bean, Spring Boot uses that. +If your configuration defines an instance name, Spring Boot tries to locate an existing instance rather than creating a new one. + +You could also specify the Hazelcast configuration file to use through configuration, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + hazelcast: + config: "classpath:config/my-hazelcast.xml" +---- + +Otherwise, Spring Boot tries to find the Hazelcast configuration from the default locations: `hazelcast.xml` in the working directory or at the root of the classpath, or a `.yaml` counterpart in the same locations. +We also check if the `hazelcast.config` system property is set. +See the https://docs.hazelcast.org/docs/latest/manual/html-single/[Hazelcast documentation] for more details. + +NOTE: Spring Boot also has <>. +If caching is enabled, the `HazelcastInstance` is automatically wrapped in a `CacheManager` implementation. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/internationalization.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/internationalization.adoc new file mode 100644 index 000000000000..11a9ca9c96e2 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/internationalization.adoc @@ -0,0 +1,22 @@ +[[features.internationalization]] +== Internationalization +Spring Boot supports localized messages so that your application can cater to users of different language preferences. +By default, Spring Boot looks for the presence of a `messages` resource bundle at the root of the classpath. + +NOTE: The auto-configuration applies when the default properties file for the configured resource bundle is available (i.e. `messages.properties` by default). +If your resource bundle contains only language-specific properties files, you are required to add the default. +If no properties file is found that matches any of the configured base names, there will be no auto-configured `MessageSource`. + +The basename of the resource bundle as well as several other attributes can be configured using the `spring.messages` namespace, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + messages: + basename: "messages,config.i18n.messages" + fallback-to-system-locale: false +---- + +TIP: `spring.messages.basename` supports comma-separated list of locations, either a package qualifier or a resource resolved from the classpath root. + +See {spring-boot-autoconfigure-module-code}/context/MessageSourceProperties.java[`MessageSourceProperties`] for more supported options. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/jmx.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/jmx.adoc new file mode 100644 index 000000000000..e0ac4ec3d19d --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/jmx.adoc @@ -0,0 +1,10 @@ +[[features.jmx]] +== Monitoring and Management over JMX +Java Management Extensions (JMX) provide a standard mechanism to monitor and manage applications. +Spring Boot exposes the most suitable `MBeanServer` as a bean with an ID of `mbeanServer`. +Any of your beans that are annotated with Spring JMX annotations (`@ManagedResource`, `@ManagedAttribute`, or `@ManagedOperation`) are exposed to it. + +If your platform provides a standard `MBeanServer`, Spring Boot will use that and default to the VM `MBeanServer` if necessary. +If all that fails, a new `MBeanServer` will be created. + +See the {spring-boot-autoconfigure-module-code}/jmx/JmxAutoConfiguration.java[`JmxAutoConfiguration`] class for more details. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/json.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/json.adoc new file mode 100644 index 000000000000..e3f82ba9c827 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/json.adoc @@ -0,0 +1,34 @@ +[[features.json]] +== JSON +Spring Boot provides integration with three JSON mapping libraries: + +- Gson +- Jackson +- JSON-B + +Jackson is the preferred and default library. + + + +[[features.json.jackson]] +=== Jackson +Auto-configuration for Jackson is provided and Jackson is part of `spring-boot-starter-json`. +When Jackson is on the classpath an `ObjectMapper` bean is automatically configured. +Several configuration properties are provided for <>. + + + +[[features.json.gson]] +=== Gson +Auto-configuration for Gson is provided. +When Gson is on the classpath a `Gson` bean is automatically configured. +Several `+spring.gson.*+` configuration properties are provided for customizing the configuration. +To take more control, one or more `GsonBuilderCustomizer` beans can be used. + + + +[[features.json.json-b]] +=== JSON-B +Auto-configuration for JSON-B is provided. +When the JSON-B API and an implementation are on the classpath a `Jsonb` bean will be automatically configured. +The preferred JSON-B implementation is Apache Johnzon for which dependency management is provided. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/jta.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/jta.adoc new file mode 100644 index 000000000000..611516b9b232 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/jta.adoc @@ -0,0 +1,74 @@ +[[features.jta]] +== Distributed Transactions with JTA +Spring Boot supports distributed JTA transactions across multiple XA resources by using an https://www.atomikos.com/[Atomikos] embedded transaction manager. +JTA transactions are also supported when deploying to a suitable Java EE Application Server. + +When a JTA environment is detected, Spring's `JtaTransactionManager` is used to manage transactions. +Auto-configured JMS, DataSource, and JPA beans are upgraded to support XA transactions. +You can use standard Spring idioms, such as `@Transactional`, to participate in a distributed transaction. +If you are within a JTA environment and still want to use local transactions, you can set the configprop:spring.jta.enabled[] property to `false` to disable the JTA auto-configuration. + + + +[[features.jta.atomikos]] +=== Using an Atomikos Transaction Manager +https://www.atomikos.com/[Atomikos] is a popular open source transaction manager which can be embedded into your Spring Boot application. +You can use the `spring-boot-starter-jta-atomikos` starter to pull in the appropriate Atomikos libraries. +Spring Boot auto-configures Atomikos and ensures that appropriate `depends-on` settings are applied to your Spring beans for correct startup and shutdown ordering. + +By default, Atomikos transaction logs are written to a `transaction-logs` directory in your application's home directory (the directory in which your application jar file resides). +You can customize the location of this directory by setting a configprop:spring.jta.log-dir[] property in your `application.properties` file. +Properties starting with `spring.jta.atomikos.properties` can also be used to customize the Atomikos `UserTransactionServiceImp`. +See the {spring-boot-module-api}/jta/atomikos/AtomikosProperties.html[`AtomikosProperties` Javadoc] for complete details. + +NOTE: To ensure that multiple transaction managers can safely coordinate the same resource managers, each Atomikos instance must be configured with a unique ID. +By default, this ID is the IP address of the machine on which Atomikos is running. +To ensure uniqueness in production, you should configure the configprop:spring.jta.transaction-manager-id[] property with a different value for each instance of your application. + + + +[[features.jta.javaee]] +=== Using a Java EE Managed Transaction Manager +If you package your Spring Boot application as a `war` or `ear` file and deploy it to a Java EE application server, you can use your application server's built-in transaction manager. +Spring Boot tries to auto-configure a transaction manager by looking at common JNDI locations (`java:comp/UserTransaction`, `java:comp/TransactionManager`, and so on). +If you use a transaction service provided by your application server, you generally also want to ensure that all resources are managed by the server and exposed over JNDI. +Spring Boot tries to auto-configure JMS by looking for a `ConnectionFactory` at the JNDI path (`java:/JmsXA` or `java:/XAConnectionFactory`), and you can use the <> to configure your `DataSource`. + + + +[[features.jta.mixing-xa-and-non-xa-connections]] +=== Mixing XA and Non-XA JMS Connections +When using JTA, the primary JMS `ConnectionFactory` bean is XA-aware and participates in distributed transactions. +You can inject into your bean without needing to use any `@Qualifier`: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/jta/mixingxaandnonxaconnections/primary/MyBean.java[tag=*] +---- + +In some situations, you might want to process certain JMS messages by using a non-XA `ConnectionFactory`. +For example, your JMS processing logic might take longer than the XA timeout. + +If you want to use a non-XA `ConnectionFactory`, you can the `nonXaJmsConnectionFactory` bean: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/jta/mixingxaandnonxaconnections/nonxa/MyBean.java[tag=*] +---- + +For consistency, the `jmsConnectionFactory` bean is also provided by using the bean alias `xaJmsConnectionFactory`: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/jta/mixingxaandnonxaconnections/xa/MyBean.java[tag=*] +---- + + + +[[features.jta.supporting-alternative-embedded-transaction-manager]] +=== Supporting an Alternative Embedded Transaction Manager +The {spring-boot-module-code}/jms/XAConnectionFactoryWrapper.java[`XAConnectionFactoryWrapper`] and {spring-boot-module-code}/jdbc/XADataSourceWrapper.java[`XADataSourceWrapper`] interfaces can be used to support alternative embedded transaction managers. +The interfaces are responsible for wrapping `XAConnectionFactory` and `XADataSource` beans and exposing them as regular `ConnectionFactory` and `DataSource` beans, which transparently enroll in the distributed transaction. +DataSource and JMS auto-configuration use JTA variants, provided you have a `JtaTransactionManager` bean and appropriate XA wrapper beans registered within your `ApplicationContext`. + +The {spring-boot-module-code}/jta/atomikos/AtomikosXAConnectionFactoryWrapper.java[AtomikosXAConnectionFactoryWrapper] and {spring-boot-module-code}/jta/atomikos/AtomikosXADataSourceWrapper.java[AtomikosXADataSourceWrapper] provide good examples of how to write XA wrappers. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/kotlin.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/kotlin.adoc new file mode 100644 index 000000000000..50b8adc2f235 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/kotlin.adoc @@ -0,0 +1,173 @@ +[[features.kotlin]] +== Kotlin support +https://kotlinlang.org[Kotlin] is a statically-typed language targeting the JVM (and other platforms) which allows writing concise and elegant code while providing {kotlin-docs}java-interop.html[interoperability] with existing libraries written in Java. + +Spring Boot provides Kotlin support by leveraging the support in other Spring projects such as Spring Framework, Spring Data, and Reactor. +See the {spring-framework-docs}/languages.html#kotlin[Spring Framework Kotlin support documentation] for more information. + +The easiest way to start with Spring Boot and Kotlin is to follow https://spring.io/guides/tutorials/spring-boot-kotlin/[this comprehensive tutorial]. +You can create new Kotlin projects via https://start.spring.io/#!language=kotlin[start.spring.io]. +Feel free to join the #spring channel of https://slack.kotlinlang.org/[Kotlin Slack] or ask a question with the `spring` and `kotlin` tags on https://stackoverflow.com/questions/tagged/spring+kotlin[Stack Overflow] if you need support. + + + +[[features.kotlin.requirements]] +=== Requirements +Spring Boot requires at least Kotlin 1.3.x and manages a suitable Kotlin version via dependency management. +To use Kotlin, `org.jetbrains.kotlin:kotlin-stdlib` and `org.jetbrains.kotlin:kotlin-reflect` must be present on the classpath. +The `kotlin-stdlib` variants `kotlin-stdlib-jdk7` and `kotlin-stdlib-jdk8` can also be used. + +Since https://discuss.kotlinlang.org/t/classes-final-by-default/166[Kotlin classes are final by default], you are likely to want to configure {kotlin-docs}compiler-plugins.html#spring-support[kotlin-spring] plugin in order to automatically open Spring-annotated classes so that they can be proxied. + +https://github.com/FasterXML/jackson-module-kotlin[Jackson's Kotlin module] is required for serializing / deserializing JSON data in Kotlin. +It is automatically registered when found on the classpath. +A warning message is logged if Jackson and Kotlin are present but the Jackson Kotlin module is not. + +TIP: These dependencies and plugins are provided by default if one bootstraps a Kotlin project on https://start.spring.io/#!language=kotlin[start.spring.io]. + + + +[[features.kotlin.null-safety]] +=== Null-safety +One of Kotlin's key features is {kotlin-docs}null-safety.html[null-safety]. +It deals with `null` values at compile time rather than deferring the problem to runtime and encountering a `NullPointerException`. +This helps to eliminate a common source of bugs without paying the cost of wrappers like `Optional`. +Kotlin also allows using functional constructs with nullable values as described in this https://www.baeldung.com/kotlin-null-safety[comprehensive guide to null-safety in Kotlin]. + +Although Java does not allow one to express null-safety in its type system, Spring Framework, Spring Data, and Reactor now provide null-safety of their API via tooling-friendly annotations. +By default, types from Java APIs used in Kotlin are recognized as {kotlin-docs}java-interop.html#null-safety-and-platform-types[platform types] for which null-checks are relaxed. +{kotlin-docs}java-interop.html#jsr-305-support[Kotlin's support for JSR 305 annotations] combined with nullability annotations provide null-safety for the related Spring API in Kotlin. + +The JSR 305 checks can be configured by adding the `-Xjsr305` compiler flag with the following options: `-Xjsr305={strict|warn|ignore}`. +The default behavior is the same as `-Xjsr305=warn`. +The `strict` value is required to have null-safety taken in account in Kotlin types inferred from Spring API but should be used with the knowledge that Spring API nullability declaration could evolve even between minor releases and more checks may be added in the future). + +WARNING: Generic type arguments, varargs and array elements nullability are not yet supported. +See https://jira.spring.io/browse/SPR-15942[SPR-15942] for up-to-date information. +Also be aware that Spring Boot's own API is {github-issues}10712[not yet annotated]. + + + +[[features.kotlin.api]] +=== Kotlin API + + + +[[features.kotlin.api.run-application]] +==== runApplication +Spring Boot provides an idiomatic way to run an application with `runApplication(*args)` as shown in the following example: + +[source,kotlin,indent=0,subs="verbatim"] +---- + import org.springframework.boot.autoconfigure.SpringBootApplication + import org.springframework.boot.runApplication + + @SpringBootApplication + class MyApplication + + fun main(args: Array) { + runApplication(*args) + } +---- + +This is a drop-in replacement for `SpringApplication.run(MyApplication::class.java, *args)`. +It also allows customization of the application as shown in the following example: + +[source,kotlin,indent=0,subs="verbatim"] +---- + runApplication(*args) { + setBannerMode(OFF) + } +---- + + + +[[features.kotlin.api.extensions]] +==== Extensions +Kotlin {kotlin-docs}extensions.html[extensions] provide the ability to extend existing classes with additional functionality. +The Spring Boot Kotlin API makes use of these extensions to add new Kotlin specific conveniences to existing APIs. + +`TestRestTemplate` extensions, similar to those provided by Spring Framework for `RestOperations` in Spring Framework, are provided. +Among other things, the extensions make it possible to take advantage of Kotlin reified type parameters. + + + +[[features.kotlin.dependency-management]] +=== Dependency management +In order to avoid mixing different versions of Kotlin dependencies on the classpath, Spring Boot imports the Kotlin BOM. + +With Maven, the Kotlin version can be customized via the `kotlin.version` property and plugin management is provided for `kotlin-maven-plugin`. +With Gradle, the Spring Boot plugin automatically aligns the `kotlin.version` with the version of the Kotlin plugin. + +Spring Boot also manages the version of Coroutines dependencies by importing the Kotlin Coroutines BOM. +The version can be customized via the `kotlin-coroutines.version` property. + +TIP: `org.jetbrains.kotlinx:kotlinx-coroutines-reactor` dependency is provided by default if one bootstraps a Kotlin project with at least one reactive dependency on https://start.spring.io/#!language=kotlin[start.spring.io]. + + + +[[features.kotlin.configuration-properties]] +=== @ConfigurationProperties +`@ConfigurationProperties` when used in combination with <> supports classes with immutable `val` properties as shown in the following example: + +[source,kotlin,indent=0,subs="verbatim"] +---- +@ConstructorBinding +@ConfigurationProperties("example.kotlin") +data class KotlinExampleProperties( + val name: String, + val description: String, + val myService: MyService) { + + data class MyService( + val apiToken: String, + val uri: URI + ) +} +---- + +TIP: To generate <> using the annotation processor, {kotlin-docs}kapt.html[`kapt` should be configured] with the `spring-boot-configuration-processor` dependency. +Note that some features (such as detecting the default value or deprecated items) are not working due to limitations in the model kapt provides. + + + +[[features.kotlin.testing]] +=== Testing +While it is possible to use JUnit 4 to test Kotlin code, JUnit 5 is provided by default and is recommended. +JUnit 5 enables a test class to be instantiated once and reused for all of the class's tests. +This makes it possible to use `@BeforeAll` and `@AfterAll` annotations on non-static methods, which is a good fit for Kotlin. + +To mock Kotlin classes, https://mockk.io/[MockK] is recommended. +If you need the `Mockk` equivalent of the Mockito specific <>, you can use https://github.com/Ninja-Squad/springmockk[SpringMockK] which provides similar `@MockkBean` and `@SpykBean` annotations. + + + +[[features.kotlin.resources]] +=== Resources + + + +[[features.kotlin.resources.further-reading]] +==== Further reading +* {kotlin-docs}[Kotlin language reference] +* https://kotlinlang.slack.com/[Kotlin Slack] (with a dedicated #spring channel) +* https://stackoverflow.com/questions/tagged/spring+kotlin[Stackoverflow with `spring` and `kotlin` tags] +* https://try.kotlinlang.org/[Try Kotlin in your browser] +* https://blog.jetbrains.com/kotlin/[Kotlin blog] +* https://kotlin.link/[Awesome Kotlin] +* https://spring.io/guides/tutorials/spring-boot-kotlin/[Tutorial: building web applications with Spring Boot and Kotlin] +* https://spring.io/blog/2016/02/15/developing-spring-boot-applications-with-kotlin[Developing Spring Boot applications with Kotlin] +* https://spring.io/blog/2016/03/20/a-geospatial-messenger-with-kotlin-spring-boot-and-postgresql[A Geospatial Messenger with Kotlin, Spring Boot and PostgreSQL] +* https://spring.io/blog/2017/01/04/introducing-kotlin-support-in-spring-framework-5-0[Introducing Kotlin support in Spring Framework 5.0] +* https://spring.io/blog/2017/08/01/spring-framework-5-kotlin-apis-the-functional-way[Spring Framework 5 Kotlin APIs, the functional way] + + + +[[features.kotlin.resources.examples]] +==== Examples +* https://github.com/sdeleuze/spring-boot-kotlin-demo[spring-boot-kotlin-demo]: regular Spring Boot + Spring Data JPA project +* https://github.com/mixitconf/mixit[mixit]: Spring Boot 2 + WebFlux + Reactive Spring Data MongoDB +* https://github.com/sdeleuze/spring-kotlin-fullstack[spring-kotlin-fullstack]: WebFlux Kotlin fullstack example with Kotlin2js for frontend instead of JavaScript or TypeScript +* https://github.com/spring-petclinic/spring-petclinic-kotlin[spring-petclinic-kotlin]: Kotlin version of the Spring PetClinic Sample Application +* https://github.com/sdeleuze/spring-kotlin-deepdive[spring-kotlin-deepdive]: a step by step migration for Boot 1.0 + Java to Boot 2.0 + Kotlin +* https://github.com/sdeleuze/spring-boot-coroutines-demo[spring-boot-coroutines-demo]: Coroutines sample project diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/logging.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/logging.adoc new file mode 100644 index 000000000000..887cebdff3ce --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/logging.adoc @@ -0,0 +1,481 @@ +[[features.logging]] +== Logging +Spring Boot uses https://commons.apache.org/logging[Commons Logging] for all internal logging but leaves the underlying log implementation open. +Default configurations are provided for {java-api}/java/util/logging/package-summary.html[Java Util Logging], https://logging.apache.org/log4j/2.x/[Log4J2], and https://logback.qos.ch/[Logback]. +In each case, loggers are pre-configured to use console output with optional file output also available. + +By default, if you use the "`Starters`", Logback is used for logging. +Appropriate Logback routing is also included to ensure that dependent libraries that use Java Util Logging, Commons Logging, Log4J, or SLF4J all work correctly. + +TIP: There are a lot of logging frameworks available for Java. +Do not worry if the above list seems confusing. +Generally, you do not need to change your logging dependencies and the Spring Boot defaults work just fine. + +TIP: When you deploy your application to a servlet container or application server, logging performed via the Java Util Logging API is not routed into your application's logs. +This prevents logging performed by the container or other applications that have been deployed to it from appearing in your application's logs. + + + +[[features.logging.log-format]] +=== Log Format +The default log output from Spring Boot resembles the following example: + +[indent=0] +---- +2019-03-05 10:57:51.112 INFO 45469 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/7.0.52 +2019-03-05 10:57:51.253 INFO 45469 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext +2019-03-05 10:57:51.253 INFO 45469 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1358 ms +2019-03-05 10:57:51.698 INFO 45469 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean : Mapping servlet: 'dispatcherServlet' to [/] +2019-03-05 10:57:51.702 INFO 45469 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*] +---- + +The following items are output: + +* Date and Time: Millisecond precision and easily sortable. +* Log Level: `ERROR`, `WARN`, `INFO`, `DEBUG`, or `TRACE`. +* Process ID. +* A `---` separator to distinguish the start of actual log messages. +* Thread name: Enclosed in square brackets (may be truncated for console output). +* Logger name: This is usually the source class name (often abbreviated). +* The log message. + +NOTE: Logback does not have a `FATAL` level. +It is mapped to `ERROR`. + + + +[[features.logging.console-output]] +=== Console Output +The default log configuration echoes messages to the console as they are written. +By default, `ERROR`-level, `WARN`-level, and `INFO`-level messages are logged. +You can also enable a "`debug`" mode by starting your application with a `--debug` flag. + +[source,shell,indent=0,subs="verbatim"] +---- + $ java -jar myapp.jar --debug +---- + +NOTE: You can also specify `debug=true` in your `application.properties`. + +When the debug mode is enabled, a selection of core loggers (embedded container, Hibernate, and Spring Boot) are configured to output more information. +Enabling the debug mode does _not_ configure your application to log all messages with `DEBUG` level. + +Alternatively, you can enable a "`trace`" mode by starting your application with a `--trace` flag (or `trace=true` in your `application.properties`). +Doing so enables trace logging for a selection of core loggers (embedded container, Hibernate schema generation, and the whole Spring portfolio). + + + +[[features.logging.console-output.color-coded]] +==== Color-coded Output +If your terminal supports ANSI, color output is used to aid readability. +You can set `spring.output.ansi.enabled` to a {spring-boot-module-api}/ansi/AnsiOutput.Enabled.html[supported value] to override the auto-detection. + +Color coding is configured by using the `%clr` conversion word. +In its simplest form, the converter colors the output according to the log level, as shown in the following example: + +[source,indent=0,subs="verbatim"] +---- +%clr(%5p) +---- + +The following table describes the mapping of log levels to colors: + +|=== +| Level | Color + +| `FATAL` +| Red + +| `ERROR` +| Red + +| `WARN` +| Yellow + +| `INFO` +| Green + +| `DEBUG` +| Green + +| `TRACE` +| Green +|=== + +Alternatively, you can specify the color or style that should be used by providing it as an option to the conversion. +For example, to make the text yellow, use the following setting: + +[source,indent=0,subs="verbatim"] +---- + %clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){yellow} +---- + +The following colors and styles are supported: + +* `blue` +* `cyan` +* `faint` +* `green` +* `magenta` +* `red` +* `yellow` + + + +[[features.logging.file-output]] +=== File Output +By default, Spring Boot logs only to the console and does not write log files. +If you want to write log files in addition to the console output, you need to set a configprop:logging.file.name[] or configprop:logging.file.path[] property (for example, in your `application.properties`). + +The following table shows how the `logging.*` properties can be used together: + +.Logging properties +[cols="1,1,1,4"] +|=== +| configprop:logging.file.name[] | configprop:logging.file.path[] | Example | Description + +| _(none)_ +| _(none)_ +| +| Console only logging. + +| Specific file +| _(none)_ +| `my.log` +| Writes to the specified log file. + Names can be an exact location or relative to the current directory. + +| _(none)_ +| Specific directory +| `/var/log` +| Writes `spring.log` to the specified directory. + Names can be an exact location or relative to the current directory. +|=== + +Log files rotate when they reach 10 MB and, as with console output, `ERROR`-level, `WARN`-level, and `INFO`-level messages are logged by default. + +TIP: Logging properties are independent of the actual logging infrastructure. +As a result, specific configuration keys (such as `logback.configurationFile` for Logback) are not managed by spring Boot. + + + +[[features.logging.file-rotation]] +=== File Rotation +If you are using the Logback, it's possible to fine-tune log rotation settings using your `application.properties` or `application.yaml` file. +For all other logging system, you'll need to configure rotation settings directly yourself (for example, if you use Log4J2 then you could add a `log4j2.xml` or `log4j2-spring.xml` file). + +The following rotation policy properties are supported: + +|=== +| Name | Description + +| configprop:logging.logback.rollingpolicy.file-name-pattern[] +| The filename pattern used to create log archives. + +| configprop:logging.logback.rollingpolicy.clean-history-on-start[] +| If log archive cleanup should occur when the application starts. + +| configprop:logging.logback.rollingpolicy.max-file-size[] +| The maximum size of log file before it's archived. + +| configprop:logging.logback.rollingpolicy.total-size-cap[] +| The maximum amount of size log archives can take before being deleted. + +| configprop:logging.logback.rollingpolicy.max-history[] +| The maximum number of archive log files to keep (defaults to 7). +|=== + + + +[[features.logging.log-levels]] +=== Log Levels +All the supported logging systems can have the logger levels set in the Spring `Environment` (for example, in `application.properties`) by using `+logging.level.=+` where `level` is one of TRACE, DEBUG, INFO, WARN, ERROR, FATAL, or OFF. +The `root` logger can be configured by using `logging.level.root`. + +The following example shows potential logging settings in `application.properties`: + +[source,properties,indent=0,subs="verbatim",configprops,role="primary"] +.Properties +---- + logging.level.root=warn + logging.level.org.springframework.web=debug + logging.level.org.hibernate=error +---- + +[source,properties,indent=0,subs="verbatim",role="secondary"] +.Yaml +---- + logging: + level: + root: "warn" + org.springframework.web: "debug" + org.hibernate: "error" +---- + +It's also possible to set logging levels using environment variables. +For example, `LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_WEB=DEBUG` will set `org.springframework.web` to `DEBUG`. + +NOTE: The above approach will only work for package level logging. +Since relaxed binding always converts environment variables to lowercase, it's not possible to configure logging for an individual class in this way. +If you need to configure logging for a class, you can use <> variable. + + + +[[features.logging.log-groups]] +=== Log Groups +It's often useful to be able to group related loggers together so that they can all be configured at the same time. +For example, you might commonly change the logging levels for _all_ Tomcat related loggers, but you can't easily remember top level packages. + +To help with this, Spring Boot allows you to define logging groups in your Spring `Environment`. +For example, here's how you could define a "`tomcat`" group by adding it to your `application.properties`: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + logging: + group: + tomcat: "org.apache.catalina,org.apache.coyote,org.apache.tomcat" +---- + +Once defined, you can change the level for all the loggers in the group with a single line: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + logging: + level: + tomcat: "trace" +---- + +Spring Boot includes the following pre-defined logging groups that can be used out-of-the-box: + +[cols="1,4"] +|=== +| Name | Loggers + +| web +| `org.springframework.core.codec`, `org.springframework.http`, `org.springframework.web`, `org.springframework.boot.actuate.endpoint.web`, `org.springframework.boot.web.servlet.ServletContextInitializerBeans` + +| sql +| `org.springframework.jdbc.core`, `org.hibernate.SQL`, `org.jooq.tools.LoggerListener` +|=== + + + +[[features.logging.shutdown-hook]] +=== Using a Log Shutdown Hook +In order to release logging resources when your application terminates, a shutdown hook that will trigger log system cleanup when the JVM exits is provided. +This shutdown hook is registered automatically unless your application is deployed as a war file. +If your application has complex context hierarchies the shutdown hook may not meet your needs. +If it does not, disable the shutdown hook and investigate the options provided directly by the underlying logging system. +For example, Logback offers http://logback.qos.ch/manual/loggingSeparation.html[context selectors] which allow each Logger to be created in its own context. +You can use the configprop:logging.register-shutdown-hook[] property to disable the shutdown hook. +Setting it to `false` will disable the registration. +You can set the property in your `application.properties` or `application.yaml` file: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + logging: + register-shutdown-hook: false +---- + + + +[[features.logging.custom-log-configuration]] +=== Custom Log Configuration +The various logging systems can be activated by including the appropriate libraries on the classpath and can be further customized by providing a suitable configuration file in the root of the classpath or in a location specified by the following Spring `Environment` property: configprop:logging.config[]. + +You can force Spring Boot to use a particular logging system by using the `org.springframework.boot.logging.LoggingSystem` system property. +The value should be the fully qualified class name of a `LoggingSystem` implementation. +You can also disable Spring Boot's logging configuration entirely by using a value of `none`. + +NOTE: Since logging is initialized *before* the `ApplicationContext` is created, it is not possible to control logging from `@PropertySources` in Spring `@Configuration` files. +The only way to change the logging system or disable it entirely is via System properties. + +Depending on your logging system, the following files are loaded: + +|=== +| Logging System | Customization + +| Logback +| `logback-spring.xml`, `logback-spring.groovy`, `logback.xml`, or `logback.groovy` + +| Log4j2 +| `log4j2-spring.xml` or `log4j2.xml` + +| JDK (Java Util Logging) +| `logging.properties` +|=== + +NOTE: When possible, we recommend that you use the `-spring` variants for your logging configuration (for example, `logback-spring.xml` rather than `logback.xml`). +If you use standard configuration locations, Spring cannot completely control log initialization. + +WARNING: There are known classloading issues with Java Util Logging that cause problems when running from an 'executable jar'. +We recommend that you avoid it when running from an 'executable jar' if at all possible. + +To help with the customization, some other properties are transferred from the Spring `Environment` to System properties, as described in the following table: + +|=== +| Spring Environment | System Property | Comments + +| configprop:logging.exception-conversion-word[] +| `LOG_EXCEPTION_CONVERSION_WORD` +| The conversion word used when logging exceptions. + +| configprop:logging.file.name[] +| `LOG_FILE` +| If defined, it is used in the default log configuration. + +| configprop:logging.file.path[] +| `LOG_PATH` +| If defined, it is used in the default log configuration. + +| configprop:logging.pattern.console[] +| `CONSOLE_LOG_PATTERN` +| The log pattern to use on the console (stdout). + +| configprop:logging.pattern.dateformat[] +| `LOG_DATEFORMAT_PATTERN` +| Appender pattern for log date format. + +| configprop:logging.charset.console[] +| `CONSOLE_LOG_CHARSET` +| The charset to use for console logging. + +| configprop:logging.pattern.file[] +| `FILE_LOG_PATTERN` +| The log pattern to use in a file (if `LOG_FILE` is enabled). + +| configprop:logging.charset.file[] +| `FILE_LOG_CHARSET` +| The charset to use for file logging (if `LOG_FILE` is enabled). + +| configprop:logging.pattern.level[] +| `LOG_LEVEL_PATTERN` +| The format to use when rendering the log level (default `%5p`). + +| `PID` +| `PID` +| The current process ID (discovered if possible and when not already defined as an OS environment variable). +|=== + +If you're using Logback, the following properties are also transferred: + +|=== +| Spring Environment | System Property | Comments + +| configprop:logging.logback.rollingpolicy.file-name-pattern[] +| `LOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN` +| Pattern for rolled-over log file names (default `$\{LOG_FILE}.%d\{yyyy-MM-dd}.%i.gz`). + +| configprop:logging.logback.rollingpolicy.clean-history-on-start[] +| `LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START` +| Whether to clean the archive log files on startup. + +| configprop:logging.logback.rollingpolicy.max-file-size[] +| `LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE` +| Maximum log file size. + +| configprop:logging.logback.rollingpolicy.total-size-cap[] +| `LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP` +| Total size of log backups to be kept. + +| configprop:logging.logback.rollingpolicy.max-history[] +| `LOGBACK_ROLLINGPOLICY_MAX_HISTORY` +| Maximum number of archive log files to keep. +|=== + + +All the supported logging systems can consult System properties when parsing their configuration files. +See the default configurations in `spring-boot.jar` for examples: + +* {spring-boot-code}/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/logback/defaults.xml[Logback] +* {spring-boot-code}/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2.xml[Log4j 2] +* {spring-boot-code}/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/java/logging-file.properties[Java Util logging] + +[TIP] +==== +If you want to use a placeholder in a logging property, you should use <> and not the syntax of the underlying framework. +Notably, if you use Logback, you should use `:` as the delimiter between a property name and its default value and not use `:-`. +==== + +[TIP] +==== +You can add MDC and other ad-hoc content to log lines by overriding only the `LOG_LEVEL_PATTERN` (or `logging.pattern.level` with Logback). +For example, if you use `logging.pattern.level=user:%X\{user} %5p`, then the default log format contains an MDC entry for "user", if it exists, as shown in the following example. + +[indent=0] +---- + 2019-08-30 12:30:04.031 user:someone INFO 22174 --- [ nio-8080-exec-0] demo.Controller + Handling authenticated request +---- +==== + + + +[[features.logging.logback-extensions]] +=== Logback Extensions +Spring Boot includes a number of extensions to Logback that can help with advanced configuration. +You can use these extensions in your `logback-spring.xml` configuration file. + +NOTE: Because the standard `logback.xml` configuration file is loaded too early, you cannot use extensions in it. +You need to either use `logback-spring.xml` or define a configprop:logging.config[] property. + +WARNING: The extensions cannot be used with Logback's https://logback.qos.ch/manual/configuration.html#autoScan[configuration scanning]. +If you attempt to do so, making changes to the configuration file results in an error similar to one of the following being logged: + +[indent=0] +---- + ERROR in ch.qos.logback.core.joran.spi.Interpreter@4:71 - no applicable action for [springProperty], current ElementPath is [[configuration][springProperty]] + ERROR in ch.qos.logback.core.joran.spi.Interpreter@4:71 - no applicable action for [springProfile], current ElementPath is [[configuration][springProfile]] +---- + + + +[[features.logging.logback-extensions.profile-specific]] +==== Profile-specific Configuration +The `` tag lets you optionally include or exclude sections of configuration based on the active Spring profiles. +Profile sections are supported anywhere within the `` element. +Use the `name` attribute to specify which profile accepts the configuration. +The `` tag can contain a profile name (for example `staging`) or a profile expression. +A profile expression allows for more complicated profile logic to be expressed, for example `production & (eu-central | eu-west)`. +Check the {spring-framework-docs}/core.html#beans-definition-profiles-java[reference guide] for more details. +The following listing shows three sample profiles: + +[source,xml,subs="verbatim",indent=0] +---- + + + + + + + + + + + +---- + + + +[[features.logging.logback-extensions.environment-properties]] +==== Environment Properties +The `` tag lets you expose properties from the Spring `Environment` for use within Logback. +Doing so can be useful if you want to access values from your `application.properties` file in your Logback configuration. +The tag works in a similar way to Logback's standard `` tag. +However, rather than specifying a direct `value`, you specify the `source` of the property (from the `Environment`). +If you need to store the property somewhere other than in `local` scope, you can use the `scope` attribute. +If you need a fallback value (in case the property is not set in the `Environment`), you can use the `defaultValue` attribute. +The following example shows how to expose properties for use within Logback: + +[source,xml,subs="verbatim",indent=0] +---- + + + ${fluentHost} + ... + +---- + +NOTE: The `source` must be specified in kebab case (such as `my.property-name`). +However, properties can be added to the `Environment` by using the relaxed rules. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/messaging.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/messaging.adoc new file mode 100644 index 000000000000..7393b9d03f29 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/messaging.adoc @@ -0,0 +1,506 @@ +[[features.messaging]] +== Messaging +The Spring Framework provides extensive support for integrating with messaging systems, from simplified use of the JMS API using `JmsTemplate` to a complete infrastructure to receive messages asynchronously. +Spring AMQP provides a similar feature set for the Advanced Message Queuing Protocol. +Spring Boot also provides auto-configuration options for `RabbitTemplate` and RabbitMQ. +Spring WebSocket natively includes support for STOMP messaging, and Spring Boot has support for that through starters and a small amount of auto-configuration. +Spring Boot also has support for Apache Kafka. + + + +[[features.messaging.jms]] +=== JMS +The `javax.jms.ConnectionFactory` interface provides a standard method of creating a `javax.jms.Connection` for interacting with a JMS broker. +Although Spring needs a `ConnectionFactory` to work with JMS, you generally need not use it directly yourself and can instead rely on higher level messaging abstractions. +(See the {spring-framework-docs}/integration.html#jms[relevant section] of the Spring Framework reference documentation for details.) +Spring Boot also auto-configures the necessary infrastructure to send and receive messages. + + + +[[features.messaging.jms.activemq]] +==== ActiveMQ Support +When https://activemq.apache.org/[ActiveMQ] is available on the classpath, Spring Boot can also configure a `ConnectionFactory`. +If the broker is present, an embedded broker is automatically started and configured (provided no broker URL is specified through configuration and the embedded broker is not disabled in the configuration). + +NOTE: If you use `spring-boot-starter-activemq`, the necessary dependencies to connect or embed an ActiveMQ instance are provided, as is the Spring infrastructure to integrate with JMS. + +ActiveMQ configuration is controlled by external configuration properties in `+spring.activemq.*+`. + +By default, ActiveMQ is auto-configured to use the https://activemq.apache.org/vm-transport-reference.html[VM transport], which starts a broker embedded in the same JVM instance. + +You can disable the embedded broker by configuring the configprop:spring.activemq.in-memory[] property, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + activemq: + in-memory: false +---- + +The embedded broker will also be disabled if you configure the broker URL, as shown in the following example: + +[source,yaml,indent=0,configprops,configblocks] +---- + spring: + activemq: + broker-url: "tcp://192.168.1.210:9876" + user: "admin" + password: "secret" +---- + +If you want to take full control over the embedded broker, refer to https://activemq.apache.org/how-do-i-embed-a-broker-inside-a-connection.html[the ActiveMQ documentation] for further information. + +By default, a `CachingConnectionFactory` wraps the native `ConnectionFactory` with sensible settings that you can control by external configuration properties in `+spring.jms.*+`: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + jms: + cache: + session-cache-size: 5 +---- + +If you'd rather use native pooling, you can do so by adding a dependency to `org.messaginghub:pooled-jms` and configuring the `JmsPoolConnectionFactory` accordingly, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + activemq: + pool: + enabled: true + max-connections: 50 +---- + +TIP: See {spring-boot-autoconfigure-module-code}/jms/activemq/ActiveMQProperties.java[`ActiveMQProperties`] for more of the supported options. +You can also register an arbitrary number of beans that implement `ActiveMQConnectionFactoryCustomizer` for more advanced customizations. + +By default, ActiveMQ creates a destination if it does not yet exist so that destinations are resolved against their provided names. + + + +[[features.messaging.jms.artemis]] +==== ActiveMQ Artemis Support +Spring Boot can auto-configure a `ConnectionFactory` when it detects that https://activemq.apache.org/components/artemis/[ActiveMQ Artemis] is available on the classpath. +If the broker is present, an embedded broker is automatically started and configured (unless the mode property has been explicitly set). +The supported modes are `embedded` (to make explicit that an embedded broker is required and that an error should occur if the broker is not available on the classpath) and `native` (to connect to a broker using the `netty` transport protocol). +When the latter is configured, Spring Boot configures a `ConnectionFactory` that connects to a broker running on the local machine with the default settings. + +NOTE: If you use `spring-boot-starter-artemis`, the necessary dependencies to connect to an existing ActiveMQ Artemis instance are provided, as well as the Spring infrastructure to integrate with JMS. +Adding `org.apache.activemq:artemis-jms-server` to your application lets you use embedded mode. + +ActiveMQ Artemis configuration is controlled by external configuration properties in `+spring.artemis.*+`. +For example, you might declare the following section in `application.properties`: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + artemis: + mode: native + broker-url: "tcp://192.168.1.210:9876" + user: "admin" + password: "secret" +---- + +When embedding the broker, you can choose if you want to enable persistence and list the destinations that should be made available. +These can be specified as a comma-separated list to create them with the default options, or you can define bean(s) of type `org.apache.activemq.artemis.jms.server.config.JMSQueueConfiguration` or `org.apache.activemq.artemis.jms.server.config.TopicConfiguration`, for advanced queue and topic configurations, respectively. + +By default, a `CachingConnectionFactory` wraps the native `ConnectionFactory` with sensible settings that you can control by external configuration properties in `+spring.jms.*+`: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + jms: + cache: + session-cache-size: 5 +---- + +If you'd rather use native pooling, you can do so by adding a dependency to `org.messaginghub:pooled-jms` and configuring the `JmsPoolConnectionFactory` accordingly, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + artemis: + pool: + enabled: true + max-connections: 50 +---- + +See {spring-boot-autoconfigure-module-code}/jms/artemis/ArtemisProperties.java[`ArtemisProperties`] for more supported options. + +No JNDI lookup is involved, and destinations are resolved against their names, using either the `name` attribute in the Artemis configuration or the names provided through configuration. + + + +[[features.messaging.jms.jndi]] +==== Using a JNDI ConnectionFactory +If you are running your application in an application server, Spring Boot tries to locate a JMS `ConnectionFactory` by using JNDI. +By default, the `java:/JmsXA` and `java:/XAConnectionFactory` location are checked. +You can use the configprop:spring.jms.jndi-name[] property if you need to specify an alternative location, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + jms: + jndi-name: "java:/MyConnectionFactory" +---- + + + +[[features.messaging.jms.sending]] +==== Sending a Message +Spring's `JmsTemplate` is auto-configured, and you can autowire it directly into your own beans, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/messaging/jms/sending/MyBean.java[] +---- + +NOTE: {spring-framework-api}/jms/core/JmsMessagingTemplate.html[`JmsMessagingTemplate`] can be injected in a similar manner. +If a `DestinationResolver` or a `MessageConverter` bean is defined, it is associated automatically to the auto-configured `JmsTemplate`. + + + +[[features.messaging.jms.receiving]] +==== Receiving a Message +When the JMS infrastructure is present, any bean can be annotated with `@JmsListener` to create a listener endpoint. +If no `JmsListenerContainerFactory` has been defined, a default one is configured automatically. +If a `DestinationResolver`, a `MessageConverter`, or a `javax.jms.ExceptionListener` beans are defined, they are associated automatically with the default factory. + +By default, the default factory is transactional. +If you run in an infrastructure where a `JtaTransactionManager` is present, it is associated to the listener container by default. +If not, the `sessionTransacted` flag is enabled. +In that latter scenario, you can associate your local data store transaction to the processing of an incoming message by adding `@Transactional` on your listener method (or a delegate thereof). +This ensures that the incoming message is acknowledged, once the local transaction has completed. +This also includes sending response messages that have been performed on the same JMS session. + +The following component creates a listener endpoint on the `someQueue` destination: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/messaging/jms/receiving/MyBean.java[] +---- + +TIP: See {spring-framework-api}/jms/annotation/EnableJms.html[the Javadoc of `@EnableJms`] for more details. + +If you need to create more `JmsListenerContainerFactory` instances or if you want to override the default, Spring Boot provides a `DefaultJmsListenerContainerFactoryConfigurer` that you can use to initialize a `DefaultJmsListenerContainerFactory` with the same settings as the one that is auto-configured. + +For instance, the following example exposes another factory that uses a specific `MessageConverter`: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/messaging/jms/receiving/custom/MyJmsConfiguration.java[] +---- + +Then you can use the factory in any `@JmsListener`-annotated method as follows: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/messaging/jms/receiving/custom/MyBean.java[] +---- + + + +[[features.messaging.amqp]] +=== AMQP +The Advanced Message Queuing Protocol (AMQP) is a platform-neutral, wire-level protocol for message-oriented middleware. +The Spring AMQP project applies core Spring concepts to the development of AMQP-based messaging solutions. +Spring Boot offers several conveniences for working with AMQP through RabbitMQ, including the `spring-boot-starter-amqp` "`Starter`". + + + +[[features.messaging.amqp.rabbitmq]] +==== RabbitMQ support +https://www.rabbitmq.com/[RabbitMQ] is a lightweight, reliable, scalable, and portable message broker based on the AMQP protocol. +Spring uses `RabbitMQ` to communicate through the AMQP protocol. + +RabbitMQ configuration is controlled by external configuration properties in `+spring.rabbitmq.*+`. +For example, you might declare the following section in `application.properties`: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + rabbitmq: + host: "localhost" + port: 5672 + username: "admin" + password: "secret" +---- + +Alternatively, you could configure the same connection using the `addresses` attribute: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + rabbitmq: + addresses: "amqp://admin:secret@localhost" +---- + +NOTE: When specifying addresses that way, the `host` and `port` properties are ignored. +If the address uses the `amqps` protocol, SSL support is enabled automatically. + +See {spring-boot-autoconfigure-module-code}/amqp/RabbitProperties.java[`RabbitProperties`] for more of the supported property-based configuration options. +To configure lower-level details of the RabbitMQ `ConnectionFactory` that is used by Spring AMQP, define a `ConnectionFactoryCustomizer` bean. + +If a `ConnectionNameStrategy` bean exists in the context, it will be automatically used to name connections created by the auto-configured `CachingConnectionFactory`. + +TIP: See https://spring.io/blog/2010/06/14/understanding-amqp-the-protocol-used-by-rabbitmq/[Understanding AMQP, the protocol used by RabbitMQ] for more details. + + + +[[features.messaging.amqp.sending]] +==== Sending a Message +Spring's `AmqpTemplate` and `AmqpAdmin` are auto-configured, and you can autowire them directly into your own beans, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/messaging/amqp/sending/MyBean.java[] +---- + +NOTE: {spring-amqp-api}/rabbit/core/RabbitMessagingTemplate.html[`RabbitMessagingTemplate`] can be injected in a similar manner. +If a `MessageConverter` bean is defined, it is associated automatically to the auto-configured `AmqpTemplate`. + +If necessary, any `org.springframework.amqp.core.Queue` that is defined as a bean is automatically used to declare a corresponding queue on the RabbitMQ instance. + +To retry operations, you can enable retries on the `AmqpTemplate` (for example, in the event that the broker connection is lost): + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + rabbitmq: + template: + retry: + enabled: true + initial-interval: "2s" +---- + +Retries are disabled by default. +You can also customize the `RetryTemplate` programmatically by declaring a `RabbitRetryTemplateCustomizer` bean. + +If you need to create more `RabbitTemplate` instances or if you want to override the default, Spring Boot provides a `RabbitTemplateConfigurer` bean that you can use to initialize a `RabbitTemplate` with the same settings as the factories used by the auto-configuration. + + + +[[features.messaging.amqp.receiving]] +==== Receiving a Message +When the Rabbit infrastructure is present, any bean can be annotated with `@RabbitListener` to create a listener endpoint. +If no `RabbitListenerContainerFactory` has been defined, a default `SimpleRabbitListenerContainerFactory` is automatically configured and you can switch to a direct container using the configprop:spring.rabbitmq.listener.type[] property. +If a `MessageConverter` or a `MessageRecoverer` bean is defined, it is automatically associated with the default factory. + +The following sample component creates a listener endpoint on the `someQueue` queue: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/messaging/amqp/receiving/MyBean.java[] +---- + +TIP: See {spring-amqp-api}/rabbit/annotation/EnableRabbit.html[the Javadoc of `@EnableRabbit`] for more details. + +If you need to create more `RabbitListenerContainerFactory` instances or if you want to override the default, Spring Boot provides a `SimpleRabbitListenerContainerFactoryConfigurer` and a `DirectRabbitListenerContainerFactoryConfigurer` that you can use to initialize a `SimpleRabbitListenerContainerFactory` and a `DirectRabbitListenerContainerFactory` with the same settings as the factories used by the auto-configuration. + +TIP: It does not matter which container type you chose. +Those two beans are exposed by the auto-configuration. + +For instance, the following configuration class exposes another factory that uses a specific `MessageConverter`: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/messaging/amqp/receiving/custom/MyRabbitConfiguration.java[] +---- + +Then you can use the factory in any `@RabbitListener`-annotated method, as follows: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/messaging/amqp/receiving/custom/MyBean.java[] +---- + +You can enable retries to handle situations where your listener throws an exception. +By default, `RejectAndDontRequeueRecoverer` is used, but you can define a `MessageRecoverer` of your own. +When retries are exhausted, the message is rejected and either dropped or routed to a dead-letter exchange if the broker is configured to do so. +By default, retries are disabled. +You can also customize the `RetryTemplate` programmatically by declaring a `RabbitRetryTemplateCustomizer` bean. + +IMPORTANT: By default, if retries are disabled and the listener throws an exception, the delivery is retried indefinitely. +You can modify this behavior in two ways: Set the `defaultRequeueRejected` property to `false` so that zero re-deliveries are attempted or throw an `AmqpRejectAndDontRequeueException` to signal the message should be rejected. +The latter is the mechanism used when retries are enabled and the maximum number of delivery attempts is reached. + + + +[[features.messaging.kafka]] +=== Apache Kafka Support +https://kafka.apache.org/[Apache Kafka] is supported by providing auto-configuration of the `spring-kafka` project. + +Kafka configuration is controlled by external configuration properties in `spring.kafka.*`. +For example, you might declare the following section in `application.properties`: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + kafka: + bootstrap-servers: "localhost:9092" + consumer: + group-id: "myGroup" +---- + +TIP: To create a topic on startup, add a bean of type `NewTopic`. +If the topic already exists, the bean is ignored. + +See {spring-boot-autoconfigure-module-code}/kafka/KafkaProperties.java[`KafkaProperties`] for more supported options. + + + +[[features.messaging.kafka.sending]] +==== Sending a Message +Spring's `KafkaTemplate` is auto-configured, and you can autowire it directly in your own beans, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/messaging/kafka/sending/MyBean.java[] +---- + +NOTE: If the property configprop:spring.kafka.producer.transaction-id-prefix[] is defined, a `KafkaTransactionManager` is automatically configured. +Also, if a `RecordMessageConverter` bean is defined, it is automatically associated to the auto-configured `KafkaTemplate`. + + + +[[features.messaging.kafka.receiving]] +==== Receiving a Message +When the Apache Kafka infrastructure is present, any bean can be annotated with `@KafkaListener` to create a listener endpoint. +If no `KafkaListenerContainerFactory` has been defined, a default one is automatically configured with keys defined in `spring.kafka.listener.*`. + +The following component creates a listener endpoint on the `someTopic` topic: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/messaging/kafka/receiving/MyBean.java[] +---- + +If a `KafkaTransactionManager` bean is defined, it is automatically associated to the container factory. +Similarly, if a `RecordFilterStrategy`, `ErrorHandler`, `AfterRollbackProcessor` or `ConsumerAwareRebalanceListener` bean is defined, it is automatically associated to the default factory. + +Depending on the listener type, a `RecordMessageConverter` or `BatchMessageConverter` bean is associated to the default factory. +If only a `RecordMessageConverter` bean is present for a batch listener, it is wrapped in a `BatchMessageConverter`. + +TIP: A custom `ChainedKafkaTransactionManager` must be marked `@Primary` as it usually references the auto-configured `KafkaTransactionManager` bean. + + + +[[features.messaging.kafka.streams]] +==== Kafka Streams +Spring for Apache Kafka provides a factory bean to create a `StreamsBuilder` object and manage the lifecycle of its streams. +Spring Boot auto-configures the required `KafkaStreamsConfiguration` bean as long as `kafka-streams` is on the classpath and Kafka Streams is enabled via the `@EnableKafkaStreams` annotation. + +Enabling Kafka Streams means that the application id and bootstrap servers must be set. +The former can be configured using `spring.kafka.streams.application-id`, defaulting to `spring.application.name` if not set. +The latter can be set globally or specifically overridden only for streams. + +Several additional properties are available using dedicated properties; other arbitrary Kafka properties can be set using the `spring.kafka.streams.properties` namespace. +See also <> for more information. + +To use the factory bean, wire `StreamsBuilder` into your `@Bean` as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/messaging/kafka/streams/MyKafkaStreamsConfiguration.java[] +---- + +By default, the streams managed by the `StreamBuilder` object it creates are started automatically. +You can customize this behavior using the configprop:spring.kafka.streams.auto-startup[] property. + + + +[[features.messaging.kafka.additional-properties]] +==== Additional Kafka Properties +The properties supported by auto configuration are shown in the <> section of the Appendix. +Note that, for the most part, these properties (hyphenated or camelCase) map directly to the Apache Kafka dotted properties. +Refer to the Apache Kafka documentation for details. + +The first few of these properties apply to all components (producers, consumers, admins, and streams) but can be specified at the component level if you wish to use different values. +Apache Kafka designates properties with an importance of HIGH, MEDIUM, or LOW. +Spring Boot auto-configuration supports all HIGH importance properties, some selected MEDIUM and LOW properties, and any properties that do not have a default value. + +Only a subset of the properties supported by Kafka are available directly through the `KafkaProperties` class. +If you wish to configure the producer or consumer with additional properties that are not directly supported, use the following properties: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + kafka: + properties: + "[prop.one]": "first" + admin: + properties: + "[prop.two]": "second" + consumer: + properties: + "[prop.three]": "third" + producer: + properties: + "[prop.four]": "fourth" + streams: + properties: + "[prop.five]": "fifth" +---- + +This sets the common `prop.one` Kafka property to `first` (applies to producers, consumers and admins), the `prop.two` admin property to `second`, the `prop.three` consumer property to `third`, the `prop.four` producer property to `fourth` and the `prop.five` streams property to `fifth`. + +You can also configure the Spring Kafka `JsonDeserializer` as follows: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + kafka: + consumer: + value-deserializer: "org.springframework.kafka.support.serializer.JsonDeserializer" + properties: + "[spring.json.value.default.type]": "com.example.Invoice" + "[spring.json.trusted.packages]": "com.example.main,com.example.another" +---- + +Similarly, you can disable the `JsonSerializer` default behavior of sending type information in headers: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + kafka: + producer: + value-serializer: "org.springframework.kafka.support.serializer.JsonSerializer" + properties: + "[spring.json.add.type.headers]": false +---- + +IMPORTANT: Properties set in this way override any configuration item that Spring Boot explicitly supports. + + + +[[features.messaging.kafka.embedded]] +==== Testing with Embedded Kafka +Spring for Apache Kafka provides a convenient way to test projects with an embedded Apache Kafka broker. +To use this feature, annotate a test class with `@EmbeddedKafka` from the `spring-kafka-test` module. +For more information, please see the Spring for Apache Kafka {spring-kafka-docs}#embedded-kafka-annotation[reference manual]. + +To make Spring Boot auto-configuration work with the aforementioned embedded Apache Kafka broker, you need to remap a system property for embedded broker addresses (populated by the `EmbeddedKafkaBroker`) into the Spring Boot configuration property for Apache Kafka. +There are several ways to do that: + +* Provide a system property to map embedded broker addresses into configprop:spring.kafka.bootstrap-servers[] in the test class: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/messaging/kafka/embedded/property/MyTest.java[tag=*] +---- + +* Configure a property name on the `@EmbeddedKafka` annotation: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/messaging/kafka/embedded/annotation/MyTest.java[] +---- + +* Use a placeholder in configuration properties: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + kafka: + bootstrap-servers: "${spring.embedded.kafka.brokers}" +---- diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/nosql.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/nosql.adoc new file mode 100644 index 000000000000..a346d7a00b5a --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/nosql.adoc @@ -0,0 +1,648 @@ +[[features.nosql]] +== Working with NoSQL Technologies +Spring Data provides additional projects that help you access a variety of NoSQL technologies, including: + +* {spring-data-mongodb}[MongoDB] +* {spring-data-neo4j}[Neo4J] +* {spring-data-elasticsearch}[Elasticsearch] +* {spring-data-redis}[Redis] +* {spring-data-gemfire}[GemFire] or {spring-data-geode}[Geode] +* {spring-data-cassandra}[Cassandra] +* {spring-data-couchbase}[Couchbase] +* {spring-data-ldap}[LDAP] + +Spring Boot provides auto-configuration for Redis, MongoDB, Neo4j, Solr, Elasticsearch, Cassandra, Couchbase, LDAP and InfluxDB. +Additionally, {spring-boot-for-apache-geode}[Spring Boot for Apache Geode] provides {spring-boot-for-apache-geode-docs}#geode-repositories[auto-configuration for Apache Geode]. +You can make use of the other projects, but you must configure them yourself. +Refer to the appropriate reference documentation at {spring-data}. + + + +[[features.nosql.redis]] +=== Redis +https://redis.io/[Redis] is a cache, message broker, and richly-featured key-value store. +Spring Boot offers basic auto-configuration for the https://github.com/lettuce-io/lettuce-core/[Lettuce] and https://github.com/xetorthio/jedis/[Jedis] client libraries and the abstractions on top of them provided by https://github.com/spring-projects/spring-data-redis[Spring Data Redis]. + +There is a `spring-boot-starter-data-redis` "`Starter`" for collecting the dependencies in a convenient way. +By default, it uses https://github.com/lettuce-io/lettuce-core/[Lettuce]. +That starter handles both traditional and reactive applications. + +TIP: We also provide a `spring-boot-starter-data-redis-reactive` "`Starter`" for consistency with the other stores with reactive support. + + + +[[features.nosql.redis.connecting]] +==== Connecting to Redis +You can inject an auto-configured `RedisConnectionFactory`, `StringRedisTemplate`, or vanilla `RedisTemplate` instance as you would any other Spring Bean. +By default, the instance tries to connect to a Redis server at `localhost:6379`. +The following listing shows an example of such a bean: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/nosql/redis/connecting/MyBean.java[] +---- + +TIP: You can also register an arbitrary number of beans that implement `LettuceClientConfigurationBuilderCustomizer` for more advanced customizations. +If you use Jedis, `JedisClientConfigurationBuilderCustomizer` is also available. + +If you add your own `@Bean` of any of the auto-configured types, it replaces the default (except in the case of `RedisTemplate`, when the exclusion is based on the bean name, `redisTemplate`, not its type). + +A pooled connection factory is auto-configured if `commons-pool2` is on the classpath and at least one `Pool` option of {spring-boot-autoconfigure-module-code}/data/redis/RedisProperties.java[`RedisProperties`] is set. + + + +[[features.nosql.mongodb]] +=== MongoDB +https://www.mongodb.com/[MongoDB] is an open-source NoSQL document database that uses a JSON-like schema instead of traditional table-based relational data. +Spring Boot offers several conveniences for working with MongoDB, including the `spring-boot-starter-data-mongodb` and `spring-boot-starter-data-mongodb-reactive` "`Starters`". + + + +[[features.nosql.mongodb.connecting]] +==== Connecting to a MongoDB Database +To access MongoDB databases, you can inject an auto-configured `org.springframework.data.mongodb.MongoDatabaseFactory`. +By default, the instance tries to connect to a MongoDB server at `mongodb://localhost/test`. +The following example shows how to connect to a MongoDB database: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/nosql/mongodb/connecting/MyBean.java[] +---- + +If you have defined your own `MongoClient`, it will be used to auto-configure a suitable `MongoDatabaseFactory`. + +The auto-configured `MongoClient` is created using a `MongoClientSettings` bean. +If you have defined your own `MongoClientSettings`, it will be used without modification and the `spring.data.mongodb` properties will be ignored. +Otherwise a `MongoClientSettings` will be auto-configured and will have the `spring.data.mongodb` properties applied to it. +In either case, you can declare one or more `MongoClientSettingsBuilderCustomizer` beans to fine-tune the `MongoClientSettings` configuration. +Each will be called in order with the `MongoClientSettings.Builder` that is used to build the `MongoClientSettings`. + +You can set the configprop:spring.data.mongodb.uri[] property to change the URL and configure additional settings such as the _replica set_, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + data: + mongodb: + uri: "mongodb://user:secret@mongo1.example.com:12345,mongo2.example.com:23456/test" +---- + +Alternatively, you can specify connection details using discrete properties. +For example, you might declare the following settings in your `application.properties`: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + data: + mongodb: + host: "mongoserver.example.com" + port: 27017 + database: "test" + username: "user" + password: "secret" +---- + +TIP: If `spring.data.mongodb.port` is not specified, the default of `27017` is used. +You could delete this line from the example shown earlier. + +TIP: If you do not use Spring Data MongoDB, you can inject a `MongoClient` bean instead of using `MongoDatabaseFactory`. +If you want to take complete control of establishing the MongoDB connection, you can also declare your own `MongoDatabaseFactory` or `MongoClient` bean. + +NOTE: If you are using the reactive driver, Netty is required for SSL. +The auto-configuration configures this factory automatically if Netty is available and the factory to use hasn't been customized already. + + + +[[features.nosql.mongodb.template]] +==== MongoTemplate +{spring-data-mongodb}[Spring Data MongoDB] provides a {spring-data-mongodb-api}/core/MongoTemplate.html[`MongoTemplate`] class that is very similar in its design to Spring's `JdbcTemplate`. +As with `JdbcTemplate`, Spring Boot auto-configures a bean for you to inject the template, as follows: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/nosql/mongodb/template/MyBean.java[] +---- + +See the {spring-data-mongodb-api}/core/MongoOperations.html[`MongoOperations` Javadoc] for complete details. + + + +[[features.nosql.mongodb.repositories]] +==== Spring Data MongoDB Repositories +Spring Data includes repository support for MongoDB. +As with the JPA repositories discussed earlier, the basic principle is that queries are constructed automatically, based on method names. + +In fact, both Spring Data JPA and Spring Data MongoDB share the same common infrastructure. +You could take the JPA example from earlier and, assuming that `City` is now a MongoDB data class rather than a JPA `@Entity`, it works in the same way, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/nosql/mongodb/repositories/CityRepository.java[] +---- + +TIP: You can customize document scanning locations by using the `@EntityScan` annotation. + +TIP: For complete details of Spring Data MongoDB, including its rich object mapping technologies, refer to its {spring-data-mongodb}[reference documentation]. + + + +[[features.nosql.mongodb.embedded]] +==== Embedded Mongo +Spring Boot offers auto-configuration for https://github.com/flapdoodle-oss/de.flapdoodle.embed.mongo[Embedded Mongo]. +To use it in your Spring Boot application, add a dependency on `de.flapdoodle.embed:de.flapdoodle.embed.mongo`. + +The port that Mongo listens on can be configured by setting the configprop:spring.data.mongodb.port[] property. +To use a randomly allocated free port, use a value of 0. +The `MongoClient` created by `MongoAutoConfiguration` is automatically configured to use the randomly allocated port. + +NOTE: If you do not configure a custom port, the embedded support uses a random port (rather than 27017) by default. + +If you have SLF4J on the classpath, the output produced by Mongo is automatically routed to a logger named `org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongo`. + +You can declare your own `IMongodConfig` and `IRuntimeConfig` beans to take control of the Mongo instance's configuration and logging routing. +The download configuration can be customized by declaring a `DownloadConfigBuilderCustomizer` bean. + + + +[[features.nosql.neo4j]] +=== Neo4j +https://neo4j.com/[Neo4j] is an open-source NoSQL graph database that uses a rich data model of nodes connected by first class relationships, which is better suited for connected big data than traditional RDBMS approaches. +Spring Boot offers several conveniences for working with Neo4j, including the `spring-boot-starter-data-neo4j` "`Starter`". + + + +[[features.nosql.neo4j.connecting]] +==== Connecting to a Neo4j Database +To access a Neo4j server, you can inject an auto-configured `org.neo4j.driver.Driver`. +By default, the instance tries to connect to a Neo4j server at `localhost:7687` using the Bolt protocol. +The following example shows how to inject a Neo4j `Driver` that gives you access, amongst other things, to a `Session`: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/nosql/neo4j/connecting/MyBean.java[] +---- + +You can configure various aspects of the driver using `spring.neo4j.*` properties. +The following example shows how to configure the uri and credentials to use: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + neo4j: + uri: "bolt://my-server:7687" + authentication: + username: "neo4j" + password: "secret" +---- + +The auto-configured `Driver` is created using `ConfigBuilder`. +To fine-tune its configuration, declare one or more `ConfigBuilderCustomizer` beans. +Each will be called in order with the `ConfigBuilder` that is used to build the `Driver`. + + + +[[features.nosql.neo4j.repositories]] +==== Spring Data Neo4j Repositories +Spring Data includes repository support for Neo4j. +For complete details of Spring Data Neo4j, refer to the {spring-data-neo4j-docs}[reference documentation]. + +Spring Data Neo4j shares the common infrastructure with Spring Data JPA as many other Spring Data modules do. +You could take the JPA example from earlier and define `City` as Spring Data Neo4j `@Node` rather than JPA `@Entity` and the repository abstraction works in the same way, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/nosql/neo4j/repositories/CityRepository.java[] +---- + +The `spring-boot-starter-data-neo4j` "`Starter`" enables the repository support as well as transaction management. +Spring Boot supports both classic and reactive Neo4j repositories, using the `Neo4jTemplate` or `ReactiveNeo4jTemplate` beans. +When Project Reactor is available on the classpath, the reactive style is also auto-configured. + +You can customize the locations to look for repositories and entities by using `@EnableNeo4jRepositories` and `@EntityScan` respectively on a `@Configuration`-bean. + +[NOTE] +==== +In an application using the reactive style, a `ReactiveTransactionManager` is not auto-configured. +To enable transaction management, the following bean must be defined in your configuration: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/nosql/neo4j/repositories/MyNeo4jConfiguration.java[] +---- +==== + + + +[[features.nosql.solr]] +=== Solr +https://lucene.apache.org/solr/[Apache Solr] is a search engine. +Spring Boot offers basic auto-configuration for the Solr 5 client library. + + + +[[features.nosql.solr.connecting]] +==== Connecting to Solr +You can inject an auto-configured `SolrClient` instance as you would any other Spring bean. +By default, the instance tries to connect to a server at `http://localhost:8983/solr`. +The following example shows how to inject a Solr bean: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/nosql/solr/connecting/MyBean.java[] +---- + +If you add your own `@Bean` of type `SolrClient`, it replaces the default. + + + +[[features.nosql.elasticsearch]] +=== Elasticsearch +https://www.elastic.co/products/elasticsearch[Elasticsearch] is an open source, distributed, RESTful search and analytics engine. +Spring Boot offers basic auto-configuration for Elasticsearch. + +Spring Boot supports several clients: + +* The official Java "Low Level" and "High Level" REST clients +* The `ReactiveElasticsearchClient` provided by Spring Data Elasticsearch + +Spring Boot provides a dedicated "`Starter`", `spring-boot-starter-data-elasticsearch`. + + + +[[features.nosql.elasticsearch.connecting-using-rest]] +==== Connecting to Elasticsearch using REST clients +Elasticsearch ships https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/index.html[two different REST clients] that you can use to query a cluster: the "Low Level" client and the "High Level" client. +Spring Boot provides support for the "High Level" client, which ships with `org.elasticsearch.client:elasticsearch-rest-high-level-client`. + +If you have this dependency on the classpath, Spring Boot will auto-configure and register a `RestHighLevelClient` bean that by default targets `http://localhost:9200`. +You can further tune how `RestHighLevelClient` is configured, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + elasticsearch: + rest: + uris: "https://search.example.com:9200" + read-timeout: "10s" + username: "user" + password: "secret" +---- + +You can also register an arbitrary number of beans that implement `RestClientBuilderCustomizer` for more advanced customizations. +To take full control over the registration, define a `RestClientBuilder` bean. + +TIP: If your application needs access to a "Low Level" `RestClient`, you can get it by calling `client.getLowLevelClient()` on the auto-configured `RestHighLevelClient`. + +Additionally, if `elasticsearch-rest-client-sniffer` is on the classpath, a `Sniffer` is auto-configured to automatically discover nodes from a running Elasticsearch cluster and set them to the `RestHighLevelClient` bean. +You can further tune how `Sniffer` is configured, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + elasticsearch: + rest: + sniffer: + interval: 10m + delay-after-failure: 30s +---- + + + +[[features.nosql.elasticsearch.connecting-using-reactive-rest]] +==== Connecting to Elasticsearch using Reactive REST clients +{spring-data-elasticsearch}[Spring Data Elasticsearch] ships `ReactiveElasticsearchClient` for querying Elasticsearch instances in a reactive fashion. +It is built on top of WebFlux's `WebClient`, so both `spring-boot-starter-elasticsearch` and `spring-boot-starter-webflux` dependencies are useful to enable this support. + +By default, Spring Boot will auto-configure and register a `ReactiveElasticsearchClient` +bean that targets `http://localhost:9200`. +You can further tune how it is configured, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + data: + elasticsearch: + client: + reactive: + endpoints: "search.example.com:9200" + use-ssl: true + socket-timeout: "10s" + username: "user" + password: "secret" +---- + +If the configuration properties are not enough and you'd like to fully control the client +configuration, you can register a custom `ClientConfiguration` bean. + + + +[[features.nosql.elasticsearch.connecting-using-spring-data]] +==== Connecting to Elasticsearch by Using Spring Data +To connect to Elasticsearch, a `RestHighLevelClient` bean must be defined, +auto-configured by Spring Boot or manually provided by the application (see previous sections). +With this configuration in place, an +`ElasticsearchRestTemplate` can be injected like any other Spring bean, +as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/nosql/elasticsearch/connectingusingspringdata/MyBean.java[] +---- + +In the presence of `spring-data-elasticsearch` and the required dependencies for using a `WebClient` (typically `spring-boot-starter-webflux`), Spring Boot can also auto-configure a <> and a `ReactiveElasticsearchTemplate` as beans. +They are the reactive equivalent of the other REST clients. + + + +[[features.nosql.elasticsearch.repositories]] +==== Spring Data Elasticsearch Repositories +Spring Data includes repository support for Elasticsearch. +As with the JPA repositories discussed earlier, the basic principle is that queries are constructed for you automatically based on method names. + +In fact, both Spring Data JPA and Spring Data Elasticsearch share the same common infrastructure. +You could take the JPA example from earlier and, assuming that `City` is now an Elasticsearch `@Document` class rather than a JPA `@Entity`, it works in the same way. + +TIP: For complete details of Spring Data Elasticsearch, refer to the {spring-data-elasticsearch-docs}[reference documentation]. + +Spring Boot supports both classic and reactive Elasticsearch repositories, using the `ElasticsearchRestTemplate` or `ReactiveElasticsearchTemplate` beans. +Most likely those beans are auto-configured by Spring Boot given the required dependencies are present. + +If you wish to use your own template for backing the Elasticsearch repositories, you can add your own `ElasticsearchRestTemplate` or `ElasticsearchOperations` `@Bean`, as long as it is named `"elasticsearchTemplate"`. +Same applies to `ReactiveElasticsearchTemplate` and `ReactiveElasticsearchOperations`, with the bean name `"reactiveElasticsearchTemplate"`. + +You can choose to disable the repositories support with the following property: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + data: + elasticsearch: + repositories: + enabled: false +---- + + + +[[features.nosql.cassandra]] +=== Cassandra +https://cassandra.apache.org/[Cassandra] is an open source, distributed database management system designed to handle large amounts of data across many commodity servers. +Spring Boot offers auto-configuration for Cassandra and the abstractions on top of it provided by https://github.com/spring-projects/spring-data-cassandra[Spring Data Cassandra]. +There is a `spring-boot-starter-data-cassandra` "`Starter`" for collecting the dependencies in a convenient way. + + + +[[features.nosql.cassandra.connecting]] +==== Connecting to Cassandra +You can inject an auto-configured `CassandraTemplate` or a Cassandra `CqlSession` instance as you would with any other Spring Bean. +The `spring.data.cassandra.*` properties can be used to customize the connection. +Generally, you provide `keyspace-name` and `contact-points` as well the local datacenter name, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + data: + cassandra: + keyspace-name: "mykeyspace" + contact-points: "cassandrahost1:9042,cassandrahost2:9042" + local-datacenter: "datacenter1" +---- + +If the port is the same for all your contact points you can use a shortcut and only specify the host names, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + data: + cassandra: + keyspace-name: "mykeyspace" + contact-points: "cassandrahost1,cassandrahost2" + local-datacenter: "datacenter1" +---- + +TIP: Those two examples are identical as the port default to `9042`. +If you need to configure the port, use `spring.data.cassandra.port`. + +[NOTE] +==== +The Cassandra driver has its own configuration infrastructure that loads an `application.conf` at the root of the classpath. + +Spring Boot does not look for such a file by default but can load one using `spring.data.cassandra.config`. +If a property is both present in `+spring.data.cassandra.*+` and the configuration file, the value in `+spring.data.cassandra.*+` takes precedence. + +For more advanced driver customizations, you can register an arbitrary number of beans that implement `DriverConfigLoaderBuilderCustomizer`. +The `CqlSession` can be customized with a bean of type `CqlSessionBuilderCustomizer`. +==== + +NOTE: If you're using `CqlSessionBuilder` to create multiple `CqlSession` beans, keep in mind the builder is mutable so make sure to inject a fresh copy for each session. + +The following code listing shows how to inject a Cassandra bean: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/nosql/cassandra/connecting/MyBean.java[] +---- + +If you add your own `@Bean` of type `CassandraTemplate`, it replaces the default. + + + +[[features.nosql.cassandra.repositories]] +==== Spring Data Cassandra Repositories +Spring Data includes basic repository support for Cassandra. +Currently, this is more limited than the JPA repositories discussed earlier and needs to annotate finder methods with `@Query`. + +TIP: For complete details of Spring Data Cassandra, refer to the https://docs.spring.io/spring-data/cassandra/docs/[reference documentation]. + + + +[[features.nosql.couchbase]] +=== Couchbase +https://www.couchbase.com/[Couchbase] is an open-source, distributed, multi-model NoSQL document-oriented database that is optimized for interactive applications. +Spring Boot offers auto-configuration for Couchbase and the abstractions on top of it provided by https://github.com/spring-projects/spring-data-couchbase[Spring Data Couchbase]. +There are `spring-boot-starter-data-couchbase` and `spring-boot-starter-data-couchbase-reactive` "`Starters`" for collecting the dependencies in a convenient way. + + + +[[features.nosql.couchbase.connecting]] +==== Connecting to Couchbase +You can get a `Cluster` by adding the Couchbase SDK and some configuration. +The `spring.couchbase.*` properties can be used to customize the connection. +Generally, you provide the https://github.com/couchbaselabs/sdk-rfcs/blob/master/rfc/0011-connection-string.md[connection string], username, and password, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + couchbase: + connection-string: "couchbase://192.168.1.123" + username: "user" + password: "secret" +---- + +It is also possible to customize some of the `ClusterEnvironment` settings. +For instance, the following configuration changes the timeout to use to open a new `Bucket` and enables SSL support: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + couchbase: + env: + timeouts: + connect: "3s" + ssl: + key-store: "/location/of/keystore.jks" + key-store-password: "secret" +---- + +TIP: Check the `spring.couchbase.env.*` properties for more details. +To take more control, one or more `ClusterEnvironmentBuilderCustomizer` beans can be used. + + + +[[features.nosql.couchbase.repositories]] +==== Spring Data Couchbase Repositories +Spring Data includes repository support for Couchbase. +For complete details of Spring Data Couchbase, refer to the {spring-data-couchbase-docs}[reference documentation]. + +You can inject an auto-configured `CouchbaseTemplate` instance as you would with any other Spring Bean, provided a `CouchbaseClientFactory` bean is available. +This happens when a `Cluster` is available, as described above, and a bucket name has been specified: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + data: + couchbase: + bucket-name: "my-bucket" +---- + +The following examples shows how to inject a `CouchbaseTemplate` bean: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/nosql/couchbase/repositories/MyBean.java[] +---- + +There are a few beans that you can define in your own configuration to override those provided by the auto-configuration: + +* A `CouchbaseMappingContext` `@Bean` with a name of `couchbaseMappingContext`. +* A `CustomConversions` `@Bean` with a name of `couchbaseCustomConversions`. +* A `CouchbaseTemplate` `@Bean` with a name of `couchbaseTemplate`. + +To avoid hard-coding those names in your own config, you can reuse `BeanNames` provided by Spring Data Couchbase. +For instance, you can customize the converters to use, as follows: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/nosql/couchbase/repositories/MyCouchbaseConfiguration.java[] +---- + + + +[[features.nosql.ldap]] +=== LDAP +https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol[LDAP] (Lightweight Directory Access Protocol) is an open, vendor-neutral, industry standard application protocol for accessing and maintaining distributed directory information services over an IP network. +Spring Boot offers auto-configuration for any compliant LDAP server as well as support for the embedded in-memory LDAP server from https://ldap.com/unboundid-ldap-sdk-for-java/[UnboundID]. + +LDAP abstractions are provided by https://github.com/spring-projects/spring-data-ldap[Spring Data LDAP]. +There is a `spring-boot-starter-data-ldap` "`Starter`" for collecting the dependencies in a convenient way. + + + +[[features.nosql.ldap.connecting]] +==== Connecting to an LDAP Server +To connect to an LDAP server, make sure you declare a dependency on the `spring-boot-starter-data-ldap` "`Starter`" or `spring-ldap-core` and then declare the URLs of your server in your application.properties, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + ldap: + urls: "ldap://myserver:1235" + username: "admin" + password: "secret" +---- + +If you need to customize connection settings, you can use the `spring.ldap.base` and `spring.ldap.base-environment` properties. + +An `LdapContextSource` is auto-configured based on these settings. +If a `DirContextAuthenticationStrategy` bean is available, it is associated to the auto-configured `LdapContextSource`. +If you need to customize it, for instance to use a `PooledContextSource`, you can still inject the auto-configured `LdapContextSource`. +Make sure to flag your customized `ContextSource` as `@Primary` so that the auto-configured `LdapTemplate` uses it. + + + +[[features.nosql.ldap.repositories]] +==== Spring Data LDAP Repositories +Spring Data includes repository support for LDAP. +For complete details of Spring Data LDAP, refer to the https://docs.spring.io/spring-data/ldap/docs/1.0.x/reference/html/[reference documentation]. + +You can also inject an auto-configured `LdapTemplate` instance as you would with any other Spring Bean, as shown in the following example: + + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/nosql/ldap/repositories/MyBean.java[] +---- + + + +[[features.nosql.ldap.embedded]] +==== Embedded In-memory LDAP Server +For testing purposes, Spring Boot supports auto-configuration of an in-memory LDAP server from https://ldap.com/unboundid-ldap-sdk-for-java/[UnboundID]. +To configure the server, add a dependency to `com.unboundid:unboundid-ldapsdk` and declare a configprop:spring.ldap.embedded.base-dn[] property, as follows: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + ldap: + embedded: + base-dn: "dc=spring,dc=io" +---- + +[NOTE] +==== +It is possible to define multiple base-dn values, however, since distinguished names usually contain commas, they must be defined using the correct notation. + +In yaml files, you can use the yaml list notation. In properties files, you must include the index as part of the property name: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring.ldap.embedded.base-dn: + - dc=spring,dc=io + - dc=pivotal,dc=io +---- +==== + +By default, the server starts on a random port and triggers the regular LDAP support. +There is no need to specify a configprop:spring.ldap.urls[] property. + +If there is a `schema.ldif` file on your classpath, it is used to initialize the server. +If you want to load the initialization script from a different resource, you can also use the configprop:spring.ldap.embedded.ldif[] property. + +By default, a standard schema is used to validate `LDIF` files. +You can turn off validation altogether by setting the configprop:spring.ldap.embedded.validation.enabled[] property. +If you have custom attributes, you can use configprop:spring.ldap.embedded.validation.schema[] to define your custom attribute types or object classes. + + + +[[features.nosql.influxdb]] +=== InfluxDB +https://www.influxdata.com/[InfluxDB] is an open-source time series database optimized for fast, high-availability storage and retrieval of time series data in fields such as operations monitoring, application metrics, Internet-of-Things sensor data, and real-time analytics. + + + +[[features.nosql.influxdb.connecting]] +==== Connecting to InfluxDB +Spring Boot auto-configures an `InfluxDB` instance, provided the `influxdb-java` client is on the classpath and the URL of the database is set, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + influx: + url: "https://172.0.0.1:8086" +---- + +If the connection to InfluxDB requires a user and password, you can set the `spring.influx.user` and `spring.influx.password` properties accordingly. + +InfluxDB relies on OkHttp. +If you need to tune the http client `InfluxDB` uses behind the scenes, you can register an `InfluxDbOkHttpClientBuilderProvider` bean. + +If you need more control over the configuration, consider registering an `InfluxDbCustomizer` bean. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/profiles.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/profiles.adoc new file mode 100644 index 000000000000..3602354de6cc --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/profiles.adoc @@ -0,0 +1,122 @@ +[[features.profiles]] +== Profiles +Spring Profiles provide a way to segregate parts of your application configuration and make it be available only in certain environments. +Any `@Component`, `@Configuration` or `@ConfigurationProperties` can be marked with `@Profile` to limit when it is loaded, as shown in the following example: + +[source,java,indent=0] +---- +include::{docs-java}/features/profiles/ProductionConfiguration.java[] +---- + +NOTE: If `@ConfigurationProperties` beans are registered via `@EnableConfigurationProperties` instead of automatic scanning, the `@Profile` annotation needs to be specified on the `@Configuration` class that has the `@EnableConfigurationProperties` annotation. +In the case where `@ConfigurationProperties` are scanned, `@Profile` can be specified on the `@ConfigurationProperties` class itself. + +You can use a configprop:spring.profiles.active[] `Environment` property to specify which profiles are active. +You can specify the property in any of the ways described earlier in this chapter. +For example, you could include it in your `application.properties`, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + profiles: + active: "dev,hsqldb" +---- + +You could also specify it on the command line by using the following switch: `--spring.profiles.active=dev,hsqldb`. + +If no profile is active, a default profile is enabled. +The name of the default profile is `default` and it can be tuned using the configprop:spring.profiles.default[] `Environment` property, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + profiles: + default: "none" +---- + +`spring.profiles.active` and `spring.profiles.default` can only be used in non-profile specific documents. +This means they cannot be included in <> or <> by `spring.config.activate.on-profile`. + +For example, the second document configuration is invalid: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + # this document is valid + spring: + profiles: + active: "prod" + --- + # this document is invalid + spring: + config: + activate: + on-profile: "prod" + profiles: + active: "metrics" +---- + + + +[[features.profiles.adding-active-profiles]] +=== Adding Active Profiles +The configprop:spring.profiles.active[] property follows the same ordering rules as other properties: The highest `PropertySource` wins. +This means that you can specify active profiles in `application.properties` and then *replace* them by using the command line switch. + +Sometimes, it is useful to have properties that *add* to the active profiles rather than replace them. +The `spring.profiles.include` property can be used to add active profiles on top of those activated by the configprop:spring.profiles.active[] property. +The `SpringApplication` entry point also has a Java API for setting additional profiles. +See the `setAdditionalProfiles()` method in {spring-boot-module-api}/SpringApplication.html[SpringApplication]. + +For example, when an application with the following properties is run, the common and local profiles will be activated even when it runs using the --spring.profiles.active switch: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + profiles: + include: + - "common" + - "local" +---- + +WARNING: Similar to `spring.profiles.active`, `spring.profiles.include` can only be used in non-profile specific documents. +This means it cannot be included in <> or <> by `spring.config.activate.on-profile`. + +Profile groups, which are described in the <> can also be used to add active profiles if a given profile is active. + + + +[[features.profiles.groups]] +=== Profile Groups +Occasionally the profiles that you define and use in your application are too fine-grained and become cumbersome to use. +For example, you might have `proddb` and `prodmq` profiles that you use to enable database and messaging features independently. + +To help with this, Spring Boot lets you define profile groups. +A profile group allows you to define a logical name for a related group of profiles. + +For example, we can create a `production` group that consists of our `proddb` and `prodmq` profiles. + +[source,yaml,indent=0,subs="verbatim",configblocks] +---- + spring: + profiles: + group: + production: + - "proddb" + - "prodmq" +---- + +Our application can now be started using `--spring.profiles.active=production` to active the `production`, `proddb` and `prodmq` profiles in one hit. + + + +[[features.profiles.programmatically-setting-profiles]] +=== Programmatically Setting Profiles +You can programmatically set active profiles by calling `SpringApplication.setAdditionalProfiles(...)` before your application runs. +It is also possible to activate profiles by using Spring's `ConfigurableEnvironment` interface. + + + +[[features.profiles.profile-specific-configuration-files]] +=== Profile-specific Configuration Files +Profile-specific variants of both `application.properties` (or `application.yml`) and files referenced through `@ConfigurationProperties` are considered as files and loaded. +See "<>" for details. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/quartz.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/quartz.adoc new file mode 100644 index 000000000000..1bf925d7eb64 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/quartz.adoc @@ -0,0 +1,56 @@ +[[features.quartz]] +== Quartz Scheduler +Spring Boot offers several conveniences for working with the https://www.quartz-scheduler.org/[Quartz scheduler], including the `spring-boot-starter-quartz` "`Starter`". +If Quartz is available, a `Scheduler` is auto-configured (through the `SchedulerFactoryBean` abstraction). + +Beans of the following types are automatically picked up and associated with the `Scheduler`: + +* `JobDetail`: defines a particular Job. + `JobDetail` instances can be built with the `JobBuilder` API. +* `Calendar`. +* `Trigger`: defines when a particular job is triggered. + +By default, an in-memory `JobStore` is used. +However, it is possible to configure a JDBC-based store if a `DataSource` bean is available in your application and if the configprop:spring.quartz.job-store-type[] property is configured accordingly, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + quartz: + job-store-type: "jdbc" +---- + +When the JDBC store is used, the schema can be initialized on startup, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + quartz: + jdbc: + initialize-schema: "always" +---- + +WARNING: By default, the database is detected and initialized by using the standard scripts provided with the Quartz library. +These scripts drop existing tables, deleting all triggers on every restart. +It is also possible to provide a custom script by setting the configprop:spring.quartz.jdbc.schema[] property. + +To have Quartz use a `DataSource` other than the application's main `DataSource`, declare a `DataSource` bean, annotating its `@Bean` method with `@QuartzDataSource`. +Doing so ensures that the Quartz-specific `DataSource` is used by both the `SchedulerFactoryBean` and for schema initialization. +Similarly, to have Quartz use a `TransactionManager` other than the application's main `TransactionManager` declare a `TransactionManager` bean, annotating its `@Bean` method with `@QuartzTransactionManager`. + +By default, jobs created by configuration will not overwrite already registered jobs that have been read from a persistent job store. +To enable overwriting existing job definitions set the configprop:spring.quartz.overwrite-existing-jobs[] property. + +Quartz Scheduler configuration can be customized using `spring.quartz` properties and `SchedulerFactoryBeanCustomizer` beans, which allow programmatic `SchedulerFactoryBean` customization. +Advanced Quartz configuration properties can be customized using `spring.quartz.properties.*`. + +NOTE: In particular, an `Executor` bean is not associated with the scheduler as Quartz offers a way to configure the scheduler via `spring.quartz.properties`. +If you need to customize the task executor, consider implementing `SchedulerFactoryBeanCustomizer`. + +Jobs can define setters to inject data map properties. +Regular beans can also be injected in a similar manner, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/quartz/MySampleJob.java[] +---- diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/resttemplate.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/resttemplate.adoc new file mode 100644 index 000000000000..630206d75be6 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/resttemplate.adoc @@ -0,0 +1,48 @@ +[[features.resttemplate]] +== Calling REST Services with RestTemplate +If you need to call remote REST services from your application, you can use the Spring Framework's {spring-framework-api}/web/client/RestTemplate.html[`RestTemplate`] class. +Since `RestTemplate` instances often need to be customized before being used, Spring Boot does not provide any single auto-configured `RestTemplate` bean. +It does, however, auto-configure a `RestTemplateBuilder`, which can be used to create `RestTemplate` instances when needed. +The auto-configured `RestTemplateBuilder` ensures that sensible `HttpMessageConverters` are applied to `RestTemplate` instances. + +The following code shows a typical example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/resttemplate/MyService.java[] +---- + +TIP: `RestTemplateBuilder` includes a number of useful methods that can be used to quickly configure a `RestTemplate`. +For example, to add BASIC auth support, you can use `builder.basicAuthentication("user", "password").build()`. + + + +[[features.resttemplate.customization]] +=== RestTemplate Customization +There are three main approaches to `RestTemplate` customization, depending on how broadly you want the customizations to apply. + +To make the scope of any customizations as narrow as possible, inject the auto-configured `RestTemplateBuilder` and then call its methods as required. +Each method call returns a new `RestTemplateBuilder` instance, so the customizations only affect this use of the builder. + +To make an application-wide, additive customization, use a `RestTemplateCustomizer` bean. +All such beans are automatically registered with the auto-configured `RestTemplateBuilder` and are applied to any templates that are built with it. + +The following example shows a customizer that configures the use of a proxy for all hosts except `192.168.0.5`: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/resttemplate/customization/MyRestTemplateCustomizer.java[] +---- + +Finally, you can define your own `RestTemplateBuilder` bean. +Doing so will replace the auto-configured builder. +If you want any `RestTemplateCustomizer` beans to be applied to your custom builder, as the auto-configuration would have done, configure it using a `RestTemplateBuilderConfigurer`. +The following example exposes a `RestTemplateBuilder` that matches what Spring Boot's auto-configuration would have done, except that custom connect and read timeouts are also specified: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/resttemplate/customization/MyRestTemplateBuilderConfiguration.java[] +---- + +The most extreme (and rarely used) option is to create your own `RestTemplateBuilder` bean without using a configurer. +In addition to replacing the auto-configured builder, this also prevents any `RestTemplateCustomizer` beans from being used. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/rsocket.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/rsocket.adoc new file mode 100644 index 000000000000..8318b8faf1e9 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/rsocket.adoc @@ -0,0 +1,86 @@ +[[features.rsocket]] +== RSocket +https://rsocket.io[RSocket] is a binary protocol for use on byte stream transports. +It enables symmetric interaction models via async message passing over a single connection. + + +The `spring-messaging` module of the Spring Framework provides support for RSocket requesters and responders, both on the client and on the server side. +See the {spring-framework-docs}/web-reactive.html#rsocket-spring[RSocket section] of the Spring Framework reference for more details, including an overview of the RSocket protocol. + + + +[[features.rsocket.strategies-auto-configuration]] +=== RSocket Strategies Auto-configuration +Spring Boot auto-configures an `RSocketStrategies` bean that provides all the required infrastructure for encoding and decoding RSocket payloads. +By default, the auto-configuration will try to configure the following (in order): + +. https://cbor.io/[CBOR] codecs with Jackson +. JSON codecs with Jackson + +The `spring-boot-starter-rsocket` starter provides both dependencies. +Check out the <> to know more about customization possibilities. + +Developers can customize the `RSocketStrategies` component by creating beans that implement the `RSocketStrategiesCustomizer` interface. +Note that their `@Order` is important, as it determines the order of codecs. + + + +[[features.rsocket.server-auto-configuration]] +=== RSocket server Auto-configuration +Spring Boot provides RSocket server auto-configuration. +The required dependencies are provided by the `spring-boot-starter-rsocket`. + +Spring Boot allows exposing RSocket over WebSocket from a WebFlux server, or standing up an independent RSocket server. +This depends on the type of application and its configuration. + +For WebFlux application (i.e. of type `WebApplicationType.REACTIVE`), the RSocket server will be plugged into the Web Server only if the following properties match: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + rsocket: + server: + mapping-path: "/rsocket" + transport: "websocket" +---- + +WARNING: Plugging RSocket into a web server is only supported with Reactor Netty, as RSocket itself is built with that library. + +Alternatively, an RSocket TCP or websocket server is started as an independent, embedded server. +Besides the dependency requirements, the only required configuration is to define a port for that server: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + rsocket: + server: + port: 9898 +---- + + + +[[features.rsocket.messaging]] +=== Spring Messaging RSocket support +Spring Boot will auto-configure the Spring Messaging infrastructure for RSocket. + +This means that Spring Boot will create a `RSocketMessageHandler` bean that will handle RSocket requests to your application. + + + +[[features.rsocket.requester]] +=== Calling RSocket Services with RSocketRequester +Once the `RSocket` channel is established between server and client, any party can send or receive requests to the other. + +As a server, you can get injected with an `RSocketRequester` instance on any handler method of an RSocket `@Controller`. +As a client, you need to configure and establish an RSocket connection first. +Spring Boot auto-configures an `RSocketRequester.Builder` for such cases with the expected codecs. + +The `RSocketRequester.Builder` instance is a prototype bean, meaning each injection point will provide you with a new instance . +This is done on purpose since this builder is stateful and you shouldn't create requesters with different setups using the same instance. + +The following code shows a typical example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/rsocket//requester/MyService.java[] +---- diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/security.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/security.adoc new file mode 100644 index 000000000000..668b345754a7 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/security.adoc @@ -0,0 +1,316 @@ +[[features.security]] +== Security +If {spring-security}[Spring Security] is on the classpath, then web applications are secured by default. +Spring Boot relies on Spring Security’s content-negotiation strategy to determine whether to use `httpBasic` or `formLogin`. +To add method-level security to a web application, you can also add `@EnableGlobalMethodSecurity` with your desired settings. +Additional information can be found in the {spring-security-docs}#jc-method[Spring Security Reference Guide]. + +The default `UserDetailsService` has a single user. +The user name is `user`, and the password is random and is printed at WARN level when the application starts, as shown in the following example: + +[indent=0] +---- + Using generated security password: 78fa095d-3f4c-48b1-ad50-e24c31d5cf35 + + This generated password is for development use only. Your security configuration must be updated before running your application in production. +---- + +NOTE: If you fine-tune your logging configuration, ensure that the `org.springframework.boot.autoconfigure.security` category is set to log `WARN`-level messages. +Otherwise, the default password is not printed. + +You can change the username and password by providing a `spring.security.user.name` and `spring.security.user.password`. + +The basic features you get by default in a web application are: + +* A `UserDetailsService` (or `ReactiveUserDetailsService` in case of a WebFlux application) bean with in-memory store and a single user with a generated password (see {spring-boot-module-api}/autoconfigure/security/SecurityProperties.User.html[`SecurityProperties.User`] for the properties of the user). +* Form-based login or HTTP Basic security (depending on the `Accept` header in the request) for the entire application (including actuator endpoints if actuator is on the classpath). +* A `DefaultAuthenticationEventPublisher` for publishing authentication events. + +You can provide a different `AuthenticationEventPublisher` by adding a bean for it. + + + +[[features.security.spring-mvc]] +=== MVC Security +The default security configuration is implemented in `SecurityAutoConfiguration` and `UserDetailsServiceAutoConfiguration`. +`SecurityAutoConfiguration` imports `SpringBootWebSecurityConfiguration` for web security and `UserDetailsServiceAutoConfiguration` configures authentication, which is also relevant in non-web applications. +To switch off the default web application security configuration completely or to combine multiple Spring Security components such as OAuth2 Client and Resource Server, add a bean of type `SecurityFilterChain` (doing so does not disable the `UserDetailsService` configuration or Actuator's security). + +To also switch off the `UserDetailsService` configuration, you can add a bean of type `UserDetailsService`, `AuthenticationProvider`, or `AuthenticationManager`. + +Access rules can be overridden by adding a custom `SecurityFilterChain` or `WebSecurityConfigurerAdapter` bean. +Spring Boot provides convenience methods that can be used to override access rules for actuator endpoints and static resources. +`EndpointRequest` can be used to create a `RequestMatcher` that is based on the configprop:management.endpoints.web.base-path[] property. +`PathRequest` can be used to create a `RequestMatcher` for resources in commonly used locations. + + + +[[features.security.spring-webflux]] +=== WebFlux Security +Similar to Spring MVC applications, you can secure your WebFlux applications by adding the `spring-boot-starter-security` dependency. +The default security configuration is implemented in `ReactiveSecurityAutoConfiguration` and `UserDetailsServiceAutoConfiguration`. +`ReactiveSecurityAutoConfiguration` imports `WebFluxSecurityConfiguration` for web security and `UserDetailsServiceAutoConfiguration` configures authentication, which is also relevant in non-web applications. +To switch off the default web application security configuration completely, you can add a bean of type `WebFilterChainProxy` (doing so does not disable the `UserDetailsService` configuration or Actuator's security). + +To also switch off the `UserDetailsService` configuration, you can add a bean of type `ReactiveUserDetailsService` or `ReactiveAuthenticationManager`. + +Access rules and the use of multiple Spring Security components such as OAuth 2 Client and Resource Server can be configured by adding a custom `SecurityWebFilterChain` bean. +Spring Boot provides convenience methods that can be used to override access rules for actuator endpoints and static resources. +`EndpointRequest` can be used to create a `ServerWebExchangeMatcher` that is based on the configprop:management.endpoints.web.base-path[] property. + +`PathRequest` can be used to create a `ServerWebExchangeMatcher` for resources in commonly used locations. + +For example, you can customize your security configuration by adding something like: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/security/springwebflux/MyWebFluxSecurityConfiguration.java[] +---- + + + +[[features.security.oauth2]] +=== OAuth2 +https://oauth.net/2/[OAuth2] is a widely used authorization framework that is supported by Spring. + + + +[[features.security.oauth2.client]] +==== Client +If you have `spring-security-oauth2-client` on your classpath, you can take advantage of some auto-configuration to set up an OAuth2/Open ID Connect clients. +This configuration makes use of the properties under `OAuth2ClientProperties`. +The same properties are applicable to both servlet and reactive applications. + +You can register multiple OAuth2 clients and providers under the `spring.security.oauth2.client` prefix, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + security: + oauth2: + client: + registration: + my-client-1: + client-id: "abcd" + client-secret: "password" + client-name: "Client for user scope" + provider: "my-oauth-provider" + scope: "user" + redirect-uri: "https://my-redirect-uri.com" + client-authentication-method: "basic" + authorization-grant-type: "authorization-code" + + my-client-2: + client-id: "abcd" + client-secret: "password" + client-name: "Client for email scope" + provider: "my-oauth-provider" + scope: "email" + redirect-uri: "https://my-redirect-uri.com" + client-authentication-method: "basic" + authorization-grant-type: "authorization_code" + + provider: + my-oauth-provider: + authorization-uri: "https://my-auth-server/oauth/authorize" + token-uri: "https://my-auth-server/oauth/token" + user-info-uri: "https://my-auth-server/userinfo" + user-info-authentication-method: "header" + jwk-set-uri: "https://my-auth-server/token_keys" + user-name-attribute: "name" +---- + +For OpenID Connect providers that support https://openid.net/specs/openid-connect-discovery-1_0.html[OpenID Connect discovery], the configuration can be further simplified. +The provider needs to be configured with an `issuer-uri` which is the URI that the it asserts as its Issuer Identifier. +For example, if the `issuer-uri` provided is "https://example.com", then an `OpenID Provider Configuration Request` will be made to "https://example.com/.well-known/openid-configuration". +The result is expected to be an `OpenID Provider Configuration Response`. +The following example shows how an OpenID Connect Provider can be configured with the `issuer-uri`: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + security: + oauth2: + client: + provider: + oidc-provider: + issuer-uri: "https://dev-123456.oktapreview.com/oauth2/default/" +---- + +By default, Spring Security's `OAuth2LoginAuthenticationFilter` only processes URLs matching `/login/oauth2/code/*`. +If you want to customize the `redirect-uri` to use a different pattern, you need to provide configuration to process that custom pattern. +For example, for servlet applications, you can add your own `SecurityFilterChain` that resembles the following: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/security/oauth2/client/MyOAuthClientConfiguration.java[] +---- + +TIP: Spring Boot auto-configures an `InMemoryOAuth2AuthorizedClientService` which is used by Spring Security for the management of client registrations. +The `InMemoryOAuth2AuthorizedClientService` has limited capabilities and we recommend using it only for development environments. +For production environments, consider using a `JdbcOAuth2AuthorizedClientService` or creating your own implementation of `OAuth2AuthorizedClientService`. + + + +[[features.security.oauth2.client.common-providers]] +===== OAuth2 client registration for common providers +For common OAuth2 and OpenID providers, including Google, Github, Facebook, and Okta, we provide a set of provider defaults (`google`, `github`, `facebook`, and `okta`, respectively). + +If you do not need to customize these providers, you can set the `provider` attribute to the one for which you need to infer defaults. +Also, if the key for the client registration matches a default supported provider, Spring Boot infers that as well. + +In other words, the two configurations in the following example use the Google provider: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + security: + oauth2: + client: + registration: + my-client: + client-id: "abcd" + client-secret: "password" + provider: "google" + google: + client-id: "abcd" + client-secret: "password" +---- + + + +[[features.security.oauth2.server]] +==== Resource Server +If you have `spring-security-oauth2-resource-server` on your classpath, Spring Boot can set up an OAuth2 Resource Server. +For JWT configuration, a JWK Set URI or OIDC Issuer URI needs to be specified, as shown in the following examples: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + security: + oauth2: + resourceserver: + jwt: + jwk-set-uri: "https://example.com/oauth2/default/v1/keys" +---- + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + security: + oauth2: + resourceserver: + jwt: + issuer-uri: "https://dev-123456.oktapreview.com/oauth2/default/" +---- + +NOTE: If the authorization server does not support a JWK Set URI, you can configure the resource server with the Public Key used for verifying the signature of the JWT. +This can be done using the configprop:spring.security.oauth2.resourceserver.jwt.public-key-location[] property, where the value needs to point to a file containing the public key in the PEM-encoded x509 format. + +The same properties are applicable for both servlet and reactive applications. + +Alternatively, you can define your own `JwtDecoder` bean for servlet applications or a `ReactiveJwtDecoder` for reactive applications. + +In cases where opaque tokens are used instead of JWTs, you can configure the following properties to validate tokens via introspection: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + security: + oauth2: + resourceserver: + opaquetoken: + introspection-uri: "https://example.com/check-token" + client-id: "my-client-id" + client-secret: "my-client-secret" +---- + +Again, the same properties are applicable for both servlet and reactive applications. + +Alternatively, you can define your own `OpaqueTokenIntrospector` bean for servlet applications or a `ReactiveOpaqueTokenIntrospector` for reactive applications. + + + +[[features.security.oauth2.authorization-server]] +==== Authorization Server +Currently, Spring Security does not provide support for implementing an OAuth 2.0 Authorization Server. +However, this functionality is available from the {spring-security-oauth2}[Spring Security OAuth] project, which will eventually be superseded by Spring Security completely. +Until then, you can use the `spring-security-oauth2-autoconfigure` module to easily set up an OAuth 2.0 authorization server; see its https://docs.spring.io/spring-security-oauth2-boot/[documentation] for instructions. + + + +[[features.security.saml2]] +=== SAML 2.0 + + + +[[features.security.saml2.relying-party]] +==== Relying Party +If you have `spring-security-saml2-service-provider` on your classpath, you can take advantage of some auto-configuration to set up a SAML 2.0 Relying Party. +This configuration makes use of the properties under `Saml2RelyingPartyProperties`. + +A relying party registration represents a paired configuration between an Identity Provider, IDP, and a Service Provider, SP. +You can register multiple relying parties under the `spring.security.saml2.relyingparty` prefix, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + security: + saml2: + relyingparty: + registration: + my-relying-party1: + signing: + credentials: + - private-key-location: "path-to-private-key" + certificate-location: "path-to-certificate" + decryption: + credentials: + - private-key-location: "path-to-private-key" + certificate-location: "path-to-certificate" + identityprovider: + verification: + credentials: + - certificate-location: "path-to-verification-cert" + entity-id: "remote-idp-entity-id1" + sso-url: "https://remoteidp1.sso.url" + + my-relying-party2: + signing: + credentials: + - private-key-location: "path-to-private-key" + certificate-location: "path-to-certificate" + decryption: + credentials: + - private-key-location: "path-to-private-key" + certificate-location: "path-to-certificate" + identityprovider: + verification: + credentials: + - certificate-location: "path-to-other-verification-cert" + entity-id: "remote-idp-entity-id2" + sso-url: "https://remoteidp2.sso.url" +---- + + + +[[features.security.actuator]] +=== Actuator Security +For security purposes, all actuators other than `/health` are disabled by default. +The configprop:management.endpoints.web.exposure.include[] property can be used to enable the actuators. + +If Spring Security is on the classpath and no other `WebSecurityConfigurerAdapter` or `SecurityFilterChain` bean is present, all actuators other than `/health` are secured by Spring Boot auto-configuration. +If you define a custom `WebSecurityConfigurerAdapter` or `SecurityFilterChain` bean, Spring Boot auto-configuration will back off and you will be in full control of actuator access rules. + +NOTE: Before setting the `management.endpoints.web.exposure.include`, ensure that the exposed actuators do not contain sensitive information and/or are secured by placing them behind a firewall or by something like Spring Security. + + + +[[features.security.actuator.csrf]] +==== Cross Site Request Forgery Protection +Since Spring Boot relies on Spring Security's defaults, CSRF protection is turned on by default. +This means that the actuator endpoints that require a `POST` (shutdown and loggers endpoints), `PUT` or `DELETE` will get a 403 forbidden error when the default security configuration is in use. + +NOTE: We recommend disabling CSRF protection completely only if you are creating a service that is used by non-browser clients. + +Additional information about CSRF protection can be found in the {spring-security-docs}#csrf[Spring Security Reference Guide]. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/spring-application.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/spring-application.adoc new file mode 100644 index 000000000000..e33e83b10739 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/spring-application.adoc @@ -0,0 +1,421 @@ +[[features.spring-application]] +== SpringApplication +The `SpringApplication` class provides a convenient way to bootstrap a Spring application that is started from a `main()` method. +In many situations, you can delegate to the static `SpringApplication.run` method, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/springapplication/MyApplication.java[] +---- + +When your application starts, you should see something similar to the following output: + +[indent=0,subs="verbatim,attributes"] +---- + . ____ _ __ _ _ + /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ +( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ + \\/ ___)| |_)| | | | | || (_| | ) ) ) ) + ' |____| .__|_| |_|_| |_\__, | / / / / + =========|_|==============|___/=/_/_/_/ + :: Spring Boot :: v{spring-boot-version} + +2021-02-03 10:33:25.224 INFO 17321 --- [ main] o.s.b.d.s.s.SpringApplicationExample : Starting SpringApplicationExample using Java 1.8.0_232 on mycomputer with PID 17321 (/apps/myjar.jar started by pwebb) +2021-02-03 10:33:25.226 INFO 17900 --- [ main] o.s.b.d.s.s.SpringApplicationExample : No active profile set, falling back to default profiles: default +2021-02-03 10:33:26.046 INFO 17321 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) +2021-02-03 10:33:26.054 INFO 17900 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] +2021-02-03 10:33:26.055 INFO 17900 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.41] +2021-02-03 10:33:26.097 INFO 17900 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext +2021-02-03 10:33:26.097 INFO 17900 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 821 ms +2021-02-03 10:33:26.144 INFO 17900 --- [ main] s.tomcat.SampleTomcatApplication : ServletContext initialized +2021-02-03 10:33:26.376 INFO 17900 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' +2021-02-03 10:33:26.384 INFO 17900 --- [ main] o.s.b.d.s.s.SpringApplicationExample : Started SampleTomcatApplication in 1.514 seconds (JVM running for 1.823) +---- + + + +By default, `INFO` logging messages are shown, including some relevant startup details, such as the user that launched the application. +If you need a log level other than `INFO`, you can set it, as described in <>. +The application version is determined using the implementation version from the main application class's package. +Startup information logging can be turned off by setting `spring.main.log-startup-info` to `false`. +This will also turn off logging of the application's active profiles. + +TIP: To add additional logging during startup, you can override `logStartupInfo(boolean)` in a subclass of `SpringApplication`. + + + +[[features.spring-application.startup-failure]] +=== Startup Failure +If your application fails to start, registered `FailureAnalyzers` get a chance to provide a dedicated error message and a concrete action to fix the problem. +For instance, if you start a web application on port `8080` and that port is already in use, you should see something similar to the following message: + +[indent=0] +---- + *************************** + APPLICATION FAILED TO START + *************************** + + Description: + + Embedded servlet container failed to start. Port 8080 was already in use. + + Action: + + Identify and stop the process that's listening on port 8080 or configure this application to listen on another port. +---- + +NOTE: Spring Boot provides numerous `FailureAnalyzer` implementations, and you can <>. + +If no failure analyzers are able to handle the exception, you can still display the full conditions report to better understand what went wrong. +To do so, you need to <> or <> for `org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener`. + +For instance, if you are running your application by using `java -jar`, you can enable the `debug` property as follows: + +[source,shell,indent=0,subs="verbatim"] +---- + $ java -jar myproject-0.0.1-SNAPSHOT.jar --debug +---- + + + +[[features.spring-application.lazy-initialization]] +=== Lazy Initialization +`SpringApplication` allows an application to be initialized lazily. +When lazy initialization is enabled, beans are created as they are needed rather than during application startup. +As a result, enabling lazy initialization can reduce the time that it takes your application to start. +In a web application, enabling lazy initialization will result in many web-related beans not being initialized until an HTTP request is received. + +A downside of lazy initialization is that it can delay the discovery of a problem with the application. +If a misconfigured bean is initialized lazily, a failure will no longer occur during startup and the problem will only become apparent when the bean is initialized. +Care must also be taken to ensure that the JVM has sufficient memory to accommodate all of the application's beans and not just those that are initialized during startup. +For these reasons, lazy initialization is not enabled by default and it is recommended that fine-tuning of the JVM's heap size is done before enabling lazy initialization. + +Lazy initialization can be enabled programmatically using the `lazyInitialization` method on `SpringApplicationBuilder` or the `setLazyInitialization` method on `SpringApplication`. +Alternatively, it can be enabled using the configprop:spring.main.lazy-initialization[] property as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + main: + lazy-initialization: true +---- + +TIP: If you want to disable lazy initialization for certain beans while using lazy initialization for the rest of the application, you can explicitly set their lazy attribute to false using the `@Lazy(false)` annotation. + + + +[[features.spring-application.banner]] +=== Customizing the Banner +The banner that is printed on start up can be changed by adding a `banner.txt` file to your classpath or by setting the configprop:spring.banner.location[] property to the location of such a file. +If the file has an encoding other than UTF-8, you can set `spring.banner.charset`. +In addition to a text file, you can also add a `banner.gif`, `banner.jpg`, or `banner.png` image file to your classpath or set the configprop:spring.banner.image.location[] property. +Images are converted into an ASCII art representation and printed above any text banner. + +Inside your `banner.txt` file, you can use any key available in the `Environment` as well as any of the following placeholders: + +.Banner variables +|=== +| Variable | Description + +| `${application.version}` +| The version number of your application, as declared in `MANIFEST.MF`. + For example, `Implementation-Version: 1.0` is printed as `1.0`. + +| `${application.formatted-version}` +| The version number of your application, as declared in `MANIFEST.MF` and formatted for display (surrounded with brackets and prefixed with `v`). + For example `(v1.0)`. + +| `${spring-boot.version}` +| The Spring Boot version that you are using. + For example `{spring-boot-version}`. + +| `${spring-boot.formatted-version}` +| The Spring Boot version that you are using, formatted for display (surrounded with brackets and prefixed with `v`). + For example `(v{spring-boot-version})`. + +| `${Ansi.NAME}` (or `${AnsiColor.NAME}`, `${AnsiBackground.NAME}`, `${AnsiStyle.NAME}`) +| Where `NAME` is the name of an ANSI escape code. + See {spring-boot-module-code}/ansi/AnsiPropertySource.java[`AnsiPropertySource`] for details. + +| `${application.title}` +| The title of your application, as declared in `MANIFEST.MF`. + For example `Implementation-Title: MyApp` is printed as `MyApp`. +|=== + +TIP: The `SpringApplication.setBanner(...)` method can be used if you want to generate a banner programmatically. +Use the `org.springframework.boot.Banner` interface and implement your own `printBanner()` method. + +You can also use the configprop:spring.main.banner-mode[] property to determine if the banner has to be printed on `System.out` (`console`), sent to the configured logger (`log`), or not produced at all (`off`). + +The printed banner is registered as a singleton bean under the following name: `springBootBanner`. + +[NOTE] +==== +The `${application.version}` and `${application.formatted-version}` properties are only available if you are using Spring Boot launchers. +The values won't be resolved if you are running an unpacked jar and starting it with `java -cp `. + +This is why we recommend that you always launch unpacked jars using `java org.springframework.boot.loader.JarLauncher`. +This will initialize the `application.*` banner variables before building the classpath and launching your app. +==== + + + +[[features.spring-application.customizing-spring-application]] +=== Customizing SpringApplication +If the `SpringApplication` defaults are not to your taste, you can instead create a local instance and customize it. +For example, to turn off the banner, you could write: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/springapplication/customizingspringapplication/MyApplication.java[] +---- + +NOTE: The constructor arguments passed to `SpringApplication` are configuration sources for Spring beans. +In most cases, these are references to `@Configuration` classes, but they could also be direct references `@Component` classes. + +It is also possible to configure the `SpringApplication` by using an `application.properties` file. +See _<>_ for details. + +For a complete list of the configuration options, see the {spring-boot-module-api}/SpringApplication.html[`SpringApplication` Javadoc]. + + + +[[features.spring-application.fluent-builder-api]] +=== Fluent Builder API +If you need to build an `ApplicationContext` hierarchy (multiple contexts with a parent/child relationship) or if you prefer using a "`fluent`" builder API, you can use the `SpringApplicationBuilder`. + +The `SpringApplicationBuilder` lets you chain together multiple method calls and includes `parent` and `child` methods that let you create a hierarchy, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/springapplication/fluentbuilderapi/MyApplication.java[tag=*] +---- + +NOTE: There are some restrictions when creating an `ApplicationContext` hierarchy. +For example, Web components *must* be contained within the child context, and the same `Environment` is used for both parent and child contexts. +See the {spring-boot-module-api}/builder/SpringApplicationBuilder.html[`SpringApplicationBuilder` Javadoc] for full details. + + + +[[features.spring-application.application-availability]] +=== Application Availability +When deployed on platforms, applications can provide information about their availability to the platform using infrastructure such as https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/[Kubernetes Probes]. +Spring Boot includes out-of-the box support for the commonly used "`liveness`" and "`readiness`" availability states. +If you are using Spring Boot's "`actuator`" support then these states are exposed as health endpoint groups. + +In addition, you can also obtain availability states by injecting the `ApplicationAvailability` interface into your own beans. + + + +[[features.spring-application.application-availability.liveness]] +==== Liveness State +The "`Liveness`" state of an application tells whether its internal state allows it to work correctly, or recover by itself if it's currently failing. +A broken "`Liveness`" state means that the application is in a state that it cannot recover from, and the infrastructure should restart the application. + +NOTE: In general, the "Liveness" state should not be based on external checks, such as <>. +If it did, a failing external system (a database, a Web API, an external cache) would trigger massive restarts and cascading failures across the platform. + +The internal state of Spring Boot applications is mostly represented by the Spring `ApplicationContext`. +If the application context has started successfully, Spring Boot assumes that the application is in a valid state. +An application is considered live as soon as the context has been refreshed, see <>. + + + +[[features.spring-application.application-availability.readiness]] +==== Readiness State +The "`Readiness`" state of an application tells whether the application is ready to handle traffic. +A failing "`Readiness`" state tells the platform that it should not route traffic to the application for now. +This typically happens during startup, while `CommandLineRunner` and `ApplicationRunner` components are being processed, or at any time if the application decides that it's too busy for additional traffic. + +An application is considered ready as soon as application and command-line runners have been called, see <>. + +TIP: Tasks expected to run during startup should be executed by `CommandLineRunner` and `ApplicationRunner` components instead of using Spring component lifecycle callbacks such as `@PostConstruct`. + + + +[[features.spring-application.application-availability.managing]] +==== Managing the Application Availability State +Application components can retrieve the current availability state at any time, by injecting the `ApplicationAvailability` interface and calling methods on it. +More often, applications will want to listen to state updates or update the state of the application. + +For example, we can export the "Readiness" state of the application to a file so that a Kubernetes "exec Probe" can look at this file: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/springapplication/applicationavailability/managing/MyReadinessStateExporter.java[] +---- + +We can also update the state of the application, when the application breaks and cannot recover: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/springapplication/applicationavailability/managing/MyLocalCacheVerifier.java[] +---- + +Spring Boot provides <>. +You can get more guidance about <>. + + + +[[features.spring-application.application-events-and-listeners]] +=== Application Events and Listeners +In addition to the usual Spring Framework events, such as {spring-framework-api}/context/event/ContextRefreshedEvent.html[`ContextRefreshedEvent`], a `SpringApplication` sends some additional application events. + +[NOTE] +==== +Some events are actually triggered before the `ApplicationContext` is created, so you cannot register a listener on those as a `@Bean`. +You can register them with the `SpringApplication.addListeners(...)` method or the `SpringApplicationBuilder.listeners(...)` method. + +If you want those listeners to be registered automatically, regardless of the way the application is created, you can add a `META-INF/spring.factories` file to your project and reference your listener(s) by using the `org.springframework.context.ApplicationListener` key, as shown in the following example: + +[indent=0] +---- + org.springframework.context.ApplicationListener=com.example.project.MyListener +---- + +==== + +Application events are sent in the following order, as your application runs: + +. An `ApplicationStartingEvent` is sent at the start of a run but before any processing, except for the registration of listeners and initializers. +. An `ApplicationEnvironmentPreparedEvent` is sent when the `Environment` to be used in the context is known but before the context is created. +. An `ApplicationContextInitializedEvent` is sent when the `ApplicationContext` is prepared and ApplicationContextInitializers have been called but before any bean definitions are loaded. +. An `ApplicationPreparedEvent` is sent just before the refresh is started but after bean definitions have been loaded. +. An `ApplicationStartedEvent` is sent after the context has been refreshed but before any application and command-line runners have been called. +. An `AvailabilityChangeEvent` is sent right after with `LivenessState.CORRECT` to indicate that the application is considered as live. +. An `ApplicationReadyEvent` is sent after any <> have been called. +. An `AvailabilityChangeEvent` is sent right after with `ReadinessState.ACCEPTING_TRAFFIC` to indicate that the application is ready to service requests. +. An `ApplicationFailedEvent` is sent if there is an exception on startup. + +The above list only includes ``SpringApplicationEvent``s that are tied to a `SpringApplication`. +In addition to these, the following events are also published after `ApplicationPreparedEvent` and before `ApplicationStartedEvent`: + +- A `WebServerInitializedEvent` is sent after the `WebServer` is ready. + `ServletWebServerInitializedEvent` and `ReactiveWebServerInitializedEvent` are the servlet and reactive variants respectively. +- A `ContextRefreshedEvent` is sent when an `ApplicationContext` is refreshed. + +TIP: You often need not use application events, but it can be handy to know that they exist. +Internally, Spring Boot uses events to handle a variety of tasks. + +NOTE: Event listeners should not run potentially lengthy tasks as they execute in the same thread by default. +Consider using <> instead. + +Application events are sent by using Spring Framework's event publishing mechanism. +Part of this mechanism ensures that an event published to the listeners in a child context is also published to the listeners in any ancestor contexts. +As a result of this, if your application uses a hierarchy of `SpringApplication` instances, a listener may receive multiple instances of the same type of application event. + +To allow your listener to distinguish between an event for its context and an event for a descendant context, it should request that its application context is injected and then compare the injected context with the context of the event. +The context can be injected by implementing `ApplicationContextAware` or, if the listener is a bean, by using `@Autowired`. + + + +[[features.spring-application.web-environment]] +=== Web Environment +A `SpringApplication` attempts to create the right type of `ApplicationContext` on your behalf. +The algorithm used to determine a `WebApplicationType` is the following: + +* If Spring MVC is present, an `AnnotationConfigServletWebServerApplicationContext` is used +* If Spring MVC is not present and Spring WebFlux is present, an `AnnotationConfigReactiveWebServerApplicationContext` is used +* Otherwise, `AnnotationConfigApplicationContext` is used + +This means that if you are using Spring MVC and the new `WebClient` from Spring WebFlux in the same application, Spring MVC will be used by default. +You can override that easily by calling `setWebApplicationType(WebApplicationType)`. + +It is also possible to take complete control of the `ApplicationContext` type that is used by calling `setApplicationContextClass(...)`. + +TIP: It is often desirable to call `setWebApplicationType(WebApplicationType.NONE)` when using `SpringApplication` within a JUnit test. + + + +[[features.spring-application.application-arguments]] +=== Accessing Application Arguments +If you need to access the application arguments that were passed to `SpringApplication.run(...)`, you can inject a `org.springframework.boot.ApplicationArguments` bean. +The `ApplicationArguments` interface provides access to both the raw `String[]` arguments as well as parsed `option` and `non-option` arguments, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/springapplication/applicationarguments/MyBean.java[] +---- + +TIP: Spring Boot also registers a `CommandLinePropertySource` with the Spring `Environment`. +This lets you also inject single application arguments by using the `@Value` annotation. + + + +[[features.spring-application.command-line-runner]] +=== Using the ApplicationRunner or CommandLineRunner +If you need to run some specific code once the `SpringApplication` has started, you can implement the `ApplicationRunner` or `CommandLineRunner` interfaces. +Both interfaces work in the same way and offer a single `run` method, which is called just before `SpringApplication.run(...)` completes. + +NOTE: This contract is well suited for tasks that should run after application startup but before it starts accepting traffic. + + +The `CommandLineRunner` interfaces provides access to application arguments as a string array, whereas the `ApplicationRunner` uses the `ApplicationArguments` interface discussed earlier. +The following example shows a `CommandLineRunner` with a `run` method: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/springapplication/commandlinerunner/MyCommandLineRunner.java[] +---- + +If several `CommandLineRunner` or `ApplicationRunner` beans are defined that must be called in a specific order, you can additionally implement the `org.springframework.core.Ordered` interface or use the `org.springframework.core.annotation.Order` annotation. + + + +[[features.spring-application.application-exit]] +=== Application Exit +Each `SpringApplication` registers a shutdown hook with the JVM to ensure that the `ApplicationContext` closes gracefully on exit. +All the standard Spring lifecycle callbacks (such as the `DisposableBean` interface or the `@PreDestroy` annotation) can be used. + +In addition, beans may implement the `org.springframework.boot.ExitCodeGenerator` interface if they wish to return a specific exit code when `SpringApplication.exit()` is called. +This exit code can then be passed to `System.exit()` to return it as a status code, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/springapplication/applicationexit/MyApplication.java[] +---- + +Also, the `ExitCodeGenerator` interface may be implemented by exceptions. +When such an exception is encountered, Spring Boot returns the exit code provided by the implemented `getExitCode()` method. + + + +[[features.spring-application.admin]] +=== Admin Features +It is possible to enable admin-related features for the application by specifying the configprop:spring.application.admin.enabled[] property. +This exposes the {spring-boot-module-code}/admin/SpringApplicationAdminMXBean.java[`SpringApplicationAdminMXBean`] on the platform `MBeanServer`. +You could use this feature to administer your Spring Boot application remotely. +This feature could also be useful for any service wrapper implementation. + +TIP: If you want to know on which HTTP port the application is running, get the property with a key of `local.server.port`. + + + +[[features.spring-application.startup-tracking]] +=== Application Startup tracking +During the application startup, the `SpringApplication` and the `ApplicationContext` perform many tasks related to the application lifecycle, +the beans lifecycle or even processing application events. +With {spring-framework-api}/core/metrics/ApplicationStartup.html[`ApplicationStartup`], Spring Framework {spring-framework-docs}/core.html#context-functionality-startup[allows you to track the application startup sequence with `StartupStep` objects]. +This data can be collected for profiling purposes, or just to have a better understanding of an application startup process. + +You can choose an `ApplicationStartup` implementation when setting up the `SpringApplication` instance. +For example, to use the `BufferingApplicationStartup`, you could write: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/springapplication/startuptracking/MyApplication.java[] +---- + +The first available implementation, `FlightRecorderApplicationStartup` is provided by Spring Framework. +It adds Spring-specific startup events to a Java Flight Recorder session and is meant for profiling applications and correlating their Spring context lifecycle with JVM events (such as allocations, GCs, class loading...). +Once configured, you can record data by running the application with the Flight Recorder enabled: + +[source,shell,indent=0,subs="verbatim"] +---- + $ java -XX:StartFlightRecording:filename=recording.jfr,duration=10s -jar demo.jar +---- + +Spring Boot ships with the `BufferingApplicationStartup` variant; this implementation is meant for buffering the startup steps and draining them into an external metrics system. +Applications can ask for the bean of type `BufferingApplicationStartup` in any component. + +Spring Boot can also be configured to expose a {spring-boot-actuator-restapi-docs}/#startup[`startup` endpoint] that provides this information as a JSON document. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/spring-integration.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/spring-integration.adoc new file mode 100644 index 000000000000..eca5f9fc46e6 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/spring-integration.adoc @@ -0,0 +1,47 @@ +[[features.spring-integration]] +== Spring Integration +Spring Boot offers several conveniences for working with {spring-integration}[Spring Integration], including the `spring-boot-starter-integration` "`Starter`". +Spring Integration provides abstractions over messaging and also other transports such as HTTP, TCP, and others. +If Spring Integration is available on your classpath, it is initialized through the `@EnableIntegration` annotation. + +Spring Integration polling logic relies <>. + +Spring Boot also configures some features that are triggered by the presence of additional Spring Integration modules. +If `spring-integration-jmx` is also on the classpath, message processing statistics are published over JMX. +If `spring-integration-jdbc` is available, the default database schema can be created on startup, as shown in the following line: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + integration: + jdbc: + initialize-schema: "always" +---- + +If `spring-integration-rsocket` is available, developers can configure an RSocket server using `"spring.rsocket.server.*"` properties and let it use `IntegrationRSocketEndpoint` or `RSocketOutboundGateway` components to handle incoming RSocket messages. +This infrastructure can handle Spring Integration RSocket channel adapters and `@MessageMapping` handlers (given `"spring.integration.rsocket.server.message-mapping-enabled"` is configured). + +Spring Boot can also auto-configure an `ClientRSocketConnector` using configuration properties: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + # Connecting to a RSocket server over TCP + spring: + integration: + rsocket: + client: + host: "example.org" + port: 9898 +---- + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + # Connecting to a RSocket Server over WebSocket + spring: + integration: + rsocket: + client: + uri: "ws://example.org" +---- + +See the {spring-boot-autoconfigure-module-code}/integration/IntegrationAutoConfiguration.java[`IntegrationAutoConfiguration`] and {spring-boot-autoconfigure-module-code}/integration/IntegrationProperties.java[`IntegrationProperties`] classes for more details. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/spring-session.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/spring-session.adoc new file mode 100644 index 000000000000..f733119d77ca --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/spring-session.adoc @@ -0,0 +1,52 @@ +[[features.spring-session]] +== Spring Session +Spring Boot provides {spring-session}[Spring Session] auto-configuration for a wide range of data stores. +When building a Servlet web application, the following stores can be auto-configured: + +* JDBC +* Redis +* Hazelcast +* MongoDB + +Additionally, {spring-boot-for-apache-geode}[Spring Boot for Apache Geode] provides {spring-boot-for-apache-geode-docs}#geode-session[auto-configuration for using Apache Geode as a session store]. + +The Servlet auto-configuration replaces the need to use `@Enable*HttpSession`. + +When building a reactive web application, the following stores can be auto-configured: + +* Redis +* MongoDB + +The reactive auto-configuration replaces the need to use `@Enable*WebSession`. + +If a single Spring Session module is present on the classpath, Spring Boot uses that store implementation automatically. +If you have more than one implementation, you must choose the {spring-boot-autoconfigure-module-code}/session/StoreType.java[`StoreType`] that you wish to use to store the sessions. +For instance, to use JDBC as the back-end store, you can configure your application as follows: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + session: + store-type: "jdbc" +---- + +TIP: You can disable Spring Session by setting the `store-type` to `none`. + +Each store has specific additional settings. +For instance, it is possible to customize the name of the table for the JDBC store, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + session: + jdbc: + table-name: "SESSIONS" +---- + +For setting the timeout of the session you can use the configprop:spring.session.timeout[] property. +If that property is not set with a Servlet web application, the auto-configuration falls back to the value of configprop:server.servlet.session.timeout[]. + + +You can take control over Spring Session's configuration using `@Enable*HttpSession` (Servlet) or `@Enable*WebSession` (Reactive). +This will cause the auto-configuration to back off. +Spring Session can then be configured using the annotation's attributes rather than the previously described configuration properties. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/sql.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/sql.adoc new file mode 100644 index 000000000000..c5035058c32f --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/sql.adoc @@ -0,0 +1,530 @@ +[[features.sql]] +== Working with SQL Databases +The {spring-framework}[Spring Framework] provides extensive support for working with SQL databases, from direct JDBC access using `JdbcTemplate` to complete "`object relational mapping`" technologies such as Hibernate. +{spring-data}[Spring Data] provides an additional level of functionality: creating `Repository` implementations directly from interfaces and using conventions to generate queries from your method names. + + + +[[features.sql.datasource]] +=== Configure a DataSource +Java's `javax.sql.DataSource` interface provides a standard method of working with database connections. +Traditionally, a 'DataSource' uses a `URL` along with some credentials to establish a database connection. + +TIP: See <> for more advanced examples, typically to take full control over the configuration of the DataSource. + + + +[[features.sql.datasource.embedded]] +==== Embedded Database Support +It is often convenient to develop applications by using an in-memory embedded database. +Obviously, in-memory databases do not provide persistent storage. +You need to populate your database when your application starts and be prepared to throw away data when your application ends. + +TIP: The "`How-to`" section includes a <>. + +Spring Boot can auto-configure embedded https://www.h2database.com[H2], http://hsqldb.org/[HSQL], and https://db.apache.org/derby/[Derby] databases. +You need not provide any connection URLs. +You need only include a build dependency to the embedded database that you want to use. +If there are multiple embedded databases on the classpath, set the configprop:spring.datasource.embedded-database-connection[] configuration property to control which one is used. +Setting the property to `none` disables auto-configuration of an embedded database. + +[NOTE] +==== +If you are using this feature in your tests, you may notice that the same database is reused by your whole test suite regardless of the number of application contexts that you use. +If you want to make sure that each context has a separate embedded database, you should set `spring.datasource.generate-unique-name` to `true`. +==== + +For example, the typical POM dependencies would be as follows: + +[source,xml,indent=0,subs="verbatim"] +---- + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.hsqldb + hsqldb + runtime + +---- + +NOTE: You need a dependency on `spring-jdbc` for an embedded database to be auto-configured. +In this example, it is pulled in transitively through `spring-boot-starter-data-jpa`. + +TIP: If, for whatever reason, you do configure the connection URL for an embedded database, take care to ensure that the database's automatic shutdown is disabled. +If you use H2, you should use `DB_CLOSE_ON_EXIT=FALSE` to do so. +If you use HSQLDB, you should ensure that `shutdown=true` is not used. +Disabling the database's automatic shutdown lets Spring Boot control when the database is closed, thereby ensuring that it happens once access to the database is no longer needed. + + + +[[features.sql.datasource.production]] +==== Connection to a Production Database +Production database connections can also be auto-configured by using a pooling `DataSource`. + + + +[[features.sql.datasource.configuration]] +==== DataSource Configuration +DataSource configuration is controlled by external configuration properties in `+spring.datasource.*+`. +For example, you might declare the following section in `application.properties`: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + datasource: + url: "jdbc:mysql://localhost/test" + username: "dbuser" + password: "dbpass" +---- + +NOTE: You should at least specify the URL by setting the configprop:spring.datasource.url[] property. +Otherwise, Spring Boot tries to auto-configure an embedded database. + +TIP: Spring Boot can deduce the JDBC driver class for most databases from the URL. +If you need to specify a specific class, you can use the configprop:spring.datasource.driver-class-name[] property. + +NOTE: For a pooling `DataSource` to be created, we need to be able to verify that a valid `Driver` class is available, so we check for that before doing anything. +In other words, if you set `spring.datasource.driver-class-name=com.mysql.jdbc.Driver`, then that class has to be loadable. + +See {spring-boot-autoconfigure-module-code}/jdbc/DataSourceProperties.java[`DataSourceProperties`] for more of the supported options. +These are the standard options that work regardless of <>. +It is also possible to fine-tune implementation-specific settings by using their respective prefix (`+spring.datasource.hikari.*+`, `+spring.datasource.tomcat.*+`, `+spring.datasource.dbcp2.*+`, and `+spring.datasource.oracleucp.*+`). +Refer to the documentation of the connection pool implementation you are using for more details. + +For instance, if you use the {tomcat-docs}/jdbc-pool.html#Common_Attributes[Tomcat connection pool], you could customize many additional settings, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + datasource: + tomcat: + max-wait: 10000 + max-active: 50 + test-on-borrow: true +---- + +This will set the pool to wait 10000ms before throwing an exception if no connection is available, limit the maximum number of connections to 50 and validate the connection before borrowing it from the pool. + + + +[[features.sql.datasource.connection-pool]] +==== Supported Connection Pools +Spring Boot uses the following algorithm for choosing a specific implementation: + +. We prefer https://github.com/brettwooldridge/HikariCP[HikariCP] for its performance and concurrency. +If HikariCP is available, we always choose it. +. Otherwise, if the Tomcat pooling `DataSource` is available, we use it. +. Otherwise, if https://commons.apache.org/proper/commons-dbcp/[Commons DBCP2] is available, we use it. +. If none of HikariCP, Tomcat, and DBCP2 are available and if Oracle UCP is available, we use it. + +NOTE: If you use the `spring-boot-starter-jdbc` or `spring-boot-starter-data-jpa` "`starters`", you automatically get a dependency to `HikariCP`. + +You can bypass that algorithm completely and specify the connection pool to use by setting the configprop:spring.datasource.type[] property. +This is especially important if you run your application in a Tomcat container, as `tomcat-jdbc` is provided by default. + +Additional connection pools can always be configured manually, using `DataSourceBuilder`. +If you define your own `DataSource` bean, auto-configuration does not occur. +The following connection pools are supported by `DataSourceBuilder`: + +* HikariCP +* Tomcat pooling `Datasource` +* Commons DBCP2 +* Oracle UCP & `OracleDataSource` +* Spring Framework's `SimpleDriverDataSource` +* H2 `JdbcDataSource` +* PostgreSQL `PGSimpleDataSource` + + + +[[features.sql.datasource.jndi]] +==== Connection to a JNDI DataSource +If you deploy your Spring Boot application to an Application Server, you might want to configure and manage your DataSource by using your Application Server's built-in features and access it by using JNDI. + +The configprop:spring.datasource.jndi-name[] property can be used as an alternative to the configprop:spring.datasource.url[], configprop:spring.datasource.username[], and configprop:spring.datasource.password[] properties to access the `DataSource` from a specific JNDI location. +For example, the following section in `application.properties` shows how you can access a JBoss AS defined `DataSource`: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + datasource: + jndi-name: "java:jboss/datasources/customers" +---- + + + +[[features.sql.jdbc-template]] +=== Using JdbcTemplate +Spring's `JdbcTemplate` and `NamedParameterJdbcTemplate` classes are auto-configured, and you can `@Autowire` them directly into your own beans, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/sql/jdbctemplate/MyBean.java[] +---- + +You can customize some properties of the template by using the `spring.jdbc.template.*` properties, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + jdbc: + template: + max-rows: 500 +---- + +NOTE: The `NamedParameterJdbcTemplate` reuses the same `JdbcTemplate` instance behind the scenes. +If more than one `JdbcTemplate` is defined and no primary candidate exists, the `NamedParameterJdbcTemplate` is not auto-configured. + + + +[[features.sql.jpa-and-spring-data]] +=== JPA and Spring Data JPA +The Java Persistence API is a standard technology that lets you "`map`" objects to relational databases. +The `spring-boot-starter-data-jpa` POM provides a quick way to get started. +It provides the following key dependencies: + +* Hibernate: One of the most popular JPA implementations. +* Spring Data JPA: Helps you to implement JPA-based repositories. +* Spring ORM: Core ORM support from the Spring Framework. + +TIP: We do not go into too many details of JPA or {spring-data}[Spring Data] here. +You can follow the https://spring.io/guides/gs/accessing-data-jpa/["`Accessing Data with JPA`"] guide from https://spring.io and read the {spring-data-jpa}[Spring Data JPA] and https://hibernate.org/orm/documentation/[Hibernate] reference documentation. + + + +[[features.sql.jpa-and-spring-data.entity-classes]] +==== Entity Classes +Traditionally, JPA "`Entity`" classes are specified in a `persistence.xml` file. +With Spring Boot, this file is not necessary and "`Entity Scanning`" is used instead. +By default, all packages below your main configuration class (the one annotated with `@EnableAutoConfiguration` or `@SpringBootApplication`) are searched. + +Any classes annotated with `@Entity`, `@Embeddable`, or `@MappedSuperclass` are considered. +A typical entity class resembles the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/sql/jpaandspringdata/entityclasses/City.java[] +---- + +TIP: You can customize entity scanning locations by using the `@EntityScan` annotation. +See the "`<>`" how-to. + + + +[[features.sql.jpa-and-spring-data.repositories]] +==== Spring Data JPA Repositories +{spring-data-jpa}[Spring Data JPA] repositories are interfaces that you can define to access data. +JPA queries are created automatically from your method names. +For example, a `CityRepository` interface might declare a `findAllByState(String state)` method to find all the cities in a given state. + +For more complex queries, you can annotate your method with Spring Data's {spring-data-jpa-api}/repository/Query.html[`Query`] annotation. + +Spring Data repositories usually extend from the {spring-data-commons-api}/repository/Repository.html[`Repository`] or {spring-data-commons-api}/repository/CrudRepository.html[`CrudRepository`] interfaces. +If you use auto-configuration, repositories are searched from the package containing your main configuration class (the one annotated with `@EnableAutoConfiguration` or `@SpringBootApplication`) down. + +The following example shows a typical Spring Data repository interface definition: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/sql/jpaandspringdata/repositories/CityRepository.java[] +---- + +Spring Data JPA repositories support three different modes of bootstrapping: default, deferred, and lazy. +To enable deferred or lazy bootstrapping, set the configprop:spring.data.jpa.repositories.bootstrap-mode[] property to `deferred` or `lazy` respectively. +When using deferred or lazy bootstrapping, the auto-configured `EntityManagerFactoryBuilder` will use the context's `AsyncTaskExecutor`, if any, as the bootstrap executor. +If more than one exists, the one named `applicationTaskExecutor` will be used. + +[NOTE] +==== +When using deferred or lazy bootstrapping, make sure to defer any access to the JPA infrastructure after the application context bootstrap phase. +You can use `SmartInitializingSingleton` to invoke any initialization that requires the JPA infrastructure. +For JPA components (such as converters) that are created as Spring beans, use `ObjectProvider` to delay the resolution of dependencies, if any. +==== + +TIP: We have barely scratched the surface of Spring Data JPA. +For complete details, see the {spring-data-jpa-docs}[Spring Data JPA reference documentation]. + + + +[[features.sql.jpa-and-spring-data.creating-and-dropping]] +==== Creating and Dropping JPA Databases +By default, JPA databases are automatically created *only* if you use an embedded database (H2, HSQL, or Derby). +You can explicitly configure JPA settings by using `+spring.jpa.*+` properties. +For example, to create and drop tables you can add the following line to your `application.properties`: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + jpa: + hibernate.ddl-auto: "create-drop" +---- + +NOTE: Hibernate's own internal property name for this (if you happen to remember it better) is `hibernate.hbm2ddl.auto`. +You can set it, along with other Hibernate native properties, by using `+spring.jpa.properties.*+` (the prefix is stripped before adding them to the entity manager). +The following line shows an example of setting JPA properties for Hibernate: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + jpa: + properties: + hibernate: + "globally_quoted_identifiers": "true" +---- + +The line in the preceding example passes a value of `true` for the `hibernate.globally_quoted_identifiers` property to the Hibernate entity manager. + +By default, the DDL execution (or validation) is deferred until the `ApplicationContext` has started. +There is also a `spring.jpa.generate-ddl` flag, but it is not used if Hibernate auto-configuration is active, because the `ddl-auto` settings are more fine-grained. + + + +[[features.sql.jpa-and-spring-data.open-entity-manager-in-view]] +==== Open EntityManager in View +If you are running a web application, Spring Boot by default registers {spring-framework-api}/orm/jpa/support/OpenEntityManagerInViewInterceptor.html[`OpenEntityManagerInViewInterceptor`] to apply the "`Open EntityManager in View`" pattern, to allow for lazy loading in web views. +If you do not want this behavior, you should set `spring.jpa.open-in-view` to `false` in your `application.properties`. + + + +[[features.sql.jdbc]] +=== Spring Data JDBC +Spring Data includes repository support for JDBC and will automatically generate SQL for the methods on `CrudRepository`. +For more advanced queries, a `@Query` annotation is provided. + +Spring Boot will auto-configure Spring Data's JDBC repositories when the necessary dependencies are on the classpath. +They can be added to your project with a single dependency on `spring-boot-starter-data-jdbc`. +If necessary, you can take control of Spring Data JDBC's configuration by adding the `@EnableJdbcRepositories` annotation or a `JdbcConfiguration` subclass to your application. + +TIP: For complete details of Spring Data JDBC, please refer to the {spring-data-jdbc-docs}[reference documentation]. + + + +[[features.sql.h2-web-console]] +=== Using H2's Web Console +The https://www.h2database.com[H2 database] provides a https://www.h2database.com/html/quickstart.html#h2_console[browser-based console] that Spring Boot can auto-configure for you. +The console is auto-configured when the following conditions are met: + +* You are developing a servlet-based web application. +* `com.h2database:h2` is on the classpath. +* You are using <>. + +TIP: If you are not using Spring Boot's developer tools but would still like to make use of H2's console, you can configure the configprop:spring.h2.console.enabled[] property with a value of `true`. + +NOTE: The H2 console is only intended for use during development, so you should take care to ensure that `spring.h2.console.enabled` is not set to `true` in production. + + + +[[features.sql.h2-web-console.custom-path]] +==== Changing the H2 Console's Path +By default, the console is available at `/h2-console`. +You can customize the console's path by using the configprop:spring.h2.console.path[] property. + + + +[[features.sql.h2-web-console.spring-security]] +==== Accessing the H2 Console in a Secured Application +H2 Console uses frames and, as it is intended for development only, does not implement CSRF protection measures. +If your application uses Spring Security, you need to configure it to + +* disable CSRF protection for requests against the console, +* set the header `X-Frame-Options` to `SAMEORIGIN` on responses from the console. + +More information on {spring-security-docs}#csrf[CSRF] and the header {spring-security-docs}#headers-frame-options[X-Frame-Options] can be found in the Spring Security Reference Guide. + +In simple setups, a `SecurityFilterChain` like the following can be used: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/sql/h2webconsole/springsecurity/DevProfileSecurityConfiguration.java[] +---- + +WARNING: The H2 console is only intended for use during development. +In production, disabling CSRF protection or allowing frames for a website may create severe security risks. + +TIP: `PathRequest.toH2Console()` returns the correct request matcher also when the console's path has been customized. + + + +[[features.sql.jooq]] +=== Using jOOQ +jOOQ Object Oriented Querying (https://www.jooq.org/[jOOQ]) is a popular product from https://www.datageekery.com/[Data Geekery] which generates Java code from your database and lets you build type-safe SQL queries through its fluent API. +Both the commercial and open source editions can be used with Spring Boot. + + + +[[features.sql.jooq.codegen]] +==== Code Generation +In order to use jOOQ type-safe queries, you need to generate Java classes from your database schema. +You can follow the instructions in the {jooq-docs}/#jooq-in-7-steps-step3[jOOQ user manual]. +If you use the `jooq-codegen-maven` plugin and you also use the `spring-boot-starter-parent` "`parent POM`", you can safely omit the plugin's `` tag. +You can also use Spring Boot-defined version variables (such as `h2.version`) to declare the plugin's database dependency. +The following listing shows an example: + +[source,xml,indent=0,subs="verbatim"] +---- + + org.jooq + jooq-codegen-maven + + ... + + + + com.h2database + h2 + ${h2.version} + + + + + org.h2.Driver + jdbc:h2:~/yourdatabase + + + ... + + + +---- + + + +[[features.sql.jooq.dslcontext]] +==== Using DSLContext +The fluent API offered by jOOQ is initiated through the `org.jooq.DSLContext` interface. +Spring Boot auto-configures a `DSLContext` as a Spring Bean and connects it to your application `DataSource`. +To use the `DSLContext`, you can inject it, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/sql/jooq/dslcontext/MyBean.java[tag=!method] +---- + +TIP: The jOOQ manual tends to use a variable named `create` to hold the `DSLContext`. + +You can then use the `DSLContext` to construct your queries, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/sql/jooq/dslcontext/MyBean.java[tag=method] +---- + + + +[[features.sql.jooq.sqldialect]] +==== jOOQ SQL Dialect +Unless the configprop:spring.jooq.sql-dialect[] property has been configured, Spring Boot determines the SQL dialect to use for your datasource. +If Spring Boot could not detect the dialect, it uses `DEFAULT`. + +NOTE: Spring Boot can only auto-configure dialects supported by the open source version of jOOQ. + + + +[[features.sql.jooq.customizing]] +==== Customizing jOOQ +More advanced customizations can be achieved by defining your own `DefaultConfigurationCustomizer` bean that will be invoked prior to creating the `org.jooq.Configuration` `@Bean`. +This takes precedence to anything that is applied by the auto-configuration. + +You can also create your own `org.jooq.Configuration` `@Bean` if you want to take complete control of the jOOQ configuration. + + + +[[features.sql.r2dbc]] +=== Using R2DBC +The Reactive Relational Database Connectivity (https://r2dbc.io[R2DBC]) project brings reactive programming APIs to relational databases. +R2DBC's `io.r2dbc.spi.Connection` provides a standard method of working with non-blocking database connections. +Connections are provided via a `ConnectionFactory`, similar to a `DataSource` with jdbc. + +`ConnectionFactory` configuration is controlled by external configuration properties in `+spring.r2dbc.*+`. +For example, you might declare the following section in `application.properties`: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + r2dbc: + url: "r2dbc:postgresql://localhost/test" + username: "dbuser" + password: "dbpass" +---- + +TIP: You do not need to specify a driver class name, since Spring Boot obtains the driver from R2DBC's Connection Factory discovery. + +NOTE: At least the url should be provided. +Information specified in the URL takes precedence over individual properties, i.e. `name`, `username`, `password` and pooling options. + +TIP: The "`How-to`" section includes a <>. + +To customize the connections created by a `ConnectionFactory`, i.e., set specific parameters that you do not want (or cannot) configure in your central database configuration, you can use a `ConnectionFactoryOptionsBuilderCustomizer` `@Bean`. +The following example shows how to manually override the database port while the rest of the options is taken from the application configuration: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/sql/r2dbc/MyR2dbcConfiguration.java[] +---- + +The following examples show how to set some PostgreSQL connection options: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/sql/r2dbc/MyPostgresR2dbcConfiguration.java[] +---- + +When a `ConnectionFactory` bean is available, the regular JDBC `DataSource` auto-configuration backs off. +If you want to retain the JDBC `DataSource` auto-configuration, and are comfortable with the risk of using the blocking JDBC API in a reactive application, add `@Import(DataSourceAutoConfiguration.class)` on a `@Configuration` class in your application to re-enable it. + + + +[[features.sql.r2dbc.embedded]] +==== Embedded Database Support +Similarly to <>, Spring Boot can automatically configure an embedded database for reactive usage. +You need not provide any connection URLs. +You need only include a build dependency to the embedded database that you want to use, as shown in the following example: + +[source,xml,indent=0,subs="verbatim"] +---- + + io.r2dbc + r2dbc-h2 + runtime + +---- + +[NOTE] +==== +If you are using this feature in your tests, you may notice that the same database is reused by your whole test suite regardless of the number of application contexts that you use. +If you want to make sure that each context has a separate embedded database, you should set `spring.r2dbc.generate-unique-name` to `true`. +==== + + + +[[features.sql.r2dbc.using-database-client]] +==== Using DatabaseClient +A `DatabaseClient` bean is auto-configured, and you can `@Autowire` it directly into your own beans, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/sql/r2dbc/usingdatabaseclient/MyBean.java[] +---- + + + +[[features.sql.r2dbc.repositories]] +==== Spring Data R2DBC Repositories +https://spring.io/projects/spring-data-r2dbc[Spring Data R2DBC] repositories are interfaces that you can define to access data. +Queries are created automatically from your method names. +For example, a `CityRepository` interface might declare a `findAllByState(String state)` method to find all the cities in a given state. + +For more complex queries, you can annotate your method with Spring Data's {spring-data-r2dbc-api}/repository/Query.html[`Query`] annotation. + +Spring Data repositories usually extend from the {spring-data-commons-api}/repository/Repository.html[`Repository`] or {spring-data-commons-api}/repository/CrudRepository.html[`CrudRepository`] interfaces. +If you use auto-configuration, repositories are searched from the package containing your main configuration class (the one annotated with `@EnableAutoConfiguration` or `@SpringBootApplication`) down. + +The following example shows a typical Spring Data repository interface definition: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/sql/r2dbc/repositories/CityRepository.java[] +---- + +TIP: We have barely scratched the surface of Spring Data R2DBC. For complete details, see the {spring-data-r2dbc-docs}[Spring Data R2DBC reference documentation]. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc new file mode 100644 index 000000000000..a43c6fc5c1d5 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc @@ -0,0 +1,43 @@ +[[features.task-execution-and-scheduling]] +== Task Execution and Scheduling +In the absence of an `Executor` bean in the context, Spring Boot auto-configures a `ThreadPoolTaskExecutor` with sensible defaults that can be automatically associated to asynchronous task execution (`@EnableAsync`) and Spring MVC asynchronous request processing. + +[TIP] +==== +If you have defined a custom `Executor` in the context, regular task execution (i.e. `@EnableAsync`) will use it transparently but the Spring MVC support will not be configured as it requires an `AsyncTaskExecutor` implementation (named `applicationTaskExecutor`). +Depending on your target arrangement, you could change your `Executor` into a `ThreadPoolTaskExecutor` or define both a `ThreadPoolTaskExecutor` and an `AsyncConfigurer` wrapping your custom `Executor`. + +The auto-configured `TaskExecutorBuilder` allows you to easily create instances that reproduce what the auto-configuration does by default. +==== + +The thread pool uses 8 core threads that can grow and shrink according to the load. +Those default settings can be fine-tuned using the `spring.task.execution` namespace, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + task: + execution: + pool: + max-size: 16 + queue-capacity: 100 + keep-alive: "10s" +---- + +This changes the thread pool to use a bounded queue so that when the queue is full (100 tasks), the thread pool increases to maximum 16 threads. +Shrinking of the pool is more aggressive as threads are reclaimed when they are idle for 10 seconds (rather than 60 seconds by default). + +A `ThreadPoolTaskScheduler` can also be auto-configured if need to be associated to scheduled task execution (e.g. `@EnableScheduling`). +The thread pool uses one thread by default and its settings can be fine-tuned using the `spring.task.scheduling` namespace, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + task: + scheduling: + thread-name-prefix: "scheduling-" + pool: + size: 2 +---- + +Both a `TaskExecutorBuilder` bean and a `TaskSchedulerBuilder` bean are made available in the context if a custom executor or scheduler needs to be created. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc new file mode 100644 index 000000000000..dfb80686f0f4 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc @@ -0,0 +1,970 @@ +[[features.testing]] +== Testing +Spring Boot provides a number of utilities and annotations to help when testing your application. +Test support is provided by two modules: `spring-boot-test` contains core items, and `spring-boot-test-autoconfigure` supports auto-configuration for tests. + +Most developers use the `spring-boot-starter-test` "`Starter`", which imports both Spring Boot test modules as well as JUnit Jupiter, AssertJ, Hamcrest, and a number of other useful libraries. + +[TIP] +==== +If you have tests that use JUnit 4, JUnit 5's vintage engine can be used to run them. +To use the vintage engine, add a dependency on `junit-vintage-engine`, as shown in the following example: + +[source,xml,indent=0,subs="verbatim"] +---- + + org.junit.vintage + junit-vintage-engine + test + + + org.hamcrest + hamcrest-core + + + +---- +==== + +`hamcrest-core` is excluded in favor of `org.hamcrest:hamcrest` that is part of `spring-boot-starter-test`. + + + +[[features.testing.test-scope-dependencies]] +=== Test Scope Dependencies +The `spring-boot-starter-test` "`Starter`" (in the `test` `scope`) contains the following provided libraries: + +* https://junit.org/junit5/[JUnit 5]: The de-facto standard for unit testing Java applications. +* {spring-framework-docs}/testing.html#integration-testing[Spring Test] & Spring Boot Test: Utilities and integration test support for Spring Boot applications. +* https://assertj.github.io/doc/[AssertJ]: A fluent assertion library. +* https://github.com/hamcrest/JavaHamcrest[Hamcrest]: A library of matcher objects (also known as constraints or predicates). +* https://site.mockito.org/[Mockito]: A Java mocking framework. +* https://github.com/skyscreamer/JSONassert[JSONassert]: An assertion library for JSON. +* https://github.com/jayway/JsonPath[JsonPath]: XPath for JSON. + +We generally find these common libraries to be useful when writing tests. +If these libraries do not suit your needs, you can add additional test dependencies of your own. + + + +[[features.testing.spring-applications]] +=== Testing Spring Applications +One of the major advantages of dependency injection is that it should make your code easier to unit test. +You can instantiate objects by using the `new` operator without even involving Spring. +You can also use _mock objects_ instead of real dependencies. + +Often, you need to move beyond unit testing and start integration testing (with a Spring `ApplicationContext`). +It is useful to be able to perform integration testing without requiring deployment of your application or needing to connect to other infrastructure. + +The Spring Framework includes a dedicated test module for such integration testing. +You can declare a dependency directly to `org.springframework:spring-test` or use the `spring-boot-starter-test` "`Starter`" to pull it in transitively. + +If you have not used the `spring-test` module before, you should start by reading the {spring-framework-docs}/testing.html#testing[relevant section] of the Spring Framework reference documentation. + + + +[[features.testing.spring-boot-applications]] +=== Testing Spring Boot Applications +A Spring Boot application is a Spring `ApplicationContext`, so nothing very special has to be done to test it beyond what you would normally do with a vanilla Spring context. + +NOTE: External properties, logging, and other features of Spring Boot are installed in the context by default only if you use `SpringApplication` to create it. + +Spring Boot provides a `@SpringBootTest` annotation, which can be used as an alternative to the standard `spring-test` `@ContextConfiguration` annotation when you need Spring Boot features. +The annotation works by <>. +In addition to `@SpringBootTest` a number of other annotations are also provided for <> of an application. + +TIP: If you are using JUnit 4, don't forget to also add `@RunWith(SpringRunner.class)` to your test, otherwise the annotations will be ignored. +If you are using JUnit 5, there's no need to add the equivalent `@ExtendWith(SpringExtension.class)` as `@SpringBootTest` and the other `@...Test` annotations are already annotated with it. + +By default, `@SpringBootTest` will not start a server. +You can use the `webEnvironment` attribute of `@SpringBootTest` to further refine how your tests run: + +* `MOCK`(Default) : Loads a web `ApplicationContext` and provides a mock web environment. + Embedded servers are not started when using this annotation. + If a web environment is not available on your classpath, this mode transparently falls back to creating a regular non-web `ApplicationContext`. + It can be used in conjunction with <> for mock-based testing of your web application. +* `RANDOM_PORT`: Loads a `WebServerApplicationContext` and provides a real web environment. + Embedded servers are started and listen on a random port. +* `DEFINED_PORT`: Loads a `WebServerApplicationContext` and provides a real web environment. + Embedded servers are started and listen on a defined port (from your `application.properties`) or on the default port of `8080`. +* `NONE`: Loads an `ApplicationContext` by using `SpringApplication` but does not provide _any_ web environment (mock or otherwise). + +NOTE: If your test is `@Transactional`, it rolls back the transaction at the end of each test method by default. +However, as using this arrangement with either `RANDOM_PORT` or `DEFINED_PORT` implicitly provides a real servlet environment, the HTTP client and server run in separate threads and, thus, in separate transactions. +Any transaction initiated on the server does not roll back in this case. + +NOTE: `@SpringBootTest` with `webEnvironment = WebEnvironment.RANDOM_PORT` will also start the management server on a separate random port if your application uses a different port for the management server. + + + +[[features.testing.spring-boot-applications.detecting-web-app-type]] +==== Detecting Web Application Type +If Spring MVC is available, a regular MVC-based application context is configured. +If you have only Spring WebFlux, we'll detect that and configure a WebFlux-based application context instead. + +If both are present, Spring MVC takes precedence. +If you want to test a reactive web application in this scenario, you must set the configprop:spring.main.web-application-type[] property: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/detectingwebapptype/MyWebFluxTests.java[] +---- + + + +[[features.testing.spring-boot-applications.detecting-configuration]] +==== Detecting Test Configuration +If you are familiar with the Spring Test Framework, you may be used to using `@ContextConfiguration(classes=...)` in order to specify which Spring `@Configuration` to load. +Alternatively, you might have often used nested `@Configuration` classes within your test. + +When testing Spring Boot applications, this is often not required. +Spring Boot's `@*Test` annotations search for your primary configuration automatically whenever you do not explicitly define one. + +The search algorithm works up from the package that contains the test until it finds a class annotated with `@SpringBootApplication` or `@SpringBootConfiguration`. +As long as you <> in a sensible way, your main configuration is usually found. + +[NOTE] +==== +If you use a <>, you should avoid adding configuration settings that are specific to a particular area on the <>. + +The underlying component scan configuration of `@SpringBootApplication` defines exclude filters that are used to make sure slicing works as expected. +If you are using an explicit `@ComponentScan` directive on your `@SpringBootApplication`-annotated class, be aware that those filters will be disabled. +If you are using slicing, you should define them again. +==== + +If you want to customize the primary configuration, you can use a nested `@TestConfiguration` class. +Unlike a nested `@Configuration` class, which would be used instead of your application's primary configuration, a nested `@TestConfiguration` class is used in addition to your application's primary configuration. + +NOTE: Spring's test framework caches application contexts between tests. +Therefore, as long as your tests share the same configuration (no matter how it is discovered), the potentially time-consuming process of loading the context happens only once. + + + +[[features.testing.spring-boot-applications.excluding-configuration]] +==== Excluding Test Configuration +If your application uses component scanning (for example, if you use `@SpringBootApplication` or `@ComponentScan`), you may find top-level configuration classes that you created only for specific tests accidentally get picked up everywhere. + +As we <>, `@TestConfiguration` can be used on an inner class of a test to customize the primary configuration. +When placed on a top-level class, `@TestConfiguration` indicates that classes in `src/test/java` should not be picked up by scanning. +You can then import that class explicitly where it is required, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/excludingconfiguration/MyTests.java[] +---- + +NOTE: If you directly use `@ComponentScan` (that is, not through `@SpringBootApplication`) you need to register the `TypeExcludeFilter` with it. +See {spring-boot-module-api}/context/TypeExcludeFilter.html[the Javadoc] for details. + + + +[[features.testing.spring-boot-applications.using-application-arguments]] +==== Using Application Arguments +If your application expects <>, you can +have `@SpringBootTest` inject them using the `args` attribute. + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/usingapplicationarguments/MyApplicationArgumentTests.java[] +---- + + + +[[features.testing.spring-boot-applications.with-mock-environment]] +==== Testing with a mock environment +By default, `@SpringBootTest` does not start the server. +If you have web endpoints that you want to test against this mock environment, you can additionally configure {spring-framework-docs}/testing.html#spring-mvc-test-framework[`MockMvc`] as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/withmockenvironment/MyMockMvcTests.java[] +---- + +TIP: If you want to focus only on the web layer and not start a complete `ApplicationContext`, consider <>. + +Alternatively, you can configure a {spring-framework-docs}/testing.html#webtestclient-tests[`WebTestClient`] as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/withmockenvironment/MyMockWebTestClientTests.java[] +---- + +[TIP] +==== +Testing within a mocked environment is usually faster than running with a full Servlet container. +However, since mocking occurs at the Spring MVC layer, code that relies on lower-level Servlet container behavior cannot be directly tested with MockMvc. + +For example, Spring Boot's error handling is based on the "`error page`" support provided by the Servlet container. +This means that, whilst you can test your MVC layer throws and handles exceptions as expected, you cannot directly test that a specific <> is rendered. +If you need to test these lower-level concerns, you can start a fully running server as described in the next section. +==== + + + +[[features.testing.spring-boot-applications.with-running-server]] +==== Testing with a running server +If you need to start a full running server, we recommend that you use random ports. +If you use `@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT)`, an available port is picked at random each time your test runs. + +The `@LocalServerPort` annotation can be used to <> into your test. +For convenience, tests that need to make REST calls to the started server can additionally `@Autowire` a {spring-framework-docs}/testing.html#webtestclient-tests[`WebTestClient`], which resolves relative links to the running server and comes with a dedicated API for verifying responses, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/withrunningserver/MyRandomPortWebTestClientTests.java[] +---- + +This setup requires `spring-webflux` on the classpath. +If you can't or won't add webflux, Spring Boot also provides a `TestRestTemplate` facility: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/withrunningserver/MyRandomPortTestRestTemplateTests.java[] +---- + + + +[[features.testing.spring-boot-applications.customizing-web-test-client]] +==== Customizing WebTestClient +To customize the `WebTestClient` bean, configure a `WebTestClientBuilderCustomizer` bean. +Any such beans are called with the `WebTestClient.Builder` that is used to create the `WebTestClient`. + + + +[[features.testing.spring-boot-applications.jmx]] +==== Using JMX +As the test context framework caches context, JMX is disabled by default to prevent identical components to register on the same domain. +If such test needs access to an `MBeanServer`, consider marking it dirty as well: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/jmx/MyJmxTests.java[] +---- + + + +[[features.testing.spring-boot-applications.metrics]] +==== Using Metrics +Regardless of your classpath, meter registries, except the in-memory backed, are not auto-configured when using `@SpringBootTest`. + +If you need to export metrics to a different backend as part of an integration test, annotate it with `@AutoConfigureMetrics`. + + + +[[features.testing.spring-boot-applications.mocking-beans]] +==== Mocking and Spying Beans +When running tests, it is sometimes necessary to mock certain components within your application context. +For example, you may have a facade over some remote service that is unavailable during development. +Mocking can also be useful when you want to simulate failures that might be hard to trigger in a real environment. + +Spring Boot includes a `@MockBean` annotation that can be used to define a Mockito mock for a bean inside your `ApplicationContext`. +You can use the annotation to add new beans or replace a single existing bean definition. +The annotation can be used directly on test classes, on fields within your test, or on `@Configuration` classes and fields. +When used on a field, the instance of the created mock is also injected. +Mock beans are automatically reset after each test method. + +[NOTE] +==== +If your test uses one of Spring Boot's test annotations (such as `@SpringBootTest`), this feature is automatically enabled. +To use this feature with a different arrangement, listeners must be explicitly added, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/mockingbeans/listener/MyTests.java[] +---- + +==== + +The following example replaces an existing `RemoteService` bean with a mock implementation: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/mockingbeans/bean/MyTests.java[] +---- + +NOTE: `@MockBean` cannot be used to mock the behavior of a bean that's exercised during application context refresh. +By the time the test is executed, the application context refresh has completed and it is too late to configure the mocked behavior. +We recommend using a `@Bean` method to create and configure the mock in this situation. + +Additionally, you can use `@SpyBean` to wrap any existing bean with a Mockito `spy`. +See the {spring-boot-test-module-api}/mock/mockito/SpyBean.html[Javadoc] for full details. + +NOTE: CGLib proxies, such as those created for scoped beans, declare the proxied methods as `final`. +This stops Mockito from functioning correctly as it cannot mock or spy on `final` methods in its default configuration. +If you want to mock or spy on such a bean, configure Mockito to use its inline mock maker by adding `org.mockito:mockito-inline` to your application's test dependencies. +This allows Mockito to mock and spy on `final` methods. + +NOTE: While Spring's test framework caches application contexts between tests and reuses a context for tests sharing the same configuration, the use of `@MockBean` or `@SpyBean` influences the cache key, which will most likely increase the number of contexts. + +TIP: If you are using `@SpyBean` to spy on a bean with `@Cacheable` methods that refer to parameters by name, your application must be compiled with `-parameters`. +This ensures that the parameter names are available to the caching infrastructure once the bean has been spied upon. + +TIP: When you are using `@SpyBean` to spy on a bean that is proxied by Spring, you may need to remove Spring's proxy in some situations, for example when setting expectations using `given` or `when`. +Use `AopTestUtils.getTargetObject(yourProxiedSpy)` to do so. + + + +[[features.testing.spring-boot-applications.autoconfigured-tests]] +==== Auto-configured Tests +Spring Boot's auto-configuration system works well for applications but can sometimes be a little too much for tests. +It often helps to load only the parts of the configuration that are required to test a "`slice`" of your application. +For example, you might want to test that Spring MVC controllers are mapping URLs correctly, and you do not want to involve database calls in those tests, or you might want to test JPA entities, and you are not interested in the web layer when those tests run. + +The `spring-boot-test-autoconfigure` module includes a number of annotations that can be used to automatically configure such "`slices`". +Each of them works in a similar way, providing a `@...Test` annotation that loads the `ApplicationContext` and one or more `@AutoConfigure...` annotations that can be used to customize auto-configuration settings. + +NOTE: Each slice restricts component scan to appropriate components and loads a very restricted set of auto-configuration classes. +If you need to exclude one of them, most `@...Test` annotations provide an `excludeAutoConfiguration` attribute. +Alternatively, you can use `@ImportAutoConfiguration#exclude`. + +NOTE: Including multiple "`slices`" by using several `@...Test` annotations in one test is not supported. +If you need multiple "`slices`", pick one of the `@...Test` annotations and include the `@AutoConfigure...` annotations of the other "`slices`" by hand. + +TIP: It is also possible to use the `@AutoConfigure...` annotations with the standard `@SpringBootTest` annotation. +You can use this combination if you are not interested in "`slicing`" your application but you want some of the auto-configured test beans. + + + +[[features.testing.spring-boot-applications.json-tests]] +==== Auto-configured JSON Tests +To test that object JSON serialization and deserialization is working as expected, you can use the `@JsonTest` annotation. +`@JsonTest` auto-configures the available supported JSON mapper, which can be one of the following libraries: + +* Jackson `ObjectMapper`, any `@JsonComponent` beans and any Jackson ``Module``s +* `Gson` +* `Jsonb` + +TIP: A list of the auto-configurations that are enabled by `@JsonTest` can be <>. + +If you need to configure elements of the auto-configuration, you can use the `@AutoConfigureJsonTesters` annotation. + +Spring Boot includes AssertJ-based helpers that work with the JSONAssert and JsonPath libraries to check that JSON appears as expected. +The `JacksonTester`, `GsonTester`, `JsonbTester`, and `BasicJsonTester` classes can be used for Jackson, Gson, Jsonb, and Strings respectively. +Any helper fields on the test class can be `@Autowired` when using `@JsonTest`. +The following example shows a test class for Jackson: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/jsontests/MyJsonTests.java[] +---- + +NOTE: JSON helper classes can also be used directly in standard unit tests. +To do so, call the `initFields` method of the helper in your `@Before` method if you do not use `@JsonTest`. + +If you're using Spring Boot's AssertJ-based helpers to assert on a number value at a given JSON path, you might not be able to use `isEqualTo` depending on the type. +Instead, you can use AssertJ's `satisfies` to assert that the value matches the given condition. +For instance, the following example asserts that the actual number is a float value close to `0.15` within an offset of `0.01`. + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/jsontests/MyJsonAssertJTests.java[tag=*] +---- + + + +[[features.testing.spring-boot-applications.spring-mvc-tests]] +==== Auto-configured Spring MVC Tests +To test whether Spring MVC controllers are working as expected, use the `@WebMvcTest` annotation. +`@WebMvcTest` auto-configures the Spring MVC infrastructure and limits scanned beans to `@Controller`, `@ControllerAdvice`, `@JsonComponent`, `Converter`, `GenericConverter`, `Filter`, `HandlerInterceptor`, `WebMvcConfigurer`, and `HandlerMethodArgumentResolver`. +Regular `@Component` and `@ConfigurationProperties` beans are not scanned when the `@WebMvcTest` annotation is used. +`@EnableConfigurationProperties` can be used to include `@ConfigurationProperties` beans. + +TIP: A list of the auto-configuration settings that are enabled by `@WebMvcTest` can be <>. + +TIP: If you need to register extra components, such as the Jackson `Module`, you can import additional configuration classes by using `@Import` on your test. + +Often, `@WebMvcTest` is limited to a single controller and is used in combination with `@MockBean` to provide mock implementations for required collaborators. + +`@WebMvcTest` also auto-configures `MockMvc`. +Mock MVC offers a powerful way to quickly test MVC controllers without needing to start a full HTTP server. + +TIP: You can also auto-configure `MockMvc` in a non-`@WebMvcTest` (such as `@SpringBootTest`) by annotating it with `@AutoConfigureMockMvc`. +The following example uses `MockMvc`: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/springmvctests/MyControllerTests.java[] +---- + +TIP: If you need to configure elements of the auto-configuration (for example, when servlet filters should be applied) you can use attributes in the `@AutoConfigureMockMvc` annotation. + +If you use HtmlUnit and Selenium, auto-configuration also provides an HtmlUnit `WebClient` bean and/or a Selenium `WebDriver` bean. +The following example uses HtmlUnit: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/springmvctests/MyHtmlUnitTests.java[] +---- + +NOTE: By default, Spring Boot puts `WebDriver` beans in a special "`scope`" to ensure that the driver exits after each test and that a new instance is injected. +If you do not want this behavior, you can add `@Scope("singleton")` to your `WebDriver` `@Bean` definition. + +WARNING: The `webDriver` scope created by Spring Boot will replace any user defined scope of the same name. +If you define your own `webDriver` scope you may find it stops working when you use `@WebMvcTest`. + +If you have Spring Security on the classpath, `@WebMvcTest` will also scan `WebSecurityConfigurer` beans. +Instead of disabling security completely for such tests, you can use Spring Security's test support. +More details on how to use Spring Security's `MockMvc` support can be found in this _<>_ how-to section. + +TIP: Sometimes writing Spring MVC tests is not enough; Spring Boot can help you run <>. + + + +[[features.testing.spring-boot-applications.spring-webflux-tests]] +==== Auto-configured Spring WebFlux Tests +To test that {spring-framework-docs}/web-reactive.html[Spring WebFlux] controllers are working as expected, you can use the `@WebFluxTest` annotation. +`@WebFluxTest` auto-configures the Spring WebFlux infrastructure and limits scanned beans to `@Controller`, `@ControllerAdvice`, `@JsonComponent`, `Converter`, `GenericConverter`, `WebFilter`, and `WebFluxConfigurer`. +Regular `@Component` and `@ConfigurationProperties` beans are not scanned when the `@WebFluxTest` annotation is used. +`@EnableConfigurationProperties` can be used to include `@ConfigurationProperties` beans. + +TIP: A list of the auto-configurations that are enabled by `@WebFluxTest` can be <>. + +TIP: If you need to register extra components, such as Jackson `Module`, you can import additional configuration classes using `@Import` on your test. + +Often, `@WebFluxTest` is limited to a single controller and used in combination with the `@MockBean` annotation to provide mock implementations for required collaborators. + +`@WebFluxTest` also auto-configures {spring-framework-docs}/testing.html#webtestclient[`WebTestClient`], which offers a powerful way to quickly test WebFlux controllers without needing to start a full HTTP server. + +TIP: You can also auto-configure `WebTestClient` in a non-`@WebFluxTest` (such as `@SpringBootTest`) by annotating it with `@AutoConfigureWebTestClient`. +The following example shows a class that uses both `@WebFluxTest` and a `WebTestClient`: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/springwebfluxtests/MyControllerTests.java[] +---- + +TIP: This setup is only supported by WebFlux applications as using `WebTestClient` in a mocked web application only works with WebFlux at the moment. + +NOTE: `@WebFluxTest` cannot detect routes registered via the functional web framework. +For testing `RouterFunction` beans in the context, consider importing your `RouterFunction` yourself via `@Import` or using `@SpringBootTest`. + +NOTE: `@WebFluxTest` cannot detect custom security configuration registered via a `@Bean` of type `SecurityWebFilterChain`. +To include that in your test, you will need to import the configuration that registers the bean via `@Import` or use `@SpringBootTest`. + +TIP: Sometimes writing Spring WebFlux tests is not enough; Spring Boot can help you run <>. + + + +[[features.testing.spring-boot-applications.autoconfigured-spring-data-cassandra]] +==== Auto-configured Data Cassandra Tests +You can use `@DataCassandraTest` to test Cassandra applications. +By default, it configures a `CassandraTemplate`, scans for `@Table` classes, and configures Spring Data Cassandra repositories. +Regular `@Component` and `@ConfigurationProperties` beans are not scanned when the `@DataCassandraTest` annotation is used. +`@EnableConfigurationProperties` can be used to include `@ConfigurationProperties` beans. +(For more about using Cassandra with Spring Boot, see "<>", earlier in this chapter.) + +TIP: A list of the auto-configuration settings that are enabled by `@DataCassandraTest` can be <>. + +The following example shows a typical setup for using Cassandra tests in Spring Boot: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/autoconfiguredspringdatacassandra/MyDataCassandraTests.java[] +---- + + + +[[features.testing.spring-boot-applications.autoconfigured-spring-data-jpa]] +==== Auto-configured Data JPA Tests +You can use the `@DataJpaTest` annotation to test JPA applications. +By default, it scans for `@Entity` classes and configures Spring Data JPA repositories. +If an embedded database is available on the classpath, it configures one as well. +SQL queries are logged by default by setting the `spring.jpa.show-sql` property to `true`. +This can be disabled using the `showSql()` attribute of the annotation. + +Regular `@Component` and `@ConfigurationProperties` beans are not scanned when the `@DataJpaTest` annotation is used. +`@EnableConfigurationProperties` can be used to include `@ConfigurationProperties` beans. + +TIP: A list of the auto-configuration settings that are enabled by `@DataJpaTest` can be <>. + +By default, data JPA tests are transactional and roll back at the end of each test. +See the {spring-framework-docs}/testing.html#testcontext-tx-enabling-transactions[relevant section] in the Spring Framework Reference Documentation for more details. +If that is not what you want, you can disable transaction management for a test or for the whole class as follows: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/autoconfiguredspringdatajpa/MyNonTransactionalTests.java[] +---- + +Data JPA tests may also inject a {spring-boot-test-autoconfigure-module-code}/orm/jpa/TestEntityManager.java[`TestEntityManager`] bean, which provides an alternative to the standard JPA `EntityManager` that is specifically designed for tests. + +TIP: `TestEntityManager` can also be auto-configured to any of your Spring-based test class by adding `@AutoConfigureTestEntityManager`. +When doing so, make sure that your test is running in a transaction, for instance by adding `@Transactional` on your test class or method. + +A `JdbcTemplate` is also available if you need that. +The following example shows the `@DataJpaTest` annotation in use: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/autoconfiguredspringdatajpa/withoutdb/MyRepositoryTests.java[] +---- + +In-memory embedded databases generally work well for tests, since they are fast and do not require any installation. +If, however, you prefer to run tests against a real database you can use the `@AutoConfigureTestDatabase` annotation, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/autoconfiguredspringdatajpa/withdb/MyRepositoryTests.java[] +---- + + + +[[features.testing.spring-boot-applications.autoconfigured-jdbc]] +==== Auto-configured JDBC Tests +`@JdbcTest` is similar to `@DataJpaTest` but is for tests that only require a `DataSource` and do not use Spring Data JDBC. +By default, it configures an in-memory embedded database and a `JdbcTemplate`. +Regular `@Component` and `@ConfigurationProperties` beans are not scanned when the `@JdbcTest` annotation is used. +`@EnableConfigurationProperties` can be used to include `@ConfigurationProperties` beans. + +TIP: A list of the auto-configurations that are enabled by `@JdbcTest` can be <>. + +By default, JDBC tests are transactional and roll back at the end of each test. +See the {spring-framework-docs}/testing.html#testcontext-tx-enabling-transactions[relevant section] in the Spring Framework Reference Documentation for more details. +If that is not what you want, you can disable transaction management for a test or for the whole class, as follows: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/autoconfiguredjdbc/MyTransactionalTests.java[] +---- + +If you prefer your test to run against a real database, you can use the `@AutoConfigureTestDatabase` annotation in the same way as for `DataJpaTest`. +(See "<>".) + + + +[[features.testing.spring-boot-applications.autoconfigured-spring-data-jdbc]] +==== Auto-configured Data JDBC Tests +`@DataJdbcTest` is similar to `@JdbcTest` but is for tests that use Spring Data JDBC repositories. +By default, it configures an in-memory embedded database, a `JdbcTemplate`, and Spring Data JDBC repositories. +Regular `@Component` and `@ConfigurationProperties` beans are not scanned when the `@DataJdbcTest` annotation is used. +`@EnableConfigurationProperties` can be used to include `@ConfigurationProperties` beans. + +TIP: A list of the auto-configurations that are enabled by `@DataJdbcTest` can be <>. + +By default, Data JDBC tests are transactional and roll back at the end of each test. +See the {spring-framework-docs}/testing.html#testcontext-tx-enabling-transactions[relevant section] in the Spring Framework Reference Documentation for more details. +If that is not what you want, you can disable transaction management for a test or for the whole test class as <>. + +If you prefer your test to run against a real database, you can use the `@AutoConfigureTestDatabase` annotation in the same way as for `DataJpaTest`. +(See "<>".) + + + +[[features.testing.spring-boot-applications.autoconfigured-jooq]] +==== Auto-configured jOOQ Tests +You can use `@JooqTest` in a similar fashion as `@JdbcTest` but for jOOQ-related tests. +As jOOQ relies heavily on a Java-based schema that corresponds with the database schema, the existing `DataSource` is used. +If you want to replace it with an in-memory database, you can use `@AutoConfigureTestDatabase` to override those settings. +(For more about using jOOQ with Spring Boot, see "<>", earlier in this chapter.) +Regular `@Component` and `@ConfigurationProperties` beans are not scanned when the `@JooqTest` annotation is used. +`@EnableConfigurationProperties` can be used to include `@ConfigurationProperties` beans. + +TIP: A list of the auto-configurations that are enabled by `@JooqTest` can be <>. + +`@JooqTest` configures a `DSLContext`. +The following example shows the `@JooqTest` annotation in use: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/autoconfiguredjooq/MyJooqTests.java[] +---- + +JOOQ tests are transactional and roll back at the end of each test by default. +If that is not what you want, you can disable transaction management for a test or for the whole test class as <>. + + + +[[features.testing.spring-boot-applications.autoconfigured-spring-data-mongodb]] +==== Auto-configured Data MongoDB Tests +You can use `@DataMongoTest` to test MongoDB applications. +By default, it configures an in-memory embedded MongoDB (if available), configures a `MongoTemplate`, scans for `@Document` classes, and configures Spring Data MongoDB repositories. +Regular `@Component` and `@ConfigurationProperties` beans are not scanned when the `@DataMongoTest` annotation is used. +`@EnableConfigurationProperties` can be used to include `@ConfigurationProperties` beans. +(For more about using MongoDB with Spring Boot, see "<>", earlier in this chapter.) + +TIP: A list of the auto-configuration settings that are enabled by `@DataMongoTest` can be <>. + +The following class shows the `@DataMongoTest` annotation in use: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/autoconfiguredspringdatamongodb/withoutdb/MyDataMongoDbTests.java[] +---- + +In-memory embedded MongoDB generally works well for tests, since it is fast and does not require any developer installation. +If, however, you prefer to run tests against a real MongoDB server, you should exclude the embedded MongoDB auto-configuration, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/autoconfiguredspringdatamongodb/withdb/MyDataMongoDbTests.java[] +---- + + + +[[features.testing.spring-boot-applications.autoconfigured-spring-data-neo4j]] +==== Auto-configured Data Neo4j Tests +You can use `@DataNeo4jTest` to test Neo4j applications. +By default, it scans for `@Node` classes, and configures Spring Data Neo4j repositories. +Regular `@Component` and `@ConfigurationProperties` beans are not scanned when the `@DataNeo4jTest` annotation is used. +`@EnableConfigurationProperties` can be used to include `@ConfigurationProperties` beans. +(For more about using Neo4J with Spring Boot, see "<>", earlier in this chapter.) + +TIP: A list of the auto-configuration settings that are enabled by `@DataNeo4jTest` can be <>. + +The following example shows a typical setup for using Neo4J tests in Spring Boot: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/autoconfiguredspringdataneo4j/propagation/MyDataNeo4jTests.java[] +---- + +By default, Data Neo4j tests are transactional and roll back at the end of each test. +See the {spring-framework-docs}/testing.html#testcontext-tx-enabling-transactions[relevant section] in the Spring Framework Reference Documentation for more details. +If that is not what you want, you can disable transaction management for a test or for the whole class, as follows: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/autoconfiguredspringdataneo4j/nopropagation/MyDataNeo4jTests.java[] +---- + +NOTE: Transactional tests are not supported with reactive access. +If you are using this style, you must configure `@DataNeo4jTest` tests as described above. + + + +[[features.testing.spring-boot-applications.autoconfigured-spring-data-redis]] +==== Auto-configured Data Redis Tests +You can use `@DataRedisTest` to test Redis applications. +By default, it scans for `@RedisHash` classes and configures Spring Data Redis repositories. +Regular `@Component` and `@ConfigurationProperties` beans are not scanned when the `@DataRedisTest` annotation is used. +`@EnableConfigurationProperties` can be used to include `@ConfigurationProperties` beans. +(For more about using Redis with Spring Boot, see "<>", earlier in this chapter.) + +TIP: A list of the auto-configuration settings that are enabled by `@DataRedisTest` can be <>. + +The following example shows the `@DataRedisTest` annotation in use: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/autoconfiguredspringdataredis/MyDataRedisTests.java[] +---- + + + +[[features.testing.spring-boot-applications.autoconfigured-spring-data-ldap]] +==== Auto-configured Data LDAP Tests +You can use `@DataLdapTest` to test LDAP applications. +By default, it configures an in-memory embedded LDAP (if available), configures an `LdapTemplate`, scans for `@Entry` classes, and configures Spring Data LDAP repositories. +Regular `@Component` and `@ConfigurationProperties` beans are not scanned when the `@DataLdapTest` annotation is used. +`@EnableConfigurationProperties` can be used to include `@ConfigurationProperties` beans. +(For more about using LDAP with Spring Boot, see "<>", earlier in this chapter.) + +TIP: A list of the auto-configuration settings that are enabled by `@DataLdapTest` can be <>. + +The following example shows the `@DataLdapTest` annotation in use: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/autoconfiguredspringdataldap/inmemory/MyDataLdapTests.java[] +---- + +In-memory embedded LDAP generally works well for tests, since it is fast and does not require any developer installation. +If, however, you prefer to run tests against a real LDAP server, you should exclude the embedded LDAP auto-configuration, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/autoconfiguredspringdataldap/server/MyDataLdapTests.java[] +---- + + + +[[features.testing.spring-boot-applications.autoconfigured-rest-client]] +==== Auto-configured REST Clients +You can use the `@RestClientTest` annotation to test REST clients. +By default, it auto-configures Jackson, GSON, and Jsonb support, configures a `RestTemplateBuilder`, and adds support for `MockRestServiceServer`. +Regular `@Component` and `@ConfigurationProperties` beans are not scanned when the `@RestClientTest` annotation is used. +`@EnableConfigurationProperties` can be used to include `@ConfigurationProperties` beans. + +TIP: A list of the auto-configuration settings that are enabled by `@RestClientTest` can be <>. + +The specific beans that you want to test should be specified by using the `value` or `components` attribute of `@RestClientTest`, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/autoconfiguredrestclient/MyRestClientTests.java[] +---- + + + +[[features.testing.spring-boot-applications.autoconfigured-spring-restdocs]] +==== Auto-configured Spring REST Docs Tests +You can use the `@AutoConfigureRestDocs` annotation to use {spring-restdocs}[Spring REST Docs] in your tests with Mock MVC, REST Assured, or WebTestClient. +It removes the need for the JUnit extension in Spring REST Docs. + +`@AutoConfigureRestDocs` can be used to override the default output directory (`target/generated-snippets` if you are using Maven or `build/generated-snippets` if you are using Gradle). +It can also be used to configure the host, scheme, and port that appears in any documented URIs. + + + +[[features.testing.spring-boot-applications.autoconfigured-spring-restdocs.with-mock-mvc]] +===== Auto-configured Spring REST Docs Tests with Mock MVC +`@AutoConfigureRestDocs` customizes the `MockMvc` bean to use Spring REST Docs when testing Servlet-based web applications. +You can inject it by using `@Autowired` and use it in your tests as you normally would when using Mock MVC and Spring REST Docs, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/autoconfiguredspringrestdocs/withmockmvc/MyUserDocumentationTests.java[] +---- + +If you require more control over Spring REST Docs configuration than offered by the attributes of `@AutoConfigureRestDocs`, you can use a `RestDocsMockMvcConfigurationCustomizer` bean, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/autoconfiguredspringrestdocs/withmockmvc/MyRestDocsConfiguration.java[] +---- + +If you want to make use of Spring REST Docs support for a parameterized output directory, you can create a `RestDocumentationResultHandler` bean. +The auto-configuration calls `alwaysDo` with this result handler, thereby causing each `MockMvc` call to automatically generate the default snippets. +The following example shows a `RestDocumentationResultHandler` being defined: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/autoconfiguredspringrestdocs/withmockmvc/MyResultHandlerConfiguration.java[] +---- + + + +[[features.testing.spring-boot-applications.autoconfigured-spring-restdocs.with-web-test-client]] +===== Auto-configured Spring REST Docs Tests with WebTestClient +`@AutoConfigureRestDocs` can also be used with `WebTestClient` when testing reactive web applications. +You can inject it by using `@Autowired` and use it in your tests as you normally would when using `@WebFluxTest` and Spring REST Docs, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/autoconfiguredspringrestdocs/withwebtestclient/MyUsersDocumentationTests.java[] +---- + +If you require more control over Spring REST Docs configuration than offered by the attributes of `@AutoConfigureRestDocs`, you can use a `RestDocsWebTestClientConfigurationCustomizer` bean, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/autoconfiguredspringrestdocs/withwebtestclient/MyRestDocsConfiguration.java[] +---- + +If you want to make use of Spring REST Docs support for a parameterized output directory, you can use a `WebTestClientBuilderCustomizer` to configure a consumer for every entity exchange result. +The following example shows such a `WebTestClientBuilderCustomizer` being defined: + +[source,java,indent=0] +---- +include::{docs-java}/features/testing/springbootapplications/autoconfiguredspringrestdocs/withwebtestclient/MyWebTestClientBuilderCustomizerConfiguration.java[] +---- + + + +[[features.testing.spring-boot-applications.autoconfigured-spring-restdocs.with-rest-assured]] +===== Auto-configured Spring REST Docs Tests with REST Assured +`@AutoConfigureRestDocs` makes a `RequestSpecification` bean, preconfigured to use Spring REST Docs, available to your tests. +You can inject it by using `@Autowired` and use it in your tests as you normally would when using REST Assured and Spring REST Docs, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/autoconfiguredspringrestdocs/withrestassured/MyUserDocumentationTests.java[] +---- + +If you require more control over Spring REST Docs configuration than offered by the attributes of `@AutoConfigureRestDocs`, a `RestDocsRestAssuredConfigurationCustomizer` bean can be used, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/autoconfiguredspringrestdocs/withrestassured/MyRestDocsConfiguration.java[] +---- + + + +[[features.testing.spring-boot-applications.autoconfigured-webservices]] +==== Auto-configured Spring Web Services Tests +You can use `@WebServiceClientTest` to test applications that use call web services using the Spring Web Services project. +By default, it configures a mock `WebServiceServer` bean and automatically customizes your `WebServiceTemplateBuilder`. +(For more about using Web Services with Spring Boot, see "<>", earlier in this chapter.) + + +TIP: A list of the auto-configuration settings that are enabled by `@WebServiceClientTest` can be <>. + +The following example shows the `@WebServiceClientTest` annotation in use: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/autoconfiguredwebservices/MyWebServiceClientTests.java[] +---- + + + +[[features.testing.spring-boot-applications.additional-autoconfiguration-and-slicing]] +==== Additional Auto-configuration and Slicing +Each slice provides one or more `@AutoConfigure...` annotations that namely defines the auto-configurations that should be included as part of a slice. +Additional auto-configurations can be added on a test-by-test basis by creating a custom `@AutoConfigure...` annotation or by adding `@ImportAutoConfiguration` to the test as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/additionalautoconfigurationandslicing/MyJdbcTests.java[] +---- + +NOTE: Make sure to not use the regular `@Import` annotation to import auto-configurations as they are handled in a specific way by Spring Boot. + +Alternatively, additional auto-configurations can be added for any use of a slice annotation by registering them in `META-INF/spring.factories` as shown in the following example: + +[indent=0] +---- + org.springframework.boot.test.autoconfigure.jdbc.JdbcTest=com.example.IntegrationAutoConfiguration +---- + +TIP: A slice or `@AutoConfigure...` annotation can be customized this way as long as it is meta-annotated with `@ImportAutoConfiguration`. + + + +[[features.testing.spring-boot-applications.user-configuration-and-slicing]] +==== User Configuration and Slicing +If you <> in a sensible way, your `@SpringBootApplication` class is <> as the configuration of your tests. + +It then becomes important not to litter the application's main class with configuration settings that are specific to a particular area of its functionality. + +Assume that you are using Spring Batch and you rely on the auto-configuration for it. +You could define your `@SpringBootApplication` as follows: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/userconfigurationandslicing/MyApplication.java[] +---- + +Because this class is the source configuration for the test, any slice test actually tries to start Spring Batch, which is definitely not what you want to do. +A recommended approach is to move that area-specific configuration to a separate `@Configuration` class at the same level as your application, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/userconfigurationandslicing/MyBatchConfiguration.java[] +---- + +NOTE: Depending on the complexity of your application, you may either have a single `@Configuration` class for your customizations or one class per domain area. +The latter approach lets you enable it in one of your tests, if necessary, with the `@Import` annotation. +See <> for more details on when you might want to enable specific `@Configuration` classes for slice tests. + +Test slices exclude `@Configuration` classes from scanning. +For example, for a `@WebMvcTest`, the following configuration will not include the given `WebMvcConfigurer` bean in the application context loaded by the test slice: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/userconfigurationandslicing/MyWebConfiguration.java[] +---- + +The configuration below will, however, cause the custom `WebMvcConfigurer` to be loaded by the test slice. + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/userconfigurationandslicing/MyWebMvcConfigurer.java[] +---- + +Another source of confusion is classpath scanning. +Assume that, while you structured your code in a sensible way, you need to scan an additional package. +Your application may resemble the following code: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/userconfigurationandslicing/scan/MyApplication.java[] +---- + +Doing so effectively overrides the default component scan directive with the side effect of scanning those two packages regardless of the slice that you chose. +For instance, a `@DataJpaTest` seems to suddenly scan components and user configurations of your application. +Again, moving the custom directive to a separate class is a good way to fix this issue. + +TIP: If this is not an option for you, you can create a `@SpringBootConfiguration` somewhere in the hierarchy of your test so that it is used instead. +Alternatively, you can specify a source for your test, which disables the behavior of finding a default one. + + + +[[features.testing.spring-boot-applications.spock]] +==== Using Spock to Test Spring Boot Applications +Spock 2.x can be used to test a Spring Boot application. +To do so, add a dependency on Spock's `spock-spring` module to your application's build. +`spock-spring` integrates Spring's test framework into Spock. +See https://spockframework.org/spock/docs/2.0/modules.html#_spring_module[the documentation for Spock's Spring module] for further details. + + + +[[features.testing.utilities]] +=== Test Utilities +A few test utility classes that are generally useful when testing your application are packaged as part of `spring-boot`. + + + +[[features.testing.utilities.config-data-application-context-initializer]] +==== ConfigDataApplicationContextInitializer +`ConfigDataApplicationContextInitializer` is an `ApplicationContextInitializer` that you can apply to your tests to load Spring Boot `application.properties` files. +You can use it when you do not need the full set of features provided by `@SpringBootTest`, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/utilities/configdataapplicationcontextinitializer/MyConfigFileTests.java[] +---- + +NOTE: Using `ConfigDataApplicationContextInitializer` alone does not provide support for `@Value("${...}")` injection. +Its only job is to ensure that `application.properties` files are loaded into Spring's `Environment`. +For `@Value` support, you need to either additionally configure a `PropertySourcesPlaceholderConfigurer` or use `@SpringBootTest`, which auto-configures one for you. + + + +[[features.testing.utilities.test-property-values]] +==== TestPropertyValues +`TestPropertyValues` lets you quickly add properties to a `ConfigurableEnvironment` or `ConfigurableApplicationContext`. +You can call it with `key=value` strings, as follows: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/utilities/testpropertyvalues/MyEnvironmentTests.java[] +---- + + + +[[features.testing.utilities.output-capture]] +==== OutputCapture +`OutputCapture` is a JUnit `Extension` that you can use to capture `System.out` and `System.err` output. +To use add `@ExtendWith(OutputCaptureExtension.class)` and inject `CapturedOutput` as an argument to your test class constructor or test method as follows: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/utilities/outputcapture/MyOutputCaptureTests.java[] +---- + + + +[[features.testing.utilities.test-rest-template]] +==== TestRestTemplate +`TestRestTemplate` is a convenience alternative to Spring's `RestTemplate` that is useful in integration tests. +You can get a vanilla template or one that sends Basic HTTP authentication (with a username and password). +In either case, the template is fault tolerant. +This means that it behaves in a test-friendly way by not throwing exceptions on 4xx and 5xx errors. +Instead, such errors can be detected via the returned `ResponseEntity` and its status code. + +TIP: Spring Framework 5.0 provides a new `WebTestClient` that works for <> and both <>. +It provides a fluent API for assertions, unlike `TestRestTemplate`. + +It is recommended, but not mandatory, to use the Apache HTTP Client (version 4.3.2 or better). +If you have that on your classpath, the `TestRestTemplate` responds by configuring the client appropriately. +If you do use Apache's HTTP client, some additional test-friendly features are enabled: + +* Redirects are not followed (so you can assert the response location). +* Cookies are ignored (so the template is stateless). + +`TestRestTemplate` can be instantiated directly in your integration tests, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/utilities/testresttemplate/MyTests.java[] +---- + +Alternatively, if you use the `@SpringBootTest` annotation with `WebEnvironment.RANDOM_PORT` or `WebEnvironment.DEFINED_PORT`, you can inject a fully configured `TestRestTemplate` and start using it. +If necessary, additional customizations can be applied through the `RestTemplateBuilder` bean. +Any URLs that do not specify a host and port automatically connect to the embedded server, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/utilities/testresttemplate/MySpringBootTests.java[] +---- diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/validation.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/validation.adoc new file mode 100644 index 000000000000..009b5dd324ce --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/validation.adoc @@ -0,0 +1,12 @@ +[[features.validation]] +== Validation +The method validation feature supported by Bean Validation 1.1 is automatically enabled as long as a JSR-303 implementation (such as Hibernate validator) is on the classpath. +This lets bean methods be annotated with `javax.validation` constraints on their parameters and/or on their return value. +Target classes with such annotated methods need to be annotated with the `@Validated` annotation at the type level for their methods to be searched for inline constraint annotations. + +For instance, the following service triggers the validation of the first argument, making sure its size is between 8 and 10: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/validation/MyBean.java[] +---- diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/webclient.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/webclient.adoc new file mode 100644 index 000000000000..ccf22e177a6f --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/webclient.adoc @@ -0,0 +1,48 @@ +[[features.webclient]] +== Calling REST Services with WebClient +If you have Spring WebFlux on your classpath, you can also choose to use `WebClient` to call remote REST services. +Compared to `RestTemplate`, this client has a more functional feel and is fully reactive. +You can learn more about the `WebClient` in the dedicated {spring-framework-docs}/web-reactive.html#webflux-client[section in the Spring Framework docs]. + +Spring Boot creates and pre-configures a `WebClient.Builder` for you. +It is strongly advised to inject it in your components and use it to create `WebClient` instances. +Spring Boot is configuring that builder to share HTTP resources, reflect codecs setup in the same fashion as the server ones (see <>), and more. + +The following code shows a typical example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/webclient/MyService.java[] +---- + + + +[[features.webclient.runtime]] +=== WebClient Runtime +Spring Boot will auto-detect which `ClientHttpConnector` to use to drive `WebClient`, depending on the libraries available on the application classpath. +For now, Reactor Netty and Jetty RS client are supported. + +The `spring-boot-starter-webflux` starter depends on `io.projectreactor.netty:reactor-netty` by default, which brings both server and client implementations. +If you choose to use Jetty as a reactive server instead, you should add a dependency on the Jetty Reactive HTTP client library, `org.eclipse.jetty:jetty-reactive-httpclient`. +Using the same technology for server and client has it advantages, as it will automatically share HTTP resources between client and server. + +Developers can override the resource configuration for Jetty and Reactor Netty by providing a custom `ReactorResourceFactory` or `JettyResourceFactory` bean - this will be applied to both clients and servers. + +If you wish to override that choice for the client, you can define your own `ClientHttpConnector` bean and have full control over the client configuration. + +You can learn more about the {spring-framework-docs}/web-reactive.html#webflux-client-builder[`WebClient` configuration options in the Spring Framework reference documentation]. + + + +[[features.webclient.customization]] +=== WebClient Customization +There are three main approaches to `WebClient` customization, depending on how broadly you want the customizations to apply. + +To make the scope of any customizations as narrow as possible, inject the auto-configured `WebClient.Builder` and then call its methods as required. +`WebClient.Builder` instances are stateful: Any change on the builder is reflected in all clients subsequently created with it. +If you want to create several clients with the same builder, you can also consider cloning the builder with `WebClient.Builder other = builder.clone();`. + +To make an application-wide, additive customization to all `WebClient.Builder` instances, you can declare `WebClientCustomizer` beans and change the `WebClient.Builder` locally at the point of injection. + +Finally, you can fall back to the original API and use `WebClient.create()`. +In that case, no auto-configuration or `WebClientCustomizer` is applied. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/webservices.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/webservices.adoc new file mode 100644 index 000000000000..f345260b9958 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/webservices.adoc @@ -0,0 +1,39 @@ +[[features.webservices]] +== Web Services +Spring Boot provides Web Services auto-configuration so that all you must do is define your `Endpoints`. + +The {spring-webservices-docs}[Spring Web Services features] can be easily accessed with the `spring-boot-starter-webservices` module. + +`SimpleWsdl11Definition` and `SimpleXsdSchema` beans can be automatically created for your WSDLs and XSDs respectively. +To do so, configure their location, as shown in the following example: + + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + webservices: + wsdl-locations: "classpath:/wsdl" +---- + + + +[[features.webservices.template]] +=== Calling Web Services with WebServiceTemplate +If you need to call remote Web services from your application, you can use the {spring-webservices-docs}#client-web-service-template[`WebServiceTemplate`] class. +Since `WebServiceTemplate` instances often need to be customized before being used, Spring Boot does not provide any single auto-configured `WebServiceTemplate` bean. +It does, however, auto-configure a `WebServiceTemplateBuilder`, which can be used to create `WebServiceTemplate` instances when needed. + +The following code shows a typical example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/webservices/template/MyService.java[] +---- + +By default, `WebServiceTemplateBuilder` detects a suitable HTTP-based `WebServiceMessageSender` using the available HTTP client libraries on the classpath. +You can also customize read and connection timeouts as follows: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/webservices/template/MyWebServiceTemplateConfiguration.java[] +---- diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/websockets.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/websockets.adoc new file mode 100644 index 000000000000..479ee7985035 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/websockets.adoc @@ -0,0 +1,16 @@ +[[features.websockets]] +== WebSockets +Spring Boot provides WebSockets auto-configuration for embedded Tomcat, Jetty, and Undertow. +If you deploy a war file to a standalone container, Spring Boot assumes that the container is responsible for the configuration of its WebSocket support. + +Spring Framework provides {spring-framework-docs}/web.html#websocket[rich WebSocket support] for MVC web applications that can be easily accessed through the `spring-boot-starter-websocket` module. + +WebSocket support is also available for {spring-framework-docs}/web-reactive.html#webflux-websocket[reactive web applications] and requires to include the WebSocket API alongside `spring-boot-starter-webflux`: + +[source,xml,indent=0,subs="verbatim"] +---- + + javax.websocket + javax.websocket-api + +---- diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/whats-next.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/whats-next.adoc new file mode 100644 index 000000000000..f08953adde70 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/whats-next.adoc @@ -0,0 +1,6 @@ +[[features.whats-next]] +== What to Read Next +If you want to learn more about any of the classes discussed in this section, you can check out the {spring-boot-api}/[Spring Boot API documentation] or you can browse the {spring-boot-code}[source code directly]. +If you have specific questions, take a look at the <> section. + +If you are comfortable with Spring Boot's core features, you can continue on and read about <>. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/getting-help.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/getting-help.adoc new file mode 100644 index 000000000000..93c8cbdc82f6 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/getting-help.adoc @@ -0,0 +1,18 @@ +[[getting-help]] += Getting Help +include::attributes.adoc[] + +If you have trouble with Spring Boot, we would like to help. + +* Try the <>. +They provide solutions to the most common questions. +* Learn the Spring basics. +Spring Boot builds on many other Spring projects. +Check the https://spring.io[spring.io] web-site for a wealth of reference documentation. +If you are starting out with Spring, try one of the https://spring.io/guides[guides]. +* Ask a question. +We monitor https://stackoverflow.com[stackoverflow.com] for questions tagged with https://stackoverflow.com/tags/spring-boot[`spring-boot`]. +* Report bugs with Spring Boot at https://github.com/spring-projects/spring-boot/issues. + +NOTE: All of Spring Boot is open source, including the documentation. +If you find problems with the docs or if you want to improve them, please {spring-boot-code}[get involved]. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/getting-started.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/getting-started.adoc new file mode 100644 index 000000000000..57ea7dbd2d9c --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/getting-started.adoc @@ -0,0 +1,23 @@ +[[getting-started]] += Getting Started +include::attributes.adoc[] + + + +If you are getting started with Spring Boot, or "`Spring`" in general, start by reading this section. +It answers the basic "`what?`", "`how?`" and "`why?`" questions. +It includes an introduction to Spring Boot, along with installation instructions. +We then walk you through building your first Spring Boot application, discussing some core principles as we go. + + + +include::getting-started/introducing-spring-boot.adoc[] + +include::getting-started/system-requirements.adoc[] + +include::getting-started/installing.adoc[] + +include::getting-started/first-application.adoc[] + +include::getting-started/whats-next.adoc[] + diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/getting-started/first-application.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/getting-started/first-application.adoc new file mode 100644 index 000000000000..e5bf793229b5 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/getting-started/first-application.adoc @@ -0,0 +1,316 @@ +[[getting-started.first-application]] +== Developing Your First Spring Boot Application +This section describes how to develop a small "`Hello World!`" web application that highlights some of Spring Boot's key features. +We use Maven to build this project, since most IDEs support it. + +[TIP] +==== +The https://spring.io[spring.io] web site contains many "`Getting Started`" https://spring.io/guides[guides] that use Spring Boot. +If you need to solve a specific problem, check there first. + +You can shortcut the steps below by going to https://start.spring.io and choosing the "Web" starter from the dependencies searcher. +Doing so generates a new project structure so that you can <>. +Check the https://github.com/spring-io/start.spring.io/blob/main/USING.adoc[start.spring.io user guide] for more details. +==== + +Before we begin, open a terminal and run the following commands to ensure that you have valid versions of Java and Maven installed: + +[source,shell,indent=0,subs="verbatim"] +---- + $ java -version + java version "1.8.0_102" + Java(TM) SE Runtime Environment (build 1.8.0_102-b14) + Java HotSpot(TM) 64-Bit Server VM (build 25.102-b14, mixed mode) +---- + +[source,shell,indent=0,subs="verbatim"] +---- + $ mvn -v + Apache Maven 3.5.4 (1edded0938998edf8bf061f1ceb3cfdeccf443fe; 2018-06-17T14:33:14-04:00) + Maven home: /usr/local/Cellar/maven/3.3.9/libexec + Java version: 1.8.0_102, vendor: Oracle Corporation +---- + +NOTE: This sample needs to be created in its own directory. +Subsequent instructions assume that you have created a suitable directory and that it is your current directory. + + + +[[getting-started.first-application.pom]] +=== Creating the POM +We need to start by creating a Maven `pom.xml` file. +The `pom.xml` is the recipe that is used to build your project. +Open your favorite text editor and add the following: + +[source,xml,indent=0,subs="verbatim,attributes"] +---- + + + 4.0.0 + + com.example + myproject + 0.0.1-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + {spring-boot-version} + + + + +ifeval::["{spring-boot-artifactory-repo}" != "release"] + + + + spring-snapshots + https://repo.spring.io/snapshot + true + + + spring-milestones + https://repo.spring.io/milestone + + + + + spring-snapshots + https://repo.spring.io/snapshot + + + spring-milestones + https://repo.spring.io/milestone + + +endif::[] + +---- + +The preceding listing should give you a working build. +You can test it by running `mvn package` (for now, you can ignore the "`jar will be empty - no content was marked for inclusion!`" warning). + +NOTE: At this point, you could import the project into an IDE (most modern Java IDEs include built-in support for Maven). +For simplicity, we continue to use a plain text editor for this example. + + + +[[getting-started.first-application.dependencies]] +=== Adding Classpath Dependencies +Spring Boot provides a number of "`Starters`" that let you add jars to your classpath. +Our applications for smoke tests use the `spring-boot-starter-parent` in the `parent` section of the POM. +The `spring-boot-starter-parent` is a special starter that provides useful Maven defaults. +It also provides a <> section so that you can omit `version` tags for "`blessed`" dependencies. + +Other "`Starters`" provide dependencies that you are likely to need when developing a specific type of application. +Since we are developing a web application, we add a `spring-boot-starter-web` dependency. +Before that, we can look at what we currently have by running the following command: + +[source,shell,indent=0,subs="verbatim"] +---- + $ mvn dependency:tree + + [INFO] com.example:myproject:jar:0.0.1-SNAPSHOT +---- + +The `mvn dependency:tree` command prints a tree representation of your project dependencies. +You can see that `spring-boot-starter-parent` provides no dependencies by itself. +To add the necessary dependencies, edit your `pom.xml` and add the `spring-boot-starter-web` dependency immediately below the `parent` section: + +[source,xml,indent=0,subs="verbatim"] +---- + + + org.springframework.boot + spring-boot-starter-web + + +---- + +If you run `mvn dependency:tree` again, you see that there are now a number of additional dependencies, including the Tomcat web server and Spring Boot itself. + + + +[[getting-started.first-application.code]] +=== Writing the Code +To finish our application, we need to create a single Java file. +By default, Maven compiles sources from `src/main/java`, so you need to create that directory structure and then add a file named `src/main/java/MyApplication.java` to contain the following code: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/gettingstarted/firstapplication/code/MyApplication.java[] +---- + +Although there is not much code here, quite a lot is going on. +We step through the important parts in the next few sections. + + + +[[getting-started.first-application.code.mvc-annotations]] +==== The @RestController and @RequestMapping Annotations +The first annotation on our `MyApplication` class is `@RestController`. +This is known as a _stereotype_ annotation. +It provides hints for people reading the code and for Spring that the class plays a specific role. +In this case, our class is a web `@Controller`, so Spring considers it when handling incoming web requests. + +The `@RequestMapping` annotation provides "`routing`" information. +It tells Spring that any HTTP request with the `/` path should be mapped to the `home` method. +The `@RestController` annotation tells Spring to render the resulting string directly back to the caller. + +TIP: The `@RestController` and `@RequestMapping` annotations are Spring MVC annotations (they are not specific to Spring Boot). +See the {spring-framework-docs}/web.html#mvc[MVC section] in the Spring Reference Documentation for more details. + + + +[[getting-started.first-application.code.enable-auto-configuration]] +==== The @EnableAutoConfiguration Annotation +The second class-level annotation is `@EnableAutoConfiguration`. +This annotation tells Spring Boot to "`guess`" how you want to configure Spring, based on the jar dependencies that you have added. +Since `spring-boot-starter-web` added Tomcat and Spring MVC, the auto-configuration assumes that you are developing a web application and sets up Spring accordingly. + +.Starters and Auto-configuration +**** +Auto-configuration is designed to work well with "`Starters`", but the two concepts are not directly tied. +You are free to pick and choose jar dependencies outside of the starters. +Spring Boot still does its best to auto-configure your application. +**** + + + +[[getting-started.first-application.code.main-method]] +==== The "`main`" Method +The final part of our application is the `main` method. +This is a standard method that follows the Java convention for an application entry point. +Our main method delegates to Spring Boot's `SpringApplication` class by calling `run`. +`SpringApplication` bootstraps our application, starting Spring, which, in turn, starts the auto-configured Tomcat web server. +We need to pass `MyApplication.class` as an argument to the `run` method to tell `SpringApplication` which is the primary Spring component. +The `args` array is also passed through to expose any command-line arguments. + + + +[[getting-started.first-application.run]] +=== Running the Example +At this point, your application should work. +Since you used the `spring-boot-starter-parent` POM, you have a useful `run` goal that you can use to start the application. +Type `mvn spring-boot:run` from the root project directory to start the application. +You should see output similar to the following: + +[source,shell,indent=0,subs="verbatim,attributes"] +---- + $ mvn spring-boot:run + + . ____ _ __ _ _ + /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ + ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ + \\/ ___)| |_)| | | | | || (_| | ) ) ) ) + ' |____| .__|_| |_|_| |_\__, | / / / / + =========|_|==============|___/=/_/_/_/ + :: Spring Boot :: (v{spring-boot-version}) + ....... . . . + ....... . . . (log output here) + ....... . . . + ........ Started MyApplication in 2.222 seconds (JVM running for 6.514) +---- + +If you open a web browser to `http://localhost:8080`, you should see the following output: + +[indent=0] +---- + Hello World! +---- + +To gracefully exit the application, press `ctrl-c`. + + + +[[getting-started.first-application.executable-jar]] +=== Creating an Executable Jar +We finish our example by creating a completely self-contained executable jar file that we could run in production. +Executable jars (sometimes called "`fat jars`") are archives containing your compiled classes along with all of the jar dependencies that your code needs to run. + +.Executable jars and Java +**** +Java does not provide a standard way to load nested jar files (jar files that are themselves contained within a jar). +This can be problematic if you are looking to distribute a self-contained application. + +To solve this problem, many developers use "`uber`" jars. +An uber jar packages all the classes from all the application's dependencies into a single archive. +The problem with this approach is that it becomes hard to see which libraries are in your application. +It can also be problematic if the same filename is used (but with different content) in multiple jars. + +Spring Boot takes a <> and lets you actually nest jars directly. +**** + +To create an executable jar, we need to add the `spring-boot-maven-plugin` to our `pom.xml`. +To do so, insert the following lines just below the `dependencies` section: + +[source,xml,indent=0,subs="verbatim"] +---- + + + + org.springframework.boot + spring-boot-maven-plugin + + + +---- + +NOTE: The `spring-boot-starter-parent` POM includes `` configuration to bind the `repackage` goal. +If you do not use the parent POM, you need to declare this configuration yourself. +See the {spring-boot-maven-plugin-docs}#getting-started[plugin documentation] for details. + +Save your `pom.xml` and run `mvn package` from the command line, as follows: + +[source,shell,indent=0,subs="verbatim,attributes"] +---- + $ mvn package + + [INFO] Scanning for projects... + [INFO] + [INFO] ------------------------------------------------------------------------ + [INFO] Building myproject 0.0.1-SNAPSHOT + [INFO] ------------------------------------------------------------------------ + [INFO] .... .. + [INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ myproject --- + [INFO] Building jar: /Users/developer/example/spring-boot-example/target/myproject-0.0.1-SNAPSHOT.jar + [INFO] + [INFO] --- spring-boot-maven-plugin:{spring-boot-version}:repackage (default) @ myproject --- + [INFO] ------------------------------------------------------------------------ + [INFO] BUILD SUCCESS + [INFO] ------------------------------------------------------------------------ +---- + +If you look in the `target` directory, you should see `myproject-0.0.1-SNAPSHOT.jar`. +The file should be around 10 MB in size. +If you want to peek inside, you can use `jar tvf`, as follows: + +[source,shell,indent=0,subs="verbatim"] +---- + $ jar tvf target/myproject-0.0.1-SNAPSHOT.jar +---- + +You should also see a much smaller file named `myproject-0.0.1-SNAPSHOT.jar.original` in the `target` directory. +This is the original jar file that Maven created before it was repackaged by Spring Boot. + +To run that application, use the `java -jar` command, as follows: + +[source,shell,indent=0,subs="verbatim,attributes"] +---- + $ java -jar target/myproject-0.0.1-SNAPSHOT.jar + + . ____ _ __ _ _ + /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ + ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ + \\/ ___)| |_)| | | | | || (_| | ) ) ) ) + ' |____| .__|_| |_|_| |_\__, | / / / / + =========|_|==============|___/=/_/_/_/ + :: Spring Boot :: (v{spring-boot-version}) + ....... . . . + ....... . . . (log output here) + ....... . . . + ........ Started MyApplication in 2.536 seconds (JVM running for 2.864) +---- + +As before, to exit the application, press `ctrl-c`. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/getting-started/installing.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/getting-started/installing.adoc new file mode 100644 index 000000000000..0ad32e14b67b --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/getting-started/installing.adoc @@ -0,0 +1,235 @@ +[[getting-started.installing]] +== Installing Spring Boot +Spring Boot can be used with "`classic`" Java development tools or installed as a command line tool. +Either way, you need https://www.java.com[Java SDK v1.8] or higher. +Before you begin, you should check your current Java installation by using the following command: + +[source,shell,indent=0,subs="verbatim"] +---- + $ java -version +---- + +If you are new to Java development or if you want to experiment with Spring Boot, you might want to try the <> (Command Line Interface) first. +Otherwise, read on for "`classic`" installation instructions. + + + +[[getting-started.installing.java]] +=== Installation Instructions for the Java Developer +You can use Spring Boot in the same way as any standard Java library. +To do so, include the appropriate `+spring-boot-*.jar+` files on your classpath. +Spring Boot does not require any special tools integration, so you can use any IDE or text editor. +Also, there is nothing special about a Spring Boot application, so you can run and debug a Spring Boot application as you would any other Java program. + +Although you _could_ copy Spring Boot jars, we generally recommend that you use a build tool that supports dependency management (such as Maven or Gradle). + + + +[[getting-started.installing.java.maven]] +==== Maven Installation +Spring Boot is compatible with Apache Maven 3.3 or above. +If you do not already have Maven installed, you can follow the instructions at https://maven.apache.org. + +TIP: On many operating systems, Maven can be installed with a package manager. +If you use OSX Homebrew, try `brew install maven`. +Ubuntu users can run `sudo apt-get install maven`. +Windows users with https://chocolatey.org/[Chocolatey] can run `choco install maven` from an elevated (administrator) prompt. + +Spring Boot dependencies use the `org.springframework.boot` `groupId`. +Typically, your Maven POM file inherits from the `spring-boot-starter-parent` project and declares dependencies to one or more <>. +Spring Boot also provides an optional <> to create executable jars. + +More details on getting started with Spring Boot and Maven can be found in the {spring-boot-maven-plugin-docs}#getting-started[Getting Started section] of the Maven plugin's reference guide. + + + +[[getting-started.installing.java.gradle]] +==== Gradle Installation +Spring Boot is compatible with Gradle 6.8, 6.9, and 7.x. +If you do not already have Gradle installed, you can follow the instructions at https://gradle.org. + +Spring Boot dependencies can be declared by using the `org.springframework.boot` `group`. +Typically, your project declares dependencies to one or more <>. +Spring Boot provides a useful <> that can be used to simplify dependency declarations and to create executable jars. + +.Gradle Wrapper +**** +The Gradle Wrapper provides a nice way of "`obtaining`" Gradle when you need to build a project. +It is a small script and library that you commit alongside your code to bootstrap the build process. +See {gradle-docs}/gradle_wrapper.html for details. +**** + +More details on getting started with Spring Boot and Gradle can be found in the {spring-boot-gradle-plugin-docs}#getting-started[Getting Started section] of the Gradle plugin's reference guide. + + + +[[getting-started.installing.cli]] +=== Installing the Spring Boot CLI +The Spring Boot CLI (Command Line Interface) is a command line tool that you can use to quickly prototype with Spring. +It lets you run https://groovy-lang.org/[Groovy] scripts, which means that you have a familiar Java-like syntax without so much boilerplate code. + +You do not need to use the CLI to work with Spring Boot, but it is a quick way to get a Spring application off the ground without an IDE. + + + +[[getting-started.installing.cli.manual-installation]] +==== Manual Installation +You can download the Spring CLI distribution from the Spring software repository: + +* https://repo.spring.io/{spring-boot-artifactory-repo}/org/springframework/boot/spring-boot-cli/{spring-boot-version}/spring-boot-cli-{spring-boot-version}-bin.zip[spring-boot-cli-{spring-boot-version}-bin.zip] +* https://repo.spring.io/{spring-boot-artifactory-repo}/org/springframework/boot/spring-boot-cli/{spring-boot-version}/spring-boot-cli-{spring-boot-version}-bin.tar.gz[spring-boot-cli-{spring-boot-version}-bin.tar.gz] + +Cutting edge +https://repo.spring.io/snapshot/org/springframework/boot/spring-boot-cli/[snapshot distributions] are also available. + +Once downloaded, follow the {github-raw}/spring-boot-project/spring-boot-cli/src/main/content/INSTALL.txt[INSTALL.txt] instructions from the unpacked archive. +In summary, there is a `spring` script (`spring.bat` for Windows) in a `bin/` directory in the `.zip` file. +Alternatively, you can use `java -jar` with the `.jar` file (the script helps you to be sure that the classpath is set correctly). + + + +[[getting-started.installing.cli.sdkman]] +==== Installation with SDKMAN! +SDKMAN! (The Software Development Kit Manager) can be used for managing multiple versions of various binary SDKs, including Groovy and the Spring Boot CLI. +Get SDKMAN! from https://sdkman.io and install Spring Boot by using the following commands: + +[source,shell,indent=0,subs="verbatim,attributes"] +---- + $ sdk install springboot + $ spring --version + Spring CLI v{spring-boot-version} +---- + +If you develop features for the CLI and want access to the version you built, use the following commands: + +[source,shell,indent=0,subs="verbatim,attributes"] +---- + $ sdk install springboot dev /path/to/spring-boot/spring-boot-cli/target/spring-boot-cli-{spring-boot-version}-bin/spring-{spring-boot-version}/ + $ sdk default springboot dev + $ spring --version + Spring CLI v{spring-boot-version} +---- + +The preceding instructions install a local instance of `spring` called the `dev` instance. +It points at your target build location, so every time you rebuild Spring Boot, `spring` is up-to-date. + +You can see it by running the following command: + +[source,shell,indent=0,subs="verbatim,attributes"] +---- + $ sdk ls springboot + + ================================================================================ + Available Springboot Versions + ================================================================================ + > + dev + * {spring-boot-version} + + ================================================================================ + + - local version + * - installed + > - currently in use + ================================================================================ +---- + + + +[[getting-started.installing.cli.homebrew]] +==== OSX Homebrew Installation +If you are on a Mac and use https://brew.sh/[Homebrew], you can install the Spring Boot CLI by using the following commands: + +[source,shell,indent=0,subs="verbatim"] +---- + $ brew tap spring-io/tap + $ brew install spring-boot +---- + +Homebrew installs `spring` to `/usr/local/bin`. + +NOTE: If you do not see the formula, your installation of brew might be out-of-date. +In that case, run `brew update` and try again. + + + +[[getting-started.installing.cli.macports]] +==== MacPorts Installation +If you are on a Mac and use https://www.macports.org/[MacPorts], you can install the Spring Boot CLI by using the following command: + +[source,shell,indent=0,subs="verbatim"] +---- + $ sudo port install spring-boot-cli +---- + + + +[[getting-started.installing.cli.completion]] +==== Command-line Completion +The Spring Boot CLI includes scripts that provide command completion for the https://en.wikipedia.org/wiki/Bash_%28Unix_shell%29[BASH] and https://en.wikipedia.org/wiki/Z_shell[zsh] shells. +You can `source` the script (also named `spring`) in any shell or put it in your personal or system-wide bash completion initialization. +On a Debian system, the system-wide scripts are in `/shell-completion/bash` and all scripts in that directory are executed when a new shell starts. +For example, to run the script manually if you have installed by using SDKMAN!, use the following commands: + +[source,shell,indent=0,subs="verbatim"] +---- + $ . ~/.sdkman/candidates/springboot/current/shell-completion/bash/spring + $ spring + grab help jar run test version +---- + +NOTE: If you install the Spring Boot CLI by using Homebrew or MacPorts, the command-line completion scripts are automatically registered with your shell. + + + +[[getting-started.installing.cli.scoop]] +==== Windows Scoop Installation +If you are on a Windows and use https://scoop.sh/[Scoop], you can install the Spring Boot CLI by using the following commands: + +[indent=0] +---- + > scoop bucket add extras + > scoop install springboot +---- + +Scoop installs `spring` to `~/scoop/apps/springboot/current/bin`. + +NOTE: If you do not see the app manifest, your installation of scoop might be out-of-date. +In that case, run `scoop update` and try again. + + + +[[getting-started.installing.cli.quick-start]] +==== Quick-start Spring CLI Example +You can use the following web application to test your installation. +To start, create a file called `app.groovy`, as follows: + +[source,groovy,indent=0,pending-extract=true,subs="verbatim"] +---- + @RestController + class ThisWillActuallyRun { + + @RequestMapping("/") + String home() { + "Hello World!" + } + + } +---- + +Then run it from a shell, as follows: + +[source,shell,indent=0,subs="verbatim"] +---- + $ spring run app.groovy +---- + +NOTE: The first run of your application is slow, as dependencies are downloaded. +Subsequent runs are much quicker. + +Open `http://localhost:8080` in your favorite web browser. +You should see the following output: + +[indent=0] +---- + Hello World! +---- + diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/getting-started/introducing-spring-boot.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/getting-started/introducing-spring-boot.adoc new file mode 100644 index 000000000000..d22ad7719558 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/getting-started/introducing-spring-boot.adoc @@ -0,0 +1,15 @@ +[[getting-started.introducing-spring-boot]] +== Introducing Spring Boot +Spring Boot helps you to create stand-alone, production-grade Spring-based applications that you can run. +We take an opinionated view of the Spring platform and third-party libraries, so that you can get started with minimum fuss. +Most Spring Boot applications need very little Spring configuration. + +You can use Spring Boot to create Java applications that can be started by using `java -jar` or more traditional war deployments. +We also provide a command line tool that runs "`spring scripts`". + +Our primary goals are: + +* Provide a radically faster and widely accessible getting-started experience for all Spring development. +* Be opinionated out of the box but get out of the way quickly as requirements start to diverge from the defaults. +* Provide a range of non-functional features that are common to large classes of projects (such as embedded servers, security, metrics, health checks, and externalized configuration). +* Absolutely no code generation and no requirement for XML configuration. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/getting-started/system-requirements.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/getting-started/system-requirements.adoc new file mode 100644 index 000000000000..fedf10c75ad0 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/getting-started/system-requirements.adoc @@ -0,0 +1,40 @@ +[[getting-started.system-requirements]] +== System Requirements +Spring Boot {spring-boot-version} requires https://www.java.com[Java 8] and is compatible up to and including Java 17. +{spring-framework-docs}/[Spring Framework {spring-framework-version}] or above is also required. + +Explicit build support is provided for the following build tools: + +|=== +| Build Tool | Version + +| Maven +| 3.5+ + +| Gradle +| 6.8.x, 6.9.x, and 7.x +|=== + + + +[[getting-started.system-requirements.servlet-containers]] +=== Servlet Containers +Spring Boot supports the following embedded servlet containers: + +|=== +| Name | Servlet Version + +| Tomcat 9.0 +| 4.0 + +| Jetty 9.4 +| 3.1 + +| Jetty 10.0 +| 4.0 + +| Undertow 2.0 +| 4.0 +|=== + +You can also deploy Spring Boot applications to any Servlet 3.1+ compatible container. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/getting-started/whats-next.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/getting-started/whats-next.adoc new file mode 100644 index 000000000000..556f98c613c6 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/getting-started/whats-next.adoc @@ -0,0 +1,8 @@ +[[getting-started.whats-next]] +== What to Read Next +Hopefully, this section provided some of the Spring Boot basics and got you on your way to writing your own applications. +If you are a task-oriented type of developer, you might want to jump over to https://spring.io and check out some of the https://spring.io/guides/[getting started] guides that solve specific "`How do I do that with Spring?`" problems. +We also have Spring Boot-specific "`<>`" reference documentation. + +Otherwise, the next logical step is to read _<>_. +If you are really impatient, you could also jump ahead and read about _<>_. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto.adoc new file mode 100644 index 000000000000..76523555c917 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto.adoc @@ -0,0 +1,50 @@ +[[howto]] += "`How-to`" Guides +include::attributes.adoc[] + + + +This section provides answers to some common '`how do I do that...`' questions that often arise when using Spring Boot. +Its coverage is not exhaustive, but it does cover quite a lot. + +If you have a specific problem that we do not cover here, you might want to check out https://stackoverflow.com/tags/spring-boot[stackoverflow.com] to see if someone has already provided an answer. +This is also a great place to ask new questions (please use the `spring-boot` tag). + +We are also more than happy to extend this section. +If you want to add a '`how-to`', send us a {spring-boot-code}[pull request]. + + + +include::howto/application.adoc[] + +include::howto/properties-and-configuration.adoc[] + +include::howto/webserver.adoc[] + +include::howto/spring-mvc.adoc[] + +include::howto/jersey.adoc[] + +include::howto/http-clients.adoc[] + +include::howto/logging.adoc[] + +include::howto/data-access.adoc[] + +include::howto/data-initialization.adoc[] + +include::howto/messaging.adoc[] + +include::howto/batch.adoc[] + +include::howto/actuator.adoc[] + +include::howto/security.adoc[] + +include::howto/hotswapping.adoc[] + +include::howto/testing.adoc[] + +include::howto/build.adoc[] + +include::howto/traditional-deployment.adoc[] diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/actuator.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/actuator.adoc new file mode 100644 index 000000000000..86866b89bdf5 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/actuator.adoc @@ -0,0 +1,72 @@ +[[howto.actuator]] +== Actuator +Spring Boot includes the Spring Boot Actuator. +This section answers questions that often arise from its use. + + + +[[howto.actuator.change-http-port-or-address]] +=== Change the HTTP Port or Address of the Actuator Endpoints +In a standalone application, the Actuator HTTP port defaults to the same as the main HTTP port. +To make the application listen on a different port, set the external property: configprop:management.server.port[]. +To listen on a completely different network address (such as when you have an internal network for management and an external one for user applications), you can also set `management.server.address` to a valid IP address to which the server is able to bind. + +For more detail, see the {spring-boot-actuator-autoconfigure-module-code}/web/server/ManagementServerProperties.java[`ManagementServerProperties`] source code and "`<>`" in the "`Production-ready features`" section. + + + +[[howto.actuator.customize-whitelabel-error-page]] +=== Customize the '`whitelabel`' Error Page +Spring Boot installs a '`whitelabel`' error page that you see in a browser client if you encounter a server error (machine clients consuming JSON and other media types should see a sensible response with the right error code). + +NOTE: Set `server.error.whitelabel.enabled=false` to switch the default error page off. +Doing so restores the default of the servlet container that you are using. +Note that Spring Boot still tries to resolve the error view, so you should probably add your own error page rather than disabling it completely. + +Overriding the error page with your own depends on the templating technology that you use. +For example, if you use Thymeleaf, you can add an `error.html` template. +If you use FreeMarker, you can add an `error.ftlh` template. +In general, you need a `View` that resolves with a name of `error` or a `@Controller` that handles the `/error` path. +Unless you replaced some of the default configuration, you should find a `BeanNameViewResolver` in your `ApplicationContext`, so a `@Bean` named `error` would be one way of doing that. +See {spring-boot-autoconfigure-module-code}/web/servlet/error/ErrorMvcAutoConfiguration.java[`ErrorMvcAutoConfiguration`] for more options. + +See also the section on "`<>`" for details of how to register handlers in the servlet container. + + + +[[howto.actuator.sanitize-sensitive-values]] +=== Sanitize Sensitive Values +Information returned by the `env` and `configprops` endpoints can be somewhat sensitive so keys matching certain patterns are sanitized by default (i.e. their values are replaced by `+******+`). Spring Boot uses sensible defaults for such keys: any key ending with the word "password", "secret", "key", "token", "vcap_services", "sun.java.command" is entirely sanitized. +Additionally, any key that holds the word `credentials` (configured as a regular expression, i.e. `+.*credentials.*+`) as part of the key is also entirely sanitized. + +Furthermore, Spring Boot sanitizes the sensitive portion of URI-like values for keys with one of the following endings: + +- `address` +- `addresses` +- `uri` +- `uris` +- `url` +- `urls` + +The sensitive portion of the URI is identified using the format `://:@:/`. +For example, for the property `myclient.uri=http://user1:password1@localhost:8081`, the resulting sanitized value is +`++http://user1:******@localhost:8081++`. + +The default patterns used by the `env` and `configprops` endpoints can be replaced using configprop:management.endpoint.env.keys-to-sanitize[] and configprop:management.endpoint.configprops.keys-to-sanitize[] respectively. +Alternatively, additional patterns can be configured using configprop:management.endpoint.env.additional-keys-to-sanitize[] and configprop:management.endpoint.configprops.additional-keys-to-sanitize[]. + + + +[[howto.actuator.map-health-indicators-to-metrics]] +=== Map Health Indicators to Micrometer Metrics +Spring Boot health indicators return a `Status` type to indicate the overall system health. +If you want to monitor or alert on levels of health for a particular application, you can export these statuses as metrics via Micrometer. +By default, the status codes "`UP`", "`DOWN`", "`OUT_OF_SERVICE`" and "`UNKNOWN`" are used by Spring Boot. +To export these, you'll need to convert these states to some set of numbers so that they can be used with a Micrometer `Gauge`. + +The following example shows one way to write such an exporter: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/actuator/maphealthindicatorstometrics/MyHealthMetricsExportConfiguration.java[] +---- diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/application.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/application.adoc new file mode 100644 index 000000000000..9f1be91f328c --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/application.adoc @@ -0,0 +1,111 @@ +[[howto.application]] +== Spring Boot Application +This section includes topics relating directly to Spring Boot applications. + + + +[[howto.application.failure-analyzer]] +=== Create Your Own FailureAnalyzer +{spring-boot-module-api}/diagnostics/FailureAnalyzer.html[`FailureAnalyzer`] is a great way to intercept an exception on startup and turn it into a human-readable message, wrapped in a {spring-boot-module-api}/diagnostics/FailureAnalysis.html[`FailureAnalysis`]. +Spring Boot provides such an analyzer for application-context-related exceptions, JSR-303 validations, and more. +You can also create your own. + +`AbstractFailureAnalyzer` is a convenient extension of `FailureAnalyzer` that checks the presence of a specified exception type in the exception to handle. +You can extend from that so that your implementation gets a chance to handle the exception only when it is actually present. +If, for whatever reason, you cannot handle the exception, return `null` to give another implementation a chance to handle the exception. + +`FailureAnalyzer` implementations must be registered in `META-INF/spring.factories`. +The following example registers `ProjectConstraintViolationFailureAnalyzer`: + +[source,properties,indent=0,subs="verbatim"] +---- + org.springframework.boot.diagnostics.FailureAnalyzer=\ + com.example.ProjectConstraintViolationFailureAnalyzer +---- + +NOTE: If you need access to the `BeanFactory` or the `Environment`, your `FailureAnalyzer` can implement `BeanFactoryAware` or `EnvironmentAware` respectively. + + + +[[howto.application.troubleshoot-auto-configuration]] +=== Troubleshoot Auto-configuration +The Spring Boot auto-configuration tries its best to "`do the right thing`", but sometimes things fail, and it can be hard to tell why. + +There is a really useful `ConditionEvaluationReport` available in any Spring Boot `ApplicationContext`. +You can see it if you enable `DEBUG` logging output. +If you use the `spring-boot-actuator` (see <>), there is also a `conditions` endpoint that renders the report in JSON. +Use that endpoint to debug the application and see what features have been added (and which have not been added) by Spring Boot at runtime. + +Many more questions can be answered by looking at the source code and the Javadoc. +When reading the code, remember the following rules of thumb: + +* Look for classes called `+*AutoConfiguration+` and read their sources. + Pay special attention to the `+@Conditional*+` annotations to find out what features they enable and when. + Add `--debug` to the command line or a System property `-Ddebug` to get a log on the console of all the auto-configuration decisions that were made in your app. + In a running application with actuator enabled, look at the `conditions` endpoint (`/actuator/conditions` or the JMX equivalent) for the same information. +* Look for classes that are `@ConfigurationProperties` (such as {spring-boot-autoconfigure-module-code}/web/ServerProperties.java[`ServerProperties`]) and read from there the available external configuration options. + The `@ConfigurationProperties` annotation has a `name` attribute that acts as a prefix to external properties. + Thus, `ServerProperties` has `prefix="server"` and its configuration properties are `server.port`, `server.address`, and others. + In a running application with actuator enabled, look at the `configprops` endpoint. +* Look for uses of the `bind` method on the `Binder` to pull configuration values explicitly out of the `Environment` in a relaxed manner. + It is often used with a prefix. +* Look for `@Value` annotations that bind directly to the `Environment`. +* Look for `@ConditionalOnExpression` annotations that switch features on and off in response to SpEL expressions, normally evaluated with placeholders resolved from the `Environment`. + + + +[[howto.application.customize-the-environment-or-application-context]] +=== Customize the Environment or ApplicationContext Before It Starts +A `SpringApplication` has `ApplicationListeners` and `ApplicationContextInitializers` that are used to apply customizations to the context or environment. +Spring Boot loads a number of such customizations for use internally from `META-INF/spring.factories`. +There is more than one way to register additional customizations: + +* Programmatically, per application, by calling the `addListeners` and `addInitializers` methods on `SpringApplication` before you run it. +* Declaratively, per application, by setting the `context.initializer.classes` or `context.listener.classes` properties. +* Declaratively, for all applications, by adding a `META-INF/spring.factories` and packaging a jar file that the applications all use as a library. + +The `SpringApplication` sends some special `ApplicationEvents` to the listeners (some even before the context is created) and then registers the listeners for events published by the `ApplicationContext` as well. +See "`<>`" in the '`Spring Boot features`' section for a complete list. + +It is also possible to customize the `Environment` before the application context is refreshed by using `EnvironmentPostProcessor`. +Each implementation should be registered in `META-INF/spring.factories`, as shown in the following example: + +[indent=0] +---- + org.springframework.boot.env.EnvironmentPostProcessor=com.example.YourEnvironmentPostProcessor +---- + +The implementation can load arbitrary files and add them to the `Environment`. +For instance, the following example loads a YAML configuration file from the classpath: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/application/customizetheenvironmentorapplicationcontext/MyEnvironmentPostProcessor.java[] +---- + +TIP: The `Environment` has already been prepared with all the usual property sources that Spring Boot loads by default. +It is therefore possible to get the location of the file from the environment. +The preceding example adds the `custom-resource` property source at the end of the list so that a key defined in any of the usual other locations takes precedence. +A custom implementation may define another order. + +CAUTION: While using `@PropertySource` on your `@SpringBootApplication` may seem to be a convenient way to load a custom resource in the `Environment`, we do not recommend it. +Such property sources are not added to the `Environment` until the application context is being refreshed. +This is too late to configure certain properties such as `+logging.*+` and `+spring.main.*+` which are read before refresh begins. + + + +[[howto.application.context-hierarchy]] +=== Build an ApplicationContext Hierarchy (Adding a Parent or Root Context) +You can use the `ApplicationBuilder` class to create parent/child `ApplicationContext` hierarchies. +See "`<>`" in the '`Spring Boot features`' section for more information. + + + +[[howto.application.non-web-application]] +=== Create a Non-web Application +Not all Spring applications have to be web applications (or web services). +If you want to execute some code in a `main` method but also bootstrap a Spring application to set up the infrastructure to use, you can use the `SpringApplication` features of Spring Boot. +A `SpringApplication` changes its `ApplicationContext` class, depending on whether it thinks it needs a web application or not. +The first thing you can do to help it is to leave server-related dependencies (e.g. servlet API) off the classpath. +If you cannot do that (for example, you run two applications from the same code base) then you can explicitly call `setWebApplicationType(WebApplicationType.NONE)` on your `SpringApplication` instance or set the `applicationContextClass` property (through the Java API or with external properties). +Application code that you want to run as your business logic can be implemented as a `CommandLineRunner` and dropped into the context as a `@Bean` definition. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/batch.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/batch.adoc new file mode 100644 index 000000000000..7a82ca6c9254 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/batch.adoc @@ -0,0 +1,59 @@ +[[howto.batch]] +== Batch Applications +A number of questions often arise when people use Spring Batch from within a Spring Boot application. +This section addresses those questions. + + + +[[howto.batch.specifying-a-data-source]] +=== Specifying a Batch Data Source +By default, batch applications require a `DataSource` to store job details. +Spring Batch expects a single `DataSource` by default. +To have it use a `DataSource` other than the application’s main `DataSource`, declare a `DataSource` bean, annotating its `@Bean` method with `@BatchDataSource`. +If you do so and want two data sources, remember to mark the other one `@Primary`. +To take greater control, implement `BatchConfigurer`. +See {spring-batch-api}/core/configuration/annotation/EnableBatchProcessing.html[The Javadoc of `@EnableBatchProcessing`] for more details. + +For more info about Spring Batch, see the {spring-batch}[Spring Batch project page]. + + + +[[howto.batch.running-jobs-on-startup]] +=== Running Spring Batch Jobs on Startup +Spring Batch auto-configuration is enabled by adding `@EnableBatchProcessing` to one of your `@Configuration` classes. + +By default, it executes *all* `Jobs` in the application context on startup (see {spring-boot-autoconfigure-module-code}/batch/JobLauncherApplicationRunner.java[`JobLauncherApplicationRunner`] for details). +You can narrow down to a specific job or jobs by specifying `spring.batch.job.names` (which takes a comma-separated list of job name patterns). + +See {spring-boot-autoconfigure-module-code}/batch/BatchAutoConfiguration.java[BatchAutoConfiguration] and {spring-batch-api}/core/configuration/annotation/EnableBatchProcessing.html[@EnableBatchProcessing] for more details. + + + +[[howto.batch.running-from-the-command-line]] +=== Running from the Command Line +Spring Boot converts any command line argument starting with `--` to a property to add to the `Environment`, see <>. +This should not be used to pass arguments to batch jobs. +To specify batch arguments on the command line, use the regular format (i.e. without `--`), as shown in the following example: + +[source,shell,indent=0,subs="verbatim"] +---- + $ java -jar myapp.jar someParameter=someValue anotherParameter=anotherValue +---- + +If you specify a property of the `Environment` on the command line, it is ignored by the job. +Consider the following command: + +[source,shell,indent=0,subs="verbatim"] +---- + $ java -jar myapp.jar --server.port=7070 someParameter=someValue +---- + +This provides only one argument to the batch job: `someParameter=someValue`. + + + +[[howto.batch.storing-job-repository]] +=== Storing the Job Repository +Spring Batch requires a data store for the `Job` repository. +If you use Spring Boot, you must use an actual database. +Note that it can be an in-memory database, see {spring-batch-docs}job.html#configuringJobRepository[Configuring a Job Repository]. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/build.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/build.adoc new file mode 100644 index 000000000000..8f6eac81229f --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/build.adoc @@ -0,0 +1,298 @@ +[[howto.build]] +== Build +Spring Boot includes build plugins for Maven and Gradle. +This section answers common questions about these plugins. + + + +[[howto.build.generate-info]] +=== Generate Build Information +Both the Maven plugin and the Gradle plugin allow generating build information containing the coordinates, name, and version of the project. +The plugins can also be configured to add additional properties through configuration. +When such a file is present, Spring Boot auto-configures a `BuildProperties` bean. + +To generate build information with Maven, add an execution for the `build-info` goal, as shown in the following example: + +[source,xml,indent=0,subs="verbatim,attributes"] +---- + + + + org.springframework.boot + spring-boot-maven-plugin + {spring-boot-version} + + + + build-info + + + + + + +---- + +TIP: See the {spring-boot-maven-plugin-docs}#goals-build-info[Spring Boot Maven Plugin documentation] for more details. + +The following example does the same with Gradle: + +[source,gradle,indent=0,subs="verbatim"] +---- + springBoot { + buildInfo() + } +---- + +TIP: See the {spring-boot-gradle-plugin-docs}#integrating-with-actuator-build-info[Spring Boot Gradle Plugin documentation] for more details. + + + +[[howto.build.generate-git-info]] +=== Generate Git Information +Both Maven and Gradle allow generating a `git.properties` file containing information about the state of your `git` source code repository when the project was built. + +For Maven users, the `spring-boot-starter-parent` POM includes a pre-configured plugin to generate a `git.properties` file. +To use it, add the following declaration for the https://github.com/git-commit-id/git-commit-id-maven-plugin[`Git Commit Id Plugin`] to your POM: + +[source,xml,indent=0,subs="verbatim"] +---- + + + + pl.project13.maven + git-commit-id-plugin + + + +---- + +Gradle users can achieve the same result by using the https://plugins.gradle.org/plugin/com.gorylenko.gradle-git-properties[`gradle-git-properties`] plugin, as shown in the following example: + +[source,gradle,indent=0,subs="verbatim"] +---- + plugins { + id "com.gorylenko.gradle-git-properties" version "2.3.2" + } +---- + +Both the Maven and Gradle plugins allow the properties that are included in `git.properties` to be configured. + +TIP: The commit time in `git.properties` is expected to match the following format: `yyyy-MM-dd'T'HH:mm:ssZ`. +This is the default format for both plugins listed above. +Using this format lets the time be parsed into a `Date` and its format, when serialized to JSON, to be controlled by Jackson's date serialization configuration settings. + + + +[[howto.build.customize-dependency-versions]] +=== Customize Dependency Versions +The `spring-boot-dependencies` POM manages the versions of common dependencies. +The Spring Boot plugins for Maven and Gradle allow these managed dependency versions to be customized using build properties. + +WARNING: Each Spring Boot release is designed and tested against this specific set of third-party dependencies. +Overriding versions may cause compatibility issues. + +To override dependency versions with Maven, see {spring-boot-maven-plugin-docs}#using[this section] of the Maven plugin's documentation. + +To override dependency versions in Gradle, see {spring-boot-gradle-plugin-docs}#managing-dependencies-dependency-management-plugin-customizing[this section] of the Gradle plugin's documentation. + + + +[[howto.build.create-an-executable-jar-with-maven]] +=== Create an Executable JAR with Maven +The `spring-boot-maven-plugin` can be used to create an executable "`fat`" JAR. +If you use the `spring-boot-starter-parent` POM, you can declare the plugin and your jars are repackaged as follows: + +[source,xml,indent=0,subs="verbatim"] +---- + + + + org.springframework.boot + spring-boot-maven-plugin + + + +---- + +If you do not use the parent POM, you can still use the plugin. +However, you must additionally add an `` section, as follows: + +[source,xml,indent=0,subs="verbatim"] +---- + + + + org.springframework.boot + spring-boot-maven-plugin + {spring-boot-version} + + + + repackage + + + + + + +---- + +See the {spring-boot-maven-plugin-docs}#repackage[plugin documentation] for full usage details. + + + +[[howto.build.use-a-spring-boot-application-as-dependency]] +=== Use a Spring Boot Application as a Dependency +Like a war file, a Spring Boot application is not intended to be used as a dependency. +If your application contains classes that you want to share with other projects, the recommended approach is to move that code into a separate module. +The separate module can then be depended upon by your application and other projects. + +If you cannot rearrange your code as recommended above, Spring Boot's Maven and Gradle plugins must be configured to produce a separate artifact that is suitable for use as a dependency. +The executable archive cannot be used as a dependency as the <> packages application classes in `BOOT-INF/classes`. +This means that they cannot be found when the executable jar is used as a dependency. + +To produce the two artifacts, one that can be used as a dependency and one that is executable, a classifier must be specified. +This classifier is applied to the name of the executable archive, leaving the default archive for use as a dependency. + +To configure a classifier of `exec` in Maven, you can use the following configuration: + +[source,xml,indent=0,subs="verbatim"] +---- + + + + org.springframework.boot + spring-boot-maven-plugin + + exec + + + + +---- + + + +[[howto.build.extract-specific-libraries-when-an-executable-jar-runs]] +=== Extract Specific Libraries When an Executable Jar Runs +Most nested libraries in an executable jar do not need to be unpacked in order to run. +However, certain libraries can have problems. +For example, JRuby includes its own nested jar support, which assumes that the `jruby-complete.jar` is always directly available as a file in its own right. + +To deal with any problematic libraries, you can flag that specific nested jars should be automatically unpacked when the executable jar first runs. +Such nested jars are written beneath the temporary directory identified by the `java.io.tmpdir` system property. + +WARNING: Care should be taken to ensure that your operating system is configured so that it will not delete the jars that have been unpacked to the temporary directory while the application is still running. + +For example, to indicate that JRuby should be flagged for unpacking by using the Maven Plugin, you would add the following configuration: + +[source,xml,indent=0,subs="verbatim"] +---- + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.jruby + jruby-complete + + + + + + +---- + + + +[[howto.build.create-a-nonexecutable-jar]] +=== Create a Non-executable JAR with Exclusions +Often, if you have an executable and a non-executable jar as two separate build products, the executable version has additional configuration files that are not needed in a library jar. +For example, the `application.yml` configuration file might be excluded from the non-executable JAR. + +In Maven, the executable jar must be the main artifact and you can add a classified jar for the library, as follows: + +[source,xml,indent=0,subs="verbatim"] +---- + + + + org.springframework.boot + spring-boot-maven-plugin + + + maven-jar-plugin + + + lib + package + + jar + + + lib + + application.yml + + + + + + + +---- + + + +[[howto.build.remote-debug-maven]] +=== Remote Debug a Spring Boot Application Started with Maven +To attach a remote debugger to a Spring Boot application that was started with Maven, you can use the `jvmArguments` property of the {spring-boot-maven-plugin-docs}[maven plugin]. + +See {spring-boot-maven-plugin-docs}#run-example-debug[this example] for more details. + + + +[[howto.build.build-an-executable-archive-with-ant-without-using-spring-boot-antlib]] +=== Build an Executable Archive from Ant without Using spring-boot-antlib +To build with Ant, you need to grab dependencies, compile, and then create a jar or war archive. +To make it executable, you can either use the `spring-boot-antlib` module or you can follow these instructions: + +. If you are building a jar, package the application's classes and resources in a nested `BOOT-INF/classes` directory. + If you are building a war, package the application's classes in a nested `WEB-INF/classes` directory as usual. +. Add the runtime dependencies in a nested `BOOT-INF/lib` directory for a jar or `WEB-INF/lib` for a war. + Remember *not* to compress the entries in the archive. +. Add the `provided` (embedded container) dependencies in a nested `BOOT-INF/lib` directory for a jar or `WEB-INF/lib-provided` for a war. + Remember *not* to compress the entries in the archive. +. Add the `spring-boot-loader` classes at the root of the archive (so that the `Main-Class` is available). +. Use the appropriate launcher (such as `JarLauncher` for a jar file) as a `Main-Class` attribute in the manifest and specify the other properties it needs as manifest entries -- principally, by setting a `Start-Class` property. + +The following example shows how to build an executable archive with Ant: + +[source,xml,indent=0,subs="verbatim"] +---- + + + + + + + + + + + + + + + + + + + + + +---- diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-access.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-access.adoc new file mode 100644 index 000000000000..3a5357475a0a --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-access.adoc @@ -0,0 +1,405 @@ +[[howto.data-access]] +== Data Access +Spring Boot includes a number of starters for working with data sources. +This section answers questions related to doing so. + + + +[[howto.data-access.configure-custom-datasource]] +=== Configure a Custom DataSource +To configure your own `DataSource`, define a `@Bean` of that type in your configuration. +Spring Boot reuses your `DataSource` anywhere one is required, including database initialization. +If you need to externalize some settings, you can bind your `DataSource` to the environment (see "`<>`"). + +The following example shows how to define a data source in a bean: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/dataaccess/configurecustomdatasource/custom/MyDataSourceConfiguration.java[] +---- + +The following example shows how to define a data source by setting properties: + +[source,yaml,indent=0,subs="verbatim",configblocks] +---- + app: + datasource: + url: "jdbc:h2:mem:mydb" + username: "sa" + pool-size: 30 +---- + +Assuming that `SomeDataSource` has regular JavaBean properties for the URL, the username, and the pool size, these settings are bound automatically before the `DataSource` is made available to other components. + +Spring Boot also provides a utility builder class, called `DataSourceBuilder`, that can be used to create one of the standard data sources (if it is on the classpath). +The builder can detect the one to use based on what's available on the classpath. +It also auto-detects the driver based on the JDBC URL. + +The following example shows how to create a data source by using a `DataSourceBuilder`: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/dataaccess/configurecustomdatasource/builder/MyDataSourceConfiguration.java[] +---- + +To run an app with that `DataSource`, all you need is the connection information. +Pool-specific settings can also be provided. +Check the implementation that is going to be used at runtime for more details. + +The following example shows how to define a JDBC data source by setting properties: + +[source,yaml,indent=0,subs="verbatim",configblocks] +---- + app: + datasource: + url: "jdbc:mysql://localhost/test" + username: "dbuser" + password: "dbpass" + pool-size: 30 +---- + +However, there is a catch. +Because the actual type of the connection pool is not exposed, no keys are generated in the metadata for your custom `DataSource` and no completion is available in your IDE (because the `DataSource` interface exposes no properties). +Also, if you happen to have Hikari on the classpath, this basic setup does not work, because Hikari has no `url` property (but does have a `jdbcUrl` property). +In that case, you must rewrite your configuration as follows: + +[source,yaml,indent=0,subs="verbatim",configblocks] +---- + app: + datasource: + jdbc-url: "jdbc:mysql://localhost/test" + username: "dbuser" + password: "dbpass" + pool-size: 30 +---- + +You can fix that by forcing the connection pool to use and return a dedicated implementation rather than `DataSource`. +You cannot change the implementation at runtime, but the list of options will be explicit. + +The following example shows how create a `HikariDataSource` with `DataSourceBuilder`: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/dataaccess/configurecustomdatasource/simple/MyDataSourceConfiguration.java[] +---- + +You can even go further by leveraging what `DataSourceProperties` does for you -- that is, by providing a default embedded database with a sensible username and password if no URL is provided. +You can easily initialize a `DataSourceBuilder` from the state of any `DataSourceProperties` object, so you could also inject the DataSource that Spring Boot creates automatically. +However, that would split your configuration into two namespaces: `url`, `username`, `password`, `type`, and `driver` on `spring.datasource` and the rest on your custom namespace (`app.datasource`). +To avoid that, you can redefine a custom `DataSourceProperties` on your custom namespace, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/dataaccess/configurecustomdatasource/configurable/MyDataSourceConfiguration.java[] +---- + +This setup puts you _in sync_ with what Spring Boot does for you by default, except that a dedicated connection pool is chosen (in code) and its settings are exposed in the `app.datasource.configuration` sub namespace. +Because `DataSourceProperties` is taking care of the `url`/`jdbcUrl` translation for you, you can configure it as follows: + +[source,yaml,indent=0,subs="verbatim",configblocks] +---- + app: + datasource: + url: "jdbc:mysql://localhost/test" + username: "dbuser" + password: "dbpass" + configuration: + maximum-pool-size: 30 +---- + +TIP: Spring Boot will expose Hikari-specific settings to `spring.datasource.hikari`. +This example uses a more generic `configuration` sub namespace as the example does not support multiple datasource implementations. + +NOTE: Because your custom configuration chooses to go with Hikari, `app.datasource.type` has no effect. +In practice, the builder is initialized with whatever value you might set there and then overridden by the call to `.type()`. + +See "`<>`" in the "`Spring Boot features`" section and the {spring-boot-autoconfigure-module-code}/jdbc/DataSourceAutoConfiguration.java[`DataSourceAutoConfiguration`] class for more details. + + + +[[howto.data-access.configure-two-datasources]] +=== Configure Two DataSources +If you need to configure multiple data sources, you can apply the same tricks that are described in the previous section. +You must, however, mark one of the `DataSource` instances as `@Primary`, because various auto-configurations down the road expect to be able to get one by type. + +If you create your own `DataSource`, the auto-configuration backs off. +In the following example, we provide the _exact_ same feature set as the auto-configuration provides on the primary data source: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/dataaccess/configuretwodatasources/MyDataSourcesConfiguration.java[] +---- + +TIP: `firstDataSourceProperties` has to be flagged as `@Primary` so that the database initializer feature uses your copy (if you use the initializer). + +Both data sources are also bound for advanced customizations. +For instance, you could configure them as follows: + +[source,yaml,indent=0,subs="verbatim",configblocks] +---- + app: + datasource: + first: + url: "jdbc:mysql://localhost/first" + username: "dbuser" + password: "dbpass" + configuration: + maximum-pool-size: 30 + + second: + url: "jdbc:mysql://localhost/second" + username: "dbuser" + password: "dbpass" + max-total: 30 +---- + +You can apply the same concept to the secondary `DataSource` as well, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/dataaccess/configuretwodatasources/MyCompleteDataSourcesConfiguration.java[] +---- + +The preceding example configures two data sources on custom namespaces with the same logic as Spring Boot would use in auto-configuration. +Note that each `configuration` sub namespace provides advanced settings based on the chosen implementation. + + + +[[howto.data-access.spring-data-repositories]] +=== Use Spring Data Repositories +Spring Data can create implementations of `@Repository` interfaces of various flavors. +Spring Boot handles all of that for you, as long as those `@Repositories` are included in the same package (or a sub-package) of your `@EnableAutoConfiguration` class. + +For many applications, all you need is to put the right Spring Data dependencies on your classpath. +There is a `spring-boot-starter-data-jpa` for JPA, `spring-boot-starter-data-mongodb` for Mongodb, etc. +To get started, create some repository interfaces to handle your `@Entity` objects. + +Spring Boot tries to guess the location of your `@Repository` definitions, based on the `@EnableAutoConfiguration` it finds. +To get more control, use the `@EnableJpaRepositories` annotation (from Spring Data JPA). + +For more about Spring Data, see the {spring-data}[Spring Data project page]. + + + +[[howto.data-access.separate-entity-definitions-from-spring-configuration]] +=== Separate @Entity Definitions from Spring Configuration +Spring Boot tries to guess the location of your `@Entity` definitions, based on the `@EnableAutoConfiguration` it finds. +To get more control, you can use the `@EntityScan` annotation, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/dataaccess/separateentitydefinitionsfromspringconfiguration/MyApplication.java[] +---- + + + +[[howto.data-access.jpa-properties]] +=== Configure JPA Properties +Spring Data JPA already provides some vendor-independent configuration options (such as those for SQL logging), and Spring Boot exposes those options and a few more for Hibernate as external configuration properties. +Some of them are automatically detected according to the context so you should not have to set them. + +The `spring.jpa.hibernate.ddl-auto` is a special case, because, depending on runtime conditions, it has different defaults. +If an embedded database is used and no schema manager (such as Liquibase or Flyway) is handling the `DataSource`, it defaults to `create-drop`. +In all other cases, it defaults to `none`. + +The dialect to use is detected by the JPA provider. +If you prefer to set the dialect yourself, set the configprop:spring.jpa.database-platform[] property. + +The most common options to set are shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + jpa: + hibernate: + naming: + physical-strategy: "com.example.MyPhysicalNamingStrategy" + show-sql: true +---- + +In addition, all properties in `+spring.jpa.properties.*+` are passed through as normal JPA properties (with the prefix stripped) when the local `EntityManagerFactory` is created. + +[WARNING] +==== +You need to ensure that names defined under `+spring.jpa.properties.*+` exactly match those expected by your JPA provider. +Spring Boot will not attempt any kind of relaxed binding for these entries. + +For example, if you want to configure Hibernate's batch size you must use `+spring.jpa.properties.hibernate.jdbc.batch_size+`. +If you use other forms, such as `batchSize` or `batch-size`, Hibernate will not apply the setting. +==== + +TIP: If you need to apply advanced customization to Hibernate properties, consider registering a `HibernatePropertiesCustomizer` bean that will be invoked prior to creating the `EntityManagerFactory`. +This takes precedence to anything that is applied by the auto-configuration. + + + +[[howto.data-access.configure-hibernate-naming-strategy]] +=== Configure Hibernate Naming Strategy +Hibernate uses {hibernate-docs}#naming[two different naming strategies] to map names from the object model to the corresponding database names. +The fully qualified class name of the physical and the implicit strategy implementations can be configured by setting the `spring.jpa.hibernate.naming.physical-strategy` and `spring.jpa.hibernate.naming.implicit-strategy` properties, respectively. +Alternatively, if `ImplicitNamingStrategy` or `PhysicalNamingStrategy` beans are available in the application context, Hibernate will be automatically configured to use them. + +By default, Spring Boot configures the physical naming strategy with `SpringPhysicalNamingStrategy`. +This implementation provides the same table structure as Hibernate 4: all dots are replaced by underscores and camel casing is replaced by underscores as well. Additionally, by default, all table names are generated in lower case. For example, a `TelephoneNumber` entity is mapped to the `telephone_number` table. If your schema requires mixed-case identifiers, define a custom `SpringPhysicalNamingStrategy` bean, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/dataaccess/configurehibernatenamingstrategy/spring/MyHibernateConfiguration.java[] +---- + +If you prefer to use Hibernate 5's default instead, set the following property: + +[indent=0,properties,subs="verbatim"] +---- + spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl +---- + +Alternatively, you can configure the following bean: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/dataaccess/configurehibernatenamingstrategy/standard/MyHibernateConfiguration.java[] +---- + +See {spring-boot-autoconfigure-module-code}/orm/jpa/HibernateJpaAutoConfiguration.java[`HibernateJpaAutoConfiguration`] and {spring-boot-autoconfigure-module-code}/orm/jpa/JpaBaseConfiguration.java[`JpaBaseConfiguration`] for more details. + + + +[[howto.data-access.configure-hibernate-second-level-caching]] +=== Configure Hibernate Second-Level Caching +Hibernate {hibernate-docs}#caching[second-level cache] can be configured for a range of cache providers. +Rather than configuring Hibernate to lookup the cache provider again, it is better to provide the one that is available in the context whenever possible. + +To do this with JCache, first make sure that `org.hibernate:hibernate-jcache` is available on the classpath. +Then, add a `HibernatePropertiesCustomizer` bean as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/dataaccess/configurehibernatesecondlevelcaching/MyHibernateSecondLevelCacheConfiguration.java[] +---- + +This customizer will configure Hibernate to use the same `CacheManager` as the one that the application uses. +It is also possible to use separate `CacheManager` instances. +For details, refer to {hibernate-docs}#caching-provider-jcache[the Hibernate user guide]. + + + +[[howto.data-access.dependency-injection-in-hibernate-components]] +=== Use Dependency Injection in Hibernate Components +By default, Spring Boot registers a `BeanContainer` implementation that uses the `BeanFactory` so that converters and entity listeners can use regular dependency injection. + +You can disable or tune this behavior by registering a `HibernatePropertiesCustomizer` that removes or changes the `hibernate.resource.beans.container` property. + + + +[[howto.data-access.use-custom-entity-manager]] +=== Use a Custom EntityManagerFactory +To take full control of the configuration of the `EntityManagerFactory`, you need to add a `@Bean` named '`entityManagerFactory`'. +Spring Boot auto-configuration switches off its entity manager in the presence of a bean of that type. + + + +[[howto.data-access.use-multiple-entity-managers]] +[[howto.data-access.use-multiple-entity-managers]] +=== Using Multiple EntityManagerFactories +If you need to use JPA against multiple data sources, you likely need one `EntityManagerFactory` per data source. +The `LocalContainerEntityManagerFactoryBean` from Spring ORM allows you to configure an `EntityManagerFactory` for your needs. +You can also reuse `JpaProperties` to bind settings for each `EntityManagerFactory`, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/dataaccess/usemultipleentitymanagers/MyEntityManagerFactoryConfiguration.java[] +---- + +The example above creates an `EntityManagerFactory` using a `DataSource` bean named `firstDataSource`. +It scans entities located in the same package as `Order`. +It is possible to map additional JPA properties using the `app.first.jpa` namespace. + +NOTE: When you create a bean for `LocalContainerEntityManagerFactoryBean` yourself, any customization that was applied during the creation of the auto-configured `LocalContainerEntityManagerFactoryBean` is lost. +For example, in case of Hibernate, any properties under the `spring.jpa.hibernate` prefix will not be automatically applied to your `LocalContainerEntityManagerFactoryBean`. +If you were relying on these properties for configuring things like the naming strategy or the DDL mode, you will need to explicitly configure that when creating the `LocalContainerEntityManagerFactoryBean` bean. + +You should provide a similar configuration for any additional data sources for which you need JPA access. +To complete the picture, you need to configure a `JpaTransactionManager` for each `EntityManagerFactory` as well. +Alternatively, you might be able to use a JTA transaction manager that spans both. + +If you use Spring Data, you need to configure `@EnableJpaRepositories` accordingly, as shown in the following examples: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/dataaccess/usemultipleentitymanagers/OrderConfiguration.java[] +---- + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/dataaccess/usemultipleentitymanagers/CustomerConfiguration.java[] +---- + + + +[[howto.data-access.use-traditional-persistence-xml]] +=== Use a Traditional persistence.xml File +Spring Boot will not search for or use a `META-INF/persistence.xml` by default. +If you prefer to use a traditional `persistence.xml`, you need to define your own `@Bean` of type `LocalEntityManagerFactoryBean` (with an ID of '`entityManagerFactory`') and set the persistence unit name there. + +See {spring-boot-autoconfigure-module-code}/orm/jpa/JpaBaseConfiguration.java[`JpaBaseConfiguration`] for the default settings. + + + +[[howto.data-access.use-spring-data-jpa-and-mongo-repositories]] +=== Use Spring Data JPA and Mongo Repositories +Spring Data JPA and Spring Data Mongo can both automatically create `Repository` implementations for you. +If they are both present on the classpath, you might have to do some extra configuration to tell Spring Boot which repositories to create. +The most explicit way to do that is to use the standard Spring Data `+@EnableJpaRepositories+` and `+@EnableMongoRepositories+` annotations and provide the location of your `Repository` interfaces. + +There are also flags (`+spring.data.*.repositories.enabled+` and `+spring.data.*.repositories.type+`) that you can use to switch the auto-configured repositories on and off in external configuration. +Doing so is useful, for instance, in case you want to switch off the Mongo repositories and still use the auto-configured `MongoTemplate`. + +The same obstacle and the same features exist for other auto-configured Spring Data repository types (Elasticsearch, Solr, and others). +To work with them, change the names of the annotations and flags accordingly. + + + +[[howto.data-access.customize-spring-data-web-support]] +=== Customize Spring Data's Web Support +Spring Data provides web support that simplifies the use of Spring Data repositories in a web application. +Spring Boot provides properties in the `spring.data.web` namespace for customizing its configuration. +Note that if you are using Spring Data REST, you must use the properties in the `spring.data.rest` namespace instead. + + + +[[howto.data-access.exposing-spring-data-repositories-as-rest]] +=== Expose Spring Data Repositories as REST Endpoint +Spring Data REST can expose the `Repository` implementations as REST endpoints for you, +provided Spring MVC has been enabled for the application. + +Spring Boot exposes a set of useful properties (from the `spring.data.rest` namespace) that customize the {spring-data-rest-api}/core/config/RepositoryRestConfiguration.html[`RepositoryRestConfiguration`]. +If you need to provide additional customization, you should use a {spring-data-rest-api}/webmvc/config/RepositoryRestConfigurer.html[`RepositoryRestConfigurer`] bean. + +NOTE: If you do not specify any order on your custom `RepositoryRestConfigurer`, it runs after the one Spring Boot uses internally. +If you need to specify an order, make sure it is higher than 0. + + + +[[howto.data-access.configure-a-component-that-is-used-by-jpa]] +=== Configure a Component that is Used by JPA +If you want to configure a component that JPA uses, then you need to ensure that the component is initialized before JPA. +When the component is auto-configured, Spring Boot takes care of this for you. +For example, when Flyway is auto-configured, Hibernate is configured to depend upon Flyway so that Flyway has a chance to initialize the database before Hibernate tries to use it. + +If you are configuring a component yourself, you can use an `EntityManagerFactoryDependsOnPostProcessor` subclass as a convenient way of setting up the necessary dependencies. +For example, if you use Hibernate Search with Elasticsearch as its index manager, any `EntityManagerFactory` beans must be configured to depend on the `elasticsearchClient` bean, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/dataaccess/configureacomponentthatisusedbyjpa/ElasticsearchEntityManagerFactoryDependsOnPostProcessor.java[] +---- + + + +[[howto.data-access.configure-jooq-with-multiple-datasources]] +=== Configure jOOQ with Two DataSources +If you need to use jOOQ with multiple data sources, you should create your own `DSLContext` for each one. +Refer to {spring-boot-autoconfigure-module-code}/jooq/JooqAutoConfiguration.java[JooqAutoConfiguration] for more details. + +TIP: In particular, `JooqExceptionTranslator` and `SpringTransactionProvider` can be reused to provide similar features to what the auto-configuration does with a single `DataSource`. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-initialization.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-initialization.adoc new file mode 100644 index 000000000000..a1ac1d721951 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-initialization.adoc @@ -0,0 +1,219 @@ +[[howto.data-initialization]] +== Database Initialization +An SQL database can be initialized in different ways depending on what your stack is. +Of course, you can also do it manually, provided the database is a separate process. +It is recommended to use a single mechanism for schema generation. + + + +[[howto.data-initialization.using-jpa]] +=== Initialize a Database Using JPA +JPA has features for DDL generation, and these can be set up to run on startup against the database. +This is controlled through two external properties: + +* `spring.jpa.generate-ddl` (boolean) switches the feature on and off and is vendor independent. +* `spring.jpa.hibernate.ddl-auto` (enum) is a Hibernate feature that controls the behavior in a more fine-grained way. + This feature is described in more detail later in this guide. + + + +[[howto.data-initialization.using-hibernate]] +=== Initialize a Database Using Hibernate +You can set `spring.jpa.hibernate.ddl-auto` explicitly and the standard Hibernate property values are `none`, `validate`, `update`, `create`, and `create-drop`. +Spring Boot chooses a default value for you based on whether it thinks your database is embedded. +It defaults to `create-drop` if no schema manager has been detected or `none` in all other cases. +An embedded database is detected by looking at the `Connection` type and JDBC url. +`hsqldb`, `h2`, and `derby` are candidates, and others are not. +Be careful when switching from in-memory to a '`real`' database that you do not make assumptions about the existence of the tables and data in the new platform. +You either have to set `ddl-auto` explicitly or use one of the other mechanisms to initialize the database. + +NOTE: You can output the schema creation by enabling the `org.hibernate.SQL` logger. +This is done for you automatically if you enable the <>. + +In addition, a file named `import.sql` in the root of the classpath is executed on startup if Hibernate creates the schema from scratch (that is, if the `ddl-auto` property is set to `create` or `create-drop`). +This can be useful for demos and for testing if you are careful but is probably not something you want to be on the classpath in production. +It is a Hibernate feature (and has nothing to do with Spring). + + + +[[howto.data-initialization.using-basic-sql-scripts]] +=== Initialize a Database Using Basic SQL Scripts +Spring Boot can automatically create the schema (DDL scripts) of your JDBC `DataSource` or R2DBC `ConnectionFactory` and initialize it (DML scripts). +It loads SQL from the standard root classpath locations: `schema.sql` and `data.sql`, respectively. +In addition, Spring Boot processes the `schema-$\{platform}.sql` and `data-$\{platform}.sql` files (if present), where `platform` is the value of configprop:spring.sql.init.platform[]. +This allows you to switch to database-specific scripts if necessary. +For example, you might choose to set it to the vendor name of the database (`hsqldb`, `h2`, `oracle`, `mysql`, `postgresql`, and so on). +By default, SQL database initialization is only performed when using an embedded in-memory database. +To always initialize an SQL database, irrespective of its type, set configprop:spring.sql.init.mode[] to `always`. +Similarly, to disable initialization, set configprop:spring.sql.init.mode[] to `never`. +By default, Spring Boot enables the fail-fast feature of its script-based database initializer. +This means that, if the scripts cause exceptions, the application fails to start. +You can tune that behavior by setting configprop:spring.sql.init.continue-on-error[]. + +Script-based `DataSource` initialization is performed, by default, before any JPA `EntityManagerFactory` beans are created. +`schema.sql` can be used to create the schema for JPA-managed entities and `data.sql` can be used to populate it. +While we do not recommend using multiple data source initialization technologies, if you want script-based `DataSource` initialization to be able to build upon the schema creation performed by Hibernate, set configprop:spring.jpa.defer-datasource-initialization[] to `true`. +This will defer data source initialization until after any `EntityManagerFactory` beans have been created and initialized. +`schema.sql` can then be used to make additions to any schema creation performed by Hibernate and `data.sql` can be used to populate it. + +If you are using a <>, like Flyway or Liquibase, you should use them alone to create and initialize the schema. +Using the basic `schema.sql` and `data.sql` scripts alongside Flyway or Liquibase is not recommended and support will be removed in a future release. + + + +[[howto.data-initialization.batch]] +=== Initialize a Spring Batch Database +If you use Spring Batch, it comes pre-packaged with SQL initialization scripts for most popular database platforms. +Spring Boot can detect your database type and execute those scripts on startup. +If you use an embedded database, this happens by default. +You can also enable it for any database type, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + batch: + jdbc: + initialize-schema: "always" +---- + +You can also switch off the initialization explicitly by setting `spring.batch.jdbc.initialize-schema` to `never`. + + + +[[howto.data-initialization.migration-tool]] +=== Use a Higher-level Database Migration Tool +Spring Boot supports two higher-level migration tools: https://flywaydb.org/[Flyway] and https://www.liquibase.org/[Liquibase]. + + + +[[howto.data-initialization.migration-tool.flyway]] +==== Execute Flyway Database Migrations on Startup +To automatically run Flyway database migrations on startup, add the `org.flywaydb:flyway-core` to your classpath. + +Typically, migrations are scripts in the form `V__.sql` (with `` an underscore-separated version, such as '`1`' or '`2_1`'). +By default, they are in a directory called `classpath:db/migration`, but you can modify that location by setting `spring.flyway.locations`. +This is a comma-separated list of one or more `classpath:` or `filesystem:` locations. +For example, the following configuration would search for scripts in both the default classpath location and the `/opt/migration` directory: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + flyway: + locations: "classpath:db/migration,filesystem:/opt/migration" +---- + +You can also add a special `\{vendor}` placeholder to use vendor-specific scripts. +Assume the following: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + flyway: + locations: "classpath:db/migration/{vendor}" +---- + +Rather than using `db/migration`, the preceding configuration sets the directory to use according to the type of the database (such as `db/migration/mysql` for MySQL). +The list of supported databases is available in {spring-boot-module-code}/jdbc/DatabaseDriver.java[`DatabaseDriver`]. + +Migrations can also be written in Java. +Flyway will be auto-configured with any beans that implement `JavaMigration`. + +{spring-boot-autoconfigure-module-code}/flyway/FlywayProperties.java[`FlywayProperties`] provides most of Flyway's settings and a small set of additional properties that can be used to disable the migrations or switch off the location checking. +If you need more control over the configuration, consider registering a `FlywayConfigurationCustomizer` bean. + +Spring Boot calls `Flyway.migrate()` to perform the database migration. +If you would like more control, provide a `@Bean` that implements {spring-boot-autoconfigure-module-code}/flyway/FlywayMigrationStrategy.java[`FlywayMigrationStrategy`]. + +Flyway supports SQL and Java https://flywaydb.org/documentation/concepts/callbacks[callbacks]. +To use SQL-based callbacks, place the callback scripts in the `classpath:db/migration` directory. +To use Java-based callbacks, create one or more beans that implement `Callback`. +Any such beans are automatically registered with `Flyway`. +They can be ordered by using `@Order` or by implementing `Ordered`. +Beans that implement the deprecated `FlywayCallback` interface can also be detected, however they cannot be used alongside `Callback` beans. + +By default, Flyway autowires the (`@Primary`) `DataSource` in your context and uses that for migrations. +If you like to use a different `DataSource`, you can create one and mark its `@Bean` as `@FlywayDataSource`. +If you do so and want two data sources, remember to create another one and mark it as `@Primary`. +Alternatively, you can use Flyway's native `DataSource` by setting `spring.flyway.[url,user,password]` in external properties. +Setting either `spring.flyway.url` or `spring.flyway.user` is sufficient to cause Flyway to use its own `DataSource`. +If any of the three properties has not been set, the value of its equivalent `spring.datasource` property will be used. + +You can also use Flyway to provide data for specific scenarios. +For example, you can place test-specific migrations in `src/test/resources` and they are run only when your application starts for testing. +Also, you can use profile-specific configuration to customize `spring.flyway.locations` so that certain migrations run only when a particular profile is active. +For example, in `application-dev.properties`, you might specify the following setting: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + flyway: + locations: "classpath:/db/migration,classpath:/dev/db/migration" +---- + +With that setup, migrations in `dev/db/migration` run only when the `dev` profile is active. + + + +[[howto.data-initialization.migration-tool.liquibase]] +==== Execute Liquibase Database Migrations on Startup +To automatically run Liquibase database migrations on startup, add the `org.liquibase:liquibase-core` to your classpath. + +[NOTE] +==== +When you add the `org.liquibase:liquibase-core` to your classpath, database migrations run by default for both during application startup and before your tests run. +This behavior can be customized by using the configprop:spring.liquibase.enabled[] property, setting different values in the `main` and `test` configurations. +It is not possible to use two different ways to initialize the database (e.g. Liquibase for application startup, JPA for test runs). +==== + +By default, the master change log is read from `db/changelog/db.changelog-master.yaml`, but you can change the location by setting `spring.liquibase.change-log`. +In addition to YAML, Liquibase also supports JSON, XML, and SQL change log formats. + +By default, Liquibase autowires the (`@Primary`) `DataSource` in your context and uses that for migrations. +If you need to use a different `DataSource`, you can create one and mark its `@Bean` as `@LiquibaseDataSource`. +If you do so and you want two data sources, remember to create another one and mark it as `@Primary`. +Alternatively, you can use Liquibase's native `DataSource` by setting `spring.liquibase.[driver-class-name,url,user,password]` in external properties. +Setting either `spring.liquibase.url` or `spring.liquibase.user` is sufficient to cause Liquibase to use its own `DataSource`. +If any of the three properties has not been set, the value of its equivalent `spring.datasource` property will be used. + +See {spring-boot-autoconfigure-module-code}/liquibase/LiquibaseProperties.java[`LiquibaseProperties`] for details about available settings such as contexts, the default schema, and others. + + + +[[howto.data-initialization.dependencies]] +=== Depend Upon an Initialized Database +Database initialization is performed while the application is starting up as part of application context refresh. +To allow an initialized database to be accessed during startup, beans that act as database initializers and beans that require that database to have been initialized are detected automatically. +Beans whose initialization depends upon the database having been initialized are configured to depend upon those that initialize it. +If, during startup, your application tries to access the database and it has not been initialized, you can configure additional detection of beans that initialize the database and require the database to have been initialized. + + + +[[howto.data-initialization.dependencies.initializer-detection]] +==== Detect a Database Initializer +Spring Boot will automatically detect beans of the following types that initialize an SQL database: + +- `DataSourceScriptDatabaseInitializer` +- `EntityManagerFactory` +- `Flyway` +- `FlywayMigrationInitializer` +- `R2dbcScriptDatabaseInitializer` +- `SpringLiquibase` + +If you are using a third-party starter for a database initialization library, it may provide a detector such that beans of other types are also detected automatically. +To have other beans be detected, register an implementation of `DatabaseInitializerDetector` in `META-INF/spring-factories`. + + + +[[howto.data-initialization.dependencies.depends-on-initialization-detection]] +==== Detect a Bean That Depends On Database Initialization +Spring Boot will automatically detect beans of the following types that depends upon database initialization: + +- `AbstractEntityManagerFactoryBean` (unless configprop:spring.jpa.defer-datasource-initialization[] is set to `true`) +- `DSLContext` (jOOQ) +- `EntityManagerFactory` (unless configprop:spring.jpa.defer-datasource-initialization[] is set to `true`) +- `JdbcOperations` +- `NamedParameterJdbcOperations` + +If you are using a third-party starter data access library, it may provide a detector such that beans of other types are also detected automatically. +To have other beans be detected, register an implementation of `DependsOnDatabaseInitializationDetector` in `META-INF/spring-factories`. +Alternatively, annotate the bean's class or its `@Bean` method with `@DependsOnDatabaseInitialization`. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/hotswapping.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/hotswapping.adoc new file mode 100644 index 000000000000..9a6ec3f495f7 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/hotswapping.adoc @@ -0,0 +1,67 @@ +[[howto.hotswapping]] +== Hot Swapping +Spring Boot supports hot swapping. +This section answers questions about how it works. + + + +[[howto.hotswapping.reload-static-content]] +=== Reload Static Content +There are several options for hot reloading. +The recommended approach is to use <>, as it provides additional development-time features, such as support for fast application restarts and LiveReload as well as sensible development-time configuration (such as template caching). +Devtools works by monitoring the classpath for changes. +This means that static resource changes must be "built" for the change to take effect. +By default, this happens automatically in Eclipse when you save your changes. +In IntelliJ IDEA, the Make Project command triggers the necessary build. +Due to the <>, changes to static resources do not trigger a restart of your application. +They do, however, trigger a live reload. + +Alternatively, running in an IDE (especially with debugging on) is a good way to do development (all modern IDEs allow reloading of static resources and usually also allow hot-swapping of Java class changes). + +Finally, the <> can be configured (see the `addResources` property) to support running from the command line with reloading of static files directly from source. +You can use that with an external css/js compiler process if you are writing that code with higher-level tools. + + + +[[howto.hotswapping.reload-templates]] +=== Reload Templates without Restarting the Container +Most of the templating technologies supported by Spring Boot include a configuration option to disable caching (described later in this document). +If you use the `spring-boot-devtools` module, these properties are <> for you at development time. + + + +[[howto.hotswapping.reload-templates.thymeleaf]] +==== Thymeleaf Templates +If you use Thymeleaf, set `spring.thymeleaf.cache` to `false`. +See {spring-boot-autoconfigure-module-code}/thymeleaf/ThymeleafAutoConfiguration.java[`ThymeleafAutoConfiguration`] for other Thymeleaf customization options. + + + +[[howto.hotswapping.reload-templates.freemarker]] +==== FreeMarker Templates +If you use FreeMarker, set `spring.freemarker.cache` to `false`. +See {spring-boot-autoconfigure-module-code}/freemarker/FreeMarkerAutoConfiguration.java[`FreeMarkerAutoConfiguration`] for other FreeMarker customization options. + + + +[[howto.hotswapping.reload-templates.groovy]] +==== Groovy Templates +If you use Groovy templates, set `spring.groovy.template.cache` to `false`. +See {spring-boot-autoconfigure-module-code}/groovy/template/GroovyTemplateAutoConfiguration.java[`GroovyTemplateAutoConfiguration`] for other Groovy customization options. + + + +[[howto.hotswapping.fast-application-restarts]] +=== Fast Application Restarts +The `spring-boot-devtools` module includes support for automatic application restarts. +While not as fast as technologies such as https://www.jrebel.com/products/jrebel[JRebel] it is usually significantly faster than a "`cold start`". +You should probably give it a try before investigating some of the more complex reload options discussed later in this document. + +For more details, see the <> section. + + + +[[howto.hotswapping.reload-java-classes-without-restarting]] +=== Reload Java Classes without Restarting the Container +Many modern IDEs (Eclipse, IDEA, and others) support hot swapping of bytecode. +Consequently, if you make a change that does not affect class or method signatures, it should reload cleanly with no side effects. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/http-clients.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/http-clients.adoc new file mode 100644 index 000000000000..540e7a5737f3 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/http-clients.adoc @@ -0,0 +1,29 @@ +[[howto.http-clients]] +== HTTP Clients +Spring Boot offers a number of starters that work with HTTP clients. +This section answers questions related to using them. + + + +[[howto.http-clients.rest-template-proxy-configuration]] +=== Configure RestTemplate to Use a Proxy +As described in <>, you can use a `RestTemplateCustomizer` with `RestTemplateBuilder` to build a customized `RestTemplate`. +This is the recommended approach for creating a `RestTemplate` configured to use a proxy. + +The exact details of the proxy configuration depend on the underlying client request factory that is being used. + + + +[[howto.http-clients.webclient-reactor-netty-customization]] +=== Configure the TcpClient used by a Reactor Netty-based WebClient +When Reactor Netty is on the classpath a Reactor Netty-based `WebClient` is auto-configured. +To customize the client's handling of network connections, provide a `ClientHttpConnector` bean. +The following example configures a 60 second connect timeout and adds a `ReadTimeoutHandler`: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/httpclients/webclientreactornettycustomization/MyReactorNettyClientConfiguration.java[] +---- + +TIP: Note the use of `ReactorResourceFactory` for the connection provider and event loop resources. +This ensures efficient sharing of resources for the server receiving requests and the client making requests. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/jersey.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/jersey.adoc new file mode 100644 index 000000000000..d81c2eb326ab --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/jersey.adoc @@ -0,0 +1,30 @@ +[[howto.jersey]] +== Jersey + + + +[[howto.jersey.spring-security]] +=== Secure Jersey endpoints with Spring Security +Spring Security can be used to secure a Jersey-based web application in much the same way as it can be used to secure a Spring MVC-based web application. +However, if you want to use Spring Security's method-level security with Jersey, you must configure Jersey to use `setStatus(int)` rather `sendError(int)`. +This prevents Jersey from committing the response before Spring Security has had an opportunity to report an authentication or authorization failure to the client. + +The `jersey.config.server.response.setStatusOverSendError` property must be set to `true` on the application's `ResourceConfig` bean, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/jersey/springsecurity/JerseySetStatusOverSendErrorConfig.java[] +---- + + + +[[howto.jersey.alongside-another-web-framework]] +=== Use Jersey Alongside Another Web Framework +To use Jersey alongside another web framework, such as Spring MVC, it should be configured so that it will allow the other framework to handle requests that it cannot handle. +First, configure Jersey to use a Filter rather than a Servlet by configuring the configprop:spring.jersey.type[] application property with a value of `filter`. +Second, configure your `ResourceConfig` to forward requests that would have resulted in a 404, as shown in the following example. + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/jersey/alongsideanotherwebframework/JerseyConfig.java[] +---- diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/logging.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/logging.adoc new file mode 100644 index 000000000000..6b06869308c6 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/logging.adoc @@ -0,0 +1,187 @@ +[[howto.logging]] +== Logging +Spring Boot has no mandatory logging dependency, except for the Commons Logging API, which is typically provided by Spring Framework's `spring-jcl` module. +To use https://logback.qos.ch[Logback], you need to include it and `spring-jcl` on the classpath. +The recommended way to do that is through the starters, which all depend on `spring-boot-starter-logging`. +For a web application, you need only `spring-boot-starter-web`, since it depends transitively on the logging starter. +If you use Maven, the following dependency adds logging for you: + +[source,xml,indent=0,subs="verbatim"] +---- + + org.springframework.boot + spring-boot-starter-web + +---- + +Spring Boot has a `LoggingSystem` abstraction that attempts to configure logging based on the content of the classpath. +If Logback is available, it is the first choice. + +If the only change you need to make to logging is to set the levels of various loggers, you can do so in `application.properties` by using the "logging.level" prefix, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + logging: + level: + org.springframework.web: "debug" + org.hibernate: "error" +---- + +You can also set the location of a file to which to write the log (in addition to the console) by using `logging.file.name`. + +To configure the more fine-grained settings of a logging system, you need to use the native configuration format supported by the `LoggingSystem` in question. +By default, Spring Boot picks up the native configuration from its default location for the system (such as `classpath:logback.xml` for Logback), but you can set the location of the config file by using the configprop:logging.config[] property. + + + +[[howto.logging.logback]] +=== Configure Logback for Logging +If you need to apply customizations to logback beyond those that can be achieved with `application.properties`, you'll need to add a standard logback configuration file. +You can add a `logback.xml` file to the root of your classpath for logback to find. +You can also use `logback-spring.xml` if you want to use the <>. + +TIP: The Logback documentation has a https://logback.qos.ch/manual/configuration.html[dedicated section that covers configuration] in some detail. + +Spring Boot provides a number of logback configurations that be `included` from your own configuration. +These includes are designed to allow certain common Spring Boot conventions to be re-applied. + +The following files are provided under `org/springframework/boot/logging/logback/`: + +* `defaults.xml` - Provides conversion rules, pattern properties and common logger configurations. +* `console-appender.xml` - Adds a `ConsoleAppender` using the `CONSOLE_LOG_PATTERN`. +* `file-appender.xml` - Adds a `RollingFileAppender` using the `FILE_LOG_PATTERN` and `ROLLING_FILE_NAME_PATTERN` with appropriate settings. + +In addition, a legacy `base.xml` file is provided for compatibility with earlier versions of Spring Boot. + +A typical custom `logback.xml` file would look something like this: + +[source,xml,indent=0,subs="verbatim"] +---- + + + + + + + + + +---- + +Your logback configuration file can also make use of System properties that the `LoggingSystem` takes care of creating for you: + +* `$\{PID}`: The current process ID. +* `$\{LOG_FILE}`: Whether `logging.file.name` was set in Boot's external configuration. +* `$\{LOG_PATH}`: Whether `logging.file.path` (representing a directory for log files to live in) was set in Boot's external configuration. +* `$\{LOG_EXCEPTION_CONVERSION_WORD}`: Whether `logging.exception-conversion-word` was set in Boot's external configuration. +* `$\{ROLLING_FILE_NAME_PATTERN}`: Whether `logging.pattern.rolling-file-name` was set in Boot's external configuration. + +Spring Boot also provides some nice ANSI color terminal output on a console (but not in a log file) by using a custom Logback converter. +See the `CONSOLE_LOG_PATTERN` in the `defaults.xml` configuration for an example. + +If Groovy is on the classpath, you should be able to configure Logback with `logback.groovy` as well. +If present, this setting is given preference. + +NOTE: Spring extensions are not supported with Groovy configuration. +Any `logback-spring.groovy` files will not be detected. + + + +[[howto.logging.logback.file-only-output]] +==== Configure Logback for File-only Output +If you want to disable console logging and write output only to a file, you need a custom `logback-spring.xml` that imports `file-appender.xml` but not `console-appender.xml`, as shown in the following example: + +[source,xml,indent=0,subs="verbatim"] +---- + + + + + + + + + +---- + +You also need to add `logging.file.name` to your `application.properties` or `application.yaml`, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + logging: + file: + name: "myapplication.log" +---- + + + +[[howto.logging.log4j]] +=== Configure Log4j for Logging +Spring Boot supports https://logging.apache.org/log4j/2.x/[Log4j 2] for logging configuration if it is on the classpath. +If you use the starters for assembling dependencies, you have to exclude Logback and then include log4j 2 instead. +If you do not use the starters, you need to provide (at least) `spring-jcl` in addition to Log4j 2. + +The recommended path is through the starters, even though it requires some jiggling. +The following example shows how to set up the starters in Maven: + +[source,xml,indent=0,subs="verbatim"] +---- + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-log4j2 + +---- + +Gradle provides a few different ways to set up the starters. +One way is to use a {gradle-docs}/resolution_rules.html#sec:module_replacement[module replacement]. +To do so, declare a dependency on the Log4j 2 starter and tell Gradle that any occurrences of the default logging starter should be replaced by the Log4j 2 starter, as shown in the following example: + +[source,gradle,indent=0,subs="verbatim"] +---- + dependencies { + implementation "org.springframework.boot:spring-boot-starter-log4j2" + modules { + module("org.springframework.boot:spring-boot-starter-logging") { + replacedBy("org.springframework.boot:spring-boot-starter-log4j2", "Use Log4j2 instead of Logback") + } + } + } +---- + +NOTE: The Log4j starters gather together the dependencies for common logging requirements (such as having Tomcat use `java.util.logging` but configuring the output using Log4j 2). + +NOTE: To ensure that debug logging performed using `java.util.logging` is routed into Log4j 2, configure its https://logging.apache.org/log4j/2.x/log4j-jul/index.html[JDK logging adapter] by setting the `java.util.logging.manager` system property to `org.apache.logging.log4j.jul.LogManager`. + + + +[[howto.logging.log4j.yaml-or-json-config]] +==== Use YAML or JSON to Configure Log4j 2 +In addition to its default XML configuration format, Log4j 2 also supports YAML and JSON configuration files. +To configure Log4j 2 to use an alternative configuration file format, add the appropriate dependencies to the classpath and name your configuration files to match your chosen file format, as shown in the following example: + +[cols="10,75a,15a"] +|=== +| Format | Dependencies | File names + +|YAML +| `com.fasterxml.jackson.core:jackson-databind` + `com.fasterxml.jackson.dataformat:jackson-dataformat-yaml` +| `log4j2.yaml` + `log4j2.yml` + +|JSON +| `com.fasterxml.jackson.core:jackson-databind` +| `log4j2.json` + `log4j2.jsn` +|=== diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/messaging.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/messaging.adoc new file mode 100644 index 000000000000..1b3c5920afc8 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/messaging.adoc @@ -0,0 +1,19 @@ +[[howto.messaging]] +== Messaging +Spring Boot offers a number of starters to support messaging. +This section answers questions that arise from using messaging with Spring Boot. + + + +[[howto.messaging.disable-transacted-jms-session]] +=== Disable Transacted JMS Session +If your JMS broker does not support transacted sessions, you have to disable the support of transactions altogether. +If you create your own `JmsListenerContainerFactory`, there is nothing to do, since, by default it cannot be transacted. +If you want to use the `DefaultJmsListenerContainerFactoryConfigurer` to reuse Spring Boot's default, you can disable transacted sessions, as follows: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/messaging/disabletransactedjmssession/MyJmsConfiguration.java[] +---- + +The preceding example overrides the default factory, and it should be applied to any other factory that your application defines, if any. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/nosql.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/nosql.adoc new file mode 100644 index 000000000000..c7be21231a0b --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/nosql.adoc @@ -0,0 +1,45 @@ +[[howto.nosql]] +== NoSQL +Spring Boot offers a number of starters that support NoSQL technologies. +This section answers questions that arise from using NoSQL with Spring Boot. + + + +[[howto.nosql.jedis-instead-of-lettuce]] +=== Use Jedis Instead of Lettuce +By default, the Spring Boot starter (`spring-boot-starter-data-redis`) uses https://github.com/lettuce-io/lettuce-core/[Lettuce]. +You need to exclude that dependency and include the https://github.com/xetorthio/jedis/[Jedis] one instead. +Spring Boot manages both of these dependencies so you can switch to Jedis without specifying a version. + +The following example shows how to do so in Maven: + +[source,xml,indent=0,subs="verbatim"] +---- + + org.springframework.boot + spring-boot-starter-data-redis + + + io.lettuce + lettuce-core + + + + + redis.clients + jedis + +---- + +The following example shows how to do so in Gradle: + +[source,gradle,indent=0,subs="verbatim"] +---- + dependencies { + implementation('org.springframework.boot:spring-boot-starter-data-redis') { + exclude group: 'io.lettuce', module: 'lettuce-core' + } + implementation 'redis.clients:jedis' + // ... + } +---- diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/properties-and-configuration.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/properties-and-configuration.adoc new file mode 100644 index 000000000000..d05c5f85234f --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/properties-and-configuration.adoc @@ -0,0 +1,305 @@ +[[howto.properties-and-configuration]] +== Properties and Configuration +This section includes topics about setting and reading properties and configuration settings and their interaction with Spring Boot applications. + + + +[[howto.properties-and-configuration.expand-properties]] +=== Automatically Expand Properties at Build Time +Rather than hardcoding some properties that are also specified in your project's build configuration, you can automatically expand them by instead using the existing build configuration. +This is possible in both Maven and Gradle. + + + +[[howto.properties-and-configuration.expand-properties.maven]] +==== Automatic Property Expansion Using Maven +You can automatically expand properties from the Maven project by using resource filtering. +If you use the `spring-boot-starter-parent`, you can then refer to your Maven '`project properties`' with `@..@` placeholders, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configblocks] +---- + app: + encoding: "@project.build.sourceEncoding@" + java: + version: "@java.version@" +---- + +NOTE: Only production configuration is filtered that way (in other words, no filtering is applied on `src/test/resources`). + +TIP: If you enable the `addResources` flag, the `spring-boot:run` goal can add `src/main/resources` directly to the classpath (for hot reloading purposes). +Doing so circumvents the resource filtering and this feature. +Instead, you can use the `exec:java` goal or customize the plugin's configuration. +See the {spring-boot-maven-plugin-docs}#getting-started[plugin usage page] for more details. + +If you do not use the starter parent, you need to include the following element inside the `` element of your `pom.xml`: + +[source,xml,indent=0,subs="verbatim"] +---- + + + src/main/resources + true + + +---- + +You also need to include the following element inside ``: + +[source,xml,indent=0,subs="verbatim"] +---- + + org.apache.maven.plugins + maven-resources-plugin + 2.7 + + + @ + + false + + +---- + +NOTE: The `useDefaultDelimiters` property is important if you use standard Spring placeholders (such as `$\{placeholder}`) in your configuration. +If that property is not set to `false`, these may be expanded by the build. + + + +[[howto.properties-and-configuration.expand-properties.gradle]] +==== Automatic Property Expansion Using Gradle +You can automatically expand properties from the Gradle project by configuring the Java plugin's `processResources` task to do so, as shown in the following example: + +[source,gradle,indent=0,subs="verbatim"] +---- + tasks.named('processResources') { + expand(project.properties) + } +---- + +You can then refer to your Gradle project's properties by using placeholders, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configblocks] +---- + app: + name: "${name}" + description: "${description}" +---- + +NOTE: Gradle's `expand` method uses Groovy's `SimpleTemplateEngine`, which transforms `${..}` tokens. +The `${..}` style conflicts with Spring's own property placeholder mechanism. +To use Spring property placeholders together with automatic expansion, escape the Spring property placeholders as follows: `\${..}`. + + + +[[howto.properties-and-configuration.externalize-configuration]] +=== Externalize the Configuration of SpringApplication +A `SpringApplication` has bean property setters, so you can use its Java API as you create the application to modify its behavior. +Alternatively, you can externalize the configuration by setting properties in `+spring.main.*+`. +For example, in `application.properties`, you might have the following settings: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + main: + web-application-type: "none" + banner-mode: "off" +---- + +Then the Spring Boot banner is not printed on startup, and the application is not starting an embedded web server. + +Properties defined in external configuration override and replace the values specified with the Java API, with the notable exception of the primary sources. +Primary sources are those provided to the `SpringApplication` constructor: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/propertiesandconfiguration/externalizeconfiguration/application/MyApplication.java[] +---- + +Or to `sources(...)` method of a `SpringApplicationBuilder`: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/propertiesandconfiguration/externalizeconfiguration/builder/MyApplication.java[] +---- + +Given the examples above, if we have the following configuration: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + main: + sources: "com.example.MyDatabaseConfig,com.example.MyJmsConfig" + banner-mode: "console" +---- + +The actual application will show the banner (as overridden by configuration) and uses three sources for the `ApplicationContext`. +The application sources are: + +. `MyApplication` (from the code) +. `MyDatabaseConfig` (from the external config) +. `MyJmsConfig`(from the external config) + + + +[[howto.properties-and-configuration.external-properties-location]] +=== Change the Location of External Properties of an Application +By default, properties from different sources are added to the Spring `Environment` in a defined order (see "`<>`" in the '`Spring Boot features`' section for the exact order). + +You can also provide the following System properties (or environment variables) to change the behavior: + +* configprop:spring.config.name[] (configprop:spring.config.name[format=envvar]): Defaults to `application` as the root of the file name. +* configprop:spring.config.location[] (configprop:spring.config.location[format=envvar]): The file to load (such as a classpath resource or a URL). + A separate `Environment` property source is set up for this document and it can be overridden by system properties, environment variables, or the command line. + +No matter what you set in the environment, Spring Boot always loads `application.properties` as described above. +By default, if YAML is used, then files with the '`.yml`' extension are also added to the list. + +Spring Boot logs the configuration files that are loaded at the `DEBUG` level and the candidates it has not found at `TRACE` level. + +See {spring-boot-module-code}/context/config/ConfigFileApplicationListener.java[`ConfigFileApplicationListener`] for more detail. + + + +[[howto.properties-and-configuration.short-command-line-arguments]] +=== Use '`Short`' Command Line Arguments +Some people like to use (for example) `--port=9000` instead of `--server.port=9000` to set configuration properties on the command line. +You can enable this behavior by using placeholders in `application.properties`, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + server: + port: "${port:8080}" +---- + +TIP: If you inherit from the `spring-boot-starter-parent` POM, the default filter token of the `maven-resources-plugins` has been changed from `+${*}+` to `@` (that is, `@maven.token@` instead of `${maven.token}`) to prevent conflicts with Spring-style placeholders. +If you have enabled Maven filtering for the `application.properties` directly, you may want to also change the default filter token to use https://maven.apache.org/plugins/maven-resources-plugin/resources-mojo.html#delimiters[other delimiters]. + +NOTE: In this specific case, the port binding works in a PaaS environment such as Heroku or Cloud Foundry. +In those two platforms, the `PORT` environment variable is set automatically and Spring can bind to capitalized synonyms for `Environment` properties. + + + +[[howto.properties-and-configuration.yaml]] +=== Use YAML for External Properties +YAML is a superset of JSON and, as such, is a convenient syntax for storing external properties in a hierarchical format, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim"] +---- + spring: + application: + name: "cruncher" + datasource: + driver-class-name: "com.mysql.jdbc.Driver" + url: "jdbc:mysql://localhost/test" + server: + port: 9000 +---- + +Create a file called `application.yml` and put it in the root of your classpath. +Then add `snakeyaml` to your dependencies (Maven coordinates `org.yaml:snakeyaml`, already included if you use the `spring-boot-starter`). +A YAML file is parsed to a Java `Map` (like a JSON object), and Spring Boot flattens the map so that it is one level deep and has period-separated keys, as many people are used to with `Properties` files in Java. + +The preceding example YAML corresponds to the following `application.properties` file: + +[source,properties,indent=0,subs="verbatim",configprops] +---- + spring.application.name=cruncher + spring.datasource.driver-class-name=com.mysql.jdbc.Driver + spring.datasource.url=jdbc:mysql://localhost/test + server.port=9000 +---- + +See "`<>`" in the '`Spring Boot features`' section for more information about YAML. + + + +[[howto.properties-and-configuration.set-active-spring-profiles]] +=== Set the Active Spring Profiles +The Spring `Environment` has an API for this, but you would normally set a System property (configprop:spring.profiles.active[]) or an OS environment variable (configprop:spring.profiles.active[format=envvar]). +Also, you can launch your application with a `-D` argument (remember to put it before the main class or jar archive), as follows: + +[source,shell,indent=0,subs="verbatim"] +---- + $ java -jar -Dspring.profiles.active=production demo-0.0.1-SNAPSHOT.jar +---- + +In Spring Boot, you can also set the active profile in `application.properties`, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + profiles: + active: "production" +---- + +A value set this way is replaced by the System property or environment variable setting but not by the `SpringApplicationBuilder.profiles()` method. +Thus, the latter Java API can be used to augment the profiles without changing the defaults. + +See "`<>`" in the "`Spring Boot features`" section for more information. + + + +[[howto.properties-and-configuration.set-default-spring-profile-name]] +=== Set the Default Profile Name +The default profile is a profile that is enabled if no profile is active. +By default, the name of the default profile is `default`, but it could be changed using a System property (configprop:spring.profiles.default[]) or an OS environment variable (configprop:spring.profiles.default[format=envvar]). + +In Spring Boot, you can also set the default profile name in `application.properties`, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + profiles: + default: "dev" +---- + +See "`<>`" in the "`Spring Boot features`" section for more information. + + + +[[howto.properties-and-configuration.change-configuration-depending-on-the-environment]] +=== Change Configuration Depending on the Environment +Spring Boot supports multi-document YAML and Properties files (see <> for details) which can be activated conditionally based on the active profiles. + +If a document contains a `spring.config.activate.on-profile` key, then the profiles value (a comma-separated list of profiles or a profile expression) is fed into the Spring `Environment.acceptsProfiles()` method. +If the profile expression matches then that document is included in the final merge (otherwise, it is not), as shown in the following example: + +[source,yaml,indent=0,subs="verbatim,attributes",configprops,configblocks] +---- + server: + port: 9000 + --- + spring: + config: + activate: + on-profile: "development" + server: + port: 9001 + --- + spring: + config: + activate: + on-profile: "production" + server: + port: 0 +---- + +In the preceding example, the default port is 9000. +However, if the Spring profile called '`development`' is active, then the port is 9001. +If '`production`' is active, then the port is 0. + +NOTE: The documents are merged in the order in which they are encountered. +Later values override earlier values. + + + +[[howto.properties-and-configuration.discover-build-in-options-for-external-properties]] +=== Discover Built-in Options for External Properties +Spring Boot binds external properties from `application.properties` (or `.yml` files and other places) into an application at runtime. +There is not (and technically cannot be) an exhaustive list of all supported properties in a single location, because contributions can come from additional jar files on your classpath. + +A running application with the Actuator features has a `configprops` endpoint that shows all the bound and bindable properties available through `@ConfigurationProperties`. + +The appendix includes an <> example with a list of the most common properties supported by Spring Boot. +The definitive list comes from searching the source code for `@ConfigurationProperties` and `@Value` annotations as well as the occasional use of `Binder`. +For more about the exact ordering of loading properties, see "<>". diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/security.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/security.adoc new file mode 100644 index 000000000000..af2d3a31ab1e --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/security.adoc @@ -0,0 +1,48 @@ +[[howto.security]] +== Security +This section addresses questions about security when working with Spring Boot, including questions that arise from using Spring Security with Spring Boot. + +For more about Spring Security, see the {spring-security}[Spring Security project page]. + + + +[[howto.security.switch-off-spring-boot-configuration]] +=== Switch off the Spring Boot Security Configuration +If you define a `@Configuration` with a `WebSecurityConfigurerAdapter` or a `SecurityFilterChain` bean in your application, it switches off the default webapp security settings in Spring Boot. + + + +[[howto.security.change-user-details-service-and-add-user-accounts]] +=== Change the UserDetailsService and Add User Accounts +If you provide a `@Bean` of type `AuthenticationManager`, `AuthenticationProvider`, or `UserDetailsService`, the default `@Bean` for `InMemoryUserDetailsManager` is not created. +This means you have the full feature set of Spring Security available (such as {spring-security-docs}#servlet-authentication[various authentication options]). + +The easiest way to add user accounts is to provide your own `UserDetailsService` bean. + + + +[[howto.security.enable-https]] +=== Enable HTTPS When Running behind a Proxy Server +Ensuring that all your main endpoints are only available over HTTPS is an important chore for any application. +If you use Tomcat as a servlet container, then Spring Boot adds Tomcat's own `RemoteIpValve` automatically if it detects some environment settings, and you should be able to rely on the `HttpServletRequest` to report whether it is secure or not (even downstream of a proxy server that handles the real SSL termination). +The standard behavior is determined by the presence or absence of certain request headers (`x-forwarded-for` and `x-forwarded-proto`), whose names are conventional, so it should work with most front-end proxies. +You can switch on the valve by adding some entries to `application.properties`, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + server: + tomcat: + remoteip: + remote-ip-header: "x-forwarded-for" + protocol-header: "x-forwarded-proto" +---- + +(The presence of either of those properties switches on the valve. +Alternatively, you can add the `RemoteIpValve` by customizing the `TomcatServletWebServerFactory` using a `WebServerFactoryCustomizer` bean.) + +To configure Spring Security to require a secure channel for all (or some) requests, consider adding your own `SecurityFilterChain` bean that adds the following `HttpSecurity` configuration: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/security/enablehttps/MySecurityConfig.java[] +---- diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/spring-mvc.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/spring-mvc.adoc new file mode 100644 index 000000000000..058f8ee5422c --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/spring-mvc.adoc @@ -0,0 +1,231 @@ +[[howto.spring-mvc]] +== Spring MVC +Spring Boot has a number of starters that include Spring MVC. +Note that some starters include a dependency on Spring MVC rather than include it directly. +This section answers common questions about Spring MVC and Spring Boot. + + + +[[howto.spring-mvc.write-json-rest-service]] +=== Write a JSON REST Service +Any Spring `@RestController` in a Spring Boot application should render JSON response by default as long as Jackson2 is on the classpath, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/springmvc/writejsonrestservice/MyController.java[] +---- + +As long as `MyThing` can be serialized by Jackson2 (true for a normal POJO or Groovy object), then `http://localhost:8080/thing` serves a JSON representation of it by default. +Note that, in a browser, you might sometimes see XML responses, because browsers tend to send accept headers that prefer XML. + + + +[[howto.spring-mvc.write-xml-rest-service]] +=== Write an XML REST Service +If you have the Jackson XML extension (`jackson-dataformat-xml`) on the classpath, you can use it to render XML responses. +The previous example that we used for JSON would work. +To use the Jackson XML renderer, add the following dependency to your project: + +[source,xml,indent=0,subs="verbatim"] +---- + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + +---- + +If Jackson's XML extension is not available and JAXB is available, XML can be rendered with the additional requirement of having `MyThing` annotated as `@XmlRootElement`, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/springmvc/writexmlrestservice/MyThing.java[] +---- + +JAXB is only available out of the box with Java 8. +If you're using a more recent Java generation, add the following dependency to your project: + +[source,xml,indent=0,subs="verbatim"] +---- + + org.glassfish.jaxb + jaxb-runtime + +---- + +NOTE: To get the server to render XML instead of JSON, you might have to send an `Accept: text/xml` header (or use a browser). + + + +[[howto.spring-mvc.customize-jackson-objectmapper]] +=== Customize the Jackson ObjectMapper +Spring MVC (client and server side) uses `HttpMessageConverters` to negotiate content conversion in an HTTP exchange. +If Jackson is on the classpath, you already get the default converter(s) provided by `Jackson2ObjectMapperBuilder`, an instance of which is auto-configured for you. + +The `ObjectMapper` (or `XmlMapper` for Jackson XML converter) instance (created by default) has the following customized properties: + +* `MapperFeature.DEFAULT_VIEW_INCLUSION` is disabled +* `DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES` is disabled +* `SerializationFeature.WRITE_DATES_AS_TIMESTAMPS` is disabled + +Spring Boot also has some features to make it easier to customize this behavior. + +You can configure the `ObjectMapper` and `XmlMapper` instances by using the environment. +Jackson provides an extensive suite of on/off features that can be used to configure various aspects of its processing. +These features are described in six enums (in Jackson) that map onto properties in the environment: + +|=== +| Enum | Property | Values + +| `com.fasterxml.jackson.databind.DeserializationFeature` +| `spring.jackson.deserialization.` +| `true`, `false` + +| `com.fasterxml.jackson.core.JsonGenerator.Feature` +| `spring.jackson.generator.` +| `true`, `false` + +| `com.fasterxml.jackson.databind.MapperFeature` +| `spring.jackson.mapper.` +| `true`, `false` + +| `com.fasterxml.jackson.core.JsonParser.Feature` +| `spring.jackson.parser.` +| `true`, `false` + +| `com.fasterxml.jackson.databind.SerializationFeature` +| `spring.jackson.serialization.` +| `true`, `false` + +| `com.fasterxml.jackson.annotation.JsonInclude.Include` +| configprop:spring.jackson.default-property-inclusion[] +| `always`, `non_null`, `non_absent`, `non_default`, `non_empty` +|=== + +For example, to enable pretty print, set `spring.jackson.serialization.indent_output=true`. +Note that, thanks to the use of <>, the case of `indent_output` does not have to match the case of the corresponding enum constant, which is `INDENT_OUTPUT`. + +This environment-based configuration is applied to the auto-configured `Jackson2ObjectMapperBuilder` bean and applies to any mappers created by using the builder, including the auto-configured `ObjectMapper` bean. + +The context's `Jackson2ObjectMapperBuilder` can be customized by one or more `Jackson2ObjectMapperBuilderCustomizer` beans. +Such customizer beans can be ordered (Boot's own customizer has an order of 0), letting additional customization be applied both before and after Boot's customization. + +Any beans of type `com.fasterxml.jackson.databind.Module` are automatically registered with the auto-configured `Jackson2ObjectMapperBuilder` and are applied to any `ObjectMapper` instances that it creates. +This provides a global mechanism for contributing custom modules when you add new features to your application. + +If you want to replace the default `ObjectMapper` completely, either define a `@Bean` of that type and mark it as `@Primary` or, if you prefer the builder-based approach, define a `Jackson2ObjectMapperBuilder` `@Bean`. +Note that, in either case, doing so disables all auto-configuration of the `ObjectMapper`. + +If you provide any `@Beans` of type `MappingJackson2HttpMessageConverter`, they replace the default value in the MVC configuration. +Also, a convenience bean of type `HttpMessageConverters` is provided (and is always available if you use the default MVC configuration). +It has some useful methods to access the default and user-enhanced message converters. + +See the "`<>`" section and the {spring-boot-autoconfigure-module-code}/web/servlet/WebMvcAutoConfiguration.java[`WebMvcAutoConfiguration`] source code for more details. + + + +[[howto.spring-mvc.customize-responsebody-rendering]] +=== Customize the @ResponseBody Rendering +Spring uses `HttpMessageConverters` to render `@ResponseBody` (or responses from `@RestController`). +You can contribute additional converters by adding beans of the appropriate type in a Spring Boot context. +If a bean you add is of a type that would have been included by default anyway (such as `MappingJackson2HttpMessageConverter` for JSON conversions), it replaces the default value. +A convenience bean of type `HttpMessageConverters` is provided and is always available if you use the default MVC configuration. +It has some useful methods to access the default and user-enhanced message converters (For example, it can be useful if you want to manually inject them into a custom `RestTemplate`). + +As in normal MVC usage, any `WebMvcConfigurer` beans that you provide can also contribute converters by overriding the `configureMessageConverters` method. +However, unlike with normal MVC, you can supply only additional converters that you need (because Spring Boot uses the same mechanism to contribute its defaults). +Finally, if you opt out of the Spring Boot default MVC configuration by providing your own `@EnableWebMvc` configuration, you can take control completely and do everything manually by using `getMessageConverters` from `WebMvcConfigurationSupport`. + +See the {spring-boot-autoconfigure-module-code}/web/servlet/WebMvcAutoConfiguration.java[`WebMvcAutoConfiguration`] source code for more details. + + + +[[howto.spring-mvc.multipart-file-uploads]] +=== Handling Multipart File Uploads +Spring Boot embraces the Servlet 3 `javax.servlet.http.Part` API to support uploading files. +By default, Spring Boot configures Spring MVC with a maximum size of 1MB per file and a maximum of 10MB of file data in a single request. +You may override these values, the location to which intermediate data is stored (for example, to the `/tmp` directory), and the threshold past which data is flushed to disk by using the properties exposed in the `MultipartProperties` class. +For example, if you want to specify that files be unlimited, set the configprop:spring.servlet.multipart.max-file-size[] property to `-1`. + +The multipart support is helpful when you want to receive multipart encoded file data as a `@RequestParam`-annotated parameter of type `MultipartFile` in a Spring MVC controller handler method. + +See the {spring-boot-autoconfigure-module-code}/web/servlet/MultipartAutoConfiguration.java[`MultipartAutoConfiguration`] source for more details. + +NOTE: It is recommended to use the container's built-in support for multipart uploads rather than introducing an additional dependency such as Apache Commons File Upload. + + + +[[howto.spring-mvc.switch-off-dispatcherservlet]] +=== Switch Off the Spring MVC DispatcherServlet +By default, all content is served from the root of your application (`/`). +If you would rather map to a different path, you can configure one as follows: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + mvc: + servlet: + path: "/mypath" +---- + +If you have additional servlets you can declare a `@Bean` of type `Servlet` or `ServletRegistrationBean` for each and Spring Boot will register them transparently to the container. +Because servlets are registered that way, they can be mapped to a sub-context of the `DispatcherServlet` without invoking it. + +Configuring the `DispatcherServlet` yourself is unusual but if you really need to do it, a `@Bean` of type `DispatcherServletPath` must be provided as well to provide the path of your custom `DispatcherServlet`. + + + +[[howto.spring-mvc.switch-off-default-configuration]] +=== Switch off the Default MVC Configuration +The easiest way to take complete control over MVC configuration is to provide your own `@Configuration` with the `@EnableWebMvc` annotation. +Doing so leaves all MVC configuration in your hands. + + + +[[howto.spring-mvc.customize-view-resolvers]] +=== Customize ViewResolvers +A `ViewResolver` is a core component of Spring MVC, translating view names in `@Controller` to actual `View` implementations. +Note that `ViewResolvers` are mainly used in UI applications, rather than REST-style services (a `View` is not used to render a `@ResponseBody`). +There are many implementations of `ViewResolver` to choose from, and Spring on its own is not opinionated about which ones you should use. +Spring Boot, on the other hand, installs one or two for you, depending on what it finds on the classpath and in the application context. +The `DispatcherServlet` uses all the resolvers it finds in the application context, trying each one in turn until it gets a result. +If you add your own, you have to be aware of the order and in which position your resolver is added. + +`WebMvcAutoConfiguration` adds the following `ViewResolvers` to your context: + +* An `InternalResourceViewResolver` named '`defaultViewResolver`'. + This one locates physical resources that can be rendered by using the `DefaultServlet` (including static resources and JSP pages, if you use those). + It applies a prefix and a suffix to the view name and then looks for a physical resource with that path in the servlet context (the defaults are both empty but are accessible for external configuration through `spring.mvc.view.prefix` and `spring.mvc.view.suffix`). + You can override it by providing a bean of the same type. +* A `BeanNameViewResolver` named '`beanNameViewResolver`'. + This is a useful member of the view resolver chain and picks up any beans with the same name as the `View` being resolved. + It should not be necessary to override or replace it. +* A `ContentNegotiatingViewResolver` named '`viewResolver`' is added only if there *are* actually beans of type `View` present. + This is a composite resolver, delegating to all the others and attempting to find a match to the '`Accept`' HTTP header sent by the client. + There is a useful https://spring.io/blog/2013/06/03/content-negotiation-using-views[blog about `ContentNegotiatingViewResolver`] that you might like to study to learn more, and you might also look at the source code for detail. + You can switch off the auto-configured `ContentNegotiatingViewResolver` by defining a bean named '`viewResolver`'. +* If you use Thymeleaf, you also have a `ThymeleafViewResolver` named '`thymeleafViewResolver`'. + It looks for resources by surrounding the view name with a prefix and suffix. + The prefix is `spring.thymeleaf.prefix`, and the suffix is `spring.thymeleaf.suffix`. + The values of the prefix and suffix default to '`classpath:/templates/`' and '`.html`', respectively. + You can override `ThymeleafViewResolver` by providing a bean of the same name. +* If you use FreeMarker, you also have a `FreeMarkerViewResolver` named '`freeMarkerViewResolver`'. + It looks for resources in a loader path (which is externalized to `spring.freemarker.templateLoaderPath` and has a default value of '`classpath:/templates/`') by surrounding the view name with a prefix and a suffix. + The prefix is externalized to `spring.freemarker.prefix`, and the suffix is externalized to `spring.freemarker.suffix`. + The default values of the prefix and suffix are empty and '`.ftlh`', respectively. + You can override `FreeMarkerViewResolver` by providing a bean of the same name. +* If you use Groovy templates (actually, if `groovy-templates` is on your classpath), you also have a `GroovyMarkupViewResolver` named '`groovyMarkupViewResolver`'. + It looks for resources in a loader path by surrounding the view name with a prefix and suffix (externalized to `spring.groovy.template.prefix` and `spring.groovy.template.suffix`). + The prefix and suffix have default values of '`classpath:/templates/`' and '`.tpl`', respectively. + You can override `GroovyMarkupViewResolver` by providing a bean of the same name. +* If you use Mustache, you also have a `MustacheViewResolver` named '`mustacheViewResolver`'. + It looks for resources by surrounding the view name with a prefix and suffix. + The prefix is `spring.mustache.prefix`, and the suffix is `spring.mustache.suffix`. + The values of the prefix and suffix default to '`classpath:/templates/`' and '`.mustache`', respectively. + You can override `MustacheViewResolver` by providing a bean of the same name. + +For more detail, see the following sections: + +* {spring-boot-autoconfigure-module-code}/web/servlet/WebMvcAutoConfiguration.java[`WebMvcAutoConfiguration`] +* {spring-boot-autoconfigure-module-code}/thymeleaf/ThymeleafAutoConfiguration.java[`ThymeleafAutoConfiguration`] +* {spring-boot-autoconfigure-module-code}/freemarker/FreeMarkerAutoConfiguration.java[`FreeMarkerAutoConfiguration`] +* {spring-boot-autoconfigure-module-code}/groovy/template/GroovyTemplateAutoConfiguration.java[`GroovyTemplateAutoConfiguration`] diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/testing.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/testing.adoc new file mode 100644 index 000000000000..8172b595c991 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/testing.adoc @@ -0,0 +1,80 @@ +[[howto.testing]] +== Testing +Spring Boot includes a number of testing utilities and support classes as well as a dedicated starter that provides common test dependencies. +This section answers common questions about testing. + + + +[[howto.testing.with-spring-security]] +=== Testing With Spring Security +Spring Security provides support for running tests as a specific user. +For example, the test in the snippet below will run with an authenticated user that has the `ADMIN` role. + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/testing/withspringsecurity/MySecurityTests.java[] +---- + +Spring Security provides comprehensive integration with Spring MVC Test and this can also be used when testing controllers using the `@WebMvcTest` slice and `MockMvc`. + +For additional details on Spring Security's testing support, refer to Spring Security's {spring-security-docs}#test[reference documentation]). + + + + +[[howto.testing.testcontainers]] +=== Use Testcontainers for Integration Testing +The https://www.testcontainers.org/[Testcontainers] library provides a way to manage services running inside Docker containers. +It integrates with JUnit, allowing you to write a test class that can start up a container before any of the tests run. +Testcontainers is especially useful for writing integration tests that talk to a real backend service such as MySQL, MongoDB, Cassandra etc. +Testcontainers can be used in a Spring Boot test as follows: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/testing/testcontainers/vanilla/MyIntegrationTests.java[] +---- + +This will start up a docker container running Neo4j (if Docker is running locally) before any of the tests are run. +In most cases, you will need to configure the application using details from the running container, such as container IP or port. + +This can be done with a static `@DynamicPropertySource` method that allows adding dynamic property values to the Spring Environment. + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/testing/testcontainers/dynamicproperties/MyIntegrationTests.java[] +---- + +The above configuration allows Neo4j-related beans in the application to communicate with Neo4j running inside the Testcontainers-managed Docker container. + + + +[[howto.testing.slice-tests]] +=== Structure `@Configuration` classes for inclusion in slice tests +Slice tests work by restricting Spring Framework's component scanning to a limited set of components based on their type. +For any beans that are not created via component scanning, for example, beans that are created using the `@Bean` annotation, slice tests will not be able to include/exclude them from the application context. +Consider this example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/testing/slicetests/MyConfiguration.java[] +---- + +For a `@WebMvcTest` for an application with the above `@Configuration` class, you might expect to have the `SecurityFilterChain` bean in the application context so that you can test if your controller endpoints are secured properly. +However, `MyConfiguration` is not picked up by @WebMvcTest's component scanning filter because it doesn't match any of the types specified by the filter. +You can include the configuration explicitly by annotating the test class with `@Import(MySecurityConfiguration.class)`. +This will load all the beans in `MyConfiguration` including the `BasicDataSource` bean which isn't required when testing the web tier. +Splitting the configuration class into two will enable importing just the security configuration. + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/testing/slicetests/MySecurityConfiguration.java[] +---- + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/testing/slicetests/MyDatasourceConfiguration.java[] +---- + +Having a single configuration class can be inefficient when beans of a certain domain needed to be included in slice tests. +Instead, structuring the application's configuration as multiple granular classes with beans for a specific domain can enable importing them only for specific slice tests. + diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/traditional-deployment.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/traditional-deployment.adoc new file mode 100644 index 000000000000..db67d906e50b --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/traditional-deployment.adoc @@ -0,0 +1,171 @@ +[[howto.traditional-deployment]] +== Traditional Deployment +Spring Boot supports traditional deployment as well as more modern forms of deployment. +This section answers common questions about traditional deployment. + + + +[[howto.traditional-deployment.war]] +=== Create a Deployable War File + +WARNING: Because Spring WebFlux does not strictly depend on the Servlet API and applications are deployed by default on an embedded Reactor Netty server, War deployment is not supported for WebFlux applications. + +The first step in producing a deployable war file is to provide a `SpringBootServletInitializer` subclass and override its `configure` method. +Doing so makes use of Spring Framework's Servlet 3.0 support and lets you configure your application when it is launched by the servlet container. +Typically, you should update your application's main class to extend `SpringBootServletInitializer`, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/traditionaldeployment/war/MyApplication.java[] +---- + +The next step is to update your build configuration such that your project produces a war file rather than a jar file. +If you use Maven and `spring-boot-starter-parent` (which configures Maven's war plugin for you), all you need to do is to modify `pom.xml` to change the packaging to war, as follows: + +[source,xml,indent=0,subs="verbatim"] +---- + war +---- + +If you use Gradle, you need to modify `build.gradle` to apply the war plugin to the project, as follows: + +[source,gradle,indent=0,subs="verbatim"] +---- + apply plugin: 'war' +---- + +The final step in the process is to ensure that the embedded servlet container does not interfere with the servlet container to which the war file is deployed. +To do so, you need to mark the embedded servlet container dependency as being provided. + +If you use Maven, the following example marks the servlet container (Tomcat, in this case) as being provided: + +[source,xml,indent=0,subs="verbatim"] +---- + + + + org.springframework.boot + spring-boot-starter-tomcat + provided + + + +---- + +If you use Gradle, the following example marks the servlet container (Tomcat, in this case) as being provided: + +[source,gradle,indent=0,subs="verbatim"] +---- + dependencies { + // ... + providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat' + // ... + } +---- + +TIP: `providedRuntime` is preferred to Gradle's `compileOnly` configuration. +Among other limitations, `compileOnly` dependencies are not on the test classpath, so any web-based integration tests fail. + +If you use the <>, marking the embedded servlet container dependency as provided produces an executable war file with the provided dependencies packaged in a `lib-provided` directory. +This means that, in addition to being deployable to a servlet container, you can also run your application by using `java -jar` on the command line. + + + +[[howto.traditional-deployment.convert-existing-application]] +=== Convert an Existing Application to Spring Boot +To convert an existing non-web Spring application to a Spring Boot application, replace the code that creates your `ApplicationContext` and replace it with calls to `SpringApplication` or `SpringApplicationBuilder`. +Spring MVC web applications are generally amenable to first creating a deployable war application and then migrating it later to an executable war or jar. +See the https://spring.io/guides/gs/convert-jar-to-war/[Getting Started Guide on Converting a jar to a war]. + +To create a deployable war by extending `SpringBootServletInitializer` (for example, in a class called `Application`) and adding the Spring Boot `@SpringBootApplication` annotation, use code similar to that shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/traditionaldeployment/convertexistingapplication/MyApplication.java[tag=!main] +---- + +Remember that, whatever you put in the `sources` is merely a Spring `ApplicationContext`. +Normally, anything that already works should work here. +There might be some beans you can remove later and let Spring Boot provide its own defaults for them, but it should be possible to get something working before you need to do that. + +Static resources can be moved to `/public` (or `/static` or `/resources` or `/META-INF/resources`) in the classpath root. +The same applies to `messages.properties` (which Spring Boot automatically detects in the root of the classpath). + +Vanilla usage of Spring `DispatcherServlet` and Spring Security should require no further changes. +If you have other features in your application (for instance, using other servlets or filters), you may need to add some configuration to your `Application` context, by replacing those elements from the `web.xml`, as follows: + +* A `@Bean` of type `Servlet` or `ServletRegistrationBean` installs that bean in the container as if it were a `` and `` in `web.xml`. +* A `@Bean` of type `Filter` or `FilterRegistrationBean` behaves similarly (as a `` and ``). +* An `ApplicationContext` in an XML file can be added through an `@ImportResource` in your `Application`. + Alternatively, cases where annotation configuration is heavily used already can be recreated in a few lines as `@Bean` definitions. + +Once the war file is working, you can make it executable by adding a `main` method to your `Application`, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/traditionaldeployment/convertexistingapplication/MyApplication.java[tag=main] +---- + +[NOTE] +==== +If you intend to start your application as a war or as an executable application, you need to share the customizations of the builder in a method that is both available to the `SpringBootServletInitializer` callback and in the `main` method in a class similar to the following: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/traditionaldeployment/convertexistingapplication/both/MyApplication.java[] +---- +==== + +Applications can fall into more than one category: + +* Servlet 3.0+ applications with no `web.xml`. +* Applications with a `web.xml`. +* Applications with a context hierarchy. +* Applications without a context hierarchy. + +All of these should be amenable to translation, but each might require slightly different techniques. + +Servlet 3.0+ applications might translate pretty easily if they already use the Spring Servlet 3.0+ initializer support classes. +Normally, all the code from an existing `WebApplicationInitializer` can be moved into a `SpringBootServletInitializer`. +If your existing application has more than one `ApplicationContext` (for example, if it uses `AbstractDispatcherServletInitializer`) then you might be able to combine all your context sources into a single `SpringApplication`. +The main complication you might encounter is if combining does not work and you need to maintain the context hierarchy. +See the <> for examples. +An existing parent context that contains web-specific features usually needs to be broken up so that all the `ServletContextAware` components are in the child context. + +Applications that are not already Spring applications might be convertible to Spring Boot applications, and the previously mentioned guidance may help. +However, you may yet encounter problems. +In that case, we suggest https://stackoverflow.com/questions/tagged/spring-boot[asking questions on Stack Overflow with a tag of `spring-boot`]. + + + +[[howto.traditional-deployment.weblogic]] +=== Deploying a WAR to WebLogic +To deploy a Spring Boot application to WebLogic, you must ensure that your servlet initializer *directly* implements `WebApplicationInitializer` (even if you extend from a base class that already implements it). + +A typical initializer for WebLogic should resemble the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/traditionaldeployment/weblogic/MyApplication.java[] +---- + +If you use Logback, you also need to tell WebLogic to prefer the packaged version rather than the version that was pre-installed with the server. +You can do so by adding a `WEB-INF/weblogic.xml` file with the following contents: + +[source,xml,indent=0,subs="verbatim"] +---- + + + + + org.slf4j + + + +---- diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/webserver.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/webserver.adoc new file mode 100644 index 000000000000..e5465b0515f1 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/webserver.adoc @@ -0,0 +1,540 @@ +[[howto.webserver]] +== Embedded Web Servers +Each Spring Boot web application includes an embedded web server. +This feature leads to a number of how-to questions, including how to change the embedded server and how to configure the embedded server. +This section answers those questions. + + + +[[howto.webserver.use-another]] +=== Use Another Web Server +Many Spring Boot starters include default embedded containers. + +* For servlet stack applications, the `spring-boot-starter-web` includes Tomcat by including `spring-boot-starter-tomcat`, but you can use `spring-boot-starter-jetty` or `spring-boot-starter-undertow` instead. +* For reactive stack applications, the `spring-boot-starter-webflux` includes Reactor Netty by including `spring-boot-starter-reactor-netty`, but you can use `spring-boot-starter-tomcat`, `spring-boot-starter-jetty`, or `spring-boot-starter-undertow` instead. + +When switching to a different HTTP server, you need to swap the default dependencies for those that you need instead. +To help with this process, Spring Boot provides a separate starter for each of the supported HTTP servers. + +The following Maven example shows how to exclude Tomcat and include Jetty for Spring MVC: + +[source,xml,indent=0,subs="verbatim"] +---- + + 3.1.0 + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + + org.springframework.boot + spring-boot-starter-jetty + +---- + +NOTE: The version of the Servlet API has been overridden as, unlike Tomcat 9 and Undertow 2, Jetty 9.4 does not support Servlet 4.0. + +If you wish to use Jetty 10, you can do so as shown in the following example: + +[source,xml,indent=0,subs="verbatim"] +---- + + 10.0.8 + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-tomcat + + + + org.eclipse.jetty.websocket + websocket-server + + + org.eclipse.jetty.websocket + javax-websocket-server-impl + + + + + + org.springframework.boot + spring-boot-starter-jetty + +---- + +Note that along with excluding the Tomcat starter, a couple of Jetty9-specific dependencies also need to be excluded. + +The following Gradle example configures the necessary dependencies and a {gradle-docs}/resolution_rules.html#sec:module_replacement[module replacement] to use Undertow in place of Reactor Netty for Spring WebFlux: + +[source,gradle,indent=0,subs="verbatim"] +---- + dependencies { + implementation "org.springframework.boot:spring-boot-starter-undertow" + implementation "org.springframework.boot:spring-boot-starter-webflux" + modules { + module("org.springframework.boot:spring-boot-starter-reactor-netty") { + replacedBy("org.springframework.boot:spring-boot-starter-undertow", "Use Undertow instead of Reactor Netty") + } + } + } +---- + +NOTE: `spring-boot-starter-reactor-netty` is required to use the `WebClient` class, so you may need to keep a dependency on Netty even when you need to include a different HTTP server. + + + +[[howto.webserver.disable]] +=== Disabling the Web Server +If your classpath contains the necessary bits to start a web server, Spring Boot will automatically start it. +To disable this behavior configure the `WebApplicationType` in your `application.properties`, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + main: + web-application-type: "none" +---- + + + +[[howto.webserver.change-port]] +=== Change the HTTP Port +In a standalone application, the main HTTP port defaults to `8080` but can be set with configprop:server.port[] (for example, in `application.properties` or as a System property). +Thanks to relaxed binding of `Environment` values, you can also use configprop:server.port[format=envvar] (for example, as an OS environment variable). + +To switch off the HTTP endpoints completely but still create a `WebApplicationContext`, use `server.port=-1` (doing so is sometimes useful for testing). + +For more details, see "`<>`" in the '`Spring Boot Features`' section, or the {spring-boot-autoconfigure-module-code}/web/ServerProperties.java[`ServerProperties`] source code. + + + +[[howto.webserver.use-random-port]] +=== Use a Random Unassigned HTTP Port +To scan for a free port (using OS natives to prevent clashes) use `server.port=0`. + + + +[[howto.webserver.discover-port]] +=== Discover the HTTP Port at Runtime +You can access the port the server is running on from log output or from the `WebServerApplicationContext` through its `WebServer`. +The best way to get that and be sure it has been initialized is to add a `@Bean` of type `ApplicationListener` and pull the container out of the event when it is published. + +Tests that use `@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT)` can also inject the actual port into a field by using the `@LocalServerPort` annotation, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/webserver/discoverport/MyWebIntegrationTests.java[] +---- + +[NOTE] +==== +`@LocalServerPort` is a meta-annotation for `@Value("${local.server.port}")`. +Do not try to inject the port in a regular application. +As we just saw, the value is set only after the container has been initialized. +Contrary to a test, application code callbacks are processed early (before the value is actually available). +==== + + + +[[howto.webserver.enable-response-compression]] +=== Enable HTTP Response Compression +HTTP response compression is supported by Jetty, Tomcat, Reactor Netty, and Undertow. +It can be enabled in `application.properties`, as follows: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + server: + compression: + enabled: true +---- + +By default, responses must be at least 2048 bytes in length for compression to be performed. +You can configure this behavior by setting the configprop:server.compression.min-response-size[] property. + +By default, responses are compressed only if their content type is one of the following: + +* `text/html` +* `text/xml` +* `text/plain` +* `text/css` +* `text/javascript` +* `application/javascript` +* `application/json` +* `application/xml` + +You can configure this behavior by setting the configprop:server.compression.mime-types[] property. + + + +[[howto.webserver.configure-ssl]] +=== Configure SSL +SSL can be configured declaratively by setting the various `+server.ssl.*+` properties, typically in `application.properties` or `application.yml`. +The following example shows setting SSL properties in `application.properties`: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + server: + port: 8443 + ssl: + key-store: "classpath:keystore.jks" + key-store-password: "secret" + key-password: "another-secret" +---- + +See {spring-boot-module-code}/web/server/Ssl.java[`Ssl`] for details of all of the supported properties. + +Using configuration such as the preceding example means the application no longer supports a plain HTTP connector at port 8080. +Spring Boot does not support the configuration of both an HTTP connector and an HTTPS connector through `application.properties`. +If you want to have both, you need to configure one of them programmatically. +We recommend using `application.properties` to configure HTTPS, as the HTTP connector is the easier of the two to configure programmatically. + + + +[[howto.webserver.configure-http2]] +=== Configure HTTP/2 +You can enable HTTP/2 support in your Spring Boot application with the configprop:server.http2.enabled[] configuration property. +Both `h2` (HTTP/2 over TLS) and `h2c` (HTTP/2 over TCP) are supported. +To use `h2`, SSL must also be enabled. +When SSL is not enabled, `h2c` will be used. +The details of the `h2` support depend on the chosen web server and the application environment, since that protocol is not supported out-of-the-box by all JDK 8 releases. + + + +[[howto.webserver.configure-http2.tomcat]] +==== HTTP/2 with Tomcat +Spring Boot ships by default with Tomcat 9.0.x which supports `h2c` out of the box and `h2` out of the box when using JDK 9 or later. +Alternatively, `h2` can be used on JDK 8 if the `libtcnative` library and its dependencies are installed on the host operating system. + +The library directory must be made available, if not already, to the JVM library path. +You can do so with a JVM argument such as `-Djava.library.path=/usr/local/opt/tomcat-native/lib`. +More on this in the https://tomcat.apache.org/tomcat-9.0-doc/apr.html[official Tomcat documentation]. + +Starting Tomcat 9.0.x on JDK 8 with HTTP/2 and SSL enabled but without that native support logs the following error: + +[indent=0,subs="verbatim"] +---- + ERROR 8787 --- [ main] o.a.coyote.http11.Http11NioProtocol : The upgrade handler [org.apache.coyote.http2.Http2Protocol] for [h2] only supports upgrade via ALPN but has been configured for the ["https-jsse-nio-8443"] connector that does not support ALPN. +---- + +This error is not fatal, and the application still starts with HTTP/1.1 SSL support. + + + +[[howto.webserver.configure-http2.jetty]] +==== HTTP/2 with Jetty +For HTTP/2 support, Jetty requires the additional `org.eclipse.jetty.http2:http2-server` dependency. +To use `h2c` no other dependencies are required. +To use `h2`, you also need to choose one of the following dependencies, depending on your deployment: + +* `org.eclipse.jetty:jetty-alpn-java-server` for applications running on JDK9+ +* `org.eclipse.jetty:jetty-alpn-openjdk8-server` for applications running on JDK8u252+ +* `org.eclipse.jetty:jetty-alpn-conscrypt-server` and the https://www.conscrypt.org/[Conscrypt library] with no JDK requirement + + + +[[howto.webserver.configure-http2.netty]] +==== HTTP/2 with Reactor Netty +The `spring-boot-webflux-starter` is using by default Reactor Netty as a server. +Reactor Netty supports `h2c` using JDK 8 or later with no additional dependencies. +Reactor Netty supports `h2` using the JDK support with JDK 9 or later. +For JDK 8 environments, or for optimal runtime performance, this server also supports `h2` with native libraries. +To enable that, your application needs to have an additional dependency. + +Spring Boot manages the version for the `io.netty:netty-tcnative-boringssl-static` "uber jar", containing native libraries for all platforms. +Developers can choose to import only the required dependencies using a classifier (see https://netty.io/wiki/forked-tomcat-native.html[the Netty official documentation]). + + + +[[howto.webserver.configure-http2.undertow]] +==== HTTP/2 with Undertow +As of Undertow 1.4.0+, both `h2` and `h2c` are supported on JDK 8 without any additional dependencies. + + + +[[howto.webserver.configure]] +=== Configure the Web Server +Generally, you should first consider using one of the many available configuration keys and customize your web server by adding new entries in your `application.properties` (or `application.yml`, or environment, etc. see "`<>`"). +The `server.{asterisk}` namespace is quite useful here, and it includes namespaces like `server.tomcat.{asterisk}`, `server.jetty.{asterisk}` and others, for server-specific features. +See the list of <>. + +The previous sections covered already many common use cases, such as compression, SSL or HTTP/2. +However, if a configuration key doesn't exist for your use case, you should then look at {spring-boot-module-api}/web/server/WebServerFactoryCustomizer.html[`WebServerFactoryCustomizer`]. +You can declare such a component and get access to the server factory relevant to your choice: you should select the variant for the chosen Server (Tomcat, Jetty, Reactor Netty, Undertow) and the chosen web stack (Servlet or Reactive). + +The example below is for Tomcat with the `spring-boot-starter-web` (Servlet stack): + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/webserver/configure/MyTomcatWebServerCustomizer.java[] +---- + +NOTE: Spring Boot uses that infrastructure internally to auto-configure the server. +Auto-configured `WebServerFactoryCustomizer` beans have an order of `0` and will be processed before any user-defined customizers, unless it has an explicit order that states otherwise. + +Once you've got access to a `WebServerFactory` using the customizer, you can use it to configure specific parts, like connectors, server resources, or the server itself - all using server-specific APIs. + +In addition Spring Boot provides: + +[[howto-configure-webserver-customizers]] +[cols="1,2,2", options="header"] +|=== +| Server | Servlet stack | Reactive stack + +| Tomcat +| `TomcatServletWebServerFactory` +| `TomcatReactiveWebServerFactory` + +| Jetty +| `JettyServletWebServerFactory` +| `JettyReactiveWebServerFactory` + +| Undertow +| `UndertowServletWebServerFactory` +| `UndertowReactiveWebServerFactory` + +| Reactor +| N/A +| `NettyReactiveWebServerFactory` +|=== + +As a last resort, you can also declare your own `WebServerFactory` bean, which will override the one provided by Spring Boot. +When you do so, auto-configured customizers are still applied on your custom factory, so use that option carefully. + + + +[[howto.webserver.add-servlet-filter-listener]] +=== Add a Servlet, Filter, or Listener to an Application +In a servlet stack application, i.e. with the `spring-boot-starter-web`, there are two ways to add `Servlet`, `Filter`, `ServletContextListener`, and the other listeners supported by the Servlet API to your application: + +* <> +* <> + + + +[[howto.webserver.add-servlet-filter-listener.spring-bean]] +==== Add a Servlet, Filter, or Listener by Using a Spring Bean +To add a `Servlet`, `Filter`, or Servlet `*Listener` by using a Spring bean, you must provide a `@Bean` definition for it. +Doing so can be very useful when you want to inject configuration or dependencies. +However, you must be very careful that they do not cause eager initialization of too many other beans, because they have to be installed in the container very early in the application lifecycle. +(For example, it is not a good idea to have them depend on your `DataSource` or JPA configuration.) +You can work around such restrictions by initializing the beans lazily when first used instead of on initialization. + +In the case of `Filters` and `Servlets`, you can also add mappings and init parameters by adding a `FilterRegistrationBean` or a `ServletRegistrationBean` instead of or in addition to the underlying component. + +[NOTE] +==== +If no `dispatcherType` is specified on a filter registration, `REQUEST` is used. +This aligns with the Servlet Specification's default dispatcher type. +==== + +Like any other Spring bean, you can define the order of Servlet filter beans; please make sure to check the "`<>`" section. + + + +[[howto.webserver.add-servlet-filter-listener.spring-bean.disable]] +===== Disable Registration of a Servlet or Filter +As <>, any `Servlet` or `Filter` beans are registered with the servlet container automatically. +To disable registration of a particular `Filter` or `Servlet` bean, create a registration bean for it and mark it as disabled, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/webserver/addservletfilterlistener/springbean/disable/MyFilterConfiguration.java[] +---- + + + +[[howto.webserver.add-servlet-filter-listener.using-scanning]] +==== Add Servlets, Filters, and Listeners by Using Classpath Scanning +`@WebServlet`, `@WebFilter`, and `@WebListener` annotated classes can be automatically registered with an embedded servlet container by annotating a `@Configuration` class with `@ServletComponentScan` and specifying the package(s) containing the components that you want to register. +By default, `@ServletComponentScan` scans from the package of the annotated class. + + + +[[howto.webserver.configure-access-logs]] +=== Configure Access Logging +Access logs can be configured for Tomcat, Undertow, and Jetty through their respective namespaces. + +For instance, the following settings log access on Tomcat with a {tomcat-docs}/config/valve.html#Access_Logging[custom pattern]. + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + server: + tomcat: + basedir: "my-tomcat" + accesslog: + enabled: true + pattern: "%t %a %r %s (%D ms)" +---- + +NOTE: The default location for logs is a `logs` directory relative to the Tomcat base directory. +By default, the `logs` directory is a temporary directory, so you may want to fix Tomcat's base directory or use an absolute path for the logs. +In the preceding example, the logs are available in `my-tomcat/logs` relative to the working directory of the application. + +Access logging for Undertow can be configured in a similar fashion, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + server: + undertow: + accesslog: + enabled: true + pattern: "%t %a %r %s (%D ms)" +---- + +Logs are stored in a `logs` directory relative to the working directory of the application. +You can customize this location by setting the configprop:server.undertow.accesslog.dir[] property. + +Finally, access logging for Jetty can also be configured as follows: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + server: + jetty: + accesslog: + enabled: true + filename: "/var/log/jetty-access.log" +---- + +By default, logs are redirected to `System.err`. +For more details, see the Jetty documentation. + + + +[[howto.webserver.use-behind-a-proxy-server]] +=== Running Behind a Front-end Proxy Server +If your application is running behind a proxy, a load-balancer or in the cloud, the request information (like the host, port, scheme...) might change along the way. +Your application may be running on `10.10.10.10:8080`, but HTTP clients should only see `example.org`. + +https://tools.ietf.org/html/rfc7239[RFC7239 "Forwarded Headers"] defines the `Forwarded` HTTP header; proxies can use this header to provide information about the original request. +You can configure your application to read those headers and automatically use that information when creating links and sending them to clients in HTTP 302 responses, JSON documents or HTML pages. +There are also non-standard headers, like `X-Forwarded-Host`, `X-Forwarded-Port`, `X-Forwarded-Proto`, `X-Forwarded-Ssl`, and `X-Forwarded-Prefix`. + +If the proxy adds the commonly used `X-Forwarded-For` and `X-Forwarded-Proto` headers, setting `server.forward-headers-strategy` to `NATIVE` is enough to support those. +With this option, the Web servers themselves natively support this feature; you can check their specific documentation to learn about specific behavior. + +If this is not enough, Spring Framework provides a {spring-framework-docs}/web.html#filters-forwarded-headers[ForwardedHeaderFilter]. +You can register it as a Servlet Filter in your application by setting `server.forward-headers-strategy` is set to `FRAMEWORK`. + +TIP: If you are using Tomcat and terminating SSL at the proxy, configprop:server.tomcat.redirect-context-root[] should be set to `false`. +This allows the `X-Forwarded-Proto` header to be honored before any redirects are performed. + +NOTE: If your application runs in Cloud Foundry or Heroku, the configprop:server.forward-headers-strategy[] property defaults to `NATIVE`. +In all other instances, it defaults to `NONE`. + + + +[[howto.webserver.use-behind-a-proxy-server.tomcat]] +==== Customize Tomcat's Proxy Configuration +If you use Tomcat, you can additionally configure the names of the headers used to carry "`forwarded`" information, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + server: + tomcat: + remoteip: + remote-ip-header: "x-your-remote-ip-header" + protocol-header: "x-your-protocol-header" +---- + +Tomcat is also configured with a default regular expression that matches internal proxies that are to be trusted. +By default, IP addresses in `10/8`, `192.168/16`, `169.254/16` and `127/8` are trusted. +You can customize the valve's configuration by adding an entry to `application.properties`, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + server: + tomcat: + remoteip: + internal-proxies: "192\\.168\\.\\d{1,3}\\.\\d{1,3}" +---- + +NOTE: You can trust all proxies by setting the `internal-proxies` to empty (but do not do so in production). + +You can take complete control of the configuration of Tomcat's `RemoteIpValve` by switching the automatic one off (to do so, set `server.forward-headers-strategy=NONE`) and adding a new valve instance using a `WebServerFactoryCustomizer` bean. + + + +[[howto.webserver.enable-multiple-connectors-in-tomcat]] +=== Enable Multiple Connectors with Tomcat +You can add an `org.apache.catalina.connector.Connector` to the `TomcatServletWebServerFactory`, which can allow multiple connectors, including HTTP and HTTPS connectors, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/webserver/enablemultipleconnectorsintomcat/MyTomcatConfiguration.java[] +---- + + + +[[howto.webserver.use-tomcat-legacycookieprocessor]] +=== Use Tomcat's LegacyCookieProcessor +By default, the embedded Tomcat used by Spring Boot does not support "Version 0" of the Cookie format, so you may see the following error: + +[indent=0] +---- + java.lang.IllegalArgumentException: An invalid character [32] was present in the Cookie value +---- + +If at all possible, you should consider updating your code to only store values compliant with later Cookie specifications. +If, however, you cannot change the way that cookies are written, you can instead configure Tomcat to use a `LegacyCookieProcessor`. +To switch to the `LegacyCookieProcessor`, use an `WebServerFactoryCustomizer` bean that adds a `TomcatContextCustomizer`, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/webserver/usetomcatlegacycookieprocessor/MyLegacyCookieProcessorConfiguration.java[] +---- + + + +[[howto.webserver.enable-tomcat-mbean-registry]] +=== Enable Tomcat's MBean Registry +Embedded Tomcat's MBean registry is disabled by default. +This minimizes Tomcat's memory footprint. +If you want to use Tomcat's MBeans, for example so that they can be used to expose metrics via Micrometer, you must use the configprop:server.tomcat.mbeanregistry.enabled[] property to do so, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- +server: + tomcat: + mbeanregistry: + enabled: true +---- + + + +[[howto.webserver.enable-multiple-listeners-in-undertow]] +=== Enable Multiple Listeners with Undertow +Add an `UndertowBuilderCustomizer` to the `UndertowServletWebServerFactory` and add a listener to the `Builder`, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/webserver/enablemultiplelistenersinundertow/MyUndertowConfiguration.java[] +---- + + + +[[howto.webserver.create-websocket-endpoints-using-serverendpoint]] +=== Create WebSocket Endpoints Using @ServerEndpoint +If you want to use `@ServerEndpoint` in a Spring Boot application that used an embedded container, you must declare a single `ServerEndpointExporter` `@Bean`, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/howto/webserver/createwebsocketendpointsusingserverendpoint/MyWebSocketConfiguration.java[] +---- + +The bean shown in the preceding example registers any `@ServerEndpoint` annotated beans with the underlying WebSocket container. +When deployed to a standalone servlet container, this role is performed by a servlet container initializer, and the `ServerEndpointExporter` bean is not required. diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/index-docinfo.xml b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/index-docinfo.xml similarity index 94% rename from spring-boot-project/spring-boot-docs/src/main/asciidoc/index-docinfo.xml rename to spring-boot-project/spring-boot-docs/src/docs/asciidoc/index-docinfo.xml index a05a95b2694d..f9d2c16c6fc0 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/index-docinfo.xml +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/index-docinfo.xml @@ -1,7 +1,7 @@ Spring Boot {spring-boot-version} - 2012-2019 + 2012-2022 diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/index.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/index.adoc new file mode 100644 index 000000000000..5741c7f5db0c --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/index.adoc @@ -0,0 +1,34 @@ +[[index]] += Spring Boot Reference Documentation +include::authors.adoc[] +v{spring-boot-version} +include::attributes.adoc[] + +This document is also available as {spring-boot-docs}/html/[Multi-page HTML], {spring-boot-docs}/htmlsingle/[Single page HTML] and {spring-boot-docs}/pdf/spring-boot-reference.pdf[PDF]. + + +The reference documentation consists of the following sections: + +[horizontal] +<> :: Legal information. +<> :: Resources for getting help. +<> :: About the Documentation, First Steps, and more. +<> :: Introducing Spring Boot, System Requirements, Servlet Containers, Installing Spring Boot, Developing Your First Spring Boot Application +<> :: Upgrading from 1.x, Upgrading to a new feature release, Upgrading the Spring Boot CLI +<> :: Build Systems, Structuring Your Code, Configuration, Spring Beans and Dependency Injection, DevTools, and more. +<> :: Profiles, Logging, Security, Caching, Spring Integration, Testing, and more. +<> :: Monitoring, Metrics, Auditing, and more. +<> :: Deploying to the Cloud, Installing as a Unix application. +<> :: Installing the CLI, Using the CLI, Configuring the CLI, and more. +<> :: Maven Plugin, Gradle Plugin, Antlib, and more. +<> :: Application Development, Configuration, Embedded Servers, Data Access, and many more. + +The reference documentation has the following appendices: + +[horizontal] +<> :: Common application properties that can be used to configure your application. +<> :: Metadata used to describe configuration properties. +<> :: Auto-configuration classes provided by Spring Boot. +<> :: Test auto-configuration annotations used to test slices of your application. +<> :: Spring Boot's executable jars, their launchers, and their format. +<> :: Details of the dependencies that are managed by Spring Boot. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/index.singleadoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/index.singleadoc new file mode 100644 index 000000000000..cf397c9a432f --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/index.singleadoc @@ -0,0 +1,50 @@ +[[spring-boot-reference-documentation]] += Spring Boot Reference Documentation +include::authors.adoc[] +v{spring-boot-version} +include::attributes.adoc[] + +This document is also available as {spring-boot-docs}/html/[Multi-page HTML], {spring-boot-docs}/htmlsingle/[Single page HTML] and {spring-boot-docs}/pdf/spring-boot-reference.pdf[PDF]. + + +include::legal.adoc[leveloffset=+1] + +include::getting-help.adoc[leveloffset=+1] + +include::documentation.adoc[leveloffset=+1] + +include::getting-started.adoc[leveloffset=+1] + +include::upgrading.adoc[leveloffset=+1] + +include::using.adoc[leveloffset=+1] + +include::features.adoc[leveloffset=+1] + +include::actuator.adoc[leveloffset=+1] + +include::deployment.adoc[leveloffset=+1] + +include::cli.adoc[leveloffset=+1] + +include::build-tool-plugins.adoc[leveloffset=+1] + +include::howto.adoc[leveloffset=+1] + + + +:sectnums!: +[[appendix]] +== Appendices + +include::application-properties.adoc[leveloffset=+2] + +include::configuration-metadata.adoc[leveloffset=+2] + +include::auto-configuration-classes.adoc[leveloffset=+2] + +include::test-auto-configuration.adoc[leveloffset=+2] + +include::executable-jar.adoc[leveloffset=+2] + +include::dependency-versions.adoc[leveloffset=+2] diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/legal.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/legal.adoc similarity index 82% rename from spring-boot-project/spring-boot-docs/src/main/asciidoc/legal.adoc rename to spring-boot-project/spring-boot-docs/src/docs/asciidoc/legal.adoc index ff14d6717775..2f29c53ab198 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/legal.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/legal.adoc @@ -1,9 +1,7 @@ -[legal] +[[legal]] = Legal -{spring-boot-version} - -Copyright © 2012-2019 +Copyright © 2012-2022 Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/test-auto-configuration.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/test-auto-configuration.adoc new file mode 100644 index 000000000000..23b985725e56 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/test-auto-configuration.adoc @@ -0,0 +1,12 @@ +[appendix] +[[appendix.test-auto-configuration]] += Test Auto-configuration Annotations +include::attributes.adoc[] + + + +This appendix describes the `@...Test` auto-configuration annotations that Spring Boot provides to test slices of your application. + + + +include::test-auto-configuration/slices.adoc[] diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/test-auto-configuration/slices.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/test-auto-configuration/slices.adoc new file mode 100644 index 000000000000..298de249c541 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/test-auto-configuration/slices.adoc @@ -0,0 +1,6 @@ +[[appendix.test-auto-configuration.slices]] +== Test Slices + +The following table lists the various `@...Test` annotations that can be used to test slices of your application and the auto-configuration that they import by default: + +include::documented-slices.adoc[] diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/upgrading.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/upgrading.adoc new file mode 100644 index 000000000000..a7d72ccb797e --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/upgrading.adoc @@ -0,0 +1,15 @@ +[[upgrading]] += Upgrading Spring Boot +include::attributes.adoc[] + +Instructions for how to upgrade from earlier versions of Spring Boot are provided on the project {github-wiki}[wiki]. +Follow the links in the {github-wiki}#release-notes[release notes] section to find the version that you want to upgrade to. + +Upgrading instructions are always the first item in the release notes. +If you are more than one release behind, please make sure that you also review the release notes of the versions that you jumped. + +include::upgrading/from-1x.adoc[] + +include::upgrading/to-feature.adoc[] + +include::upgrading/cli.adoc[] \ No newline at end of file diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/upgrading/cli.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/upgrading/cli.adoc new file mode 100644 index 000000000000..692c238105e8 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/upgrading/cli.adoc @@ -0,0 +1,5 @@ +[[upgrading.cli]] +== Upgrading the Spring Boot CLI + +To upgrade an existing CLI installation, use the appropriate package manager command (for example, `brew upgrade`). +If you manually installed the CLI, follow the <>, remembering to update your `PATH` environment variable to remove any older references. \ No newline at end of file diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/upgrading/from-1x.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/upgrading/from-1x.adoc new file mode 100644 index 000000000000..10b440cac895 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/upgrading/from-1x.adoc @@ -0,0 +1,5 @@ +[[upgrading.from-1x]] +== Upgrading from 1.x + +If you are upgrading from the `1.x` release of Spring Boot, check the {github-wiki}/Spring-Boot-2.0-Migration-Guide["`migration guide`" on the project wiki] that provides detailed upgrade instructions. +Check also the {github-wiki}["`release notes`"] for a list of "`new and noteworthy`" features for each release. \ No newline at end of file diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/upgrading/to-feature.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/upgrading/to-feature.adoc new file mode 100644 index 000000000000..1432559e6377 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/upgrading/to-feature.adoc @@ -0,0 +1,19 @@ +[[upgrading.to-feature]] +== Upgrading to a new feature release + +When upgrading to a new feature release, some properties may have been renamed or removed. +Spring Boot provides a way to analyze your application's environment and print diagnostics at startup, but also temporarily migrate properties at runtime for you. +To enable that feature, add the following dependency to your project: + +[source,xml,indent=0,subs="verbatim"] +---- + + org.springframework.boot + spring-boot-properties-migrator + runtime + +---- + +WARNING: Properties that are added late to the environment, such as when using `@PropertySource`, will not be taken into account. + +NOTE: Once you're done with the migration, please make sure to remove this module from your project's dependencies. \ No newline at end of file diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using.adoc new file mode 100644 index 000000000000..832abd9e8679 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using.adoc @@ -0,0 +1,34 @@ +[[using]] += Developing with Spring Boot +include::attributes.adoc[] + + + +This section goes into more detail about how you should use Spring Boot. +It covers topics such as build systems, auto-configuration, and how to run your applications. +We also cover some Spring Boot best practices. +Although there is nothing particularly special about Spring Boot (it is just another library that you can consume), there are a few recommendations that, when followed, make your development process a little easier. + +If you are starting out with Spring Boot, you should probably read the _<>_ guide before diving into this section. + + + +include::using/build-systems.adoc[] + +include::using/structuring-your-code.adoc[] + +include::using/configuration-classes.adoc[] + +include::using/auto-configuration.adoc[] + +include::using/spring-beans-and-dependency-injection.adoc[] + +include::using/using-the-springbootapplication-annotation.adoc[] + +include::using/running-your-application.adoc[] + +include::using/devtools.adoc[] + +include::using/packaging-for-production.adoc[] + +include::using/whats-next.adoc[] diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/auto-configuration.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/auto-configuration.adoc new file mode 100644 index 000000000000..d96ad654217f --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/auto-configuration.adoc @@ -0,0 +1,40 @@ +[[using.auto-configuration]] +== Auto-configuration +Spring Boot auto-configuration attempts to automatically configure your Spring application based on the jar dependencies that you have added. +For example, if `HSQLDB` is on your classpath, and you have not manually configured any database connection beans, then Spring Boot auto-configures an in-memory database. + +You need to opt-in to auto-configuration by adding the `@EnableAutoConfiguration` or `@SpringBootApplication` annotations to one of your `@Configuration` classes. + +TIP: You should only ever add one `@SpringBootApplication` or `@EnableAutoConfiguration` annotation. +We generally recommend that you add one or the other to your primary `@Configuration` class only. + + + +[[using.auto-configuration.replacing]] +=== Gradually Replacing Auto-configuration +Auto-configuration is non-invasive. +At any point, you can start to define your own configuration to replace specific parts of the auto-configuration. +For example, if you add your own `DataSource` bean, the default embedded database support backs away. + +If you need to find out what auto-configuration is currently being applied, and why, start your application with the `--debug` switch. +Doing so enables debug logs for a selection of core loggers and logs a conditions report to the console. + + + +[[using.auto-configuration.disabling-specific]] +=== Disabling Specific Auto-configuration Classes +If you find that specific auto-configuration classes that you do not want are being applied, you can use the exclude attribute of `@SpringBootApplication` to disable them, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/using/autoconfiguration/disablingspecific/MyApplication.java[] +---- + +If the class is not on the classpath, you can use the `excludeName` attribute of the annotation and specify the fully qualified name instead. +If you prefer to use `@EnableAutoConfiguration` rather than `@SpringBootApplication`, `exclude` and `excludeName` are also available. +Finally, you can also control the list of auto-configuration classes to exclude by using the configprop:spring.autoconfigure.exclude[] property. + +TIP: You can define exclusions both at the annotation level and by using the property. + +NOTE: Even though auto-configuration classes are `public`, the only aspect of the class that is considered public API is the name of the class which can be used for disabling the auto-configuration. +The actual contents of those classes, such as nested configuration classes or bean methods are for internal use only and we do not recommend using those directly. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/build-systems.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/build-systems.adoc new file mode 100644 index 000000000000..404ae1482a6a --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/build-systems.adoc @@ -0,0 +1,145 @@ +[[using.build-systems]] +== Build Systems +It is strongly recommended that you choose a build system that supports <> and that can consume artifacts published to the "`Maven Central`" repository. +We would recommend that you choose Maven or Gradle. +It is possible to get Spring Boot to work with other build systems (Ant, for example), but they are not particularly well supported. + + + +[[using.build-systems.dependency-management]] +=== Dependency Management +Each release of Spring Boot provides a curated list of dependencies that it supports. +In practice, you do not need to provide a version for any of these dependencies in your build configuration, as Spring Boot manages that for you. +When you upgrade Spring Boot itself, these dependencies are upgraded as well in a consistent way. + +NOTE: You can still specify a version and override Spring Boot's recommendations if you need to do so. + +The curated list contains all the Spring modules that you can use with Spring Boot as well as a refined list of third party libraries. +The list is available as a standard Bills of Materials (`spring-boot-dependencies`) that can be used with both <> and <>. + +WARNING: Each release of Spring Boot is associated with a base version of the Spring Framework. +We **highly** recommend that you not specify its version. + + + +[[using.build-systems.maven]] +=== Maven +To learn about using Spring Boot with Maven, please refer to the documentation for Spring Boot's Maven plugin: + +* Reference ({spring-boot-maven-plugin-docs}[HTML] and {spring-boot-maven-plugin-pdfdocs}[PDF]) +* {spring-boot-maven-plugin-api}[API] + + + +[[using.build-systems.gradle]] +=== Gradle +To learn about using Spring Boot with Gradle, please refer to the documentation for Spring Boot's Gradle plugin: + +* Reference ({spring-boot-gradle-plugin-docs}[HTML] and {spring-boot-gradle-plugin-pdfdocs}[PDF]) +* {spring-boot-gradle-plugin-api}[API] + + + +[[using.build-systems.ant]] +=== Ant +It is possible to build a Spring Boot project using Apache Ant+Ivy. +The `spring-boot-antlib` "`AntLib`" module is also available to help Ant create executable jars. + +To declare dependencies, a typical `ivy.xml` file looks something like the following example: + +[source,xml,indent=0,subs="verbatim"] +---- + + + + + + + + + + +---- + +A typical `build.xml` looks like the following example: + +[source,xml,indent=0,subs="verbatim,attributes"] +---- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +---- + +TIP: If you do not want to use the `spring-boot-antlib` module, see the _<>_ "`How-to`" . + + + +[[using.build-systems.starters]] +=== Starters +Starters are a set of convenient dependency descriptors that you can include in your application. +You get a one-stop shop for all the Spring and related technologies that you need without having to hunt through sample code and copy-paste loads of dependency descriptors. +For example, if you want to get started using Spring and JPA for database access, include the `spring-boot-starter-data-jpa` dependency in your project. + +The starters contain a lot of the dependencies that you need to get a project up and running quickly and with a consistent, supported set of managed transitive dependencies. + +.What's in a name +**** +All **official** starters follow a similar naming pattern; `+spring-boot-starter-*+`, where `+*+` is a particular type of application. +This naming structure is intended to help when you need to find a starter. +The Maven integration in many IDEs lets you search dependencies by name. +For example, with the appropriate Eclipse or Spring Tools plugin installed, you can press `ctrl-space` in the POM editor and type "`spring-boot-starter`" for a complete list. + +As explained in the "`<>`" section, third party starters should not start with `spring-boot`, as it is reserved for official Spring Boot artifacts. +Rather, a third-party starter typically starts with the name of the project. +For example, a third-party starter project called `thirdpartyproject` would typically be named `thirdpartyproject-spring-boot-starter`. +**** + +The following application starters are provided by Spring Boot under the `org.springframework.boot` group: + +.Spring Boot application starters +include::starters/application-starters.adoc[] + +In addition to the application starters, the following starters can be used to add _<>_ features: + +.Spring Boot production starters +include::starters/production-starters.adoc[] + +Finally, Spring Boot also includes the following starters that can be used if you want to exclude or swap specific technical facets: + +.Spring Boot technical starters +include::starters/technical-starters.adoc[] + +To learn how to swap technical facets, please see the how-to documentation for <> and <>. + +TIP: For a list of additional community contributed starters, see the {spring-boot-latest-code}/spring-boot-project/spring-boot-starters/README.adoc[README file] in the `spring-boot-starters` module on GitHub. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/configuration-classes.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/configuration-classes.adoc new file mode 100644 index 000000000000..b73af3b0fb32 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/configuration-classes.adoc @@ -0,0 +1,24 @@ +[[using.configuration-classes]] +== Configuration Classes +Spring Boot favors Java-based configuration. +Although it is possible to use `SpringApplication` with XML sources, we generally recommend that your primary source be a single `@Configuration` class. +Usually the class that defines the `main` method is a good candidate as the primary `@Configuration`. + +TIP: Many Spring configuration examples have been published on the Internet that use XML configuration. +If possible, always try to use the equivalent Java-based configuration. +Searching for `+Enable*+` annotations can be a good starting point. + + + +[[using.configuration-classes.importing-additional-configuration]] +=== Importing Additional Configuration Classes +You need not put all your `@Configuration` into a single class. +The `@Import` annotation can be used to import additional configuration classes. +Alternatively, you can use `@ComponentScan` to automatically pick up all Spring components, including `@Configuration` classes. + + + +[[using.configuration-classes.importing-xml-configuration]] +=== Importing XML Configuration +If you absolutely must use XML based configuration, we recommend that you still start with a `@Configuration` class. +You can then use an `@ImportResource` annotation to load XML configuration files. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/devtools.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/devtools.adoc new file mode 100644 index 000000000000..459c03bb72f2 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/devtools.adoc @@ -0,0 +1,434 @@ +[[using.devtools]] +== Developer Tools +Spring Boot includes an additional set of tools that can make the application development experience a little more pleasant. +The `spring-boot-devtools` module can be included in any project to provide additional development-time features. +To include devtools support, add the module dependency to your build, as shown in the following listings for Maven and Gradle: + +.Maven +[source,xml,indent=0,subs="verbatim"] +---- + + + org.springframework.boot + spring-boot-devtools + true + + +---- + +.Gradle +[source,gradle,indent=0,subs="verbatim"] +---- + dependencies { + developmentOnly("org.springframework.boot:spring-boot-devtools") + } +---- + +CAUTION: Devtools might cause classloading issues, in particular in multi-module projects. +<> explains how to diagnose and solve them. + +NOTE: Developer tools are automatically disabled when running a fully packaged application. +If your application is launched from `java -jar` or if it is started from a special classloader, then it is considered a "`production application`". +You can control this behavior by using the `spring.devtools.restart.enabled` system property. +To enable devtools, irrespective of the classloader used to launch your application, set the `-Dspring.devtools.restart.enabled=true` system property. +This must not be done in a production environment where running devtools is a security risk. +To disable devtools, exclude the dependency or set the `-Dspring.devtools.restart.enabled=false` system property. + +TIP: Flagging the dependency as optional in Maven or using the `developmentOnly` configuration in Gradle (as shown above) prevents devtools from being transitively applied to other modules that use your project. + +TIP: Repackaged archives do not contain devtools by default. +If you want to use a <>, you need to include it. +When using the Maven plugin, set the `excludeDevtools` property to `false`. +When using the Gradle plugin, {spring-boot-gradle-plugin-docs}#packaging-executable-configuring-including-development-only-dependencies[configure the task's classpath to include the `developmentOnly` configuration]. + + + +[[using.devtools.diagnosing-classloading-issues]] +=== Diagnosing Classloading Issues +As described in the <> section, restart functionality is implemented by using two classloaders. +For most applications, this approach works well. +However, it can sometimes cause classloading issues, in particular in multi-module projects. + +To diagnose whether the classloading issues are indeed caused by devtools and its two classloaders, <>. +If this solves your problems, <> to include your entire project. + + + +[[using.devtools.property-defaults]] +=== Property Defaults +Several of the libraries supported by Spring Boot use caches to improve performance. +For example, <> cache compiled templates to avoid repeatedly parsing template files. +Also, Spring MVC can add HTTP caching headers to responses when serving static resources. + +While caching is very beneficial in production, it can be counter-productive during development, preventing you from seeing the changes you just made in your application. +For this reason, spring-boot-devtools disables the caching options by default. + +Cache options are usually configured by settings in your `application.properties` file. +For example, Thymeleaf offers the configprop:spring.thymeleaf.cache[] property. +Rather than needing to set these properties manually, the `spring-boot-devtools` module automatically applies sensible development-time configuration. + +The following table lists all the properties that are applied: + +include::devtools-property-defaults.adoc[] + +NOTE: If you don't want property defaults to be applied you can set configprop:spring.devtools.add-properties[] to `false` in your `application.properties`. + +Because you need more information about web requests while developing Spring MVC and Spring WebFlux applications, developer tools suggests you to enable `DEBUG` logging for the `web` logging group. +This will give you information about the incoming request, which handler is processing it, the response outcome, etc. +If you wish to log all request details (including potentially sensitive information), you can turn on the configprop:spring.mvc.log-request-details[] or configprop:spring.codec.log-request-details[] configuration properties. + + + +[[using.devtools.restart]] +=== Automatic Restart +Applications that use `spring-boot-devtools` automatically restart whenever files on the classpath change. +This can be a useful feature when working in an IDE, as it gives a very fast feedback loop for code changes. +By default, any entry on the classpath that points to a directory is monitored for changes. +Note that certain resources, such as static assets and view templates, <>. + +.Triggering a restart +**** +As DevTools monitors classpath resources, the only way to trigger a restart is to update the classpath. +Whether you're using an IDE or one of the build plugins, the modified files have to be recompiled to trigger a restart. +The way in which you cause the classpath to be updated depends on the tool that you are using: + +* In Eclipse, saving a modified file causes the classpath to be updated and triggers a restart. +* In IntelliJ IDEA, building the project (`Build +->+ Build Project`) has the same effect. +* If using a build plugin, running `mvn compile` for Maven or `gradle build` for Gradle will trigger a restart. +**** + +NOTE: If you are restarting with Maven or Gradle using the build plugin you must leave the `forking` set to `enabled`. +If you disable forking, the isolated application classloader used by devtools will not be created and restarts will not operate properly. + +TIP: Automatic restart works very well when used with LiveReload. +<> for details. +If you use JRebel, automatic restarts are disabled in favor of dynamic class reloading. +Other devtools features (such as LiveReload and property overrides) can still be used. + +NOTE: DevTools relies on the application context's shutdown hook to close it during a restart. +It does not work correctly if you have disabled the shutdown hook (`SpringApplication.setRegisterShutdownHook(false)`). + +NOTE: DevTools needs to customize the `ResourceLoader` used by the `ApplicationContext`. +If your application provides one already, it is going to be wrapped. +Direct override of the `getResource` method on the `ApplicationContext` is not supported. + +CAUTION: Automatic restart is not supported when using AspectJ weaving. + +[[using-spring-boot-restart-vs-reload]] +.Restart vs Reload +**** +The restart technology provided by Spring Boot works by using two classloaders. +Classes that do not change (for example, those from third-party jars) are loaded into a _base_ classloader. +Classes that you are actively developing are loaded into a _restart_ classloader. +When the application is restarted, the _restart_ classloader is thrown away and a new one is created. +This approach means that application restarts are typically much faster than "`cold starts`", since the _base_ classloader is already available and populated. + +If you find that restarts are not quick enough for your applications or you encounter classloading issues, you could consider reloading technologies such as https://jrebel.com/software/jrebel/[JRebel] from ZeroTurnaround. +These work by rewriting classes as they are loaded to make them more amenable to reloading. +**** + + + +[[using.devtools.restart.logging-condition-delta]] +==== Logging changes in condition evaluation +By default, each time your application restarts, a report showing the condition evaluation delta is logged. +The report shows the changes to your application's auto-configuration as you make changes such as adding or removing beans and setting configuration properties. + +To disable the logging of the report, set the following property: + +[source,yaml,indent=0,subs="verbatim",configblocks] +---- + spring: + devtools: + restart: + log-condition-evaluation-delta: false +---- + + + +[[using.devtools.restart.excluding-resources]] +==== Excluding Resources +Certain resources do not necessarily need to trigger a restart when they are changed. +For example, Thymeleaf templates can be edited in-place. +By default, changing resources in `/META-INF/maven`, `/META-INF/resources`, `/resources`, `/static`, `/public`, or `/templates` does not trigger a restart but does trigger a <>. +If you want to customize these exclusions, you can use the configprop:spring.devtools.restart.exclude[] property. +For example, to exclude only `/static` and `/public` you would set the following property: + +[source,yaml,indent=0,subs="verbatim",configblocks] +---- + spring: + devtools: + restart: + exclude: "static/**,public/**" +---- + +TIP: If you want to keep those defaults and _add_ additional exclusions, use the configprop:spring.devtools.restart.additional-exclude[] property instead. + + + +[[using.devtools.restart.watching-additional-paths]] +==== Watching Additional Paths +You may want your application to be restarted or reloaded when you make changes to files that are not on the classpath. +To do so, use the configprop:spring.devtools.restart.additional-paths[] property to configure additional paths to watch for changes. +You can use the configprop:spring.devtools.restart.exclude[] property <> to control whether changes beneath the additional paths trigger a full restart or a <>. + + + +[[using.devtools.restart.disable]] +==== Disabling Restart +If you do not want to use the restart feature, you can disable it by using the configprop:spring.devtools.restart.enabled[] property. +In most cases, you can set this property in your `application.properties` (doing so still initializes the restart classloader, but it does not watch for file changes). + +If you need to _completely_ disable restart support (for example, because it does not work with a specific library), you need to set the configprop:spring.devtools.restart.enabled[] `System` property to `false` before calling `SpringApplication.run(...)`, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/devtools/restart/disable/MyApplication.java[] +---- + + + +[[using.devtools.restart.triggerfile]] +==== Using a Trigger File +If you work with an IDE that continuously compiles changed files, you might prefer to trigger restarts only at specific times. +To do so, you can use a "`trigger file`", which is a special file that must be modified when you want to actually trigger a restart check. + +NOTE: Any update to the file will trigger a check, but restart only actually occurs if Devtools has detected it has something to do. + +To use a trigger file, set the configprop:spring.devtools.restart.trigger-file[] property to the name (excluding any path) of your trigger file. +The trigger file must appear somewhere on your classpath. + +For example, if you have a project with the following structure: + +[indent=0] +---- + src + +- main + +- resources + +- .reloadtrigger +---- + +Then your `trigger-file` property would be: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + devtools: + restart: + trigger-file: ".reloadtrigger" +---- + +Restarts will now only happen when the `src/main/resources/.reloadtrigger` is updated. + +TIP: You might want to set `spring.devtools.restart.trigger-file` as a <>, so that all your projects behave in the same way. + +Some IDEs have features that save you from needing to update your trigger file manually. +https://spring.io/tools[Spring Tools for Eclipse] and https://www.jetbrains.com/idea/[IntelliJ IDEA (Ultimate Edition)] both have such support. +With Spring Tools, you can use the "`reload`" button from the console view (as long as your `trigger-file` is named `.reloadtrigger`). +For IntelliJ IDEA, you can follow the https://www.jetbrains.com/help/idea/spring-boot.html#application-update-policies[instructions in their documentation]. + + + +[[using.devtools.restart.customizing-the-classload]] +==== Customizing the Restart Classloader +As described earlier in the <> section, restart functionality is implemented by using two classloaders. +If this causes issues, you might need to customize what gets loaded by which classloader. + +By default, any open project in your IDE is loaded with the "`restart`" classloader, and any regular `.jar` file is loaded with the "`base`" classloader. +The same is true if you use `mvn spring-boot:run` or `gradle bootRun`: the project containing your `@SpringBootApplication` is loaded with the "`restart`" classloader, and everything else with the "`base`" classloader. + +You can instruct Spring Boot to load parts of your project with a different classloader by creating a `META-INF/spring-devtools.properties` file. +The `spring-devtools.properties` file can contain properties prefixed with `restart.exclude` and `restart.include`. +The `include` elements are items that should be pulled up into the "`restart`" classloader, and the `exclude` elements are items that should be pushed down into the "`base`" classloader. +The value of the property is a regex pattern that is applied to the classpath, as shown in the following example: + +[source,yaml,indent=0,subs="verbatim",configblocks] +---- + restart: + exclude: + companycommonlibs: "/mycorp-common-[\\w\\d-\\.]+\\.jar" + include: + projectcommon: "/mycorp-myproj-[\\w\\d-\\.]+\\.jar" +---- + +NOTE: All property keys must be unique. +As long as a property starts with `restart.include.` or `restart.exclude.` it is considered. + +TIP: All `META-INF/spring-devtools.properties` from the classpath are loaded. +You can package files inside your project, or in the libraries that the project consumes. + + + +[[using.devtools.restart.limitations]] +==== Known Limitations +Restart functionality does not work well with objects that are deserialized by using a standard `ObjectInputStream`. +If you need to deserialize data, you may need to use Spring's `ConfigurableObjectInputStream` in combination with `Thread.currentThread().getContextClassLoader()`. + +Unfortunately, several third-party libraries deserialize without considering the context classloader. +If you find such a problem, you need to request a fix with the original authors. + + + +[[using.devtools.livereload]] +=== LiveReload +The `spring-boot-devtools` module includes an embedded LiveReload server that can be used to trigger a browser refresh when a resource is changed. +LiveReload browser extensions are freely available for Chrome, Firefox and Safari from http://livereload.com/extensions/[livereload.com]. + +If you do not want to start the LiveReload server when your application runs, you can set the configprop:spring.devtools.livereload.enabled[] property to `false`. + +NOTE: You can only run one LiveReload server at a time. +Before starting your application, ensure that no other LiveReload servers are running. +If you start multiple applications from your IDE, only the first has LiveReload support. + +WARNING: To trigger LiveReload when a file changes, <> must be enabled. + + + +[[using.devtools.globalsettings]] +=== Global Settings +You can configure global devtools settings by adding any of the following files to the `$HOME/.config/spring-boot` directory: + +. `spring-boot-devtools.properties` +. `spring-boot-devtools.yaml` +. `spring-boot-devtools.yml` + +Any properties added to these file apply to _all_ Spring Boot applications on your machine that use devtools. +For example, to configure restart to always use a <>, you would add the following property to your `spring-boot-devtools` file: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + devtools: + restart: + trigger-file: ".reloadtrigger" +---- + +NOTE: If devtools configuration files are not found in `$HOME/.config/spring-boot`, the root of the `$HOME` directory is searched for the presence of a `.spring-boot-devtools.properties` file. +This allows you to share the devtools global configuration with applications that are on an older version of Spring Boot that does not support the `$HOME/.config/spring-boot` location. + +[NOTE] +==== +Profiles are not supported in devtools properties/yaml files. + +Any profiles activated in `.spring-boot-devtools.properties` will not affect the loading of <>. +Profile specific filenames (of the form `spring-boot-devtools-.properties`) and `spring.config.activate.on-profile` documents in both YAML and Properties files are not supported. +==== + + + +[[using.devtools.globalsettings.configuring-file-system-watcher]] +==== Configuring File System Watcher +{spring-boot-devtools-module-code}/filewatch/FileSystemWatcher.java[FileSystemWatcher] works by polling the class changes with a certain time interval, and then waiting for a predefined quiet period to make sure there are no more changes. +Since Spring Boot relies entirely on the IDE to compile and copy files into the location from where Spring Boot can read them, you might find that there are times when certain changes are not reflected when devtools restarts the application. +If you observe such problems constantly, try increasing the `spring.devtools.restart.poll-interval` and `spring.devtools.restart.quiet-period` parameters to the values that fit your development environment: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + devtools: + restart: + poll-interval: "2s" + quiet-period: "1s" +---- + +The monitored classpath directories are now polled every 2 seconds for changes, and a 1 second quiet period is maintained to make sure there are no additional class changes. + + + +[[using.devtools.remote-applications]] +=== Remote Applications +The Spring Boot developer tools are not limited to local development. +You can also use several features when running applications remotely. +Remote support is opt-in as enabling it can be a security risk. +It should only be enabled when running on a trusted network or when secured with SSL. +If neither of these options is available to you, you should not use DevTools' remote support. +You should never enable support on a production deployment. + +To enable it, you need to make sure that `devtools` is included in the repackaged archive, as shown in the following listing: + +[source,xml,indent=0,subs="verbatim"] +---- + + + + org.springframework.boot + spring-boot-maven-plugin + + false + + + + +---- + +Then you need to set the configprop:spring.devtools.remote.secret[] property. +Like any important password or secret, the value should be unique and strong such that it cannot be guessed or brute-forced. + +Remote devtools support is provided in two parts: a server-side endpoint that accepts connections and a client application that you run in your IDE. +The server component is automatically enabled when the configprop:spring.devtools.remote.secret[] property is set. +The client component must be launched manually. + +NOTE: Remote devtools is not supported for Spring WebFlux applications. + + + +[[using.devtools.remote-applications.client]] +==== Running the Remote Client Application +The remote client application is designed to be run from within your IDE. +You need to run `org.springframework.boot.devtools.RemoteSpringApplication` with the same classpath as the remote project that you connect to. +The application's single required argument is the remote URL to which it connects. + +For example, if you are using Eclipse or Spring Tools and you have a project named `my-app` that you have deployed to Cloud Foundry, you would do the following: + +* Select `Run Configurations...` from the `Run` menu. +* Create a new `Java Application` "`launch configuration`". +* Browse for the `my-app` project. +* Use `org.springframework.boot.devtools.RemoteSpringApplication` as the main class. +* Add `+++https://myapp.cfapps.io+++` to the `Program arguments` (or whatever your remote URL is). + +A running remote client might resemble the following listing: + +[indent=0,subs="verbatim,attributes"] +---- + . ____ _ __ _ _ + /\\ / ___'_ __ _ _(_)_ __ __ _ ___ _ \ \ \ \ + ( ( )\___ | '_ | '_| | '_ \/ _` | | _ \___ _ __ ___| |_ ___ \ \ \ \ + \\/ ___)| |_)| | | | | || (_| []::::::[] / -_) ' \/ _ \ _/ -_) ) ) ) ) + ' |____| .__|_| |_|_| |_\__, | |_|_\___|_|_|_\___/\__\___|/ / / / + =========|_|==============|___/===================================/_/_/_/ + :: Spring Boot Remote :: {spring-boot-version} + + 2015-06-10 18:25:06.632 INFO 14938 --- [ main] o.s.b.devtools.RemoteSpringApplication : Starting RemoteSpringApplication on pwmbp with PID 14938 (/Users/pwebb/projects/spring-boot/code/spring-boot-project/spring-boot-devtools/target/classes started by pwebb in /Users/pwebb/projects/spring-boot/code) + 2015-06-10 18:25:06.671 INFO 14938 --- [ main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@2a17b7b6: startup date [Wed Jun 10 18:25:06 PDT 2015]; root of context hierarchy + 2015-06-10 18:25:07.043 WARN 14938 --- [ main] o.s.b.d.r.c.RemoteClientConfiguration : The connection to http://localhost:8080 is insecure. You should use a URL starting with 'https://'. + 2015-06-10 18:25:07.074 INFO 14938 --- [ main] o.s.b.d.a.OptionalLiveReloadServer : LiveReload server is running on port 35729 + 2015-06-10 18:25:07.130 INFO 14938 --- [ main] o.s.b.devtools.RemoteSpringApplication : Started RemoteSpringApplication in 0.74 seconds (JVM running for 1.105) +---- + +NOTE: Because the remote client is using the same classpath as the real application it can directly read application properties. +This is how the configprop:spring.devtools.remote.secret[] property is read and passed to the server for authentication. + +TIP: It is always advisable to use `https://` as the connection protocol, so that traffic is encrypted and passwords cannot be intercepted. + +TIP: If you need to use a proxy to access the remote application, configure the `spring.devtools.remote.proxy.host` and `spring.devtools.remote.proxy.port` properties. + + + +[[using.devtools.remote-applications.update]] +==== Remote Update +The remote client monitors your application classpath for changes in the same way as the <>. +Any updated resource is pushed to the remote application and (_if required_) triggers a restart. +This can be helpful if you iterate on a feature that uses a cloud service that you do not have locally. +Generally, remote updates and restarts are much quicker than a full rebuild and deploy cycle. + +On a slower development environment, it may happen that the quiet period is not enough, and the changes in the classes may be split into batches. +The server is restarted after the first batch of class changes is uploaded. +The next batch can’t be sent to the application, since the server is restarting. + +This is typically manifested by a warning in the `RemoteSpringApplication` logs about failing to upload some of the classes, and a consequent retry. +But it may also lead to application code inconsistency and failure to restart after the first batch of changes is uploaded. +If you observe such problems constantly, try increasing the `spring.devtools.restart.poll-interval` and `spring.devtools.restart.quiet-period` parameters to the values that fit your development environment. +See the <> section for configuring these properties. + +NOTE: Files are only monitored when the remote client is running. +If you change a file before starting the remote client, it is not pushed to the remote server. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/packaging-for-production.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/packaging-for-production.adoc new file mode 100644 index 000000000000..2adc987456e9 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/packaging-for-production.adoc @@ -0,0 +1,7 @@ +[[using.packaging-for-production]] +== Packaging Your Application for Production +Executable jars can be used for production deployment. +As they are self-contained, they are also ideally suited for cloud-based deployment. + +For additional "`production ready`" features, such as health, auditing, and metric REST or JMX end-points, consider adding `spring-boot-actuator`. +See _<>_ for details. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/running-your-application.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/running-your-application.adoc new file mode 100644 index 000000000000..857c5cae6ea7 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/running-your-application.adoc @@ -0,0 +1,95 @@ +[[using.running-your-application]] +== Running Your Application +One of the biggest advantages of packaging your application as a jar and using an embedded HTTP server is that you can run your application as you would any other. +The sample applies to debugging Spring Boot applications. +You do not need any special IDE plugins or extensions. + +NOTE: This section only covers jar based packaging. +If you choose to package your application as a war file, you should refer to your server and IDE documentation. + + + +[[using.running-your-application.from-an-ide]] +=== Running from an IDE +You can run a Spring Boot application from your IDE as a Java application. +However, you first need to import your project. +Import steps vary depending on your IDE and build system. +Most IDEs can import Maven projects directly. +For example, Eclipse users can select `Import...` -> `Existing Maven Projects` from the `File` menu. + +If you cannot directly import your project into your IDE, you may be able to generate IDE metadata by using a build plugin. +Maven includes plugins for https://maven.apache.org/plugins/maven-eclipse-plugin/[Eclipse] and https://maven.apache.org/plugins/maven-idea-plugin/[IDEA]. +Gradle offers plugins for {gradle-docs}/userguide.html[various IDEs]. + +TIP: If you accidentally run a web application twice, you see a "`Port already in use`" error. +Spring Tools users can use the `Relaunch` button rather than the `Run` button to ensure that any existing instance is closed. + + + +[[using.running-your-application.as-a-packaged-application]] +=== Running as a Packaged Application +If you use the Spring Boot Maven or Gradle plugins to create an executable jar, you can run your application using `java -jar`, as shown in the following example: + +[source,shell,indent=0,subs="verbatim"] +---- + $ java -jar target/myapplication-0.0.1-SNAPSHOT.jar +---- + +It is also possible to run a packaged application with remote debugging support enabled. +Doing so lets you attach a debugger to your packaged application, as shown in the following example: + +[source,shell,indent=0,subs="verbatim"] +---- + $ java -Xdebug -Xrunjdwp:server=y,transport=dt_socket,address=8000,suspend=n \ + -jar target/myapplication-0.0.1-SNAPSHOT.jar +---- + + + +[[using.running-your-application.with-the-maven-plugin]] +=== Using the Maven Plugin +The Spring Boot Maven plugin includes a `run` goal that can be used to quickly compile and run your application. +Applications run in an exploded form, as they do in your IDE. +The following example shows a typical Maven command to run a Spring Boot application: + +[source,shell,indent=0,subs="verbatim"] +---- + $ mvn spring-boot:run +---- + +You might also want to use the `MAVEN_OPTS` operating system environment variable, as shown in the following example: + +[source,shell,indent=0,subs="verbatim"] +---- + $ export MAVEN_OPTS=-Xmx1024m +---- + + + +[[using.running-your-application.with-the-gradle-plugin]] +=== Using the Gradle Plugin +The Spring Boot Gradle plugin also includes a `bootRun` task that can be used to run your application in an exploded form. +The `bootRun` task is added whenever you apply the `org.springframework.boot` and `java` plugins and is shown in the following example: + +[indent=0,subs="verbatim"] +---- + $ gradle bootRun +---- + +You might also want to use the `JAVA_OPTS` operating system environment variable, as shown in the following example: + +[indent=0,subs="verbatim"] +---- + $ export JAVA_OPTS=-Xmx1024m +---- + + + +[[using.running-your-application.hot-swapping]] +=== Hot Swapping +Since Spring Boot applications are plain Java applications, JVM hot-swapping should work out of the box. +JVM hot swapping is somewhat limited with the bytecode that it can replace. +For a more complete solution, https://www.jrebel.com/products/jrebel[JRebel] can be used. + +The `spring-boot-devtools` module also includes support for quick application restarts. +See the <> for details. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/spring-beans-and-dependency-injection.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/spring-beans-and-dependency-injection.adoc new file mode 100644 index 000000000000..1c0347bba560 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/spring-beans-and-dependency-injection.adoc @@ -0,0 +1,23 @@ +[[using.spring-beans-and-dependency-injection]] +== Spring Beans and Dependency Injection +You are free to use any of the standard Spring Framework techniques to define your beans and their injected dependencies. +We generally recommend using constructor injection to wire up dependencies and `@ComponentScan` to find beans. + +If you structure your code as suggested above (locating your application class in a top package), you can add `@ComponentScan` without any arguments or use the `@SpringBootApplication` annotation which implicitly includes it. +All of your application components (`@Component`, `@Service`, `@Repository`, `@Controller` etc.) are automatically registered as Spring Beans. + +The following example shows a `@Service` Bean that uses constructor injection to obtain a required `RiskAssessor` bean: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/using/springbeansanddependencyinjection/singleconstructor/MyAccountService.java[] +---- + +If a bean has more than one constructor, you'll need to mark the one you want Spring to use with `@Autowired`: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/using/springbeansanddependencyinjection/multipleconstructors/MyAccountService.java[] +---- + +TIP: Notice how using constructor injection lets the `riskAssessor` field be marked as `final`, indicating that it cannot be subsequently changed. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/structuring-your-code.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/structuring-your-code.adoc new file mode 100644 index 000000000000..a3b022a007a8 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/structuring-your-code.adoc @@ -0,0 +1,54 @@ +[[using.structuring-your-code]] +== Structuring Your Code +Spring Boot does not require any specific code layout to work. +However, there are some best practices that help. + + + +[[using.structuring-your-code.using-the-default-package]] +=== Using the "`default`" Package +When a class does not include a `package` declaration, it is considered to be in the "`default package`". +The use of the "`default package`" is generally discouraged and should be avoided. +It can cause particular problems for Spring Boot applications that use the `@ComponentScan`, `@ConfigurationPropertiesScan`, `@EntityScan`, or `@SpringBootApplication` annotations, since every class from every jar is read. + +TIP: We recommend that you follow Java's recommended package naming conventions and use a reversed domain name (for example, `com.example.project`). + + + +[[using.structuring-your-code.locating-the-main-class]] +=== Locating the Main Application Class +We generally recommend that you locate your main application class in a root package above other classes. +The <> is often placed on your main class, and it implicitly defines a base "`search package`" for certain items. +For example, if you are writing a JPA application, the package of the `@SpringBootApplication` annotated class is used to search for `@Entity` items. +Using a root package also allows component scan to apply only on your project. + +TIP: If you don't want to use `@SpringBootApplication`, the `@EnableAutoConfiguration` and `@ComponentScan` annotations that it imports defines that behavior so you can also use those instead. + +The following listing shows a typical layout: + +[indent=0] +---- + com + +- example + +- myapplication + +- MyApplication.java + | + +- customer + | +- Customer.java + | +- CustomerController.java + | +- CustomerService.java + | +- CustomerRepository.java + | + +- order + +- Order.java + +- OrderController.java + +- OrderService.java + +- OrderRepository.java +---- + +The `MyApplication.java` file would declare the `main` method, along with the basic `@SpringBootApplication`, as follows: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/using/structuringyourcode/locatingthemainclass/MyApplication.java[] +---- diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/using-the-springbootapplication-annotation.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/using-the-springbootapplication-annotation.adoc new file mode 100644 index 000000000000..f3edc3df127c --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/using-the-springbootapplication-annotation.adoc @@ -0,0 +1,29 @@ +[[using.using-the-springbootapplication-annotation]] +== Using the @SpringBootApplication Annotation +Many Spring Boot developers like their apps to use auto-configuration, component scan and be able to define extra configuration on their "application class". +A single `@SpringBootApplication` annotation can be used to enable those three features, that is: + +* `@EnableAutoConfiguration`: enable <> +* `@ComponentScan`: enable `@Component` scan on the package where the application is located (see <>) +* `@SpringBootConfiguration`: enable registration of extra beans in the context or the import of additional configuration classes. +An alternative to Spring's standard `@Configuration` that aids <> in your integration tests. + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/using/usingthespringbootapplicationannotation/springapplication/MyApplication.java[] +---- + +NOTE: `@SpringBootApplication` also provides aliases to customize the attributes of `@EnableAutoConfiguration` and `@ComponentScan`. + +[NOTE] +==== +None of these features are mandatory and you may choose to replace this single annotation by any of the features that it enables. +For instance, you may not want to use component scan or configuration properties scan in your application: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/using/usingthespringbootapplicationannotation/individualannotations/MyApplication.java[] +---- + +In this example, `MyApplication` is just like any other Spring Boot application except that `@Component`-annotated classes and `@ConfigurationProperties`-annotated classes are not detected automatically and the user-defined beans are imported explicitly (see `@Import`). +==== diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/whats-next.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/whats-next.adoc new file mode 100644 index 000000000000..6aaf2da0ba21 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/whats-next.adoc @@ -0,0 +1,4 @@ +[[using.whats-next]] +== What to Read Next +You should now understand how you can use Spring Boot and some best practices that you should follow. +You can now go on to learn about specific _<>_ in depth, or you could skip ahead and read about the "`<>`" aspects of Spring Boot. diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc deleted file mode 100644 index 1afb9c5e9279..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ /dev/null @@ -1,78 +0,0 @@ -:numbered!: -[appendix] -[[common-application-properties]] -= Common Application properties -include::{asciidoc-sources-root}/attributes.adoc[] - -Various properties can be specified inside your `application.properties` file, inside your `application.yml` file, or as command line switches. -This appendix provides a list of common Spring Boot properties and references to the underlying classes that consume them. - -TIP: Spring Boot provides various conversion mechanism with advanced value formatting, make sure to review <>. - -NOTE: Property contributions can come from additional jar files on your classpath, so you should not consider this an exhaustive list. -Also, you can define your own properties. - - -== Core properties - -include::{generated-resources-root}/config-docs/core.adoc[] - -== Cache properties - -include::{generated-resources-root}/config-docs/cache.adoc[] - -== Mail properties - -include::{generated-resources-root}/config-docs/mail.adoc[] - -== JSON properties - -include::{generated-resources-root}/config-docs/json.adoc[] - -== Data properties - -include::{generated-resources-root}/config-docs/data.adoc[] - -== Transaction properties - -include::{generated-resources-root}/config-docs/transaction.adoc[] - -== Data migration properties - -include::{generated-resources-root}/config-docs/data-migration.adoc[] - -== Integration properties - -include::{generated-resources-root}/config-docs/integration.adoc[] - -== Web properties - -include::{generated-resources-root}/config-docs/web.adoc[] - -== Templating properties - -include::{generated-resources-root}/config-docs/templating.adoc[] - -== Server properties - -include::{generated-resources-root}/config-docs/server.adoc[] - -== Security properties - -include::{generated-resources-root}/config-docs/security.adoc[] - -== RSocket properties - -include::{generated-resources-root}/config-docs/rsocket.adoc[] - -== Actuator properties - -include::{generated-resources-root}/config-docs/actuator.adoc[] - -== Devtools properties - -include::{generated-resources-root}/config-docs/devtools.adoc[] - -== Testing properties - -include::{generated-resources-root}/config-docs/testing.adoc[] diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-auto-configuration-classes.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-auto-configuration-classes.adoc deleted file mode 100644 index bfcbcf21012c..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-auto-configuration-classes.adoc +++ /dev/null @@ -1,24 +0,0 @@ -[appendix] -[[auto-configuration-classes]] -= Auto-configuration Classes -include::{asciidoc-sources-root}/attributes.adoc[] - -This appendix contains details of all of the auto-configuration classes provided by Spring Boot, with links to documentation and source code. -Remember to also look at the conditions report in your application for more details of which features are switched on. -(To do so, start the app with `--debug` or `-Ddebug` or, in an Actuator application, use the `conditions` endpoint). - - - -[[auto-configuration-classes-from-autoconfigure-module]] -== `spring-boot-autoconfigure` -The following auto-configuration classes are from the `spring-boot-autoconfigure` module: - -include::{generated-resources-root}/auto-configuration-classes-spring-boot-autoconfigure.adoc[] - - - -[[auto-configuration-classes-from-actuator]] -== `spring-boot-actuator-autoconfigure` -The following auto-configuration classes are from the `spring-boot-actuator-autoconfigure` module: - -include::{generated-resources-root}/auto-configuration-classes-spring-boot-actuator-autoconfigure.adoc[] diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-configuration-metadata.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-configuration-metadata.adoc deleted file mode 100644 index 23ddc25c53f3..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-configuration-metadata.adoc +++ /dev/null @@ -1,860 +0,0 @@ -[appendix] -[[configuration-metadata]] -= Configuration Metadata -include::{asciidoc-sources-root}/attributes.adoc[] - -Spring Boot jars include metadata files that provide details of all supported configuration properties. -The files are designed to let IDE developers offer contextual help and "`code completion`" as users are working with `application.properties` or `application.yml` files. - -The majority of the metadata file is generated automatically at compile time by processing all items annotated with `@ConfigurationProperties`. -However, it is possible to <> for corner cases or more advanced use cases. - - - -[[configuration-metadata-format]] -== Metadata Format -Configuration metadata files are located inside jars under `META-INF/spring-configuration-metadata.json`. -They use a simple JSON format with items categorized under either "`groups`" or "`properties`" and additional values hints categorized under "hints", as shown in the following example: - -[source,json,indent=0] ----- - {"groups": [ - { - "name": "server", - "type": "org.springframework.boot.autoconfigure.web.ServerProperties", - "sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties" - }, - { - "name": "spring.jpa.hibernate", - "type": "org.springframework.boot.autoconfigure.orm.jpa.JpaProperties$Hibernate", - "sourceType": "org.springframework.boot.autoconfigure.orm.jpa.JpaProperties", - "sourceMethod": "getHibernate()" - } - ... - ],"properties": [ - { - "name": "server.port", - "type": "java.lang.Integer", - "sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties" - }, - { - "name": "server.address", - "type": "java.net.InetAddress", - "sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties" - }, - { - "name": "spring.jpa.hibernate.ddl-auto", - "type": "java.lang.String", - "description": "DDL mode. This is actually a shortcut for the \"hibernate.hbm2ddl.auto\" property.", - "sourceType": "org.springframework.boot.autoconfigure.orm.jpa.JpaProperties$Hibernate" - } - ... - ],"hints": [ - { - "name": "spring.jpa.hibernate.ddl-auto", - "values": [ - { - "value": "none", - "description": "Disable DDL handling." - }, - { - "value": "validate", - "description": "Validate the schema, make no changes to the database." - }, - { - "value": "update", - "description": "Update the schema if necessary." - }, - { - "value": "create", - "description": "Create the schema and destroy previous data." - }, - { - "value": "create-drop", - "description": "Create and then destroy the schema at the end of the session." - } - ] - } - ]} ----- - -Each "`property`" is a configuration item that the user specifies with a given value. -For example, `server.port` and `server.address` might be specified in `application.properties`, as follows: - -[source,properties,indent=0,configprops] ----- - server.port=9090 - server.address=127.0.0.1 ----- - -The "`groups`" are higher level items that do not themselves specify a value but instead provide a contextual grouping for properties. -For example, the `server.port` and `server.address` properties are part of the `server` group. - -NOTE: It is not required that every "`property`" has a "`group`". -Some properties might exist in their own right. - -Finally, "`hints`" are additional information used to assist the user in configuring a given property. -For example, when a developer is configuring the configprop:spring.jpa.hibernate.ddl-auto[] property, a tool can use the hints to offer some auto-completion help for the `none`, `validate`, `update`, `create`, and `create-drop` values. - - - -[[configuration-metadata-group-attributes]] -=== Group Attributes -The JSON object contained in the `groups` array can contain the attributes shown in the following table: - -[cols="1,1,4"] -|=== -| Name | Type | Purpose - -| `name` -| String -| The full name of the group. - This attribute is mandatory. - -| `type` -| String -| The class name of the data type of the group. - For example, if the group were based on a class annotated with `@ConfigurationProperties`, the attribute would contain the fully qualified name of that class. - If it were based on a `@Bean` method, it would be the return type of that method. - If the type is not known, the attribute may be omitted. - -| `description` -| String -| A short description of the group that can be displayed to users. - If no description is available, it may be omitted. - It is recommended that descriptions be short paragraphs, with the first line providing a concise summary. - The last line in the description should end with a period (`.`). - -| `sourceType` -| String -| The class name of the source that contributed this group. - For example, if the group were based on a `@Bean` method annotated with `@ConfigurationProperties`, this attribute would contain the fully qualified name of the `@Configuration` class that contains the method. - If the source type is not known, the attribute may be omitted. - -| `sourceMethod` -| String -| The full name of the method (include parenthesis and argument types) that contributed this group (for example, the name of a `@ConfigurationProperties` annotated `@Bean` method). - If the source method is not known, it may be omitted. -|=== - - - -[[configuration-metadata-property-attributes]] -=== Property Attributes -The JSON object contained in the `properties` array can contain the attributes described in the following table: - -[cols="1,1,4"] -|=== -| Name | Type | Purpose - -| `name` -| String -| The full name of the property. - Names are in lower-case period-separated form (for example, `server.address`). - This attribute is mandatory. - -| `type` -| String -| The full signature of the data type of the property (for example, `java.lang.String`) but also a full generic type (such as `java.util.Map`). - You can use this attribute to guide the user as to the types of values that they can enter. - For consistency, the type of a primitive is specified by using its wrapper counterpart (for example, `boolean` becomes `java.lang.Boolean`). - Note that this class may be a complex type that gets converted from a `String` as values are bound. - If the type is not known, it may be omitted. - -| `description` -| String -| A short description of the property that can be displayed to users. - If no description is available, it may be omitted. - It is recommended that descriptions be short paragraphs, with the first line providing a concise summary. - The last line in the description should end with a period (`.`). - -| `sourceType` -| String -| The class name of the source that contributed this property. - For example, if the property were from a class annotated with `@ConfigurationProperties`, this attribute would contain the fully qualified name of that class. - If the source type is unknown, it may be omitted. - -| `defaultValue` -| Object -| The default value, which is used if the property is not specified. - If the type of the property is an array, it can be an array of value(s). - If the default value is unknown, it may be omitted. - -| `deprecation` -| Deprecation -| Specify whether the property is deprecated. - If the field is not deprecated or if that information is not known, it may be omitted. - The next table offers more detail about the `deprecation` attribute. -|=== - -The JSON object contained in the `deprecation` attribute of each `properties` element can contain the following attributes: - -[cols="1,1,4"] -|=== -| Name | Type | Purpose - -| `level` -| String -| The level of deprecation, which can be either `warning` (the default) or `error`. - When a property has a `warning` deprecation level, it should still be bound in the environment. - However, when it has an `error` deprecation level, the property is no longer managed and is not bound. - -| `reason` -| String -| A short description of the reason why the property was deprecated. - If no reason is available, it may be omitted. - It is recommended that descriptions be short paragraphs, with the first line providing a concise summary. - The last line in the description should end with a period (`.`). - -| `replacement` -| String -| The full name of the property that _replaces_ this deprecated property. - If there is no replacement for this property, it may be omitted. -|=== - -NOTE: Prior to Spring Boot 1.3, a single `deprecated` boolean attribute can be used instead of the `deprecation` element. -This is still supported in a deprecated fashion and should no longer be used. -If no reason and replacement are available, an empty `deprecation` object should be set. - -Deprecation can also be specified declaratively in code by adding the `@DeprecatedConfigurationProperty` annotation to the getter exposing the deprecated property. -For instance, assume that the `app.acme.target` property was confusing and was renamed to `app.acme.name`. -The following example shows how to handle that situation: - -[source,java,indent=0] ----- - @ConfigurationProperties("app.acme") - public class AcmeProperties { - - private String name; - - public String getName() { ... } - - public void setName(String name) { ... } - - @DeprecatedConfigurationProperty(replacement = "app.acme.name") - @Deprecated - public String getTarget() { - return getName(); - } - - @Deprecated - public void setTarget(String target) { - setName(target); - } - } ----- - -NOTE: There is no way to set a `level`. -`warning` is always assumed, since code is still handling the property. - -The preceding code makes sure that the deprecated property still works (delegating to the `name` property behind the scenes). -Once the `getTarget` and `setTarget` methods can be removed from your public API, the automatic deprecation hint in the metadata goes away as well. -If you want to keep a hint, adding manual metadata with an `error` deprecation level ensures that users are still informed about that property. -Doing so is particularly useful when a `replacement` is provided. - - - -[[configuration-metadata-hints-attributes]] -=== Hint Attributes -The JSON object contained in the `hints` array can contain the attributes shown in the following table: - -[cols="1,1,4"] -|=== -| Name | Type | Purpose - -| `name` -| String -| The full name of the property to which this hint refers. - Names are in lower-case period-separated form (such as `spring.mvc.servlet.path`). - If the property refers to a map (such as `system.contexts`), the hint either applies to the _keys_ of the map (`system.contexts.keys`) or the _values_ (`system.contexts.values`) of the map. - This attribute is mandatory. - -| `values` -| ValueHint[] -| A list of valid values as defined by the `ValueHint` object (described in the next table). - Each entry defines the value and may have a description. - -| `providers` -| ValueProvider[] -| A list of providers as defined by the `ValueProvider` object (described later in this document). - Each entry defines the name of the provider and its parameters, if any. -|=== - -The JSON object contained in the `values` attribute of each `hint` element can contain the attributes described in the following table: - -[cols="1,1,4"] -|=== -| Name | Type | Purpose - -| `value` -| Object -| A valid value for the element to which the hint refers. - If the type of the property is an array, it can also be an array of value(s). - This attribute is mandatory. - -| `description` -| String -| A short description of the value that can be displayed to users. - If no description is available, it may be omitted. - It is recommended that descriptions be short paragraphs, with the first line providing a concise summary. - The last line in the description should end with a period (`.`). -|=== - -The JSON object contained in the `providers` attribute of each `hint` element can contain the attributes described in the following table: - -[cols="1,1,4"] -|=== -|Name | Type |Purpose - -| `name` -| String -| The name of the provider to use to offer additional content assistance for the element to which the hint refers. - -| `parameters` -| JSON object -| Any additional parameter that the provider supports (check the documentation of the provider for more details). -|=== - - - -[[configuration-metadata-repeated-items]] -=== Repeated Metadata Items -Objects with the same "`property`" and "`group`" name can appear multiple times within a metadata file. -For example, you could bind two separate classes to the same prefix, with each having potentially overlapping property names. -While the same names appearing in the metadata multiple times should not be common, consumers of metadata should take care to ensure that they support it. - - - -[[configuration-metadata-providing-manual-hints]] -== Providing Manual Hints -To improve the user experience and further assist the user in configuring a given property, you can provide additional metadata that: - -* Describes the list of potential values for a property. -* Associates a provider, to attach a well defined semantic to a property, so that a tool can discover the list of potential values based on the project's context. - - -=== Value Hint -The `name` attribute of each hint refers to the `name` of a property. -In the <>, we provide five values for the `spring.jpa.hibernate.ddl-auto` property: `none`, `validate`, `update`, `create`, and `create-drop`. -Each value may have a description as well. - -If your property is of type `Map`, you can provide hints for both the keys and the values (but not for the map itself). -The special `.keys` and `.values` suffixes must refer to the keys and the values, respectively. - -Assume a `sample.contexts` maps magic `String` values to an integer, as shown in the following example: - -[source,java,indent=0] ----- - @ConfigurationProperties("sample") - public class SampleProperties { - - private Map contexts; - // getters and setters - } ----- - -The magic values are (in this example) are `sample1` and `sample2`. -In order to offer additional content assistance for the keys, you could add the following JSON to <>: - -[source,json,indent=0] ----- - {"hints": [ - { - "name": "sample.contexts.keys", - "values": [ - { - "value": "sample1" - }, - { - "value": "sample2" - } - ] - } - ]} ----- - -TIP: We recommend that you use an `Enum` for those two values instead. -If your IDE supports it, this is by far the most effective approach to auto-completion. - - - -=== Value Providers -Providers are a powerful way to attach semantics to a property. -In this section, we define the official providers that you can use for your own hints. -However, your favorite IDE may implement some of these or none of them. -Also, it could eventually provide its own. - -NOTE: As this is a new feature, IDE vendors must catch up with how it works. -Adoption times naturally vary. - -The following table summarizes the list of supported providers: - -[cols="2,4"] -|=== -| Name | Description - -| `any` -| Permits any additional value to be provided. - -| `class-reference` -| Auto-completes the classes available in the project. - Usually constrained by a base class that is specified by the `target` parameter. - -| `handle-as` -| Handles the property as if it were defined by the type defined by the mandatory `target` parameter. - -| `logger-name` -| Auto-completes valid logger names and <>. - Typically, package and class names available in the current project can be auto-completed as well as defined groups. - -| `spring-bean-reference` -| Auto-completes the available bean names in the current project. - Usually constrained by a base class that is specified by the `target` parameter. - -| `spring-profile-name` -| Auto-completes the available Spring profile names in the project. -|=== - -TIP: Only one provider can be active for a given property, but you can specify several providers if they can all manage the property _in some way_. -Make sure to place the most powerful provider first, as the IDE must use the first one in the JSON section that it can handle. -If no provider for a given property is supported, no special content assistance is provided, either. - - - -==== Any -The special **any** provider value permits any additional values to be provided. -Regular value validation based on the property type should be applied if this is supported. - -This provider is typically used if you have a list of values and any extra values should still be considered as valid. - -The following example offers `on` and `off` as auto-completion values for `system.state`: - -[source,json,indent=0] ----- - {"hints": [ - { - "name": "system.state", - "values": [ - { - "value": "on" - }, - { - "value": "off" - } - ], - "providers": [ - { - "name": "any" - } - ] - } - ]} ----- - -Note that, in the preceding example, any other value is also allowed. - -==== Class Reference -The **class-reference** provider auto-completes classes available in the project. -This provider supports the following parameters: - -[cols="1,1,2,4"] -|=== -| Parameter | Type | Default value | Description - -| `target` -| `String` (`Class`) -| _none_ -| The fully qualified name of the class that should be assignable to the chosen value. - Typically used to filter out-non candidate classes. - Note that this information can be provided by the type itself by exposing a class with the appropriate upper bound. - -| `concrete` -| `boolean` -| true -| Specify whether only concrete classes are to be considered as valid candidates. -|=== - - -The following metadata snippet corresponds to the standard `server.servlet.jsp.class-name` property that defines the `JspServlet` class name to use: - -[source,json,indent=0] ----- - {"hints": [ - { - "name": "server.servlet.jsp.class-name", - "providers": [ - { - "name": "class-reference", - "parameters": { - "target": "javax.servlet.http.HttpServlet" - } - } - ] - } - ]} ----- - - - -==== Handle As -The **handle-as** provider lets you substitute the type of the property to a more high-level type. -This typically happens when the property has a `java.lang.String` type, because you do not want your configuration classes to rely on classes that may not be on the classpath. -This provider supports the following parameters: - -[cols="1,1,2,4"] -|=== -| Parameter | Type | Default value | Description - -| **`target`** -| `String` (`Class`) -| _none_ -| The fully qualified name of the type to consider for the property. - This parameter is mandatory. -|=== - -The following types can be used: - -* Any `java.lang.Enum`: Lists the possible values for the property. - (We recommend defining the property with the `Enum` type, as no further hint should be required for the IDE to auto-complete the values) -* `java.nio.charset.Charset`: Supports auto-completion of charset/encoding values (such as `UTF-8`) -* `java.util.Locale`: auto-completion of locales (such as `en_US`) -* `org.springframework.util.MimeType`: Supports auto-completion of content type values (such as `text/plain`) -* `org.springframework.core.io.Resource`: Supports auto-completion of Spring’s Resource abstraction to refer to a file on the filesystem or on the classpath (such as `classpath:/sample.properties`) - -TIP: If multiple values can be provided, use a `Collection` or _Array_ type to teach the IDE about it. - -The following metadata snippet corresponds to the standard `spring.liquibase.change-log` property that defines the path to the changelog to use. -It is actually used internally as a `org.springframework.core.io.Resource` but cannot be exposed as such, because we need to keep the original String value to pass it to the Liquibase API. - -[source,json,indent=0] ----- - {"hints": [ - { - "name": "spring.liquibase.change-log", - "providers": [ - { - "name": "handle-as", - "parameters": { - "target": "org.springframework.core.io.Resource" - } - } - ] - } - ]} ----- - - - -==== Logger Name -The **logger-name** provider auto-completes valid logger names and <>. -Typically, package and class names available in the current project can be auto-completed. -If groups are enabled (default) and if a custom logger group is identified in the configuration, auto-completion for it should be provided. -Specific frameworks may have extra magic logger names that can be supported as well. - -This provider supports the following parameters: - -[cols="1,1,2,4"] -|=== -| Parameter | Type | Default value | Description - -| `group` -| `boolean` -| `true` -| Specify whether known groups should be considered. -|=== - -Since a logger name can be any arbitrary name, this provider should allow any value but could highlight valid package and class names that are not available in the project's classpath. - -The following metadata snippet corresponds to the standard `logging.level` property. -Keys are _logger names_, and values correspond to the standard log levels or any custom level. -As Spring Boot defines a few logger groups out-of-the-box, dedicated value hints have been added for those. - -[source,json,indent=0] ----- - {"hints": [ - { - "name": "logging.level.keys", - "values": [ - { - "value": "root", - "description": "Root logger used to assign the default logging level." - }, - { - "value": "sql", - "description": "SQL logging group including Hibernate SQL logger." - }, - { - "value": "web", - "description": "Web logging group including codecs." - } - ], - "providers": [ - { - "name": "logger-name" - } - ] - }, - { - "name": "logging.level.values", - "values": [ - { - "value": "trace" - }, - { - "value": "debug" - }, - { - "value": "info" - }, - { - "value": "warn" - }, - { - "value": "error" - }, - { - "value": "fatal" - }, - { - "value": "off" - } - - ], - "providers": [ - { - "name": "any" - } - ] - } - ]} ----- - - - -==== Spring Bean Reference -The **spring-bean-reference** provider auto-completes the beans that are defined in the configuration of the current project. -This provider supports the following parameters: - -[cols="1,1,2,4"] -|=== -| Parameter | Type | Default value | Description - -| `target` -| `String` (`Class`) -| _none_ -| The fully qualified name of the bean class that should be assignable to the candidate. - Typically used to filter out non-candidate beans. -|=== - -The following metadata snippet corresponds to the standard `spring.jmx.server` property that defines the name of the `MBeanServer` bean to use: - -[source,json,indent=0] ----- - {"hints": [ - { - "name": "spring.jmx.server", - "providers": [ - { - "name": "spring-bean-reference", - "parameters": { - "target": "javax.management.MBeanServer" - } - } - ] - } - ]} ----- - -NOTE: The binder is not aware of the metadata. -If you provide that hint, you still need to transform the bean name into an actual Bean reference using by the `ApplicationContext`. - - - -==== Spring Profile Name -The **spring-profile-name** provider auto-completes the Spring profiles that are defined in the configuration of the current project. - -The following metadata snippet corresponds to the standard `spring.profiles.active` property that defines the name of the Spring profile(s) to enable: - -[source,json,indent=0] ----- - {"hints": [ - { - "name": "spring.profiles.active", - "providers": [ - { - "name": "spring-profile-name" - } - ] - } - ]} ----- - - - -[[configuration-metadata-annotation-processor]] -== Generating Your Own Metadata by Using the Annotation Processor -You can easily generate your own configuration metadata file from items annotated with `@ConfigurationProperties` by using the `spring-boot-configuration-processor` jar. -The jar includes a Java annotation processor which is invoked as your project is compiled. -To use the processor, include a dependency on `spring-boot-configuration-processor`. - -With Maven the dependency should be declared as optional, as shown in the following example: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - org.springframework.boot - spring-boot-configuration-processor - true - ----- - -With Gradle 4.5 and earlier, the dependency should be declared in the `compileOnly` configuration, as shown in the following example: - -[source,groovy,indent=0,subs="verbatim,quotes,attributes"] ----- - dependencies { - compileOnly "org.springframework.boot:spring-boot-configuration-processor" - } ----- - -With Gradle 4.6 and later, the dependency should be declared in the `annotationProcessor` configuration, as shown in the following example: - -[source,groovy,indent=0,subs="verbatim,quotes,attributes"] ----- - dependencies { - annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" - } ----- - -If you are using an `additional-spring-configuration-metadata.json` file, the `compileJava` task should be configured to depend on the `processResources` task, as shown in the following example: - -[source,groovy,indent=0,subs="verbatim,quotes,attributes"] ----- - compileJava.dependsOn(processResources) ----- - -This dependency ensures that the additional metadata is available when the annotation processor runs during compilation. - -The processor picks up both classes and methods that are annotated with `@ConfigurationProperties`. -The Javadoc for field values within configuration classes is used to populate the `description` attribute. - -NOTE: You should only use simple text with `@ConfigurationProperties` field Javadoc, since they are not processed before being added to the JSON. - -If the class has a single constructor with at least one parameters, one property is created per constructor parameter. -Otherwise, properties are discovered through the presence of standard getters and setters with special handling for collection types (that is detected even if only a getter is present). - -The annotation processor also supports the use of the `@Data`, `@Getter`, and `@Setter` lombok annotations. - -The annotation processor cannot auto-detect default values for ``Enum``s and ``Collections``s. -In the cases where a `Collection` or `Enum` property has a non-empty default value, <> should be provided. - -Consider the following class: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- - @ConfigurationProperties(prefix="acme.messaging") - public class MessagingProperties { - - private List addresses = new ArrayList<>(Arrays.asList("a", "b")) ; - - private ContainerType = ContainerType.SIMPLE; - - // ... getter and setters - - public enum ContainerType { - - SIMPLE, - DIRECT - - } - - } ----- - -In order to document default values for properties in the class above, you could add the following content to <>: - -[source,json,indent=0] ----- - {"properties": [ - { - "name": "acme.messaging.addresses", - "defaultValue": ["a", "b"] - }, - { - "name": "acme.messaging.container-type", - "defaultValue": "simple" - } - ]} ----- - -Only the `name` of the property is required to document additional fields with manual metadata. - -[NOTE] -==== -If you are using AspectJ in your project, you need to make sure that the annotation processor runs only once. -There are several ways to do this. -With Maven, you can configure the `maven-apt-plugin` explicitly and add the dependency to the annotation processor only there. -You could also let the AspectJ plugin run all the processing and disable annotation processing in the `maven-compiler-plugin` configuration, as follows: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - org.apache.maven.plugins - maven-compiler-plugin - - none - - ----- -==== - - - -[[configuration-metadata-nested-properties]] -=== Nested Properties -The annotation processor automatically considers inner classes as nested properties. -Consider the following class: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- - @ConfigurationProperties(prefix="server") - public class ServerProperties { - - private String name; - - private Host host; - - // ... getter and setters - - public static class Host { - - private String ip; - - private int port; - - // ... getter and setters - - } - - } ----- - -The preceding example produces metadata information for `server.name`, `server.host.ip`, and `server.host.port` properties. -You can use the `@NestedConfigurationProperty` annotation on a field to indicate that a regular (non-inner) class should be treated as if it were nested. - -TIP: This has no effect on collections and maps, as those types are automatically identified, and a single metadata property is generated for each of them. - - - -[[configuration-metadata-additional-metadata]] -=== Adding Additional Metadata -Spring Boot's configuration file handling is quite flexible, and it is often the case that properties may exist that are not bound to a `@ConfigurationProperties` bean. -You may also need to tune some attributes of an existing key. -To support such cases and let you provide custom "hints", the annotation processor automatically merges items from `META-INF/additional-spring-configuration-metadata.json` into the main metadata file. - -If you refer to a property that has been detected automatically, the description, default value, and deprecation information are overridden, if specified. -If the manual property declaration is not identified in the current module, it is added as a new property. - -The format of the `additional-spring-configuration-metadata.json` file is exactly the same as the regular `spring-configuration-metadata.json`. -The additional properties file is optional. -If you do not have any additional properties, do not add the file. diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-dependency-versions.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-dependency-versions.adoc deleted file mode 100644 index 32653b9dfab9..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-dependency-versions.adoc +++ /dev/null @@ -1,14 +0,0 @@ -[appendix] -[[dependency-versions]] -= Dependency versions -include::{asciidoc-sources-root}/attributes.adoc[] - -This appendix provides details of the dependencies that are managed by Spring Boot. - -[[dependency-versions-coordinates]] -== Managed Dependency Coordinates - -The following table provides details of all of the dependency versions that are provided by Spring Boot in its CLI (Command Line Interface), Maven dependency management, and Gradle plugin. -When you declare a dependency on one of these artifacts without declaring a version, the version listed in the table is used. - -include::{generated-resources-root}/effective-pom.adoc[] diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-executable-jar-format.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-executable-jar-format.adoc deleted file mode 100644 index 048aaee57c6a..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-executable-jar-format.adoc +++ /dev/null @@ -1,281 +0,0 @@ -[appendix] -[[executable-jar]] -= The Executable Jar Format -include::{asciidoc-sources-root}/attributes.adoc[] - -The `spring-boot-loader` modules lets Spring Boot support executable jar and war files. -If you use the Maven plugin or the Gradle plugin, executable jars are automatically generated, and you generally do not need to know the details of how they work. - -If you need to create executable jars from a different build system or if you are just curious about the underlying technology, this appendix provides some background. - - - -[[executable-jar-nested-jars]] -== Nested JARs -Java does not provide any standard way to load nested jar files (that is, jar files that are themselves contained within a jar). -This can be problematic if you need to distribute a self-contained application that can be run from the command line without unpacking. - -To solve this problem, many developers use "`shaded`" jars. -A shaded jar packages all classes, from all jars, into a single "`uber jar`". -The problem with shaded jars is that it becomes hard to see which libraries are actually in your application. -It can also be problematic if the same filename is used (but with different content) in multiple jars. -Spring Boot takes a different approach and lets you actually nest jars directly. - - - -[[executable-jar-jar-file-structure]] -=== The Executable Jar File Structure -Spring Boot Loader-compatible jar files should be structured in the following way: - -[indent=0] ----- - example.jar - | - +-META-INF - | +-MANIFEST.MF - +-org - | +-springframework - | +-boot - | +-loader - | +- - +-BOOT-INF - +-classes - | +-mycompany - | +-project - | +-YourClasses.class - +-lib - +-dependency1.jar - +-dependency2.jar ----- - -Application classes should be placed in a nested `BOOT-INF/classes` directory. -Dependencies should be placed in a nested `BOOT-INF/lib` directory. - - - -[[executable-jar-war-file-structure]] -=== The Executable War File Structure -Spring Boot Loader-compatible war files should be structured in the following way: - -[indent=0] ----- - example.war - | - +-META-INF - | +-MANIFEST.MF - +-org - | +-springframework - | +-boot - | +-loader - | +- - +-WEB-INF - +-classes - | +-com - | +-mycompany - | +-project - | +-YourClasses.class - +-lib - | +-dependency1.jar - | +-dependency2.jar - +-lib-provided - +-servlet-api.jar - +-dependency3.jar ----- - -Dependencies should be placed in a nested `WEB-INF/lib` directory. -Any dependencies that are required when running embedded but are not required when deploying to a traditional web container should be placed in `WEB-INF/lib-provided`. - - - -[[executable-jar-jarfile]] -== Spring Boot's "`JarFile`" Class -The core class used to support loading nested jars is `org.springframework.boot.loader.jar.JarFile`. -It lets you load jar content from a standard jar file or from nested child jar data. -When first loaded, the location of each `JarEntry` is mapped to a physical file offset of the outer jar, as shown in the following example: - -[indent=0] ----- - myapp.jar - +-------------------+-------------------------+ - | /BOOT-INF/classes | /BOOT-INF/lib/mylib.jar | - |+-----------------+||+-----------+----------+| - || A.class ||| B.class | C.class || - |+-----------------+||+-----------+----------+| - +-------------------+-------------------------+ - ^ ^ ^ - 0063 3452 3980 ----- - -The preceding example shows how `A.class` can be found in `/BOOT-INF/classes` in `myapp.jar` at position `0063`. -`B.class` from the nested jar can actually be found in `myapp.jar` at position `3452`, and `C.class` is at position `3980`. - -Armed with this information, we can load specific nested entries by seeking to the appropriate part of the outer jar. -We do not need to unpack the archive, and we do not need to read all entry data into memory. - - - -[[executable-jar-jarfile-compatibility]] -=== Compatibility with the Standard Java "`JarFile`" -Spring Boot Loader strives to remain compatible with existing code and libraries. -`org.springframework.boot.loader.jar.JarFile` extends from `java.util.jar.JarFile` and should work as a drop-in replacement. -The `getURL()` method returns a `URL` that opens a connection compatible with `java.net.JarURLConnection` and can be used with Java's `URLClassLoader`. - - - -[[executable-jar-launching]] -== Launching Executable Jars -The `org.springframework.boot.loader.Launcher` class is a special bootstrap class that is used as an executable jar's main entry point. -It is the actual `Main-Class` in your jar file, and it is used to setup an appropriate `URLClassLoader` and ultimately call your `main()` method. - -There are three launcher subclasses (`JarLauncher`, `WarLauncher`, and `PropertiesLauncher`). -Their purpose is to load resources (`.class` files and so on) from nested jar files or war files in directories (as opposed to those explicitly on the classpath). -In the case of `JarLauncher` and `WarLauncher`, the nested paths are fixed. -`JarLauncher` looks in `BOOT-INF/lib/`, and `WarLauncher` looks in `WEB-INF/lib/` and `WEB-INF/lib-provided/`. -You can add extra jars in those locations if you want more. -The `PropertiesLauncher` looks in `BOOT-INF/lib/` in your application archive by default. -You can add additional locations by setting an environment variable called `LOADER_PATH` or `loader.path` in `loader.properties` (which is a comma-separated list of directories, archives, or directories within archives). - - - -[[executable-jar-launcher-manifest]] -=== Launcher Manifest -You need to specify an appropriate `Launcher` as the `Main-Class` attribute of `META-INF/MANIFEST.MF`. -The actual class that you want to launch (that is, the class that contains a `main` method) should be specified in the `Start-Class` attribute. - -The following example shows a typical `MANIFEST.MF` for an executable jar file: - -[indent=0] ----- - Main-Class: org.springframework.boot.loader.JarLauncher - Start-Class: com.mycompany.project.MyApplication ----- - -For a war file, it would be as follows: - -[indent=0] ----- - Main-Class: org.springframework.boot.loader.WarLauncher - Start-Class: com.mycompany.project.MyApplication ----- - -NOTE: You need not specify `Class-Path` entries in your manifest file. -The classpath is deduced from the nested jars. - - - -[[executable-jar-property-launcher-features]] -== `PropertiesLauncher` Features -`PropertiesLauncher` has a few special features that can be enabled with external properties (System properties, environment variables, manifest entries, or `loader.properties`). -The following table describes these properties: - -|=== -| Key | Purpose - -| `loader.path` -| Comma-separated Classpath, such as `lib,$\{HOME}/app/lib`. - Earlier entries take precedence, like a regular `-classpath` on the `javac` command line. - -| `loader.home` -| Used to resolve relative paths in `loader.path`. - For example, given `loader.path=lib`, then `${loader.home}/lib` is a classpath location (along with all jar files in that directory). - This property is also used to locate a `loader.properties` file, as in the following example `file:///opt/app` It defaults to `${user.dir}`. - -| `loader.args` -| Default arguments for the main method (space separated). - -| `loader.main` -| Name of main class to launch (for example, `com.app.Application`). - -| `loader.config.name` -| Name of properties file (for example, `launcher`). - It defaults to `loader`. - -| `loader.config.location` -| Path to properties file (for example, `classpath:loader.properties`). - It defaults to `loader.properties`. - -| `loader.system` -| Boolean flag to indicate that all properties should be added to System properties. - It defaults to `false`. -|=== - -When specified as environment variables or manifest entries, the following names should be used: - -|=== -| Key | Manifest entry | Environment variable - -| `loader.path` -| `Loader-Path` -| `LOADER_PATH` - -| `loader.home` -| `Loader-Home` -| `LOADER_HOME` - -| `loader.args` -| `Loader-Args` -| `LOADER_ARGS` - -| `loader.main` -| `Start-Class` -| `LOADER_MAIN` - -| `loader.config.location` -| `Loader-Config-Location` -| `LOADER_CONFIG_LOCATION` - -| `loader.system` -| `Loader-System` -| `LOADER_SYSTEM` -|=== - -TIP: Build plugins automatically move the `Main-Class` attribute to `Start-Class` when the fat jar is built. -If you use that, specify the name of the class to launch by using the `Main-Class` attribute and leaving out `Start-Class`. - -The following rules apply to working with `PropertiesLauncher`: - -* `loader.properties` is searched for in `loader.home`, then in the root of the classpath, and then in `classpath:/BOOT-INF/classes`. - The first location where a file with that name exists is used. -* `loader.home` is the directory location of an additional properties file (overriding the default) only when `loader.config.location` is not specified. -* `loader.path` can contain directories (which are scanned recursively for jar and zip files), archive paths, a directory within an archive that is scanned for jar files (for example, `dependencies.jar!/lib`), or wildcard patterns (for the default JVM behavior). - Archive paths can be relative to `loader.home` or anywhere in the file system with a `jar:file:` prefix. -* `loader.path` (if empty) defaults to `BOOT-INF/lib` (meaning a local directory or a nested one if running from an archive). - Because of this, `PropertiesLauncher` behaves the same as `JarLauncher` when no additional configuration is provided. -* `loader.path` can not be used to configure the location of `loader.properties` (the classpath used to search for the latter is the JVM classpath when `PropertiesLauncher` is launched). -* Placeholder replacement is done from System and environment variables plus the properties file itself on all values before use. -* The search order for properties (where it makes sense to look in more than one place) is environment variables, system properties, `loader.properties`, the exploded archive manifest, and the archive manifest. - - - -[[executable-jar-restrictions]] -== Executable Jar Restrictions -You need to consider the following restrictions when working with a Spring Boot Loader packaged application: - - - -[[executable-jar-zip-entry-compression]] -* Zip entry compression: -The `ZipEntry` for a nested jar must be saved by using the `ZipEntry.STORED` method. -This is required so that we can seek directly to individual content within the nested jar. -The content of the nested jar file itself can still be compressed, as can any other entries in the outer jar. - - - -[[executable-jar-system-classloader]] -* System classLoader: -Launched applications should use `Thread.getContextClassLoader()` when loading classes (most libraries and frameworks do so by default). -Trying to load nested jar classes with `ClassLoader.getSystemClassLoader()` fails. -`java.util.Logging` always uses the system classloader. -For this reason, you should consider a different logging implementation. - - - -[[executable-jar-alternatives]] -== Alternative Single Jar Solutions -If the preceding restrictions mean that you cannot use Spring Boot Loader, consider the following alternatives: - -* https://maven.apache.org/plugins/maven-shade-plugin/[Maven Shade Plugin] -* http://www.jdotsoft.com/JarClassLoader.php[JarClassLoader] -* https://sourceforge.net/projects/one-jar/[OneJar] -* https://imperceptiblethoughts.com/shadow/[Gradle Shadow Plugin] - diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-test-auto-configuration.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-test-auto-configuration.adoc deleted file mode 100644 index a354ff5081fe..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-test-auto-configuration.adoc +++ /dev/null @@ -1,13 +0,0 @@ -[appendix] -[[test-auto-configuration]] -= Test Auto-configuration Annotations -include::{asciidoc-sources-root}/attributes.adoc[] - -This appendix describes the `@…Test` auto-configuration annotations that Spring Boot provides to test slices of your application. - -[[test-auto-configuration-slices]] -== Test Slices - -The following table lists the various `@…Test` annotations that can be used to test slices of your application and the auto-configuration that they import by default: - -include::{generated-resources-root}/test-slice-auto-configuration.adoc[] diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/attributes.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/attributes.adoc deleted file mode 100644 index 3d5152a9692d..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/attributes.adoc +++ /dev/null @@ -1,107 +0,0 @@ -:doctype: book -:idprefix: -:idseparator: - -:toc: left -:toclevels: 4 -:tabsize: 4 -:numbered: -:sectanchors: -:sectnums: -:icons: font -:hide-uri-scheme: -:docinfo: shared,private - -:spring-boot-artifactory-repo: snapshot -:github-tag: master -:spring-boot-version: current - -:github-repo: spring-projects/spring-boot -:github-raw: https://raw.githubusercontent.com/{github-repo}/{github-tag} -:github-issues: https://github.com/{github-repo}/issues/ -:github-wiki: https://github.com/{github-repo}/wiki - -:code-examples: {sources-root}/main/java/org/springframework/boot/docs -:test-examples: {sources-root}/test/java/org/springframework/boot/docs - -:spring-boot-code: https://github.com/{github-repo}/tree/{github-tag} -:spring-boot-api: https://docs.spring.io/spring-boot/docs/{spring-boot-version}/api/ -:spring-boot-docs: https://docs.spring.io/spring-boot/docs/{spring-boot-version}/reference -:spring-boot-master-code: https://github.com/{github-repo}/tree/master -:spring-boot-current-docs: https://docs.spring.io/spring-boot/docs/current/reference -:spring-boot-actuator-restapi: https://docs.spring.io/spring-boot/docs/{spring-boot-version}/actuator-api/ -:spring-boot-maven-plugin-docs: https://docs.spring.io/spring-boot/docs/{spring-boot-version}/maven-plugin/ -:spring-boot-gradle-plugin-docs: https://docs.spring.io/spring-boot/docs/{spring-boot-version}/gradle-plugin/reference/html/ -:spring-boot-gradle-plugin-pdfdocs: https://docs.spring.io/spring-boot/docs/{spring-boot-version}/gradle-plugin/reference/pdf/spring-boot-gradle-plugin-reference.pdf -:spring-boot-gradle-plugin-api: https://docs.spring.io/spring-boot/docs/{spring-boot-version}/gradle-plugin/reference/api/ - -:spring-boot-module-code: {spring-boot-code}/spring-boot-project/spring-boot/src/main/java/org/springframework/boot -:spring-boot-module-api: {spring-boot-api}/org/springframework/boot -:spring-boot-autoconfigure-module-code: {spring-boot-code}/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure -:spring-boot-autoconfigure-module-api: {spring-boot-api}/org/springframework/boot/autoconfigure -:spring-boot-actuator-module-code: {spring-boot-code}/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate -:spring-boot-actuator-module-api: {spring-boot-api}/org/springframework/boot/actuate -:spring-boot-actuator-autoconfigure-module-code: {spring-boot-code}/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure -:spring-boot-actuator-autoconfigure-module-api: : {spring-boot-api}/org/springframework/boot/actuate/autoconfigure -:spring-boot-cli-module-code: {spring-boot-code}/spring-boot-project/spring-boot-cli/src/main/java/org/springframework/boot/cli -:spring-boot-cli-module-api: {spring-boot-api}/org/springframework/boot/cli -:spring-boot-devtools-module-code: {spring-boot-code}/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools -:spring-boot-devtools-module-api: {spring-boot-api}/org/springframework/boot/devtools -:spring-boot-test-module-code: {spring-boot-code}/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test -:spring-boot-test-module-api: {spring-boot-api}/org/springframework/boot/test -:spring-boot-test-autoconfigure-module-code: {spring-boot-code}/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure -:spring-boot-test-autoconfigure-module-api: {spring-boot-api}/org/springframework/boot/test/autoconfigure - -:spring-amqp-api: https://docs.spring.io/spring-amqp/docs/{spring-amqp-version}/api/org/springframework/amqp -:spring-batch: https://spring.io/projects/spring-batch -:spring-batch-api: https://docs.spring.io/spring-batch/apidocs/org/springframework/batch -:spring-data: https://spring.io/projects/spring-data -:spring-data-cassandra: https://spring.io/projects/spring-data-cassandra -:spring-data-commons-api: https://docs.spring.io/spring-data/commons/docs/{spring-data-commons-version}/api/org/springframework/data -:spring-data-couchbase: https://spring.io/projects/spring-data-couchbase -:spring-data-couchbase-docs: https://docs.spring.io/spring-data/couchbase/docs/{spring-data-couchbase-version}/reference/html/ -:spring-data-elasticsearch: https://spring.io/projects/spring-data-elasticsearch -:spring-data-elasticsearch-docs: https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/ -:spring-data-gemfire: https://spring.io/projects/spring-data-gemfire -:spring-data-geode: https://spring.io/projects/spring-data-geode -:spring-data-jpa: https://spring.io/projects/spring-data-jpa -:spring-data-jpa-api: https://docs.spring.io/spring-data/jpa/docs/{spring-data-jpa-version}/api/org/springframework/data/jpa -:spring-data-jdbc-docs: https://docs.spring.io/spring-data/jdbc/docs/{spring-data-jdbc-version}/reference/html/ -:spring-data-ldap: https://spring.io/projects/spring-data-ldap -:spring-data-mongodb: https://spring.io/projects/spring-data-mongodb -:spring-data-mongodb-api: https://docs.spring.io/spring-data/mongodb/docs/{spring-data-mongodb-version}/api/org/springframework/data/mongodb -:spring-data-neo4j: https://spring.io/projects/spring-data-neo4j -:spring-data-neo4j-docs: https://docs.spring.io/spring-data/neo4j/docs/{spring-data-neo4j-version}/reference/html/ -:spring-data-redis: https://spring.io/projects/spring-data-redis -:spring-data-rest-api: https://docs.spring.io/spring-data/rest/docs/{spring-data-rest-version}/api/org/springframework/data/rest -:spring-data-solr: https://spring.io/projects/spring-data-solr -:spring-data-solr-docs: https://docs.spring.io/spring-data/solr/docs/{spring-data-solr-version}/reference/html/ -:spring-framework: https://spring.io/projects/spring-framework -:spring-framework-api: https://docs.spring.io/spring/docs/{spring-framework-version}/javadoc-api/org/springframework -:spring-framework-docs: https://docs.spring.io/spring/docs/{spring-framework-version}/spring-framework-reference/ -:spring-initializr-docs: https://docs.spring.io/initializr/docs/current/reference/html/ -:spring-integration: https://spring.io/projects/spring-integration -:spring-integration-docs: https://docs.spring.io/spring-integration/docs/{spring-integration-version}/reference/html/ -:spring-restdocs: https://spring.io/projects/spring-restdocs -:spring-security: https://spring.io/projects/spring-security -:spring-security-docs: https://docs.spring.io/spring-security/site/docs/{spring-security-version}/reference/htmlsingle/ -:spring-security-oauth2: https://spring.io/projects/spring-security-oauth -:spring-security-oauth2-docs: https://projects.spring.io/spring-security-oauth/docs/oauth2.html -:spring-session: https://spring.io/projects/spring-session -:spring-webservices-docs: https://docs.spring.io/spring-ws/docs/{spring-webservices-version}/reference/ - - - - -:ant-docs: https://ant.apache.org/manual -:dependency-management-plugin-code: https://github.com/spring-gradle-plugins/dependency-management-plugin -:gradle-docs: https://docs.gradle.org/current/userguide -:hibernate-docs: https://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html -:java-api: https://docs.oracle.com/javase/8/docs/api/ -:jetty-docs: https://www.eclipse.org/jetty/documentation/{jetty-version} -:jooq-docs: https://www.jooq.org/doc/{jooq-version}/manual-single-page -:junit5-docs: https://junit.org/junit5/docs/current/user-guide -:kotlin-docs: https://kotlinlang.org/docs/reference/ -:micrometer-docs: https://micrometer.io/docs -:micrometer-concepts-docs: {micrometer-docs}/concepts -:micrometer-registry-docs: {micrometer-docs}/registry -:tomcat-docs: https://tomcat.apache.org/tomcat-9.0-doc diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/build-tool-plugins.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/build-tool-plugins.adoc deleted file mode 100644 index a25d94d13666..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/build-tool-plugins.adoc +++ /dev/null @@ -1,382 +0,0 @@ -[[build-tool-plugins]] -= Build Tool Plugins -include::attributes.adoc[] - -Spring Boot provides build tool plugins for Maven and Gradle. -The plugins offer a variety of features, including the packaging of executable jars. -This section provides more details on both plugins as well as some help should you need to extend an unsupported build system. -If you are just getting started, you might want to read "`<>`" from the "`<>`" section first. - - - -[[build-tool-plugins-maven-plugin]] -== Spring Boot Maven Plugin -The Spring Boot Maven Plugin provides Spring Boot support in Maven, letting you package executable jar or war archives and run an application "`in-place`". -To use it, you must use Maven 3.2 (or later). - -NOTE: See the {spring-boot-maven-plugin-docs}[Spring Boot Maven Plugin Site] for complete plugin documentation. - - - -[[build-tool-plugins-include-maven-plugin]] -=== Including the Plugin -To use the Spring Boot Maven Plugin, include the appropriate XML in the `plugins` section of your `pom.xml`, as shown in the following example: - -[source,xml,indent=0,subs="verbatim,attributes"] ----- - - - 4.0.0 - - - - - org.springframework.boot - spring-boot-maven-plugin - {spring-boot-version} - - - - repackage - - - - - - - ----- - -The preceding configuration repackages a jar or war that is built during the `package` phase of the Maven lifecycle. -The following example shows both the repackaged jar as well as the original jar in the `target` directory: - -[indent=0] ----- - $ mvn package - $ ls target/*.jar - target/myproject-1.0.0.jar target/myproject-1.0.0.jar.original ----- - - -If you do not include the `` configuration, as shown in the prior example, you can run the plugin on its own (but only if the package goal is used as well), as shown in the following example: - -[indent=0] ----- - $ mvn package spring-boot:repackage - $ ls target/*.jar - target/myproject-1.0.0.jar target/myproject-1.0.0.jar.original ----- - -If you use a milestone or snapshot release, you also need to add the appropriate `pluginRepository` elements, as shown in the following listing: - -[source,xml,indent=0,subs="verbatim,attributes"] ----- - - - spring-snapshots - https://repo.spring.io/snapshot - - - spring-milestones - https://repo.spring.io/milestone - - ----- - - - -[[build-tool-plugins-maven-packaging]] -=== Packaging Executable Jar and War Files -Once `spring-boot-maven-plugin` has been included in your `pom.xml`, it automatically tries to rewrite archives to make them executable by using the `spring-boot:repackage` goal. -You should configure your project to build a jar or war (as appropriate) by using the usual `packaging` element, as shown in the following example: - -[source,xml,indent=0,subs="verbatim,attributes"] ----- - - - - jar - - ----- - -Your existing archive is enhanced by Spring Boot during the `package` phase. -The main class that you want to launch can be specified either by using a configuration option, as shown below, or by adding a `Main-Class` attribute to the manifest. -If you do not specify a main class, the plugin searches for a class with a `public static void main(String[] args)` method. - -[source,xml,indent=0,subs="verbatim,attributes"] ----- - - org.springframework.boot - spring-boot-maven-plugin - - com.example.app.Main - - ----- - - -To build and run a project artifact, you can type the following: - -[indent=0] ----- - $ mvn package - $ java -jar target/mymodule-0.0.1-SNAPSHOT.jar ----- - -To build a war file that is both executable and deployable into an external container, you need to mark the embedded container dependencies as "`provided`", as shown in the following example: - -[source,xml,indent=0,subs="verbatim,attributes"] ----- - - - - war - - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-tomcat - provided - - - - ----- - -TIP: See the "`<>`" section for more details on how to create a deployable war file. - -Advanced configuration options and examples are available in the {spring-boot-maven-plugin-docs}[plugin info page]. - - - -[[build-tool-plugins-gradle-plugin]] -== Spring Boot Gradle Plugin -The Spring Boot Gradle Plugin provides Spring Boot support in Gradle, letting you package executable jar or war archives, run Spring Boot applications, and use the dependency management provided by `spring-boot-dependencies`. -It requires Gradle 5.x (4.10 is also supported but this support is deprecated and will be removed in a future release). -Please refer to the plugin's documentation to learn more: - -* Reference ({spring-boot-gradle-plugin-docs}[HTML] and {spring-boot-gradle-plugin-pdfdocs}[PDF]) -* {spring-boot-gradle-plugin-api}[API] - - - -[[build-tool-plugins-antlib]] -== Spring Boot AntLib Module -The Spring Boot AntLib module provides basic Spring Boot support for Apache Ant. -You can use the module to create executable jars. -To use the module, you need to declare an additional `spring-boot` namespace in your `build.xml`, as shown in the following example: - -[source,xml,indent=0] ----- - - ... - ----- - -You need to remember to start Ant using the `-lib` option, as shown in the following example: - -[indent=0,subs="verbatim,quotes,attributes"] ----- - $ ant -lib ----- - -TIP: The "`Using Spring Boot`" section includes a more complete example of <>. - - - -[[spring-boot-ant-tasks]] -=== Spring Boot Ant Tasks -Once the `spring-boot-antlib` namespace has been declared, the following additional tasks are available: - -* <> -* <> - - - -[[spring-boot-ant-exejar]] -==== `spring-boot:exejar` -You can use the `exejar` task to create a Spring Boot executable jar. -The following attributes are supported by the task: - -[cols="1,2,2"] -|==== -| Attribute | Description | Required - -| `destfile` -| The destination jar file to create -| Yes - -| `classes` -| The root directory of Java class files -| Yes - -| `start-class` -| The main application class to run -| No _(the default is the first class found that declares a `main` method)_ -|==== - -The following nested elements can be used with the task: - -[cols="1,4"] -|==== -| Element | Description - -| `resources` -| One or more {ant-docs}/Types/resources.html#collection[Resource Collections] describing a set of {ant-docs}/Types/resources.html[Resources] that should be added to the content of the created +jar+ file. - -| `lib` -| One or more {ant-docs}/Types/resources.html#collection[Resource Collections] that should be added to the set of jar libraries that make up the runtime dependency classpath of the application. -|==== - - - - -[[spring-boot-ant-exejar-examples]] -==== Examples -This section shows two examples of Ant tasks. - -.Specify +start-class+ -[source,xml,indent=0] ----- - - - - - - - - ----- - -.Detect +start-class+ -[source,xml,indent=0] ----- - - - - - ----- - - - -[[spring-boot-ant-findmainclass]] -=== `spring-boot:findmainclass` -The `findmainclass` task is used internally by `exejar` to locate a class declaring a `main`. -If necessary, you can also use this task directly in your build. -The following attributes are supported: - -[cols="1,2,2"] -|==== -| Attribute | Description | Required - -| `classesroot` -| The root directory of Java class files -| Yes _(unless `mainclass` is specified)_ - -| `mainclass` -| Can be used to short-circuit the `main` class search -| No - -| `property` -| The Ant property that should be set with the result -| No _(result will be logged if unspecified)_ -|==== - - - -[[spring-boot-ant-findmainclass-examples]] -==== Examples -This section contains three examples of using `findmainclass`. - -.Find and log -[source,xml,indent=0] ----- - ----- - -.Find and set -[source,xml,indent=0] ----- - ----- - -.Override and set -[source,xml,indent=0] ----- - ----- - - - -[[build-tool-plugins-other-build-systems]] -== Supporting Other Build Systems -If you want to use a build tool other than Maven, Gradle, or Ant, you likely need to develop your own plugin. -Executable jars need to follow a specific format and certain entries need to be written in an uncompressed form (see the "`<>`" section in the appendix for details). - -The Spring Boot Maven and Gradle plugins both make use of `spring-boot-loader-tools` to actually generate jars. -If you need to, you may use this library directly. - - - -[[build-tool-plugins-repackaging-archives]] -=== Repackaging Archives -To repackage an existing archive so that it becomes a self-contained executable archive, use `org.springframework.boot.loader.tools.Repackager`. -The `Repackager` class takes a single constructor argument that refers to an existing jar or war archive. -Use one of the two available `repackage()` methods to either replace the original file or write to a new destination. -Various settings can also be configured on the repackager before it is run. - - - -[[build-tool-plugins-nested-libraries]] -=== Nested Libraries -When repackaging an archive, you can include references to dependency files by using the `org.springframework.boot.loader.tools.Libraries` interface. -We do not provide any concrete implementations of `Libraries` here as they are usually build-system-specific. - -If your archive already includes libraries, you can use `Libraries.NONE`. - - - -[[build-tool-plugins-find-a-main-class]] -=== Finding a Main Class -If you do not use `Repackager.setMainClass()` to specify a main class, the repackager uses https://asm.ow2.io/[ASM] to read class files and tries to find a suitable class with a `public static void main(String[] args)` method. -An exception is thrown if more than one candidate is found. - - - -[[build-tool-plugins-repackage-implementation]] -=== Example Repackage Implementation -The following example shows a typical repackage implementation: - -[source,java,indent=0] ----- - Repackager repackager = new Repackager(sourceJarFile); - repackager.setBackupSource(false); - repackager.repackage(new Libraries() { - @Override - public void doWithLibraries(LibraryCallback callback) throws IOException { - // Build system specific implementation, callback for each dependency - // callback.library(new Library(nestedFile, LibraryScope.COMPILE)); - } - }); ----- - - - -[[build-tool-plugins-whats-next]] -== What to Read Next -If you are interested in how the build tool plugins work, you can look at the {spring-boot-code}/spring-boot-project/spring-boot-tools[`spring-boot-tools`] module on GitHub. -More technical details of the executable jar format are covered in <>. - -If you have specific build-related questions, you can check out the "`<>`" guides. diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/deployment.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/deployment.adoc deleted file mode 100644 index 2a9fe0227a1e..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/deployment.adoc +++ /dev/null @@ -1,834 +0,0 @@ -[[deployment]] -= Deploying Spring Boot Applications -include::attributes.adoc[] - -Spring Boot's flexible packaging options provide a great deal of choice when it comes to deploying your application. -You can deploy Spring Boot applications to a variety of cloud platforms, to container images (such as Docker), or to virtual/real machines. - -This section covers some of the more common deployment scenarios. - - - -[[containers-deployment]] -== Deploying to Containers -If you are running your application from a container, you can use an executable jar, but it is also often an advantage to explode it and run it in a different way. -Certain PaaS implementations may also choose to unpack archives before they run. -For example, Cloud Foundry operates this way. -The simplest way to run an unpacked archive is by starting the appropriate launcher, as follows: - -[indent=0] ----- - $ jar -xf myapp.jar - $ java org.springframework.boot.loader.JarLauncher ----- - -This is actually slightly faster on startup (depending on the size of the jar) than running from an unexploded archive. -At runtime you shouldn't expect any differences. - -Once you have unpacked the jar file, you can also get an extra boost to startup time by running the app with its "natural" main method instead of the `JarLauncher`. For example: - -[indent=0] ----- - $ jar -xf myapp.jar - $ java -cp BOOT-INF/classes:BOOT-INF/lib/* com.example.MyApplication ----- - -More efficient container images can also be created by copying the dependencies to the image as a separate layer from the application classes and resources (which normally change more frequently). -There is more than one way to achieve this layer separation. -For example, using a `Dockerfile` you could express it in this form (assuming the jar is already unpacked at `target/dependency`): - -[indent=0] ----- - FROM openjdk:8-jdk-alpine - VOLUME /tmp - ARG DEPENDENCY=target/dependency - COPY ${DEPENDENCY}/BOOT-INF/lib /app/lib - COPY ${DEPENDENCY}/META-INF /app/META-INF - COPY ${DEPENDENCY}/BOOT-INF/classes /app - ENTRYPOINT ["java","-cp","app:app/lib/*","com.example.MyApplication"] ----- - - - -[[cloud-deployment]] -== Deploying to the Cloud -Spring Boot's executable jars are ready-made for most popular cloud PaaS (Platform-as-a-Service) providers. -These providers tend to require that you "`bring your own container`". -They manage application processes (not Java applications specifically), so they need an intermediary layer that adapts _your_ application to the _cloud's_ notion of a running process. - -Two popular cloud providers, Heroku and Cloud Foundry, employ a "`buildpack`" approach. -The buildpack wraps your deployed code in whatever is needed to _start_ your application. -It might be a JDK and a call to `java`, an embedded web server, or a full-fledged application server. -A buildpack is pluggable, but ideally you should be able to get by with as few customizations to it as possible. -This reduces the footprint of functionality that is not under your control. -It minimizes divergence between development and production environments. - -Ideally, your application, like a Spring Boot executable jar, has everything that it needs to run packaged within it. - -In this section, we look at what it takes to get the <> in the "`Getting Started`" section up and running in the Cloud. - - - -[[cloud-deployment-cloud-foundry]] -=== Cloud Foundry -Cloud Foundry provides default buildpacks that come into play if no other buildpack is specified. -The Cloud Foundry https://github.com/cloudfoundry/java-buildpack[Java buildpack] has excellent support for Spring applications, including Spring Boot. -You can deploy stand-alone executable jar applications as well as traditional `.war` packaged applications. - -Once you have built your application (by using, for example, `mvn clean package`) and have https://docs.cloudfoundry.org/cf-cli/install-go-cli.html[installed the `cf` command line tool], deploy your application by using the `cf push` command, substituting the path to your compiled `.jar`. -Be sure to have https://docs.cloudfoundry.org/cf-cli/getting-started.html#login[logged in with your `cf` command line client] before pushing an application. -The following line shows using the `cf push` command to deploy an application: - -[indent=0,subs="verbatim,quotes,attributes"] ----- - $ cf push acloudyspringtime -p target/demo-0.0.1-SNAPSHOT.jar ----- - -NOTE: In the preceding example, we substitute `acloudyspringtime` for whatever value you give `cf` as the name of your application. - -See the https://docs.cloudfoundry.org/cf-cli/getting-started.html#push[`cf push` documentation] for more options. -If there is a Cloud Foundry https://docs.cloudfoundry.org/devguide/deploy-apps/manifest.html[`manifest.yml`] file present in the same directory, it is considered. - -At this point, `cf` starts uploading your application, producing output similar to the following example: - -[indent=0,subs="verbatim,quotes,attributes"] ----- - Uploading acloudyspringtime... *OK* - Preparing to start acloudyspringtime... *OK* - -----> Downloaded app package (*8.9M*) - -----> Java Buildpack Version: v3.12 (offline) | https://github.com/cloudfoundry/java-buildpack.git#6f25b7e - -----> Downloading Open Jdk JRE 1.8.0_121 from https://java-buildpack.cloudfoundry.org/openjdk/trusty/x86_64/openjdk-1.8.0_121.tar.gz (found in cache) - Expanding Open Jdk JRE to .java-buildpack/open_jdk_jre (1.6s) - -----> Downloading Open JDK Like Memory Calculator 2.0.2_RELEASE from https://java-buildpack.cloudfoundry.org/memory-calculator/trusty/x86_64/memory-calculator-2.0.2_RELEASE.tar.gz (found in cache) - Memory Settings: -Xss349K -Xmx681574K -XX:MaxMetaspaceSize=104857K -Xms681574K -XX:MetaspaceSize=104857K - -----> Downloading Container Certificate Trust Store 1.0.0_RELEASE from https://java-buildpack.cloudfoundry.org/container-certificate-trust-store/container-certificate-trust-store-1.0.0_RELEASE.jar (found in cache) - Adding certificates to .java-buildpack/container_certificate_trust_store/truststore.jks (0.6s) - -----> Downloading Spring Auto Reconfiguration 1.10.0_RELEASE from https://java-buildpack.cloudfoundry.org/auto-reconfiguration/auto-reconfiguration-1.10.0_RELEASE.jar (found in cache) - Checking status of app 'acloudyspringtime'... - 0 of 1 instances running (1 starting) - ... - 0 of 1 instances running (1 starting) - ... - 0 of 1 instances running (1 starting) - ... - 1 of 1 instances running (1 running) - - App started ----- - -Congratulations! The application is now live! - -Once your application is live, you can verify the status of the deployed application by using the `cf apps` command, as shown in the following example: - -[indent=0,subs="verbatim,quotes,attributes"] ----- - $ cf apps - Getting applications in ... - OK - - name requested state instances memory disk urls - ... - acloudyspringtime started 1/1 512M 1G acloudyspringtime.cfapps.io - ... ----- - -Once Cloud Foundry acknowledges that your application has been deployed, you should be able to find the application at the URI given. -In the preceding example, you could find it at `\https://acloudyspringtime.cfapps.io/`. - - - -[[cloud-deployment-cloud-foundry-services]] -==== Binding to Services -By default, metadata about the running application as well as service connection information is exposed to the application as environment variables (for example: `$VCAP_SERVICES`). -This architecture decision is due to Cloud Foundry's polyglot (any language and platform can be supported as a buildpack) nature. -Process-scoped environment variables are language agnostic. - -Environment variables do not always make for the easiest API, so Spring Boot automatically extracts them and flattens the data into properties that can be accessed through Spring's `Environment` abstraction, as shown in the following example: - -[source,java,indent=0] ----- - @Component - class MyBean implements EnvironmentAware { - - private String instanceId; - - @Override - public void setEnvironment(Environment environment) { - this.instanceId = environment.getProperty("vcap.application.instance_id"); - } - - // ... - - } ----- - -All Cloud Foundry properties are prefixed with `vcap`. -You can use `vcap` properties to access application information (such as the public URL of the application) and service information (such as database credentials). -See the {spring-boot-module-api}/cloud/CloudFoundryVcapEnvironmentPostProcessor.html['`CloudFoundryVcapEnvironmentPostProcessor`'] Javadoc for complete details. - -TIP: The https://github.com/pivotal-cf/java-cfenv/[Java CFEnv] project is a better fit for tasks such as configuring a DataSource. - - - -[[cloud-deployment-heroku]] -=== Heroku -Heroku is another popular PaaS platform. -To customize Heroku builds, you provide a `Procfile`, which provides the incantation required to deploy an application. -Heroku assigns a `port` for the Java application to use and then ensures that routing to the external URI works. - -You must configure your application to listen on the correct port. -The following example shows the `Procfile` for our starter REST application: - -[indent=0] ----- - web: java -Dserver.port=$PORT -jar target/demo-0.0.1-SNAPSHOT.jar ----- - -Spring Boot makes `-D` arguments available as properties accessible from a Spring `Environment` instance. -The `server.port` configuration property is fed to the embedded Tomcat, Jetty, or Undertow instance, which then uses the port when it starts up. -The `$PORT` environment variable is assigned to us by the Heroku PaaS. - -This should be everything you need. -The most common deployment workflow for Heroku deployments is to `git push` the code to production, as shown in the following example: - -[indent=0,subs="verbatim,quotes,attributes"] ----- - $ git push heroku master - - Initializing repository, *done*. - Counting objects: 95, *done*. - Delta compression using up to 8 threads. - Compressing objects: 100% (78/78), *done*. - Writing objects: 100% (95/95), 8.66 MiB | 606.00 KiB/s, *done*. - Total 95 (delta 31), reused 0 (delta 0) - - -----> Java app detected - -----> Installing OpenJDK 1.8... *done* - -----> Installing Maven 3.3.1... *done* - -----> Installing settings.xml... *done* - -----> Executing: mvn -B -DskipTests=true clean install - - [INFO] Scanning for projects... - Downloading: https://repo.spring.io/... - Downloaded: https://repo.spring.io/... (818 B at 1.8 KB/sec) - .... - Downloaded: https://s3pository.heroku.com/jvm/... (152 KB at 595.3 KB/sec) - [INFO] Installing /tmp/build_0c35a5d2-a067-4abc-a232-14b1fb7a8229/target/... - [INFO] Installing /tmp/build_0c35a5d2-a067-4abc-a232-14b1fb7a8229/pom.xml ... - [INFO] ------------------------------------------------------------------------ - [INFO] *BUILD SUCCESS* - [INFO] ------------------------------------------------------------------------ - [INFO] Total time: 59.358s - [INFO] Finished at: Fri Mar 07 07:28:25 UTC 2014 - [INFO] Final Memory: 20M/493M - [INFO] ------------------------------------------------------------------------ - - -----> Discovering process types - Procfile declares types -> *web* - - -----> Compressing... *done*, 70.4MB - -----> Launching... *done*, v6 - https://agile-sierra-1405.herokuapp.com/ *deployed to Heroku* - - To git@heroku.com:agile-sierra-1405.git - * [new branch] master -> master ----- - -Your application should now be up and running on Heroku. -For more details, refer to https://devcenter.heroku.com/articles/deploying-spring-boot-apps-to-heroku[Deploying Spring Boot Applications to Heroku]. - - - -[[cloud-deployment-openshift]] -=== OpenShift -https://www.openshift.com/[OpenShift] is the Red Hat public (and enterprise) extension of the Kubernetes container orchestration platform. -Similarly to Kubernetes, OpenShift has many options for installing Spring Boot based applications. - -OpenShift has many resources describing how to deploy Spring Boot applications, including: - -* https://blog.openshift.com/using-openshift-enterprise-grade-spring-boot-deployments/[Using the S2I builder] -* https://access.redhat.com/documentation/en-us/reference_architectures/2017/html-single/spring_boot_microservices_on_red_hat_openshift_container_platform_3/[Architecture guide] -* https://blog.openshift.com/using-spring-boot-on-openshift/[Running as a traditional web application on Wildfly] -* https://blog.openshift.com/openshift-commons-briefing-96-cloud-native-applications-spring-rhoar/[OpenShift Commons Briefing] - - - -[[cloud-deployment-aws]] -=== Amazon Web Services (AWS) -Amazon Web Services offers multiple ways to install Spring Boot-based applications, either as traditional web applications (war) or as executable jar files with an embedded web server. -The options include: - -* AWS Elastic Beanstalk -* AWS Code Deploy -* AWS OPS Works -* AWS Cloud Formation -* AWS Container Registry - -Each has different features and pricing models. -In this document, we describe only the simplest option: AWS Elastic Beanstalk. - - - -==== AWS Elastic Beanstalk -As described in the official https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/create_deploy_Java.html[Elastic Beanstalk Java guide], there are two main options to deploy a Java application. -You can either use the "`Tomcat Platform`" or the "`Java SE platform`". - - - -===== Using the Tomcat Platform -This option applies to Spring Boot projects that produce a war file. -No special configuration is required. -You need only follow the official guide. - - - -===== Using the Java SE Platform -This option applies to Spring Boot projects that produce a jar file and run an embedded web container. -Elastic Beanstalk environments run an nginx instance on port 80 to proxy the actual application, running on port 5000. -To configure it, add the following line to your `application.properties` file: - -[indent=0] ----- - server.port=5000 ----- - - -[TIP] -.Upload binaries instead of sources -==== -By default, Elastic Beanstalk uploads sources and compiles them in AWS. -However, it is best to upload the binaries instead. -To do so, add lines similar to the following to your `.elasticbeanstalk/config.yml` file: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - deploy: - artifact: target/demo-0.0.1-SNAPSHOT.jar ----- -==== - -[TIP] -.Reduce costs by setting the environment type -==== -By default an Elastic Beanstalk environment is load balanced. -The load balancer has a significant cost. -To avoid that cost, set the environment type to "`Single instance`", as described in https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/environments-create-wizard.html#environments-create-wizard-capacity[the Amazon documentation]. -You can also create single instance environments by using the CLI and the following command: - -[indent=0] ----- - eb create -s ----- -==== - - - -==== Summary -This is one of the easiest ways to get to AWS, but there are more things to cover, such as how to integrate Elastic Beanstalk into any CI / CD tool, use the Elastic Beanstalk Maven plugin instead of the CLI, and others. -There is a https://exampledriven.wordpress.com/2017/01/09/spring-boot-aws-elastic-beanstalk-example/[blog post] covering these topics more in detail. - - - -[[cloud-deployment-boxfuse]] -=== Boxfuse and Amazon Web Services -https://boxfuse.com/[Boxfuse] works by turning your Spring Boot executable jar or war into a minimal VM image that can be deployed unchanged either on VirtualBox or on AWS. -Boxfuse comes with deep integration for Spring Boot and uses the information from your Spring Boot configuration file to automatically configure ports and health check URLs. -Boxfuse leverages this information both for the images it produces as well as for all the resources it provisions (instances, security groups, elastic load balancers, and so on). - -Once you have created a https://console.boxfuse.com[Boxfuse account], connected it to your AWS account, installed the latest version of the Boxfuse Client, and ensured that the application has been built by Maven or Gradle (by using, for example, `mvn clean package`), you can deploy your Spring Boot application to AWS with a command similar to the following: - -[indent=0] ----- - $ boxfuse run myapp-1.0.jar -env=prod ----- - -See the https://boxfuse.com/docs/commandline/run.html[`boxfuse run` documentation] for more options. -If there is a https://boxfuse.com/docs/commandline/#configuration[`boxfuse.conf`] file present in the current directory, it is considered. - -TIP: By default, Boxfuse activates a Spring profile named `boxfuse` on startup. -If your executable jar or war contains an https://boxfuse.com/docs/payloads/springboot.html#configuration[`application-boxfuse.properties`] file, Boxfuse bases its configuration on the properties it contains. - -At this point, `boxfuse` creates an image for your application, uploads it, and configures and starts the necessary resources on AWS, resulting in output similar to the following example: - -[indent=0,subs="verbatim,quotes,attributes"] ----- - Fusing Image for myapp-1.0.jar ... - Image fused in 00:06.838s (53937 K) -> axelfontaine/myapp:1.0 - Creating axelfontaine/myapp ... - Pushing axelfontaine/myapp:1.0 ... - Verifying axelfontaine/myapp:1.0 ... - Creating Elastic IP ... - Mapping myapp-axelfontaine.boxfuse.io to 52.28.233.167 ... - Waiting for AWS to create an AMI for axelfontaine/myapp:1.0 in eu-central-1 (this may take up to 50 seconds) ... - AMI created in 00:23.557s -> ami-d23f38cf - Creating security group boxfuse-sg_axelfontaine/myapp:1.0 ... - Launching t2.micro instance of axelfontaine/myapp:1.0 (ami-d23f38cf) in eu-central-1 ... - Instance launched in 00:30.306s -> i-92ef9f53 - Waiting for AWS to boot Instance i-92ef9f53 and Payload to start at https://52.28.235.61/ ... - Payload started in 00:29.266s -> https://52.28.235.61/ - Remapping Elastic IP 52.28.233.167 to i-92ef9f53 ... - Waiting 15s for AWS to complete Elastic IP Zero Downtime transition ... - Deployment completed successfully. axelfontaine/myapp:1.0 is up and running at https://myapp-axelfontaine.boxfuse.io/ ----- - -Your application should now be up and running on AWS. - -See the blog post on https://boxfuse.com/blog/spring-boot-ec2.html[deploying Spring Boot apps on EC2] as well as the https://boxfuse.com/docs/payloads/springboot.html[documentation for the Boxfuse Spring Boot integration] to get started with a Maven build to run the app. - - - -[[cloud-deployment-gae]] -=== Google Cloud -Google Cloud has several options that can be used to launch Spring Boot applications. -The easiest to get started with is probably App Engine, but you could also find ways to run Spring Boot in a container with Container Engine or on a virtual machine with Compute Engine. - -To run in App Engine, you can create a project in the UI first, which sets up a unique identifier for you and also sets up HTTP routes. -Add a Java app to the project and leave it empty and then use the https://cloud.google.com/sdk/install[Google Cloud SDK] to push your Spring Boot app into that slot from the command line or CI build. - -App Engine Standard requires you to use WAR packaging. -Follow https://github.com/GoogleCloudPlatform/getting-started-java/blob/master/appengine-standard-java8/springboot-appengine-standard/README.md[these steps] to deploy App Engine Standard application to Google Cloud. - -Alternatively, App Engine Flex requires you to create an `app.yaml` file to describe the resources your app requires. -Normally, you put this file in `src/main/appengine`, and it should resemble the following file: - -[source,yaml,indent=0] ----- - service: default - - runtime: java - env: flex - - runtime_config: - jdk: openjdk8 - - handlers: - - url: /.* - script: this field is required, but ignored - - manual_scaling: - instances: 1 - - health_check: - enable_health_check: False - - env_variables: - ENCRYPT_KEY: your_encryption_key_here ----- - -You can deploy the app (for example, with a Maven plugin) by adding the project ID to the build configuration, as shown in the following example: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - com.google.cloud.tools - appengine-maven-plugin - 1.3.0 - - myproject - - ----- - -Then deploy with `mvn appengine:deploy` (if you need to authenticate first, the build fails). - - - -[[deployment-install]] -== Installing Spring Boot Applications -In addition to running Spring Boot applications by using `java -jar`, it is also possible to make fully executable applications for Unix systems. -A fully executable jar can be executed like any other executable binary or it can be <>. -This makes it very easy to install and manage Spring Boot applications in common production environments. - -CAUTION: Fully executable jars work by embedding an extra script at the front of the file. -Currently, some tools do not accept this format, so you may not always be able to use this technique. -For example, `jar -xf` may silently fail to extract a jar or war that has been made fully executable. -It is recommended that you make your jar or war fully executable only if you intend to execute it directly, rather than running it with `java -jar`or deploying it to a servlet container. - -CAUTION: A zip64-format jar file cannot be made fully executable. -Attempting to do so will result in a jar file that is reported as corrupt when executed directly or with `java -jar`. -A standard-format jar file that contains one or more zip64-format nested jars can be fully executable. - -To create a '`fully executable`' jar with Maven, use the following plugin configuration: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - org.springframework.boot - spring-boot-maven-plugin - - true - - ----- - -The following example shows the equivalent Gradle configuration: - -[source,groovy,indent=0,subs="verbatim,quotes,attributes"] ----- - bootJar { - launchScript() - } ----- - -You can then run your application by typing `./my-application.jar` (where `my-application` is the name of your artifact). -The directory containing the jar is used as your application's working directory. - - - -[[deployment-install-supported-operating-systems]] -=== Supported Operating Systems -The default script supports most Linux distributions and is tested on CentOS and Ubuntu. -Other platforms, such as OS X and FreeBSD, require the use of a custom `embeddedLaunchScript`. - - - -[[deployment-service]] -=== Unix/Linux Services -Spring Boot application can be easily started as Unix/Linux services by using either `init.d` or `systemd`. - - - -[[deployment-initd-service]] -==== Installation as an `init.d` Service (System V) -If you configured Spring Boot's Maven or Gradle plugin to generate a <>, and you do not use a custom `embeddedLaunchScript`, your application can be used as an `init.d` service. -To do so, symlink the jar to `init.d` to support the standard `start`, `stop`, `restart`, and `status` commands. - -The script supports the following features: - -* Starts the services as the user that owns the jar file -* Tracks the application's PID by using `/var/run//.pid` -* Writes console logs to `/var/log/.log` - -Assuming that you have a Spring Boot application installed in `/var/myapp`, to install a Spring Boot application as an `init.d` service, create a symlink, as follows: - -[indent=0,subs="verbatim,quotes,attributes"] ----- - $ sudo ln -s /var/myapp/myapp.jar /etc/init.d/myapp ----- - -Once installed, you can start and stop the service in the usual way. -For example, on a Debian-based system, you could start it with the following command: - -[indent=0,subs="verbatim,quotes,attributes"] ----- - $ service myapp start ----- - -TIP: If your application fails to start, check the log file written to `/var/log/.log` for errors. - -You can also flag the application to start automatically by using your standard operating system tools. -For example, on Debian, you could use the following command: - -[indent=0,subs="verbatim,quotes,attributes"] ----- - $ update-rc.d myapp defaults ----- - - - -[[deployment-initd-service-securing]] -===== Securing an `init.d` Service -NOTE: The following is a set of guidelines on how to secure a Spring Boot application that runs as an init.d service. -It is not intended to be an exhaustive list of everything that should be done to harden an application and the environment in which it runs. - -When executed as root, as is the case when root is being used to start an init.d service, the default executable script runs the application as the user specified in the `RUN_AS_USER` environment variable. -When the environment variable is not set, the user who owns the jar file is used instead. -You should never run a Spring Boot application as `root`, so `RUN_AS_USER` should never be root and your application's jar file should never be owned by root. -Instead, create a specific user to run your application and set the `RUN_AS_USER` environment variable or use `chown` to make it the owner of the jar file, as shown in the following example: - -[indent=0,subs="verbatim,quotes,attributes"] ----- - $ chown bootapp:bootapp your-app.jar ----- - -In this case, the default executable script runs the application as the `bootapp` user. - -TIP: To reduce the chances of the application's user account being compromised, you should consider preventing it from using a login shell. -For example, you can set the account's shell to `/usr/sbin/nologin`. - -You should also take steps to prevent the modification of your application's jar file. -Firstly, configure its permissions so that it cannot be written and can only be read or executed by its owner, as shown in the following example: - -[indent=0,subs="verbatim,quotes,attributes"] ----- - $ chmod 500 your-app.jar ----- - -Second, you should also take steps to limit the damage if your application or the account that's running it is compromised. -If an attacker does gain access, they could make the jar file writable and change its contents. -One way to protect against this is to make it immutable by using `chattr`, as shown in the following example: - -[indent=0,subs="verbatim,quotes,attributes"] ----- - $ sudo chattr +i your-app.jar ----- - -This will prevent any user, including root, from modifying the jar. - -If root is used to control the application's service and you <> to customize its startup, the `.conf` file is read and evaluated by the root user. -It should be secured accordingly. -Use `chmod` so that the file can only be read by the owner and use `chown` to make root the owner, as shown in the following example: - -[indent=0,subs="verbatim,quotes,attributes"] ----- - $ chmod 400 your-app.conf - $ sudo chown root:root your-app.conf ----- - - - -[[deployment-systemd-service]] -==== Installation as a `systemd` Service -`systemd` is the successor of the System V init system and is now being used by many modern Linux distributions. -Although you can continue to use `init.d` scripts with `systemd`, it is also possible to launch Spring Boot applications by using `systemd` '`service`' scripts. - -Assuming that you have a Spring Boot application installed in `/var/myapp`, to install a Spring Boot application as a `systemd` service, create a script named `myapp.service` and place it in `/etc/systemd/system` directory. -The following script offers an example: - -[indent=0] ----- - [Unit] - Description=myapp - After=syslog.target - - [Service] - User=myapp - ExecStart=/var/myapp/myapp.jar - SuccessExitStatus=143 - - [Install] - WantedBy=multi-user.target ----- - -IMPORTANT: Remember to change the `Description`, `User`, and `ExecStart` fields for your application. - -NOTE: The `ExecStart` field does not declare the script action command, which means that the `run` command is used by default. - -Note that, unlike when running as an `init.d` service, the user that runs the application, the PID file, and the console log file are managed by `systemd` itself and therefore must be configured by using appropriate fields in the '`service`' script. -Consult the https://www.freedesktop.org/software/systemd/man/systemd.service.html[service unit configuration man page] for more details. - -To flag the application to start automatically on system boot, use the following command: - -[indent=0,subs="verbatim,quotes,attributes"] ----- - $ systemctl enable myapp.service ----- - -Refer to `man systemctl` for more details. - - - -[[deployment-script-customization]] -==== Customizing the Startup Script -The default embedded startup script written by the Maven or Gradle plugin can be customized in a number of ways. -For most people, using the default script along with a few customizations is usually enough. -If you find you cannot customize something that you need to, use the `embeddedLaunchScript` option to write your own file entirely. - - - -[[deployment-script-customization-when-it-written]] -===== Customizing the Start Script when It Is Written -It often makes sense to customize elements of the start script as it is written into the jar file. -For example, init.d scripts can provide a "`description`". -Since you know the description up front (and it need not change), you may as well provide it when the jar is generated. - -To customize written elements, use the `embeddedLaunchScriptProperties` option of the Spring Boot Maven plugin or the {spring-boot-gradle-plugin-docs}/#packaging-executable-configuring-launch-script[`properties` property of the Spring Boot Gradle plugin's `launchScript`]. - -The following property substitutions are supported with the default script: - -[cols="1,3,3,3"] -|=== -| Name | Description | Gradle default | Maven default - -| `mode` -| The script mode. -| `auto` -| `auto` - -| `initInfoProvides` -| The `Provides` section of "`INIT INFO`" -| `${task.baseName}` -| `${project.artifactId}` - -| `initInfoRequiredStart` -| `Required-Start` section of "`INIT INFO`". -| `$remote_fs $syslog $network` -| `$remote_fs $syslog $network` - -| `initInfoRequiredStop` -| `Required-Stop` section of "`INIT INFO`". -| `$remote_fs $syslog $network` -| `$remote_fs $syslog $network` - -| `initInfoDefaultStart` -| `Default-Start` section of "`INIT INFO`". -| `2 3 4 5` -| `2 3 4 5` - -| `initInfoDefaultStop` -| `Default-Stop` section of "`INIT INFO`". -| `0 1 6` -| `0 1 6` - -| `initInfoShortDescription` -| `Short-Description` section of "`INIT INFO`". -| Single-line version of `${project.description}` (falling back to `${task.baseName}`) -| `${project.name}` - -| `initInfoDescription` -| `Description` section of "`INIT INFO`". -| `${project.description}` (falling back to `${task.baseName}`) -| `${project.description}` (falling back to `${project.name}`) - -| `initInfoChkconfig` -| `chkconfig` section of "`INIT INFO`" -| `2345 99 01` -| `2345 99 01` - -| `confFolder` -| The default value for `CONF_FOLDER` -| Folder containing the jar -| Folder containing the jar - -| `inlinedConfScript` -| Reference to a file script that should be inlined in the default launch script. - This can be used to set environmental variables such as `JAVA_OPTS` before any external config files are loaded -| -| - -| `logFolder` -| Default value for `LOG_FOLDER`. - Only valid for an `init.d` service -| -| - -| `logFilename` -| Default value for `LOG_FILENAME`. - Only valid for an `init.d` service -| -| - -| `pidFolder` -| Default value for `PID_FOLDER`. - Only valid for an `init.d` service -| -| - -| `pidFilename` -| Default value for the name of the PID file in `PID_FOLDER`. - Only valid for an `init.d` service -| -| - -| `useStartStopDaemon` -| Whether the `start-stop-daemon` command, when it's available, should be used to control the process -| `true` -| `true` - -| `stopWaitTime` -| Default value for `STOP_WAIT_TIME` in seconds. - Only valid for an `init.d` service -| 60 -| 60 -|=== - - - -[[deployment-script-customization-when-it-runs]] -===== Customizing a Script When It Runs -For items of the script that need to be customized _after_ the jar has been written, you can use environment variables or a <>. - -The following environment properties are supported with the default script: - -[cols="1,6"] -|=== -| Variable | Description - -| `MODE` -| The "`mode`" of operation. - The default depends on the way the jar was built but is usually `auto` (meaning it tries to guess if it is an init script by checking if it is a symlink in a directory called `init.d`). - You can explicitly set it to `service` so that the `stop\|start\|status\|restart` commands work or to `run` if you want to run the script in the foreground. - -| `RUN_AS_USER` -| The user that will be used to run the application. - When not set, the user that owns the jar file will be used. - -| `USE_START_STOP_DAEMON` -| Whether the `start-stop-daemon` command, when it's available, should be used to control the process. - Defaults to `true`. - -| `PID_FOLDER` -| The root name of the pid folder (`/var/run` by default). - -| `LOG_FOLDER` -| The name of the folder in which to put log files (`/var/log` by default). - -| `CONF_FOLDER` -| The name of the folder from which to read .conf files (same folder as jar-file by default). - -| `LOG_FILENAME` -| The name of the log file in the `LOG_FOLDER` (`.log` by default). - -| `APP_NAME` -| The name of the app. - If the jar is run from a symlink, the script guesses the app name. - If it is not a symlink or you want to explicitly set the app name, this can be useful. - -| `RUN_ARGS` -| The arguments to pass to the program (the Spring Boot app). - -| `JAVA_HOME` -| The location of the `java` executable is discovered by using the `PATH` by default, but you can set it explicitly if there is an executable file at `$JAVA_HOME/bin/java`. - -| `JAVA_OPTS` -| Options that are passed to the JVM when it is launched. - -| `JARFILE` -| The explicit location of the jar file, in case the script is being used to launch a jar that it is not actually embedded. - -| `DEBUG` -| If not empty, sets the `-x` flag on the shell process, making it easy to see the logic in the script. - -| `STOP_WAIT_TIME` -| The time in seconds to wait when stopping the application before forcing a shutdown (`60` by default). -|=== - -NOTE: The `PID_FOLDER`, `LOG_FOLDER`, and `LOG_FILENAME` variables are only valid for an `init.d` service. -For `systemd`, the equivalent customizations are made by using the '`service`' script. -See the https://www.freedesktop.org/software/systemd/man/systemd.service.html[service unit configuration man page] for more details. - - - -[[deployment-script-customization-conf-file]] -With the exception of `JARFILE` and `APP_NAME`, the settings listed in the preceding section can be configured by using a `.conf` file. -The file is expected to be next to the jar file and have the same name but suffixed with `.conf` rather than `.jar`. -For example, a jar named `/var/myapp/myapp.jar` uses the configuration file named `/var/myapp/myapp.conf`, as shown in the following example: - -.myapp.conf -[indent=0,subs="verbatim,quotes,attributes"] ----- - JAVA_OPTS=-Xmx1024M - LOG_FOLDER=/custom/log/folder ----- - -TIP: If you do not like having the config file next to the jar file, you can set a `CONF_FOLDER` environment variable to customize the location of the config file. - -To learn about securing this file appropriately, see <>. - - - -[[deployment-windows]] -=== Microsoft Windows Services -A Spring Boot application can be started as a Windows service by using https://github.com/kohsuke/winsw[`winsw`]. - -A (https://github.com/snicoll-scratches/spring-boot-daemon[separately maintained sample]) describes step-by-step how you can create a Windows service for your Spring Boot application. - - - -[[deployment-whats-next]] -== What to Read Next -Check out the https://www.cloudfoundry.org/[Cloud Foundry], https://www.heroku.com/[Heroku], https://www.openshift.com[OpenShift], and https://boxfuse.com[Boxfuse] web sites for more information about the kinds of features that a PaaS can offer. -These are just four of the most popular Java PaaS providers. -Since Spring Boot is so amenable to cloud-based deployment, you can freely consider other providers as well. - -The next section goes on to cover the _<>_, or you can jump ahead to read about _<>_. diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/documentation-overview.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/documentation-overview.adoc deleted file mode 100644 index 6a7d1fa7e21d..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/documentation-overview.adoc +++ /dev/null @@ -1,91 +0,0 @@ -[[boot-documentation]] -= Spring Boot Documentation -include::attributes.adoc[] - -This section provides a brief overview of Spring Boot reference documentation. -It serves as a map for the rest of the document. - - - -[[boot-documentation-about]] -== About the Documentation -The Spring Boot reference guide is available as: - -* {spring-boot-docs}/html[Multi-page HTML] -* {spring-boot-docs}/htmlsingle[Single page HTML] -* {spring-boot-docs}/pdf/spring-boot-reference.pdf[PDF] - -The latest copy is available at {spring-boot-current-docs}. - -Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically. - - - -[[boot-documentation-getting-help]] -== Getting Help -If you have trouble with Spring Boot, we would like to help. - -* Try the <>. - They provide solutions to the most common questions. -* Learn the Spring basics. - Spring Boot builds on many other Spring projects. - Check the https://spring.io[spring.io] web-site for a wealth of reference documentation. - If you are starting out with Spring, try one of the https://spring.io/guides[guides]. -* Ask a question. - We monitor https://stackoverflow.com[stackoverflow.com] for questions tagged with https://stackoverflow.com/tags/spring-boot[`spring-boot`]. -* Report bugs with Spring Boot at https://github.com/spring-projects/spring-boot/issues. - -NOTE: All of Spring Boot is open source, including the documentation. -If you find problems with the docs or if you want to improve them, please {spring-boot-code}[get involved]. - - - -[[boot-documentation-first-steps]] -== First Steps -If you are getting started with Spring Boot or 'Spring' in general, start with <>: - -* *From scratch:* <> | <> | <> -* *Tutorial:* <> | <> -* *Running your example:* <> | <> - - - -== Working with Spring Boot -Ready to actually start using Spring Boot? <>: - -* *Build systems:* <> | <> | <> | <> -* *Best practices:* <> | <> | <> | <> -* *Running your code:* <> | <> | <> | <> -* *Packaging your app:* <> -* *Spring Boot CLI:* <> - - - -== Learning about Spring Boot Features -Need more details about Spring Boot's core features? -<>: - -* *Core Features:* <> | <> | <> | <> -* *Web Applications:* <> | <> -* *Working with data:* <> | <> -* *Messaging:* <> | <> -* *Testing:* <> | <> | <> -* *Extending:* <> | <> - - - -== Moving to Production -When you are ready to push your Spring Boot application to production, we have <> that you might like: - -* *Management endpoints:* <> -* *Connection options:* <> | <> -* *Monitoring:* <> | <> | <> | <> - - - -== Advanced Topics -Finally, we have a few topics for more advanced users: - -* *Spring Boot Applications Deployment:* <> | <> -* *Build tool plugins:* <> | <> -* *Appendix:* <> | <> | <> | <> | <> | <> diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/getting-started.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/getting-started.adoc deleted file mode 100644 index dc1b01c736b6..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/getting-started.adoc +++ /dev/null @@ -1,771 +0,0 @@ -[[getting-started]] -= Getting Started -include::attributes.adoc[] - -If you are getting started with Spring Boot, or "`Spring`" in general, start by reading this section. -It answers the basic "`what?`", "`how?`" and "`why?`" questions. -It includes an introduction to Spring Boot, along with installation instructions. -We then walk you through building your first Spring Boot application, discussing some core principles as we go. - - - -[[getting-started-introducing-spring-boot]] -== Introducing Spring Boot -Spring Boot makes it easy to create stand-alone, production-grade Spring-based Applications that you can run. -We take an opinionated view of the Spring platform and third-party libraries, so that you can get started with minimum fuss. -Most Spring Boot applications need very little Spring configuration. - -You can use Spring Boot to create Java applications that can be started by using `java -jar` or more traditional war deployments. -We also provide a command line tool that runs "`spring scripts`". - -Our primary goals are: - -* Provide a radically faster and widely accessible getting-started experience for all Spring development. -* Be opinionated out of the box but get out of the way quickly as requirements start to diverge from the defaults. -* Provide a range of non-functional features that are common to large classes of projects (such as embedded servers, security, metrics, health checks, and externalized configuration). -* Absolutely no code generation and no requirement for XML configuration. - - - -[[getting-started-system-requirements]] -== System Requirements -Spring Boot {spring-boot-version} requires https://www.java.com[Java 8] and is compatible up to Java 13 (included). -{spring-framework-docs}[Spring Framework {spring-framework-version}] or above is also required. - -Explicit build support is provided for the following build tools: - -|=== -| Build Tool | Version - -| Maven -| 3.3+ - -| Gradle -| 5.x (4.10 is also supported but in a deprecated form) -|=== - - - -[[getting-started-system-requirements-servlet-containers]] -=== Servlet Containers -Spring Boot supports the following embedded servlet containers: - -|=== -| Name | Servlet Version - -| Tomcat 9.0 -| 4.0 - -| Jetty 9.4 -| 3.1 - -| Undertow 2.0 -| 4.0 -|=== - -You can also deploy Spring Boot applications to any Servlet 3.1+ compatible container. - - - -[[getting-started-installing-spring-boot]] -== Installing Spring Boot -Spring Boot can be used with "`classic`" Java development tools or installed as a command line tool. -Either way, you need https://www.java.com[Java SDK v1.8] or higher. -Before you begin, you should check your current Java installation by using the following command: - -[indent=0] ----- - $ java -version ----- - -If you are new to Java development or if you want to experiment with Spring Boot, you might want to try the <> (Command Line Interface) first. -Otherwise, read on for "`classic`" installation instructions. - - - -[[getting-started-installation-instructions-for-java]] -=== Installation Instructions for the Java Developer -You can use Spring Boot in the same way as any standard Java library. -To do so, include the appropriate `+spring-boot-*.jar+` files on your classpath. -Spring Boot does not require any special tools integration, so you can use any IDE or text editor. -Also, there is nothing special about a Spring Boot application, so you can run and debug a Spring Boot application as you would any other Java program. - -Although you _could_ copy Spring Boot jars, we generally recommend that you use a build tool that supports dependency management (such as Maven or Gradle). - - - -[[getting-started-maven-installation]] -==== Maven Installation -Spring Boot is compatible with Apache Maven 3.3 or above. -If you do not already have Maven installed, you can follow the instructions at https://maven.apache.org. - -TIP: On many operating systems, Maven can be installed with a package manager. -If you use OSX Homebrew, try `brew install maven`. -Ubuntu users can run `sudo apt-get install maven`. -Windows users with https://chocolatey.org/[Chocolatey] can run `choco install maven` from an elevated (administrator) prompt. - -Spring Boot dependencies use the `org.springframework.boot` `groupId`. -Typically, your Maven POM file inherits from the `spring-boot-starter-parent` project and declares dependencies to one or more <>. -Spring Boot also provides an optional <> to create executable jars. - -The following listing shows a typical `pom.xml` file: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - - 4.0.0 - - com.example - myproject - 0.0.1-SNAPSHOT - - - - org.springframework.boot - spring-boot-starter-parent - {spring-boot-version} - - - - - - - - - - - - - - - - - - - org.springframework.boot - spring-boot-starter-web - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - -ifeval::["{spring-boot-artifactory-repo}" != "release"] - - - - - spring-snapshots - https://repo.spring.io/snapshot - true - - - spring-milestones - https://repo.spring.io/milestone - - - - - spring-snapshots - https://repo.spring.io/snapshot - - - spring-milestones - https://repo.spring.io/milestone - - -endif::[] - ----- - -TIP: The `spring-boot-starter-parent` is a great way to use Spring Boot, but it might not be suitable all of the time. -Sometimes you may need to inherit from a different parent POM, or you might not like our default settings. -In those cases, see <> for an alternative solution that uses an `import` scope. - - - -[[getting-started-gradle-installation]] -==== Gradle Installation -Spring Boot is compatible with 5.x. -4.10 is also supported but this support is deprecated and will be removed in a future release. -If you do not already have Gradle installed, you can follow the instructions at https://gradle.org. - -Spring Boot dependencies can be declared by using the `org.springframework.boot` `group`. -Typically, your project declares dependencies to one or more <>. -Spring Boot provides a useful <> that can be used to simplify dependency declarations and to create executable jars. - -.Gradle Wrapper -**** -The Gradle Wrapper provides a nice way of "`obtaining`" Gradle when you need to build a project. -It is a small script and library that you commit alongside your code to bootstrap the build process. -See {gradle-docs}/gradle_wrapper.html for details. -**** - -More details on getting started with Spring Boot and Gradle can be found in the {spring-boot-gradle-plugin-docs}/#getting-started[Getting Started section] of the Gradle plugin's reference guide. - - - -[[getting-started-installing-the-cli]] -=== Installing the Spring Boot CLI -The Spring Boot CLI (Command Line Interface) is a command line tool that you can use to quickly prototype with Spring. -It lets you run http://groovy-lang.org/[Groovy] scripts, which means that you have a familiar Java-like syntax without so much boilerplate code. - -You do not need to use the CLI to work with Spring Boot, but it is definitely the quickest way to get a Spring application off the ground. - - - -[[getting-started-manual-cli-installation]] -==== Manual Installation -You can download the Spring CLI distribution from the Spring software repository: - -* https://repo.spring.io/{spring-boot-artifactory-repo}/org/springframework/boot/spring-boot-cli/{spring-boot-version}/spring-boot-cli-{spring-boot-version}-bin.zip[spring-boot-cli-{spring-boot-version}-bin.zip] -* https://repo.spring.io/{spring-boot-artifactory-repo}/org/springframework/boot/spring-boot-cli/{spring-boot-version}/spring-boot-cli-{spring-boot-version}-bin.tar.gz[spring-boot-cli-{spring-boot-version}-bin.tar.gz] - -Cutting edge -https://repo.spring.io/snapshot/org/springframework/boot/spring-boot-cli/[snapshot distributions] are also available. - -Once downloaded, follow the {github-raw}/spring-boot-project/spring-boot-cli/src/main/content/INSTALL.txt[INSTALL.txt] instructions from the unpacked archive. -In summary, there is a `spring` script (`spring.bat` for Windows) in a `bin/` directory in the `.zip` file. -Alternatively, you can use `java -jar` with the `.jar` file (the script helps you to be sure that the classpath is set correctly). - - - -[[getting-started-sdkman-cli-installation]] -==== Installation with SDKMAN! -SDKMAN! (The Software Development Kit Manager) can be used for managing multiple versions of various binary SDKs, including Groovy and the Spring Boot CLI. -Get SDKMAN! from https://sdkman.io and install Spring Boot by using the following commands: - -[indent=0,subs="verbatim,quotes,attributes"] ----- - $ sdk install springboot - $ spring --version - Spring Boot v{spring-boot-version} ----- - -If you develop features for the CLI and want easy access to the version you built, use the following commands: - -[indent=0,subs="verbatim,quotes,attributes"] ----- - $ sdk install springboot dev /path/to/spring-boot/spring-boot-cli/target/spring-boot-cli-{spring-boot-version}-bin/spring-{spring-boot-version}/ - $ sdk default springboot dev - $ spring --version - Spring CLI v{spring-boot-version} ----- - -The preceding instructions install a local instance of `spring` called the `dev` instance. -It points at your target build location, so every time you rebuild Spring Boot, `spring` is up-to-date. - -You can see it by running the following command: - -[indent=0,subs="verbatim,quotes,attributes"] ----- - $ sdk ls springboot - - ================================================================================ - Available Springboot Versions - ================================================================================ - > + dev - * {spring-boot-version} - - ================================================================================ - + - local version - * - installed - > - currently in use - ================================================================================ ----- - - - -[[getting-started-homebrew-cli-installation]] -==== OSX Homebrew Installation -If you are on a Mac and use https://brew.sh/[Homebrew], you can install the Spring Boot CLI by using the following commands: - -[indent=0] ----- - $ brew tap pivotal/tap - $ brew install springboot ----- - -Homebrew installs `spring` to `/usr/local/bin`. - -NOTE: If you do not see the formula, your installation of brew might be out-of-date. -In that case, run `brew update` and try again. - - - -[[getting-started-macports-cli-installation]] -==== MacPorts Installation -If you are on a Mac and use https://www.macports.org/[MacPorts], you can install the Spring Boot CLI by using the following command: - -[indent=0] ----- - $ sudo port install spring-boot-cli ----- - - - -[[getting-started-cli-command-line-completion]] -==== Command-line Completion -The Spring Boot CLI includes scripts that provide command completion for the https://en.wikipedia.org/wiki/Bash_%28Unix_shell%29[BASH] and https://en.wikipedia.org/wiki/Z_shell[zsh] shells. -You can `source` the script (also named `spring`) in any shell or put it in your personal or system-wide bash completion initialization. -On a Debian system, the system-wide scripts are in `/shell-completion/bash` and all scripts in that directory are executed when a new shell starts. -For example, to run the script manually if you have installed by using SDKMAN!, use the following commands: - -[indent=0] ----- - $ . ~/.sdkman/candidates/springboot/current/shell-completion/bash/spring - $ spring - grab help jar run test version ----- - -NOTE: If you install the Spring Boot CLI by using Homebrew or MacPorts, the command-line completion scripts are automatically registered with your shell. - - - -[[getting-started-scoop-cli-installation]] -==== Windows Scoop Installation -If you are on a Windows and use https://scoop.sh/[Scoop], you can install the Spring Boot CLI by using the following commands: - -[indent=0] ----- - > scoop bucket add extras - > scoop install springboot ----- - -Scoop installs `spring` to `~/scoop/apps/springboot/current/bin`. - -NOTE: If you do not see the app manifest, your installation of scoop might be out-of-date. -In that case, run `scoop update` and try again. - - - -[[getting-started-cli-example]] -==== Quick-start Spring CLI Example -You can use the following web application to test your installation. -To start, create a file called `app.groovy`, as follows: - -[source,groovy,indent=0,subs="verbatim,quotes,attributes"] ----- - @RestController - class ThisWillActuallyRun { - - @RequestMapping("/") - String home() { - "Hello World!" - } - - } ----- - -Then run it from a shell, as follows: - -[indent=0] ----- - $ spring run app.groovy ----- - -NOTE: The first run of your application is slow, as dependencies are downloaded. -Subsequent runs are much quicker. - -Open `http://localhost:8080` in your favorite web browser. -You should see the following output: - -[indent=0] ----- - Hello World! ----- - - - -[[getting-started-upgrading-from-an-earlier-version]] -=== Upgrading from an Earlier Version of Spring Boot -If you are upgrading from the `1.x` release of Spring Boot, check the {github-wiki}/Spring-Boot-2.0-Migration-Guide["`migration guide`" on the project wiki] that provides detailed upgrade instructions. -Check also the {github-wiki}["`release notes`"] for a list of "`new and noteworthy`" features for each release. - -When upgrading to a new feature release, some properties may have been renamed or removed. -Spring Boot provides a way to analyze your application's environment and print diagnostics at startup, but also temporarily migrate properties at runtime for you. -To enable that feature, add the following dependency to your project: - -[source,xml,indent=0] ----- - - org.springframework.boot - spring-boot-properties-migrator - runtime - ----- - -WARNING: Properties that are added late to the environment, such as when using `@PropertySource`, will not be taken into account. - -NOTE: Once you're done with the migration, please make sure to remove this module from your project's dependencies. - -To upgrade an existing CLI installation, use the appropriate package manager command (for example, `brew upgrade`). -If you manually installed the CLI, follow the <>, remembering to update your `PATH` environment variable to remove any older references. - - - -[[getting-started-first-application]] -== Developing Your First Spring Boot Application -This section describes how to develop a simple "`Hello World!`" web application that highlights some of Spring Boot's key features. -We use Maven to build this project, since most IDEs support it. - -[TIP] -==== -The https://spring.io[spring.io] web site contains many "`Getting Started`" https://spring.io/guides[guides] that use Spring Boot. -If you need to solve a specific problem, check there first. - -You can shortcut the steps below by going to https://start.spring.io and choosing the "Web" starter from the dependencies searcher. -Doing so generates a new project structure so that you can <>. -Check the {spring-initializr-docs}/#user-guide[Spring Initializr documentation] for more details. -==== - -Before we begin, open a terminal and run the following commands to ensure that you have valid versions of Java and Maven installed: - -[indent=0] ----- - $ java -version - java version "1.8.0_102" - Java(TM) SE Runtime Environment (build 1.8.0_102-b14) - Java HotSpot(TM) 64-Bit Server VM (build 25.102-b14, mixed mode) ----- - -[indent=0] ----- - $ mvn -v - Apache Maven 3.5.4 (1edded0938998edf8bf061f1ceb3cfdeccf443fe; 2018-06-17T14:33:14-04:00) - Maven home: /usr/local/Cellar/maven/3.3.9/libexec - Java version: 1.8.0_102, vendor: Oracle Corporation ----- - -NOTE: This sample needs to be created in its own folder. -Subsequent instructions assume that you have created a suitable folder and that it is your current directory. - - - -[[getting-started-first-application-pom]] -=== Creating the POM -We need to start by creating a Maven `pom.xml` file. -The `pom.xml` is the recipe that is used to build your project. -Open your favorite text editor and add the following: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - - 4.0.0 - - com.example - myproject - 0.0.1-SNAPSHOT - - - org.springframework.boot - spring-boot-starter-parent - {spring-boot-version} - - - - - - - - - - - - - - - - -ifeval::["{spring-boot-artifactory-repo}" != "release"] - - - - spring-snapshots - https://repo.spring.io/snapshot - true - - - spring-milestones - https://repo.spring.io/milestone - - - - - spring-snapshots - https://repo.spring.io/snapshot - - - spring-milestones - https://repo.spring.io/milestone - - -endif::[] - ----- - -The preceding listing should give you a working build. -You can test it by running `mvn package` (for now, you can ignore the "`jar will be empty - no content was marked for inclusion!`" warning). - -NOTE: At this point, you could import the project into an IDE (most modern Java IDEs include built-in support for Maven). -For simplicity, we continue to use a plain text editor for this example. - - - -[[getting-started-first-application-dependencies]] -=== Adding Classpath Dependencies -Spring Boot provides a number of "`Starters`" that let you add jars to your classpath. -Our applications for smoke tests use the `spring-boot-starter-parent` in the `parent` section of the POM. -The `spring-boot-starter-parent` is a special starter that provides useful Maven defaults. -It also provides a <> section so that you can omit `version` tags for "`blessed`" dependencies. - -Other "`Starters`" provide dependencies that you are likely to need when developing a specific type of application. -Since we are developing a web application, we add a `spring-boot-starter-web` dependency. -Before that, we can look at what we currently have by running the following command: - -[indent=0] ----- - $ mvn dependency:tree - - [INFO] com.example:myproject:jar:0.0.1-SNAPSHOT ----- - -The `mvn dependency:tree` command prints a tree representation of your project dependencies. -You can see that `spring-boot-starter-parent` provides no dependencies by itself. -To add the necessary dependencies, edit your `pom.xml` and add the `spring-boot-starter-web` dependency immediately below the `parent` section: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - - org.springframework.boot - spring-boot-starter-web - - ----- - -If you run `mvn dependency:tree` again, you see that there are now a number of additional dependencies, including the Tomcat web server and Spring Boot itself. - - - -[[getting-started-first-application-code]] -=== Writing the Code -To finish our application, we need to create a single Java file. -By default, Maven compiles sources from `src/main/java`, so you need to create that folder structure and then add a file named `src/main/java/Example.java` to contain the following code: - -[source,java,indent=0] ----- - import org.springframework.boot.*; - import org.springframework.boot.autoconfigure.*; - import org.springframework.web.bind.annotation.*; - - @RestController - @EnableAutoConfiguration - public class Example { - - @RequestMapping("/") - String home() { - return "Hello World!"; - } - - public static void main(String[] args) { - SpringApplication.run(Example.class, args); - } - - } ----- - -Although there is not much code here, quite a lot is going on. -We step through the important parts in the next few sections. - - - -[[getting-started-first-application-annotations]] -==== The @RestController and @RequestMapping Annotations -The first annotation on our `Example` class is `@RestController`. -This is known as a _stereotype_ annotation. -It provides hints for people reading the code and for Spring that the class plays a specific role. -In this case, our class is a web `@Controller`, so Spring considers it when handling incoming web requests. - -The `@RequestMapping` annotation provides "`routing`" information. -It tells Spring that any HTTP request with the `/` path should be mapped to the `home` method. -The `@RestController` annotation tells Spring to render the resulting string directly back to the caller. - -TIP: The `@RestController` and `@RequestMapping` annotations are Spring MVC annotations (they are not specific to Spring Boot). -See the {spring-framework-docs}web.html#mvc[MVC section] in the Spring Reference Documentation for more details. - - - -[[getting-started-first-application-auto-configuration]] -==== The @EnableAutoConfiguration Annotation -The second class-level annotation is `@EnableAutoConfiguration`. -This annotation tells Spring Boot to "`guess`" how you want to configure Spring, based on the jar dependencies that you have added. -Since `spring-boot-starter-web` added Tomcat and Spring MVC, the auto-configuration assumes that you are developing a web application and sets up Spring accordingly. - -.Starters and Auto-configuration -**** -Auto-configuration is designed to work well with "`Starters`", but the two concepts are not directly tied. -You are free to pick and choose jar dependencies outside of the starters. -Spring Boot still does its best to auto-configure your application. -**** - - - -[[getting-started-first-application-main-method]] -==== The "`main`" Method -The final part of our application is the `main` method. -This is just a standard method that follows the Java convention for an application entry point. -Our main method delegates to Spring Boot's `SpringApplication` class by calling `run`. -`SpringApplication` bootstraps our application, starting Spring, which, in turn, starts the auto-configured Tomcat web server. -We need to pass `Example.class` as an argument to the `run` method to tell `SpringApplication` which is the primary Spring component. -The `args` array is also passed through to expose any command-line arguments. - - - -[[getting-started-first-application-run]] -=== Running the Example -At this point, your application should work. -Since you used the `spring-boot-starter-parent` POM, you have a useful `run` goal that you can use to start the application. -Type `mvn spring-boot:run` from the root project directory to start the application. -You should see output similar to the following: - -[indent=0,subs="attributes"] ----- - $ mvn spring-boot:run - - . ____ _ __ _ _ - /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ - ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ - \\/ ___)| |_)| | | | | || (_| | ) ) ) ) - ' |____| .__|_| |_|_| |_\__, | / / / / - =========|_|==============|___/=/_/_/_/ - :: Spring Boot :: (v{spring-boot-version}) - ....... . . . - ....... . . . (log output here) - ....... . . . - ........ Started Example in 2.222 seconds (JVM running for 6.514) ----- - -If you open a web browser to `http://localhost:8080`, you should see the following output: - -[indent=0] ----- - Hello World! ----- - -To gracefully exit the application, press `ctrl-c`. - - - -[[getting-started-first-application-executable-jar]] -=== Creating an Executable Jar -We finish our example by creating a completely self-contained executable jar file that we could run in production. -Executable jars (sometimes called "`fat jars`") are archives containing your compiled classes along with all of the jar dependencies that your code needs to run. - -.Executable jars and Java -**** -Java does not provide a standard way to load nested jar files (jar files that are themselves contained within a jar). -This can be problematic if you are looking to distribute a self-contained application. - -To solve this problem, many developers use "`uber`" jars. -An uber jar packages all the classes from all the application's dependencies into a single archive. -The problem with this approach is that it becomes hard to see which libraries are in your application. -It can also be problematic if the same filename is used (but with different content) in multiple jars. - -Spring Boot takes a <> and lets you actually nest jars directly. -**** - -To create an executable jar, we need to add the `spring-boot-maven-plugin` to our `pom.xml`. -To do so, insert the following lines just below the `dependencies` section: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - - - org.springframework.boot - spring-boot-maven-plugin - - - ----- - -NOTE: The `spring-boot-starter-parent` POM includes `` configuration to bind the `repackage` goal. -If you do not use the parent POM, you need to declare this configuration yourself. -See the {spring-boot-maven-plugin-docs}/usage.html[plugin documentation] for details. - -Save your `pom.xml` and run `mvn package` from the command line, as follows: - -[indent=0,subs="attributes"] ----- - $ mvn package - - [INFO] Scanning for projects... - [INFO] - [INFO] ------------------------------------------------------------------------ - [INFO] Building myproject 0.0.1-SNAPSHOT - [INFO] ------------------------------------------------------------------------ - [INFO] .... .. - [INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ myproject --- - [INFO] Building jar: /Users/developer/example/spring-boot-example/target/myproject-0.0.1-SNAPSHOT.jar - [INFO] - [INFO] --- spring-boot-maven-plugin:{spring-boot-version}:repackage (default) @ myproject --- - [INFO] ------------------------------------------------------------------------ - [INFO] BUILD SUCCESS - [INFO] ------------------------------------------------------------------------ ----- - -If you look in the `target` directory, you should see `myproject-0.0.1-SNAPSHOT.jar`. -The file should be around 10 MB in size. -If you want to peek inside, you can use `jar tvf`, as follows: - -[indent=0] ----- - $ jar tvf target/myproject-0.0.1-SNAPSHOT.jar ----- - -You should also see a much smaller file named `myproject-0.0.1-SNAPSHOT.jar.original` in the `target` directory. -This is the original jar file that Maven created before it was repackaged by Spring Boot. - -To run that application, use the `java -jar` command, as follows: - -[indent=0,subs="attributes"] ----- - $ java -jar target/myproject-0.0.1-SNAPSHOT.jar - - . ____ _ __ _ _ - /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ - ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ - \\/ ___)| |_)| | | | | || (_| | ) ) ) ) - ' |____| .__|_| |_|_| |_\__, | / / / / - =========|_|==============|___/=/_/_/_/ - :: Spring Boot :: (v{spring-boot-version}) - ....... . . . - ....... . . . (log output here) - ....... . . . - ........ Started Example in 2.536 seconds (JVM running for 2.864) ----- - -As before, to exit the application, press `ctrl-c`. - - - -[[getting-started-whats-next]] -== What to Read Next -Hopefully, this section provided some of the Spring Boot basics and got you on your way to writing your own applications. -If you are a task-oriented type of developer, you might want to jump over to https://spring.io and check out some of the https://spring.io/guides/[getting started] guides that solve specific "`How do I do that with Spring?`" problems. -We also have Spring Boot-specific "`<>`" reference documentation. - -Otherwise, the next logical step is to read _<>_. -If you are really impatient, you could also jump ahead and read about _<>_. diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/howto.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/howto.adoc deleted file mode 100644 index 59b90d5e0423..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/howto.adoc +++ /dev/null @@ -1,2875 +0,0 @@ -[[howto]] -= "`How-to`" Guides -include::attributes.adoc[] - -This section provides answers to some common '`how do I do that...`' questions that often arise when using Spring Boot. -Its coverage is not exhaustive, but it does cover quite a lot. - -If you have a specific problem that we do not cover here, you might want to check out https://stackoverflow.com/tags/spring-boot[stackoverflow.com] to see if someone has already provided an answer. -This is also a great place to ask new questions (please use the `spring-boot` tag). - -We are also more than happy to extend this section. -If you want to add a '`how-to`', send us a {spring-boot-code}[pull request]. - - - -[[howto-spring-boot-application]] -== Spring Boot Application -This section includes topics relating directly to Spring Boot applications. - - - -[[howto-failure-analyzer]] -=== Create Your Own FailureAnalyzer -{spring-boot-module-api}/diagnostics/FailureAnalyzer.html[`FailureAnalyzer`] is a great way to intercept an exception on startup and turn it into a human-readable message, wrapped in a {spring-boot-module-api}/diagnostics/FailureAnalysis.html[`FailureAnalysis`]. -Spring Boot provides such an analyzer for application-context-related exceptions, JSR-303 validations, and more. -You can also create your own. - -`AbstractFailureAnalyzer` is a convenient extension of `FailureAnalyzer` that checks the presence of a specified exception type in the exception to handle. -You can extend from that so that your implementation gets a chance to handle the exception only when it is actually present. -If, for whatever reason, you cannot handle the exception, return `null` to give another implementation a chance to handle the exception. - -`FailureAnalyzer` implementations must be registered in `META-INF/spring.factories`. -The following example registers `ProjectConstraintViolationFailureAnalyzer`: - -[source,properties,indent=0] ----- - org.springframework.boot.diagnostics.FailureAnalyzer=\ - com.example.ProjectConstraintViolationFailureAnalyzer ----- - -NOTE: If you need access to the `BeanFactory` or the `Environment`, your `FailureAnalyzer` can simply implement `BeanFactoryAware` or `EnvironmentAware` respectively. - - - -[[howto-troubleshoot-auto-configuration]] -=== Troubleshoot Auto-configuration -The Spring Boot auto-configuration tries its best to "`do the right thing`", but sometimes things fail, and it can be hard to tell why. - -There is a really useful `ConditionEvaluationReport` available in any Spring Boot `ApplicationContext`. -You can see it if you enable `DEBUG` logging output. -If you use the `spring-boot-actuator` (see <>), there is also a `conditions` endpoint that renders the report in JSON. -Use that endpoint to debug the application and see what features have been added (and which have not been added) by Spring Boot at runtime. - -Many more questions can be answered by looking at the source code and the Javadoc. -When reading the code, remember the following rules of thumb: - -* Look for classes called `+*AutoConfiguration+` and read their sources. - Pay special attention to the `+@Conditional*+` annotations to find out what features they enable and when. - Add `--debug` to the command line or a System property `-Ddebug` to get a log on the console of all the auto-configuration decisions that were made in your app. - In a running application with actuator enabled, look at the `conditions` endpoint (`/actuator/conditions` or the JMX equivalent) for the same information. -* Look for classes that are `@ConfigurationProperties` (such as {spring-boot-autoconfigure-module-code}/web/ServerProperties.java[`ServerProperties`]) and read from there the available external configuration options. - The `@ConfigurationProperties` annotation has a `name` attribute that acts as a prefix to external properties. - Thus, `ServerProperties` has `prefix="server"` and its configuration properties are `server.port`, `server.address`, and others. - In a running application with actuator enabled, look at the `configprops` endpoint. -* Look for uses of the `bind` method on the `Binder` to pull configuration values explicitly out of the `Environment` in a relaxed manner. - It is often used with a prefix. -* Look for `@Value` annotations that bind directly to the `Environment`. -* Look for `@ConditionalOnExpression` annotations that switch features on and off in response to SpEL expressions, normally evaluated with placeholders resolved from the `Environment`. - - - -[[howto-customize-the-environment-or-application-context]] -=== Customize the Environment or ApplicationContext Before It Starts -A `SpringApplication` has `ApplicationListeners` and `ApplicationContextInitializers` that are used to apply customizations to the context or environment. -Spring Boot loads a number of such customizations for use internally from `META-INF/spring.factories`. -There is more than one way to register additional customizations: - -* Programmatically, per application, by calling the `addListeners` and `addInitializers` methods on `SpringApplication` before you run it. -* Declaratively, per application, by setting the `context.initializer.classes` or `context.listener.classes` properties. -* Declaratively, for all applications, by adding a `META-INF/spring.factories` and packaging a jar file that the applications all use as a library. - -The `SpringApplication` sends some special `ApplicationEvents` to the listeners (some even before the context is created) and then registers the listeners for events published by the `ApplicationContext` as well. -See "`<>`" in the '`Spring Boot features`' section for a complete list. - -It is also possible to customize the `Environment` before the application context is refreshed by using `EnvironmentPostProcessor`. -Each implementation should be registered in `META-INF/spring.factories`, as shown in the following example: - -[source,properties,indent=0] ----- - org.springframework.boot.env.EnvironmentPostProcessor=com.example.YourEnvironmentPostProcessor ----- - -The implementation can load arbitrary files and add them to the `Environment`. -For instance, the following example loads a YAML configuration file from the classpath: - -[source,java,indent=0] ----- -include::{code-examples}/context/EnvironmentPostProcessorExample.java[tag=example] ----- - -TIP: The `Environment` has already been prepared with all the usual property sources that Spring Boot loads by default. -It is therefore possible to get the location of the file from the environment. -The preceding example adds the `custom-resource` property source at the end of the list so that a key defined in any of the usual other locations takes precedence. -A custom implementation may define another order. - -CAUTION: While using `@PropertySource` on your `@SpringBootApplication` may seem to be a convenient and easy way to load a custom resource in the `Environment`, we do not recommend it, because Spring Boot prepares the `Environment` before the `ApplicationContext` is refreshed. -Any key defined with `@PropertySource` is loaded too late to have any effect on auto-configuration. - - - -[[howto-build-an-application-context-hierarchy]] -=== Build an ApplicationContext Hierarchy (Adding a Parent or Root Context) -You can use the `ApplicationBuilder` class to create parent/child `ApplicationContext` hierarchies. -See "`<>`" in the '`Spring Boot features`' section for more information. - - - -[[howto-create-a-non-web-application]] -=== Create a Non-web Application -Not all Spring applications have to be web applications (or web services). -If you want to execute some code in a `main` method but also bootstrap a Spring application to set up the infrastructure to use, you can use the `SpringApplication` features of Spring Boot. -A `SpringApplication` changes its `ApplicationContext` class, depending on whether it thinks it needs a web application or not. -The first thing you can do to help it is to leave server-related dependencies (e.g. servlet API) off the classpath. -If you cannot do that (for example, you run two applications from the same code base) then you can explicitly call `setWebApplicationType(WebApplicationType.NONE)` on your `SpringApplication` instance or set the `applicationContextClass` property (through the Java API or with external properties). -Application code that you want to run as your business logic can be implemented as a `CommandLineRunner` and dropped into the context as a `@Bean` definition. - - - -[[howto-properties-and-configuration]] -== Properties and Configuration -This section includes topics about setting and reading properties and configuration settings and their interaction with Spring Boot applications. - - - -[[howto-automatic-expansion]] -=== Automatically Expand Properties at Build Time -Rather than hardcoding some properties that are also specified in your project's build configuration, you can automatically expand them by instead using the existing build configuration. -This is possible in both Maven and Gradle. - - - -[[howto-automatic-expansion-maven]] -==== Automatic Property Expansion Using Maven -You can automatically expand properties from the Maven project by using resource filtering. -If you use the `spring-boot-starter-parent`, you can then refer to your Maven '`project properties`' with `@..@` placeholders, as shown in the following example: - -[source,properties,indent=0] ----- - app.encoding=@project.build.sourceEncoding@ - app.java.version=@java.version@ ----- - -NOTE: Only production configuration is filtered that way (in other words, no filtering is applied on `src/test/resources`). - -TIP: If you enable the `addResources` flag, the `spring-boot:run` goal can add `src/main/resources` directly to the classpath (for hot reloading purposes). -Doing so circumvents the resource filtering and this feature. -Instead, you can use the `exec:java` goal or customize the plugin's configuration. -See the {spring-boot-maven-plugin-docs}/usage.html[plugin usage page] for more details. - -If you do not use the starter parent, you need to include the following element inside the `` element of your `pom.xml`: - -[source,xml,indent=0] ----- - - - src/main/resources - true - - ----- - -You also need to include the following element inside ``: - -[source,xml,indent=0] ----- - - org.apache.maven.plugins - maven-resources-plugin - 2.7 - - - @ - - false - - ----- - -NOTE: The `useDefaultDelimiters` property is important if you use standard Spring placeholders (such as `$\{placeholder}`) in your configuration. -If that property is not set to `false`, these may be expanded by the build. - - - -[[howto-automatic-expansion-gradle]] -==== Automatic Property Expansion Using Gradle -You can automatically expand properties from the Gradle project by configuring the Java plugin's `processResources` task to do so, as shown in the following example: - -[source,groovy,indent=0] ----- - processResources { - expand(project.properties) - } ----- - -You can then refer to your Gradle project's properties by using placeholders, as shown in the following example: - -[source,properties,indent=0] ----- - app.name=${name} - app.description=${description} ----- - -NOTE: Gradle's `expand` method uses Groovy's `SimpleTemplateEngine`, which transforms `${..}` tokens. -The `${..}` style conflicts with Spring's own property placeholder mechanism. -To use Spring property placeholders together with automatic expansion, escape the Spring property placeholders as follows: `\${..}`. - - - -[[howto-externalize-configuration]] -=== Externalize the Configuration of `SpringApplication` -A `SpringApplication` has bean properties (mainly setters), so you can use its Java API as you create the application to modify its behavior. -Alternatively, you can externalize the configuration by setting properties in `+spring.main.*+`. -For example, in `application.properties`, you might have the following settings: - -[source,properties,indent=0,subs="verbatim,quotes,attributes",configprops] ----- - spring.main.web-application-type=none - spring.main.banner-mode=off ----- - -Then the Spring Boot banner is not printed on startup, and the application is not starting an embedded web server. - -Properties defined in external configuration override the values specified with the Java API, with the notable exception of the sources used to create the `ApplicationContext`. -Consider the following application: - -[source,java,indent=0] ----- - new SpringApplicationBuilder() - .bannerMode(Banner.Mode.OFF) - .sources(demo.MyApp.class) - .run(args); ----- - -Now consider the following configuration: - -[source,properties,indent=0,subs="verbatim,quotes,attributes",configprops] ----- - spring.main.sources=com.acme.Config,com.acme.ExtraConfig - spring.main.banner-mode=console ----- - -The actual application _now_ shows the banner (as overridden by configuration) and uses three sources for the `ApplicationContext` (in the following order): `demo.MyApp`, `com.acme.Config`, and `com.acme.ExtraConfig`. - - - -[[howto-change-the-location-of-external-properties]] -=== Change the Location of External Properties of an Application -By default, properties from different sources are added to the Spring `Environment` in a defined order (see "`<>`" in the '`Spring Boot features`' section for the exact order). - -A nice way to augment and modify this ordering is to add `@PropertySource` annotations to your application sources. -Classes passed to the `SpringApplication` static convenience methods and those added using `setSources()` are inspected to see if they have `@PropertySources`. -If they do, those properties are added to the `Environment` early enough to be used in all phases of the `ApplicationContext` lifecycle. -Properties added in this way have lower priority than any added by using the default locations (such as `application.properties`), system properties, environment variables, or the command line. - -You can also provide the following System properties (or environment variables) to change the behavior: - -* configprop:spring.config.name[] (configprop:spring.config.name[format=envvar]): Defaults to `application` as the root of the file name. -* configprop:spring.config.location[] (configprop:spring.config.location[format=envvar]): The file to load (such as a classpath resource or a URL). - A separate `Environment` property source is set up for this document and it can be overridden by system properties, environment variables, or the command line. - -No matter what you set in the environment, Spring Boot always loads `application.properties` as described above. -By default, if YAML is used, then files with the '`.yml`' extension are also added to the list. - -Spring Boot logs the configuration files that are loaded at the `DEBUG` level and the candidates it has not found at `TRACE` level. - -See {spring-boot-module-code}/context/config/ConfigFileApplicationListener.java[`ConfigFileApplicationListener`] for more detail. - - - -[[howto-use-short-command-line-arguments]] -=== Use '`Short`' Command Line Arguments -Some people like to use (for example) `--port=9000` instead of `--server.port=9000` to set configuration properties on the command line. -You can enable this behavior by using placeholders in `application.properties`, as shown in the following example: - -[source,properties,indent=0,subs="verbatim,quotes,attributes",configprops] ----- - server.port=${port:8080} ----- - -TIP: If you inherit from the `spring-boot-starter-parent` POM, the default filter token of the `maven-resources-plugins` has been changed from `+${*}+` to `@` (that is, `@maven.token@` instead of `${maven.token}`) to prevent conflicts with Spring-style placeholders. -If you have enabled Maven filtering for the `application.properties` directly, you may want to also change the default filter token to use https://maven.apache.org/plugins/maven-resources-plugin/resources-mojo.html#delimiters[other delimiters]. - -NOTE: In this specific case, the port binding works in a PaaS environment such as Heroku or Cloud Foundry. -In those two platforms, the `PORT` environment variable is set automatically and Spring can bind to capitalized synonyms for `Environment` properties. - - - -[[howto-use-yaml-for-external-properties]] -=== Use YAML for External Properties -YAML is a superset of JSON and, as such, is a convenient syntax for storing external properties in a hierarchical format, as shown in the following example: - -[source,yaml,indent=0,subs="verbatim,quotes,attributes"] ----- - spring: - application: - name: cruncher - datasource: - driverClassName: com.mysql.jdbc.Driver - url: jdbc:mysql://localhost/test - server: - port: 9000 ----- - -Create a file called `application.yml` and put it in the root of your classpath. -Then add `snakeyaml` to your dependencies (Maven coordinates `org.yaml:snakeyaml`, already included if you use the `spring-boot-starter`). -A YAML file is parsed to a Java `Map` (like a JSON object), and Spring Boot flattens the map so that it is one level deep and has period-separated keys, as many people are used to with `Properties` files in Java. - -The preceding example YAML corresponds to the following `application.properties` file: - -[source,properties,indent=0,subs="verbatim,quotes,attributes",configprops] ----- - spring.application.name=cruncher - spring.datasource.driver-class-name=com.mysql.jdbc.Driver - spring.datasource.url=jdbc:mysql://localhost/test - server.port=9000 ----- - -See "`<>`" in the '`Spring Boot features`' section for more information about YAML. - - - -[[howto-set-active-spring-profiles]] -=== Set the Active Spring Profiles -The Spring `Environment` has an API for this, but you would normally set a System property (configprop:spring.profiles.active[]) or an OS environment variable (configprop:spring.profiles.active[format=envvar]). -Also, you can launch your application with a `-D` argument (remember to put it before the main class or jar archive), as follows: - -[indent=0,subs="verbatim,quotes,attributes"] ----- - $ java -jar -Dspring.profiles.active=production demo-0.0.1-SNAPSHOT.jar ----- - -In Spring Boot, you can also set the active profile in `application.properties`, as shown in the following example: - -[source,properties,indent=0,subs="verbatim,quotes,attributes",configprops] ----- - spring.profiles.active=production ----- - -A value set this way is replaced by the System property or environment variable setting but not by the `SpringApplicationBuilder.profiles()` method. -Thus, the latter Java API can be used to augment the profiles without changing the defaults. - -See "`<>`" in the "`Spring Boot features`" section for more information. - - - -[[howto-change-configuration-depending-on-the-environment]] -=== Change Configuration Depending on the Environment -A YAML file is actually a sequence of documents separated by `---` lines, and each document is parsed separately to a flattened map. - -If a YAML document contains a `spring.profiles` key, then the profiles value (a comma-separated list of profiles) is fed into the Spring `Environment.acceptsProfiles()` method. -If any of those profiles is active, that document is included in the final merge (otherwise, it is not), as shown in the following example: - -[source,yaml,indent=0,subs="verbatim,quotes,attributes"] ----- - server: - port: 9000 - --- - - spring: - profiles: development - server: - port: 9001 - - --- - - spring: - profiles: production - server: - port: 0 ----- - -In the preceding example, the default port is 9000. -However, if the Spring profile called '`development`' is active, then the port is 9001. -If '`production`' is active, then the port is 0. - -NOTE: The YAML documents are merged in the order in which they are encountered. -Later values override earlier values. - -To do the same thing with properties files, you can use `application-$\{profile}.properties` to specify profile-specific values. - - - -[[howto-discover-build-in-options-for-external-properties]] -=== Discover Built-in Options for External Properties -Spring Boot binds external properties from `application.properties` (or `.yml` files and other places) into an application at runtime. -There is not (and technically cannot be) an exhaustive list of all supported properties in a single location, because contributions can come from additional jar files on your classpath. - -A running application with the Actuator features has a `configprops` endpoint that shows all the bound and bindable properties available through `@ConfigurationProperties`. - -The appendix includes an <> example with a list of the most common properties supported by Spring Boot. -The definitive list comes from searching the source code for `@ConfigurationProperties` and `@Value` annotations as well as the occasional use of `Binder`. -For more about the exact ordering of loading properties, see "<>". - - - -[[howto-embedded-web-servers]] -== Embedded Web Servers -Each Spring Boot web application includes an embedded web server. -This feature leads to a number of how-to questions, including how to change the embedded server and how to configure the embedded server. -This section answers those questions. - - - -[[howto-use-another-web-server]] -=== Use Another Web Server -Many Spring Boot starters include default embedded containers. - -* For servlet stack applications, the `spring-boot-starter-web` includes Tomcat by including `spring-boot-starter-tomcat`, but you can use `spring-boot-starter-jetty` or `spring-boot-starter-undertow` instead. -* For reactive stack applications, the `spring-boot-starter-webflux` includes Reactor Netty by including `spring-boot-starter-reactor-netty`, but you can use `spring-boot-starter-tomcat`, `spring-boot-starter-jetty`, or `spring-boot-starter-undertow` instead. - -When switching to a different HTTP server, you need to exclude the default dependencies in addition to including the one you need. -Spring Boot provides separate starters for HTTP servers to help make this process as easy as possible. - -The following Maven example shows how to exclude Tomcat and include Jetty for Spring MVC: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - 3.1.0 - - - org.springframework.boot - spring-boot-starter-web - - - - org.springframework.boot - spring-boot-starter-tomcat - - - - - - org.springframework.boot - spring-boot-starter-jetty - ----- - -NOTE: The version of the Servlet API has been overridden as, unlike Tomcat 9 and Undertow 2.0, Jetty 9.4 does not support Servlet 4.0. - -The following Gradle example shows how to exclude Netty and include Undertow for Spring WebFlux: - -[source,groovy,indent=0,subs="verbatim,quotes,attributes"] ----- - configurations { - // exclude Reactor Netty - compile.exclude module: 'spring-boot-starter-reactor-netty' - } - - dependencies { - compile 'org.springframework.boot:spring-boot-starter-webflux' - // Use Undertow instead - compile 'org.springframework.boot:spring-boot-starter-undertow' - // ... - } ----- - -NOTE: `spring-boot-starter-reactor-netty` is required to use the `WebClient` class, so you may need to keep a dependency on Netty even when you need to include a different HTTP server. - - - -[[howto-disable-web-server]] -=== Disabling the Web Server -If your classpath contains the necessary bits to start a web server, Spring Boot will automatically start it. -To disable this behavior configure the `WebApplicationType` in your `application.properties`, as shown in the following example: - -[source,properties,indent=0,configprops] ----- - spring.main.web-application-type=none ----- - - - -[[howto-change-the-http-port]] -=== Change the HTTP Port -In a standalone application, the main HTTP port defaults to `8080` but can be set with configprop:server.port[] (for example, in `application.properties` or as a System property). -Thanks to relaxed binding of `Environment` values, you can also use configprop:server.port[format=envvar] (for example, as an OS environment variable). - -To switch off the HTTP endpoints completely but still create a `WebApplicationContext`, use `server.port=-1` (doing so is sometimes useful for testing). - -For more details, see "`<>`" in the '`Spring Boot Features`' section, or the {spring-boot-autoconfigure-module-code}/web/ServerProperties.java[`ServerProperties`] source code. - - - -[[howto-user-a-random-unassigned-http-port]] -=== Use a Random Unassigned HTTP Port -To scan for a free port (using OS natives to prevent clashes) use `server.port=0`. - - - -[[howto-discover-the-http-port-at-runtime]] -=== Discover the HTTP Port at Runtime -You can access the port the server is running on from log output or from the `ServletWebServerApplicationContext` through its `WebServer`. -The best way to get that and be sure that it has been initialized is to add a `@Bean` of type `ApplicationListener` and pull the container out of the event when it is published. - -Tests that use `@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT)` can also inject the actual port into a field by using the `@LocalServerPort` annotation, as shown in the following example: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- - @SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT) - public class MyWebIntegrationTests { - - @Autowired - ServletWebServerApplicationContext server; - - @LocalServerPort - int port; - - // ... - - } ----- - -[NOTE] -==== -`@LocalServerPort` is a meta-annotation for `@Value("${local.server.port}")`. -Do not try to inject the port in a regular application. -As we just saw, the value is set only after the container has been initialized. -Contrary to a test, application code callbacks are processed early (before the value is actually available). -==== - - - -[[how-to-enable-http-response-compression]] -=== Enable HTTP Response Compression -HTTP response compression is supported by Jetty, Tomcat, and Undertow. -It can be enabled in `application.properties`, as follows: - -[source,properties,indent=0,subs="verbatim,quotes,attributes",configprops] ----- - server.compression.enabled=true ----- - -By default, responses must be at least 2048 bytes in length for compression to be performed. -You can configure this behavior by setting the configprop:server.compression.min-response-size[] property. - -By default, responses are compressed only if their content type is one of the following: - -* `text/html` -* `text/xml` -* `text/plain` -* `text/css` -* `text/javascript` -* `application/javascript` -* `application/json` -* `application/xml` - -You can configure this behavior by setting the configprop:server.compression.mime-types[] property. - - - -[[howto-configure-ssl]] -=== Configure SSL -SSL can be configured declaratively by setting the various `+server.ssl.*+` properties, typically in `application.properties` or `application.yml`. -The following example shows setting SSL properties in `application.properties`: - -[source,properties,indent=0,subs="verbatim,quotes,attributes",configprops] ----- - server.port=8443 - server.ssl.key-store=classpath:keystore.jks - server.ssl.key-store-password=secret - server.ssl.key-password=another-secret ----- - -See {spring-boot-module-code}/web/server/Ssl.java[`Ssl`] for details of all of the supported properties. - -Using configuration such as the preceding example means the application no longer supports a plain HTTP connector at port 8080. -Spring Boot does not support the configuration of both an HTTP connector and an HTTPS connector through `application.properties`. -If you want to have both, you need to configure one of them programmatically. -We recommend using `application.properties` to configure HTTPS, as the HTTP connector is the easier of the two to configure programmatically. - - - -[[howto-configure-http2]] -=== Configure HTTP/2 -You can enable HTTP/2 support in your Spring Boot application with the configprop:server.http2.enabled[] configuration property. -This support depends on the chosen web server and the application environment, since that protocol is not supported out-of-the-box by JDK8. - -[NOTE] -==== -Spring Boot does not support `h2c`, the cleartext version of the HTTP/2 protocol. -So you must <>. -==== - - - -[[howto-configure-http2-undertow]] -==== HTTP/2 with Undertow -As of Undertow 1.4.0+, HTTP/2 is supported without any additional requirement on JDK8. - - - -[[howto-configure-http2-jetty]] -==== HTTP/2 with Jetty -As of Jetty 9.4.8, HTTP/2 is also supported with the https://www.conscrypt.org/[Conscrypt library]. -To enable that support, your application needs to have two additional dependencies: `org.eclipse.jetty:jetty-alpn-conscrypt-server` and `org.eclipse.jetty.http2:http2-server`. - - - -[[howto-configure-http2-tomcat]] -==== HTTP/2 with Tomcat -Spring Boot ships by default with Tomcat 9.0.x which supports HTTP/2 out of the box when using JDK 9 or later. -Alternatively, HTTP/2 can be used on JDK 8 if the `libtcnative` library and its dependencies are installed on the host operating system. - -The library folder must be made available, if not already, to the JVM library path. -You can do so with a JVM argument such as `-Djava.library.path=/usr/local/opt/tomcat-native/lib`. -More on this in the https://tomcat.apache.org/tomcat-9.0-doc/apr.html[official Tomcat documentation]. - -Starting Tomcat 9.0.x on JDK 8 without that native support logs the following error: - -[indent=0,subs="attributes"] ----- - ERROR 8787 --- [ main] o.a.coyote.http11.Http11NioProtocol : The upgrade handler [org.apache.coyote.http2.Http2Protocol] for [h2] only supports upgrade via ALPN but has been configured for the ["https-jsse-nio-8443"] connector that does not support ALPN. ----- - -This error is not fatal, and the application still starts with HTTP/1.1 SSL support. - - - -[[howto-configure-http2-netty]] -==== HTTP/2 with Reactor Netty -The `spring-boot-webflux-starter` is using by default Reactor Netty as a server. -Reactor Netty can be configured for HTTP/2 using the JDK support with JDK 9 or later. -For JDK 8 environments, or for optimal runtime performance, this server also supports HTTP/2 with native libraries. -To enable that, your application needs to have an additional dependency. - -Spring Boot manages the version for the `io.netty:netty-tcnative-boringssl-static` "uber jar", containing native libraries for all platforms. -Developers can choose to import only the required dependencies using a classifier (see https://netty.io/wiki/forked-tomcat-native.html[the Netty official documentation]). - - - -[[howto-configure-webserver]] -=== Configure the Web Server -Generally, you should first consider using one of the many available configuration keys and customize your web server by adding new entries in your `application.properties` (or `application.yml`, or environment, etc. see "`<>`"). -The `server.{asterisk}` namespace is quite useful here, and it includes namespaces like `server.tomcat.{asterisk}`, `server.jetty.{asterisk}` and others, for server-specific features. -See the list of <>. - -The previous sections covered already many common use cases, such as compression, SSL or HTTP/2. -However, if a configuration key doesn't exist for your use case, you should then look at {spring-boot-module-api}/web/server/WebServerFactoryCustomizer.html[`WebServerFactoryCustomizer`]. -You can declare such a component and get access to the server factory relevant to your choice: you should select the variant for the chosen Server (Tomcat, Jetty, Reactor Netty, Undertow) and the chosen web stack (Servlet or Reactive). - -The example below is for Tomcat with the `spring-boot-starter-web` (Servlet stack): - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- - @Component - public class MyTomcatWebServerCustomizer - implements WebServerFactoryCustomizer { - - @Override - public void customize(TomcatServletWebServerFactory factory) { - // customize the factory here - } - } ----- - -In addition Spring Boot provides: - -[[howto-configure-webserver-customizers]] -[cols="1,2,2", options="header"] -|=== -| Server | Servlet stack | Reactive stack - -| Tomcat -| `TomcatServletWebServerFactory` -| `TomcatReactiveWebServerFactory` - -| Jetty -| `JettyServletWebServerFactory` -| `JettyReactiveWebServerFactory` - -| Undertow -| `UndertowServletWebServerFactory` -| `UndertowReactiveWebServerFactory` - -| Reactor -| N/A -| `NettyReactiveWebServerFactory` -|=== - -Once you've got access to a `WebServerFactory`, you can often add customizers to it to configure specific parts, like connectors, server resources, or the server itself - all using server-specific APIs. - -As a last resort, you can also declare your own `WebServerFactory` component, which will override the one provided by Spring Boot. -In this case, you can't rely on configuration properties in the `server` namespace anymore. - - - -[[howto-add-a-servlet-filter-or-listener]] -=== Add a Servlet, Filter, or Listener to an Application -In a servlet stack application, i.e. with the `spring-boot-starter-web`, there are two ways to add `Servlet`, `Filter`, `ServletContextListener`, and the other listeners supported by the Servlet API to your application: - -* <> -* <> - - - -[[howto-add-a-servlet-filter-or-listener-as-spring-bean]] -==== Add a Servlet, Filter, or Listener by Using a Spring Bean -To add a `Servlet`, `Filter`, or Servlet `*Listener` by using a Spring bean, you must provide a `@Bean` definition for it. -Doing so can be very useful when you want to inject configuration or dependencies. -However, you must be very careful that they do not cause eager initialization of too many other beans, because they have to be installed in the container very early in the application lifecycle. -(For example, it is not a good idea to have them depend on your `DataSource` or JPA configuration.) -You can work around such restrictions by initializing the beans lazily when first used instead of on initialization. - -In the case of `Filters` and `Servlets`, you can also add mappings and init parameters by adding a `FilterRegistrationBean` or a `ServletRegistrationBean` instead of or in addition to the underlying component. - -[NOTE] -==== -If no `dispatcherType` is specified on a filter registration, `REQUEST` is used. -This aligns with the Servlet Specification's default dispatcher type. -==== - -Like any other Spring bean, you can define the order of Servlet filter beans; please make sure to check the "`<>`" section. - - - -[[howto-disable-registration-of-a-servlet-or-filter]] -===== Disable Registration of a Servlet or Filter -As <>, any `Servlet` or `Filter` beans are registered with the servlet container automatically. -To disable registration of a particular `Filter` or `Servlet` bean, create a registration bean for it and mark it as disabled, as shown in the following example: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- - @Bean - public FilterRegistrationBean registration(MyFilter filter) { - FilterRegistrationBean registration = new FilterRegistrationBean(filter); - registration.setEnabled(false); - return registration; - } ----- - - - -[[howto-add-a-servlet-filter-or-listener-using-scanning]] -==== Add Servlets, Filters, and Listeners by Using Classpath Scanning -`@WebServlet`, `@WebFilter`, and `@WebListener` annotated classes can be automatically registered with an embedded servlet container by annotating a `@Configuration` class with `@ServletComponentScan` and specifying the package(s) containing the components that you want to register. -By default, `@ServletComponentScan` scans from the package of the annotated class. - - - -[[howto-configure-accesslogs]] -=== Configure Access Logging -Access logs can be configured for Tomcat, Undertow, and Jetty through their respective namespaces. - -For instance, the following settings log access on Tomcat with a {tomcat-docs}/config/valve.html#Access_Logging[custom pattern]. - -[source,properties,indent=0,subs="verbatim,quotes,attributes",configprops] ----- - server.tomcat.basedir=my-tomcat - server.tomcat.accesslog.enabled=true - server.tomcat.accesslog.pattern=%t %a "%r" %s (%D ms) ----- - -NOTE: The default location for logs is a `logs` directory relative to the Tomcat base directory. -By default, the `logs` directory is a temporary directory, so you may want to fix Tomcat's base directory or use an absolute path for the logs. -In the preceding example, the logs are available in `my-tomcat/logs` relative to the working directory of the application. - -Access logging for Undertow can be configured in a similar fashion, as shown in the following example: - -[source,properties,indent=0,subs="verbatim,quotes,attributes",configprops] ----- - server.undertow.accesslog.enabled=true - server.undertow.accesslog.pattern=%t %a "%r" %s (%D ms) ----- - -Logs are stored in a `logs` directory relative to the working directory of the application. -You can customize this location by setting the configprop:server.undertow.accesslog.dir[] property. - -Finally, access logging for Jetty can also be configured as follows: - -[source,properties,indent=0,subs="verbatim,quotes,attributes",configprops] ----- - server.jetty.accesslog.enabled=true - server.jetty.accesslog.filename=/var/log/jetty-access.log ----- - -By default, logs are redirected to `System.err`. -For more details, see {jetty-docs}/configuring-jetty-request-logs.html[the Jetty documentation]. - - - -[[howto-use-behind-a-proxy-server]] -[[howto-use-tomcat-behind-a-proxy-server]] -=== Running Behind a Front-end Proxy Server -Your application might need to send `302` redirects or render content with absolute links back to itself. -When running behind a proxy, the caller wants a link to the proxy and not to the physical address of the machine hosting your app. -Typically, such situations are handled through a contract with the proxy, which adds headers to tell the back end how to construct links to itself. - -If the proxy adds conventional `X-Forwarded-For` and `X-Forwarded-Proto` headers (most proxy servers do so), the absolute links should be rendered correctly, provided `server.forward-headers-strategy` is set to `NATIVE` or `FRAMEWORK` in your `application.properties`. - -NOTE: If your application runs in Cloud Foundry or Heroku, the configprop:server.forward-headers-strategy[] property defaults to `NATIVE`. -In all other instances, it defaults to `NONE`. - - - -[[howto-customize-tomcat-behind-a-proxy-server]] -==== Customize Tomcat's Proxy Configuration -If you use Tomcat, you can additionally configure the names of the headers used to carry "`forwarded`" information, as shown in the following example: - -[indent=0] ----- - server.tomcat.remote-ip-header=x-your-remote-ip-header - server.tomcat.protocol-header=x-your-protocol-header ----- - -Tomcat is also configured with a default regular expression that matches internal proxies that are to be trusted. -By default, IP addresses in `10/8`, `192.168/16`, `169.254/16` and `127/8` are trusted. -You can customize the valve's configuration by adding an entry to `application.properties`, as shown in the following example: - -[indent=0] ----- - server.tomcat.internal-proxies=192\\.168\\.\\d{1,3}\\.\\d{1,3} ----- - -NOTE: The double backslashes are required only when you use a properties file for configuration. -If you use YAML, single backslashes are sufficient, and a value equivalent to that shown in the preceding example would be `192\.168\.\d{1,3}\.\d{1,3}`. - -NOTE: You can trust all proxies by setting the `internal-proxies` to empty (but do not do so in production). - -You can take complete control of the configuration of Tomcat's `RemoteIpValve` by switching the automatic one off (to do so, set `server.forward-headers-strategy=NONE`) and adding a new valve instance in a `TomcatServletWebServerFactory` bean. - - - -[[howto-enable-multiple-connectors-in-tomcat]] -=== Enable Multiple Connectors with Tomcat -You can add an `org.apache.catalina.connector.Connector` to the `TomcatServletWebServerFactory`, which can allow multiple connectors, including HTTP and HTTPS connectors, as shown in the following example: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- - @Bean - public ServletWebServerFactory servletContainer() { - TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory(); - tomcat.addAdditionalTomcatConnectors(createSslConnector()); - return tomcat; - } - - private Connector createSslConnector() { - Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol"); - Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler(); - try { - File keystore = new ClassPathResource("keystore").getFile(); - File truststore = new ClassPathResource("keystore").getFile(); - connector.setScheme("https"); - connector.setSecure(true); - connector.setPort(8443); - protocol.setSSLEnabled(true); - protocol.setKeystoreFile(keystore.getAbsolutePath()); - protocol.setKeystorePass("changeit"); - protocol.setTruststoreFile(truststore.getAbsolutePath()); - protocol.setTruststorePass("changeit"); - protocol.setKeyAlias("apitester"); - return connector; - } - catch (IOException ex) { - throw new IllegalStateException("can't access keystore: [" + "keystore" - + "] or truststore: [" + "keystore" + "]", ex); - } - } ----- - - - -[[howto-use-tomcat-legacycookieprocessor]] -=== Use Tomcat's LegacyCookieProcessor -By default, the embedded Tomcat used by Spring Boot does not support "Version 0" of the Cookie format, so you may see the following error: - -[indent=0] ----- - java.lang.IllegalArgumentException: An invalid character [32] was present in the Cookie value ----- - -If at all possible, you should consider updating your code to only store values compliant with later Cookie specifications. -If, however, you cannot change the way that cookies are written, you can instead configure Tomcat to use a `LegacyCookieProcessor`. -To switch to the `LegacyCookieProcessor`, use an `WebServerFactoryCustomizer` bean that adds a `TomcatContextCustomizer`, as shown in the following example: - -[source,java,indent=0] ----- -include::{code-examples}/context/embedded/TomcatLegacyCookieProcessorExample.java[tag=customizer] ----- - - - -[[howto-enable-tomcat-mbean-registry]] -=== Enable Tomcat's MBean Registry -Embedded Tomcat's MBean registry is disabled by default. -This minimizes Tomcat's memory footprint. -If you want to use Tomcat's MBeans, for example so that they can be used to expose metrics via Micrometer, you must use the configprop:server.tomcat.mbeanregistry.enabled[] property to do so, as shown in the following example: - -[source,properties,indent=0,configprops] ----- -server.tomcat.mbeanregistry.enabled=true ----- - - - -[[howto-enable-multiple-listeners-in-undertow]] -=== Enable Multiple Listeners with Undertow -Add an `UndertowBuilderCustomizer` to the `UndertowServletWebServerFactory` and add a listener to the `Builder`, as shown in the following example: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- - @Bean - public UndertowServletWebServerFactory servletWebServerFactory() { - UndertowServletWebServerFactory factory = new UndertowServletWebServerFactory(); - factory.addBuilderCustomizers(new UndertowBuilderCustomizer() { - - @Override - public void customize(Builder builder) { - builder.addHttpListener(8080, "0.0.0.0"); - } - - }); - return factory; - } ----- - - - -[[howto-create-websocket-endpoints-using-serverendpoint]] -=== Create WebSocket Endpoints Using @ServerEndpoint -If you want to use `@ServerEndpoint` in a Spring Boot application that used an embedded container, you must declare a single `ServerEndpointExporter` `@Bean`, as shown in the following example: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- - @Bean - public ServerEndpointExporter serverEndpointExporter() { - return new ServerEndpointExporter(); - } ----- - -The bean shown in the preceding example registers any `@ServerEndpoint` annotated beans with the underlying WebSocket container. -When deployed to a standalone servlet container, this role is performed by a servlet container initializer, and the `ServerEndpointExporter` bean is not required. - - - -[[howto-spring-mvc]] -== Spring MVC -Spring Boot has a number of starters that include Spring MVC. -Note that some starters include a dependency on Spring MVC rather than include it directly. -This section answers common questions about Spring MVC and Spring Boot. - - - -[[howto-write-a-json-rest-service]] -=== Write a JSON REST Service -Any Spring `@RestController` in a Spring Boot application should render JSON response by default as long as Jackson2 is on the classpath, as shown in the following example: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- - @RestController - public class MyController { - - @RequestMapping("/thing") - public MyThing thing() { - return new MyThing(); - } - - } ----- - -As long as `MyThing` can be serialized by Jackson2 (true for a normal POJO or Groovy object), then `http://localhost:8080/thing` serves a JSON representation of it by default. -Note that, in a browser, you might sometimes see XML responses, because browsers tend to send accept headers that prefer XML. - - - -[[howto-write-an-xml-rest-service]] -=== Write an XML REST Service -If you have the Jackson XML extension (`jackson-dataformat-xml`) on the classpath, you can use it to render XML responses. -The previous example that we used for JSON would work. -To use the Jackson XML renderer, add the following dependency to your project: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - com.fasterxml.jackson.dataformat - jackson-dataformat-xml - ----- - -If Jackson's XML extension is not available and JAXB is available, XML can be rendered with the additional requirement of having `MyThing` annotated as `@XmlRootElement`, as shown in the following example: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- - @XmlRootElement - public class MyThing { - private String name; - // .. getters and setters - } ----- - -JAXB is only available out of the box with Java 8. -If you're using a more recent Java generation, add the following dependency to your project: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - org.glassfish.jaxb - jaxb-runtime - ----- - -NOTE: To get the server to render XML instead of JSON, you might have to send an `Accept: text/xml` header (or use a browser). - - - -[[howto-customize-the-jackson-objectmapper]] -=== Customize the Jackson ObjectMapper -Spring MVC (client and server side) uses `HttpMessageConverters` to negotiate content conversion in an HTTP exchange. -If Jackson is on the classpath, you already get the default converter(s) provided by `Jackson2ObjectMapperBuilder`, an instance of which is auto-configured for you. - -The `ObjectMapper` (or `XmlMapper` for Jackson XML converter) instance (created by default) has the following customized properties: - -* `MapperFeature.DEFAULT_VIEW_INCLUSION` is disabled -* `DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES` is disabled -* `SerializationFeature.WRITE_DATES_AS_TIMESTAMPS` is disabled - -Spring Boot also has some features to make it easier to customize this behavior. - -You can configure the `ObjectMapper` and `XmlMapper` instances by using the environment. -Jackson provides an extensive suite of simple on/off features that can be used to configure various aspects of its processing. -These features are described in six enums (in Jackson) that map onto properties in the environment: - -|=== -| Enum | Property | Values - -| `com.fasterxml.jackson.databind.DeserializationFeature` -| `spring.jackson.deserialization.` -| `true`, `false` - -| `com.fasterxml.jackson.core.JsonGenerator.Feature` -| `spring.jackson.generator.` -| `true`, `false` - -| `com.fasterxml.jackson.databind.MapperFeature` -| `spring.jackson.mapper.` -| `true`, `false` - -| `com.fasterxml.jackson.core.JsonParser.Feature` -| `spring.jackson.parser.` -| `true`, `false` - -| `com.fasterxml.jackson.databind.SerializationFeature` -| `spring.jackson.serialization.` -| `true`, `false` - -| `com.fasterxml.jackson.annotation.JsonInclude.Include` -| configprop:spring.jackson.default-property-inclusion[] -| `always`, `non_null`, `non_absent`, `non_default`, `non_empty` -|=== - -For example, to enable pretty print, set `spring.jackson.serialization.indent_output=true`. -Note that, thanks to the use of <>, the case of `indent_output` does not have to match the case of the corresponding enum constant, which is `INDENT_OUTPUT`. - -This environment-based configuration is applied to the auto-configured `Jackson2ObjectMapperBuilder` bean and applies to any mappers created by using the builder, including the auto-configured `ObjectMapper` bean. - -The context's `Jackson2ObjectMapperBuilder` can be customized by one or more `Jackson2ObjectMapperBuilderCustomizer` beans. -Such customizer beans can be ordered (Boot's own customizer has an order of 0), letting additional customization be applied both before and after Boot's customization. - -Any beans of type `com.fasterxml.jackson.databind.Module` are automatically registered with the auto-configured `Jackson2ObjectMapperBuilder` and are applied to any `ObjectMapper` instances that it creates. -This provides a global mechanism for contributing custom modules when you add new features to your application. - -If you want to replace the default `ObjectMapper` completely, either define a `@Bean` of that type and mark it as `@Primary` or, if you prefer the builder-based approach, define a `Jackson2ObjectMapperBuilder` `@Bean`. -Note that, in either case, doing so disables all auto-configuration of the `ObjectMapper`. - -If you provide any `@Beans` of type `MappingJackson2HttpMessageConverter`, they replace the default value in the MVC configuration. -Also, a convenience bean of type `HttpMessageConverters` is provided (and is always available if you use the default MVC configuration). -It has some useful methods to access the default and user-enhanced message converters. - -See the "`<>`" section and the {spring-boot-autoconfigure-module-code}/web/servlet/WebMvcAutoConfiguration.java[`WebMvcAutoConfiguration`] source code for more details. - - - -[[howto-customize-the-responsebody-rendering]] -=== Customize the @ResponseBody Rendering -Spring uses `HttpMessageConverters` to render `@ResponseBody` (or responses from `@RestController`). -You can contribute additional converters by adding beans of the appropriate type in a Spring Boot context. -If a bean you add is of a type that would have been included by default anyway (such as `MappingJackson2HttpMessageConverter` for JSON conversions), it replaces the default value. -A convenience bean of type `HttpMessageConverters` is provided and is always available if you use the default MVC configuration. -It has some useful methods to access the default and user-enhanced message converters (For example, it can be useful if you want to manually inject them into a custom `RestTemplate`). - -As in normal MVC usage, any `WebMvcConfigurer` beans that you provide can also contribute converters by overriding the `configureMessageConverters` method. -However, unlike with normal MVC, you can supply only additional converters that you need (because Spring Boot uses the same mechanism to contribute its defaults). -Finally, if you opt out of the Spring Boot default MVC configuration by providing your own `@EnableWebMvc` configuration, you can take control completely and do everything manually by using `getMessageConverters` from `WebMvcConfigurationSupport`. - -See the {spring-boot-autoconfigure-module-code}/web/servlet/WebMvcAutoConfiguration.java[`WebMvcAutoConfiguration`] source code for more details. - - - -[[howto-multipart-file-upload-configuration]] -=== Handling Multipart File Uploads -Spring Boot embraces the Servlet 3 `javax.servlet.http.Part` API to support uploading files. -By default, Spring Boot configures Spring MVC with a maximum size of 1MB per file and a maximum of 10MB of file data in a single request. -You may override these values, the location to which intermediate data is stored (for example, to the `/tmp` directory), and the threshold past which data is flushed to disk by using the properties exposed in the `MultipartProperties` class. -For example, if you want to specify that files be unlimited, set the configprop:spring.servlet.multipart.max-file-size[] property to `-1`. - -The multipart support is helpful when you want to receive multipart encoded file data as a `@RequestParam`-annotated parameter of type `MultipartFile` in a Spring MVC controller handler method. - -See the {spring-boot-autoconfigure-module-code}/web/servlet/MultipartAutoConfiguration.java[`MultipartAutoConfiguration`] source for more details. - -NOTE: It is recommended to use the container's built-in support for multipart uploads rather than introducing an additional dependency such as Apache Commons File Upload. - - - -[[howto-switch-off-the-spring-mvc-dispatcherservlet]] -=== Switch Off the Spring MVC DispatcherServlet -By default, all content is served from the root of your application (`/`). -If you would rather map to a different path, you can configure one as follows: - -[source,properties,indent=0,subs="verbatim",configprops] ----- - spring.mvc.servlet.path=/acme ----- - -If you have additional servlets you can declare a `@Bean` of type `Servlet` or `ServletRegistrationBean` for each and Spring Boot will register them transparently to the container. -Because servlets are registered that way, they can be mapped to a sub-context of the `DispatcherServlet` without invoking it. - -Configuring the `DispatcherServlet` yourself is unusual but if you really need to do it, a `@Bean` of type `DispatcherServletPath` must be provided as well to provide the path of your custom `DispatcherServlet`. - - - -[[howto-switch-off-default-mvc-configuration]] -=== Switch off the Default MVC Configuration -The easiest way to take complete control over MVC configuration is to provide your own `@Configuration` with the `@EnableWebMvc` annotation. -Doing so leaves all MVC configuration in your hands. - - - -[[howto-customize-view-resolvers]] -=== Customize ViewResolvers -A `ViewResolver` is a core component of Spring MVC, translating view names in `@Controller` to actual `View` implementations. -Note that `ViewResolvers` are mainly used in UI applications, rather than REST-style services (a `View` is not used to render a `@ResponseBody`). -There are many implementations of `ViewResolver` to choose from, and Spring on its own is not opinionated about which ones you should use. -Spring Boot, on the other hand, installs one or two for you, depending on what it finds on the classpath and in the application context. -The `DispatcherServlet` uses all the resolvers it finds in the application context, trying each one in turn until it gets a result. -If you add your own, you have to be aware of the order and in which position your resolver is added. - -`WebMvcAutoConfiguration` adds the following `ViewResolvers` to your context: - -* An `InternalResourceViewResolver` named '`defaultViewResolver`'. - This one locates physical resources that can be rendered by using the `DefaultServlet` (including static resources and JSP pages, if you use those). - It applies a prefix and a suffix to the view name and then looks for a physical resource with that path in the servlet context (the defaults are both empty but are accessible for external configuration through `spring.mvc.view.prefix` and `spring.mvc.view.suffix`). - You can override it by providing a bean of the same type. -* A `BeanNameViewResolver` named '`beanNameViewResolver`'. - This is a useful member of the view resolver chain and picks up any beans with the same name as the `View` being resolved. - It should not be necessary to override or replace it. -* A `ContentNegotiatingViewResolver` named '`viewResolver`' is added only if there *are* actually beans of type `View` present. - This is a '`master`' resolver, delegating to all the others and attempting to find a match to the '`Accept`' HTTP header sent by the client. - There is a useful https://spring.io/blog/2013/06/03/content-negotiation-using-views[blog about `ContentNegotiatingViewResolver`] that you might like to study to learn more, and you might also look at the source code for detail. - You can switch off the auto-configured `ContentNegotiatingViewResolver` by defining a bean named '`viewResolver`'. -* If you use Thymeleaf, you also have a `ThymeleafViewResolver` named '`thymeleafViewResolver`'. - It looks for resources by surrounding the view name with a prefix and suffix. - The prefix is `spring.thymeleaf.prefix`, and the suffix is `spring.thymeleaf.suffix`. - The values of the prefix and suffix default to '`classpath:/templates/`' and '`.html`', respectively. - You can override `ThymeleafViewResolver` by providing a bean of the same name. -* If you use FreeMarker, you also have a `FreeMarkerViewResolver` named '`freeMarkerViewResolver`'. - It looks for resources in a loader path (which is externalized to `spring.freemarker.templateLoaderPath` and has a default value of '`classpath:/templates/`') by surrounding the view name with a prefix and a suffix. - The prefix is externalized to `spring.freemarker.prefix`, and the suffix is externalized to `spring.freemarker.suffix`. - The default values of the prefix and suffix are empty and '`.ftlh`', respectively. - You can override `FreeMarkerViewResolver` by providing a bean of the same name. -* If you use Groovy templates (actually, if `groovy-templates` is on your classpath), you also have a `GroovyMarkupViewResolver` named '`groovyMarkupViewResolver`'. - It looks for resources in a loader path by surrounding the view name with a prefix and suffix (externalized to `spring.groovy.template.prefix` and `spring.groovy.template.suffix`). - The prefix and suffix have default values of '`classpath:/templates/`' and '`.tpl`', respectively. - You can override `GroovyMarkupViewResolver` by providing a bean of the same name. -* If you use Mustache, you also have a `MustacheViewResolver` named '`mustacheViewResolver`'. - It looks for resources by surrounding the view name with a prefix and suffix. - The prefix is `spring.mustache.prefix`, and the suffix is `spring.mustache.suffix`. - The values of the prefix and suffix default to '`classpath:/templates/`' and '`.mustache`', respectively. - You can override `MustacheViewResolver` by providing a bean of the same name. - -For more detail, see the following sections: - -* {spring-boot-autoconfigure-module-code}/web/servlet/WebMvcAutoConfiguration.java[`WebMvcAutoConfiguration`] -* {spring-boot-autoconfigure-module-code}/thymeleaf/ThymeleafAutoConfiguration.java[`ThymeleafAutoConfiguration`] -* {spring-boot-autoconfigure-module-code}/freemarker/FreeMarkerAutoConfiguration.java[`FreeMarkerAutoConfiguration`] -* {spring-boot-autoconfigure-module-code}/groovy/template/GroovyTemplateAutoConfiguration.java[`GroovyTemplateAutoConfiguration`] - - - -[[howto-use-test-with-spring-security]] -== Testing With Spring Security -Spring Security provides support for running tests as a specific user. -For example, the test in the snippet below will run with an authenticated user that has the `ADMIN` role. - -[source,java,indent=0] ----- - @Test - @WithMockUser(roles="ADMIN") - public void requestProtectedUrlWithUser() throws Exception { - mvc - .perform(get("/")) - ... - } ----- - -Spring Security provides comprehensive integration with Spring MVC Test and this can also be used when testing controllers using the `@WebMvcTest` slice and `MockMvc`. - -For additional details on Spring Security's testing support, refer to Spring Security's {spring-security-docs}#test[reference documentation]). - - - -[[howto-jersey]] -== Jersey - - - -[[howto-jersey-spring-security]] -=== Secure Jersey endpoints with Spring Security -Spring Security can be used to secure a Jersey-based web application in much the same way as it can be used to secure a Spring MVC-based web application. -However, if you want to use Spring Security's method-level security with Jersey, you must configure Jersey to use `setStatus(int)` rather `sendError(int)`. -This prevents Jersey from committing the response before Spring Security has had an opportunity to report an authentication or authorization failure to the client. - -The `jersey.config.server.response.setStatusOverSendError` property must be set to `true` on the application's `ResourceConfig` bean, as shown in the following example: - -[source,java,indent=0] ----- -include::{code-examples}/jersey/JerseySetStatusOverSendErrorExample.java[tag=resource-config] ----- - - - -[[howto-jersey-alongside-another-web-framework]] -=== Use Jersey Alongside Another Web Framework -To use Jersey alongside another web framework, such as Spring MVC, it should be configured so that it will allow the other framework to handle requests that it cannot handle. -First, configure Jersey to use a Filter rather than a Servlet by configuring the configprop:spring.jersey.type[] application property with a value of `filter`. -Second, configure your `ResourceConfig` to forward requests that would have resulted in a 404, as shown in the following example. - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- - @Component - public class JerseyConfig extends ResourceConfig { - - public JerseyConfig() { - register(Endpoint.class); - property(ServletProperties.FILTER_FORWARD_ON_404, true); - } - - } ----- - - - -[[howto-http-clients]] -== HTTP Clients -Spring Boot offers a number of starters that work with HTTP clients. -This section answers questions related to using them. - - - -[[howto-http-clients-proxy-configuration]] -=== Configure RestTemplate to Use a Proxy -As described in <>, you can use a `RestTemplateCustomizer` with `RestTemplateBuilder` to build a customized `RestTemplate`. -This is the recommended approach for creating a `RestTemplate` configured to use a proxy. - -The exact details of the proxy configuration depend on the underlying client request factory that is being used. -The following example configures `HttpComponentsClientRequestFactory` with an `HttpClient` that uses a proxy for all hosts except `192.168.0.5`: - -[source,java,indent=0] ----- -include::{code-examples}/web/client/RestTemplateProxyCustomizationExample.java[tag=customizer] ----- - -[[howto-webclient-reactor-netty-customization]] -=== Configure the TcpClient used by a Reactor Netty-based WebClient -When Reactor Netty is on the classpath a Reactor Netty-based `WebClient` is auto-configured. -To customize the client's handling of network connections, provide a `ClientHttpConnector` bean. -The following example configures a 60 second connect timeout and adds a `ReadTimeoutHandler`: - -[source,java,indent=0] ----- -include::{code-examples}/web/reactive/function/client/ReactorNettyClientCustomizationExample.java[tag=custom-http-connector] ----- - -TIP: Note the use of `ReactorResourceFactory` for the connection provider and event loop resources. -This ensures efficient sharing of resources for the server receiving requests and the client making requests. - - -[[howto-logging]] -== Logging -Spring Boot has no mandatory logging dependency, except for the Commons Logging API, which is typically provided by Spring Framework's `spring-jcl` module. -To use https://logback.qos.ch[Logback], you need to include it and `spring-jcl` on the classpath. -The simplest way to do that is through the starters, which all depend on `spring-boot-starter-logging`. -For a web application, you need only `spring-boot-starter-web`, since it depends transitively on the logging starter. -If you use Maven, the following dependency adds logging for you: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - org.springframework.boot - spring-boot-starter-web - ----- - -Spring Boot has a `LoggingSystem` abstraction that attempts to configure logging based on the content of the classpath. -If Logback is available, it is the first choice. - -If the only change you need to make to logging is to set the levels of various loggers, you can do so in `application.properties` by using the "logging.level" prefix, as shown in the following example: - -[source,properties,indent=0,subs="verbatim,quotes,attributes",configprops] ----- - logging.level.org.springframework.web=DEBUG - logging.level.org.hibernate=ERROR ----- - -You can also set the location of a file to which to write the log (in addition to the console) by using "logging.file.name". - -To configure the more fine-grained settings of a logging system, you need to use the native configuration format supported by the `LoggingSystem` in question. -By default, Spring Boot picks up the native configuration from its default location for the system (such as `classpath:logback.xml` for Logback), but you can set the location of the config file by using the configprop:logging.config[] property. - - - -[[howto-configure-logback-for-logging]] -=== Configure Logback for Logging -If you need to apply customizations to logback beyond those that can be achieved with `application.properties`, you'll need to add a standard logback configuration file. -You can add a `logback.xml` file to the root of your classpath for logback to find. -You can also use `logback-spring.xml` if you want to use the <>. - -TIP: The Logback documentation has a https://logback.qos.ch/manual/configuration.html[dedicated section that covers configuration] in some detail. - -Spring Boot provides a number of logback configurations that be `included` from your own configuration. -These includes are designed to allow certain common Spring Boot conventions to be re-applied. - -The following files are provided under `org/springframework/boot/logging/logback/`: - -* `defaults.xml` - Provides conversion rules, pattern properties and common logger configurations. -* `console-appender.xml` - Adds a `ConsoleAppender` using the `CONSOLE_LOG_PATTERN`. -* `file-appender.xml` - Adds a `RollingFileAppender` using the `FILE_LOG_PATTERN` and `ROLLING_FILE_NAME_PATTERN` with appropriate settings. - -In addition, a legacy `base.xml` file is provided for compatibility with earlier versions of Spring Boot. - -A typical custom `logback.xml` file would look something like this: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - - - - - - - - ----- - -Your logback configuration file can also make use of System properties that the `LoggingSystem` takes care of creating for you: - -* `$\{PID}`: The current process ID. -* `$\{LOG_FILE}`: Whether `logging.file.name` was set in Boot's external configuration. -* `$\{LOG_PATH}`: Whether `logging.file.path` (representing a directory for log files to live in) was set in Boot's external configuration. -* `$\{LOG_EXCEPTION_CONVERSION_WORD}`: Whether `logging.exception-conversion-word` was set in Boot's external configuration. -* `$\{ROLLING_FILE_NAME_PATTERN}`: Whether `logging.pattern.rolling-file-name` was set in Boot's external configuration. - -Spring Boot also provides some nice ANSI color terminal output on a console (but not in a log file) by using a custom Logback converter. -See the `CONSOLE_LOG_PATTERN` in the `defaults.xml` configuration for an example. - -If Groovy is on the classpath, you should be able to configure Logback with `logback.groovy` as well. -If present, this setting is given preference. - -NOTE: Spring extensions are not supported with Groovy configuration. -Any `logback-spring.groovy` files will not be detected. - - - -[[howto-configure-logback-for-logging-fileonly]] -==== Configure Logback for File-only Output -If you want to disable console logging and write output only to a file, you need a custom `logback-spring.xml` that imports `file-appender.xml` but not `console-appender.xml`, as shown in the following example: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - - - - - - - - ----- - -You also need to add `logging.file.name` to your `application.properties`, as shown in the following example: - -[source,properties,indent=0,subs="verbatim,quotes,attributes",configprops] ----- - logging.file.name=myapplication.log ----- - - - -[[howto-configure-log4j-for-logging]] -=== Configure Log4j for Logging -Spring Boot supports https://logging.apache.org/log4j/2.x/[Log4j 2] for logging configuration if it is on the classpath. -If you use the starters for assembling dependencies, you have to exclude Logback and then include log4j 2 instead. -If you do not use the starters, you need to provide (at least) `spring-jcl` in addition to Log4j 2. - -The simplest path is probably through the starters, even though it requires some jiggling with excludes. -The following example shows how to set up the starters in Maven: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter - - - org.springframework.boot - spring-boot-starter-logging - - - - - org.springframework.boot - spring-boot-starter-log4j2 - ----- - -And the following example shows one way to set up the starters in Gradle: - -[source,groovy,indent=0,subs="verbatim,quotes,attributes"] ----- - dependencies { - compile 'org.springframework.boot:spring-boot-starter-web' - compile 'org.springframework.boot:spring-boot-starter-log4j2' - } - - configurations { - all { - exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging' - } - } ----- - -NOTE: The Log4j starters gather together the dependencies for common logging requirements (such as having Tomcat use `java.util.logging` but configuring the output using Log4j 2). - -NOTE: To ensure that debug logging performed using `java.util.logging` is routed into Log4j 2, configure its https://logging.apache.org/log4j/2.0/log4j-jul/index.html[JDK logging adapter] by setting the `java.util.logging.manager` system property to `org.apache.logging.log4j.jul.LogManager`. - - - -[[howto-configure-log4j-for-logging-yaml-or-json-config]] -==== Use YAML or JSON to Configure Log4j 2 -In addition to its default XML configuration format, Log4j 2 also supports YAML and JSON configuration files. -To configure Log4j 2 to use an alternative configuration file format, add the appropriate dependencies to the classpath and name your configuration files to match your chosen file format, as shown in the following example: - -[cols="10,75a,15a"] -|=== -| Format | Dependencies | File names - -|YAML -| `com.fasterxml.jackson.core:jackson-databind` + `com.fasterxml.jackson.dataformat:jackson-dataformat-yaml` -| `log4j2.yaml` + `log4j2.yml` - -|JSON -| `com.fasterxml.jackson.core:jackson-databind` -| `log4j2.json` + `log4j2.jsn` -|=== - - - -[[howto-data-access]] -== Data Access -Spring Boot includes a number of starters for working with data sources. -This section answers questions related to doing so. - - - -[[howto-configure-a-datasource]] -=== Configure a Custom DataSource -To configure your own `DataSource`, define a `@Bean` of that type in your configuration. -Spring Boot reuses your `DataSource` anywhere one is required, including database initialization. -If you need to externalize some settings, you can bind your `DataSource` to the environment (see "`<>`"). - -The following example shows how to define a data source in a bean: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- - @Bean - @ConfigurationProperties(prefix="app.datasource") - public DataSource dataSource() { - return new FancyDataSource(); - } ----- - -The following example shows how to define a data source by setting properties: - -[source,properties,indent=0] ----- - app.datasource.url=jdbc:h2:mem:mydb - app.datasource.username=sa - app.datasource.pool-size=30 ----- - -Assuming that your `FancyDataSource` has regular JavaBean properties for the URL, the username, and the pool size, these settings are bound automatically before the `DataSource` is made available to other components. -The regular <> also happens (so the relevant sub-set of `spring.datasource.*` can still be used with your custom configuration). - -Spring Boot also provides a utility builder class, called `DataSourceBuilder`, that can be used to create one of the standard data sources (if it is on the classpath). -The builder can detect the one to use based on what's available on the classpath. -It also auto-detects the driver based on the JDBC URL. - -The following example shows how to create a data source by using a `DataSourceBuilder`: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- -include::{code-examples}/jdbc/BasicDataSourceExample.java[tag=configuration] ----- - -To run an app with that `DataSource`, all you need is the connection information. -Pool-specific settings can also be provided. -Check the implementation that is going to be used at runtime for more details. - -The following example shows how to define a JDBC data source by setting properties: - -[source,properties,indent=0] ----- - app.datasource.url=jdbc:mysql://localhost/test - app.datasource.username=dbuser - app.datasource.password=dbpass - app.datasource.pool-size=30 ----- - -However, there is a catch. -Because the actual type of the connection pool is not exposed, no keys are generated in the metadata for your custom `DataSource` and no completion is available in your IDE (because the `DataSource` interface exposes no properties). -Also, if you happen to have Hikari on the classpath, this basic setup does not work, because Hikari has no `url` property (but does have a `jdbcUrl` property). -In that case, you must rewrite your configuration as follows: - -[source,properties,indent=0] ----- - app.datasource.jdbc-url=jdbc:mysql://localhost/test - app.datasource.username=dbuser - app.datasource.password=dbpass - app.datasource.maximum-pool-size=30 ----- - -You can fix that by forcing the connection pool to use and return a dedicated implementation rather than `DataSource`. -You cannot change the implementation at runtime, but the list of options will be explicit. - -The following example shows how create a `HikariDataSource` with `DataSourceBuilder`: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- -include::{code-examples}/jdbc/SimpleDataSourceExample.java[tag=configuration] ----- - -You can even go further by leveraging what `DataSourceProperties` does for you -- that is, by providing a default embedded database with a sensible username and password if no URL is provided. -You can easily initialize a `DataSourceBuilder` from the state of any `DataSourceProperties` object, so you could also inject the DataSource that Spring Boot creates automatically. -However, that would split your configuration into two namespaces: `url`, `username`, `password`, `type`, and `driver` on `spring.datasource` and the rest on your custom namespace (`app.datasource`). -To avoid that, you can redefine a custom `DataSourceProperties` on your custom namespace, as shown in the following example: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- -include::{code-examples}/jdbc/ConfigurableDataSourceExample.java[tag=configuration] ----- - -This setup puts you _in sync_ with what Spring Boot does for you by default, except that a dedicated connection pool is chosen (in code) and its settings are exposed in the `app.datasource.configuration` sub namespace. -Because `DataSourceProperties` is taking care of the `url`/`jdbcUrl` translation for you, you can configure it as follows: - -[source,properties,indent=0] ----- - app.datasource.url=jdbc:mysql://localhost/test - app.datasource.username=dbuser - app.datasource.password=dbpass - app.datasource.configuration.maximum-pool-size=30 ----- - -TIP: Spring Boot will expose Hikari-specific settings to `spring.datasource.hikari`. -This example uses a more generic `configuration` sub namespace as the example does not support multiple datasource implementations. - -NOTE: Because your custom configuration chooses to go with Hikari, `app.datasource.type` has no effect. -In practice, the builder is initialized with whatever value you might set there and then overridden by the call to `.type()`. - -See "`<>`" in the "`Spring Boot features`" section and the {spring-boot-autoconfigure-module-code}/jdbc/DataSourceAutoConfiguration.java[`DataSourceAutoConfiguration`] class for more details. - - - -[[howto-two-datasources]] -=== Configure Two DataSources -If you need to configure multiple data sources, you can apply the same tricks that are described in the previous section. -You must, however, mark one of the `DataSource` instances as `@Primary`, because various auto-configurations down the road expect to be able to get one by type. - -If you create your own `DataSource`, the auto-configuration backs off. -In the following example, we provide the _exact_ same feature set as the auto-configuration provides on the primary data source: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- -include::{code-examples}/jdbc/SimpleTwoDataSourcesExample.java[tag=configuration] ----- - -TIP: `firstDataSourceProperties` has to be flagged as `@Primary` so that the database initializer feature uses your copy (if you use the initializer). - -Both data sources are also bound for advanced customizations. -For instance, you could configure them as follows: - -[source,properties,indent=0] ----- - app.datasource.first.url=jdbc:mysql://localhost/first - app.datasource.first.username=dbuser - app.datasource.first.password=dbpass - app.datasource.first.configuration.maximum-pool-size=30 - - app.datasource.second.url=jdbc:mysql://localhost/second - app.datasource.second.username=dbuser - app.datasource.second.password=dbpass - app.datasource.second.max-total=30 ----- - -You can apply the same concept to the secondary `DataSource` as well, as shown in the following example: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- -include::{code-examples}/jdbc/CompleteTwoDataSourcesExample.java[tag=configuration] ----- - -The preceding example configures two data sources on custom namespaces with the same logic as Spring Boot would use in auto-configuration. -Note that each `configuration` sub namespace provides advanced settings based on the chosen implementation. - - - -[[howto-use-spring-data-repositories]] -=== Use Spring Data Repositories -Spring Data can create implementations of `@Repository` interfaces of various flavors. -Spring Boot handles all of that for you, as long as those `@Repositories` are included in the same package (or a sub-package) of your `@EnableAutoConfiguration` class. - -For many applications, all you need is to put the right Spring Data dependencies on your classpath. -There is a `spring-boot-starter-data-jpa` for JPA, spring-boot-starter-data-mongodb` for Mongodb, etc. -To get started, create some repository interfaces to handle your `@Entity` objects. - -Spring Boot tries to guess the location of your `@Repository` definitions, based on the `@EnableAutoConfiguration` it finds. -To get more control, use the `@EnableJpaRepositories` annotation (from Spring Data JPA). - -For more about Spring Data, see the {spring-data}[Spring Data project page]. - - - -[[howto-separate-entity-definitions-from-spring-configuration]] -=== Separate @Entity Definitions from Spring Configuration -Spring Boot tries to guess the location of your `@Entity` definitions, based on the `@EnableAutoConfiguration` it finds. -To get more control, you can use the `@EntityScan` annotation, as shown in the following example: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- - @Configuration(proxyBeanMethods = false) - @EnableAutoConfiguration - @EntityScan(basePackageClasses=City.class) - public class Application { - - //... - - } ----- - - - -[[howto-configure-jpa-properties]] -=== Configure JPA Properties -Spring Data JPA already provides some vendor-independent configuration options (such as those for SQL logging), and Spring Boot exposes those options and a few more for Hibernate as external configuration properties. -Some of them are automatically detected according to the context so you should not have to set them. - -The `spring.jpa.hibernate.ddl-auto` is a special case, because, depending on runtime conditions, it has different defaults. -If an embedded database is used and no schema manager (such as Liquibase or Flyway) is handling the `DataSource`, it defaults to `create-drop`. -In all other cases, it defaults to `none`. - -The dialect to use is detected by the JPA provider. -If you prefer to set the dialect yourself, set the configprop:spring.jpa.database-platform[] property. - -The most common options to set are shown in the following example: - -[indent=0,subs="verbatim,quotes,attributes"] ----- - spring.jpa.hibernate.naming.physical-strategy=com.example.MyPhysicalNamingStrategy - spring.jpa.show-sql=true ----- - -In addition, all properties in `+spring.jpa.properties.*+` are passed through as normal JPA properties (with the prefix stripped) when the local `EntityManagerFactory` is created. - -[WARNING] -==== -You need to ensure that names defined under `+spring.jpa.properties.*+` exactly match those expected by your JPA provider. -Spring Boot will not attempt any kind of relaxed binding for these entries. - -For example, if you want to configure Hibernate's batch size you must use `+spring.jpa.properties.hibernate.jdbc.batch_size+`. -If you use other forms, such as `batchSize` or `batch-size`, Hibernate will not apply the setting. -==== - -TIP: If you need to apply advanced customization to Hibernate properties, consider registering a `HibernatePropertiesCustomizer` bean that will be invoked prior to creating the `EntityManagerFactory`. -This takes precedence to anything that is applied by the auto-configuration. - - - -[[howto-configure-hibernate-naming-strategy]] -=== Configure Hibernate Naming Strategy -Hibernate uses {hibernate-docs}#naming[two different naming strategies] to map names from the object model to the corresponding database names. -The fully qualified class name of the physical and the implicit strategy implementations can be configured by setting the `spring.jpa.hibernate.naming.physical-strategy` and `spring.jpa.hibernate.naming.implicit-strategy` properties, respectively. -Alternatively, if `ImplicitNamingStrategy` or `PhysicalNamingStrategy` beans are available in the application context, Hibernate will be automatically configured to use them. - -By default, Spring Boot configures the physical naming strategy with `SpringPhysicalNamingStrategy`. -This implementation provides the same table structure as Hibernate 4: all dots are replaced by underscores and camel casing is replaced by underscores as well. -By default, all table names are generated in lower case, but it is possible to override that flag if your schema requires it. - -For example, a `TelephoneNumber` entity is mapped to the `telephone_number` table. - -If you prefer to use Hibernate 5's default instead, set the following property: - -[indent=0,subs="verbatim,quotes,attributes"] ----- - spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl ----- - -Alternatively, you can configure the following bean: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- - @Bean - public PhysicalNamingStrategy physicalNamingStrategy() { - return new PhysicalNamingStrategyStandardImpl(); - } ----- - -See {spring-boot-autoconfigure-module-code}/orm/jpa/HibernateJpaAutoConfiguration.java[`HibernateJpaAutoConfiguration`] and {spring-boot-autoconfigure-module-code}/orm/jpa/JpaBaseConfiguration.java[`JpaBaseConfiguration`] for more details. - - - -[[howto-configure-hibernate-second-level-caching]] -=== Configure Hibernate Second-Level Caching -Hibernate {hibernate-docs}#caching[second-level cache] can be configured for a range of cache providers. -Rather than configuring Hibernate to lookup the cache provider again, it is better to provide the one that is available in the context whenever possible. - -If you're using JCache, this is pretty easy. -First, make sure that `org.hibernate:hibernate-jcache` is available on the classpath. -Then, add a `HibernatePropertiesCustomizer` bean as shown in the following example: - -[source,java,indent=0] ----- -include::{code-examples}/jpa/HibernateSecondLevelCacheExample.java[tag=configuration] ----- - -This customizer will configure Hibernate to use the same `CacheManager` as the one that the application uses. -It is also possible to use separate `CacheManager` instances. -For details, refer to {hibernate-docs}#caching-provider-jcache[the Hibernate user guide]. - - - -[[howto-use-dependency-injection-hibernate-components]] -=== Use Dependency Injection in Hibernate Components -By default, Spring Boot registers a `BeanContainer` implementation that uses the `BeanFactory` so that converters and entity listeners can use regular dependency injection. - -You can disable or tune this behaviour by registering a `HibernatePropertiesCustomizer` that removes or changes the `hibernate.resource.beans.container` property. - - - -[[howto-use-custom-entity-manager]] -=== Use a Custom EntityManagerFactory -To take full control of the configuration of the `EntityManagerFactory`, you need to add a `@Bean` named '`entityManagerFactory`'. -Spring Boot auto-configuration switches off its entity manager in the presence of a bean of that type. - - - -[[howto-use-two-entity-managers]] -=== Use Two EntityManagers -Even if the default `EntityManagerFactory` works fine, you need to define a new one. -Otherwise, the presence of the second bean of that type switches off the default. -To make it easy to do, you can use the convenient `EntityManagerBuilder` provided by Spring Boot. -Alternatively, you can just the `LocalContainerEntityManagerFactoryBean` directly from Spring ORM, as shown in the following example: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- - // add two data sources configured as above - - @Bean - public LocalContainerEntityManagerFactoryBean customerEntityManagerFactory( - EntityManagerFactoryBuilder builder) { - return builder - .dataSource(customerDataSource()) - .packages(Customer.class) - .persistenceUnit("customers") - .build(); - } - - @Bean - public LocalContainerEntityManagerFactoryBean orderEntityManagerFactory( - EntityManagerFactoryBuilder builder) { - return builder - .dataSource(orderDataSource()) - .packages(Order.class) - .persistenceUnit("orders") - .build(); - } ----- - -The configuration above almost works on its own. -To complete the picture, you need to configure `TransactionManagers` for the two `EntityManagers` as well. -If you mark one of them as `@Primary`, it could be picked up by the default `JpaTransactionManager` in Spring Boot. -The other would have to be explicitly injected into a new instance. -Alternatively, you might be able to use a JTA transaction manager that spans both. - -If you use Spring Data, you need to configure `@EnableJpaRepositories` accordingly, as shown in the following example: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- - @Configuration(proxyBeanMethods = false) - @EnableJpaRepositories(basePackageClasses = Customer.class, - entityManagerFactoryRef = "customerEntityManagerFactory") - public class CustomerConfiguration { - ... - } - - @Configuration(proxyBeanMethods = false) - @EnableJpaRepositories(basePackageClasses = Order.class, - entityManagerFactoryRef = "orderEntityManagerFactory") - public class OrderConfiguration { - ... - } ----- - - - -[[howto-use-traditional-persistence-xml]] -=== Use a Traditional `persistence.xml` File -Spring Boot will not search for or use a `META-INF/persistence.xml` by default. -If you prefer to use a traditional `persistence.xml`, you need to define your own `@Bean` of type `LocalEntityManagerFactoryBean` (with an ID of '`entityManagerFactory`') and set the persistence unit name there. - -See {spring-boot-autoconfigure-module-code}/orm/jpa/JpaBaseConfiguration.java[`JpaBaseConfiguration`] for the default settings. - - - -[[howto-use-spring-data-jpa--and-mongo-repositories]] -=== Use Spring Data JPA and Mongo Repositories -Spring Data JPA and Spring Data Mongo can both automatically create `Repository` implementations for you. -If they are both present on the classpath, you might have to do some extra configuration to tell Spring Boot which repositories to create. -The most explicit way to do that is to use the standard Spring Data `+@EnableJpaRepositories+` and `+@EnableMongoRepositories+` annotations and provide the location of your `Repository` interfaces. - -There are also flags (`+spring.data.*.repositories.enabled+` and `+spring.data.*.repositories.type+`) that you can use to switch the auto-configured repositories on and off in external configuration. -Doing so is useful, for instance, in case you want to switch off the Mongo repositories and still use the auto-configured `MongoTemplate`. - -The same obstacle and the same features exist for other auto-configured Spring Data repository types (Elasticsearch, Solr, and others). -To work with them, change the names of the annotations and flags accordingly. - - - -[[howto-use-customize-spring-datas-web-support]] -=== Customize Spring Data's Web Support -Spring Data provides web support that simplifies the use of Spring Data repositories in a web application. -Spring Boot provides properties in the `spring.data.web` namespace for customizing its configuration. -Note that if you are using Spring Data REST, you must use the properties in the `spring.data.rest` namespace instead. - - -[[howto-use-exposing-spring-data-repositories-rest-endpoint]] -=== Expose Spring Data Repositories as REST Endpoint -Spring Data REST can expose the `Repository` implementations as REST endpoints for you, -provided Spring MVC has been enabled for the application. - -Spring Boot exposes a set of useful properties (from the `spring.data.rest` namespace) that customize the {spring-data-rest-api}/core/config/RepositoryRestConfiguration.html[`RepositoryRestConfiguration`]. -If you need to provide additional customization, you should use a {spring-data-rest-api}/webmvc/config/RepositoryRestConfigurer.html[`RepositoryRestConfigurer`] bean. - -NOTE: If you do not specify any order on your custom `RepositoryRestConfigurer`, it runs after the one Spring Boot uses internally. -If you need to specify an order, make sure it is higher than 0. - - - -[[howto-configure-a-component-that-is-used-by-JPA]] -=== Configure a Component that is Used by JPA -If you want to configure a component that JPA uses, then you need to ensure that the component is initialized before JPA. -When the component is auto-configured, Spring Boot takes care of this for you. -For example, when Flyway is auto-configured, Hibernate is configured to depend upon Flyway so that Flyway has a chance to initialize the database before Hibernate tries to use it. - -If you are configuring a component yourself, you can use an `EntityManagerFactoryDependsOnPostProcessor` subclass as a convenient way of setting up the necessary dependencies. -For example, if you use Hibernate Search with Elasticsearch as its index manager, any `EntityManagerFactory` beans must be configured to depend on the `elasticsearchClient` bean, as shown in the following example: - -[source,java,indent=0] ----- -include::{code-examples}/elasticsearch/HibernateSearchElasticsearchExample.java[tag=configuration] ----- - - - -[[howto-configure-jOOQ-with-multiple-datasources]] -=== Configure jOOQ with Two DataSources -If you need to use jOOQ with multiple data sources, you should create your own `DSLContext` for each one. -Refer to {spring-boot-autoconfigure-module-code}/jooq/JooqAutoConfiguration.java[JooqAutoConfiguration] for more details. - -TIP: In particular, `JooqExceptionTranslator` and `SpringTransactionProvider` can be reused to provide similar features to what the auto-configuration does with a single `DataSource`. - - - -[[howto-database-initialization]] -== Database Initialization -An SQL database can be initialized in different ways depending on what your stack is. -Of course, you can also do it manually, provided the database is a separate process. -It is recommended to use a single mechanism for schema generation. - - - -[[howto-initialize-a-database-using-jpa]] -=== Initialize a Database Using JPA -JPA has features for DDL generation, and these can be set up to run on startup against the database. -This is controlled through two external properties: - -* `spring.jpa.generate-ddl` (boolean) switches the feature on and off and is vendor independent. -* `spring.jpa.hibernate.ddl-auto` (enum) is a Hibernate feature that controls the behavior in a more fine-grained way. - This feature is described in more detail later in this guide. - - - -[[howto-initialize-a-database-using-hibernate]] -=== Initialize a Database Using Hibernate -You can set `spring.jpa.hibernate.ddl-auto` explicitly and the standard Hibernate property values are `none`, `validate`, `update`, `create`, and `create-drop`. -Spring Boot chooses a default value for you based on whether it thinks your database is embedded. -It defaults to `create-drop` if no schema manager has been detected or `none` in all other cases. -An embedded database is detected by looking at the `Connection` type. -`hsqldb`, `h2`, and `derby` are embedded, and others are not. -Be careful when switching from in-memory to a '`real`' database that you do not make assumptions about the existence of the tables and data in the new platform. -You either have to set `ddl-auto` explicitly or use one of the other mechanisms to initialize the database. - -NOTE: You can output the schema creation by enabling the `org.hibernate.SQL` logger. -This is done for you automatically if you enable the <>. - -In addition, a file named `import.sql` in the root of the classpath is executed on startup if Hibernate creates the schema from scratch (that is, if the `ddl-auto` property is set to `create` or `create-drop`). -This can be useful for demos and for testing if you are careful but is probably not something you want to be on the classpath in production. -It is a Hibernate feature (and has nothing to do with Spring). - - - -[[howto-initialize-a-database-using-spring-jdbc]] -=== Initialize a Database -Spring Boot can automatically create the schema (DDL scripts) of your `DataSource` and initialize it (DML scripts). -It loads SQL from the standard root classpath locations: `schema.sql` and `data.sql`, respectively. -In addition, Spring Boot processes the `schema-$\{platform}.sql` and `data-$\{platform}.sql` files (if present), where `platform` is the value of `spring.datasource.platform`. -This allows you to switch to database-specific scripts if necessary. -For example, you might choose to set it to the vendor name of the database (`hsqldb`, `h2`, `oracle`, `mysql`, `postgresql`, and so on). - -[NOTE] -==== -Spring Boot automatically creates the schema of an embedded `DataSource`. -This behavior can be customized by using the configprop:spring.datasource.initialization-mode[] property. -For instance, if you want to always initialize the `DataSource` regardless of its type: - -[indent=0,subs="verbatim,quotes,attributes"] ----- - spring.datasource.initialization-mode=always ----- -==== - -By default, Spring Boot enables the fail-fast feature of the Spring JDBC initializer. -This means that, if the scripts cause exceptions, the application fails to start. -You can tune that behavior by setting `spring.datasource.continue-on-error`. - -NOTE: In a JPA-based app, you can choose to let Hibernate create the schema or use `schema.sql`, but you cannot do both. -Make sure to disable `spring.jpa.hibernate.ddl-auto` if you use `schema.sql`. - - - -[[howto-initialize-a-spring-batch-database]] -=== Initialize a Spring Batch Database -If you use Spring Batch, it comes pre-packaged with SQL initialization scripts for most popular database platforms. -Spring Boot can detect your database type and execute those scripts on startup. -If you use an embedded database, this happens by default. -You can also enable it for any database type, as shown in the following example: - -[indent=0,subs="verbatim,quotes,attributes"] ----- - spring.batch.initialize-schema=always ----- - -You can also switch off the initialization explicitly by setting `spring.batch.initialize-schema=never`. - - - -[[howto-use-a-higher-level-database-migration-tool]] -=== Use a Higher-level Database Migration Tool -Spring Boot supports two higher-level migration tools: https://flywaydb.org/[Flyway] and https://www.liquibase.org/[Liquibase]. - - - -[[howto-execute-flyway-database-migrations-on-startup]] -==== Execute Flyway Database Migrations on Startup -To automatically run Flyway database migrations on startup, add the `org.flywaydb:flyway-core` to your classpath. - -Typically, migrations are scripts in the form `V__.sql` (with `` an underscore-separated version, such as '`1`' or '`2_1`'). -By default, they are in a folder called `classpath:db/migration`, but you can modify that location by setting `spring.flyway.locations`. -This is a comma-separated list of one or more `classpath:` or `filesystem:` locations. -For example, the following configuration would search for scripts in both the default classpath location and the `/opt/migration` directory: - -[source,properties,indent=0,configprops] ----- - spring.flyway.locations=classpath:db/migration,filesystem:/opt/migration ----- - -You can also add a special `\{vendor}` placeholder to use vendor-specific scripts. -Assume the following: - -[source,properties,indent=0,configprops] ----- - spring.flyway.locations=classpath:db/migration/{vendor} ----- - -Rather than using `db/migration`, the preceding configuration sets the folder to use according to the type of the database (such as `db/migration/mysql` for MySQL). -The list of supported databases is available in {spring-boot-module-code}/jdbc/DatabaseDriver.java[`DatabaseDriver`]. - -Migrations can also be written in Java. -Flyway will be auto-configured with any beans that implement `JavaMigration`. - -{spring-boot-autoconfigure-module-code}/flyway/FlywayProperties.java[`FlywayProperties`] provides most of Flyway's settings and a small set of additional properties that can be used to disable the migrations or switch off the location checking. -If you need more control over the configuration, consider registering a `FlywayConfigurationCustomizer` bean. - -Spring Boot calls `Flyway.migrate()` to perform the database migration. -If you would like more control, provide a `@Bean` that implements {spring-boot-autoconfigure-module-code}/flyway/FlywayMigrationStrategy.java[`FlywayMigrationStrategy`]. - -Flyway supports SQL and Java https://flywaydb.org/documentation/callbacks.html[callbacks]. -To use SQL-based callbacks, place the callback scripts in the `classpath:db/migration` folder. -To use Java-based callbacks, create one or more beans that implement `Callback`. -Any such beans are automatically registered with `Flyway`. -They can be ordered by using `@Order` or by implementing `Ordered`. -Beans that implement the deprecated `FlywayCallback` interface can also be detected, however they cannot be used alongside `Callback` beans. - -By default, Flyway autowires the (`@Primary`) `DataSource` in your context and uses that for migrations. -If you like to use a different `DataSource`, you can create one and mark its `@Bean` as `@FlywayDataSource`. -If you do so and want two data sources, remember to create another one and mark it as `@Primary`. -Alternatively, you can use Flyway's native `DataSource` by setting `spring.flyway.[url,user,password]` in external properties. -Setting either `spring.flyway.url` or `spring.flyway.user` is sufficient to cause Flyway to use its own `DataSource`. -If any of the three properties has not be set, the value of its equivalent `spring.datasource` property will be used. - -You can also use Flyway to provide data for specific scenarios. -For example, you can place test-specific migrations in `src/test/resources` and they are run only when your application starts for testing. -Also, you can use profile-specific configuration to customize `spring.flyway.locations` so that certain migrations run only when a particular profile is active. -For example, in `application-dev.properties`, you might specify the following setting: - -[source,properties,indent=0,configprops] ----- - spring.flyway.locations=classpath:/db/migration,classpath:/dev/db/migration ----- - -With that setup, migrations in `dev/db/migration` run only when the `dev` profile is active. - - - -[[howto-execute-liquibase-database-migrations-on-startup]] -==== Execute Liquibase Database Migrations on Startup -To automatically run Liquibase database migrations on startup, add the `org.liquibase:liquibase-core` to your classpath. - -By default, the master change log is read from `db/changelog/db.changelog-master.yaml`, but you can change the location by setting `spring.liquibase.change-log`. -In addition to YAML, Liquibase also supports JSON, XML, and SQL change log formats. - -By default, Liquibase autowires the (`@Primary`) `DataSource` in your context and uses that for migrations. -If you need to use a different `DataSource`, you can create one and mark its `@Bean` as `@LiquibaseDataSource`. -If you do so and you want two data sources, remember to create another one and mark it as `@Primary`. -Alternatively, you can use Liquibase's native `DataSource` by setting `spring.liquibase.[url,user,password]` in external properties. -Setting either `spring.liquibase.url` or `spring.liquibase.user` is sufficient to cause Liquibase to use its own `DataSource`. -If any of the three properties has not be set, the value of its equivalent `spring.datasource` property will be used. - -See {spring-boot-autoconfigure-module-code}/liquibase/LiquibaseProperties.java[`LiquibaseProperties`] for details about available settings such as contexts, the default schema, and others. - - - -[[howto-messaging]] -== Messaging -Spring Boot offers a number of starters that include messaging. -This section answers questions that arise from using messaging with Spring Boot. - - - -[[howto-jms-disable-transaction]] -=== Disable Transacted JMS Session -If your JMS broker does not support transacted sessions, you have to disable the support of transactions altogether. -If you create your own `JmsListenerContainerFactory`, there is nothing to do, since, by default it cannot be transacted. -If you want to use the `DefaultJmsListenerContainerFactoryConfigurer` to reuse Spring Boot's default, you can disable transacted sessions, as follows: - -[source,java,indent=0] ----- - @Bean - public DefaultJmsListenerContainerFactory jmsListenerContainerFactory( - ConnectionFactory connectionFactory, - DefaultJmsListenerContainerFactoryConfigurer configurer) { - DefaultJmsListenerContainerFactory listenerFactory = - new DefaultJmsListenerContainerFactory(); - configurer.configure(listenerFactory, connectionFactory); - listenerFactory.setTransactionManager(null); - listenerFactory.setSessionTransacted(false); - return listenerFactory; - } ----- - -The preceding example overrides the default factory, and it should be applied to any other factory that your application defines, if any. - - - -[[howto-batch-applications]] -== Batch Applications -This section answers questions that arise from using Spring Batch with Spring Boot. - -NOTE: By default, batch applications require a `DataSource` to store job details. -Batch autowires a single `DataSource` in your context and uses that for processing. -To have Batch use a `DataSource` other than the application’s main `DataSource`, declare a `DataSource` bean, annotating its `@Bean` method with `@BatchDataSource`. -If you do so and want two data sources, remember to create another one and mark it as `@Primary`. -To take greater control, implement `BatchConfigurer`. -See {spring-batch-api}/core/configuration/annotation/EnableBatchProcessing.html[The Javadoc of `@EnableBatchProcessing`] for more details. - -For more about Spring Batch, see the {spring-batch}[Spring Batch project page]. - - - -[[howto-execute-spring-batch-jobs-on-startup]] -=== Execute Spring Batch Jobs on Startup -Spring Batch auto-configuration is enabled by adding `@EnableBatchProcessing` (from Spring Batch) somewhere in your context. - -By default, it executes *all* `Jobs` in the application context on startup (see {spring-boot-autoconfigure-module-code}/batch/JobLauncherCommandLineRunner.java[JobLauncherCommandLineRunner] for details). -You can narrow down to a specific job or jobs by specifying `spring.batch.job.names` (which takes a comma-separated list of job name patterns). - -[TIP] -.Specifying job parameters on the command line -==== -Unlike command line option arguments that <> (i.e. by starting with `--`, such as `--my-property=value`), job parameters have to be specified on the command line without dashes (e.g. `jobParam=value`). -==== - -If the application context includes a `JobRegistry`, the jobs in `spring.batch.job.names` are looked up in the registry instead of being autowired from the context. -This is a common pattern with more complex systems, where multiple jobs are defined in child contexts and registered centrally. - -See {spring-boot-autoconfigure-module-code}/batch/BatchAutoConfiguration.java[BatchAutoConfiguration] and https://github.com/spring-projects/spring-batch/blob/master/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/EnableBatchProcessing.java[@EnableBatchProcessing] for more details. - - - -[[howto-actuator]] -== Actuator -Spring Boot includes the Spring Boot Actuator. -This section answers questions that often arise from its use. - - - -[[howto-change-the-http-port-or-address-of-the-actuator-endpoints]] -=== Change the HTTP Port or Address of the Actuator Endpoints -In a standalone application, the Actuator HTTP port defaults to the same as the main HTTP port. -To make the application listen on a different port, set the external property: configprop:management.server.port[]. -To listen on a completely different network address (such as when you have an internal network for management and an external one for user applications), you can also set `management.server.address` to a valid IP address to which the server is able to bind. - -For more detail, see the {spring-boot-actuator-autoconfigure-module-code}/web/server/ManagementServerProperties.java[`ManagementServerProperties`] source code and "`<>`" in the "`Production-ready features`" section. - - - -[[howto-customize-the-whitelabel-error-page]] -=== Customize the '`whitelabel`' Error Page -Spring Boot installs a '`whitelabel`' error page that you see in a browser client if you encounter a server error (machine clients consuming JSON and other media types should see a sensible response with the right error code). - -NOTE: Set `server.error.whitelabel.enabled=false` to switch the default error page off. -Doing so restores the default of the servlet container that you are using. -Note that Spring Boot still tries to resolve the error view, so you should probably add your own error page rather than disabling it completely. - -Overriding the error page with your own depends on the templating technology that you use. -For example, if you use Thymeleaf, you can add an `error.html` template. -If you use FreeMarker, you can add an `error.ftlh` template. -In general, you need a `View` that resolves with a name of `error` or a `@Controller` that handles the `/error` path. -Unless you replaced some of the default configuration, you should find a `BeanNameViewResolver` in your `ApplicationContext`, so a `@Bean` named `error` would be a simple way of doing that. -See {spring-boot-autoconfigure-module-code}/web/servlet/error/ErrorMvcAutoConfiguration.java[`ErrorMvcAutoConfiguration`] for more options. - -See also the section on "`<>`" for details of how to register handlers in the servlet container. - - - -[[howto-sanitize-sensible-values]] -=== Sanitize sensible values -Information returned by the `env` and `configprops` endpoints can be somewhat sensitive so keys matching a certain pattern are sanitized by default (i.e. their values are replaced by `+******+`). - -Spring Boot uses sensible defaults for such keys: for instance, any key ending with the word "password", "secret", "key" or "token" is sanitized. -It is also possible to use a regular expression instead, such as `+*credentials.*+` to sanitize any key that holds the word `credentials` as part of the key. - -The patterns to use can be customized using the `management.endpoint.env.keys-to-sanitize` and `management.endpoint.configprops.keys-to-sanitize` respectively. - - - -[[howto-security]] -== Security -This section addresses questions about security when working with Spring Boot, including questions that arise from using Spring Security with Spring Boot. - -For more about Spring Security, see the {spring-security}[Spring Security project page]. - - - -[[howto-switch-off-spring-boot-security-configuration]] -=== Switch off the Spring Boot Security Configuration -If you define a `@Configuration` with a `WebSecurityConfigurerAdapter` in your application, it switches off the default webapp security settings in Spring Boot. - - -[[howto-change-the-user-details-service-and-add-user-accounts]] -=== Change the UserDetailsService and Add User Accounts -If you provide a `@Bean` of type `AuthenticationManager`, `AuthenticationProvider`, or `UserDetailsService`, the default `@Bean` for `InMemoryUserDetailsManager` is not created. -This means you have the full feature set of Spring Security available (such as {spring-security-docs}#jc-authentication[various authentication options]). - -The easiest way to add user accounts is to provide your own `UserDetailsService` bean. - - - -[[howto-enable-https]] -=== Enable HTTPS When Running behind a Proxy Server -Ensuring that all your main endpoints are only available over HTTPS is an important chore for any application. -If you use Tomcat as a servlet container, then Spring Boot adds Tomcat's own `RemoteIpValve` automatically if it detects some environment settings, and you should be able to rely on the `HttpServletRequest` to report whether it is secure or not (even downstream of a proxy server that handles the real SSL termination). -The standard behavior is determined by the presence or absence of certain request headers (`x-forwarded-for` and `x-forwarded-proto`), whose names are conventional, so it should work with most front-end proxies. -You can switch on the valve by adding some entries to `application.properties`, as shown in the following example: - -[source,properties,indent=0,configprops] ----- - server.tomcat.remote-ip-header=x-forwarded-for - server.tomcat.protocol-header=x-forwarded-proto ----- - -(The presence of either of those properties switches on the valve. -Alternatively, you can add the `RemoteIpValve` by adding a `TomcatServletWebServerFactory` bean.) - -To configure Spring Security to require a secure channel for all (or some) requests, consider adding your own `WebSecurityConfigurerAdapter` that adds the following `HttpSecurity` configuration: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- - @Configuration(proxyBeanMethods = false) - public class SslWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { - - @Override - protected void configure(HttpSecurity http) throws Exception { - // Customize the application security - http.requiresChannel().anyRequest().requiresSecure(); - } - - } ----- - - - -[[howto-hotswapping]] -== Hot Swapping -Spring Boot supports hot swapping. -This section answers questions about how it works. - - - -[[howto-reload-static-content]] -=== Reload Static Content -There are several options for hot reloading. -The recommended approach is to use <>, as it provides additional development-time features, such as support for fast application restarts and LiveReload as well as sensible development-time configuration (such as template caching). -Devtools works by monitoring the classpath for changes. -This means that static resource changes must be "built" for the change to take effect. -By default, this happens automatically in Eclipse when you save your changes. -In IntelliJ IDEA, the Make Project command triggers the necessary build. -Due to the <>, changes to static resources do not trigger a restart of your application. -They do, however, trigger a live reload. - -Alternatively, running in an IDE (especially with debugging on) is a good way to do development (all modern IDEs allow reloading of static resources and usually also allow hot-swapping of Java class changes). - -Finally, the <> can be configured (see the `addResources` property) to support running from the command line with reloading of static files directly from source. -You can use that with an external css/js compiler process if you are writing that code with higher-level tools. - - - -[[howto-reload-thymeleaf-template-content]] -=== Reload Templates without Restarting the Container -Most of the templating technologies supported by Spring Boot include a configuration option to disable caching (described later in this document). -If you use the `spring-boot-devtools` module, these properties are <> for you at development time. - - - -[[howto-reload-thymeleaf-content]] -==== Thymeleaf Templates -If you use Thymeleaf, set `spring.thymeleaf.cache` to `false`. -See {spring-boot-autoconfigure-module-code}/thymeleaf/ThymeleafAutoConfiguration.java[`ThymeleafAutoConfiguration`] for other Thymeleaf customization options. - - - -[[howto-reload-freemarker-content]] -==== FreeMarker Templates -If you use FreeMarker, set `spring.freemarker.cache` to `false`. -See {spring-boot-autoconfigure-module-code}/freemarker/FreeMarkerAutoConfiguration.java[`FreeMarkerAutoConfiguration`] for other FreeMarker customization options. - - - -[[howto-reload-groovy-template-content]] -==== Groovy Templates -If you use Groovy templates, set `spring.groovy.template.cache` to `false`. -See {spring-boot-autoconfigure-module-code}/groovy/template/GroovyTemplateAutoConfiguration.java[`GroovyTemplateAutoConfiguration`] for other Groovy customization options. - - - -[[howto-reload-fast-restart]] -=== Fast Application Restarts -The `spring-boot-devtools` module includes support for automatic application restarts. -While not as fast as technologies such as https://jrebel.com/software/jrebel/[JRebel] it is usually significantly faster than a "`cold start`". -You should probably give it a try before investigating some of the more complex reload options discussed later in this document. - -For more details, see the <> section. - - - -[[howto-reload-java-classes-without-restarting]] -=== Reload Java Classes without Restarting the Container -Many modern IDEs (Eclipse, IDEA, and others) support hot swapping of bytecode. -Consequently, if you make a change that does not affect class or method signatures, it should reload cleanly with no side effects. - - - -[[howto-build]] -== Build -Spring Boot includes build plugins for Maven and Gradle. -This section answers common questions about these plugins. - - - -[[howto-build-info]] -=== Generate Build Information -Both the Maven plugin and the Gradle plugin allow generating build information containing the coordinates, name, and version of the project. -The plugins can also be configured to add additional properties through configuration. -When such a file is present, Spring Boot auto-configures a `BuildProperties` bean. - -To generate build information with Maven, add an execution for the `build-info` goal, as shown in the following example: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - - - org.springframework.boot - spring-boot-maven-plugin - {spring-boot-version} - - - - build-info - - - - - - ----- - -TIP: See the {spring-boot-maven-plugin-docs}[Spring Boot Maven Plugin documentation] for more details. - -The following example does the same with Gradle: - -[source,groovy,indent=0,subs="verbatim,attributes"] ----- - springBoot { - buildInfo() - } ----- - -TIP: See the {spring-boot-gradle-plugin-docs}/#integrating-with-actuator-build-info[Spring Boot Gradle Plugin documentation] for more details. - - - -[[howto-git-info]] -=== Generate Git Information -Both Maven and Gradle allow generating a `git.properties` file containing information about the state of your `git` source code repository when the project was built. - -For Maven users, the `spring-boot-starter-parent` POM includes a pre-configured plugin to generate a `git.properties` file. -To use it, add the following declaration to your POM: - -[source,xml,indent=0] ----- - - - - pl.project13.maven - git-commit-id-plugin - - - ----- - -Gradle users can achieve the same result by using the https://plugins.gradle.org/plugin/com.gorylenko.gradle-git-properties[`gradle-git-properties`] plugin, as shown in the following example: - -[source,groovy,indent=0] ----- - plugins { - id "com.gorylenko.gradle-git-properties" version "1.5.1" - } ----- - -TIP: The commit time in `git.properties` is expected to match the following format: `yyyy-MM-dd'T'HH:mm:ssZ`. -This is the default format for both plugins listed above. -Using this format lets the time be parsed into a `Date` and its format, when serialized to JSON, to be controlled by Jackson's date serialization configuration settings. - - - -[[howto-customize-dependency-versions]] -=== Customize Dependency Versions -If you use a Maven build that inherits directly or indirectly from `spring-boot-dependencies` (for instance, `spring-boot-starter-parent`) but you want to override a specific third-party dependency, you can add appropriate `` elements. -Browse the {spring-boot-code}/spring-boot-project/spring-boot-dependencies/pom.xml[`spring-boot-dependencies`] POM for a complete list of properties. -For example, to pick a different `slf4j` version, you would add the following property: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - 1.7.5 - ----- - -NOTE: Doing so only works if your Maven project inherits (directly or indirectly) from `spring-boot-dependencies`. -If you have added `spring-boot-dependencies` in your own `dependencyManagement` section with `import`, you have to redefine the artifact yourself instead of overriding the property. - -WARNING: Each Spring Boot release is designed and tested against this specific set of third-party dependencies. -Overriding versions may cause compatibility issues. - -To override dependency versions in Gradle, see {spring-boot-gradle-plugin-docs}/#managing-dependencies-customizing[this section] of the Gradle plugin's documentation. - - - -[[howto-create-an-executable-jar-with-maven]] -=== Create an Executable JAR with Maven -The `spring-boot-maven-plugin` can be used to create an executable "`fat`" JAR. -If you use the `spring-boot-starter-parent` POM, you can declare the plugin and your jars are repackaged as follows: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - - - org.springframework.boot - spring-boot-maven-plugin - - - ----- - -If you do not use the parent POM, you can still use the plugin. -However, you must additionally add an `` section, as follows: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - - - org.springframework.boot - spring-boot-maven-plugin - {spring-boot-version} - - - - repackage - - - - - - ----- - -See the {spring-boot-maven-plugin-docs}/usage.html[plugin documentation] for full usage details. - - - -[[howto-create-an-additional-executable-jar]] -=== Use a Spring Boot Application as a Dependency -Like a war file, a Spring Boot application is not intended to be used as a dependency. -If your application contains classes that you want to share with other projects, the recommended approach is to move that code into a separate module. -The separate module can then be depended upon by your application and other projects. - -If you cannot rearrange your code as recommended above, Spring Boot's Maven and Gradle plugins must be configured to produce a separate artifact that is suitable for use as a dependency. -The executable archive cannot be used as a dependency as the <> packages application classes in `BOOT-INF/classes`. -This means that they cannot be found when the executable jar is used as a dependency. - -To produce the two artifacts, one that can be used as a dependency and one that is executable, a classifier must be specified. -This classifier is applied to the name of the executable archive, leaving the default archive for use as a dependency. - -To configure a classifier of `exec` in Maven, you can use the following configuration: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - - - org.springframework.boot - spring-boot-maven-plugin - - exec - - - - ----- - - - -[[howto-extract-specific-libraries-when-an-executable-jar-runs]] -=== Extract Specific Libraries When an Executable Jar Runs -Most nested libraries in an executable jar do not need to be unpacked in order to run. -However, certain libraries can have problems. -For example, JRuby includes its own nested jar support, which assumes that the `jruby-complete.jar` is always directly available as a file in its own right. - -To deal with any problematic libraries, you can flag that specific nested jars should be automatically unpacked when the executable jar first runs. -Such nested jars are written beneath the temporary directory identified by the `java.io.tmpdir` system property. - -WARNING: Care should be taken to ensure that your operating system is configured so that it will not delete the jars that have been unpacked to the temporary directory while the application is still running. - -For example, to indicate that JRuby should be flagged for unpacking by using the Maven Plugin, you would add the following configuration: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - - - org.springframework.boot - spring-boot-maven-plugin - - - - org.jruby - jruby-complete - - - - - - ----- - - - -[[howto-create-a-nonexecutable-jar]] -=== Create a Non-executable JAR with Exclusions -Often, if you have an executable and a non-executable jar as two separate build products, the executable version has additional configuration files that are not needed in a library jar. -For example, the `application.yml` configuration file might by excluded from the non-executable JAR. - -In Maven, the executable jar must be the main artifact and you can add a classified jar for the library, as follows: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - - - org.springframework.boot - spring-boot-maven-plugin - - - maven-jar-plugin - - - lib - package - - jar - - - lib - - application.yml - - - - - - - ----- - - - -[[howto-remote-debug-maven-run]] -=== Remote Debug a Spring Boot Application Started with Maven -To attach a remote debugger to a Spring Boot application that was started with Maven, you can use the `jvmArguments` property of the {spring-boot-maven-plugin-docs}[maven plugin]. - -See {spring-boot-maven-plugin-docs}/examples/run-debug.html[this example] for more details. - - - -[[howto-build-an-executable-archive-with-ant]] -=== Build an Executable Archive from Ant without Using `spring-boot-antlib` -To build with Ant, you need to grab dependencies, compile, and then create a jar or war archive. -To make it executable, you can either use the `spring-boot-antlib` module or you can follow these instructions: - -. If you are building a jar, package the application's classes and resources in a nested `BOOT-INF/classes` directory. - If you are building a war, package the application's classes in a nested `WEB-INF/classes` directory as usual. -. Add the runtime dependencies in a nested `BOOT-INF/lib` directory for a jar or `WEB-INF/lib` for a war. - Remember *not* to compress the entries in the archive. -. Add the `provided` (embedded container) dependencies in a nested `BOOT-INF/lib` directory for a jar or `WEB-INF/lib-provided` for a war. - Remember *not* to compress the entries in the archive. -. Add the `spring-boot-loader` classes at the root of the archive (so that the `Main-Class` is available). -. Use the appropriate launcher (such as `JarLauncher` for a jar file) as a `Main-Class` attribute in the manifest and specify the other properties it needs as manifest entries -- principally, by setting a `Start-Class` property. - -The following example shows how to build an executable archive with Ant: - -[source,xml,indent=0] ----- - - - - - - - - - - - - - - - - - - - - - ----- - - - -[[howto-traditional-deployment]] -== Traditional Deployment -Spring Boot supports traditional deployment as well as more modern forms of deployment. -This section answers common questions about traditional deployment. - - - -[[howto-create-a-deployable-war-file]] -=== Create a Deployable War File - -WARNING: Because Spring WebFlux does not strictly depend on the Servlet API and applications are deployed by default on an embedded Reactor Netty server, War deployment is not supported for WebFlux applications. - -The first step in producing a deployable war file is to provide a `SpringBootServletInitializer` subclass and override its `configure` method. -Doing so makes use of Spring Framework's Servlet 3.0 support and lets you configure your application when it is launched by the servlet container. -Typically, you should update your application's main class to extend `SpringBootServletInitializer`, as shown in the following example: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- - @SpringBootApplication - public class Application extends SpringBootServletInitializer { - - @Override - protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { - return application.sources(Application.class); - } - - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } - - } ----- - -The next step is to update your build configuration such that your project produces a war file rather than a jar file. -If you use Maven and `spring-boot-starter-parent` (which configures Maven's war plugin for you), all you need to do is to modify `pom.xml` to change the packaging to war, as follows: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - war ----- - -If you use Gradle, you need to modify `build.gradle` to apply the war plugin to the project, as follows: - -[source,groovy,indent=0,subs="verbatim,quotes,attributes"] ----- - apply plugin: 'war' ----- - -The final step in the process is to ensure that the embedded servlet container does not interfere with the servlet container to which the war file is deployed. -To do so, you need to mark the embedded servlet container dependency as being provided. - -If you use Maven, the following example marks the servlet container (Tomcat, in this case) as being provided: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - - - org.springframework.boot - spring-boot-starter-tomcat - provided - - - ----- - -If you use Gradle, the following example marks the servlet container (Tomcat, in this case) as being provided: - -[source,groovy,indent=0,subs="verbatim,quotes,attributes"] ----- - dependencies { - // … - providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat' - // … - } ----- - -TIP: `providedRuntime` is preferred to Gradle's `compileOnly` configuration. -Among other limitations, `compileOnly` dependencies are not on the test classpath, so any web-based integration tests fail. - -If you use the <>, marking the embedded servlet container dependency as provided produces an executable war file with the provided dependencies packaged in a `lib-provided` directory. -This means that, in addition to being deployable to a servlet container, you can also run your application by using `java -jar` on the command line. - - - -[[howto-convert-an-existing-application-to-spring-boot]] -=== Convert an Existing Application to Spring Boot -For a non-web application, it should be easy to convert an existing Spring application to a Spring Boot application. -To do so, throw away the code that creates your `ApplicationContext` and replace it with calls to `SpringApplication` or `SpringApplicationBuilder`. -Spring MVC web applications are generally amenable to first creating a deployable war application and then migrating it later to an executable war or jar. -See the https://spring.io/guides/gs/convert-jar-to-war/[Getting Started Guide on Converting a jar to a war]. - -To create a deployable war by extending `SpringBootServletInitializer` (for example, in a class called `Application`) and adding the Spring Boot `@SpringBootApplication` annotation, use code similar to that shown in the following example: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- - @SpringBootApplication - public class Application extends SpringBootServletInitializer { - - @Override - protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { - // Customize the application or call application.sources(...) to add sources - // Since our example is itself a @Configuration class (via @SpringBootApplication) - // we actually don't need to override this method. - return application; - } - - } ----- - -Remember that, whatever you put in the `sources` is merely a Spring `ApplicationContext`. -Normally, anything that already works should work here. -There might be some beans you can remove later and let Spring Boot provide its own defaults for them, but it should be possible to get something working before you need to do that. - -Static resources can be moved to `/public` (or `/static` or `/resources` or `/META-INF/resources`) in the classpath root. -The same applies to `messages.properties` (which Spring Boot automatically detects in the root of the classpath). - -Vanilla usage of Spring `DispatcherServlet` and Spring Security should require no further changes. -If you have other features in your application (for instance, using other servlets or filters), you may need to add some configuration to your `Application` context, by replacing those elements from the `web.xml`, as follows: - -* A `@Bean` of type `Servlet` or `ServletRegistrationBean` installs that bean in the container as if it were a `` and `` in `web.xml`. -* A `@Bean` of type `Filter` or `FilterRegistrationBean` behaves similarly (as a `` and ``). -* An `ApplicationContext` in an XML file can be added through an `@ImportResource` in your `Application`. - Alternatively, simple cases where annotation configuration is heavily used already can be recreated in a few lines as `@Bean` definitions. - -Once the war file is working, you can make it executable by adding a `main` method to your `Application`, as shown in the following example: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } ----- - -[NOTE] -==== -If you intend to start your application as a war or as an executable application, you need to share the customizations of the builder in a method that is both available to the `SpringBootServletInitializer` callback and in the `main` method in a class similar to the following: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- - @SpringBootApplication - public class Application extends SpringBootServletInitializer { - - @Override - protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { - return configureApplication(builder); - } - - public static void main(String[] args) { - configureApplication(new SpringApplicationBuilder()).run(args); - } - - private static SpringApplicationBuilder configureApplication(SpringApplicationBuilder builder) { - return builder.sources(Application.class).bannerMode(Banner.Mode.OFF); - } - - } ----- -==== - -Applications can fall into more than one category: - -* Servlet 3.0+ applications with no `web.xml`. -* Applications with a `web.xml`. -* Applications with a context hierarchy. -* Applications without a context hierarchy. - -All of these should be amenable to translation, but each might require slightly different techniques. - -Servlet 3.0+ applications might translate pretty easily if they already use the Spring Servlet 3.0+ initializer support classes. -Normally, all the code from an existing `WebApplicationInitializer` can be moved into a `SpringBootServletInitializer`. -If your existing application has more than one `ApplicationContext` (for example, if it uses `AbstractDispatcherServletInitializer`) then you might be able to combine all your context sources into a single `SpringApplication`. -The main complication you might encounter is if combining does not work and you need to maintain the context hierarchy. -See the <> for examples. -An existing parent context that contains web-specific features usually needs to be broken up so that all the `ServletContextAware` components are in the child context. - -Applications that are not already Spring applications might be convertible to Spring Boot applications, and the previously mentioned guidance may help. -However, you may yet encounter problems. -In that case, we suggest https://stackoverflow.com/questions/tagged/spring-boot[asking questions on Stack Overflow with a tag of `spring-boot`]. - - - -[[howto-weblogic]] -=== Deploying a WAR to WebLogic -To deploy a Spring Boot application to WebLogic, you must ensure that your servlet initializer *directly* implements `WebApplicationInitializer` (even if you extend from a base class that already implements it). - -A typical initializer for WebLogic should resemble the following example: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- - import org.springframework.boot.autoconfigure.SpringBootApplication; - import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; - import org.springframework.web.WebApplicationInitializer; - - @SpringBootApplication - public class MyApplication extends SpringBootServletInitializer implements WebApplicationInitializer { - - } ----- - -If you use Logback, you also need to tell WebLogic to prefer the packaged version rather than the version that was pre-installed with the server. -You can do so by adding a `WEB-INF/weblogic.xml` file with the following contents: - -[source,xml,indent=0] ----- - - - - - org.slf4j - - - ----- - - - -[[howto-use-jedis-instead-of-lettuce]] -=== Use Jedis Instead of Lettuce -By default, the Spring Boot starter (`spring-boot-starter-data-redis`) uses https://github.com/lettuce-io/lettuce-core/[Lettuce]. -You need to exclude that dependency and include the https://github.com/xetorthio/jedis/[Jedis] one instead. -Spring Boot manages these dependencies to help make this process as easy as possible. - -The following example shows how to do so in Maven: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - org.springframework.boot - spring-boot-starter-data-redis - - - io.lettuce - lettuce-core - - - - - redis.clients - jedis - ----- - -The following example shows how to do so in Gradle: - -[source,groovy,indent=0,subs="verbatim,quotes,attributes"] ----- - configurations { - compile.exclude module: "lettuce" - } - - dependencies { - compile("redis.clients:jedis") - // ... - } ----- diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/index.htmladoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/index.htmladoc deleted file mode 100644 index 758425733b18..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/index.htmladoc +++ /dev/null @@ -1,28 +0,0 @@ -[[spring-boot-reference-documentation]] -= Spring Boot Reference Documentation -Phillip Webb, Dave Syer, Josh Long, Stéphane Nicoll, Rob Winch, Andy Wilkinson, Marcel Overdijk, Christian Dupuis, Sébastien Deleuze, Michael Simons, Vedran Pavić, Jay Bryant, Madhura Bhave, Eddú Meléndez -:docinfo: shared - -The reference documentation consists of the following sections: - -[horizontal] -<> :: Legal information. -<> :: About the Documentation, Getting Help, First Steps, and more. -<> :: Introducing Spring Boot, System Requirements, Servlet Containers, Installing Spring Boot, Developing Your First Spring Boot Application -<> :: Build Systems, Structuring Your Code, Configuration, Spring Beans and Dependency Injection, and more. -<> :: Profiles, Logging, Security, Caching, Spring Integration, Testing, and more. -<> :: Monitoring, Metrics, Auditing, and more. -<> :: Deploying to the Cloud, Installing as a Unix application. -<> :: Installing the CLI, Using the CLI, Configuring the CLI, and more. -<> :: Maven Plugin, Gradle Plugin, Antlib, and more. -<> :: Application Development, Configuration, Embedded Servers, Data Access, and many more. - -The reference documentation has the following appendices: - -[horizontal] -<> :: Common application properties that can be used to configure your application. -<> :: Metadata used to describe configuration properties. -<> :: Auto-configuration classes provided by Spring Boot. -<> :: Test-autoconfiguration annotations used to test slices of your application. -<> :: Spring Boot's executable jars, their launchers, and their format. -<> :: Details of the dependencies that are managed by Spring Boot. diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/index.htmlsingleadoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/index.htmlsingleadoc deleted file mode 100644 index 1e5720829ff0..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/index.htmlsingleadoc +++ /dev/null @@ -1,28 +0,0 @@ -[[spring-boot-reference-documentation]] -= Spring Boot Reference Documentation -Phillip Webb, Dave Syer, Josh Long, Stéphane Nicoll, Rob Winch, Andy Wilkinson, Marcel Overdijk, Christian Dupuis, Sébastien Deleuze, Michael Simons, Vedran Pavić, Jay Bryant, Madhura Bhave -:docinfo: shared -include::attributes.adoc[] - -include::legal.adoc[leveloffset=+1] -include::documentation-overview.adoc[leveloffset=+1] -include::getting-started.adoc[leveloffset=+1] -include::using-spring-boot.adoc[leveloffset=+1] -include::spring-boot-features.adoc[leveloffset=+1] -include::production-ready-features.adoc[leveloffset=+1] -include::deployment.adoc[leveloffset=+1] -include::spring-boot-cli.adoc[leveloffset=+1] -include::build-tool-plugins.adoc[leveloffset=+1] -include::howto.adoc[leveloffset=+1] - - - -[[appendix]] -== Appendices - -include::appendix-application-properties.adoc[leveloffset=+2] -include::appendix-configuration-metadata.adoc[leveloffset=+2] -include::appendix-auto-configuration-classes.adoc[leveloffset=+2] -include::appendix-test-auto-configuration.adoc[leveloffset=+2] -include::appendix-executable-jar-format.adoc[leveloffset=+2] -include::appendix-dependency-versions.adoc[leveloffset=+2] diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc deleted file mode 100644 index be127632d869..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc +++ /dev/null @@ -1,2128 +0,0 @@ -[[production-ready]] -= Spring Boot Actuator: Production-ready Features -include::attributes.adoc[] - -Spring Boot includes a number of additional features to help you monitor and manage your application when you push it to production. -You can choose to manage and monitor your application by using HTTP endpoints or with JMX. -Auditing, health, and metrics gathering can also be automatically applied to your application. - - - -[[production-ready-enabling]] -== Enabling Production-ready Features -The {spring-boot-code}/spring-boot-project/spring-boot-actuator[`spring-boot-actuator`] module provides all of Spring Boot's production-ready features. -The simplest way to enable the features is to add a dependency to the `spring-boot-starter-actuator` '`Starter`'. - -.Definition of Actuator -**** -An actuator is a manufacturing term that refers to a mechanical device for moving or controlling something. -Actuators can generate a large amount of motion from a small change. -**** - -To add the actuator to a Maven based project, add the following '`Starter`' dependency: - -[source,xml,indent=0] ----- - - - org.springframework.boot - spring-boot-starter-actuator - - ----- - -For Gradle, use the following declaration: - -[source,groovy,indent=0] ----- - dependencies { - compile("org.springframework.boot:spring-boot-starter-actuator") - } ----- - - - -[[production-ready-endpoints]] -== Endpoints -Actuator endpoints let you monitor and interact with your application. -Spring Boot includes a number of built-in endpoints and lets you add your own. -For example, the `health` endpoint provides basic application health information. - -Each individual endpoint can be <>. -This controls whether or not the endpoint is created and its bean exists in the application context. -To be remotely accessible an endpoint also has to be <>. -Most applications choose HTTP, where the ID of the endpoint along with a prefix of `/actuator` is mapped to a URL. -For example, by default, the `health` endpoint is mapped to `/actuator/health`. - -The following technology-agnostic endpoints are available: - -[cols="2,5"] -|=== -| ID | Description - -| `auditevents` -| Exposes audit events information for the current application. - Requires an `AuditEventRepository` bean. - -| `beans` -| Displays a complete list of all the Spring beans in your application. - -| `caches` -| Exposes available caches. - -| `conditions` -| Shows the conditions that were evaluated on configuration and auto-configuration classes and the reasons why they did or did not match. - -| `configprops` -| Displays a collated list of all `@ConfigurationProperties`. - -| `env` -| Exposes properties from Spring's `ConfigurableEnvironment`. - -| `flyway` -| Shows any Flyway database migrations that have been applied. - Requires one or more `Flyway` beans. - -| `health` -| Shows application health information. - -| `httptrace` -| Displays HTTP trace information (by default, the last 100 HTTP request-response exchanges). - Requires an `HttpTraceRepository` bean. - -| `info` -| Displays arbitrary application info. - -| `integrationgraph` -| Shows the Spring Integration graph. - Requires a dependency on `spring-integration-core`. - -| `loggers` -| Shows and modifies the configuration of loggers in the application. - -| `liquibase` -| Shows any Liquibase database migrations that have been applied. - Requires one or more `Liquibase` beans. - -| `metrics` -| Shows '`metrics`' information for the current application. - -| `mappings` -| Displays a collated list of all `@RequestMapping` paths. - -| `scheduledtasks` -| Displays the scheduled tasks in your application. - -| `sessions` -| Allows retrieval and deletion of user sessions from a Spring Session-backed session store. - Requires a Servlet-based web application using Spring Session. - -| `shutdown` -| Lets the application be gracefully shutdown. - Disabled by default. - -| `threaddump` -| Performs a thread dump. -|=== - -If your application is a web application (Spring MVC, Spring WebFlux, or Jersey), you can use the following additional endpoints: - -[cols="2,5"] -|=== -| ID | Description - -| `heapdump` -| Returns an `hprof` heap dump file. - -| `jolokia` -| Exposes JMX beans over HTTP (when Jolokia is on the classpath, not available for WebFlux). - Requires a dependency on `jolokia-core`. - -| `logfile` -| Returns the contents of the logfile (if `logging.file.name` or `logging.file.path` properties have been set). - Supports the use of the HTTP `Range` header to retrieve part of the log file's content. - -| `prometheus` -| Exposes metrics in a format that can be scraped by a Prometheus server. - Requires a dependency on `micrometer-registry-prometheus`. -|=== - -To learn more about the Actuator's endpoints and their request and response formats, please refer to the separate API documentation ({spring-boot-actuator-restapi}/html/[HTML] or {spring-boot-actuator-restapi}/pdf/spring-boot-actuator-web-api.pdf[PDF]). - - - -[[production-ready-endpoints-enabling-endpoints]] -=== Enabling Endpoints -By default, all endpoints except for `shutdown` are enabled. -To configure the enablement of an endpoint, use its `management.endpoint..enabled` property. -The following example enables the `shutdown` endpoint: - -[source,properties,indent=0,configprops] ----- - management.endpoint.shutdown.enabled=true ----- - -If you prefer endpoint enablement to be opt-in rather than opt-out, set the configprop:management.endpoints.enabled-by-default[] property to `false` and use individual endpoint `enabled` properties to opt back in. -The following example enables the `info` endpoint and disables all other endpoints: - -[source,properties,indent=0,configprops] ----- - management.endpoints.enabled-by-default=false - management.endpoint.info.enabled=true ----- - -NOTE: Disabled endpoints are removed entirely from the application context. -If you want to change only the technologies over which an endpoint is exposed, use the <> instead. - - - -[[production-ready-endpoints-exposing-endpoints]] -=== Exposing Endpoints -Since Endpoints may contain sensitive information, careful consideration should be given about when to expose them. -The following table shows the default exposure for the built-in endpoints: - -[cols="1,1,1"] -|=== -| ID | JMX | Web - -| `auditevents` -| Yes -| No - -| `beans` -| Yes -| No - -| `caches` -| Yes -| No - -| `conditions` -| Yes -| No - -| `configprops` -| Yes -| No - -| `env` -| Yes -| No - -| `flyway` -| Yes -| No - -| `health` -| Yes -| Yes - -| `heapdump` -| N/A -| No - -| `httptrace` -| Yes -| No - -| `info` -| Yes -| Yes - -| `integrationgraph` -| Yes -| No - -| `jolokia` -| N/A -| No - -| `logfile` -| N/A -| No - -| `loggers` -| Yes -| No - -| `liquibase` -| Yes -| No - -| `metrics` -| Yes -| No - -| `mappings` -| Yes -| No - -| `prometheus` -| N/A -| No - -| `scheduledtasks` -| Yes -| No - -| `sessions` -| Yes -| No - -| `shutdown` -| Yes -| No - -| `threaddump` -| Yes -| No -|=== - -To change which endpoints are exposed, use the following technology-specific `include` and `exclude` properties: - -[cols="3,1"] -|=== -| Property | Default - -| configprop:management.endpoints.jmx.exposure.exclude[] -| - -| configprop:management.endpoints.jmx.exposure.include[] -| `*` - -| configprop:management.endpoints.web.exposure.exclude[] -| - -| configprop:management.endpoints.web.exposure.include[] -| `info, health` -|=== - -The `include` property lists the IDs of the endpoints that are exposed. -The `exclude` property lists the IDs of the endpoints that should not be exposed. -The `exclude` property takes precedence over the `include` property. -Both `include` and `exclude` properties can be configured with a list of endpoint IDs. - -For example, to stop exposing all endpoints over JMX and only expose the `health` and `info` endpoints, use the following property: - -[source,properties,indent=0,configprops] ----- - management.endpoints.jmx.exposure.include=health,info ----- - -`*` can be used to select all endpoints. -For example, to expose everything over HTTP except the `env` and `beans` endpoints, use the following properties: - -[source,properties,indent=0,configprops] ----- - management.endpoints.web.exposure.include=* - management.endpoints.web.exposure.exclude=env,beans ----- - -[NOTE] -==== -`*` has a special meaning in YAML, so be sure to add quotes if you want to include (or exclude) all endpoints, as shown in the following example: - -[source,yaml,indent=0] ----- - management: - endpoints: - web: - exposure: - include: "*" ----- -==== - -NOTE: If your application is exposed publicly, we strongly recommend that you also <>. - -TIP: If you want to implement your own strategy for when endpoints are exposed, you can register an `EndpointFilter` bean. - - - -[[production-ready-endpoints-security]] -=== Securing HTTP Endpoints -You should take care to secure HTTP endpoints in the same way that you would any other sensitive URL. -If Spring Security is present, endpoints are secured by default using Spring Security’s content-negotiation strategy. -If you wish to configure custom security for HTTP endpoints, for example, only allow users with a certain role to access them, Spring Boot provides some convenient `RequestMatcher` objects that can be used in combination with Spring Security. - -A typical Spring Security configuration might look something like the following example: - -[source,java,indent=0] ----- - @Configuration(proxyBeanMethods = false) - public class ActuatorSecurity extends WebSecurityConfigurerAdapter { - - @Override - protected void configure(HttpSecurity http) throws Exception { - http.requestMatcher(EndpointRequest.toAnyEndpoint()).authorizeRequests((requests) -> - requests.anyRequest().hasRole("ENDPOINT_ADMIN")); - http.httpBasic(); - } - - } ----- - -The preceding example uses `EndpointRequest.toAnyEndpoint()` to match a request to any endpoint and then ensures that all have the `ENDPOINT_ADMIN` role. -Several other matcher methods are also available on `EndpointRequest`. -See the API documentation ({spring-boot-actuator-restapi}/html[HTML] or {spring-boot-actuator-restapi}/pdf/spring-boot-actuator-web-api.pdf[PDF]) for details. - -If you deploy applications behind a firewall, you may prefer that all your actuator endpoints can be accessed without requiring authentication. -You can do so by changing the configprop:management.endpoints.web.exposure.include[] property, as follows: - -.application.properties -[source,properties,indent=0,configprops] ----- - management.endpoints.web.exposure.include=* ----- - -Additionally, if Spring Security is present, you would need to add custom security configuration that allows unauthenticated access to the endpoints as shown in the following example: - -[source,java,indent=0] ----- - @Configuration(proxyBeanMethods = false) - public class ActuatorSecurity extends WebSecurityConfigurerAdapter { - - @Override - protected void configure(HttpSecurity http) throws Exception { - http.requestMatcher(EndpointRequest.toAnyEndpoint()).authorizeRequests((requests) -> - requests.anyRequest().permitAll()); - } - - } ----- - - - -[[production-ready-endpoints-caching]] -=== Configuring Endpoints -Endpoints automatically cache responses to read operations that do not take any parameters. -To configure the amount of time for which an endpoint will cache a response, use its `cache.time-to-live` property. -The following example sets the time-to-live of the `beans` endpoint's cache to 10 seconds: - -.application.properties -[source,properties,indent=0,configprops] ----- - management.endpoint.beans.cache.time-to-live=10s ----- - -NOTE: The prefix `management.endpoint.` is used to uniquely identify the endpoint that is being configured. - -NOTE: When making an authenticated HTTP request, the `Principal` is considered as input to the endpoint and, therefore, the response will not be cached. - - - -[[production-ready-endpoints-hypermedia]] -=== Hypermedia for Actuator Web Endpoints -A "`discovery page`" is added with links to all the endpoints. -The "`discovery page`" is available on `/actuator` by default. - -When a custom management context path is configured, the "`discovery page`" automatically moves from `/actuator` to the root of the management context. -For example, if the management context path is `/management`, then the discovery page is available from `/management`. -When the management context path is set to `/`, the discovery page is disabled to prevent the possibility of a clash with other mappings. - - - -[[production-ready-endpoints-cors]] -=== CORS Support -https://en.wikipedia.org/wiki/Cross-origin_resource_sharing[Cross-origin resource sharing] (CORS) is a https://www.w3.org/TR/cors/[W3C specification] that lets you specify in a flexible way what kind of cross-domain requests are authorized. -If you use Spring MVC or Spring WebFlux, Actuator's web endpoints can be configured to support such scenarios. - -CORS support is disabled by default and is only enabled once the configprop:management.endpoints.web.cors.allowed-origins[] property has been set. -The following configuration permits `GET` and `POST` calls from the `example.com` domain: - -[source,properties,indent=0,configprops] ----- - management.endpoints.web.cors.allowed-origins=https://example.com - management.endpoints.web.cors.allowed-methods=GET,POST ----- - -TIP: See {spring-boot-actuator-autoconfigure-module-code}/endpoint/web/CorsEndpointProperties.java[CorsEndpointProperties] for a complete list of options. - - - -[[production-ready-endpoints-custom]] -=== Implementing Custom Endpoints -If you add a `@Bean` annotated with `@Endpoint`, any methods annotated with `@ReadOperation`, `@WriteOperation`, or `@DeleteOperation` are automatically exposed over JMX and, in a web application, over HTTP as well. -Endpoints can be exposed over HTTP using Jersey, Spring MVC, or Spring WebFlux. -If both Jersey and Spring MVC are available, Spring MVC will be used. - -You can also write technology-specific endpoints by using `@JmxEndpoint` or `@WebEndpoint`. -These endpoints are restricted to their respective technologies. -For example, `@WebEndpoint` is exposed only over HTTP and not over JMX. - -You can write technology-specific extensions by using `@EndpointWebExtension` and `@EndpointJmxExtension`. -These annotations let you provide technology-specific operations to augment an existing endpoint. - -Finally, if you need access to web-framework-specific functionality, you can implement Servlet or Spring `@Controller` and `@RestController` endpoints at the cost of them not being available over JMX or when using a different web framework. - - - -[[production-ready-endpoints-custom-input]] -==== Receiving Input -Operations on an endpoint receive input via their parameters. -When exposed via the web, the values for these parameters are taken from the URL's query parameters and from the JSON request body. -When exposed via JMX, the parameters are mapped to the parameters of the MBean's operations. -Parameters are required by default. -They can be made optional by annotating them with `@org.springframework.lang.Nullable`. - -Each root property in the JSON request body can be mapped to a parameter of the endpoint. -Consider the following JSON request body: - -[source,json,indent=0] ----- - { - "name": "test", - "counter": 42 - } ----- - -This can be used to invoke a write operation that takes `String name` and `int counter` parameters. - -TIP: Because endpoints are technology agnostic, only simple types can be specified in the method signature. -In particular declaring a single parameter with a custom type defining a `name` and `counter` properties is not supported. - -NOTE: To allow the input to be mapped to the operation method's parameters, Java code implementing an endpoint should be compiled with `-parameters`, and Kotlin code implementing an endpoint should be compiled with `-java-parameters`. -This will happen automatically if you are using Spring Boot's Gradle plugin or if you are using Maven and `spring-boot-starter-parent`. - - - -[[production-ready-endpoints-custom-input-conversion]] -===== Input type conversion -The parameters passed to endpoint operation methods are, if necessary, automatically converted to the required type. -Before calling an operation method, the input received via JMX or an HTTP request is converted to the required types using an instance of `ApplicationConversionService` as well as any `Converter` or `GenericConverter` beans qualified with `@EndpointConverter`. - - - -[[production-ready-endpoints-custom-web]] -==== Custom Web Endpoints -Operations on an `@Endpoint`, `@WebEndpoint`, or `@EndpointWebExtension` are automatically exposed over HTTP using Jersey, Spring MVC, or Spring WebFlux. -If both Jersey and Spring MVC are available, Spring MVC will be used. - - - -[[production-ready-endpoints-custom-web-predicate]] -===== Web Endpoint Request Predicates -A request predicate is automatically generated for each operation on a web-exposed endpoint. - - - -[[production-ready-endpoints-custom-web-predicate-path]] -===== Path -The path of the predicate is determined by the ID of the endpoint and the base path of web-exposed endpoints. -The default base path is `/actuator`. -For example, an endpoint with the ID `sessions` will use `/actuator/sessions` as its path in the predicate. - -The path can be further customized by annotating one or more parameters of the operation method with `@Selector`. -Such a parameter is added to the path predicate as a path variable. -The variable's value is passed into the operation method when the endpoint operation is invoked. -If you want to capture all remaining path elements, you can add `@Selector(Match=ALL_REMAINING)` to the last parameter and make it a type that is conversion compatible with a `String[]`. - - - -[[production-ready-endpoints-custom-web-predicate-http-method]] -===== HTTP method -The HTTP method of the predicate is determined by the operation type, as shown in the following table: - -[cols="3, 1"] -|=== -| Operation | HTTP method - -| `@ReadOperation` -| `GET` - -| `@WriteOperation` -| `POST` - -| `@DeleteOperation` -| `DELETE` -|=== - - - -[[production-ready-endpoints-custom-web-predicate-consumes]] -===== Consumes -For a `@WriteOperation` (HTTP `POST`) that uses the request body, the consumes clause of the predicate is `application/vnd.spring-boot.actuator.v2+json, application/json`. -For all other operations the consumes clause is empty. - - - -[[production-ready-endpoints-custom-web-predicate-produces]] -===== Produces -The produces clause of the predicate can be determined by the `produces` attribute of the `@DeleteOperation`, `@ReadOperation`, and `@WriteOperation` annotations. -The attribute is optional. -If it is not used, the produces clause is determined automatically. - -If the operation method returns `void` or `Void` the produces clause is empty. -If the operation method returns a `org.springframework.core.io.Resource`, the produces clause is `application/octet-stream`. -For all other operations the produces clause is `application/vnd.spring-boot.actuator.v2+json, application/json`. - - - -[[production-ready-endpoints-custom-web-response-status]] -===== Web Endpoint Response Status -The default response status for an endpoint operation depends on the operation type (read, write, or delete) and what, if anything, the operation returns. - -A `@ReadOperation` returns a value, the response status will be 200 (OK). -If it does not return a value, the response status will be 404 (Not Found). - -If a `@WriteOperation` or `@DeleteOperation` returns a value, the response status will be 200 (OK). -If it does not return a value the response status will be 204 (No Content). - -If an operation is invoked without a required parameter, or with a parameter that cannot be converted to the required type, the operation method will not be called and the response status will be 400 (Bad Request). - - - -[[production-ready-endpoints-custom-web-range-requests]] -===== Web Endpoint Range Requests -An HTTP range request can be used to request part of an HTTP resource. -When using Spring MVC or Spring Web Flux, operations that return a `org.springframework.core.io.Resource` automatically support range requests. - -NOTE: Range requests are not supported when using Jersey. - - - -[[production-ready-endpoints-custom-web-security]] -===== Web Endpoint Security -An operation on a web endpoint or a web-specific endpoint extension can receive the current `java.security.Principal` or `org.springframework.boot.actuate.endpoint.SecurityContext` as a method parameter. -The former is typically used in conjunction with `@Nullable` to provide different behavior for authenticated and unauthenticated users. -The latter is typically used to perform authorization checks using its `isUserInRole(String)` method. - - - -[[production-ready-endpoints-custom-servlet]] -==== Servlet endpoints -A `Servlet` can be exposed as an endpoint by implementing a class annotated with `@ServletEndpoint` that also implements `Supplier`. -Servlet endpoints provide deeper integration with the Servlet container but at the expense of portability. -They are intended to be used to expose an existing `Servlet` as an endpoint. -For new endpoints, the `@Endpoint` and `@WebEndpoint` annotations should be preferred whenever possible. - - - -[[production-ready-endpoints-custom-controller]] -==== Controller endpoints -`@ControllerEndpoint` and `@RestControllerEndpoint` can be used to implement an endpoint that is only exposed by Spring MVC or Spring WebFlux. -Methods are mapped using the standard annotations for Spring MVC and Spring WebFlux such as `@RequestMapping` and `@GetMapping`, with the endpoint's ID being used as a prefix for the path. -Controller endpoints provide deeper integration with Spring's web frameworks but at the expense of portability. -The `@Endpoint` and `@WebEndpoint` annotations should be preferred whenever possible. - - - -[[production-ready-health]] -=== Health Information -You can use health information to check the status of your running application. -It is often used by monitoring software to alert someone when a production system goes down. -The information exposed by the `health` endpoint depends on the configprop:management.endpoint.health.show-details[] and configprop:management.endpoint.health.show-components[] properties which can be configured with one of the following values: - -[cols="1, 3"] -|=== -| Name | Description - -| `never` -| Details are never shown. - -| `when-authorized` -| Details are only shown to authorized users. - Authorized roles can be configured using `management.endpoint.health.roles`. - -| `always` -| Details are shown to all users. -|=== - -The default value is `never`. -A user is considered to be authorized when they are in one or more of the endpoint's roles. -If the endpoint has no configured roles (the default) all authenticated users are considered to be authorized. -The roles can be configured using the configprop:management.endpoint.health.roles[] property. - -NOTE: If you have secured your application and wish to use `always`, your security configuration must permit access to the health endpoint for both authenticated and unauthenticated users. - -Health information is collected from the content of a {spring-boot-actuator-module-code}/health/HealthContributorRegistry.java[`HealthContributorRegistry`] (by default all {spring-boot-actuator-module-code}/health/HealthContributor.java[`HealthContributor`] instances defined in your `ApplicationContext`). -Spring Boot includes a number of auto-configured `HealthContributors` and you can also write your own. - -A `HealthContributor` can either be a `HealthIndicator` or a `CompositeHealthContributor`. -A `HealthIndicator` provides actual health information, including a `Status`. -A `CompositeHealthContributor` provides a composite of other `HealthContributors`. -Taken together, contributors form a tree structure to represent the overall system health. - -By default, the final system health is derived by a `StatusAggregator` which sorts the statuses from each `HealthIndicator` based on an ordered list of statuses. -The first status in the sorted list is used as the overall health status. -If no `HealthIndicator` returns a status that is known to the `StatusAggregator`, an `UNKNOWN` status is used. - -TIP: The `HealthContributorRegistry` can be used to register and unregister health indicators at runtime. - - - -==== Auto-configured HealthIndicators -The following `HealthIndicators` are auto-configured by Spring Boot when appropriate: - -[cols="4,6"] -|=== -| Name | Description - -| {spring-boot-actuator-module-code}/cassandra/CassandraHealthIndicator.java[`CassandraHealthIndicator`] -| Checks that a Cassandra database is up. - -| {spring-boot-actuator-module-code}/couchbase/CouchbaseHealthIndicator.java[`CouchbaseHealthIndicator`] -| Checks that a Couchbase cluster is up. - -| {spring-boot-actuator-module-code}/system/DiskSpaceHealthIndicator.java[`DiskSpaceHealthIndicator`] -| Checks for low disk space. - -| {spring-boot-actuator-module-code}/elasticsearch/ElasticSearchRestHealthContributorAutoConfiguration.java[`ElasticSearchRestHealthContributorAutoConfiguration`] -| Checks that an Elasticsearch cluster is up. - -| {spring-boot-actuator-module-code}/hazelcast/HazelcastHealthIndicator.java[`HazelcastHealthIndicator`] -| Checks that a Hazelcast server is up. - -| {spring-boot-actuator-module-code}/influx/InfluxDbHealthIndicator.java[`InfluxDbHealthIndicator`] -| Checks that an InfluxDB server is up. - -| {spring-boot-actuator-module-code}/jms/JmsHealthIndicator.java[`JmsHealthIndicator`] -| Checks that a JMS broker is up. - -| {spring-boot-actuator-module-code}/mail/MailHealthIndicator.java[`MailHealthIndicator`] -| Checks that a mail server is up. - -| {spring-boot-actuator-module-code}/mongo/MongoHealthIndicator.java[`MongoHealthIndicator`] -| Checks that a Mongo database is up. - -| {spring-boot-actuator-module-code}/health/PingHealthIndicator.java[`PingHealthIndicator`] -| Always responds with `UP`. - -| {spring-boot-actuator-module-code}/amqp/RabbitHealthIndicator.java[`RabbitHealthIndicator`] -| Checks that a Rabbit server is up. - -| {spring-boot-actuator-module-code}/redis/RedisHealthIndicator.java[`RedisHealthIndicator`] -| Checks that a Redis server is up. - -| {spring-boot-actuator-module-code}/solr/SolrHealthIndicator.java[`SolrHealthIndicator`] -| Checks that a Solr server is up. -|=== - -TIP: You can disable them all by setting the configprop:management.health.defaults.enabled[] property. - - - -==== Writing Custom HealthIndicators -To provide custom health information, you can register Spring beans that implement the {spring-boot-actuator-module-code}/health/HealthIndicator.java[`HealthIndicator`] interface. -You need to provide an implementation of the `health()` method and return a `Health` response. -The `Health` response should include a status and can optionally include additional details to be displayed. -The following code shows a sample `HealthIndicator` implementation: - -[source,java,indent=0] ----- - import org.springframework.boot.actuate.health.Health; - import org.springframework.boot.actuate.health.HealthIndicator; - import org.springframework.stereotype.Component; - - @Component - public class MyHealthIndicator implements HealthIndicator { - - @Override - public Health health() { - int errorCode = check(); // perform some specific health check - if (errorCode != 0) { - return Health.down().withDetail("Error Code", errorCode).build(); - } - return Health.up().build(); - } - - } ----- - -NOTE: The identifier for a given `HealthIndicator` is the name of the bean without the `HealthIndicator` suffix, if it exists. -In the preceding example, the health information is available in an entry named `my`. - -In addition to Spring Boot's predefined {spring-boot-actuator-module-code}/health/Status.java[`Status`] types, it is also possible for `Health` to return a custom `Status` that represents a new system state. -In such cases, a custom implementation of the {spring-boot-actuator-module-code}/health/StatusAggregator.java[`StatusAggregator`] interface also needs to be provided, or the default implementation has to be configured by using the configprop:management.endpoint.health.status.order[] configuration property. - -For example, assume a new `Status` with code `FATAL` is being used in one of your `HealthIndicator` implementations. -To configure the severity order, add the following property to your application properties: - -[source,properties,indent=0,configprops] ----- - management.endpoint.health.status.order=fatal,down,out-of-service,unknown,up ----- - -The HTTP status code in the response reflects the overall health status (for example, `UP` maps to 200, while `OUT_OF_SERVICE` and `DOWN` map to 503). -You might also want to register custom status mappings if you access the health endpoint over HTTP. -For example, the following property maps `FATAL` to 503 (service unavailable): - -[source,properties,indent=0,configprops] ----- - management.endpoint.health.status.http-mapping.fatal=503 ----- - -TIP: If you need more control, you can define your own `HttpCodeStatusMapper` bean. - -The following table shows the default status mappings for the built-in statuses: - -[cols="1,3"] -|=== -| Status | Mapping - -| DOWN -| SERVICE_UNAVAILABLE (503) - -| OUT_OF_SERVICE -| SERVICE_UNAVAILABLE (503) - -| UP -| No mapping by default, so http status is 200 - -| UNKNOWN -| No mapping by default, so http status is 200 -|=== - - - -[[reactive-health-indicators]] -==== Reactive Health Indicators -For reactive applications, such as those using Spring WebFlux, `ReactiveHealthContributor` provides a non-blocking contract for getting application health. -Similar to a traditional `HealthContributor`, health information is collected from the content of a {spring-boot-actuator-module-code}/health/ReactiveHealthContributorRegistry.java[`ReactiveHealthContributorRegistry`] (by default all {spring-boot-actuator-module-code}/health/HealthContributor.java[`HealthContributor`] and {spring-boot-actuator-module-code}/health/ReactiveHealthContributor.java[`ReactiveHealthContributor`] instances defined in your `ApplicationContext`). -Regular `HealthContributors` that do not check against a reactive API are executed on the elastic scheduler. - -TIP: In a reactive application, The `ReactiveHealthContributorRegistry` can be used to register and unregister health indicators at runtime. - -To provide custom health information from a reactive API, you can register Spring beans that implement the {spring-boot-actuator-module-code}/health/ReactiveHealthIndicator.java[`ReactiveHealthIndicator`] interface. -The following code shows a sample `ReactiveHealthIndicator` implementation: - -[source,java,indent=0] ----- - @Component - public class MyReactiveHealthIndicator implements ReactiveHealthIndicator { - - @Override - public Mono health() { - return doHealthCheck() //perform some specific health check that returns a Mono - .onErrorResume(ex -> Mono.just(new Health.Builder().down(ex).build())); - } - - } ----- - -TIP: To handle the error automatically, consider extending from `AbstractReactiveHealthIndicator`. - - - -==== Auto-configured ReactiveHealthIndicators -The following `ReactiveHealthIndicators` are auto-configured by Spring Boot when appropriate: - -[cols="1,4"] -|=== -| Name | Description - -| {spring-boot-actuator-module-code}/cassandra/CassandraReactiveHealthIndicator.java[`CassandraReactiveHealthIndicator`] -| Checks that a Cassandra database is up. - -| {spring-boot-actuator-module-code}/couchbase/CouchbaseReactiveHealthIndicator.java[`CouchbaseReactiveHealthIndicator`] -| Checks that a Couchbase cluster is up. - -| {spring-boot-actuator-module-code}/mongo/MongoReactiveHealthIndicator.java[`MongoReactiveHealthIndicator`] -| Checks that a Mongo database is up. - -| {spring-boot-actuator-module-code}/redis/RedisReactiveHealthIndicator.java[`RedisReactiveHealthIndicator`] -| Checks that a Redis server is up. -|=== - -TIP: If necessary, reactive indicators replace the regular ones. -Also, any `HealthIndicator` that is not handled explicitly is wrapped automatically. - - - -==== Health Groups -It's sometimes useful to organize health indicators into groups that can be used for different purposes. -For example, if you deploy your application to Kubernetes, you may want one different sets of health indicators for your "`liveness`" and "`readiness`" probes. - -To create a health indicator group you can use the `management.endpoint.health.group.` property and specify a list of health indicator IDs to `include` or `exclude`. -For example, to create a group that includes only database indicators you can define the following: - -[source,properties,indent=0,configprops] ----- - management.endpoint.health.group.custom.include=db ----- - -You can then check the result by hitting `http://localhost:8080/actuator/health/custom`. - -By default groups will inherit the same `StatusAggregator` and `HttpCodeStatusMapper` settings as the system health, however, these can also be defined on a per-group basis. -It's also possible to override the `show-details` and `roles` properties if required: - -[source,properties,indent=0,configprops] ----- - management.endpoint.health.group.custom.show-details=when-authorized - management.endpoint.health.group.custom.roles=admin - management.endpoint.health.group.custom.status.order=fatal,up - management.endpoint.health.group.custom.status.http-mapping.fatal=500 - ----- - -TIP: You can use `@Qualifier("groupname")` if you need to register custom `StatusAggregator` or `HttpCodeStatusMapper` beans for use with the group. - - - -[[production-ready-application-info]] -=== Application Information -Application information exposes various information collected from all {spring-boot-actuator-module-code}/info/InfoContributor.java[`InfoContributor`] beans defined in your `ApplicationContext`. -Spring Boot includes a number of auto-configured `InfoContributor` beans, and you can write your own. - - - -[[production-ready-application-info-autoconfigure]] -==== Auto-configured InfoContributors -The following `InfoContributor` beans are auto-configured by Spring Boot, when appropriate: - -[cols="1,4"] -|=== -| Name | Description - -| {spring-boot-actuator-module-code}/info/EnvironmentInfoContributor.java[`EnvironmentInfoContributor`] -| Exposes any key from the `Environment` under the `info` key. - -| {spring-boot-actuator-module-code}/info/GitInfoContributor.java[`GitInfoContributor`] -| Exposes git information if a `git.properties` file is available. - -| {spring-boot-actuator-module-code}/info/BuildInfoContributor.java[`BuildInfoContributor`] -| Exposes build information if a `META-INF/build-info.properties` file is available. -|=== - -TIP: It is possible to disable them all by setting the configprop:management.info.defaults.enabled[] property. - - - -[[production-ready-application-info-env]] -==== Custom Application Information -You can customize the data exposed by the `info` endpoint by setting `+info.*+` Spring properties. -All `Environment` properties under the `info` key are automatically exposed. -For example, you could add the following settings to your `application.properties` file: - -[source,properties,indent=0,configprops] ----- - info.app.encoding=UTF-8 - info.app.java.source=1.8 - info.app.java.target=1.8 ----- - -[TIP] -==== -Rather than hardcoding those values, you could also <>. - -Assuming you use Maven, you could rewrite the preceding example as follows: - -[source,properties,indent=0,configprops] ----- - info.app.encoding=@project.build.sourceEncoding@ - info.app.java.source=@java.version@ - info.app.java.target=@java.version@ ----- -==== - - - -[[production-ready-application-info-git]] -==== Git Commit Information -Another useful feature of the `info` endpoint is its ability to publish information about the state of your `git` source code repository when the project was built. -If a `GitProperties` bean is available, the `git.branch`, `git.commit.id`, and `git.commit.time` properties are exposed. - -TIP: A `GitProperties` bean is auto-configured if a `git.properties` file is available at the root of the classpath. -See "<>" for more details. - -If you want to display the full git information (that is, the full content of `git.properties`), use the configprop:management.info.git.mode[] property, as follows: - -[source,properties,indent=0,configprops] ----- - management.info.git.mode=full ----- - - - -[[production-ready-application-info-build]] -==== Build Information -If a `BuildProperties` bean is available, the `info` endpoint can also publish information about your build. -This happens if a `META-INF/build-info.properties` file is available in the classpath. - -TIP: The Maven and Gradle plugins can both generate that file. -See "<>" for more details. - - - -[[production-ready-application-info-custom]] -==== Writing Custom InfoContributors -To provide custom application information, you can register Spring beans that implement the {spring-boot-actuator-module-code}/info/InfoContributor.java[`InfoContributor`] interface. - -The following example contributes an `example` entry with a single value: - -[source,java,indent=0] ----- - import java.util.Collections; - - import org.springframework.boot.actuate.info.Info; - import org.springframework.boot.actuate.info.InfoContributor; - import org.springframework.stereotype.Component; - - @Component - public class ExampleInfoContributor implements InfoContributor { - - @Override - public void contribute(Info.Builder builder) { - builder.withDetail("example", - Collections.singletonMap("key", "value")); - } - - } ----- - -If you reach the `info` endpoint, you should see a response that contains the following additional entry: - -[source,json,indent=0] ----- - { - "example": { - "key" : "value" - } - } ----- - - - -[[production-ready-monitoring]] -== Monitoring and Management over HTTP -If you are developing a web application, Spring Boot Actuator auto-configures all enabled endpoints to be exposed over HTTP. -The default convention is to use the `id` of the endpoint with a prefix of `/actuator` as the URL path. -For example, `health` is exposed as `/actuator/health`. - -TIP: Actuator is supported natively with Spring MVC, Spring WebFlux, and Jersey. -If both Jersey and Spring MVC are available, Spring MVC will be used. - - - -[[production-ready-customizing-management-server-context-path]] -=== Customizing the Management Endpoint Paths -Sometimes, it is useful to customize the prefix for the management endpoints. -For example, your application might already use `/actuator` for another purpose. -You can use the configprop:management.endpoints.web.base-path[] property to change the prefix for your management endpoint, as shown in the following example: - -[source,properties,indent=0,configprops] ----- - management.endpoints.web.base-path=/manage ----- - -The preceding `application.properties` example changes the endpoint from `/actuator/\{id}` to `/manage/\{id}` (for example, `/manage/info`). - -NOTE: Unless the management port has been configured to <>, `management.endpoints.web.base-path` is relative to `server.servlet.context-path`. -If `management.server.port` is configured, `management.endpoints.web.base-path` is relative to `management.server.servlet.context-path`. - -If you want to map endpoints to a different path, you can use the configprop:management.endpoints.web.path-mapping[] property. - -The following example remaps `/actuator/health` to `/healthcheck`: - -.application.properties -[source,properties,indent=0,configprops] ----- - management.endpoints.web.base-path=/ - management.endpoints.web.path-mapping.health=healthcheck ----- - - - -[[production-ready-customizing-management-server-port]] -=== Customizing the Management Server Port -Exposing management endpoints by using the default HTTP port is a sensible choice for cloud-based deployments. -If, however, your application runs inside your own data center, you may prefer to expose endpoints by using a different HTTP port. - -You can set the configprop:management.server.port[] property to change the HTTP port, as shown in the following example: - -[source,properties,indent=0,configprops] ----- - management.server.port=8081 ----- - -NOTE: On Cloud Foundry, applications only receive requests on port 8080 for both HTTP and TCP routing, by default. -If you want to use a custom management port on Cloud Foundry, you will need to explicitly set up the application's routes to forward traffic to the custom port. - - - -[[production-ready-management-specific-ssl]] -=== Configuring Management-specific SSL -When configured to use a custom port, the management server can also be configured with its own SSL by using the various `management.server.ssl.*` properties. -For example, doing so lets a management server be available over HTTP while the main application uses HTTPS, as shown in the following property settings: - -[source,properties,indent=0,configprops] ----- - server.port=8443 - server.ssl.enabled=true - server.ssl.key-store=classpath:store.jks - server.ssl.key-password=secret - management.server.port=8080 - management.server.ssl.enabled=false ----- - -Alternatively, both the main server and the management server can use SSL but with different key stores, as follows: - -[source,properties,indent=0,configprops] ----- - server.port=8443 - server.ssl.enabled=true - server.ssl.key-store=classpath:main.jks - server.ssl.key-password=secret - management.server.port=8080 - management.server.ssl.enabled=true - management.server.ssl.key-store=classpath:management.jks - management.server.ssl.key-password=secret ----- - - - -[[production-ready-customizing-management-server-address]] -=== Customizing the Management Server Address -You can customize the address that the management endpoints are available on by setting the configprop:management.server.address[] property. -Doing so can be useful if you want to listen only on an internal or ops-facing network or to listen only for connections from `localhost`. - -NOTE: You can listen on a different address only when the port differs from the main server port. - -The following example `application.properties` does not allow remote management connections: - -[source,properties,indent=0,configprops] ----- - management.server.port=8081 - management.server.address=127.0.0.1 ----- - - - -[[production-ready-disabling-http-endpoints]] -=== Disabling HTTP Endpoints -If you do not want to expose endpoints over HTTP, you can set the management port to `-1`, as shown in the following example: - -[source,properties,indent=0,configprops] ----- - management.server.port=-1 ----- - -This can be achieved using the configprop:management.endpoints.web.exposure.exclude[] property as well, as shown in following example: - -[source,properties,indent=0,configprops] ----- - management.endpoints.web.exposure.exclude=* ----- - - - -[[production-ready-jmx]] -== Monitoring and Management over JMX -Java Management Extensions (JMX) provide a standard mechanism to monitor and manage applications. -By default, this feature is not enabled and can be turned on by setting the configuration property configprop:spring.jmx.enabled[] to `true`. -Spring Boot exposes management endpoints as JMX MBeans under the `org.springframework.boot` domain by default. - - - -[[production-ready-custom-mbean-names]] -=== Customizing MBean Names -The name of the MBean is usually generated from the `id` of the endpoint. -For example, the `health` endpoint is exposed as `org.springframework.boot:type=Endpoint,name=Health`. - -If your application contains more than one Spring `ApplicationContext`, you may find that names clash. -To solve this problem, you can set the configprop:spring.jmx.unique-names[] property to `true` so that MBean names are always unique. - -You can also customize the JMX domain under which endpoints are exposed. -The following settings show an example of doing so in `application.properties`: - -[source,properties,indent=0,configprops] ----- - spring.jmx.unique-names=true - management.endpoints.jmx.domain=com.example.myapp ----- - - - -[[production-ready-disable-jmx-endpoints]] -=== Disabling JMX Endpoints -If you do not want to expose endpoints over JMX, you can set the configprop:management.endpoints.jmx.exposure.exclude[] property to `*`, as shown in the following example: - -[source,properties,indent=0,configprops] ----- - management.endpoints.jmx.exposure.exclude=* ----- - - - -[[production-ready-jolokia]] -=== Using Jolokia for JMX over HTTP -Jolokia is a JMX-HTTP bridge that provides an alternative method of accessing JMX beans. -To use Jolokia, include a dependency to `org.jolokia:jolokia-core`. -For example, with Maven, you would add the following dependency: - -[source,xml,indent=0] ----- - - org.jolokia - jolokia-core - ----- - -The Jolokia endpoint can then be exposed by adding `jolokia` or `*` to the configprop:management.endpoints.web.exposure.include[] property. -You can then access it by using `/actuator/jolokia` on your management HTTP server. - - - -[[production-ready-customizing-jolokia]] -==== Customizing Jolokia -Jolokia has a number of settings that you would traditionally configure by setting servlet parameters. -With Spring Boot, you can use your `application.properties` file. -To do so, prefix the parameter with `management.endpoint.jolokia.config.`, as shown in the following example: - -[source,properties,indent=0,configprops] ----- - management.endpoint.jolokia.config.debug=true ----- - - - -[[production-ready-disabling-jolokia]] -==== Disabling Jolokia -If you use Jolokia but do not want Spring Boot to configure it, set the configprop:management.endpoint.jolokia.enabled[] property to `false`, as follows: - -[source,properties,indent=0,configprops] ----- - management.endpoint.jolokia.enabled=false ----- - - - -[[production-ready-loggers]] -== Loggers -Spring Boot Actuator includes the ability to view and configure the log levels of your application at runtime. -You can view either the entire list or an individual logger's configuration, which is made up of both the explicitly configured logging level as well as the effective logging level given to it by the logging framework. -These levels can be one of: - -* `TRACE` -* `DEBUG` -* `INFO` -* `WARN` -* `ERROR` -* `FATAL` -* `OFF` -* `null` - -`null` indicates that there is no explicit configuration. - - - -[[production-ready-logger-configuration]] -=== Configure a Logger -To configure a given logger, `POST` a partial entity to the resource's URI, as shown in the following example: - -[source,json,indent=0] ----- - { - "configuredLevel": "DEBUG" - } ----- - -TIP: To "`reset`" the specific level of the logger (and use the default configuration instead), you can pass a value of `null` as the `configuredLevel`. - - - -[[production-ready-metrics]] -== Metrics -Spring Boot Actuator provides dependency management and auto-configuration for https://micrometer.io[Micrometer], an application metrics facade that supports numerous monitoring systems, including: - -- <> -- <> -- <> -- <> -- <> -- <> -- <> -- <> -- <> -- <> -- <> -- <> -- <> -- <> -- <> -- <> -- <> - -TIP: To learn more about Micrometer's capabilities, please refer to its https://micrometer.io/docs[reference documentation], in particular the {micrometer-concepts-docs}[concepts section]. - - - -[[production-ready-metrics-getting-started]] -=== Getting started -Spring Boot auto-configures a composite `MeterRegistry` and adds a registry to the composite for each of the supported implementations that it finds on the classpath. -Having a dependency on `micrometer-registry-\{system}` in your runtime classpath is enough for Spring Boot to configure the registry. - -Most registries share common features. -For instance, you can disable a particular registry even if the Micrometer registry implementation is on the classpath. -For example, to disable Datadog: - -[source,properties,indent=0,configprops] ----- - management.metrics.export.datadog.enabled=false ----- - -Spring Boot will also add any auto-configured registries to the global static composite registry on the `Metrics` class unless you explicitly tell it not to: - -[source,properties,indent=0,configprops] ----- - management.metrics.use-global-registry=false ----- - -You can register any number of `MeterRegistryCustomizer` beans to further configure the registry, such as applying common tags, before any meters are registered with the registry: - -[source,java,indent=0] ----- - @Bean - MeterRegistryCustomizer metricsCommonTags() { - return registry -> registry.config().commonTags("region", "us-east-1"); - } ----- - -You can apply customizations to particular registry implementations by being more specific about the generic type: - -[source,java,indent=0] ----- - @Bean - MeterRegistryCustomizer graphiteMetricsNamingConvention() { - return registry -> registry.config().namingConvention(MY_CUSTOM_CONVENTION); - } ----- - -With that setup in place you can inject `MeterRegistry` in your components and register metrics: - -[source,java,indent=0] ----- -include::{code-examples}/actuate/metrics/SampleBean.java[tag=example] ----- - -Spring Boot also <> (i.e. `MeterBinder` implementations) that you can control via configuration or dedicated annotation markers. - - - -[[production-ready-metrics-export]] -=== Supported monitoring systems - - - -[[production-ready-metrics-export-appoptics]] -==== AppOptics -By default, the AppOptics registry pushes metrics to `https://api.appoptics.com/v1/measurements` periodically. -To export metrics to SaaS {micrometer-registry-docs}/appoptics[AppOptics], your API token must be provided: - -[source,properties,indent=0,configprops] ----- - management.metrics.export.appoptics.api-token=YOUR_TOKEN ----- - - - -[[production-ready-metrics-export-atlas]] -==== Atlas -By default, metrics are exported to {micrometer-registry-docs}/atlas[Atlas] running on your local machine. -The location of the https://github.com/Netflix/atlas[Atlas server] to use can be provided using: - -[source,properties,indent=0,configprops] ----- - management.metrics.export.atlas.uri=https://atlas.example.com:7101/api/v1/publish ----- - - - -[[production-ready-metrics-export-datadog]] -==== Datadog -Datadog registry pushes metrics to https://www.datadoghq.com[datadoghq] periodically. -To export metrics to {micrometer-registry-docs}/datadog[Datadog], your API key must be provided: - -[source,properties,indent=0,configprops] ----- - management.metrics.export.datadog.api-key=YOUR_KEY ----- - -You can also change the interval at which metrics are sent to Datadog: - -[source,properties,indent=0,configprops] ----- - management.metrics.export.datadog.step=30s ----- - - - -[[production-ready-metrics-export-dynatrace]] -==== Dynatrace -Dynatrace registry pushes metrics to the configured URI periodically. -To export metrics to {micrometer-registry-docs}/dynatrace[Dynatrace], your API token, device ID, and URI must be provided: - -[source,properties,indent=0,configprops] ----- - management.metrics.export.dynatrace.api-token=YOUR_TOKEN - management.metrics.export.dynatrace.device-id=YOUR_DEVICE_ID - management.metrics.export.dynatrace.uri=YOUR_URI ----- - -You can also change the interval at which metrics are sent to Dynatrace: - -[source,properties,indent=0,configprops] ----- - management.metrics.export.dynatrace.step=30s ----- - - - -[[production-ready-metrics-export-elastic]] -==== Elastic -By default, metrics are exported to {micrometer-registry-docs}/elastic[Elastic] running on your local machine. -The location of the Elastic server to use can be provided using the following property: - -[source,properties,indent=0,configprops] ----- - management.metrics.export.elastic.host=https://elastic.example.com:8086 ----- - - - -[[production-ready-metrics-export-ganglia]] -==== Ganglia -By default, metrics are exported to {micrometer-registry-docs}/ganglia[Ganglia] running on your local machine. -The http://ganglia.sourceforge.net[Ganglia server] host and port to use can be provided using: - -[source,properties,indent=0,configprops] ----- - management.metrics.export.ganglia.host=ganglia.example.com - management.metrics.export.ganglia.port=9649 ----- - - - -[[production-ready-metrics-export-graphite]] -==== Graphite -By default, metrics are exported to {micrometer-registry-docs}/graphite[Graphite] running on your local machine. -The https://graphiteapp.org[Graphite server] host and port to use can be provided using: - -[source,properties,indent=0,configprops] ----- - management.metrics.export.graphite.host=graphite.example.com - management.metrics.export.graphite.port=9004 ----- - -Micrometer provides a default `HierarchicalNameMapper` that governs how a dimensional meter id is {micrometer-registry-docs}/graphite#_hierarchical_name_mapping[mapped to flat hierarchical names]. - -TIP: To take control over this behaviour, define your `GraphiteMeterRegistry` and supply your own `HierarchicalNameMapper`. -An auto-configured `GraphiteConfig` and `Clock` beans are provided unless you define your own: - -[source,java] ----- -@Bean -public GraphiteMeterRegistry graphiteMeterRegistry(GraphiteConfig config, Clock clock) { - return new GraphiteMeterRegistry(config, clock, MY_HIERARCHICAL_MAPPER); -} ----- - - - -[[production-ready-metrics-export-humio]] -==== Humio -By default, the Humio registry pushes metrics to https://cloud.humio.com periodically. -To export metrics to SaaS {micrometer-registry-docs}/humio[Humio], your API token must be provided: - -[source,properties,indent=0,configprops] ----- - management.metrics.export.humio.api-token=YOUR_TOKEN ----- - -You should also configure one or more tags to identify the data source to which metrics will be pushed: - -[source,properties,indent=0,configprops] ----- - management.metrics.export.humio.tags.alpha=a - management.metrics.export.humio.tags.bravo=b ----- - - - -[[production-ready-metrics-export-influx]] -==== Influx -By default, metrics are exported to {micrometer-registry-docs}/influx[Influx] running on your local machine. -The location of the https://www.influxdata.com[Influx server] to use can be provided using: - -[source,properties,indent=0,configprops] ----- - management.metrics.export.influx.uri=https://influx.example.com:8086 ----- - - - -[[production-ready-metrics-export-jmx]] -==== JMX -Micrometer provides a hierarchical mapping to {micrometer-registry-docs}/jmx[JMX], primarily as a cheap and portable way to view metrics locally. -By default, metrics are exported to the `metrics` JMX domain. -The domain to use can be provided using: - -[source,properties,indent=0,configprops] ----- - management.metrics.export.jmx.domain=com.example.app.metrics ----- - -Micrometer provides a default `HierarchicalNameMapper` that governs how a dimensional meter id is {micrometer-registry-docs}/jmx#_hierarchical_name_mapping[mapped to flat hierarchical names]. - -TIP: To take control over this behaviour, define your `JmxMeterRegistry` and supply your own `HierarchicalNameMapper`. -An auto-configured `JmxConfig` and `Clock` beans are provided unless you define your own: - -[source,java] ----- -@Bean -public JmxMeterRegistry jmxMeterRegistry(JmxConfig config, Clock clock) { - return new JmxMeterRegistry(config, clock, MY_HIERARCHICAL_MAPPER); -} ----- - - - -[[production-ready-metrics-export-kairos]] -==== KairosDB -By default, metrics are exported to {micrometer-registry-docs}/kairos[KairosDB] running on your local machine. -The location of the https://kairosdb.github.io/[KairosDB server] to use can be provided using: - -[source,properties,indent=0,configprops] ----- - management.metrics.export.kairos.uri=https://kairosdb.example.com:8080/api/v1/datapoints ----- - - - -[[production-ready-metrics-export-newrelic]] -==== New Relic -New Relic registry pushes metrics to {micrometer-registry-docs}/new-relic[New Relic] periodically. -To export metrics to https://newrelic.com[New Relic], your API key and account id must be provided: - -[source,properties,indent=0,configprops] ----- - management.metrics.export.newrelic.api-key=YOUR_KEY - management.metrics.export.newrelic.account-id=YOUR_ACCOUNT_ID ----- - -You can also change the interval at which metrics are sent to New Relic: - -[source,properties,indent=0,configprops] ----- - management.metrics.export.newrelic.step=30s ----- - - - -[[production-ready-metrics-export-prometheus]] -==== Prometheus -{micrometer-registry-docs}/prometheus[Prometheus] expects to scrape or poll individual app instances for metrics. -Spring Boot provides an actuator endpoint available at `/actuator/prometheus` to present a https://prometheus.io[Prometheus scrape] with the appropriate format. - -TIP: The endpoint is not available by default and must be exposed, see <> for more details. - -Here is an example `scrape_config` to add to `prometheus.yml`: - -[source,yaml,indent=0] ----- - scrape_configs: - - job_name: 'spring' - metrics_path: '/actuator/prometheus' - static_configs: - - targets: ['HOST:PORT'] ----- - -For ephemeral or batch jobs which may not exist long enough to be scraped, https://github.com/prometheus/pushgateway[Prometheus Pushgateway] support can be used to expose their metrics to Prometheus. -To enable Prometheus Pushgateway support, add the following dependency to your project: - -[source,xml,indent=0] ----- - - io.prometheus - simpleclient_pushgateway - ----- - -When the Prometheus Pushgateway dependency is present on the classpath, Spring Boot auto-configures a `PrometheusPushGatewayManager` bean. -This manages the pushing of metrics to a Prometheus Pushgateway. -The `PrometheusPushGatewayManager` can be tuned using properties under `management.metrics.export.prometheus.pushgateway`. -For advanced configuration, you can also provide your own `PrometheusPushGatewayManager` bean. - - - -[[production-ready-metrics-export-signalfx]] -==== SignalFx -SignalFx registry pushes metrics to {micrometer-registry-docs}/signalfx[SignalFx] periodically. -To export metrics to https://www.signalfx.com[SignalFx], your access token must be provided: - -[source,properties,indent=0,configprops] ----- - management.metrics.export.signalfx.access-token=YOUR_ACCESS_TOKEN ----- - -You can also change the interval at which metrics are sent to SignalFx: - -[source,properties,indent=0,configprops] ----- - management.metrics.export.signalfx.step=30s ----- - - - -[[production-ready-metrics-export-simple]] -==== Simple -Micrometer ships with a simple, in-memory backend that is automatically used as a fallback if no other registry is configured. -This allows you to see what metrics are collected in the <>. - -The in-memory backend disables itself as soon as you're using any of the other available backend. -You can also disable it explicitly: - -[source,properties,indent=0,configprops] ----- - management.metrics.export.simple.enabled=false ----- - - - -[[production-ready-metrics-export-statsd]] -==== StatsD -The StatsD registry pushes metrics over UDP to a StatsD agent eagerly. -By default, metrics are exported to a {micrometer-registry-docs}/statsd[StatsD] agent running on your local machine. -The StatsD agent host and port to use can be provided using: - -[source,properties,indent=0,configprops] ----- - management.metrics.export.statsd.host=statsd.example.com - management.metrics.export.statsd.port=9125 ----- - -You can also change the StatsD line protocol to use (default to Datadog): - -[source,properties,indent=0,configprops] ----- - management.metrics.export.statsd.flavor=etsy ----- - - - -[[production-ready-metrics-export-wavefront]] -==== Wavefront -Wavefront registry pushes metrics to {micrometer-registry-docs}/wavefront[Wavefront] periodically. -If you are exporting metrics to https://www.wavefront.com/[Wavefront] directly, your API token must be provided: - -[source,properties,indent=0,configprops] ----- - management.metrics.export.wavefront.api-token=YOUR_API_TOKEN ----- - -Alternatively, you may use a Wavefront sidecar or an internal proxy set up in your environment that forwards metrics data to the Wavefront API host: - -[source,properties,indent=0,configprops] ----- - management.metrics.export.wavefront.uri=proxy://localhost:2878 ----- - -TIP: If publishing metrics to a Wavefront proxy (as described in https://docs.wavefront.com/proxies_installing.html[the documentation]), the host must be in the `proxy://HOST:PORT` format. - -You can also change the interval at which metrics are sent to Wavefront: - -[source,properties,indent=0,configprops] ----- - management.metrics.export.wavefront.step=30s ----- - - - -[[production-ready-metrics-meter]] -=== Supported Metrics -Spring Boot registers the following core metrics when applicable: - -* JVM metrics, report utilization of: -** Various memory and buffer pools -** Statistics related to garbage collection -** Threads utilization -** Number of classes loaded/unloaded -* CPU metrics -* File descriptor metrics -* Kafka consumer metrics -* Log4j2 metrics: record the number of events logged to Log4j2 at each level -* Logback metrics: record the number of events logged to Logback at each level -* Uptime metrics: report a gauge for uptime and a fixed gauge representing the application's absolute start time -* Tomcat metrics (`server.tomcat.mbeanregistry.enabled` must be set to `true` for all Tomcat metrics to be registered) -* {spring-integration-docs}system-management.html#micrometer-integration[Spring Integration] metrics - - - -[[production-ready-metrics-spring-mvc]] -==== Spring MVC Metrics -Auto-configuration enables the instrumentation of requests handled by Spring MVC. -When `management.metrics.web.server.request.autotime.enabled` is `true`, this instrumentation occurs for all requests. -Alternatively, when set to `false`, you can enable instrumentation by adding `@Timed` to a request-handling method: - -[source,java,indent=0] ----- - @RestController - @Timed <1> - public class MyController { - - @GetMapping("/api/people") - @Timed(extraTags = { "region", "us-east-1" }) <2> - @Timed(value = "all.people", longTask = true) <3> - public List listPeople() { ... } - - } ----- -<1> A controller class to enable timings on every request handler in the controller. -<2> A method to enable for an individual endpoint. - This is not necessary if you have it on the class, but can be used to further customize the timer for this particular endpoint. -<3> A method with `longTask = true` to enable a long task timer for the method. - Long task timers require a separate metric name, and can be stacked with a short task timer. - -By default, metrics are generated with the name, `http.server.requests`. -The name can be customized by setting the configprop:management.metrics.web.server.request.metric-name[] property. - -By default, Spring MVC-related metrics are tagged with the following information: - -|=== -| Tag | Description - -| `exception` -| Simple class name of any exception that was thrown while handling the request. - -| `method` -| Request's method (for example, `GET` or `POST`) - -| `outcome` -| Request's outcome based on the status code of the response. - 1xx is `INFORMATIONAL`, 2xx is `SUCCESS`, 3xx is `REDIRECTION`, 4xx `CLIENT_ERROR`, and 5xx is `SERVER_ERROR` - -| `status` -| Response's HTTP status code (for example, `200` or `500`) - -| `uri` -| Request's URI template prior to variable substitution, if possible (for example, `/api/person/\{id}`) -|=== - -To customize the tags, provide a `@Bean` that implements `WebMvcTagsProvider`. - - - -[[production-ready-metrics-web-flux]] -==== Spring WebFlux Metrics -Auto-configuration enables the instrumentation of all requests handled by WebFlux controllers and functional handlers. - -By default, metrics are generated with the name `http.server.requests`. -You can customize the name by setting the configprop:management.metrics.web.server.request.metric-name[] property. - -By default, WebFlux-related metrics are tagged with the following information: - -|=== -| Tag | Description - -| `exception` -| Simple class name of any exception that was thrown while handling the request. - -| `method` -| Request's method (for example, `GET` or `POST`) - -| `outcome` -| Request's outcome based on the status code of the response. - 1xx is `INFORMATIONAL`, 2xx is `SUCCESS`, 3xx is `REDIRECTION`, 4xx `CLIENT_ERROR`, and 5xx is `SERVER_ERROR` - -| `status` -| Response's HTTP status code (for example, `200` or `500`) - -| `uri` -| Request's URI template prior to variable substitution, if possible (for example, `/api/person/\{id}`) -|=== - -To customize the tags, provide a `@Bean` that implements `WebFluxTagsProvider`. - - - -[[production-ready-metrics-jersey-server]] -==== Jersey Server Metrics -When Micrometer's `micrometer-jersey2` module is on the classpath, auto-configuration enables the instrumentation of requests handled by the Jersey JAX-RS implementation. -When `management.metrics.web.server.request.autotime.enabled` is `true`, this instrumentation occurs for all requests. -Alternatively, when set to `false`, you can enable instrumentation by adding `@Timed` to a request-handling method: - -[source,java,indent=0] ----- - @Component - @Path("/api/people") - @Timed <1> - public class Endpoint { - @GET - @Timed(extraTags = { "region", "us-east-1" }) <2> - @Timed(value = "all.people", longTask = true) <3> - public List listPeople() { ... } - } ----- -<1> On a resource class to enable timings on every request handler in the resource. -<2> On a method to enable for an individual endpoint. - This is not necessary if you have it on the class, but can be used to further customize the timer for this particular endpoint. -<3> On a method with `longTask = true` to enable a long task timer for the method. - Long task timers require a separate metric name, and can be stacked with a short task timer. - -By default, metrics are generated with the name, `http.server.requests`. -The name can be customized by setting the configprop:management.metrics.web.server.request.metric-name[] property. - -By default, Jersey server metrics are tagged with the following information: - -|=== -| Tag | Description - -| `exception` -| Simple class name of any exception that was thrown while handling the request. - -| `method` -| Request's method (for example, `GET` or `POST`) - -| `outcome` -| Request's outcome based on the status code of the response. - 1xx is `INFORMATIONAL`, 2xx is `SUCCESS`, 3xx is `REDIRECTION`, 4xx `CLIENT_ERROR`, and 5xx is `SERVER_ERROR` - -| `status` -| Response's HTTP status code (for example, `200` or `500`) - -| `uri` -| Request's URI template prior to variable substitution, if possible (for example, `/api/person/\{id}`) -|=== - -To customize the tags, provide a `@Bean` that implements `JerseyTagsProvider`. - - - -[[production-ready-metrics-http-clients]] -==== HTTP Client Metrics -Spring Boot Actuator manages the instrumentation of both `RestTemplate` and `WebClient`. -For that, you have to get injected with an auto-configured builder and use it to create instances: - -* `RestTemplateBuilder` for `RestTemplate` -* `WebClient.Builder` for `WebClient` - -It is also possible to apply manually the customizers responsible for this instrumentation, namely `MetricsRestTemplateCustomizer` and `MetricsWebClientCustomizer`. - -By default, metrics are generated with the name, `http.client.requests`. -The name can be customized by setting the configprop:management.metrics.web.client.request.metric-name[] property. - -By default, metrics generated by an instrumented client are tagged with the following information: - -|=== -| Tag | Description - -| `clientName` -| Host portion of the URI - -| `method` -| Request's method (for example, `GET` or `POST`) - -| `outcome` -| Request's outcome based on the status code of the response. - 1xx is `INFORMATIONAL`, 2xx is `SUCCESS`, 3xx is `REDIRECTION`, 4xx `CLIENT_ERROR`, and 5xx is `SERVER_ERROR`, `UNKNOWN` otherwise - -| `status` -| Response's HTTP status code if available (for example, `200` or `500`), or `IO_ERROR` in case of I/O issues, `CLIENT_ERROR` otherwise - -| `uri` -| Request's URI template prior to variable substitution, if possible (for example, `/api/person/\{id}`) -|=== - -To customize the tags, and depending on your choice of client, you can provide a `@Bean` that implements `RestTemplateExchangeTagsProvider` or `WebClientExchangeTagsProvider`. -There are convenience static functions in `RestTemplateExchangeTags` and `WebClientExchangeTags`. - - - -[[production-ready-metrics-cache]] -==== Cache Metrics -Auto-configuration enables the instrumentation of all available ``Cache``s on startup with metrics prefixed with `cache`. -Cache instrumentation is standardized for a basic set of metrics. -Additional, cache-specific metrics are also available. - -The following cache libraries are supported: - -* Caffeine -* EhCache 2 -* Hazelcast -* Any compliant JCache (JSR-107) implementation - -Metrics are tagged by the name of the cache and by the name of the `CacheManager` that is derived from the bean name. - -NOTE: Only caches that are available on startup are bound to the registry. -For caches created on-the-fly or programmatically after the startup phase, an explicit registration is required. -A `CacheMetricsRegistrar` bean is made available to make that process easier. - - - -[[production-ready-metrics-jdbc]] -==== DataSource Metrics -Auto-configuration enables the instrumentation of all available `DataSource` objects with metrics prefixed with `jdbc.connections`. -Data source instrumentation results in gauges representing the currently active, idle, maximum allowed, and minimum allowed connections in the pool. - -Metrics are also tagged by the name of the `DataSource` computed based on the bean name. - -TIP: By default, Spring Boot provides metadata for all supported data sources; you can add additional `DataSourcePoolMetadataProvider` beans if your favorite data source isn't supported out of the box. -See `DataSourcePoolMetadataProvidersConfiguration` for examples. - -Also, Hikari-specific metrics are exposed with a `hikaricp` prefix. -Each metric is tagged by the name of the Pool (can be controlled with `spring.datasource.name`). - - - -[[production-ready-metrics-hibernate]] -==== Hibernate Metrics -Auto-configuration enables the instrumentation of all available Hibernate `EntityManagerFactory` instances that have statistics enabled with a metric named `hibernate`. - -Metrics are also tagged by the name of the `EntityManagerFactory` that is derived from the bean name. - -To enable statistics, the standard JPA property `hibernate.generate_statistics` must be set to `true`. -You can enable that on the auto-configured `EntityManagerFactory` as shown in the following example: - -[source,properties,indent=0,configprops] ----- - spring.jpa.properties.hibernate.generate_statistics=true ----- - - - -[[production-ready-metrics-rabbitmq]] -==== RabbitMQ Metrics -Auto-configuration will enable the instrumentation of all available RabbitMQ connection factories with a metric named `rabbitmq`. - - - -[[production-ready-metrics-custom]] -=== Registering custom metrics -To register custom metrics, inject `MeterRegistry` into your component, as shown in the following example: - -[source,java,indent=0] ----- -include::{code-examples}/actuate/metrics/MetricsMeterRegistryInjectionExample.java[tag=component] ----- - -If you find that you repeatedly instrument a suite of metrics across components or applications, you may encapsulate this suite in a `MeterBinder` implementation. -By default, metrics from all `MeterBinder` beans will be automatically bound to the Spring-managed `MeterRegistry`. - - - -[[production-ready-metrics-per-meter-properties]] -=== Customizing individual metrics -If you need to apply customizations to specific `Meter` instances you can use the `io.micrometer.core.instrument.config.MeterFilter` interface. -By default, all `MeterFilter` beans will be automatically applied to the micrometer `MeterRegistry.Config`. - -For example, if you want to rename the `mytag.region` tag to `mytag.area` for all meter IDs beginning with `com.example`, you can do the following: - -[source,java,indent=0] ----- -include::{code-examples}/actuate/metrics/MetricsFilterBeanExample.java[tag=configuration] ----- - - - -[[production-ready-metrics-common-tags]] -==== Common tags -Common tags are generally used for dimensional drill-down on the operating environment like host, instance, region, stack, etc. -Commons tags are applied to all meters and can be configured as shown in the following example: - -[source,properties,indent=0,configprops] ----- - management.metrics.tags.region=us-east-1 - management.metrics.tags.stack=prod ----- - -The example above adds `region` and `stack` tags to all meters with a value of `us-east-1` and `prod` respectively. - -NOTE: The order of common tags is important if you are using Graphite. -As the order of common tags cannot be guaranteed using this approach, Graphite users are advised to define a custom `MeterFilter` instead. - - - -==== Per-meter properties -In addition to `MeterFilter` beans, it's also possible to apply a limited set of customization on a per-meter basis using properties. -Per-meter customizations apply to any all meter IDs that start with the given name. -For example, the following will disable any meters that have an ID starting with `example.remote` - -[source,properties,indent=0,configprops] ----- - management.metrics.enable.example.remote=false ----- - -The following properties allow per-meter customization: - -.Per-meter customizations -|=== -| Property | Description - -| configprop:management.metrics.enable[] -| Whether to deny meters from emitting any metrics. - -| configprop:management.metrics.distribution.percentiles-histogram[] -| Whether to publish a histogram suitable for computing aggregable (across dimension) percentile approximations. - -| configprop:management.metrics.distribution.minimum-expected-value[], configprop:management.metrics.distribution.maximum-expected-value[] -| Publish less histogram buckets by clamping the range of expected values. - -| configprop:management.metrics.distribution.percentiles[] -| Publish percentile values computed in your application - -| configprop:management.metrics.distribution.sla[] -| Publish a cumulative histogram with buckets defined by your SLAs. -|=== - -For more details on concepts behind `percentiles-histogram`, `percentiles` and `sla` refer to the {micrometer-concepts-docs}#_histograms_and_percentiles["Histograms and percentiles" section] of the micrometer documentation. - - - -[[production-ready-metrics-endpoint]] -=== Metrics endpoint -Spring Boot provides a `metrics` endpoint that can be used diagnostically to examine the metrics collected by an application. -The endpoint is not available by default and must be exposed, see <> for more details. - -Navigating to `/actuator/metrics` displays a list of available meter names. -You can drill down to view information about a particular meter by providing its name as a selector, e.g. `/actuator/metrics/jvm.memory.max`. - -[TIP] -==== -The name you use here should match the name used in the code, not the name after it has been naming-convention normalized for a monitoring system it is shipped to. -In other words, if `jvm.memory.max` appears as `jvm_memory_max` in Prometheus because of its snake case naming convention, you should still use `jvm.memory.max` as the selector when inspecting the meter in the `metrics` endpoint. -==== - -You can also add any number of `tag=KEY:VALUE` query parameters to the end of the URL to dimensionally drill down on a meter, e.g. `/actuator/metrics/jvm.memory.max?tag=area:nonheap`. - -[TIP] -==== -The reported measurements are the _sum_ of the statistics of all meters matching the meter name and any tags that have been applied. -So in the example above, the returned "Value" statistic is the sum of the maximum memory footprints of "Code Cache", "Compressed Class Space", and "Metaspace" areas of the heap. -If you just wanted to see the maximum size for the "Metaspace", you could add an additional `tag=id:Metaspace`, i.e. `/actuator/metrics/jvm.memory.max?tag=area:nonheap&tag=id:Metaspace`. -==== - - - -[[production-ready-auditing]] -== Auditing -Once Spring Security is in play, Spring Boot Actuator has a flexible audit framework that publishes events (by default, "`authentication success`", "`failure`" and "`access denied`" exceptions). -This feature can be very useful for reporting and for implementing a lock-out policy based on authentication failures. - -Auditing can be enabled by providing a bean of type `AuditEventRepository` in your application's configuration. -For convenience, Spring Boot offers an `InMemoryAuditEventRepository`. -`InMemoryAuditEventRepository` has limited capabilities and we recommend using it only for development environments. -For production environments, consider creating your own alternative `AuditEventRepository` implementation. - - - -[[production-ready-auditing-custom]] -=== Custom Auditing -To customize published security events, you can provide your own implementations of `AbstractAuthenticationAuditListener` and `AbstractAuthorizationAuditListener`. - -You can also use the audit services for your own business events. -To do so, either inject the `AuditEventRepository` bean into your own components and use that directly or publish an `AuditApplicationEvent` with the Spring `ApplicationEventPublisher` (by implementing `ApplicationEventPublisherAware`). - - - -[[production-ready-http-tracing]] -== HTTP Tracing -HTTP Tracing can be enabled by providing a bean of type `HttpTraceRepository` in your application's configuration. -For convenience, Spring Boot offers an `InMemoryHttpTraceRepository` that stores traces for the last 100 request-response exchanges, by default. -`InMemoryHttpTraceRepository` is limited compared to other tracing solutions and we recommend using it only for development environments. -For production environments, use of a production-ready tracing or observability solution, such as Zipkin or Spring Cloud Sleuth, is recommended. -Alternatively, create your own `HttpTraceRepository` that meets your needs. - -The `httptrace` endpoint can be used to obtain information about the request-response exchanges that are stored in the `HttpTraceRepository`. - - - -[[production-ready-http-tracing-custom]] -=== Custom HTTP tracing -To customize the items that are included in each trace, use the configprop:management.trace.http.include[] configuration property. -For advanced customization, consider registering your own `HttpExchangeTracer` implementation. - - - -[[production-ready-process-monitoring]] -== Process Monitoring -In the `spring-boot` module, you can find two classes to create files that are often useful for process monitoring: - -* `ApplicationPidFileWriter` creates a file containing the application PID (by default, in the application directory with a file name of `application.pid`). -* `WebServerPortFileWriter` creates a file (or files) containing the ports of the running web server (by default, in the application directory with a file name of `application.port`). - -By default, these writers are not activated, but you can enable: - -* <> -* <> - - - -[[production-ready-process-monitoring-configuration]] -=== Extending Configuration -In the `META-INF/spring.factories` file, you can activate the listener(s) that writes a PID file, as shown in the following example: - -[indent=0] ----- - org.springframework.context.ApplicationListener=\ - org.springframework.boot.context.ApplicationPidFileWriter,\ - org.springframework.boot.web.context.WebServerPortFileWriter ----- - - - -[[production-ready-process-monitoring-programmatically]] -=== Programmatically -You can also activate a listener by invoking the `SpringApplication.addListeners(...)` method and passing the appropriate `Writer` object. -This method also lets you customize the file name and path in the `Writer` constructor. - - - -[[production-ready-cloudfoundry]] -== Cloud Foundry Support -Spring Boot's actuator module includes additional support that is activated when you deploy to a compatible Cloud Foundry instance. -The `/cloudfoundryapplication` path provides an alternative secured route to all `@Endpoint` beans. - -The extended support lets Cloud Foundry management UIs (such as the web application that you can use to view deployed applications) be augmented with Spring Boot actuator information. -For example, an application status page may include full health information instead of the typical "`running`" or "`stopped`" status. - -NOTE: The `/cloudfoundryapplication` path is not directly accessible to regular users. -In order to use the endpoint, a valid UAA token must be passed with the request. - - - -[[production-ready-cloudfoundry-disable]] -=== Disabling Extended Cloud Foundry Actuator Support -If you want to fully disable the `/cloudfoundryapplication` endpoints, you can add the following setting to your `application.properties` file: - - -.application.properties -[source,properties,indent=0,configprops] ----- - management.cloudfoundry.enabled=false ----- - - - -[[production-ready-cloudfoundry-ssl]] -=== Cloud Foundry Self-signed Certificates -By default, the security verification for `/cloudfoundryapplication` endpoints makes SSL calls to various Cloud Foundry services. -If your Cloud Foundry UAA or Cloud Controller services use self-signed certificates, you need to set the following property: - -.application.properties -[source,properties,indent=0,configprops] ----- - management.cloudfoundry.skip-ssl-validation=true ----- - - - -=== Custom context path -If the server's context-path has been configured to anything other than `/`, the Cloud Foundry endpoints will not be available at the root of the application. -For example, if `server.servlet.context-path=/app`, Cloud Foundry endpoints will be available at `/app/cloudfoundryapplication/*`. - -If you expect the Cloud Foundry endpoints to always be available at `/cloudfoundryapplication/*`, regardless of the server's context-path, you will need to explicitly configure that in your application. -The configuration will differ depending on the web server in use. -For Tomcat, the following configuration can be added: - -[source,java,indent=0] ----- -include::{code-examples}/cloudfoundry/CloudFoundryCustomContextPathExample.java[tag=configuration] ----- - - - -[[production-ready-whats-next]] -== What to Read Next -If you want to explore some of the concepts discussed in this chapter, you can take a look at the actuator {spring-boot-code}/spring-boot-samples[sample applications]. -You also might want to read about graphing tools such as https://graphiteapp.org[Graphite]. - -Otherwise, you can continue on, to read about <> or jump ahead for some in-depth information about Spring Boot's _<>_. diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-cli.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-cli.adoc deleted file mode 100644 index 155b3dfb3e82..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-cli.adoc +++ /dev/null @@ -1,435 +0,0 @@ -[[cli]] -= Spring Boot CLI -include::attributes.adoc[] - -The Spring Boot CLI is a command line tool that you can use if you want to quickly develop a Spring application. -It lets you run Groovy scripts, which means that you have a familiar Java-like syntax without so much boilerplate code. -You can also bootstrap a new project or write your own command for it. - - - -[[cli-installation]] -== Installing the CLI -The Spring Boot CLI (Command-Line Interface) can be installed manually by using SDKMAN! (the SDK Manager) or by using Homebrew or MacPorts if you are an OSX user. -See _<>_ in the "`Getting started`" section for comprehensive installation instructions. - - - -[[cli-using-the-cli]] -== Using the CLI -Once you have installed the CLI, you can run it by typing `spring` and pressing Enter at the command line. -If you run `spring` without any arguments, a simple help screen is displayed, as follows: - -[indent=0,subs="verbatim,quotes,attributes"] ----- - $ spring - usage: spring [--help] [--version] - [] - - Available commands are: - - run [options] [--] [args] - Run a spring groovy script - - _... more command help is shown here_ ----- - -You can type `spring help` to get more details about any of the supported commands, as shown in the following example: - -[indent=0] ----- - $ spring help run - spring run - Run a spring groovy script - - usage: spring run [options] [--] [args] - - Option Description - ------ ----------- - --autoconfigure [Boolean] Add autoconfigure compiler - transformations (default: true) - --classpath, -cp Additional classpath entries - --no-guess-dependencies Do not attempt to guess dependencies - --no-guess-imports Do not attempt to guess imports - -q, --quiet Quiet logging - -v, --verbose Verbose logging of dependency - resolution - --watch Watch the specified file for changes ----- - -The `version` command provides a quick way to check which version of Spring Boot you are using, as follows: - -[indent=0,subs="verbatim,quotes,attributes"] ----- - $ spring version - Spring CLI v{spring-boot-version} ----- - - - -[[cli-run]] -=== Running Applications with the CLI -You can compile and run Groovy source code by using the `run` command. -The Spring Boot CLI is completely self-contained, so you do not need any external Groovy installation. - -The following example shows a "`hello world`" web application written in Groovy: - -.hello.groovy -[source,groovy,indent=0,subs="verbatim,quotes,attributes"] ----- - @RestController - class WebApplication { - - @RequestMapping("/") - String home() { - "Hello World!" - } - - } ----- - -To compile and run the application, type the following command: - -[indent=0,subs="verbatim,quotes,attributes"] ----- - $ spring run hello.groovy ----- - -To pass command-line arguments to the application, use `--` to separate the commands from the "`spring`" command arguments, as shown in the following example: - -[indent=0,subs="verbatim,quotes,attributes"] ----- - $ spring run hello.groovy -- --server.port=9000 ----- - -To set JVM command line arguments, you can use the `JAVA_OPTS` environment variable, as shown in the following example: - -[indent=0,subs="verbatim,quotes,attributes"] ----- - $ JAVA_OPTS=-Xmx1024m spring run hello.groovy ----- - -NOTE: When setting `JAVA_OPTS` on Microsoft Windows, make sure to quote the entire instruction, such as `set "JAVA_OPTS=-Xms256m -Xmx2048m"`. -Doing so ensures the values are properly passed to the process. - - - -[[cli-deduced-grab-annotations]] -==== Deduced "`grab`" Dependencies -Standard Groovy includes a `@Grab` annotation, which lets you declare dependencies on third-party libraries. -This useful technique lets Groovy download jars in the same way as Maven or Gradle would but without requiring you to use a build tool. - -Spring Boot extends this technique further and tries to deduce which libraries to "`grab`" based on your code. -For example, since the `WebApplication` code shown previously uses `@RestController` annotations, Spring Boot grabs "Tomcat" and "Spring MVC". - -The following items are used as "`grab hints`": - -|=== -| Items | Grabs - -| `JdbcTemplate`, `NamedParameterJdbcTemplate`, `DataSource` -| JDBC Application. - -| `@EnableJms` -| JMS Application. - -| `@EnableCaching` -| Caching abstraction. - -| `@Test` -| JUnit. - -| `@EnableRabbit` -| RabbitMQ. - -| extends `Specification` -| Spock test. - -| `@EnableBatchProcessing` -| Spring Batch. - -| `@MessageEndpoint` `@EnableIntegration` -| Spring Integration. - -| `@Controller` `@RestController` `@EnableWebMvc` -| Spring MVC + Embedded Tomcat. - -| `@EnableWebSecurity` -| Spring Security. - -| `@EnableTransactionManagement` -| Spring Transaction Management. -|=== - -TIP: See subclasses of {spring-boot-cli-module-code}/compiler/CompilerAutoConfiguration.java[`CompilerAutoConfiguration`] in the Spring Boot CLI source code to understand exactly how customizations are applied. - - - -[[cli-default-grab-deduced-coordinates]] -==== Deduced "`grab`" Coordinates -Spring Boot extends Groovy's standard `@Grab` support by letting you specify a dependency without a group or version (for example, `@Grab('freemarker')`). -Doing so consults Spring Boot's default dependency metadata to deduce the artifact's group and version. - -NOTE: The default metadata is tied to the version of the CLI that you use. -It changes only when you move to a new version of the CLI, putting you in control of when the versions of your dependencies may change. -A table showing the dependencies and their versions that are included in the default metadata can be found in the <>. - - - -[[cli-default-import-statements]] -==== Default Import Statements -To help reduce the size of your Groovy code, several `import` statements are automatically included. -Notice how the preceding example refers to `@Component`, `@RestController`, and `@RequestMapping` without needing to use fully-qualified names or `import` statements. - -TIP: Many Spring annotations work without using `import` statements. -Try running your application to see what fails before adding imports. - - - -[[cli-automatic-main-method]] -==== Automatic Main Method -Unlike the equivalent Java application, you do not need to include a `public static void main(String[] args)` method with your `Groovy` scripts. -A `SpringApplication` is automatically created, with your compiled code acting as the `source`. - - - -[[cli-default-grab-deduced-coordinates-custom-dependency-management]] -==== Custom Dependency Management -By default, the CLI uses the dependency management declared in `spring-boot-dependencies` when resolving `@Grab` dependencies. -Additional dependency management, which overrides the default dependency management, can be configured by using the `@DependencyManagementBom` annotation. -The annotation's value should specify the coordinates (`groupId:artifactId:version`) of one or more Maven BOMs. - -For example, consider the following declaration: - -[source,groovy,indent=0] ----- - @DependencyManagementBom("com.example.custom-bom:1.0.0") ----- - -The preceding declaration picks up `custom-bom-1.0.0.pom` in a Maven repository under `com/example/custom-versions/1.0.0/`. - -When you specify multiple BOMs, they are applied in the order in which you declare them, as shown in the following example: - -[source,java,indent=0] ----- - @DependencyManagementBom(["com.example.custom-bom:1.0.0", - "com.example.another-bom:1.0.0"]) ----- - -The preceding example indicates that the dependency management in `another-bom` overrides the dependency management in `custom-bom`. - -You can use `@DependencyManagementBom` anywhere that you can use `@Grab`. -However, to ensure consistent ordering of the dependency management, you can use `@DependencyManagementBom` at most once in your application. - - - -[[cli-multiple-source-files]] -=== Applications with Multiple Source Files -You can use "`shell globbing`" with all commands that accept file input. -Doing so lets you use multiple files from a single directory, as shown in the following example: - -[indent=0] ----- - $ spring run *.groovy ----- - - - -[[cli-jar]] -=== Packaging Your Application -You can use the `jar` command to package your application into a self-contained executable jar file, as shown in the following example: - -[indent=0] ----- - $ spring jar my-app.jar *.groovy ----- - -The resulting jar contains the classes produced by compiling the application and all of the application's dependencies so that it can then be run by using `java -jar`. -The jar file also contains entries from the application's classpath. -You can add and remove explicit paths to the jar by using `--include` and `--exclude`. -Both are comma-separated, and both accept prefixes, in the form of "`+`" and "`-`", to signify that they should be removed from the defaults. -The default includes are as follows: - -[indent=0] ----- - public/**, resources/**, static/**, templates/**, META-INF/**, * ----- - -The default excludes are as follows: - -[indent=0] ----- - .*, repository/**, build/**, target/**, **/*.jar, **/*.groovy ----- - -Type `spring help jar` on the command line for more information. - - - -[[cli-init]] -=== Initialize a New Project -The `init` command lets you create a new project by using https://start.spring.io without leaving the shell, as shown in the following example: - -[indent=0] ----- - $ spring init --dependencies=web,data-jpa my-project - Using service at https://start.spring.io - Project extracted to '/Users/developer/example/my-project' ----- - -The preceding example creates a `my-project` directory with a Maven-based project that uses `spring-boot-starter-web` and `spring-boot-starter-data-jpa`. -You can list the capabilities of the service by using the `--list` flag, as shown in the following example: - -[indent=0] ----- - $ spring init --list - ======================================= - Capabilities of https://start.spring.io - ======================================= - - Available dependencies: - ----------------------- - actuator - Actuator: Production ready features to help you monitor and manage your application - ... - web - Web: Support for full-stack web development, including Tomcat and spring-webmvc - websocket - Websocket: Support for WebSocket development - ws - WS: Support for Spring Web Services - - Available project types: - ------------------------ - gradle-build - Gradle Config [format:build, build:gradle] - gradle-project - Gradle Project [format:project, build:gradle] - maven-build - Maven POM [format:build, build:maven] - maven-project - Maven Project [format:project, build:maven] (default) - - ... ----- - -The `init` command supports many options. -See the `help` output for more details. -For instance, the following command creates a Gradle project that uses Java 8 and `war` packaging: - -[indent=0] ----- - $ spring init --build=gradle --java-version=1.8 --dependencies=websocket --packaging=war sample-app.zip - Using service at https://start.spring.io - Content saved to 'sample-app.zip' ----- - - - -[[cli-shell]] -=== Using the Embedded Shell -Spring Boot includes command-line completion scripts for the BASH and zsh shells. -If you do not use either of these shells (perhaps you are a Windows user), you can use the `shell` command to launch an integrated shell, as shown in the following example: - -[indent=0,subs="verbatim,quotes,attributes"] ----- - $ spring shell - *Spring Boot* (v{spring-boot-version}) - Hit TAB to complete. Type \'help' and hit RETURN for help, and \'exit' to quit. ----- - -From inside the embedded shell, you can run other commands directly: - -[indent=0,subs="verbatim,quotes,attributes"] ----- - $ version - Spring CLI v{spring-boot-version} ----- - -The embedded shell supports ANSI color output as well as `tab` completion. -If you need to run a native command, you can use the `!` prefix. -To exit the embedded shell, press `ctrl-c`. - - - -[[cli-install-uninstall]] -=== Adding Extensions to the CLI -You can add extensions to the CLI by using the `install` command. -The command takes one or more sets of artifact coordinates in the format `group:artifact:version`, as shown in the following example: - -[indent=0,subs="verbatim,quotes,attributes"] ----- - $ spring install com.example:spring-boot-cli-extension:1.0.0.RELEASE ----- - -In addition to installing the artifacts identified by the coordinates you supply, all of the artifacts' dependencies are also installed. - -To uninstall a dependency, use the `uninstall` command. -As with the `install` command, it takes one or more sets of artifact coordinates in the format of `group:artifact:version`, as shown in the following example: - -[indent=0,subs="verbatim,quotes,attributes"] ----- - $ spring uninstall com.example:spring-boot-cli-extension:1.0.0.RELEASE ----- - -It uninstalls the artifacts identified by the coordinates you supply and their dependencies. - -To uninstall all additional dependencies, you can use the `--all` option, as shown in the following example: - -[indent=0,subs="verbatim,quotes,attributes"] ----- - $ spring uninstall --all ----- - - - -[[cli-groovy-beans-dsl]] -== Developing Applications with the Groovy Beans DSL -Spring Framework 4.0 has native support for a `beans{}` "`DSL`" (borrowed from https://grails.org/[Grails]), and you can embed bean definitions in your Groovy application scripts by using the same format. -This is sometimes a good way to include external features like middleware declarations, as shown in the following example: - -[source,groovy,indent=0] ----- - @Configuration(proxyBeanMethods = false) - class Application implements CommandLineRunner { - - @Autowired - SharedService service - - @Override - void run(String... args) { - println service.message - } - - } - - import my.company.SharedService - - beans { - service(SharedService) { - message = "Hello World" - } - } ----- - -You can mix class declarations with `beans{}` in the same file as long as they stay at the top level, or, if you prefer, you can put the beans DSL in a separate file. - - - -[[cli-maven-settings]] -== Configuring the CLI with `settings.xml` -The Spring Boot CLI uses Aether, Maven's dependency resolution engine, to resolve dependencies. -The CLI makes use of the Maven configuration found in `~/.m2/settings.xml` to configure Aether. -The following configuration settings are honored by the CLI: - -* Offline -* Mirrors -* Servers -* Proxies -* Profiles -** Activation -** Repositories -* Active profiles - -See https://maven.apache.org/settings.html[Maven's settings documentation] for further information. - - - -[[cli-whats-next]] -== What to Read Next -There are some {spring-boot-code}/spring-boot-project/spring-boot-cli/samples[sample groovy scripts] available from the GitHub repository that you can use to try out the Spring Boot CLI. -There is also extensive Javadoc throughout the {spring-boot-cli-module-code}[source code]. - -If you find that you reach the limit of the CLI tool, you probably want to look at converting your application to a full Gradle or Maven built "`Groovy project`". -The next section covers Spring Boot's "<>", which you can use with Gradle or Maven. diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc deleted file mode 100644 index 962036435435..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ /dev/null @@ -1,7720 +0,0 @@ -[[boot-features]] -= Spring Boot Features -include::attributes.adoc[] - -This section dives into the details of Spring Boot. -Here you can learn about the key features that you may want to use and customize. -If you have not already done so, you might want to read the "<>" and "<>" sections, so that you have a good grounding of the basics. - - - -[[boot-features-spring-application]] -== SpringApplication -The `SpringApplication` class provides a convenient way to bootstrap a Spring application that is started from a `main()` method. -In many situations, you can delegate to the static `SpringApplication.run` method, as shown in the following example: - -[source,java,indent=0] ----- - public static void main(String[] args) { - SpringApplication.run(MySpringConfiguration.class, args); - } ----- - -When your application starts, you should see something similar to the following output: - -[indent=0,subs="attributes"] ----- - . ____ _ __ _ _ - /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ -( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ - \\/ ___)| |_)| | | | | || (_| | ) ) ) ) - ' |____| .__|_| |_|_| |_\__, | / / / / - =========|_|==============|___/=/_/_/_/ - :: Spring Boot :: v{spring-boot-version} - -2019-04-31 13:09:54.117 INFO 56603 --- [ main] o.s.b.s.app.SampleApplication : Starting SampleApplication v0.1.0 on mycomputer with PID 56603 (/apps/myapp.jar started by pwebb) -2019-04-31 13:09:54.166 INFO 56603 --- [ main] ationConfigServletWebServerApplicationContext : Refreshing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@6e5a8246: startup date [Wed Jul 31 00:08:16 PDT 2013]; root of context hierarchy -2019-04-01 13:09:56.912 INFO 41370 --- [ main] .t.TomcatServletWebServerFactory : Server initialized with port: 8080 -2019-04-01 13:09:57.501 INFO 41370 --- [ main] o.s.b.s.app.SampleApplication : Started SampleApplication in 2.992 seconds (JVM running for 3.658) ----- - -By default, `INFO` logging messages are shown, including some relevant startup details, such as the user that launched the application. -If you need a log level other than `INFO`, you can set it, as described in <>. - - - -[[boot-features-startup-failure]] -=== Startup Failure -If your application fails to start, registered `FailureAnalyzers` get a chance to provide a dedicated error message and a concrete action to fix the problem. -For instance, if you start a web application on port `8080` and that port is already in use, you should see something similar to the following message: - -[indent=0] ----- - *************************** - APPLICATION FAILED TO START - *************************** - - Description: - - Embedded servlet container failed to start. Port 8080 was already in use. - - Action: - - Identify and stop the process that's listening on port 8080 or configure this application to listen on another port. ----- - -NOTE: Spring Boot provides numerous `FailureAnalyzer` implementations, and you can <>. - -If no failure analyzers are able to handle the exception, you can still display the full conditions report to better understand what went wrong. -To do so, you need to <> or <> for `org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener`. - -For instance, if you are running your application by using `java -jar`, you can enable the `debug` property as follows: - -[indent=0,subs="attributes"] ----- - $ java -jar myproject-0.0.1-SNAPSHOT.jar --debug ----- - - - -[[boot-features-lazy-initialization]] -=== Lazy Initialization -`SpringApplication` allows an application to be initialized lazily. -When lazy initialization is enabled, beans are created as they are needed rather than during application startup. -As a result, enabling lazy initialization can reduce the time that it takes your application to start. -In a web application, enabling lazy initialization will result in many web-related beans not being initialized until an HTTP request is received. - -A downside of lazy initialization is that it can delay the discovery of a problem with the application. -If a misconfigured bean is initialized lazily, a failure will no longer occur during startup and the problem will only become apparent when the bean is initialized. -Care must also be taken to ensure that the JVM has sufficient memory to accommodate all of the application's beans and not just those that are initialized during startup. -For these reasons, lazy initialization is not enabled by default and it is recommended that fine-tuning of the JVM's heap size is done before enabling lazy initialization. - -Lazy initialization can be enabled programatically using the `lazyInitialization` method on `SpringApplicationBuilder` or the `setLazyInitialization` method on `SpringApplication`. -Alternatively, it can be enabled using the configprop:spring.main.lazy-initialization[] property as shown in the following example: - -[source,properties,indent=0,configprops] ----- - spring.main.lazy-initialization=true ----- - -TIP: If you want to disable lazy initialization for certain beans while using lazy initialization for the rest of the application, you can explicitly set their lazy attribute to false using the `@Lazy(false)` annotation. - - - -[[boot-features-banner]] -=== Customizing the Banner -The banner that is printed on start up can be changed by adding a `banner.txt` file to your classpath or by setting the configprop:spring.banner.location[] property to the location of such a file. -If the file has an encoding other than UTF-8, you can set `spring.banner.charset`. -In addition to a text file, you can also add a `banner.gif`, `banner.jpg`, or `banner.png` image file to your classpath or set the configprop:spring.banner.image.location[] property. -Images are converted into an ASCII art representation and printed above any text banner. - -Inside your `banner.txt` file, you can use any of the following placeholders: - -.Banner variables -|=== -| Variable | Description - -| `${application.version}` -| The version number of your application, as declared in `MANIFEST.MF`. - For example, `Implementation-Version: 1.0` is printed as `1.0`. - -| `${application.formatted-version}` -| The version number of your application, as declared in `MANIFEST.MF` and formatted for display (surrounded with brackets and prefixed with `v`). - For example `(v1.0)`. - -| `${spring-boot.version}` -| The Spring Boot version that you are using. - For example `{spring-boot-version}`. - -| `${spring-boot.formatted-version}` -| The Spring Boot version that you are using, formatted for display (surrounded with brackets and prefixed with `v`). - For example `(v{spring-boot-version})`. - -| `${Ansi.NAME}` (or `${AnsiColor.NAME}`, `${AnsiBackground.NAME}`, `${AnsiStyle.NAME}`) -| Where `NAME` is the name of an ANSI escape code. - See {spring-boot-module-code}/ansi/AnsiPropertySource.java[`AnsiPropertySource`] for details. - -| `${application.title}` -| The title of your application, as declared in `MANIFEST.MF`. - For example `Implementation-Title: MyApp` is printed as `MyApp`. -|=== - -TIP: The `SpringApplication.setBanner(...)` method can be used if you want to generate a banner programmatically. -Use the `org.springframework.boot.Banner` interface and implement your own `printBanner()` method. - -You can also use the configprop:spring.main.banner-mode[] property to determine if the banner has to be printed on `System.out` (`console`), sent to the configured logger (`log`), or not produced at all (`off`). - -The printed banner is registered as a singleton bean under the following name: `springBootBanner`. - - - -[[boot-features-customizing-spring-application]] -=== Customizing SpringApplication -If the `SpringApplication` defaults are not to your taste, you can instead create a local instance and customize it. -For example, to turn off the banner, you could write: - -[source,java,indent=0] ----- - public static void main(String[] args) { - SpringApplication app = new SpringApplication(MySpringConfiguration.class); - app.setBannerMode(Banner.Mode.OFF); - app.run(args); - } ----- - -NOTE: The constructor arguments passed to `SpringApplication` are configuration sources for Spring beans. -In most cases, these are references to `@Configuration` classes, but they could also be references to XML configuration or to packages that should be scanned. - -It is also possible to configure the `SpringApplication` by using an `application.properties` file. -See _<>_ for details. - -For a complete list of the configuration options, see the {spring-boot-module-api}/SpringApplication.html[`SpringApplication` Javadoc]. - - - -[[boot-features-fluent-builder-api]] -=== Fluent Builder API -If you need to build an `ApplicationContext` hierarchy (multiple contexts with a parent/child relationship) or if you prefer using a "`fluent`" builder API, you can use the `SpringApplicationBuilder`. - -The `SpringApplicationBuilder` lets you chain together multiple method calls and includes `parent` and `child` methods that let you create a hierarchy, as shown in the following example: - -[source,java,indent=0] ----- -include::{code-examples}/builder/SpringApplicationBuilderExample.java[tag=hierarchy] ----- - -NOTE: There are some restrictions when creating an `ApplicationContext` hierarchy. -For example, Web components *must* be contained within the child context, and the same `Environment` is used for both parent and child contexts. -See the {spring-boot-module-api}/builder/SpringApplicationBuilder.html[`SpringApplicationBuilder` Javadoc] for full details. - - - -[[boot-features-application-events-and-listeners]] -=== Application Events and Listeners -In addition to the usual Spring Framework events, such as {spring-framework-api}/context/event/ContextRefreshedEvent.html[`ContextRefreshedEvent`], a `SpringApplication` sends some additional application events. - -[NOTE] -==== -Some events are actually triggered before the `ApplicationContext` is created, so you cannot register a listener on those as a `@Bean`. -You can register them with the `SpringApplication.addListeners(...)` method or the `SpringApplicationBuilder.listeners(...)` method. - -If you want those listeners to be registered automatically, regardless of the way the application is created, you can add a `META-INF/spring.factories` file to your project and reference your listener(s) by using the `org.springframework.context.ApplicationListener` key, as shown in the following example: - -[indent=0] ----- - org.springframework.context.ApplicationListener=com.example.project.MyListener ----- - -==== - -Application events are sent in the following order, as your application runs: - -. An `ApplicationStartingEvent` is sent at the start of a run but before any processing, except for the registration of listeners and initializers. -. An `ApplicationEnvironmentPreparedEvent` is sent when the `Environment` to be used in the context is known but before the context is created. -. An `ApplicationContextInitializedEvent` is sent when the `ApplicationContext` is prepared and ApplicationContextInitializers have been called but before any bean definitions are loaded. -. An `ApplicationPreparedEvent` is sent just before the refresh is started but after bean definitions have been loaded. -. An `ApplicationStartedEvent` is sent after the context has been refreshed but before any application and command-line runners have been called. -. An `ApplicationReadyEvent` is sent after any application and command-line runners have been called. - It indicates that the application is ready to service requests. -. An `ApplicationFailedEvent` is sent if there is an exception on startup. - -The above list only includes ``SpringApplicationEvent``s that are tied to a `SpringApplication`. -In addition to these, the following events are also published after `ApplicationPreparedEvent` and before `ApplicationStartedEvent`: - -. A `ContextRefreshedEvent` is sent when an `ApplicationContext` is refreshed. -. A `WebServerInitializedEvent` is sent after the `WebServer` is ready. - `ServletWebServerInitializedEvent` and `ReactiveWebServerInitializedEvent` are the servlet and reactive variants respectively. - -TIP: You often need not use application events, but it can be handy to know that they exist. -Internally, Spring Boot uses events to handle a variety of tasks. - -Application events are sent by using Spring Framework's event publishing mechanism. -Part of this mechanism ensures that an event published to the listeners in a child context is also published to the listeners in any ancestor contexts. -As a result of this, if your application uses a hierarchy of `SpringApplication` instances, a listener may receive multiple instances of the same type of application event. - -To allow your listener to distinguish between an event for its context and an event for a descendant context, it should request that its application context is injected and then compare the injected context with the context of the event. -The context can be injected by implementing `ApplicationContextAware` or, if the listener is a bean, by using `@Autowired`. - - - -[[boot-features-web-environment]] -=== Web Environment -A `SpringApplication` attempts to create the right type of `ApplicationContext` on your behalf. -The algorithm used to determine a `WebApplicationType` is fairly simple: - -* If Spring MVC is present, an `AnnotationConfigServletWebServerApplicationContext` is used -* If Spring MVC is not present and Spring WebFlux is present, an `AnnotationConfigReactiveWebServerApplicationContext` is used -* Otherwise, `AnnotationConfigApplicationContext` is used - -This means that if you are using Spring MVC and the new `WebClient` from Spring WebFlux in the same application, Spring MVC will be used by default. -You can override that easily by calling `setWebApplicationType(WebApplicationType)`. - -It is also possible to take complete control of the `ApplicationContext` type that is used by calling `setApplicationContextClass(...)`. - -TIP: It is often desirable to call `setWebApplicationType(WebApplicationType.NONE)` when using `SpringApplication` within a JUnit test. - - - -[[boot-features-application-arguments]] -=== Accessing Application Arguments -If you need to access the application arguments that were passed to `SpringApplication.run(...)`, you can inject a `org.springframework.boot.ApplicationArguments` bean. -The `ApplicationArguments` interface provides access to both the raw `String[]` arguments as well as parsed `option` and `non-option` arguments, as shown in the following example: - -[source,java,indent=0] ----- - import org.springframework.boot.*; - import org.springframework.beans.factory.annotation.*; - import org.springframework.stereotype.*; - - @Component - public class MyBean { - - @Autowired - public MyBean(ApplicationArguments args) { - boolean debug = args.containsOption("debug"); - List files = args.getNonOptionArgs(); - // if run with "--debug logfile.txt" debug=true, files=["logfile.txt"] - } - - } ----- - -TIP: Spring Boot also registers a `CommandLinePropertySource` with the Spring `Environment`. -This lets you also inject single application arguments by using the `@Value` annotation. - - - -[[boot-features-command-line-runner]] -=== Using the ApplicationRunner or CommandLineRunner -If you need to run some specific code once the `SpringApplication` has started, you can implement the `ApplicationRunner` or `CommandLineRunner` interfaces. -Both interfaces work in the same way and offer a single `run` method, which is called just before `SpringApplication.run(...)` completes. - -The `CommandLineRunner` interfaces provides access to application arguments as a simple string array, whereas the `ApplicationRunner` uses the `ApplicationArguments` interface discussed earlier. -The following example shows a `CommandLineRunner` with a `run` method: - -[source,java,indent=0] ----- - import org.springframework.boot.*; - import org.springframework.stereotype.*; - - @Component - public class MyBean implements CommandLineRunner { - - public void run(String... args) { - // Do something... - } - - } ----- - -If several `CommandLineRunner` or `ApplicationRunner` beans are defined that must be called in a specific order, you can additionally implement the `org.springframework.core.Ordered` interface or use the `org.springframework.core.annotation.Order` annotation. - - - -[[boot-features-application-exit]] -=== Application Exit -Each `SpringApplication` registers a shutdown hook with the JVM to ensure that the `ApplicationContext` closes gracefully on exit. -All the standard Spring lifecycle callbacks (such as the `DisposableBean` interface or the `@PreDestroy` annotation) can be used. - -In addition, beans may implement the `org.springframework.boot.ExitCodeGenerator` interface if they wish to return a specific exit code when `SpringApplication.exit()` is called. -This exit code can then be passed to `System.exit()` to return it as a status code, as shown in the following example: - -[source,java,indent=0] ----- -include::{code-examples}/ExitCodeApplication.java[tag=example] ----- - -Also, the `ExitCodeGenerator` interface may be implemented by exceptions. -When such an exception is encountered, Spring Boot returns the exit code provided by the implemented `getExitCode()` method. - - - -[[boot-features-application-admin]] -=== Admin Features -It is possible to enable admin-related features for the application by specifying the configprop:spring.application.admin.enabled[] property. -This exposes the {spring-boot-module-code}/admin/SpringApplicationAdminMXBean.java[`SpringApplicationAdminMXBean`] on the platform `MBeanServer`. -You could use this feature to administer your Spring Boot application remotely. -This feature could also be useful for any service wrapper implementation. - -TIP: If you want to know on which HTTP port the application is running, get the property with a key of `local.server.port`. - - - -[[boot-features-external-config]] -== Externalized Configuration -Spring Boot lets you externalize your configuration so that you can work with the same application code in different environments. -You can use properties files, YAML files, environment variables, and command-line arguments to externalize configuration. -Property values can be injected directly into your beans by using the `@Value` annotation, accessed through Spring's `Environment` abstraction, or be <> through `@ConfigurationProperties`. - -Spring Boot uses a very particular `PropertySource` order that is designed to allow sensible overriding of values. -Properties are considered in the following order: - -. <> in the `$HOME/.config/spring-boot` folder when devtools is active. -. {spring-framework-api}/test/context/TestPropertySource.html[`@TestPropertySource`] annotations on your tests. -. `properties` attribute on your tests. - Available on {spring-boot-test-module-api}/context/SpringBootTest.html[`@SpringBootTest`] and the <>. -. Command line arguments. -. Properties from `SPRING_APPLICATION_JSON` (inline JSON embedded in an environment variable or system property). -. `ServletConfig` init parameters. -. `ServletContext` init parameters. -. JNDI attributes from `java:comp/env`. -. Java System properties (`System.getProperties()`). -. OS environment variables. -. A `RandomValuePropertySource` that has properties only in `+random.*+`. -. <> outside of your packaged jar (`application-\{profile}.properties` and YAML variants). -. <> packaged inside your jar (`application-\{profile}.properties` and YAML variants). -. Application properties outside of your packaged jar (`application.properties` and YAML variants). -. Application properties packaged inside your jar (`application.properties` and YAML variants). -. {spring-framework-api}/context/annotation/PropertySource.html[`@PropertySource`] annotations on your `@Configuration` classes. -. Default properties (specified by setting `SpringApplication.setDefaultProperties`). - -To provide a concrete example, suppose you develop a `@Component` that uses a `name` property, as shown in the following example: - -[source,java,indent=0] ----- - import org.springframework.stereotype.*; - import org.springframework.beans.factory.annotation.*; - - @Component - public class MyBean { - - @Value("${name}") - private String name; - - // ... - - } ----- - -On your application classpath (for example, inside your jar) you can have an `application.properties` file that provides a sensible default property value for `name`. -When running in a new environment, an `application.properties` file can be provided outside of your jar that overrides the `name`. -For one-off testing, you can launch with a specific command line switch (for example, `java -jar app.jar --name="Spring"`). - -[[boot-features-external-config-application-json]] -[TIP] -==== -The `SPRING_APPLICATION_JSON` properties can be supplied on the command line with an environment variable. -For example, you could use the following line in a UN{asterisk}X shell: - -[indent=0] ----- - $ SPRING_APPLICATION_JSON='{"acme":{"name":"test"}}' java -jar myapp.jar ----- - -In the preceding example, you end up with `acme.name=test` in the Spring `Environment`. -You can also supply the JSON as `spring.application.json` in a System property, as shown in the following example: - -[indent=0] ----- - $ java -Dspring.application.json='{"name":"test"}' -jar myapp.jar ----- - -You can also supply the JSON by using a command line argument, as shown in the following example: - -[indent=0] ----- - $ java -jar myapp.jar --spring.application.json='{"name":"test"}' ----- - -You can also supply the JSON as a JNDI variable, as follows: `java:comp/env/spring.application.json`. -==== - - - -[[boot-features-external-config-random-values]] -=== Configuring Random Values -The `RandomValuePropertySource` is useful for injecting random values (for example, into secrets or test cases). -It can produce integers, longs, uuids, or strings, as shown in the following example: - -[source,properties,indent=0] ----- - my.secret=${random.value} - my.number=${random.int} - my.bignumber=${random.long} - my.uuid=${random.uuid} - my.number.less.than.ten=${random.int(10)} - my.number.in.range=${random.int[1024,65536]} ----- - -The `+random.int*+` syntax is `OPEN value (,max) CLOSE` where the `OPEN,CLOSE` are any character and `value,max` are integers. -If `max` is provided, then `value` is the minimum value and `max` is the maximum value (exclusive). - - - -[[boot-features-external-config-command-line-args]] -=== Accessing Command Line Properties -By default, `SpringApplication` converts any command line option arguments (that is, arguments starting with `--`, such as `--server.port=9000`) to a `property` and adds them to the Spring `Environment`. -As mentioned previously, command line properties always take precedence over other property sources. - -If you do not want command line properties to be added to the `Environment`, you can disable them by using `SpringApplication.setAddCommandLineProperties(false)`. - - - -[[boot-features-external-config-application-property-files]] -=== Application Property Files -`SpringApplication` loads properties from `application.properties` files in the following locations and adds them to the Spring `Environment`: - -. A `/config` subdirectory of the current directory -. The current directory -. A classpath `/config` package -. The classpath root - -The list is ordered by precedence (properties defined in locations higher in the list override those defined in lower locations). - -NOTE: You can also <> as an alternative to '.properties'. - -If you do not like `application.properties` as the configuration file name, you can switch to another file name by specifying a configprop:spring.config.name[] environment property. -You can also refer to an explicit location by using the `spring.config.location` environment property (which is a comma-separated list of directory locations or file paths). -The following example shows how to specify a different file name: - -[indent=0] ----- - $ java -jar myproject.jar --spring.config.name=myproject ----- - -The following example shows how to specify two locations: - -[indent=0] ----- - $ java -jar myproject.jar --spring.config.location=classpath:/default.properties,classpath:/override.properties ----- - -WARNING: `spring.config.name` and `spring.config.location` are used very early to determine which files have to be loaded. -They must be defined as an environment property (typically an OS environment variable, a system property, or a command-line argument). - -If `spring.config.location` contains directories (as opposed to files), they should end in `/` (and, at runtime, be appended with the names generated from `spring.config.name` before being loaded, including profile-specific file names). -Files specified in `spring.config.location` are used as-is, with no support for profile-specific variants, and are overridden by any profile-specific properties. - -Config locations are searched in reverse order. -By default, the configured locations are `classpath:/,classpath:/config/,file:./,file:./config/`. -The resulting search order is the following: - -. `file:./config/` -. `file:./` -. `classpath:/config/` -. `classpath:/` - -When custom config locations are configured by using `spring.config.location`, they replace the default locations. -For example, if `spring.config.location` is configured with the value `classpath:/custom-config/,file:./custom-config/`, the search order becomes the following: - -. `file:./custom-config/` -. `classpath:custom-config/` - -Alternatively, when custom config locations are configured by using `spring.config.additional-location`, they are used in addition to the default locations. -Additional locations are searched before the default locations. -For example, if additional locations of `classpath:/custom-config/,file:./custom-config/` are configured, the search order becomes the following: - -. `file:./custom-config/` -. `classpath:custom-config/` -. `file:./config/` -. `file:./` -. `classpath:/config/` -. `classpath:/` - -This search ordering lets you specify default values in one configuration file and then selectively override those values in another. -You can provide default values for your application in `application.properties` (or whatever other basename you choose with `spring.config.name`) in one of the default locations. -These default values can then be overridden at runtime with a different file located in one of the custom locations. - -NOTE: If you use environment variables rather than system properties, most operating systems disallow period-separated key names, but you can use underscores instead (for example, configprop:spring.config.name[format=envvar] instead of configprop:spring.config.name[]). - -NOTE: If your application runs in a container, then JNDI properties (in `java:comp/env`) or servlet context initialization parameters can be used instead of, or as well as, environment variables or system properties. - - - -[[boot-features-external-config-profile-specific-properties]] -=== Profile-specific Properties -In addition to `application.properties` files, profile-specific properties can also be defined by using the following naming convention: `application-\{profile}.properties`. -The `Environment` has a set of default profiles (by default, `[default]`) that are used if no active profiles are set. -In other words, if no profiles are explicitly activated, then properties from `application-default.properties` are loaded. - -Profile-specific properties are loaded from the same locations as standard `application.properties`, with profile-specific files always overriding the non-specific ones, whether or not the profile-specific files are inside or outside your packaged jar. - -If several profiles are specified, a last-wins strategy applies. -For example, profiles specified by the configprop:spring.profiles.active[] property are added after those configured through the `SpringApplication` API and therefore take precedence. - -NOTE: If you have specified any files in configprop:spring.config.location[], profile-specific variants of those files are not considered. -Use directories in configprop:spring.config.location[] if you want to also use profile-specific properties. - - - -[[boot-features-external-config-placeholders-in-properties]] -=== Placeholders in Properties -The values in `application.properties` are filtered through the existing `Environment` when they are used, so you can refer back to previously defined values (for example, from System properties). - -[source,properties,indent=0] ----- - app.name=MyApp - app.description=${app.name} is a Spring Boot application ----- - -TIP: You can also use this technique to create "`short`" variants of existing Spring Boot properties. -See the _<>_ how-to for details. - - - -[[boot-features-encrypting-properties]] -=== Encrypting Properties -Spring Boot does not provide any built in support for encrypting property values, however, it does provide the hook points necessary to modify values contained in the Spring `Environment`. -The `EnvironmentPostProcessor` interface allows you to manipulate the `Environment` before the application starts. -See <> for details. - -If you're looking for a secure way to store credentials and passwords, the https://cloud.spring.io/spring-cloud-vault/[Spring Cloud Vault] project provides support for storing externalized configuration in https://www.vaultproject.io/[HashiCorp Vault]. - - - -[[boot-features-external-config-yaml]] -=== Using YAML Instead of Properties -https://yaml.org[YAML] is a superset of JSON and, as such, is a convenient format for specifying hierarchical configuration data. -The `SpringApplication` class automatically supports YAML as an alternative to properties whenever you have the https://bitbucket.org/asomov/snakeyaml[SnakeYAML] library on your classpath. - -NOTE: If you use "`Starters`", SnakeYAML is automatically provided by `spring-boot-starter`. - - - -[[boot-features-external-config-loading-yaml]] -==== Loading YAML -Spring Framework provides two convenient classes that can be used to load YAML documents. -The `YamlPropertiesFactoryBean` loads YAML as `Properties` and the `YamlMapFactoryBean` loads YAML as a `Map`. - -For example, consider the following YAML document: - -[source,yaml,indent=0] ----- - environments: - dev: - url: https://dev.example.com - name: Developer Setup - prod: - url: https://another.example.com - name: My Cool App ----- - -The preceding example would be transformed into the following properties: - -[source,properties,indent=0] ----- - environments.dev.url=https://dev.example.com - environments.dev.name=Developer Setup - environments.prod.url=https://another.example.com - environments.prod.name=My Cool App ----- - -YAML lists are represented as property keys with `[index]` dereferencers. -For example, consider the following YAML: - -[source,yaml,indent=0] ----- - my: - servers: - - dev.example.com - - another.example.com ----- - -The preceding example would be transformed into these properties: - -[source,properties,indent=0] ----- - my.servers[0]=dev.example.com - my.servers[1]=another.example.com ----- - -To bind to properties like that by using Spring Boot's `Binder` utilities (which is what `@ConfigurationProperties` does), you need to have a property in the target bean of type `java.util.List` (or `Set`) and you either need to provide a setter or initialize it with a mutable value. -For example, the following example binds to the properties shown previously: - -[source,java,indent=0] ----- - @ConfigurationProperties(prefix="my") - public class Config { - - private List servers = new ArrayList(); - - public List getServers() { - return this.servers; - } - } ----- - - - -[[boot-features-external-config-exposing-yaml-to-spring]] -==== Exposing YAML as Properties in the Spring Environment -The `YamlPropertySourceLoader` class can be used to expose YAML as a `PropertySource` in the Spring `Environment`. -Doing so lets you use the `@Value` annotation with placeholders syntax to access YAML properties. - - - -[[boot-features-external-config-multi-profile-yaml]] -==== Multi-profile YAML Documents -You can specify multiple profile-specific YAML documents in a single file by using a `spring.profiles` key to indicate when the document applies, as shown in the following example: - -[source,yaml,indent=0] ----- - server: - address: 192.168.1.100 - --- - spring: - profiles: development - server: - address: 127.0.0.1 - --- - spring: - profiles: production & eu-central - server: - address: 192.168.1.120 ----- - -In the preceding example, if the `development` profile is active, the configprop:server.address[] property is `127.0.0.1`. -Similarly, if the `production` *and* `eu-central` profiles are active, the configprop:server.address[] property is `192.168.1.120`. -If the `development`, `production` and `eu-central` profiles are *not* enabled, then the value for the property is `192.168.1.100`. - -[NOTE] -==== -`spring.profiles` can therefore contain a simple profile name (for example `production`) or a profile expression. -A profile expression allows for more complicated profile logic to be expressed, for example `production & (eu-central | eu-west)`. -Check the {spring-framework-docs}core.html#beans-definition-profiles-java[reference guide] for more details. -==== - -If none are explicitly active when the application context starts, the default profiles are activated. -So, in the following YAML, we set a value for `spring.security.user.password` that is available *only* in the "default" profile: - -[source,yaml,indent=0] ----- - server: - port: 8000 - --- - spring: - profiles: default - security: - user: - password: weak ----- - -Whereas, in the following example, the password is always set because it is not attached to any profile, and it would have to be explicitly reset in all other profiles as necessary: - -[source,yaml,indent=0] ----- - server: - port: 8000 - spring: - security: - user: - password: weak ----- - -Spring profiles designated by using the `spring.profiles` element may optionally be negated by using the `!` character. -If both negated and non-negated profiles are specified for a single document, at least one non-negated profile must match, and no negated profiles may match. - - - -[[boot-features-external-config-yaml-shortcomings]] -==== YAML Shortcomings -YAML files cannot be loaded by using the `@PropertySource` annotation. -So, in the case that you need to load values that way, you need to use a properties file. - -Using the multi YAML document syntax in profile-specific YAML files can lead to unexpected behavior. -For example, consider the following config in a file: - -.application-dev.yml -[source,yaml,indent=0] ----- - server: - port: 8000 - --- - spring: - profiles: "!test" - security: - user: - password: "secret" ----- - -If you run the application with the argument `--spring.profiles.active=dev` you might expect `security.user.password` to be set to "`secret`", but this is not the case. - -The nested document will be filtered because the main file is named `application-dev.yml`. -It is already considered to be profile-specific, and nested documents will be ignored. - -TIP: We recommend that you don't mix profile-specific YAML files and multiple YAML documents. -Stick to using only one of them. - - - -[[boot-features-external-config-typesafe-configuration-properties]] -=== Type-safe Configuration Properties -Using the `@Value("$\{property}")` annotation to inject configuration properties can sometimes be cumbersome, especially if you are working with multiple properties or your data is hierarchical in nature. -Spring Boot provides an alternative method of working with properties that lets strongly typed beans govern and validate the configuration of your application. - -TIP: See also the <>. - - - -[[boot-features-external-config-java-bean-binding]] -==== JavaBean properties binding -It is possible to bind a bean declaring standard JavaBean properties as shown in the following example: - -[source,java,indent=0] ----- - package com.example; - - import java.net.InetAddress; - import java.util.ArrayList; - import java.util.Collections; - import java.util.List; - - import org.springframework.boot.context.properties.ConfigurationProperties; - - @ConfigurationProperties("acme") - public class AcmeProperties { - - private boolean enabled; - - private InetAddress remoteAddress; - - private final Security security = new Security(); - - public boolean isEnabled() { ... } - - public void setEnabled(boolean enabled) { ... } - - public InetAddress getRemoteAddress() { ... } - - public void setRemoteAddress(InetAddress remoteAddress) { ... } - - public Security getSecurity() { ... } - - public static class Security { - - private String username; - - private String password; - - private List roles = new ArrayList<>(Collections.singleton("USER")); - - public String getUsername() { ... } - - public void setUsername(String username) { ... } - - public String getPassword() { ... } - - public void setPassword(String password) { ... } - - public List getRoles() { ... } - - public void setRoles(List roles) { ... } - - } - } ----- - -The preceding POJO defines the following properties: - -* `acme.enabled`, with a value of `false` by default. -* `acme.remote-address`, with a type that can be coerced from `String`. -* `acme.security.username`, with a nested "security" object whose name is determined by the name of the property. - In particular, the return type is not used at all there and could have been `SecurityProperties`. -* `acme.security.password`. -* `acme.security.roles`, with a collection of `String` that defaults to `USER`. - -NOTE: Spring Boot auto-configuration heavily makes use of `@ConfigurationProperties` for easily configuring auto-configured beans. -Similar to auto-configuration classes, `@ConfigurationProperties` classes available in Spring Boot are for internal use only. -The properties that map to the class, which are configured via properties files, YAML files, environment variables etc., are public API but the content of the class itself is not meant to be used directly. - -[NOTE] -==== -Such arrangement relies on a default empty constructor and getters and setters are usually mandatory, since binding is through standard Java Beans property descriptors, just like in Spring MVC. -A setter may be omitted in the following cases: - -* Maps, as long as they are initialized, need a getter but not necessarily a setter, since they can be mutated by the binder. -* Collections and arrays can be accessed either through an index (typically with YAML) or by using a single comma-separated value (properties). - In the latter case, a setter is mandatory. - We recommend to always add a setter for such types. - If you initialize a collection, make sure it is not immutable (as in the preceding example). -* If nested POJO properties are initialized (like the `Security` field in the preceding example), a setter is not required. - If you want the binder to create the instance on the fly by using its default constructor, you need a setter. - -Some people use Project Lombok to add getters and setters automatically. -Make sure that Lombok does not generate any particular constructor for such a type, as it is used automatically by the container to instantiate the object. - -Finally, only standard Java Bean properties are considered and binding on static properties is not supported. -==== - - - -[[boot-features-external-config-constructor-binding]] -==== Constructor binding -The example in the previous section can be rewritten in an immutable fashion as shown in the following example: - -[source,java,indent=0] ----- - package com.example; - - import java.net.InetAddress; - import java.util.List; - - import org.springframework.boot.context.properties.ConfigurationProperties; - import org.springframework.boot.context.properties.ConstructorBinding; - import org.springframework.boot.context.properties.DefaultValue; - - @ConstructorBinding - @ConfigurationProperties("acme") - public class AcmeProperties { - - private final boolean enabled; - - private final InetAddress remoteAddress; - - private final Security security; - - public AcmeProperties(boolean enabled, InetAddress remoteAddress, Security security) { - this.enabled = enabled; - this.remoteAddress = remoteAddress; - this.security = security; - } - - public boolean isEnabled() { ... } - - public InetAddress getRemoteAddress() { ... } - - public Security getSecurity() { ... } - - public static class Security { - - private final String username; - - private final String password; - - private final List roles; - - public Security(String username, String password, - @DefaultValue("USER") List roles) { - this.username = username; - this.password = password; - this.roles = roles; - } - - public String getUsername() { ... } - - public String getPassword() { ... } - - public List getRoles() { ... } - - } - - } ----- - -In this setup, the `@ConstructorBinding` annotation is used to indicate that constructor binding should be used. -This means that the binder will expect to find a constructor with the parameters that you wish to have bound. - -Nested members of a `@ConstructorBinding` class (such as `Security` in the example above) will also be bound via their constructor. - -Default values can be specified using `@DefaultValue` and the same conversion service will be applied to coerce the `String` value to the target type of a missing property. - -NOTE: To use constructor binding the class must be enabled using `@EnableConfigurationProperties` or configuration property scanning. -You cannot use constructor binding with beans that are created by the regular Spring mechanisms (e.g. `@Component` beans, beans created via `@Bean` methods or beans loaded using `@Import`) - -TIP: If you have more than one constructor for your class you can also use `@ConstructorBinding` directly on the constructor that should be bound. - - - -[[boot-features-external-config-enabling]] -==== Enabling `@ConfigurationProperties`-annotated types -Spring Boot provides infrastructure to bind `@ConfigurationProperties` types and register them as beans. -You can either enable configuration properties on a class-by-class basis or enable configuration property scanning that works in a similar manner to component scanning. - -Sometimes, classes annotated with `@ConfigurationProperties` might not be suitable for scanning, for example, if you're developing your own auto-configuration or you want to enable them conditionally. -In these cases, specify the list of types to process using the `@EnableConfigurationProperties` annotation. -This can be done on any `@Configuration` class, as shown in the following example: - -[source,java,indent=0] ----- - @Configuration(proxyBeanMethods = false) - @EnableConfigurationProperties(AcmeProperties.class) - public class MyConfiguration { - } ----- - -To use configuration property scanning, add the `@ConfigurationPropertiesScan` annotation to your application. -Typically, it is added to the main application class that is annotated with `@SpringBootApplication` but it can be added to any `@Configuration` class. -By default, scanning will occur from the package of the class that declares the annotation. -If you want to define specific packages to scan, you can do so as shown in the following example: - -[source,java,indent=0] ----- - @SpringBootApplication - @ConfigurationPropertiesScan({ "com.example.app", "org.acme.another" }) - public class MyApplication { - } ----- - -[NOTE] -==== -When the `@ConfigurationProperties` bean is registered using configuration property scanning or via `@EnableConfigurationProperties`, the bean has a conventional name: `-`, where `` is the environment key prefix specified in the `@ConfigurationProperties` annotation and `` is the fully qualified name of the bean. -If the annotation does not provide any prefix, only the fully qualified name of the bean is used. - -The bean name in the example above is `acme-com.example.AcmeProperties`. -==== - -We recommend that `@ConfigurationProperties` only deal with the environment and, in particular, does not inject other beans from the context. -For corner cases, setter injection can be used or any of the `*Aware` interfaces provided by the framework (such as `EnvironmentAware` if you need access to the `Environment`). -If you still want to inject other beans using the constructor, the configuration properties bean must be annotated with `@Component` and use JavaBean-based property binding. - - - -[[boot-features-external-config-using]] -==== Using `@ConfigurationProperties`-annotated types -This style of configuration works particularly well with the `SpringApplication` external YAML configuration, as shown in the following example: - -[source,yaml,indent=0] ----- - # application.yml - - acme: - remote-address: 192.168.1.1 - security: - username: admin - roles: - - USER - - ADMIN - - # additional configuration as required ----- - -To work with `@ConfigurationProperties` beans, you can inject them in the same way as any other bean, as shown in the following example: - -[source,java,indent=0] ----- - @Service - public class MyService { - - private final AcmeProperties properties; - - @Autowired - public MyService(AcmeProperties properties) { - this.properties = properties; - } - - //... - - @PostConstruct - public void openConnection() { - Server server = new Server(this.properties.getRemoteAddress()); - // ... - } - - } ----- - -TIP: Using `@ConfigurationProperties` also lets you generate metadata files that can be used by IDEs to offer auto-completion for your own keys. -See the <> for details. - - - -[[boot-features-external-config-3rd-party-configuration]] -==== Third-party Configuration -As well as using `@ConfigurationProperties` to annotate a class, you can also use it on public `@Bean` methods. -Doing so can be particularly useful when you want to bind properties to third-party components that are outside of your control. - -To configure a bean from the `Environment` properties, add `@ConfigurationProperties` to its bean registration, as shown in the following example: - -[source,java,indent=0] ----- - @ConfigurationProperties(prefix = "another") - @Bean - public AnotherComponent anotherComponent() { - ... - } ----- - -Any JavaBean property defined with the `another` prefix is mapped onto that `AnotherComponent` bean in manner similar to the preceding `AcmeProperties` example. - - - -[[boot-features-external-config-relaxed-binding]] -==== Relaxed Binding -Spring Boot uses some relaxed rules for binding `Environment` properties to `@ConfigurationProperties` beans, so there does not need to be an exact match between the `Environment` property name and the bean property name. -Common examples where this is useful include dash-separated environment properties (for example, `context-path` binds to `contextPath`), and capitalized environment properties (for example, `PORT` binds to `port`). - -As an example, consider the following `@ConfigurationProperties` class: - -[source,java,indent=0] ----- - @ConfigurationProperties(prefix="acme.my-project.person") - public class OwnerProperties { - - private String firstName; - - public String getFirstName() { - return this.firstName; - } - - public void setFirstName(String firstName) { - this.firstName = firstName; - } - - } ----- - -With the preceding code, the following properties names can all be used: - -.relaxed binding -[cols="1,4"] -|=== -| Property | Note - -| `acme.my-project.person.first-name` -| Kebab case, which is recommended for use in `.properties` and `.yml` files. - -| `acme.myProject.person.firstName` -| Standard camel case syntax. - -| `acme.my_project.person.first_name` -| Underscore notation, which is an alternative format for use in `.properties` and `.yml` files. - -| `ACME_MYPROJECT_PERSON_FIRSTNAME` -| Upper case format, which is recommended when using system environment variables. -|=== - -NOTE: The `prefix` value for the annotation _must_ be in kebab case (lowercase and separated by `-`, such as `acme.my-project.person`). - -.relaxed binding rules per property source -[cols="2,4,4"] -|=== -| Property Source | Simple | List - -| Properties Files -| Camel case, kebab case, or underscore notation -| Standard list syntax using `[ ]` or comma-separated values - -| YAML Files -| Camel case, kebab case, or underscore notation -| Standard YAML list syntax or comma-separated values - -| Environment Variables -| Upper case format with underscore as the delimiter. - `_` should not be used within a property name -| Numeric values surrounded by underscores, such as `MY_ACME_1_OTHER = my.acme[1].other` - -| System properties -| Camel case, kebab case, or underscore notation -| Standard list syntax using `[ ]` or comma-separated values -|=== - -TIP: We recommend that, when possible, properties are stored in lower-case kebab format, such as `my.property-name=acme`. - -When binding to `Map` properties, if the `key` contains anything other than lowercase alpha-numeric characters or `-`, you need to use the bracket notation so that the original value is preserved. -If the key is not surrounded by `[]`, any characters that are not alpha-numeric or `-` are removed. -For example, consider binding the following properties to a `Map`: - -[source,yaml,indent=0] ----- - acme: - map: - "[/key1]": value1 - "[/key2]": value2 - /key3: value3 - ----- - -The properties above will bind to a `Map` with `/key1`, `/key2` and `key3` as the keys in the map. - -NOTE: For YAML files, the brackets need to be surrounded by quotes for the keys to be parsed properly. - - -[[boot-features-external-config-complex-type-merge]] -==== Merging Complex Types -When lists are configured in more than one place, overriding works by replacing the entire list. - -For example, assume a `MyPojo` object with `name` and `description` attributes that are `null` by default. -The following example exposes a list of `MyPojo` objects from `AcmeProperties`: - -[source,java,indent=0] ----- - @ConfigurationProperties("acme") - public class AcmeProperties { - - private final List list = new ArrayList<>(); - - public List getList() { - return this.list; - } - - } ----- - -Consider the following configuration: - -[source,yaml,indent=0] ----- - acme: - list: - - name: my name - description: my description - --- - spring: - profiles: dev - acme: - list: - - name: my another name ----- - -If the `dev` profile is not active, `AcmeProperties.list` contains one `MyPojo` entry, as previously defined. -If the `dev` profile is enabled, however, the `list` _still_ contains only one entry (with a name of `my another name` and a description of `null`). -This configuration _does not_ add a second `MyPojo` instance to the list, and it does not merge the items. - -When a `List` is specified in multiple profiles, the one with the highest priority (and only that one) is used. -Consider the following example: - -[source,yaml,indent=0] ----- - acme: - list: - - name: my name - description: my description - - name: another name - description: another description - --- - spring: - profiles: dev - acme: - list: - - name: my another name ----- - -In the preceding example, if the `dev` profile is active, `AcmeProperties.list` contains _one_ `MyPojo` entry (with a name of `my another name` and a description of `null`). -For YAML, both comma-separated lists and YAML lists can be used for completely overriding the contents of the list. - -For `Map` properties, you can bind with property values drawn from multiple sources. -However, for the same property in multiple sources, the one with the highest priority is used. -The following example exposes a `Map` from `AcmeProperties`: - -[source,java,indent=0] ----- - @ConfigurationProperties("acme") - public class AcmeProperties { - - private final Map map = new HashMap<>(); - - public Map getMap() { - return this.map; - } - - } ----- - -Consider the following configuration: - -[source,yaml,indent=0] ----- - acme: - map: - key1: - name: my name 1 - description: my description 1 - --- - spring: - profiles: dev - acme: - map: - key1: - name: dev name 1 - key2: - name: dev name 2 - description: dev description 2 ----- - -If the `dev` profile is not active, `AcmeProperties.map` contains one entry with key `key1` (with a name of `my name 1` and a description of `my description 1`). -If the `dev` profile is enabled, however, `map` contains two entries with keys `key1` (with a name of `dev name 1` and a description of `my description 1`) and `key2` (with a name of `dev name 2` and a description of `dev description 2`). - -NOTE: The preceding merging rules apply to properties from all property sources and not just YAML files. - - - -[[boot-features-external-config-conversion]] -==== Properties Conversion -Spring Boot attempts to coerce the external application properties to the right type when it binds to the `@ConfigurationProperties` beans. -If you need custom type conversion, you can provide a `ConversionService` bean (with a bean named `conversionService`) or custom property editors (through a `CustomEditorConfigurer` bean) or custom `Converters` (with bean definitions annotated as `@ConfigurationPropertiesBinding`). - -NOTE: As this bean is requested very early during the application lifecycle, make sure to limit the dependencies that your `ConversionService` is using. -Typically, any dependency that you require may not be fully initialized at creation time. -You may want to rename your custom `ConversionService` if it is not required for configuration keys coercion and only rely on custom converters qualified with `@ConfigurationPropertiesBinding`. - - - -[[boot-features-external-config-conversion-duration]] -===== Converting durations -Spring Boot has dedicated support for expressing durations. -If you expose a `java.time.Duration` property, the following formats in application properties are available: - -* A regular `long` representation (using milliseconds as the default unit unless a `@DurationUnit` has been specified) -* The standard ISO-8601 format {java-api}/java/time/Duration.html#parse-java.lang.CharSequence-[used by `java.time.Duration`] -* A more readable format where the value and the unit are coupled (e.g. `10s` means 10 seconds) - -Consider the following example: - -[source,java,indent=0] ----- -include::{code-examples}/context/properties/bind/AppSystemProperties.java[tag=example] ----- - -To specify a session timeout of 30 seconds, `30`, `PT30S` and `30s` are all equivalent. -A read timeout of 500ms can be specified in any of the following form: `500`, `PT0.5S` and `500ms`. - -You can also use any of the supported units. -These are: - -* `ns` for nanoseconds -* `us` for microseconds -* `ms` for milliseconds -* `s` for seconds -* `m` for minutes -* `h` for hours -* `d` for days - -The default unit is milliseconds and can be overridden using `@DurationUnit` as illustrated in the sample above. - -TIP: If you are upgrading from a previous version that is simply using `Long` to express the duration, make sure to define the unit (using `@DurationUnit`) if it isn't milliseconds alongside the switch to `Duration`. -Doing so gives a transparent upgrade path while supporting a much richer format. - - - -[[boot-features-external-config-conversion-datasize]] -===== Converting Data Sizes -Spring Framework has a `DataSize` value type that expresses a size in bytes. -If you expose a `DataSize` property, the following formats in application properties are available: - -* A regular `long` representation (using bytes as the default unit unless a `@DataSizeUnit` has been specified) -* A more readable format where the value and the unit are coupled (e.g. `10MB` means 10 megabytes) - -Consider the following example: - -[source,java,indent=0] ----- -include::{code-examples}/context/properties/bind/AppIoProperties.java[tag=example] ----- - -To specify a buffer size of 10 megabytes, `10` and `10MB` are equivalent. -A size threshold of 256 bytes can be specified as `256` or `256B`. - -You can also use any of the supported units. -These are: - -* `B` for bytes -* `KB` for kilobytes -* `MB` for megabytes -* `GB` for gigabytes -* `TB` for terabytes - -The default unit is bytes and can be overridden using `@DataSizeUnit` as illustrated in the sample above. - -TIP: If you are upgrading from a previous version that is simply using `Long` to express the size, make sure to define the unit (using `@DataSizeUnit`) if it isn't bytes alongsidethe switch to `DataSize`. -Doing so gives a transparent upgrade path while supporting a much richer format. - - - -[[boot-features-external-config-validation]] -==== @ConfigurationProperties Validation -Spring Boot attempts to validate `@ConfigurationProperties` classes whenever they are annotated with Spring's `@Validated` annotation. -You can use JSR-303 `javax.validation` constraint annotations directly on your configuration class. -To do so, ensure that a compliant JSR-303 implementation is on your classpath and then add constraint annotations to your fields, as shown in the following example: - -[source,java,indent=0] ----- - @ConfigurationProperties(prefix="acme") - @Validated - public class AcmeProperties { - - @NotNull - private InetAddress remoteAddress; - - // ... getters and setters - - } ----- - -TIP: You can also trigger validation by annotating the `@Bean` method that creates the configuration properties with `@Validated`. - -To ensure that validation is always triggered for nested properties, even when no properties are found, the associated field must be annotated with `@Valid`. -The following example builds on the preceding `AcmeProperties` example: - -[source,java,indent=0] ----- - @ConfigurationProperties(prefix="acme") - @Validated - public class AcmeProperties { - - @NotNull - private InetAddress remoteAddress; - - @Valid - private final Security security = new Security(); - - // ... getters and setters - - public static class Security { - - @NotEmpty - public String username; - - // ... getters and setters - - } - - } ----- - -You can also add a custom Spring `Validator` by creating a bean definition called `configurationPropertiesValidator`. -The `@Bean` method should be declared `static`. -The configuration properties validator is created very early in the application's lifecycle, and declaring the `@Bean` method as static lets the bean be created without having to instantiate the `@Configuration` class. -Doing so avoids any problems that may be caused by early instantiation. - -TIP: The `spring-boot-actuator` module includes an endpoint that exposes all `@ConfigurationProperties` beans. -Point your web browser to `/actuator/configprops` or use the equivalent JMX endpoint. -See the "<>" section for details. - - - -[[boot-features-external-config-vs-value]] -==== @ConfigurationProperties vs. @Value -The `@Value` annotation is a core container feature, and it does not provide the same features as type-safe configuration properties. -The following table summarizes the features that are supported by `@ConfigurationProperties` and `@Value`: - -[cols="4,2,2"] -|=== -| Feature |`@ConfigurationProperties` |`@Value` - -| <> -| Yes -| No - -| <> -| Yes -| No - -| `SpEL` evaluation -| No -| Yes -|=== - -If you define a set of configuration keys for your own components, we recommend you group them in a POJO annotated with `@ConfigurationProperties`. -You should also be aware that, since `@Value` does not support relaxed binding, it is not a good candidate if you need to provide the value by using environment variables. - -Finally, while you can write a `SpEL` expression in `@Value`, such expressions are not processed from <>. - - - -[[boot-features-profiles]] -== Profiles -Spring Profiles provide a way to segregate parts of your application configuration and make it be available only in certain environments. -Any `@Component`, `@Configuration` or `@ConfigurationProperties` can be marked with `@Profile` to limit when it is loaded, as shown in the following example: - -[source,java,indent=0] ----- - @Configuration(proxyBeanMethods = false) - @Profile("production") - public class ProductionConfiguration { - - // ... - - } ----- - -NOTE: If `@ConfigurationProperties` beans are registered via `@EnableConfigurationProperties` instead of automatic scanning, the `@Profile` annotation needs to be specified on the `@Configuration` class that has the `@EnableConfigurationProperties` annotation. -In the case where `@ConfigurationProperties` are scanned, `@Profile` can be specified on the `@ConfigurationProperties` class itself. - -You can use a configprop:spring.profiles.active[] `Environment` property to specify which profiles are active. -You can specify the property in any of the ways described earlier in this chapter. -For example, you could include it in your `application.properties`, as shown in the following example: - -[source,properties,indent=0,configprops] ----- - spring.profiles.active=dev,hsqldb ----- - -You could also specify it on the command line by using the following switch: `--spring.profiles.active=dev,hsqldb`. - - - -[[boot-features-adding-active-profiles]] -=== Adding Active Profiles -The configprop:spring.profiles.active[] property follows the same ordering rules as other properties: The highest `PropertySource` wins. -This means that you can specify active profiles in `application.properties` and then *replace* them by using the command line switch. - -Sometimes, it is useful to have profil-specific properties that *add* to the active profiles rather than replace them. -The configprop:spring.profiles.include[] property can be used to unconditionally add active profiles. -The `SpringApplication` entry point also has a Java API for setting additional profiles (that is, on top of those activated by the configprop:spring.profiles.active[] property). -See the `setAdditionalProfiles()` method in {spring-boot-module-api}/SpringApplication.html[SpringApplication]. - -For example, when an application with the following properties is run by using the switch, `--spring.profiles.active=prod`, the `proddb` and `prodmq` profiles are also activated: - -[source,yaml,indent=0] ----- - --- - my.property: fromyamlfile - --- - spring.profiles: prod - spring.profiles.include: - - proddb - - prodmq ----- - -NOTE: Remember that the `spring.profiles` property can be defined in a YAML document to determine when this particular document is included in the configuration. -See <> for more details. - - - -[[boot-features-programmatically-setting-profiles]] -=== Programmatically Setting Profiles -You can programmatically set active profiles by calling `SpringApplication.setAdditionalProfiles(...)` before your application runs. -It is also possible to activate profiles by using Spring's `ConfigurableEnvironment` interface. - - - -[[boot-features-profile-specific-configuration]] -=== Profile-specific Configuration Files -Profile-specific variants of both `application.properties` (or `application.yml`) and files referenced through `@ConfigurationProperties` are considered as files and loaded. -See "<>" for details. - - - -[[boot-features-logging]] -== Logging -Spring Boot uses https://commons.apache.org/logging[Commons Logging] for all internal logging but leaves the underlying log implementation open. -Default configurations are provided for {java-api}/java/util/logging/package-summary.html[Java Util Logging], https://logging.apache.org/log4j/2.x/[Log4J2], and https://logback.qos.ch/[Logback]. -In each case, loggers are pre-configured to use console output with optional file output also available. - -By default, if you use the "`Starters`", Logback is used for logging. -Appropriate Logback routing is also included to ensure that dependent libraries that use Java Util Logging, Commons Logging, Log4J, or SLF4J all work correctly. - -TIP: There are a lot of logging frameworks available for Java. -Do not worry if the above list seems confusing. -Generally, you do not need to change your logging dependencies and the Spring Boot defaults work just fine. - -TIP: When you deploy your application to a servlet container or application server, logging performed via the Java Util Logging API is not routed into your application's logs. -This prevents logging performed by the container or other applications that have been deployed to it from appearing in your application's logs. - - -[[boot-features-logging-format]] -=== Log Format -The default log output from Spring Boot resembles the following example: - -[indent=0] ----- -2019-03-05 10:57:51.112 INFO 45469 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/7.0.52 -2019-03-05 10:57:51.253 INFO 45469 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext -2019-03-05 10:57:51.253 INFO 45469 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1358 ms -2019-03-05 10:57:51.698 INFO 45469 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean : Mapping servlet: 'dispatcherServlet' to [/] -2019-03-05 10:57:51.702 INFO 45469 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*] ----- - -The following items are output: - -* Date and Time: Millisecond precision and easily sortable. -* Log Level: `ERROR`, `WARN`, `INFO`, `DEBUG`, or `TRACE`. -* Process ID. -* A `---` separator to distinguish the start of actual log messages. -* Thread name: Enclosed in square brackets (may be truncated for console output). -* Logger name: This is usually the source class name (often abbreviated). -* The log message. - -NOTE: Logback does not have a `FATAL` level. -It is mapped to `ERROR`. - - - -[[boot-features-logging-console-output]] -=== Console Output -The default log configuration echoes messages to the console as they are written. -By default, `ERROR`-level, `WARN`-level, and `INFO`-level messages are logged. -You can also enable a "`debug`" mode by starting your application with a `--debug` flag. - -[indent=0] ----- - $ java -jar myapp.jar --debug ----- - -NOTE: You can also specify `debug=true` in your `application.properties`. - -When the debug mode is enabled, a selection of core loggers (embedded container, Hibernate, and Spring Boot) are configured to output more information. -Enabling the debug mode does _not_ configure your application to log all messages with `DEBUG` level. - -Alternatively, you can enable a "`trace`" mode by starting your application with a `--trace` flag (or `trace=true` in your `application.properties`). -Doing so enables trace logging for a selection of core loggers (embedded container, Hibernate schema generation, and the whole Spring portfolio). - - - -[[boot-features-logging-color-coded-output]] -==== Color-coded Output -If your terminal supports ANSI, color output is used to aid readability. -You can set `spring.output.ansi.enabled` to a {spring-boot-module-api}/ansi/AnsiOutput.Enabled.html[supported value] to override the auto-detection. - -Color coding is configured by using the `%clr` conversion word. -In its simplest form, the converter colors the output according to the log level, as shown in the following example: - -[source,indent=0] ----- -%clr(%5p) ----- - -The following table describes the mapping of log levels to colors: - -|=== -| Level | Color - -| `FATAL` -| Red - -| `ERROR` -| Red - -| `WARN` -| Yellow - -| `INFO` -| Green - -| `DEBUG` -| Green - -| `TRACE` -| Green -|=== - -Alternatively, you can specify the color or style that should be used by providing it as an option to the conversion. -For example, to make the text yellow, use the following setting: - -[source,indent=0] ----- -%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){yellow} ----- - -The following colors and styles are supported: - -* `blue` -* `cyan` -* `faint` -* `green` -* `magenta` -* `red` -* `yellow` - - - -[[boot-features-logging-file-output]] -=== File Output -By default, Spring Boot logs only to the console and does not write log files. -If you want to write log files in addition to the console output, you need to set a configprop:logging.file.name[] or configprop:logging.file.path[] property (for example, in your `application.properties`). - -The following table shows how the `logging.*` properties can be used together: - -.Logging properties -[cols="1,1,1,4"] -|=== -| configprop:logging.file.name[] | configprop:logging.file.path[] | Example | Description - -| _(none)_ -| _(none)_ -| -| Console only logging. - -| Specific file -| _(none)_ -| `my.log` -| Writes to the specified log file. - Names can be an exact location or relative to the current directory. - -| _(none)_ -| Specific directory -| `/var/log` -| Writes `spring.log` to the specified directory. - Names can be an exact location or relative to the current directory. -|=== - -Log files rotate when they reach 10 MB and, as with console output, `ERROR`-level, `WARN`-level, and `INFO`-level messages are logged by default. -Size limits can be changed using the configprop:logging.file.max-size[] property. -Previously rotated files are archived indefinitely unless the configprop:logging.file.max-history[] property has been set. -The total size of log archives can be capped using configprop:logging.file.total-size-cap[]. -When the total size of log archives exceeds that threshold, backups will be deleted. -To force log archive cleanup on application startup, use the configprop:logging.file.clean-history-on-start[] property. - -TIP: Logging properties are independent of the actual logging infrastructure. -As a result, specific configuration keys (such as `logback.configurationFile` for Logback) are not managed by spring Boot. - - - -[[boot-features-custom-log-levels]] -=== Log Levels -All the supported logging systems can have the logger levels set in the Spring `Environment` (for example, in `application.properties`) by using `+logging.level.=+` where `level` is one of TRACE, DEBUG, INFO, WARN, ERROR, FATAL, or OFF. -The `root` logger can be configured by using `logging.level.root`. - -The following example shows potential logging settings in `application.properties`: - -[source,properties,indent=0,subs="verbatim,quotes,attributes",configprops] ----- - logging.level.root=warn - logging.level.org.springframework.web=debug - logging.level.org.hibernate=error ----- - -It's also possible to set logging levels using environment variables. -For example, `LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_WEB=DEBUG` will set `org.springframework.web` to `DEBUG`. - -NOTE: The above approach will only work for package level logging. -Since relaxed binding always converts environment variables to lowercase, it's not possible to configure logging for an individual class in this way. -If you need to configure logging for a class, you can use <> variable. - - - -[[boot-features-custom-log-groups]] -=== Log Groups -It's often useful to be able to group related loggers together so that they can all be configured at the same time. -For example, you might commonly change the logging levels for _all_ Tomcat related loggers, but you can't easily remember top level packages. - -To help with this, Spring Boot allows you to define logging groups in your Spring `Environment`. -For example, here's how you could define a "`tomcat`" group by adding it to your `application.properties`: - -[source,properties,indent=0,subs="verbatim,quotes,attributes",configprops] ----- - logging.group.tomcat=org.apache.catalina, org.apache.coyote, org.apache.tomcat ----- - -Once defined, you can change the level for all the loggers in the group with a single line: - -[source,properties,indent=0,subs="verbatim,quotes,attributes",configprops] ----- - logging.level.tomcat=TRACE ----- - -Spring Boot includes the following pre-defined logging groups that can be used out-of-the-box: - -[cols="1,4"] -|=== -| Name | Loggers - -| web -| `org.springframework.core.codec`, `org.springframework.http`, `org.springframework.web`, `org.springframework.boot.actuate.endpoint.web`, `org.springframework.boot.web.servlet.ServletContextInitializerBeans` - -| sql -| `org.springframework.jdbc.core`, `org.hibernate.SQL`, `org.jooq.tools.LoggerListener` -|=== - - - -[[boot-features-custom-log-configuration]] -=== Custom Log Configuration -The various logging systems can be activated by including the appropriate libraries on the classpath and can be further customized by providing a suitable configuration file in the root of the classpath or in a location specified by the following Spring `Environment` property: configprop:logging.config[]. - -You can force Spring Boot to use a particular logging system by using the `org.springframework.boot.logging.LoggingSystem` system property. -The value should be the fully qualified class name of a `LoggingSystem` implementation. -You can also disable Spring Boot's logging configuration entirely by using a value of `none`. - -NOTE: Since logging is initialized *before* the `ApplicationContext` is created, it is not possible to control logging from `@PropertySources` in Spring `@Configuration` files. -The only way to change the logging system or disable it entirely is via System properties. - -Depending on your logging system, the following files are loaded: - -|=== -| Logging System | Customization - -| Logback -| `logback-spring.xml`, `logback-spring.groovy`, `logback.xml`, or `logback.groovy` - -| Log4j2 -| `log4j2-spring.xml` or `log4j2.xml` - -| JDK (Java Util Logging) -| `logging.properties` -|=== - -NOTE: When possible, we recommend that you use the `-spring` variants for your logging configuration (for example, `logback-spring.xml` rather than `logback.xml`). -If you use standard configuration locations, Spring cannot completely control log initialization. - -WARNING: There are known classloading issues with Java Util Logging that cause problems when running from an 'executable jar'. -We recommend that you avoid it when running from an 'executable jar' if at all possible. - -To help with the customization, some other properties are transferred from the Spring `Environment` to System properties, as described in the following table: - -|=== -| Spring Environment | System Property | Comments - -| configprop:logging.exception-conversion-word[] -| `LOG_EXCEPTION_CONVERSION_WORD` -| The conversion word used when logging exceptions. - -| configprop:logging.file.clean-history-on-start[] -| `LOG_FILE_CLEAN_HISTORY_ON_START` -| Whether to clean the archive log files on startup (if LOG_FILE enabled). - (Only supported with the default Logback setup.) - -| configprop:logging.file.name[] -| `LOG_FILE` -| If defined, it is used in the default log configuration. - -| configprop:logging.file.max-size[] -| `LOG_FILE_MAX_SIZE` -| Maximum log file size (if LOG_FILE enabled). - (Only supported with the default Logback setup.) - -| configprop:logging.file.max-history[] -| `LOG_FILE_MAX_HISTORY` -| Maximum number of archive log files to keep (if LOG_FILE enabled). - (Only supported with the default Logback setup.) - -| configprop:logging.file.path[] -| `LOG_PATH` -| If defined, it is used in the default log configuration. - -| configprop:logging.file.total-size-cap[] -| `LOG_FILE_TOTAL_SIZE_CAP` -| Total size of log backups to be kept (if LOG_FILE enabled). - (Only supported with the default Logback setup.) - -| configprop:logging.pattern.console[] -| `CONSOLE_LOG_PATTERN` -| The log pattern to use on the console (stdout). - (Only supported with the default Logback setup.) - -| configprop:logging.pattern.dateformat[] -| `LOG_DATEFORMAT_PATTERN` -| Appender pattern for log date format. - (Only supported with the default Logback setup.) - -| configprop:logging.pattern.file[] -| `FILE_LOG_PATTERN` -| The log pattern to use in a file (if `LOG_FILE` is enabled). - (Only supported with the default Logback setup.) - -| configprop:logging.pattern.level[] -| `LOG_LEVEL_PATTERN` -| The format to use when rendering the log level (default `%5p`). - (Only supported with the default Logback setup.) - -| configprop:logging.pattern.rolling-file-name[] -| `ROLLING_FILE_NAME_PATTERN` -| Pattern for rolled-over log file names (default `$\{LOG_FILE}.%d\{yyyy-MM-dd}.%i.gz`). - (Only supported with the default Logback setup.) - -| `PID` -| `PID` -| The current process ID (discovered if possible and when not already defined as an OS environment variable). -|=== - -All the supported logging systems can consult System properties when parsing their configuration files. -See the default configurations in `spring-boot.jar` for examples: - -* {spring-boot-code}/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/logback/defaults.xml[Logback] -* {spring-boot-code}/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2.xml[Log4j 2] -* {spring-boot-code}/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/java/logging-file.properties[Java Util logging] - -[TIP] -==== -If you want to use a placeholder in a logging property, you should use <> and not the syntax of the underlying framework. -Notably, if you use Logback, you should use `:` as the delimiter between a property name and its default value and not use `:-`. -==== - -[TIP] -==== -You can add MDC and other ad-hoc content to log lines by overriding only the `LOG_LEVEL_PATTERN` (or `logging.pattern.level` with Logback). -For example, if you use `logging.pattern.level=user:%X\{user} %5p`, then the default log format contains an MDC entry for "user", if it exists, as shown in the following example. - -[indent=0] ----- - 2019-08-30 12:30:04.031 user:someone INFO 22174 --- [ nio-8080-exec-0] demo.Controller - Handling authenticated request ----- -==== - - - -[[boot-features-logback-extensions]] -=== Logback Extensions -Spring Boot includes a number of extensions to Logback that can help with advanced configuration. -You can use these extensions in your `logback-spring.xml` configuration file. - -NOTE: Because the standard `logback.xml` configuration file is loaded too early, you cannot use extensions in it. -You need to either use `logback-spring.xml` or define a configprop:logging.config[] property. - -WARNING: The extensions cannot be used with Logback's https://logback.qos.ch/manual/configuration.html#autoScan[configuration scanning]. -If you attempt to do so, making changes to the configuration file results in an error similar to one of the following being logged: - -[indent=0] ----- - ERROR in ch.qos.logback.core.joran.spi.Interpreter@4:71 - no applicable action for [springProperty], current ElementPath is [[configuration][springProperty]] - ERROR in ch.qos.logback.core.joran.spi.Interpreter@4:71 - no applicable action for [springProfile], current ElementPath is [[configuration][springProfile]] ----- - - - -==== Profile-specific Configuration -The `` tag lets you optionally include or exclude sections of configuration based on the active Spring profiles. -Profile sections are supported anywhere within the `` element. -Use the `name` attribute to specify which profile accepts the configuration. -The `` tag can contain a simple profile name (for example `staging`) or a profile expression. -A profile expression allows for more complicated profile logic to be expressed, for example `production & (eu-central | eu-west)`. -Check the {spring-framework-docs}core.html#beans-definition-profiles-java[reference guide] for more details. -The following listing shows three sample profiles: - -[source,xml,indent=0] ----- - - - - - - - - - - - ----- - - - -==== Environment Properties -The `` tag lets you expose properties from the Spring `Environment` for use within Logback. -Doing so can be useful if you want to access values from your `application.properties` file in your Logback configuration. -The tag works in a similar way to Logback's standard `` tag. -However, rather than specifying a direct `value`, you specify the `source` of the property (from the `Environment`). -If you need to store the property somewhere other than in `local` scope, you can use the `scope` attribute. -If you need a fallback value (in case the property is not set in the `Environment`), you can use the `defaultValue` attribute. -The following example shows how to expose properties for use within Logback: - -[source,xml,indent=0] ----- - - - ${fluentHost} - ... - ----- - -NOTE: The `source` must be specified in kebab case (such as `my.property-name`). -However, properties can be added to the `Environment` by using the relaxed rules. - - - -[[boot-features-internationalization]] -== Internationalization -Spring Boot supports localized messages so that your application can cater to users of different language preferences. -By default, Spring Boot looks for the presence of a `messages` resource bundle at the root of the classpath. - -NOTE: The auto-configuration applies when the default properties file for the configured resource bundle is available (i.e. `messages.properties` by default). -If your resource bundle contains only language-specific properties files, you are required to add the default. -If no properties file is found that matches any of the configured base names, there will be no auto-configured `MessageSource`. - -The basename of the resource bundle as well as several other attributes can be configured using the `spring.messages` namespace, as shown in the following example: - -[source,properties,indent=0,configprops] ----- - spring.messages.basename=messages,config.i18n.messages - spring.messages.fallback-to-system-locale=false ----- - -TIP: `spring.messages.basename` supports comma-separated list of locations, either a package qualifier or a resource resolved from the classpath root. - -See {spring-boot-autoconfigure-module-code}/context/MessageSourceProperties.java[`MessageSourceProperties`] for more supported options. - - - -[[boot-features-json]] -== JSON -Spring Boot provides integration with three JSON mapping libraries: - -- Gson -- Jackson -- JSON-B - -Jackson is the preferred and default library. - - - -[[boot-features-json-jackson]] -=== Jackson -Auto-configuration for Jackson is provided and Jackson is part of `spring-boot-starter-json`. -When Jackson is on the classpath an `ObjectMapper` bean is automatically configured. -Several configuration properties are provided for <>. - - - -[[boot-features-json-gson]] -=== Gson -Auto-configuration for Gson is provided. -When Gson is on the classpath a `Gson` bean is automatically configured. -Several `+spring.gson.*+` configuration properties are provided for customizing the configuration. -To take more control, one or more `GsonBuilderCustomizer` beans can be used. - - - -[[boot-features-json-json-b]] -=== JSON-B -Auto-configuration for JSON-B is provided. -When the JSON-B API and an implementation are on the classpath a `Jsonb` bean will be automatically configured. -The preferred JSON-B implementation is Apache Johnzon for which dependency management is provided. - - - -[[boot-features-developing-web-applications]] -== Developing Web Applications -Spring Boot is well suited for web application development. -You can create a self-contained HTTP server by using embedded Tomcat, Jetty, Undertow, or Netty. -Most web applications use the `spring-boot-starter-web` module to get up and running quickly. -You can also choose to build reactive web applications by using the `spring-boot-starter-webflux` module. - -If you have not yet developed a Spring Boot web application, you can follow the "Hello World!" example in the _<>_ section. - - - -[[boot-features-spring-mvc]] -=== The "`Spring Web MVC Framework`" -The {spring-framework-docs}web.html#mvc[Spring Web MVC framework] (often referred to as simply "`Spring MVC`") is a rich "`model view controller`" web framework. -Spring MVC lets you create special `@Controller` or `@RestController` beans to handle incoming HTTP requests. -Methods in your controller are mapped to HTTP by using `@RequestMapping` annotations. - -The following code shows a typical `@RestController` that serves JSON data: - -[source,java,indent=0] ----- - @RestController - @RequestMapping(value="/users") - public class MyRestController { - - @RequestMapping(value="/{user}", method=RequestMethod.GET) - public User getUser(@PathVariable Long user) { - // ... - } - - @RequestMapping(value="/{user}/customers", method=RequestMethod.GET) - List getUserCustomers(@PathVariable Long user) { - // ... - } - - @RequestMapping(value="/{user}", method=RequestMethod.DELETE) - public User deleteUser(@PathVariable Long user) { - // ... - } - - } ----- - -Spring MVC is part of the core Spring Framework, and detailed information is available in the {spring-framework-docs}web.html#mvc[reference documentation]. -There are also several guides that cover Spring MVC available at https://spring.io/guides. - - - -[[boot-features-spring-mvc-auto-configuration]] -==== Spring MVC Auto-configuration -Spring Boot provides auto-configuration for Spring MVC that works well with most applications. - -The auto-configuration adds the following features on top of Spring's defaults: - -* Inclusion of `ContentNegotiatingViewResolver` and `BeanNameViewResolver` beans. -* Support for serving static resources, including support for WebJars (covered <>)). -* Automatic registration of `Converter`, `GenericConverter`, and `Formatter` beans. -* Support for `HttpMessageConverters` (covered <>). -* Automatic registration of `MessageCodesResolver` (covered <>). -* Static `index.html` support. -* Custom `Favicon` support (covered <>). -* Automatic use of a `ConfigurableWebBindingInitializer` bean (covered <>). - -If you want to keep Spring Boot MVC features and you want to add additional {spring-framework-docs}web.html#mvc[MVC configuration] (interceptors, formatters, view controllers, and other features), you can add your own `@Configuration` class of type `WebMvcConfigurer` but *without* `@EnableWebMvc`. -If you wish to provide custom instances of `RequestMappingHandlerMapping`, `RequestMappingHandlerAdapter`, or `ExceptionHandlerExceptionResolver`, you can declare a `WebMvcRegistrationsAdapter` instance to provide such components. - -If you want to take complete control of Spring MVC, you can add your own `@Configuration` annotated with `@EnableWebMvc`. - - -[[boot-features-spring-mvc-message-converters]] -==== HttpMessageConverters -Spring MVC uses the `HttpMessageConverter` interface to convert HTTP requests and responses. -Sensible defaults are included out of the box. -For example, objects can be automatically converted to JSON (by using the Jackson library) or XML (by using the Jackson XML extension, if available, or by using JAXB if the Jackson XML extension is not available). -By default, strings are encoded in `UTF-8`. - -If you need to add or customize converters, you can use Spring Boot's `HttpMessageConverters` class, as shown in the following listing: - -[source,java,indent=0] ----- - import org.springframework.boot.autoconfigure.http.HttpMessageConverters; - import org.springframework.context.annotation.*; - import org.springframework.http.converter.*; - - @Configuration(proxyBeanMethods = false) - public class MyConfiguration { - - @Bean - public HttpMessageConverters customConverters() { - HttpMessageConverter additional = ... - HttpMessageConverter another = ... - return new HttpMessageConverters(additional, another); - } - - } ----- - -Any `HttpMessageConverter` bean that is present in the context is added to the list of converters. -You can also override default converters in the same way. - - - -[[boot-features-json-components]] -==== Custom JSON Serializers and Deserializers -If you use Jackson to serialize and deserialize JSON data, you might want to write your own `JsonSerializer` and `JsonDeserializer` classes. -Custom serializers are usually https://github.com/FasterXML/jackson-docs/wiki/JacksonHowToCustomSerializers[registered with Jackson through a module], but Spring Boot provides an alternative `@JsonComponent` annotation that makes it easier to directly register Spring Beans. - -You can use the `@JsonComponent` annotation directly on `JsonSerializer`, `JsonDeserializer` or `KeyDeserializer` implementations. -You can also use it on classes that contain serializers/deserializers as inner classes, as shown in the following example: - -[source,java,indent=0] ----- - import java.io.*; - import com.fasterxml.jackson.core.*; - import com.fasterxml.jackson.databind.*; - import org.springframework.boot.jackson.*; - - @JsonComponent - public class Example { - - public static class Serializer extends JsonSerializer { - // ... - } - - public static class Deserializer extends JsonDeserializer { - // ... - } - - } ----- - -All `@JsonComponent` beans in the `ApplicationContext` are automatically registered with Jackson. -Because `@JsonComponent` is meta-annotated with `@Component`, the usual component-scanning rules apply. - -Spring Boot also provides {spring-boot-module-code}/jackson/JsonObjectSerializer.java[`JsonObjectSerializer`] and {spring-boot-module-code}/jackson/JsonObjectDeserializer.java[`JsonObjectDeserializer`] base classes that provide useful alternatives to the standard Jackson versions when serializing objects. -See {spring-boot-module-api}/jackson/JsonObjectSerializer.html[`JsonObjectSerializer`] and {spring-boot-module-api}/jackson/JsonObjectDeserializer.html[`JsonObjectDeserializer`] in the Javadoc for details. - - - -[[boot-features-spring-message-codes]] -==== MessageCodesResolver -Spring MVC has a strategy for generating error codes for rendering error messages from binding errors: `MessageCodesResolver`. -If you set the configprop:spring.mvc.message-codes-resolver-format[] property `PREFIX_ERROR_CODE` or `POSTFIX_ERROR_CODE`, Spring Boot creates one for you (see the enumeration in {spring-framework-api}/validation/DefaultMessageCodesResolver.Format.html[`DefaultMessageCodesResolver.Format`]). - - - -[[boot-features-spring-mvc-static-content]] -==== Static Content -By default, Spring Boot serves static content from a directory called `/static` (or `/public` or `/resources` or `/META-INF/resources`) in the classpath or from the root of the `ServletContext`. -It uses the `ResourceHttpRequestHandler` from Spring MVC so that you can modify that behavior by adding your own `WebMvcConfigurer` and overriding the `addResourceHandlers` method. - -In a stand-alone web application, the default servlet from the container is also enabled and acts as a fallback, serving content from the root of the `ServletContext` if Spring decides not to handle it. -Most of the time, this does not happen (unless you modify the default MVC configuration), because Spring can always handle requests through the `DispatcherServlet`. - -By default, resources are mapped on `+/**+`, but you can tune that with the configprop:spring.mvc.static-path-pattern[] property. -For instance, relocating all resources to `/resources/**` can be achieved as follows: - -[source,properties,indent=0,subs="verbatim,quotes,attributes",configprops] ----- - spring.mvc.static-path-pattern=/resources/** ----- - -You can also customize the static resource locations by using the configprop:spring.resources.static-locations[] property (replacing the default values with a list of directory locations). -The root Servlet context path, `"/"`, is automatically added as a location as well. - -In addition to the "`standard`" static resource locations mentioned earlier, a special case is made for https://www.webjars.org/[Webjars content]. -Any resources with a path in `+/webjars/**+` are served from jar files if they are packaged in the Webjars format. - -TIP: Do not use the `src/main/webapp` directory if your application is packaged as a jar. -Although this directory is a common standard, it works *only* with war packaging, and it is silently ignored by most build tools if you generate a jar. - -Spring Boot also supports the advanced resource handling features provided by Spring MVC, allowing use cases such as cache-busting static resources or using version agnostic URLs for Webjars. - -To use version agnostic URLs for Webjars, add the `webjars-locator-core` dependency. -Then declare your Webjar. -Using jQuery as an example, adding `"/webjars/jquery/jquery.min.js"` results in `"/webjars/jquery/x.y.z/jquery.min.js"` where `x.y.z` is the Webjar version. - -NOTE: If you use JBoss, you need to declare the `webjars-locator-jboss-vfs` dependency instead of the `webjars-locator-core`. -Otherwise, all Webjars resolve as a `404`. - -To use cache busting, the following configuration configures a cache busting solution for all static resources, effectively adding a content hash, such as ``, in URLs: - -[source,properties,indent=0,subs="verbatim,quotes,attributes",configprops] ----- - spring.resources.chain.strategy.content.enabled=true - spring.resources.chain.strategy.content.paths=/** ----- - -NOTE: Links to resources are rewritten in templates at runtime, thanks to a `ResourceUrlEncodingFilter` that is auto-configured for Thymeleaf and FreeMarker. -You should manually declare this filter when using JSPs. -Other template engines are currently not automatically supported but can be with custom template macros/helpers and the use of the {spring-framework-api}/web/servlet/resource/ResourceUrlProvider.html[`ResourceUrlProvider`]. - -When loading resources dynamically with, for example, a JavaScript module loader, renaming files is not an option. -That is why other strategies are also supported and can be combined. -A "fixed" strategy adds a static version string in the URL without changing the file name, as shown in the following example: - -[source,properties,indent=0,subs="verbatim,quotes,attributes",configprops] ----- - spring.resources.chain.strategy.content.enabled=true - spring.resources.chain.strategy.content.paths=/** - spring.resources.chain.strategy.fixed.enabled=true - spring.resources.chain.strategy.fixed.paths=/js/lib/ - spring.resources.chain.strategy.fixed.version=v12 ----- - -With this configuration, JavaScript modules located under `"/js/lib/"` use a fixed versioning strategy (`"/v12/js/lib/mymodule.js"`), while other resources still use the content one (``). - -See {spring-boot-autoconfigure-module-code}/web/ResourceProperties.java[`ResourceProperties`] for more supported options. - -[TIP] -==== -This feature has been thoroughly described in a dedicated https://spring.io/blog/2014/07/24/spring-framework-4-1-handling-static-web-resources[blog post] and in Spring Framework's {spring-framework-docs}web.html#mvc-config-static-resources[reference documentation]. -==== - -[[boot-features-spring-mvc-welcome-page]] -==== Welcome Page -Spring Boot supports both static and templated welcome pages. -It first looks for an `index.html` file in the configured static content locations. -If one is not found, it then looks for an `index` template. -If either is found, it is automatically used as the welcome page of the application. - - - -[[boot-features-spring-mvc-favicon]] -==== Custom Favicon -As with other static resources, Spring Boot looks for a `favicon.ico` in the configured static content locations. -If such a file is present, it is automatically used as the favicon of the application. - - - -[[boot-features-spring-mvc-pathmatch]] -==== Path Matching and Content Negotiation -Spring MVC can map incoming HTTP requests to handlers by looking at the request path and matching it to the mappings defined in your application (for example, `@GetMapping` annotations on Controller methods). - -Spring Boot chooses to disable suffix pattern matching by default, which means that requests like `"GET /projects/spring-boot.json"` won't be matched to `@GetMapping("/projects/spring-boot")` mappings. -This is considered as a {spring-framework-docs}web.html#mvc-ann-requestmapping-suffix-pattern-match[best practice for Spring MVC applications]. -This feature was mainly useful in the past for HTTP clients which did not send proper "Accept" request headers; we needed to make sure to send the correct Content Type to the client. -Nowadays, Content Negotiation is much more reliable. - -There are other ways to deal with HTTP clients that don't consistently send proper "Accept" request headers. -Instead of using suffix matching, we can use a query parameter to ensure that requests like `"GET /projects/spring-boot?format=json"` will be mapped to `@GetMapping("/projects/spring-boot")`: - -[source,properties,indent=0,subs="verbatim,quotes,attributes",configprops] ----- - spring.mvc.contentnegotiation.favor-parameter=true - - # We can change the parameter name, which is "format" by default: - # spring.mvc.contentnegotiation.parameter-name=myparam - - # We can also register additional file extensions/media types with: - spring.mvc.contentnegotiation.media-types.markdown=text/markdown ----- - -If you understand the caveats and would still like your application to use suffix pattern matching, the following configuration is required: - -[source,properties,indent=0,subs="verbatim,quotes,attributes",configprops] ----- - spring.mvc.contentnegotiation.favor-path-extension=true - spring.mvc.pathmatch.use-suffix-pattern=true ----- - -Alternatively, rather than open all suffix patterns, it's more secure to just support registered suffix patterns: - -[source,properties,indent=0,subs="verbatim,quotes,attributes",configprops] ----- - spring.mvc.contentnegotiation.favor-path-extension=true - spring.mvc.pathmatch.use-registered-suffix-pattern=true - - # You can also register additional file extensions/media types with: - # spring.mvc.contentnegotiation.media-types.adoc=text/asciidoc ----- - - - -[[boot-features-spring-mvc-web-binding-initializer]] -==== ConfigurableWebBindingInitializer -Spring MVC uses a `WebBindingInitializer` to initialize a `WebDataBinder` for a particular request. -If you create your own `ConfigurableWebBindingInitializer` `@Bean`, Spring Boot automatically configures Spring MVC to use it. - - - -[[boot-features-spring-mvc-template-engines]] -==== Template Engines -As well as REST web services, you can also use Spring MVC to serve dynamic HTML content. -Spring MVC supports a variety of templating technologies, including Thymeleaf, FreeMarker, and JSPs. -Also, many other templating engines include their own Spring MVC integrations. - -Spring Boot includes auto-configuration support for the following templating engines: - - * https://freemarker.apache.org/docs/[FreeMarker] - * http://docs.groovy-lang.org/docs/next/html/documentation/template-engines.html#_the_markuptemplateengine[Groovy] - * https://www.thymeleaf.org[Thymeleaf] - * https://mustache.github.io/[Mustache] - -TIP: If possible, JSPs should be avoided. -There are several <> when using them with embedded servlet containers. - -When you use one of these templating engines with the default configuration, your templates are picked up automatically from `src/main/resources/templates`. - -TIP: Depending on how you run your application, IntelliJ IDEA orders the classpath differently. -Running your application in the IDE from its main method results in a different ordering than when you run your application by using Maven or Gradle or from its packaged jar. -This can cause Spring Boot to fail to find the templates on the classpath. -If you have this problem, you can reorder the classpath in the IDE to place the module's classes and resources first. -Alternatively, you can configure the template prefix to search every `templates` directory on the classpath, as follows: `classpath*:/templates/`. - - - -[[boot-features-error-handling]] -==== Error Handling -By default, Spring Boot provides an `/error` mapping that handles all errors in a sensible way, and it is registered as a "`global`" error page in the servlet container. -For machine clients, it produces a JSON response with details of the error, the HTTP status, and the exception message. -For browser clients, there is a "`whitelabel`" error view that renders the same data in HTML format (to customize it, add a `View` that resolves to `error`). -To replace the default behavior completely, you can implement `ErrorController` and register a bean definition of that type or add a bean of type `ErrorAttributes` to use the existing mechanism but replace the contents. - -TIP: The `BasicErrorController` can be used as a base class for a custom `ErrorController`. -This is particularly useful if you want to add a handler for a new content type (the default is to handle `text/html` specifically and provide a fallback for everything else). -To do so, extend `BasicErrorController`, add a public method with a `@RequestMapping` that has a `produces` attribute, and create a bean of your new type. - -You can also define a class annotated with `@ControllerAdvice` to customize the JSON document to return for a particular controller and/or exception type, as shown in the following example: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- - @ControllerAdvice(basePackageClasses = AcmeController.class) - public class AcmeControllerAdvice extends ResponseEntityExceptionHandler { - - @ExceptionHandler(YourException.class) - @ResponseBody - ResponseEntity handleControllerException(HttpServletRequest request, Throwable ex) { - HttpStatus status = getStatus(request); - return new ResponseEntity<>(new CustomErrorType(status.value(), ex.getMessage()), status); - } - - private HttpStatus getStatus(HttpServletRequest request) { - Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code"); - if (statusCode == null) { - return HttpStatus.INTERNAL_SERVER_ERROR; - } - return HttpStatus.valueOf(statusCode); - } - - } ----- - -In the preceding example, if `YourException` is thrown by a controller defined in the same package as `AcmeController`, a JSON representation of the `CustomErrorType` POJO is used instead of the `ErrorAttributes` representation. - - - -[[boot-features-error-handling-custom-error-pages]] -===== Custom Error Pages -If you want to display a custom HTML error page for a given status code, you can add a file to an `/error` folder. -Error pages can either be static HTML (that is, added under any of the static resource folders) or be built by using templates. -The name of the file should be the exact status code or a series mask. - -For example, to map `404` to a static HTML file, your folder structure would be as follows: - -[source,indent=0,subs="verbatim,quotes,attributes"] ----- - src/ - +- main/ - +- java/ - | + - +- resources/ - +- public/ - +- error/ - | +- 404.html - +- ----- - -To map all `5xx` errors by using a FreeMarker template, your folder structure would be as follows: - -[source,indent=0,subs="verbatim,quotes,attributes"] ----- - src/ - +- main/ - +- java/ - | + - +- resources/ - +- templates/ - +- error/ - | +- 5xx.ftlh - +- ----- - -For more complex mappings, you can also add beans that implement the `ErrorViewResolver` interface, as shown in the following example: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- - public class MyErrorViewResolver implements ErrorViewResolver { - - @Override - public ModelAndView resolveErrorView(HttpServletRequest request, - HttpStatus status, Map model) { - // Use the request or status to optionally return a ModelAndView - return ... - } - - } ----- - - -You can also use regular Spring MVC features such as {spring-framework-docs}web.html#mvc-exceptionhandlers[`@ExceptionHandler` methods] and {spring-framework-docs}web.html#mvc-ann-controller-advice[`@ControllerAdvice`]. -The `ErrorController` then picks up any unhandled exceptions. - - - -[[boot-features-error-handling-mapping-error-pages-without-mvc]] -===== Mapping Error Pages outside of Spring MVC -For applications that do not use Spring MVC, you can use the `ErrorPageRegistrar` interface to directly register `ErrorPages`. -This abstraction works directly with the underlying embedded servlet container and works even if you do not have a Spring MVC `DispatcherServlet`. - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- - @Bean - public ErrorPageRegistrar errorPageRegistrar(){ - return new MyErrorPageRegistrar(); - } - - // ... - - private static class MyErrorPageRegistrar implements ErrorPageRegistrar { - - @Override - public void registerErrorPages(ErrorPageRegistry registry) { - registry.addErrorPages(new ErrorPage(HttpStatus.BAD_REQUEST, "/400")); - } - - } ----- - -NOTE: If you register an `ErrorPage` with a path that ends up being handled by a `Filter` (as is common with some non-Spring web frameworks, like Jersey and Wicket), then the `Filter` has to be explicitly registered as an `ERROR` dispatcher, as shown in the following example: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- - @Bean - public FilterRegistrationBean myFilter() { - FilterRegistrationBean registration = new FilterRegistrationBean(); - registration.setFilter(new MyFilter()); - ... - registration.setDispatcherTypes(EnumSet.allOf(DispatcherType.class)); - return registration; - } ----- - -Note that the default `FilterRegistrationBean` does not include the `ERROR` dispatcher type. - - - -[[boot-features-error-handling-websphere]] -CAUTION:When deployed to a servlet container, Spring Boot uses its error page filter to forward a request with an error status to the appropriate error page. -The request can only be forwarded to the correct error page if the response has not already been committed. -By default, WebSphere Application Server 8.0 and later commits the response upon successful completion of a servlet's service method. -You should disable this behavior by setting `com.ibm.ws.webcontainer.invokeFlushAfterService` to `false`. - - - -[[boot-features-spring-hateoas]] -==== Spring HATEOAS -If you develop a RESTful API that makes use of hypermedia, Spring Boot provides auto-configuration for Spring HATEOAS that works well with most applications. -The auto-configuration replaces the need to use `@EnableHypermediaSupport` and registers a number of beans to ease building hypermedia-based applications, including a `LinkDiscoverers` (for client side support) and an `ObjectMapper` configured to correctly marshal responses into the desired representation. -The `ObjectMapper` is customized by setting the various `spring.jackson.*` properties or, if one exists, by a `Jackson2ObjectMapperBuilder` bean. - -You can take control of Spring HATEOAS's configuration by using `@EnableHypermediaSupport`. -Note that doing so disables the `ObjectMapper` customization described earlier. - - - -[[boot-features-cors]] -==== CORS Support -https://en.wikipedia.org/wiki/Cross-origin_resource_sharing[Cross-origin resource sharing] (CORS) is a https://www.w3.org/TR/cors/[W3C specification] implemented by https://caniuse.com/#feat=cors[most browsers] that lets you specify in a flexible way what kind of cross-domain requests are authorized., instead of using some less secure and less powerful approaches such as IFRAME or JSONP. - -As of version 4.2, Spring MVC {spring-framework-docs}web.html#mvc-cors[supports CORS]. -Using {spring-framework-docs}web.html#mvc-cors-controller[controller method CORS configuration] with {spring-framework-api}/web/bind/annotation/CrossOrigin.html[`@CrossOrigin`] annotations in your Spring Boot application does not require any specific configuration. -{spring-framework-docs}web.html#mvc-cors-global[Global CORS configuration] can be defined by registering a `WebMvcConfigurer` bean with a customized `addCorsMappings(CorsRegistry)` method, as shown in the following example: - -[source,java,indent=0] ----- - @Configuration(proxyBeanMethods = false) - public class MyConfiguration { - - @Bean - public WebMvcConfigurer corsConfigurer() { - return new WebMvcConfigurer() { - @Override - public void addCorsMappings(CorsRegistry registry) { - registry.addMapping("/api/**"); - } - }; - } - } ----- - - - -[[boot-features-webflux]] -=== The "`Spring WebFlux Framework`" -Spring WebFlux is the new reactive web framework introduced in Spring Framework 5.0. -Unlike Spring MVC, it does not require the Servlet API, is fully asynchronous and non-blocking, and implements the https://www.reactive-streams.org/[Reactive Streams] specification through https://projectreactor.io/[the Reactor project]. - -Spring WebFlux comes in two flavors: functional and annotation-based. -The annotation-based one is quite close to the Spring MVC model, as shown in the following example: - -[source,java,indent=0] ----- - @RestController - @RequestMapping("/users") - public class MyRestController { - - @GetMapping("/{user}") - public Mono getUser(@PathVariable Long user) { - // ... - } - - @GetMapping("/{user}/customers") - public Flux getUserCustomers(@PathVariable Long user) { - // ... - } - - @DeleteMapping("/{user}") - public Mono deleteUser(@PathVariable Long user) { - // ... - } - - } ----- - -"`WebFlux.fn`", the functional variant, separates the routing configuration from the actual handling of the requests, as shown in the following example: - -[source,java,indent=0] ----- - @Configuration(proxyBeanMethods = false) - public class RoutingConfiguration { - - @Bean - public RouterFunction monoRouterFunction(UserHandler userHandler) { - return route(GET("/{user}").and(accept(APPLICATION_JSON)), userHandler::getUser) - .andRoute(GET("/{user}/customers").and(accept(APPLICATION_JSON)), userHandler::getUserCustomers) - .andRoute(DELETE("/{user}").and(accept(APPLICATION_JSON)), userHandler::deleteUser); - } - - } - - @Component - public class UserHandler { - - public Mono getUser(ServerRequest request) { - // ... - } - - public Mono getUserCustomers(ServerRequest request) { - // ... - } - - public Mono deleteUser(ServerRequest request) { - // ... - } - } ----- - -WebFlux is part of the Spring Framework and detailed information is available in its {spring-framework-docs}web-reactive.html#webflux-fn[reference documentation]. - -TIP: You can define as many `RouterFunction` beans as you like to modularize the definition of the router. -Beans can be ordered if you need to apply a precedence. - -To get started, add the `spring-boot-starter-webflux` module to your application. - -NOTE: Adding both `spring-boot-starter-web` and `spring-boot-starter-webflux` modules in your application results in Spring Boot auto-configuring Spring MVC, not WebFlux. -This behavior has been chosen because many Spring developers add `spring-boot-starter-webflux` to their Spring MVC application to use the reactive `WebClient`. -You can still enforce your choice by setting the chosen application type to `SpringApplication.setWebApplicationType(WebApplicationType.REACTIVE)`. - - - -[[boot-features-webflux-auto-configuration]] -==== Spring WebFlux Auto-configuration -Spring Boot provides auto-configuration for Spring WebFlux that works well with most applications. - -The auto-configuration adds the following features on top of Spring's defaults: - -* Configuring codecs for `HttpMessageReader` and `HttpMessageWriter` instances (described <>). -* Support for serving static resources, including support for WebJars (described <>). - -If you want to keep Spring Boot WebFlux features and you want to add additional {spring-framework-docs}web-reactive.html#webflux-config[WebFlux configuration], you can add your own `@Configuration` class of type `WebFluxConfigurer` but *without* `@EnableWebFlux`. - -If you want to take complete control of Spring WebFlux, you can add your own `@Configuration` annotated with `@EnableWebFlux`. - - - -[[boot-features-webflux-httpcodecs]] -==== HTTP Codecs with HttpMessageReaders and HttpMessageWriters -Spring WebFlux uses the `HttpMessageReader` and `HttpMessageWriter` interfaces to convert HTTP requests and responses. -They are configured with `CodecConfigurer` to have sensible defaults by looking at the libraries available in your classpath. - -Spring Boot provides dedicated configuration properties for codecs, `+spring.codec.*+`. -It also applies further customization by using `CodecCustomizer` instances. -For example, `+spring.jackson.*+` configuration keys are applied to the Jackson codec. - -If you need to add or customize codecs, you can create a custom `CodecCustomizer` component, as shown in the following example: - -[source,java,indent=0] ----- - import org.springframework.boot.web.codec.CodecCustomizer; - - @Configuration(proxyBeanMethods = false) - public class MyConfiguration { - - @Bean - public CodecCustomizer myCodecCustomizer() { - return codecConfigurer -> { - // ... - }; - } - - } ----- - -You can also leverage <>. - - - -[[boot-features-webflux-static-content]] -==== Static Content -By default, Spring Boot serves static content from a directory called `/static` (or `/public` or `/resources` or `/META-INF/resources`) in the classpath. -It uses the `ResourceWebHandler` from Spring WebFlux so that you can modify that behavior by adding your own `WebFluxConfigurer` and overriding the `addResourceHandlers` method. - -By default, resources are mapped on `+/**+`, but you can tune that by setting the configprop:spring.webflux.static-path-pattern[] property. -For instance, relocating all resources to `/resources/**` can be achieved as follows: - -[source,properties,indent=0,subs="verbatim,quotes,attributes",configprops] ----- - spring.webflux.static-path-pattern=/resources/** ----- - -You can also customize the static resource locations by using `spring.resources.static-locations`. -Doing so replaces the default values with a list of directory locations. -If you do so, the default welcome page detection switches to your custom locations. -So, if there is an `index.html` in any of your locations on startup, it is the home page of the application. - -In addition to the "`standard`" static resource locations listed earlier, a special case is made for https://www.webjars.org/[Webjars content]. -Any resources with a path in `+/webjars/**+` are served from jar files if they are packaged in the Webjars format. - -TIP: Spring WebFlux applications do not strictly depend on the Servlet API, so they cannot be deployed as war files and do not use the `src/main/webapp` directory. - - - -[[boot-features-webflux-template-engines]] -==== Template Engines -As well as REST web services, you can also use Spring WebFlux to serve dynamic HTML content. -Spring WebFlux supports a variety of templating technologies, including Thymeleaf, FreeMarker, and Mustache. - -Spring Boot includes auto-configuration support for the following templating engines: - - * https://freemarker.apache.org/docs/[FreeMarker] - * https://www.thymeleaf.org[Thymeleaf] - * https://mustache.github.io/[Mustache] - -When you use one of these templating engines with the default configuration, your templates are picked up automatically from `src/main/resources/templates`. - - - -[[boot-features-webflux-error-handling]] -==== Error Handling -Spring Boot provides a `WebExceptionHandler` that handles all errors in a sensible way. -Its position in the processing order is immediately before the handlers provided by WebFlux, which are considered last. -For machine clients, it produces a JSON response with details of the error, the HTTP status, and the exception message. -For browser clients, there is a "`whitelabel`" error handler that renders the same data in HTML format. -You can also provide your own HTML templates to display errors (see the <>). - -The first step to customizing this feature often involves using the existing mechanism but replacing or augmenting the error contents. -For that, you can add a bean of type`ErrorAttributes`. - -To change the error handling behavior, you can implement `ErrorWebExceptionHandler` and register a bean definition of that type. -Because a `WebExceptionHandler` is quite low-level, Spring Boot also provides a convenient `AbstractErrorWebExceptionHandler` to let you handle errors in a WebFlux functional way, as shown in the following example: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- - public class CustomErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler { - - // Define constructor here - - @Override - protected RouterFunction getRoutingFunction(ErrorAttributes errorAttributes) { - - return RouterFunctions - .route(aPredicate, aHandler) - .andRoute(anotherPredicate, anotherHandler); - } - - } ----- - -For a more complete picture, you can also subclass `DefaultErrorWebExceptionHandler` directly and override specific methods. - - - -[[boot-features-webflux-error-handling-custom-error-pages]] -===== Custom Error Pages -If you want to display a custom HTML error page for a given status code, you can add a file to an `/error` folder. -Error pages can either be static HTML (that is, added under any of the static resource folders) or built with templates. -The name of the file should be the exact status code or a series mask. - -For example, to map `404` to a static HTML file, your folder structure would be as follows: - -[source,indent=0,subs="verbatim,quotes,attributes"] ----- - src/ - +- main/ - +- java/ - | + - +- resources/ - +- public/ - +- error/ - | +- 404.html - +- ----- - -To map all `5xx` errors by using a Mustache template, your folder structure would be as follows: - -[source,indent=0,subs="verbatim,quotes,attributes"] ----- - src/ - +- main/ - +- java/ - | + - +- resources/ - +- templates/ - +- error/ - | +- 5xx.mustache - +- ----- - - - -[[boot-features-webflux-web-filters]] -==== Web Filters -Spring WebFlux provides a `WebFilter` interface that can be implemented to filter HTTP request-response exchanges. -`WebFilter` beans found in the application context will be automatically used to filter each exchange. - -Where the order of the filters is important they can implement `Ordered` or be annotated with `@Order`. -Spring Boot auto-configuration may configure web filters for you. -When it does so, the orders shown in the following table will be used: - -|=== -| Web Filter | Order - -| `MetricsWebFilter` -| `Ordered.HIGHEST_PRECEDENCE + 1` - -| `WebFilterChainProxy` (Spring Security) -| `-100` - -| `HttpTraceWebFilter` -| `Ordered.LOWEST_PRECEDENCE - 10` -|=== - - - -[[boot-features-jersey]] -=== JAX-RS and Jersey -If you prefer the JAX-RS programming model for REST endpoints, you can use one of the available implementations instead of Spring MVC. -https://jersey.github.io/[Jersey] and https://cxf.apache.org/[Apache CXF] work quite well out of the box. -CXF requires you to register its `Servlet` or `Filter` as a `@Bean` in your application context. -Jersey has some native Spring support, so we also provide auto-configuration support for it in Spring Boot, together with a starter. - -To get started with Jersey, include the `spring-boot-starter-jersey` as a dependency and then you need one `@Bean` of type `ResourceConfig` in which you register all the endpoints, as shown in the following example: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- - @Component - public class JerseyConfig extends ResourceConfig { - - public JerseyConfig() { - register(Endpoint.class); - } - - } ----- - -WARNING: Jersey's support for scanning executable archives is rather limited. -For example, it cannot scan for endpoints in a package found in a <> or in `WEB-INF/classes` when running an executable war file. -To avoid this limitation, the `packages` method should not be used, and endpoints should be registered individually by using the `register` method, as shown in the preceding example. - -For more advanced customizations, you can also register an arbitrary number of beans that implement `ResourceConfigCustomizer`. - -All the registered endpoints should be `@Components` with HTTP resource annotations (`@GET` and others), as shown in the following example: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- - @Component - @Path("/hello") - public class Endpoint { - - @GET - public String message() { - return "Hello"; - } - - } ----- - -Since the `Endpoint` is a Spring `@Component`, its lifecycle is managed by Spring and you can use the `@Autowired` annotation to inject dependencies and use the `@Value` annotation to inject external configuration. -By default, the Jersey servlet is registered and mapped to `/*`. -You can change the mapping by adding `@ApplicationPath` to your `ResourceConfig`. - -By default, Jersey is set up as a Servlet in a `@Bean` of type `ServletRegistrationBean` named `jerseyServletRegistration`. -By default, the servlet is initialized lazily, but you can customize that behavior by setting `spring.jersey.servlet.load-on-startup`. -You can disable or override that bean by creating one of your own with the same name. -You can also use a filter instead of a servlet by setting `spring.jersey.type=filter` (in which case, the `@Bean` to replace or override is `jerseyFilterRegistration`). -The filter has an `@Order`, which you can set with `spring.jersey.filter.order`. -Both the servlet and the filter registrations can be given init parameters by using `spring.jersey.init.*` to specify a map of properties. - - - -[[boot-features-embedded-container]] -=== Embedded Servlet Container Support -Spring Boot includes support for embedded https://tomcat.apache.org/[Tomcat], https://www.eclipse.org/jetty/[Jetty], and https://github.com/undertow-io/undertow[Undertow] servers. -Most developers use the appropriate "`Starter`" to obtain a fully configured instance. -By default, the embedded server listens for HTTP requests on port `8080`. - - - -[[boot-features-embedded-container-servlets-filters-listeners]] -==== Servlets, Filters, and listeners -When using an embedded servlet container, you can register servlets, filters, and all the listeners (such as `HttpSessionListener`) from the Servlet spec, either by using Spring beans or by scanning for Servlet components. - - - -[[boot-features-embedded-container-servlets-filters-listeners-beans]] -===== Registering Servlets, Filters, and Listeners as Spring Beans -Any `Servlet`, `Filter`, or servlet `*Listener` instance that is a Spring bean is registered with the embedded container. -This can be particularly convenient if you want to refer to a value from your `application.properties` during configuration. - -By default, if the context contains only a single Servlet, it is mapped to `/`. -In the case of multiple servlet beans, the bean name is used as a path prefix. -Filters map to `+/*+`. - -If convention-based mapping is not flexible enough, you can use the `ServletRegistrationBean`, `FilterRegistrationBean`, and `ServletListenerRegistrationBean` classes for complete control. - -It is usually safe to leave Filter beans unordered. -If a specific order is required, you should annotate the `Filter` with `@Order` or make it implement `Ordered`. -You cannot configure the order of a `Filter` by annotating its bean method with `@Order`. -If you cannot change the `Filter` class to add `@Order` or implement `Ordered`, you must define a `FilterRegistrationBean` for the `Filter` and set the registration bean's order using the `setOrder(int)` method. -Avoid configuring a Filter that reads the request body at `Ordered.HIGHEST_PRECEDENCE`, since it might go against the character encoding configuration of your application. -If a Servlet filter wraps the request, it should be configured with an order that is less than or equal to `OrderedFilter.REQUEST_WRAPPER_FILTER_MAX_ORDER`. - -TIP: To see the order of every `Filter` in your application, enable debug level logging for the `web` <> (`logging.level.web=debug`). -Details of the registered filters, including their order and URL patterns, will then be logged at startup. - -WARNING: Take care when registering `Filter` beans since they are initialized very early in the application lifectyle. -If you need to register a `Filter` that interacts with other beans, consider using a {spring-boot-module-api}/web/servlet/DelegatingFilterProxyRegistrationBean.html[`DelegatingFilterProxyRegistrationBean`] instead. - - - -[[boot-features-embedded-container-context-initializer]] -==== Servlet Context Initialization -Embedded servlet containers do not directly execute the Servlet 3.0+ `javax.servlet.ServletContainerInitializer` interface or Spring's `org.springframework.web.WebApplicationInitializer` interface. -This is an intentional design decision intended to reduce the risk that third party libraries designed to run inside a war may break Spring Boot applications. - -If you need to perform servlet context initialization in a Spring Boot application, you should register a bean that implements the `org.springframework.boot.web.servlet.ServletContextInitializer` interface. -The single `onStartup` method provides access to the `ServletContext` and, if necessary, can easily be used as an adapter to an existing `WebApplicationInitializer`. - - - -[[boot-features-embedded-container-servlets-filters-listeners-scanning]] -===== Scanning for Servlets, Filters, and listeners -When using an embedded container, automatic registration of classes annotated with `@WebServlet`, `@WebFilter`, and `@WebListener` can be enabled by using `@ServletComponentScan`. - -TIP: `@ServletComponentScan` has no effect in a standalone container, where the container's built-in discovery mechanisms are used instead. - - - -[[boot-features-embedded-container-application-context]] -==== The ServletWebServerApplicationContext -Under the hood, Spring Boot uses a different type of `ApplicationContext` for embedded servlet container support. -The `ServletWebServerApplicationContext` is a special type of `WebApplicationContext` that bootstraps itself by searching for a single `ServletWebServerFactory` bean. -Usually a `TomcatServletWebServerFactory`, `JettyServletWebServerFactory`, or `UndertowServletWebServerFactory` has been auto-configured. - -NOTE: You usually do not need to be aware of these implementation classes. -Most applications are auto-configured, and the appropriate `ApplicationContext` and `ServletWebServerFactory` are created on your behalf. - - - -[[boot-features-customizing-embedded-containers]] -==== Customizing Embedded Servlet Containers -Common servlet container settings can be configured by using Spring `Environment` properties. -Usually, you would define the properties in your `application.properties` file. - -Common server settings include: - -* Network settings: Listen port for incoming HTTP requests (`server.port`), interface address to bind to `server.address`, and so on. -* Session settings: Whether the session is persistent (`server.servlet.session.persistent`), session timeout (`server.servlet.session.timeout`), location of session data (`server.servlet.session.store-dir`), and session-cookie configuration (`server.servlet.session.cookie.*`). -* Error management: Location of the error page (`server.error.path`) and so on. -* <> -* <> - -Spring Boot tries as much as possible to expose common settings, but this is not always possible. -For those cases, dedicated namespaces offer server-specific customizations (see `server.tomcat` and `server.undertow`). -For instance, <> can be configured with specific features of the embedded servlet container. - -TIP: See the {spring-boot-autoconfigure-module-code}/web/ServerProperties.java[`ServerProperties`] class for a complete list. - - - -[[boot-features-programmatic-embedded-container-customization]] -===== Programmatic Customization -If you need to programmatically configure your embedded servlet container, you can register a Spring bean that implements the `WebServerFactoryCustomizer` interface. -`WebServerFactoryCustomizer` provides access to the `ConfigurableServletWebServerFactory`, which includes numerous customization setter methods. -The following example shows programmatically setting the port: - -[source,java,indent=0] ----- - import org.springframework.boot.web.server.WebServerFactoryCustomizer; - import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; - import org.springframework.stereotype.Component; - - @Component - public class CustomizationBean implements WebServerFactoryCustomizer { - - @Override - public void customize(ConfigurableServletWebServerFactory server) { - server.setPort(9000); - } - - } ----- - -NOTE: `TomcatServletWebServerFactory`, `JettyServletWebServerFactory` and `UndertowServletWebServerFactory` are dedicated variants of `ConfigurableServletWebServerFactory` that have additional customization setter methods for Tomcat, Jetty and Undertow respectively. - - - -[[boot-features-customizing-configurableservletwebserverfactory-directly]] -===== Customizing ConfigurableServletWebServerFactory Directly -If the preceding customization techniques are too limited, you can register the `TomcatServletWebServerFactory`, `JettyServletWebServerFactory`, or `UndertowServletWebServerFactory` bean yourself. - -[source,java,indent=0] ----- - @Bean - public ConfigurableServletWebServerFactory webServerFactory() { - TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(); - factory.setPort(9000); - factory.setSessionTimeout(10, TimeUnit.MINUTES); - factory.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/notfound.html")); - return factory; - } ----- - -Setters are provided for many configuration options. -Several protected method "`hooks`" are also provided should you need to do something more exotic. -See the {spring-boot-module-api}/web/servlet/server/ConfigurableServletWebServerFactory.html[source code documentation] for details. - - - -[[boot-features-jsp-limitations]] -==== JSP Limitations -When running a Spring Boot application that uses an embedded servlet container (and is packaged as an executable archive), there are some limitations in the JSP support. - -* With Jetty and Tomcat, it should work if you use war packaging. - An executable war will work when launched with `java -jar`, and will also be deployable to any standard container. - JSPs are not supported when using an executable jar. - -* Undertow does not support JSPs. - -* Creating a custom `error.jsp` page does not override the default view for <>. - <> should be used instead. - -There is a {spring-boot-code}/spring-boot-samples/spring-boot-sample-web-jsp[JSP sample] so that you can see how to set things up. - - - -[[boot-features-reactive-server]] -=== Embedded Reactive Server Support -Spring Boot includes support for the following embedded reactive web servers: Reactor Netty, Tomcat, Jetty, and Undertow. -Most developers use the appropriate “Starter†to obtain a fully configured instance. -By default, the embedded server listens for HTTP requests on port 8080. - - - -[[boot-features-reactive-server-resources]] -=== Reactive Server Resources Configuration -When auto-configuring a Reactor Netty or Jetty server, Spring Boot will create specific beans that will provide HTTP resources to the server instance: `ReactorResourceFactory` or `JettyResourceFactory`. - -By default, those resources will be also shared with the Reactor Netty and Jetty clients for optimal performances, given: - -* the same technology is used for server and client -* the client instance is built using the `WebClient.Builder` bean auto-configured by Spring Boot - -Developers can override the resource configuration for Jetty and Reactor Netty by providing a custom `ReactorResourceFactory` or `JettyResourceFactory` bean - this will be applied to both clients and servers. - -You can learn more about the resource configuration on the client side in the <>. - - - -[[boot-features-rsocket]] -== RSocket -https://rsocket.io[RSocket] is a binary protocol for use on byte stream transports. -It enables symmetric interaction models via async message passing over a single connection. - - -The `spring-messaging` module of the Spring Framework provides support for RSocket requesters and responders, both on the client and on the server side. -See the {spring-framework-docs}web-reactive.html#rsocket-spring[RSocket section] of the Spring Framework reference for more details, including an overview of the RSocket protocol. - - -[[boot-features-rsocket-strategies-auto-configuration]] -=== RSocket Strategies Auto-configuration -Spring Boot auto-configures an `RSocketStrategies` bean that provides all the required infrastructure for encoding and decoding RSocket payloads. -By default, the auto-configuration will try to configure the following (in order): - -. https://cbor.io/[CBOR] codecs with Jackson -. JSON codecs with Jackson - -The `spring-boot-starter-rsocket` starter provides both dependencies. -Check out the <> to know more about customization possibilities. - -Developers can customize the `RSocketStrategies` component by creating beans that implement the `RSocketStrategiesCustomizer` interface. -Note that their `@Order` is important, as it determines the order of codecs. - - -[[boot-features-rsocket-server-auto-configuration]] -=== RSocket server Auto-configuration -Spring Boot provides RSocket server auto-configuration. -The required dependencies are provided by the `spring-boot-starter-rsocket`. - -Spring Boot allows exposing RSocket over WebSocket from a WebFlux server, or standing up an independent RSocket server. -This depends on the type of application and its configuration. - -For WebFlux application (i.e. of type `WebApplicationType.REACTIVE`), the RSocket server will be plugged into the Web Server only if the following properties match: - -[source,properties,indent=0,subs="verbatim,quotes,attributes",configprops] ----- - spring.rsocket.server.mapping-path=/rsocket # a mapping path is defined - spring.rsocket.server.transport=websocket # websocket is chosen as a transport - #spring.rsocket.server.port= # no port is defined ----- - -WARNING: Plugging RSocket into a web server is only supported with Reactor Netty, as RSocket itself is built with that library. - -Alternatively, an RSocket TCP or websocket server is started as an independent, embedded server. -Besides the dependency requirements, the only required configuration is to define a port for that server: - -[source,properties,indent=0,subs="verbatim,quotes,attributes",configprops] ----- - spring.rsocket.server.port=9898 # the only required configuration - spring.rsocket.server.transport=tcp # you're free to configure other properties ----- - - - -[[boot-features-rsocket-messaging]] -=== Spring Messaging RSocket support -Spring Boot will auto-configure the Spring Messaging infrastructure for RSocket. - -This means that Spring Boot will create a `RSocketMessageHandler` bean that will handle RSocket requests to your application. - - -[[boot-features-rsocket-requester]] -=== Calling RSocket Services with `RSocketRequester` -Once the `RSocket` channel is established between server and client, any party can send or receive requests to the other. - -As a server, you can get injected with an `RSocketRequester` instance on any handler method of an RSocket `@Controller`. -As a client, you need to configure and establish an RSocket connection first. -Spring Boot auto-configures an `RSocketRequester.Builder` for such cases with the expected codecs. - -The `RSocketRequester.Builder` instance is a prototype bean, meaning each injection point will provide you with a new instance . -This is done on purpose since this builder is stateful and you shouldn't create requesters with different setups using the same instance. - -The following code shows a typical example: - -[source,java,indent=0] ----- - @Service - public class MyService { - - private final RSocketRequester rsocketRequester; - - public MyService(RSocketRequester.Builder rsocketRequesterBuilder) { - this.rsocketRequester = rsocketRequesterBuilder - .connectTcp("example.org", 9898).block(); - } - - public Mono someRSocketCall(String name) { - return this.requester.route("user").data(name) - .retrieveMono(User.class); - } - - } ----- - - - -[[boot-features-security]] -== Security -If {spring-security}[Spring Security] is on the classpath, then web applications are secured by default. -Spring Boot relies on Spring Security’s content-negotiation strategy to determine whether to use `httpBasic` or `formLogin`. -To add method-level security to a web application, you can also add `@EnableGlobalMethodSecurity` with your desired settings. -Additional information can be found in the {spring-security-docs}#jc-method[Spring Security Reference Guide]. - -The default `UserDetailsService` has a single user. -The user name is `user`, and the password is random and is printed at INFO level when the application starts, as shown in the following example: - -[indent=0] ----- - Using generated security password: 78fa095d-3f4c-48b1-ad50-e24c31d5cf35 ----- - -NOTE: If you fine-tune your logging configuration, ensure that the `org.springframework.boot.autoconfigure.security` category is set to log `INFO`-level messages. -Otherwise, the default password is not printed. - -You can change the username and password by providing a `spring.security.user.name` and `spring.security.user.password`. - -The basic features you get by default in a web application are: - -* A `UserDetailsService` (or `ReactiveUserDetailsService` in case of a WebFlux application) bean with in-memory store and a single user with a generated password (see {spring-boot-module-api}/autoconfigure/security/SecurityProperties.User.html[`SecurityProperties.User`] for the properties of the user). -* Form-based login or HTTP Basic security (depending on the `Accept` header in the request) for the entire application (including actuator endpoints if actuator is on the classpath). -* A `DefaultAuthenticationEventPublisher` for publishing authentication events. - -You can provide a different `AuthenticationEventPublisher` by adding a bean for it. - - - -[[boot-features-security-mvc]] -=== MVC Security -The default security configuration is implemented in `SecurityAutoConfiguration` and `UserDetailsServiceAutoConfiguration`. -`SecurityAutoConfiguration` imports `SpringBootWebSecurityConfiguration` for web security and `UserDetailsServiceAutoConfiguration` configures authentication, which is also relevant in non-web applications. -To switch off the default web application security configuration completely or to combine multiple Spring Security components such as OAuth 2 Client and Resource Server, add a bean of type `WebSecurityConfigurerAdapter` (doing so does not disable the `UserDetailsService` configuration or Actuator's security). - -To also switch off the `UserDetailsService` configuration, you can add a bean of type `UserDetailsService`, `AuthenticationProvider`, or `AuthenticationManager`. - -Access rules can be overridden by adding a custom `WebSecurityConfigurerAdapter`. -Spring Boot provides convenience methods that can be used to override access rules for actuator endpoints and static resources. -`EndpointRequest` can be used to create a `RequestMatcher` that is based on the configprop:management.endpoints.web.base-path[] property. -`PathRequest` can be used to create a `RequestMatcher` for resources in commonly used locations. - - - -[[boot-features-security-webflux]] -=== WebFlux Security -Similar to Spring MVC applications, you can secure your WebFlux applications by adding the `spring-boot-starter-security` dependency. -The default security configuration is implemented in `ReactiveSecurityAutoConfiguration` and `UserDetailsServiceAutoConfiguration`. -`ReactiveSecurityAutoConfiguration` imports `WebFluxSecurityConfiguration` for web security and `UserDetailsServiceAutoConfiguration` configures authentication, which is also relevant in non-web applications. -To switch off the default web application security configuration completely, you can add a bean of type `WebFilterChainProxy` (doing so does not disable the `UserDetailsService` configuration or Actuator's security). - -To also switch off the `UserDetailsService` configuration, you can add a bean of type `ReactiveUserDetailsService` or `ReactiveAuthenticationManager`. - -Access rules and the use of multiple Spring Security components such as OAuth 2 Client and Resource Server can be configured by adding a custom `SecurityWebFilterChain` bean. -Spring Boot provides convenience methods that can be used to override access rules for actuator endpoints and static resources. -`EndpointRequest` can be used to create a `ServerWebExchangeMatcher` that is based on the configprop:management.endpoints.web.base-path[] property. - -`PathRequest` can be used to create a `ServerWebExchangeMatcher` for resources in commonly used locations. - -For example, you can customize your security configuration by adding something like: - -[source,java,indent=0] ----- -include::{code-examples}/web/security/CustomWebFluxSecurityExample.java[tag=configuration] ----- - - - -[[boot-features-security-oauth2]] -=== OAuth2 -https://oauth.net/2/[OAuth2] is a widely used authorization framework that is supported by Spring. - - - -[[boot-features-security-oauth2-client]] -==== Client -If you have `spring-security-oauth2-client` on your classpath, you can take advantage of some auto-configuration to make it easy to set up an OAuth2/Open ID Connect clients. -This configuration makes use of the properties under `OAuth2ClientProperties`. -The same properties are applicable to both servlet and reactive applications. - -You can register multiple OAuth2 clients and providers under the `spring.security.oauth2.client` prefix, as shown in the following example: - -[source,properties,indent=0,configprops] ----- - spring.security.oauth2.client.registration.my-client-1.client-id=abcd - spring.security.oauth2.client.registration.my-client-1.client-secret=password - spring.security.oauth2.client.registration.my-client-1.client-name=Client for user scope - spring.security.oauth2.client.registration.my-client-1.provider=my-oauth-provider - spring.security.oauth2.client.registration.my-client-1.scope=user - spring.security.oauth2.client.registration.my-client-1.redirect-uri=https://my-redirect-uri.com - spring.security.oauth2.client.registration.my-client-1.client-authentication-method=basic - spring.security.oauth2.client.registration.my-client-1.authorization-grant-type=authorization_code - - spring.security.oauth2.client.registration.my-client-2.client-id=abcd - spring.security.oauth2.client.registration.my-client-2.client-secret=password - spring.security.oauth2.client.registration.my-client-2.client-name=Client for email scope - spring.security.oauth2.client.registration.my-client-2.provider=my-oauth-provider - spring.security.oauth2.client.registration.my-client-2.scope=email - spring.security.oauth2.client.registration.my-client-2.redirect-uri=https://my-redirect-uri.com - spring.security.oauth2.client.registration.my-client-2.client-authentication-method=basic - spring.security.oauth2.client.registration.my-client-2.authorization-grant-type=authorization_code - - spring.security.oauth2.client.provider.my-oauth-provider.authorization-uri=https://my-auth-server/oauth/authorize - spring.security.oauth2.client.provider.my-oauth-provider.token-uri=https://my-auth-server/oauth/token - spring.security.oauth2.client.provider.my-oauth-provider.user-info-uri=https://my-auth-server/userinfo - spring.security.oauth2.client.provider.my-oauth-provider.user-info-authentication-method=header - spring.security.oauth2.client.provider.my-oauth-provider.jwk-set-uri=https://my-auth-server/token_keys - spring.security.oauth2.client.provider.my-oauth-provider.user-name-attribute=name ----- - -For OpenID Connect providers that support https://openid.net/specs/openid-connect-discovery-1_0.html[OpenID Connect discovery], the configuration can be further simplified. -The provider needs to be configured with an `issuer-uri` which is the URI that the it asserts as its Issuer Identifier. -For example, if the `issuer-uri` provided is "https://example.com", then an `OpenID Provider Configuration Request` will be made to "https://example.com/.well-known/openid-configuration". -The result is expected to be an `OpenID Provider Configuration Response`. -The following example shows how an OpenID Connect Provider can be configured with the `issuer-uri`: - -[source,properties,indent=0,configprops] ----- - spring.security.oauth2.client.provider.oidc-provider.issuer-uri=https://dev-123456.oktapreview.com/oauth2/default/ ----- - -By default, Spring Security's `OAuth2LoginAuthenticationFilter` only processes URLs matching `/login/oauth2/code/*`. -If you want to customize the `redirect-uri` to use a different pattern, you need to provide configuration to process that custom pattern. -For example, for servlet applications, you can add your own `WebSecurityConfigurerAdapter` that resembles the following: - -[source,java,indent=0] ----- -public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { - - @Override - protected void configure(HttpSecurity http) throws Exception { - http - .authorizeRequests() - .anyRequest().authenticated() - .and() - .oauth2Login() - .redirectionEndpoint() - .baseUri("/custom-callback"); - } -} ----- - - - -[[boot-features-security-oauth2-common-providers]] -===== OAuth2 client registration for common providers -For common OAuth2 and OpenID providers, including Google, Github, Facebook, and Okta, we provide a set of provider defaults (`google`, `github`, `facebook`, and `okta`, respectively). - -If you do not need to customize these providers, you can set the `provider` attribute to the one for which you need to infer defaults. -Also, if the key for the client registration matches a default supported provider, Spring Boot infers that as well. - -In other words, the two configurations in the following example use the Google provider: - -[source,properties,indent=0,configprops] ----- - spring.security.oauth2.client.registration.my-client.client-id=abcd - spring.security.oauth2.client.registration.my-client.client-secret=password - spring.security.oauth2.client.registration.my-client.provider=google - - spring.security.oauth2.client.registration.google.client-id=abcd - spring.security.oauth2.client.registration.google.client-secret=password ----- - - - -[[boot-features-security-oauth2-server]] -==== Resource Server -If you have `spring-security-oauth2-resource-server` on your classpath, Spring Boot can set up an OAuth2 Resource Server. -For JWT configuration, a JWK Set URI or OIDC Issuer URI needs to be specified, as shown in the following examples: - -[source,properties,indent=0,configprops] ----- - spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://example.com/oauth2/default/v1/keys ----- - -[source,properties,indent=0,configprops] ----- - spring.security.oauth2.resourceserver.jwt.issuer-uri=https://dev-123456.oktapreview.com/oauth2/default/ ----- - -NOTE: If the authorization server does not support a JWK Set URI, you can configure the resource server with the Public Key used for verifying the signature of the JWT. -This can be done using the configprop:spring.security.oauth2.resourceserver.jwt.public-key-location[] property, where the value needs to point to a file containing the public key in the PEM-encoded x509 format. - -The same properties are applicable for both servlet and reactive applications. - -Alternatively, you can define your own `JwtDecoder` bean for servlet applications or a `ReactiveJwtDecoder` for reactive applications. - -In cases where opaque tokens are used instead of JWTs, you can configure the following properties to validate tokens via introspection: - -[source,properties,indent=0,configprops] ----- - spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://example.com/check-token - spring.security.oauth2.resourceserver.opaquetoken.client-id=my-client-id - spring.security.oauth2.resourceserver.opaquetoken.client-secret=my-client-secret ----- - -Again, the same properties are applicable for both servlet and reactive applications. - -Alternatively, you can define your own `OpaqueTokenIntrospector` bean for servlet applications or a `ReactiveOpaqueTokenIntrospector` for reactive applications. - - - -==== Authorization Server -Currently, Spring Security does not provide support for implementing an OAuth 2.0 Authorization Server. -However, this functionality is available from the {spring-security-oauth2}[Spring Security OAuth] project, which will eventually be superseded by Spring Security completely. -Until then, you can use the `spring-security-oauth2-autoconfigure` module to easily set up an OAuth 2.0 authorization server; see its https://docs.spring.io/spring-security-oauth2-boot[documentation] for instructions. - - -[[boot-features-security-saml]] -=== SAML 2.0 - - - -[[boot-features-security-saml2-relying-party]] -==== Relying Party -If you have `spring-security-saml2-service-provider` on your classpath, you can take advantage of some auto-configuration to make it easy to set up a SAML 2.0 Relying Party. -This configuration makes use of the properties under `Saml2RelyingPartyProperties`. - -A relying party registration represents a paired configuration between an Identity Provider, IDP, and a Service Provider, SP. -You can register multiple relying parties under the `spring.security.saml2.relyingparty` prefix, as shown in the following example: - -[source,properties,indent=0,configprops] ----- - spring.security.saml2.relyingparty.registration.my-relying-party1.signing.credentials[0].private-key-location=path-to-private-key - spring.security.saml2.relyingparty.registration.my-relying-party1.signing.credentials[0].certificate-location=path-to-certificate - spring.security.saml2.relyingparty.registration.my-relying-party1.identityprovider.verification.credentials[0].certificate-location=path-to-verification-cert - spring.security.saml2.relyingparty.registration.my-relying-party1.identityprovider.entity-id=remote-idp-entity-id1 - spring.security.saml2.relyingparty.registration.my-relying-party1.identityprovider.sso-url=https://remoteidp1.sso.url - - spring.security.saml2.relyingparty.registration.my-relying-party2.signing.credentials[0].private-key-location=path-to-private-key - spring.security.saml2.relyingparty.registration.my-relying-party2.signing.credentials[0].certificate-location=path-to-certificate - spring.security.saml2.relyingparty.registration.my-relying-party2.identityprovider.verification.credentials[0].certificate-location=path-to-other-verification-cert - spring.security.saml2.relyingparty.registration.my-relying-party2.identityprovider.entity-id=remote-idp-entity-id2 - spring.security.saml2.relyingparty.registration.my-relying-party2.identityprovider.sso-url=https://remoteidp2.sso.url ----- - - - -[[boot-features-security-actuator]] -=== Actuator Security -For security purposes, all actuators other than `/health` and `/info` are disabled by default. -The configprop:management.endpoints.web.exposure.include[] property can be used to enable the actuators. - -If Spring Security is on the classpath and no other WebSecurityConfigurerAdapter is present, all actuators other than `/health` and `/info` are secured by Spring Boot auto-configuration. -If you define a custom `WebSecurityConfigurerAdapter`, Spring Boot auto-configuration will back off and you will be in full control of actuator access rules. - -NOTE: Before setting the `management.endpoints.web.exposure.include`, ensure that the exposed actuators do not contain sensitive information and/or are secured by placing them behind a firewall or by something like Spring Security. - - - -[[boot-features-security-csrf]] -==== Cross Site Request Forgery Protection -Since Spring Boot relies on Spring Security's defaults, CSRF protection is turned on by default. -This means that the actuator endpoints that require a `POST` (shutdown and loggers endpoints), `PUT` or `DELETE` will get a 403 forbidden error when the default security configuration is in use. - -NOTE: We recommend disabling CSRF protection completely only if you are creating a service that is used by non-browser clients. - -Additional information about CSRF protection can be found in the {spring-security-docs}#csrf[Spring Security Reference Guide]. - - - -[[boot-features-sql]] -== Working with SQL Databases -The {spring-framework}[Spring Framework] provides extensive support for working with SQL databases, from direct JDBC access using `JdbcTemplate` to complete "`object relational mapping`" technologies such as Hibernate. -{spring-data}[Spring Data] provides an additional level of functionality: creating `Repository` implementations directly from interfaces and using conventions to generate queries from your method names. - - - -[[boot-features-configure-datasource]] -=== Configure a DataSource -Java's `javax.sql.DataSource` interface provides a standard method of working with database connections. -Traditionally, a 'DataSource' uses a `URL` along with some credentials to establish a database connection. - -TIP: See <> for more advanced examples, typically to take full control over the configuration of the DataSource. - - - -[[boot-features-embedded-database-support]] -==== Embedded Database Support -It is often convenient to develop applications by using an in-memory embedded database. -Obviously, in-memory databases do not provide persistent storage. -You need to populate your database when your application starts and be prepared to throw away data when your application ends. - -TIP: The "`How-to`" section includes a <>. - -Spring Boot can auto-configure embedded https://www.h2database.com[H2], http://hsqldb.org/[HSQL], and https://db.apache.org/derby/[Derby] databases. -You need not provide any connection URLs. -You need only include a build dependency to the embedded database that you want to use. - -[NOTE] -==== -If you are using this feature in your tests, you may notice that the same database is reused by your whole test suite regardless of the number of application contexts that you use. -If you want to make sure that each context has a separate embedded database, you should set `spring.datasource.generate-unique-name` to `true`. -==== - -For example, the typical POM dependencies would be as follows: - -[source,xml,indent=0] ----- - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.hsqldb - hsqldb - runtime - ----- - -NOTE: You need a dependency on `spring-jdbc` for an embedded database to be auto-configured. -In this example, it is pulled in transitively through `spring-boot-starter-data-jpa`. - -TIP: If, for whatever reason, you do configure the connection URL for an embedded database, take care to ensure that the database's automatic shutdown is disabled. -If you use H2, you should use `DB_CLOSE_ON_EXIT=FALSE` to do so. -If you use HSQLDB, you should ensure that `shutdown=true` is not used. -Disabling the database's automatic shutdown lets Spring Boot control when the database is closed, thereby ensuring that it happens once access to the database is no longer needed. - - - -[[boot-features-connect-to-production-database]] -==== Connection to a Production Database -Production database connections can also be auto-configured by using a pooling `DataSource`. -Spring Boot uses the following algorithm for choosing a specific implementation: - -. We prefer https://github.com/brettwooldridge/HikariCP[HikariCP] for its performance and concurrency. - If HikariCP is available, we always choose it. -. Otherwise, if the Tomcat pooling `DataSource` is available, we use it. -. If neither HikariCP nor the Tomcat pooling datasource are available and if https://commons.apache.org/proper/commons-dbcp/[Commons DBCP2] is available, we use it. - -If you use the `spring-boot-starter-jdbc` or `spring-boot-starter-data-jpa` "`starters`", you automatically get a dependency to `HikariCP`. - -NOTE: You can bypass that algorithm completely and specify the connection pool to use by setting the configprop:spring.datasource.type[] property. -This is especially important if you run your application in a Tomcat container, as `tomcat-jdbc` is provided by default. - -TIP: Additional connection pools can always be configured manually. -If you define your own `DataSource` bean, auto-configuration does not occur. - -DataSource configuration is controlled by external configuration properties in `+spring.datasource.*+`. -For example, you might declare the following section in `application.properties`: - -[source,properties,indent=0,configprops] ----- - spring.datasource.url=jdbc:mysql://localhost/test - spring.datasource.username=dbuser - spring.datasource.password=dbpass - spring.datasource.driver-class-name=com.mysql.jdbc.Driver ----- - -NOTE: You should at least specify the URL by setting the configprop:spring.datasource.url[] property. -Otherwise, Spring Boot tries to auto-configure an embedded database. - -TIP: You often do not need to specify the `driver-class-name`, since Spring Boot can deduce it for most databases from the `url`. - -NOTE: For a pooling `DataSource` to be created, we need to be able to verify that a valid `Driver` class is available, so we check for that before doing anything. -In other words, if you set `spring.datasource.driver-class-name=com.mysql.jdbc.Driver`, then that class has to be loadable. - -See {spring-boot-autoconfigure-module-code}/jdbc/DataSourceProperties.java[`DataSourceProperties`] for more of the supported options. -These are the standard options that work regardless of the actual implementation. -It is also possible to fine-tune implementation-specific settings by using their respective prefix (`+spring.datasource.hikari.*+`, `+spring.datasource.tomcat.*+`, and `+spring.datasource.dbcp2.*+`). -Refer to the documentation of the connection pool implementation you are using for more details. - -For instance, if you use the https://tomcat.apache.org/tomcat-8.0-doc/jdbc-pool.html#Common_Attributes[Tomcat connection pool], you could customize many additional settings, as shown in the following example: - -[source,properties,indent=0,configprops] ----- - # Number of ms to wait before throwing an exception if no connection is available. - spring.datasource.tomcat.max-wait=10000 - - # Maximum number of active connections that can be allocated from this pool at the same time. - spring.datasource.tomcat.max-active=50 - - # Validate the connection before borrowing it from the pool. - spring.datasource.tomcat.test-on-borrow=true ----- - - - -[[boot-features-connecting-to-a-jndi-datasource]] -==== Connection to a JNDI DataSource -If you deploy your Spring Boot application to an Application Server, you might want to configure and manage your DataSource by using your Application Server's built-in features and access it by using JNDI. - -The configprop:spring.datasource.jndi-name[] property can be used as an alternative to the configprop:spring.datasource.url[], configprop:spring.datasource.username[], and configprop:spring.datasource.password[] properties to access the `DataSource` from a specific JNDI location. -For example, the following section in `application.properties` shows how you can access a JBoss AS defined `DataSource`: - -[source,properties,indent=0,configprops] ----- - spring.datasource.jndi-name=java:jboss/datasources/customers ----- - - - -[[boot-features-using-jdbc-template]] -=== Using JdbcTemplate -Spring's `JdbcTemplate` and `NamedParameterJdbcTemplate` classes are auto-configured, and you can `@Autowire` them directly into your own beans, as shown in the following example: - -[source,java,indent=0] ----- - import org.springframework.beans.factory.annotation.Autowired; - import org.springframework.jdbc.core.JdbcTemplate; - import org.springframework.stereotype.Component; - - @Component - public class MyBean { - - private final JdbcTemplate jdbcTemplate; - - @Autowired - public MyBean(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } - - // ... - - } ----- - -You can customize some properties of the template by using the `spring.jdbc.template.*` properties, as shown in the following example: - -[source,properties,indent=0,configprops] ----- - spring.jdbc.template.max-rows=500 ----- - -NOTE: The `NamedParameterJdbcTemplate` reuses the same `JdbcTemplate` instance behind the scenes. -If more than one `JdbcTemplate` is defined and no primary candidate exists, the `NamedParameterJdbcTemplate` is not auto-configured. - - - -[[boot-features-jpa-and-spring-data]] -=== JPA and Spring Data JPA -The Java Persistence API is a standard technology that lets you "`map`" objects to relational databases. -The `spring-boot-starter-data-jpa` POM provides a quick way to get started. -It provides the following key dependencies: - -* Hibernate: One of the most popular JPA implementations. -* Spring Data JPA: Makes it easy to implement JPA-based repositories. -* Spring ORMs: Core ORM support from the Spring Framework. - -TIP: We do not go into too many details of JPA or {spring-data}[Spring Data] here. -You can follow the https://spring.io/guides/gs/accessing-data-jpa/["`Accessing Data with JPA`"] guide from https://spring.io and read the {spring-data-jpa}[Spring Data JPA] and https://hibernate.org/orm/documentation/[Hibernate] reference documentation. - - - -[[boot-features-entity-classes]] -==== Entity Classes -Traditionally, JPA "`Entity`" classes are specified in a `persistence.xml` file. -With Spring Boot, this file is not necessary and "`Entity Scanning`" is used instead. -By default, all packages below your main configuration class (the one annotated with `@EnableAutoConfiguration` or `@SpringBootApplication`) are searched. - -Any classes annotated with `@Entity`, `@Embeddable`, or `@MappedSuperclass` are considered. -A typical entity class resembles the following example: - -[source,java,indent=0] ----- - package com.example.myapp.domain; - - import java.io.Serializable; - import javax.persistence.*; - - @Entity - public class City implements Serializable { - - @Id - @GeneratedValue - private Long id; - - @Column(nullable = false) - private String name; - - @Column(nullable = false) - private String state; - - // ... additional members, often include @OneToMany mappings - - protected City() { - // no-args constructor required by JPA spec - // this one is protected since it shouldn't be used directly - } - - public City(String name, String state) { - this.name = name; - this.state = state; - } - - public String getName() { - return this.name; - } - - public String getState() { - return this.state; - } - - // ... etc - - } ----- - -TIP: You can customize entity scanning locations by using the `@EntityScan` annotation. -See the "`<>`" how-to. - - - -[[boot-features-spring-data-jpa-repositories]] -==== Spring Data JPA Repositories -{spring-data-jpa}[Spring Data JPA] repositories are interfaces that you can define to access data. -JPA queries are created automatically from your method names. -For example, a `CityRepository` interface might declare a `findAllByState(String state)` method to find all the cities in a given state. - -For more complex queries, you can annotate your method with Spring Data's {spring-data-jpa-api}/repository/Query.html[`Query`] annotation. - -Spring Data repositories usually extend from the {spring-data-commons-api}/repository/Repository.html[`Repository`] or {spring-data-commons-api}/repository/CrudRepository.html[`CrudRepository`] interfaces. -If you use auto-configuration, repositories are searched from the package containing your main configuration class (the one annotated with `@EnableAutoConfiguration` or `@SpringBootApplication`) down. - -The following example shows a typical Spring Data repository interface definition: - -[source,java,indent=0] ----- - package com.example.myapp.domain; - - import org.springframework.data.domain.*; - import org.springframework.data.repository.*; - - public interface CityRepository extends Repository { - - Page findAll(Pageable pageable); - - City findByNameAndStateAllIgnoringCase(String name, String state); - - } ----- - -Spring Data JPA repositories support three different modes of bootstrapping: default, deferred, and lazy. -To enable deferred or lazy bootstrapping, set the configprop:spring.data.jpa.repositories.bootstrap-mode[] property to `deferred` or `lazy` respectively. -When using deferred or lazy bootstrapping, the auto-configured `EntityManagerFactoryBuilder` will use the context's `AsyncTaskExecutor`, if any, as the bootstrap executor. -If more than one exists, the one named `applicationTaskExecutor` will be used. - -TIP: We have barely scratched the surface of Spring Data JPA. -For complete details, see the {spring-data-jdbc-docs}[Spring Data JPA reference documentation]. - - - -[[boot-features-creating-and-dropping-jpa-databases]] -==== Creating and Dropping JPA Databases -By default, JPA databases are automatically created *only* if you use an embedded database (H2, HSQL, or Derby). -You can explicitly configure JPA settings by using `+spring.jpa.*+` properties. -For example, to create and drop tables you can add the following line to your `application.properties`: - -[indent=0] ----- - spring.jpa.hibernate.ddl-auto=create-drop ----- - -NOTE: Hibernate's own internal property name for this (if you happen to remember it better) is `hibernate.hbm2ddl.auto`. -You can set it, along with other Hibernate native properties, by using `+spring.jpa.properties.*+` (the prefix is stripped before adding them to the entity manager). -The following line shows an example of setting JPA properties for Hibernate: - -[indent=0] ----- - spring.jpa.properties.hibernate.globally_quoted_identifiers=true ----- - -The line in the preceding example passes a value of `true` for the `hibernate.globally_quoted_identifiers` property to the Hibernate entity manager. - -By default, the DDL execution (or validation) is deferred until the `ApplicationContext` has started. -There is also a `spring.jpa.generate-ddl` flag, but it is not used if Hibernate auto-configuration is active, because the `ddl-auto` settings are more fine-grained. - - - -[[boot-features-jpa-in-web-environment]] -==== Open EntityManager in View -If you are running a web application, Spring Boot by default registers {spring-framework-api}/orm/jpa/support/OpenEntityManagerInViewInterceptor.html[`OpenEntityManagerInViewInterceptor`] to apply the "`Open EntityManager in View`" pattern, to allow for lazy loading in web views. -If you do not want this behavior, you should set `spring.jpa.open-in-view` to `false` in your `application.properties`. - - - -[[boot-features-data-jdbc]] -=== Spring Data JDBC -Spring Data includes repository support for JDBC and will automatically generate SQL for the methods on `CrudRepository`. -For more advanced queries, a `@Query` annotation is provided. - -Spring Boot will auto-configure Spring Data's JDBC repositories when the necessary dependencies are on the classpath. -They can be added to your project with a single dependency on `spring-boot-starter-data-jdbc`. -If necessary, you can take control of Spring Data JDBC's configuration by adding the `@EnableJdbcRepositories` annotation or a `JdbcConfiguration` subclass to your application. - -TIP: For complete details of Spring Data JDBC, please refer to the {spring-data-jdbc-docs}[reference documentation]. - - - -[[boot-features-sql-h2-console]] -=== Using H2's Web Console -The https://www.h2database.com[H2 database] provides a https://www.h2database.com/html/quickstart.html#h2_console[browser-based console] that Spring Boot can auto-configure for you. -The console is auto-configured when the following conditions are met: - -* You are developing a servlet-based web application. -* `com.h2database:h2` is on the classpath. -* You are using <>. - -TIP: If you are not using Spring Boot's developer tools but would still like to make use of H2's console, you can configure the configprop:spring.h2.console.enabled[] property with a value of `true`. - -NOTE: The H2 console is only intended for use during development, so you should take care to ensure that `spring.h2.console.enabled` is not set to `true` in production. - - - -[[boot-features-sql-h2-console-custom-path]] -==== Changing the H2 Console's Path -By default, the console is available at `/h2-console`. -You can customize the console's path by using the configprop:spring.h2.console.path[] property. - - - -[[boot-features-jooq]] -=== Using jOOQ -jOOQ Object Oriented Querying (https://www.jooq.org/[jOOQ]) is a popular product from https://www.datageekery.com/[Data Geekery] which generates Java code from your database and lets you build type-safe SQL queries through its fluent API. -Both the commercial and open source editions can be used with Spring Boot. - - - -==== Code Generation -In order to use jOOQ type-safe queries, you need to generate Java classes from your database schema. -You can follow the instructions in the {jooq-docs}/#jooq-in-7-steps-step3[jOOQ user manual]. -If you use the `jooq-codegen-maven` plugin and you also use the `spring-boot-starter-parent` "`parent POM`", you can safely omit the plugin's `` tag. -You can also use Spring Boot-defined version variables (such as `h2.version`) to declare the plugin's database dependency. -The following listing shows an example: - -[source,xml,indent=0] ----- - - org.jooq - jooq-codegen-maven - - ... - - - - com.h2database - h2 - ${h2.version} - - - - - org.h2.Driver - jdbc:h2:~/yourdatabase - - - ... - - - ----- - - - -==== Using DSLContext -The fluent API offered by jOOQ is initiated through the `org.jooq.DSLContext` interface. -Spring Boot auto-configures a `DSLContext` as a Spring Bean and connects it to your application `DataSource`. -To use the `DSLContext`, you can `@Autowire` it, as shown in the following example: - -[source,java,indent=0] ----- - @Component - public class JooqExample implements CommandLineRunner { - - private final DSLContext create; - - @Autowired - public JooqExample(DSLContext dslContext) { - this.create = dslContext; - } - - } ----- - -TIP: The jOOQ manual tends to use a variable named `create` to hold the `DSLContext`. - -You can then use the `DSLContext` to construct your queries, as shown in the following example: - -[source,java,indent=0] ----- - public List authorsBornAfter1980() { - return this.create.selectFrom(AUTHOR) - .where(AUTHOR.DATE_OF_BIRTH.greaterThan(new GregorianCalendar(1980, 0, 1))) - .fetch(AUTHOR.DATE_OF_BIRTH); - } ----- - - - -==== jOOQ SQL Dialect -Unless the configprop:spring.jooq.sql-dialect[] property has been configured, Spring Boot determines the SQL dialect to use for your datasource. -If Spring Boot could not detect the dialect, it uses `DEFAULT`. - -NOTE: Spring Boot can only auto-configure dialects supported by the open source version of jOOQ. - - - -==== Customizing jOOQ -More advanced customizations can be achieved by defining your own `@Bean` definitions, which is used when the jOOQ `Configuration` is created. -You can define beans for the following jOOQ Types: - -* `ConnectionProvider` -* `ExecutorProvider` -* `TransactionProvider` -* `RecordMapperProvider` -* `RecordUnmapperProvider` -* `Settings` -* `RecordListenerProvider` -* `ExecuteListenerProvider` -* `VisitListenerProvider` -* `TransactionListenerProvider` - -You can also create your own `org.jooq.Configuration` `@Bean` if you want to take complete control of the jOOQ configuration. - - - -[[boot-features-nosql]] -== Working with NoSQL Technologies -Spring Data provides additional projects that help you access a variety of NoSQL technologies, including: - -* {spring-data-mongodb}[MongoDB] -* {spring-data-neo4j}[Neo4J] -* {spring-data-elasticsearch}[Elasticsearch] -* {spring-data-solr}[Solr] -* {spring-data-redis}[Redis] -* {spring-data-gemfire}[GemFire] or {spring-data-geode}[Geode] -* {spring-data-cassandra}[Cassandra] -* {spring-data-couchbase}[Couchbase] -* {spring-data-ldap}[LDAP] - -Spring Boot provides auto-configuration for Redis, MongoDB, Neo4j, Elasticsearch, Solr Cassandra, Couchbase, and LDAP. -You can make use of the other projects, but you must configure them yourself. -Refer to the appropriate reference documentation at {spring-data}. - - - -[[boot-features-redis]] -=== Redis -https://redis.io/[Redis] is a cache, message broker, and richly-featured key-value store. -Spring Boot offers basic auto-configuration for the https://github.com/lettuce-io/lettuce-core/[Lettuce] and https://github.com/xetorthio/jedis/[Jedis] client libraries and the abstractions on top of them provided by https://github.com/spring-projects/spring-data-redis[Spring Data Redis]. - -There is a `spring-boot-starter-data-redis` "`Starter`" for collecting the dependencies in a convenient way. -By default, it uses https://github.com/lettuce-io/lettuce-core/[Lettuce]. -That starter handles both traditional and reactive applications. - -TIP: we also provide a `spring-boot-starter-data-redis-reactive` "`Starter`" for consistency with the other stores with reactive support. - - - -[[boot-features-connecting-to-redis]] -==== Connecting to Redis -You can inject an auto-configured `RedisConnectionFactory`, `StringRedisTemplate`, or vanilla `RedisTemplate` instance as you would any other Spring Bean. -By default, the instance tries to connect to a Redis server at `localhost:6379`. -The following listing shows an example of such a bean: - -[source,java,indent=0] ----- - @Component - public class MyBean { - - private StringRedisTemplate template; - - @Autowired - public MyBean(StringRedisTemplate template) { - this.template = template; - } - - // ... - - } ----- - -TIP: You can also register an arbitrary number of beans that implement `LettuceClientConfigurationBuilderCustomizer` for more advanced customizations. -If you use Jedis, `JedisClientConfigurationBuilderCustomizer` is also available. - -If you add your own `@Bean` of any of the auto-configured types, it replaces the default (except in the case of `RedisTemplate`, when the exclusion is based on the bean name, `redisTemplate`, not its type). -By default, if `commons-pool2` is on the classpath, you get a pooled connection factory. - - - -[[boot-features-mongodb]] -=== MongoDB -https://www.mongodb.com/[MongoDB] is an open-source NoSQL document database that uses a JSON-like schema instead of traditional table-based relational data. -Spring Boot offers several conveniences for working with MongoDB, including the `spring-boot-starter-data-mongodb` and `spring-boot-starter-data-mongodb-reactive` "`Starters`". - - - -[[boot-features-connecting-to-mongodb]] -==== Connecting to a MongoDB Database -To access Mongo databases, you can inject an auto-configured `org.springframework.data.mongodb.MongoDbFactory`. -By default, the instance tries to connect to a MongoDB server at `mongodb://localhost/test`. -The following example shows how to connect to a MongoDB database: - -[source,java,indent=0] ----- - import org.springframework.data.mongodb.MongoDbFactory; - import com.mongodb.DB; - - @Component - public class MyBean { - - private final MongoDbFactory mongo; - - @Autowired - public MyBean(MongoDbFactory mongo) { - this.mongo = mongo; - } - - // ... - - public void example() { - DB db = mongo.getDb(); - // ... - } - - } ----- - -You can set the configprop:spring.data.mongodb.uri[] property to change the URL and configure additional settings such as the _replica set_, as shown in the following example: - -[source,properties,indent=0,configprops] ----- - spring.data.mongodb.uri=mongodb://user:secret@mongo1.example.com:12345,mongo2.example.com:23456/test ----- - -Alternatively, as long as you use Mongo 2.x, you can specify a `host`/`port`. -For example, you might declare the following settings in your `application.properties`: - -[source,properties,indent=0,configprops] ----- - spring.data.mongodb.host=mongoserver - spring.data.mongodb.port=27017 ----- - -If you have defined your own `MongoClient`, it will be used to auto-configure a suitable `MongoDbFactory`. -Both `com.mongodb.MongoClient` and `com.mongodb.client.MongoClient` are supported. - -NOTE: If you use the Mongo 3.0 Java driver, `spring.data.mongodb.host` and `spring.data.mongodb.port` are not supported. -In such cases, `spring.data.mongodb.uri` should be used to provide all of the configuration. - -TIP: If `spring.data.mongodb.port` is not specified, the default of `27017` is used. -You could delete this line from the example shown earlier. - -TIP: If you do not use Spring Data Mongo, you can inject `com.mongodb.MongoClient` beans instead of using `MongoDbFactory`. -If you want to take complete control of establishing the MongoDB connection, you can also declare your own `MongoDbFactory` or `MongoClient` bean. - -NOTE: If you are using the reactive driver, Netty is required for SSL. -The auto-configuration configures this factory automatically if Netty is available and the factory to use hasn't been customized already. - - - -[[boot-features-mongo-template]] -==== MongoTemplate -{spring-data-mongodb}[Spring Data MongoDB] provides a {spring-data-mongodb-api}/core/MongoTemplate.html[`MongoTemplate`] class that is very similar in its design to Spring's `JdbcTemplate`. -As with `JdbcTemplate`, Spring Boot auto-configures a bean for you to inject the template, as follows: - -[source,java,indent=0] ----- - import org.springframework.beans.factory.annotation.Autowired; - import org.springframework.data.mongodb.core.MongoTemplate; - import org.springframework.stereotype.Component; - - @Component - public class MyBean { - - private final MongoTemplate mongoTemplate; - - @Autowired - public MyBean(MongoTemplate mongoTemplate) { - this.mongoTemplate = mongoTemplate; - } - - // ... - - } ----- - -See the {spring-data-mongodb-api}/core/MongoOperations.html[`MongoOperations` Javadoc] for complete details. - - - -[[boot-features-spring-data-mongo-repositories]] -[[boot-features-spring-data-mongodb-repositories]] -==== Spring Data MongoDB Repositories -Spring Data includes repository support for MongoDB. -As with the JPA repositories discussed earlier, the basic principle is that queries are constructed automatically, based on method names. - -In fact, both Spring Data JPA and Spring Data MongoDB share the same common infrastructure. -You could take the JPA example from earlier and, assuming that `City` is now a Mongo data class rather than a JPA `@Entity`, it works in the same way, as shown in the following example: - -[source,java,indent=0] ----- - package com.example.myapp.domain; - - import org.springframework.data.domain.*; - import org.springframework.data.repository.*; - - public interface CityRepository extends Repository { - - Page findAll(Pageable pageable); - - City findByNameAndStateAllIgnoringCase(String name, String state); - - } ----- - -TIP: You can customize document scanning locations by using the `@EntityScan` annotation. - -TIP: For complete details of Spring Data MongoDB, including its rich object mapping technologies, refer to its {spring-data-mongodb}[reference documentation]. - - - -[[boot-features-mongo-embedded]] -==== Embedded Mongo -Spring Boot offers auto-configuration for https://github.com/flapdoodle-oss/de.flapdoodle.embed.mongo[Embedded Mongo]. -To use it in your Spring Boot application, add a dependency on `de.flapdoodle.embed:de.flapdoodle.embed.mongo`. - -The port that Mongo listens on can be configured by setting the configprop:spring.data.mongodb.port[] property. -To use a randomly allocated free port, use a value of 0. -The `MongoClient` created by `MongoAutoConfiguration` is automatically configured to use the randomly allocated port. - -NOTE: If you do not configure a custom port, the embedded support uses a random port (rather than 27017) by default. - -If you have SLF4J on the classpath, the output produced by Mongo is automatically routed to a logger named `org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongo`. - -You can declare your own `IMongodConfig` and `IRuntimeConfig` beans to take control of the Mongo instance's configuration and logging routing. -The download configuration can be customized by declaring a `DownloadConfigBuilderCustomizer` bean. - - - -[[boot-features-neo4j]] -=== Neo4j -https://neo4j.com/[Neo4j] is an open-source NoSQL graph database that uses a rich data model of nodes connected by first class relationships, which is better suited for connected big data than traditional RDBMS approaches. -Spring Boot offers several conveniences for working with Neo4j, including the `spring-boot-starter-data-neo4j` "`Starter`". - - - -[[boot-features-connecting-to-neo4j]] -==== Connecting to a Neo4j Database -To access a Neo4j server, you can inject an auto-configured `org.neo4j.ogm.session.Session`. -By default, the instance tries to connect to a Neo4j server at `localhost:7687` using the Bolt protocol. -The following example shows how to inject a Neo4j `Session`: - -[source,java,indent=0] ----- - @Component - public class MyBean { - - private final Session session; - - @Autowired - public MyBean(Session session) { - this.session = session; - } - - // ... - - } ----- - -You can configure the uri and credentials to use by setting the `spring.data.neo4j.*` properties, as shown in the following example: - -[source,properties,indent=0,configprops] ----- - spring.data.neo4j.uri=bolt://my-server:7687 - spring.data.neo4j.username=neo4j - spring.data.neo4j.password=secret ----- - -You can take full control over the session creation by adding either an `org.neo4j.ogm.config.Configuration` bean or an `org.neo4j.ogm.session.SessionFactory` bean. - - - -[[boot-features-connecting-to-neo4j-embedded]] -==== Using the Embedded Mode -If you add `org.neo4j:neo4j-ogm-embedded-driver` to the dependencies of your application, Spring Boot automatically configures an in-process embedded instance of Neo4j that does not persist any data when your application shuts down. - -NOTE: As the embedded Neo4j OGM driver does not provide the Neo4j kernel itself, you have to declare `org.neo4j:neo4j` as dependency yourself. -Refer to https://neo4j.com/docs/ogm-manual/current/reference/#reference:getting-started[the Neo4j OGM documentation] for a list of compatible versions. - -The embedded driver takes precedence over the other drivers when there are multiple drivers on the classpath. -You can explicitly disable the embedded mode by setting `spring.data.neo4j.embedded.enabled=false`. - -<> automatically make use of an embedded Neo4j instance if the embedded driver and Neo4j kernel are on the classpath as described above. - -NOTE: You can enable persistence for the embedded mode by providing a path to a database file in your configuration, e.g. `spring.data.neo4j.uri=file://var/tmp/graph.db`. - - - -[[boot-features-neo4j-ogm-native-types]] -==== Using Native Types -Neo4j-OGM can map some types, like those in `java.time.*`, to `String`-based properties or to one of the native types that Neo4j provides. -For backwards compatibility reasons the default for Neo4j-OGM is to use a `String`-based representation. -To use native types, add a dependency on either `org.neo4j:neo4j-ogm-bolt-native-types` or `org.neo4j:neo4j-ogm-embedded-native-types`, and configure the configprop:spring.data.neo4j.use-native-types[] property as shown in the following example: - -[source,properties,indent=0,configprops] ----- - spring.data.neo4j.use-native-types=true ----- - - - -[[boot-features-neo4j-ogm-session]] -==== Neo4jSession -By default, if you are running a web application, the session is bound to the thread for the entire processing of the request (that is, it uses the "Open Session in View" pattern). -If you do not want this behavior, add the following line to your `application.properties` file: - -[source,properties,indent=0,configprops] ----- - spring.data.neo4j.open-in-view=false ----- - - - -[[boot-features-spring-data-neo4j-repositories]] -==== Spring Data Neo4j Repositories -Spring Data includes repository support for Neo4j. - -Spring Data Neo4j shares the common infrastructure with Spring Data JPA as many other Spring Data modules do. -You could take the JPA example from earlier and define `City` as Neo4j OGM `@NodeEntity` rather than JPA `@Entity` and the repository abstraction works in the same way, as shown in the following example: - -[source,java,indent=0] ----- - package com.example.myapp.domain; - - import java.util.Optional; - - import org.springframework.data.neo4j.repository.*; - - public interface CityRepository extends Neo4jRepository { - - Optional findOneByNameAndState(String name, String state); - - } ----- - -The `spring-boot-starter-data-neo4j` "`Starter`" enables the repository support as well as transaction management. -You can customize the locations to look for repositories and entities by using `@EnableNeo4jRepositories` and `@EntityScan` respectively on a `@Configuration`-bean. - -TIP: For complete details of Spring Data Neo4j, including its object mapping technologies, refer to the {spring-data-neo4j-docs}[reference documentation]. - - - -[[boot-features-solr]] -=== Solr -https://lucene.apache.org/solr/[Apache Solr] is a search engine. -Spring Boot offers basic auto-configuration for the Solr 5 client library and the abstractions on top of it provided by https://github.com/spring-projects/spring-data-solr[Spring Data Solr]. -There is a `spring-boot-starter-data-solr` "`Starter`" for collecting the dependencies in a convenient way. - - - -[[boot-features-connecting-to-solr]] -==== Connecting to Solr -You can inject an auto-configured `SolrClient` instance as you would any other Spring bean. -By default, the instance tries to connect to a server at `http://localhost:8983/solr`. -The following example shows how to inject a Solr bean: - -[source,java,indent=0] ----- - @Component - public class MyBean { - - private SolrClient solr; - - @Autowired - public MyBean(SolrClient solr) { - this.solr = solr; - } - - // ... - - } ----- - -If you add your own `@Bean` of type `SolrClient`, it replaces the default. - - - -[[boot-features-spring-data-solr-repositories]] -==== Spring Data Solr Repositories -Spring Data includes repository support for Apache Solr. -As with the JPA repositories discussed earlier, the basic principle is that queries are automatically constructed for you based on method names. - -In fact, both Spring Data JPA and Spring Data Solr share the same common infrastructure. -You could take the JPA example from earlier and, assuming that `City` is now a `@SolrDocument` class rather than a JPA `@Entity`, it works in the same way. - -IP: For complete details of Spring Data Solr, refer to the {spring-data-solr-docs}[reference documentation]. - - - -[[boot-features-elasticsearch]] -=== Elasticsearch -https://www.elastic.co/products/elasticsearch[Elasticsearch] is an open source, distributed, RESTful search and analytics engine. -Spring Boot offers basic auto-configuration for Elasticsearch. - -Spring Boot supports several clients: - -* The official Java "Low Level" and "High Level" REST clients -* The `ReactiveElasticsearchClient` provided by Spring Data Elasticsearch - -The transport client is still available but its support has been deprecated in https://github.com/spring-projects/spring-data-elasticsearch[Spring Data Elasticsearch] and Elasticsearch itself. -It will be removed in a future release. -Spring Boot provides a dedicated "`Starter`", `spring-boot-starter-data-elasticsearch`. - -The https://github.com/searchbox-io/Jest[Jest] client has been deprecated as well, since both Elasticsearch and Spring Data Elasticsearch provide official support for REST clients. - - - -[[boot-features-connecting-to-elasticsearch-rest]] -==== Connecting to Elasticsearch using REST clients -Elasticsearch ships https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/index.html[two different REST clients] that you can use to query a cluster: the "Low Level" client and the "High Level" client. - -If you have the `org.elasticsearch.client:elasticsearch-rest-client` dependency on the classpath, Spring Boot will auto-configure and register a `RestClient` bean that by default targets `http://localhost:9200`. -You can further tune how `RestClient` is configured, as shown in the following example: - -[source,properties,indent=0,configprops] ----- - spring.elasticsearch.rest.uris=https://search.example.com:9200 - spring.elasticsearch.rest.read-timeout=10s - spring.elasticsearch.rest.username=user - spring.elasticsearch.rest.password=secret ----- - -You can also register an arbitrary number of beans that implement `RestClientBuilderCustomizer` for more advanced customizations. -To take full control over the registration, define a `RestClient` bean. - -If you have the `org.elasticsearch.client:elasticsearch-rest-high-level-client` dependency on the classpath, Spring Boot will auto-configure a `RestHighLevelClient`, which wraps any existing `RestClient` bean, reusing its HTTP configuration. - - -[[boot-features-connecting-to-elasticsearch-reactive-rest]] -==== Connecting to Elasticsearch using Reactive REST clients -{spring-data-elasticsearch}[Spring Data Elasticsearch] ships `ReactiveElasticsearchClient` for querying Elasticsearch instances in a reactive fashion. -It is built on top of WebFlux's `WebClient`, so both `spring-boot-starter-elasticsearch` and `spring-boot-starter-webflux` dependencies are useful to enable this support. - -By default, Spring Boot will auto-configure and register a `ReactiveElasticsearchClient` -bean that targets `http://localhost:9200`. -You can further tune how it is configured, as shown in the following example: - -[source,properties,indent=0,configprops] ----- - spring.data.elasticsearch.client.reactive.endpoints=search.example.com:9200 - spring.data.elasticsearch.client.reactive.use-ssl=true - spring.data.elasticsearch.client.reactive.socket-timeout=10s - spring.data.elasticsearch.client.reactive.username=user - spring.data.elasticsearch.client.reactive.password=secret ----- - -If the configuration properties are not enough and you'd like to fully control the client -configuration, you can register a custom `ClientConfiguration` bean. - -[[boot-features-connecting-to-elasticsearch-jest]] -==== Connecting to Elasticsearch using Jest -Now that Spring Boot supports the official `RestHighLevelClient`, Jest support is deprecated. - -If you have `Jest` on the classpath, you can inject an auto-configured `JestClient` that by default targets `http://localhost:9200`. -You can further tune how the client is configured, as shown in the following example: - -[source,properties,indent=0,configprops] ----- - spring.elasticsearch.jest.uris=https://search.example.com:9200 - spring.elasticsearch.jest.read-timeout=10000 - spring.elasticsearch.jest.username=user - spring.elasticsearch.jest.password=secret ----- - -You can also register an arbitrary number of beans that implement `HttpClientConfigBuilderCustomizer` for more advanced customizations. -The following example tunes additional HTTP settings: - -[source,java,indent=0] ----- -include::{code-examples}/elasticsearch/jest/JestClientCustomizationExample.java[tag=customizer] ----- - -To take full control over the registration, define a `JestClient` bean. - - - -[[boot-features-connecting-to-elasticsearch-spring-data]] -==== Connecting to Elasticsearch by Using Spring Data -To connect to Elasticsearch, a `RestHighLevelClient` bean must be defined, -auto-configured by Spring Boot or manually provided by the application (see previous sections). -With this configuration in place, an -`ElasticsearchRestTemplate` can be injected like any other Spring bean, -as shown in the following example: - -[source,java,indent=0] ----- - @Component - public class MyBean { - - private final ElasticsearchRestTemplate template; - - public MyBean(ElasticsearchRestTemplate template) { - this.template = template; - } - - // ... - - } ----- - -In the presence of `spring-data-elasticsearch` and the required dependencies for using a `WebClient` (typically `spring-boot-starter-webflux`), Spring Boot can also auto-configure a <> and a `ReactiveElasticsearchTemplate` as beans. -They are the reactive equivalent of the other REST clients. - - - -[[boot-features-spring-data-elasticsearch-repositories]] -==== Spring Data Elasticsearch Repositories -Spring Data includes repository support for Elasticsearch. -As with the JPA repositories discussed earlier, the basic principle is that queries are constructed for you automatically based on method names. - -In fact, both Spring Data JPA and Spring Data Elasticsearch share the same common infrastructure. -You could take the JPA example from earlier and, assuming that `City` is now an Elasticsearch `@Document` class rather than a JPA `@Entity`, it works in the same way. - -TIP: For complete details of Spring Data Elasticsearch, refer to the {spring-data-elasticsearch-docs}[reference documentation]. - -Spring Boot supports both classic and reactive Elasticsearch repositories, using the `ElasticsearchRestTemplate` or `ReactiveElasticsearchTemplate` beans. -Most likely those beans are auto-configured by Spring Boot given the required dependencies are present. - -If you wish to use your own template for backing the Elasticsearch repositories, you can add your own `ElasticsearchRestTemplate` or `ElasticsearchOperations` `@Bean`, as long as it is named `"elasticsearchTemplate"`. -Same applies to `ReactiveElasticsearchTemplate` and `ReactiveElasticsearchOperations`, with the bean name `"reactiveElasticsearchTemplate"`. - -You can choose to disable the repositories support with the following property: - -[source,properties,indent=0,configprops] ----- - spring.data.elasticsearch.repositories.enabled=false ----- - - -[[boot-features-cassandra]] -=== Cassandra -https://cassandra.apache.org/[Cassandra] is an open source, distributed database management system designed to handle large amounts of data across many commodity servers. -Spring Boot offers auto-configuration for Cassandra and the abstractions on top of it provided by https://github.com/spring-projects/spring-data-cassandra[Spring Data Cassandra]. -There is a `spring-boot-starter-data-cassandra` "`Starter`" for collecting the dependencies in a convenient way. - - - -[[boot-features-connecting-to-cassandra]] -==== Connecting to Cassandra -You can inject an auto-configured `CassandraTemplate` or a Cassandra `Session` instance as you would with any other Spring Bean. -The `spring.data.cassandra.*` properties can be used to customize the connection. -Generally, you provide `keyspace-name` and `contact-points` properties, as shown in the following example: - -[source,properties,indent=0,configprops] ----- - spring.data.cassandra.keyspace-name=mykeyspace - spring.data.cassandra.contact-points=cassandrahost1,cassandrahost2 ----- - -You can also register an arbitrary number of beans that implement `ClusterBuilderCustomizer` for more advanced customizations. - -The following code listing shows how to inject a Cassandra bean: - -[source,java,indent=0] ----- - @Component - public class MyBean { - - private CassandraTemplate template; - - @Autowired - public MyBean(CassandraTemplate template) { - this.template = template; - } - - // ... - - } ----- - -If you add your own `@Bean` of type `CassandraTemplate`, it replaces the default. - - - -[[boot-features-spring-data-cassandra-repositories]] -==== Spring Data Cassandra Repositories -Spring Data includes basic repository support for Cassandra. -Currently, this is more limited than the JPA repositories discussed earlier and needs to annotate finder methods with `@Query`. - -TIP: For complete details of Spring Data Cassandra, refer to the https://docs.spring.io/spring-data/cassandra/docs/[reference documentation]. - - - -[[boot-features-couchbase]] -=== Couchbase -https://www.couchbase.com/[Couchbase] is an open-source, distributed, multi-model NoSQL document-oriented database that is optimized for interactive applications. -Spring Boot offers auto-configuration for Couchbase and the abstractions on top of it provided by https://github.com/spring-projects/spring-data-couchbase[Spring Data Couchbase]. -There are `spring-boot-starter-data-couchbase` and `spring-boot-starter-data-couchbase-reactive` "`Starters`" for collecting the dependencies in a convenient way. - - - -[[boot-features-connecting-to-couchbase]] -==== Connecting to Couchbase -You can get a `Bucket` and `Cluster` by adding the Couchbase SDK and some configuration. -The `spring.couchbase.*` properties can be used to customize the connection. -Generally, you provide the bootstrap hosts, bucket name, and password, as shown in the following example: - -[source,properties,indent=0,configprops] ----- - spring.couchbase.bootstrap-hosts=my-host-1,192.168.1.123 - spring.couchbase.bucket.name=my-bucket - spring.couchbase.bucket.password=secret ----- - -TIP: You need to provide _at least_ the bootstrap host(s), in which case the bucket name is `default` and the password is an empty String. -Alternatively, you can define your own `org.springframework.data.couchbase.config.CouchbaseConfigurer` `@Bean` to take control over the whole configuration. - -It is also possible to customize some of the `CouchbaseEnvironment` settings. -For instance, the following configuration changes the timeout to use to open a new `Bucket` and enables SSL support: - -[source,properties,indent=0,configprops] ----- - spring.couchbase.env.timeouts.connect=3000 - spring.couchbase.env.ssl.key-store=/location/of/keystore.jks - spring.couchbase.env.ssl.key-store-password=secret ----- - -Check the `spring.couchbase.env.*` properties for more details. - - - -[[boot-features-spring-data-couchbase-repositories]] -==== Spring Data Couchbase Repositories -Spring Data includes repository support for Couchbase. -For complete details of Spring Data Couchbase, refer to the https://docs.spring.io/spring-data/couchbase/docs/current/reference/html/[reference documentation]. - -You can inject an auto-configured `CouchbaseTemplate` instance as you would with any other Spring Bean, provided a _default_ `CouchbaseConfigurer` is available (which happens when you enable Couchbase support, as explained earlier). - -The following examples shows how to inject a Couchbase bean: - -[source,java,indent=0] ----- - @Component - public class MyBean { - - private final CouchbaseTemplate template; - - @Autowired - public MyBean(CouchbaseTemplate template) { - this.template = template; - } - - // ... - - } ----- - -There are a few beans that you can define in your own configuration to override those provided by the auto-configuration: - -* A `CouchbaseTemplate` `@Bean` with a name of `couchbaseTemplate`. -* An `IndexManager` `@Bean` with a name of `couchbaseIndexManager`. -* A `CustomConversions` `@Bean` with a name of `couchbaseCustomConversions`. - -To avoid hard-coding those names in your own config, you can reuse `BeanNames` provided by Spring Data Couchbase. -For instance, you can customize the converters to use, as follows: - -[source,java,indent=0] ----- - @Configuration(proxyBeanMethods = false) - public class SomeConfiguration { - - @Bean(BeanNames.COUCHBASE_CUSTOM_CONVERSIONS) - public CustomConversions myCustomConversions() { - return new CustomConversions(...); - } - - // ... - - } ----- - -TIP: If you want to fully bypass the auto-configuration for Spring Data Couchbase, provide your own implementation of `org.springframework.data.couchbase.config.AbstractCouchbaseDataConfiguration`. - - - -[[boot-features-ldap]] -=== LDAP -https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol[LDAP] (Lightweight Directory Access Protocol) is an open, vendor-neutral, industry standard application protocol for accessing and maintaining distributed directory information services over an IP network. -Spring Boot offers auto-configuration for any compliant LDAP server as well as support for the embedded in-memory LDAP server from https://www.ldap.com/unboundid-ldap-sdk-for-java[UnboundID]. - -LDAP abstractions are provided by https://github.com/spring-projects/spring-data-ldap[Spring Data LDAP]. -There is a `spring-boot-starter-data-ldap` "`Starter`" for collecting the dependencies in a convenient way. - - - -[[boot-features-ldap-connecting]] -==== Connecting to an LDAP Server -To connect to an LDAP server, make sure you declare a dependency on the `spring-boot-starter-data-ldap` "`Starter`" or `spring-ldap-core` and then declare the URLs of your server in your application.properties, as shown in the following example: - -[source,properties,indent=0,configprops] ----- - spring.ldap.urls=ldap://myserver:1235 - spring.ldap.username=admin - spring.ldap.password=secret ----- - -If you need to customize connection settings, you can use the `spring.ldap.base` and `spring.ldap.base-environment` properties. - -An `LdapContextSource` is auto-configured based on these settings. -If you need to customize it, for instance to use a `PooledContextSource`, you can still inject the auto-configured `LdapContextSource`. -Make sure to flag your customized `ContextSource` as `@Primary` so that the auto-configured `LdapTemplate` uses it. - - - -[[boot-features-ldap-spring-data-repositories]] -==== Spring Data LDAP Repositories -Spring Data includes repository support for LDAP. -For complete details of Spring Data LDAP, refer to the https://docs.spring.io/spring-data/ldap/docs/1.0.x/reference/html/[reference documentation]. - -You can also inject an auto-configured `LdapTemplate` instance as you would with any other Spring Bean, as shown in the following example: - - -[source,java,indent=0] ----- - @Component - public class MyBean { - - private final LdapTemplate template; - - @Autowired - public MyBean(LdapTemplate template) { - this.template = template; - } - - // ... - - } ----- - - - -[[boot-features-ldap-embedded]] -==== Embedded In-memory LDAP Server -For testing purposes, Spring Boot supports auto-configuration of an in-memory LDAP server from https://www.ldap.com/unboundid-ldap-sdk-for-java[UnboundID]. -To configure the server, add a dependency to `com.unboundid:unboundid-ldapsdk` and declare a configprop:spring.ldap.embedded.base-dn[] property, as follows: - -[source,properties,indent=0,configprops] ----- - spring.ldap.embedded.base-dn=dc=spring,dc=io ----- - -[NOTE] -==== -It is possible to define multiple base-dn values, however, since distinguished names usually contain commas, they must be defined using the correct notation. - -In yaml files, you can use the yaml list notation: - -[source,yaml,indent=0] ----- - spring.ldap.embedded.base-dn: - - dc=spring,dc=io - - dc=pivotal,dc=io ----- - -In properties files, you must include the index as part of the property name: - -[source,properties,indent=0,configprops] ----- - spring.ldap.embedded.base-dn[0]=dc=spring,dc=io - spring.ldap.embedded.base-dn[1]=dc=pivotal,dc=io ----- - -==== - -By default, the server starts on a random port and triggers the regular LDAP support. -There is no need to specify a configprop:spring.ldap.urls[] property. - -If there is a `schema.ldif` file on your classpath, it is used to initialize the server. -If you want to load the initialization script from a different resource, you can also use the configprop:spring.ldap.embedded.ldif[] property. - -By default, a standard schema is used to validate `LDIF` files. -You can turn off validation altogether by setting the configprop:spring.ldap.embedded.validation.enabled[] property. -If you have custom attributes, you can use configprop:spring.ldap.embedded.validation.schema[] to define your custom attribute types or object classes. - - - -[[boot-features-influxdb]] -=== InfluxDB -https://www.influxdata.com/[InfluxDB] is an open-source time series database optimized for fast, high-availability storage and retrieval of time series data in fields such as operations monitoring, application metrics, Internet-of-Things sensor data, and real-time analytics. - - - -[[boot-features-connecting-to-influxdb]] -==== Connecting to InfluxDB -Spring Boot auto-configures an `InfluxDB` instance, provided the `influxdb-java` client is on the classpath and the URL of the database is set, as shown in the following example: - -[source,properties,indent=0,configprops] ----- - spring.influx.url=https://172.0.0.1:8086 ----- - -If the connection to InfluxDB requires a user and password, you can set the `spring.influx.user` and `spring.influx.password` properties accordingly. - -InfluxDB relies on OkHttp. -If you need to tune the http client `InfluxDB` uses behind the scenes, you can register an `InfluxDbOkHttpClientBuilderProvider` bean. - - - -[[boot-features-caching]] -== Caching -The Spring Framework provides support for transparently adding caching to an application. -At its core, the abstraction applies caching to methods, thus reducing the number of executions based on the information available in the cache. -The caching logic is applied transparently, without any interference to the invoker. -Spring Boot auto-configures the cache infrastructure as long as caching support is enabled via the `@EnableCaching` annotation. - -NOTE: Check the {spring-framework-docs}integration.html#cache[relevant section] of the Spring Framework reference for more details. - -In a nutshell, adding caching to an operation of your service is as easy as adding the relevant annotation to its method, as shown in the following example: - -[source,java,indent=0] ----- - import org.springframework.cache.annotation.Cacheable; - import org.springframework.stereotype.Component; - - @Component - public class MathService { - - @Cacheable("piDecimals") - public int computePiDecimal(int i) { - // ... - } - - } ----- - -This example demonstrates the use of caching on a potentially costly operation. -Before invoking `computePiDecimal`, the abstraction looks for an entry in the `piDecimals` cache that matches the `i` argument. -If an entry is found, the content in the cache is immediately returned to the caller, and the method is not invoked. -Otherwise, the method is invoked, and the cache is updated before returning the value. - -CAUTION: You can also use the standard JSR-107 (JCache) annotations (such as `@CacheResult`) transparently. -However, we strongly advise you to not mix and match the Spring Cache and JCache annotations. - -If you do not add any specific cache library, Spring Boot auto-configures a <> that uses concurrent maps in memory. -When a cache is required (such as `piDecimals` in the preceding example), this provider creates it for you. -The simple provider is not really recommended for production usage, but it is great for getting started and making sure that you understand the features. -When you have made up your mind about the cache provider to use, please make sure to read its documentation to figure out how to configure the caches that your application uses. -Nearly all providers require you to explicitly configure every cache that you use in the application. -Some offer a way to customize the default caches defined by the configprop:spring.cache.cache-names[] property. - -TIP: It is also possible to transparently {spring-framework-docs}integration.html#cache-annotations-put[update] or {spring-framework-docs}integration.html#cache-annotations-evict[evict] data from the cache. - - - -[[boot-features-caching-provider]] -=== Supported Cache Providers -The cache abstraction does not provide an actual store and relies on abstraction materialized by the `org.springframework.cache.Cache` and `org.springframework.cache.CacheManager` interfaces. - -If you have not defined a bean of type `CacheManager` or a `CacheResolver` named `cacheResolver` (see {spring-framework-api}/cache/annotation/CachingConfigurer.html[`CachingConfigurer`]), Spring Boot tries to detect the following providers (in the indicated order): - -. <> -. <> (EhCache 3, Hazelcast, Infinispan, and others) -. <> -. <> -. <> -. <> -. <> -. <> -. <> - -TIP: It is also possible to _force_ a particular cache provider by setting the configprop:spring.cache.type[] property. -Use this property if you need to <> in certain environment (such as tests). - -TIP: Use the `spring-boot-starter-cache` "`Starter`" to quickly add basic caching dependencies. -The starter brings in `spring-context-support`. -If you add dependencies manually, you must include `spring-context-support` in order to use the JCache, EhCache 2.x, or Caffeine support. - -If the `CacheManager` is auto-configured by Spring Boot, you can further tune its configuration before it is fully initialized by exposing a bean that implements the `CacheManagerCustomizer` interface. -The following example sets a flag to say that `null` values should be passed down to the underlying map: - -[source,java,indent=0] ----- - @Bean - public CacheManagerCustomizer cacheManagerCustomizer() { - return new CacheManagerCustomizer() { - @Override - public void customize(ConcurrentMapCacheManager cacheManager) { - cacheManager.setAllowNullValues(false); - } - }; - } ----- - -NOTE: In the preceding example, an auto-configured `ConcurrentMapCacheManager` is expected. -If that is not the case (either you provided your own config or a different cache provider was auto-configured), the customizer is not invoked at all. -You can have as many customizers as you want, and you can also order them by using `@Order` or `Ordered`. - - - -[[boot-features-caching-provider-generic]] -==== Generic -Generic caching is used if the context defines _at least_ one `org.springframework.cache.Cache` bean. -A `CacheManager` wrapping all beans of that type is created. - - - -[[boot-features-caching-provider-jcache]] -==== JCache (JSR-107) -https://jcp.org/en/jsr/detail?id=107[JCache] is bootstrapped through the presence of a `javax.cache.spi.CachingProvider` on the classpath (that is, a JSR-107 compliant caching library exists on the classpath), and the `JCacheCacheManager` is provided by the `spring-boot-starter-cache` "`Starter`". -Various compliant libraries are available, and Spring Boot provides dependency management for Ehcache 3, Hazelcast, and Infinispan. -Any other compliant library can be added as well. - -It might happen that more than one provider is present, in which case the provider must be explicitly specified. -Even if the JSR-107 standard does not enforce a standardized way to define the location of the configuration file, Spring Boot does its best to accommodate setting a cache with implementation details, as shown in the following example: - -[source,properties,indent=0,configprops] ----- - # Only necessary if more than one provider is present - spring.cache.jcache.provider=com.acme.MyCachingProvider - spring.cache.jcache.config=classpath:acme.xml ----- - -NOTE: When a cache library offers both a native implementation and JSR-107 support, Spring Boot prefers the JSR-107 support, so that the same features are available if you switch to a different JSR-107 implementation. - -TIP: Spring Boot has <>. -If a single `HazelcastInstance` is available, it is automatically reused for the `CacheManager` as well, unless the configprop:spring.cache.jcache.config[] property is specified. - -There are two ways to customize the underlying `javax.cache.cacheManager`: - -* Caches can be created on startup by setting the configprop:spring.cache.cache-names[] property. - If a custom `javax.cache.configuration.Configuration` bean is defined, it is used to customize them. -* `org.springframework.boot.autoconfigure.cache.JCacheManagerCustomizer` beans are invoked with the reference of the `CacheManager` for full customization. - -TIP: If a standard `javax.cache.CacheManager` bean is defined, it is wrapped automatically in an `org.springframework.cache.CacheManager` implementation that the abstraction expects. -No further customization is applied to it. - - - -[[boot-features-caching-provider-ehcache2]] -==== EhCache 2.x -https://www.ehcache.org/[EhCache] 2.x is used if a file named `ehcache.xml` can be found at the root of the classpath. -If EhCache 2.x is found, the `EhCacheCacheManager` provided by the `spring-boot-starter-cache` "`Starter`" is used to bootstrap the cache manager. -An alternate configuration file can be provided as well, as shown in the following example: - -[source,properties,indent=0,configprops] ----- - spring.cache.ehcache.config=classpath:config/another-config.xml ----- - - - -[[boot-features-caching-provider-hazelcast]] -==== Hazelcast -Spring Boot has <>. -If a `HazelcastInstance` has been auto-configured, it is automatically wrapped in a `CacheManager`. - - - -[[boot-features-caching-provider-infinispan]] -==== Infinispan -https://infinispan.org/[Infinispan] has no default configuration file location, so it must be specified explicitly. -Otherwise, the default bootstrap is used. - -[source,properties,indent=0,configprops] ----- - spring.cache.infinispan.config=infinispan.xml ----- - -Caches can be created on startup by setting the configprop:spring.cache.cache-names[] property. -If a custom `ConfigurationBuilder` bean is defined, it is used to customize the caches. - -NOTE: The support of Infinispan in Spring Boot is restricted to the embedded mode and is quite basic. -If you want more options, you should use the official Infinispan Spring Boot starter instead. -See https://github.com/infinispan/infinispan-spring-boot[Infinispan's documentation] for more details. - - - -[[boot-features-caching-provider-couchbase]] -==== Couchbase -If the https://www.couchbase.com/[Couchbase] Java client and the `couchbase-spring-cache` implementation are available and Couchbase is <>, a `CouchbaseCacheManager` is auto-configured. -It is also possible to create additional caches on startup by setting the configprop:spring.cache.cache-names[] property. -These caches operate on the `Bucket` that was auto-configured. -You can _also_ create additional caches on another `Bucket` by using the customizer. -Assume you need two caches (`cache1` and `cache2`) on the "main" `Bucket` and one (`cache3`) cache with a custom time to live of 2 seconds on the "`another`" `Bucket`. -You can create the first two caches through configuration, as follows: - -[source,properties,indent=0,configprops] ----- - spring.cache.cache-names=cache1,cache2 ----- - -Then you can define a `@Configuration` class to configure the extra `Bucket` and the `cache3` cache, as follows: - -[source,java,indent=0] ----- - @Configuration(proxyBeanMethods = false) - public class CouchbaseCacheConfiguration { - - private final Cluster cluster; - - public CouchbaseCacheConfiguration(Cluster cluster) { - this.cluster = cluster; - } - - @Bean - public Bucket anotherBucket() { - return this.cluster.openBucket("another", "secret"); - } - - @Bean - public CacheManagerCustomizer cacheManagerCustomizer() { - return c -> { - c.prepareCache("cache3", CacheBuilder.newInstance(anotherBucket()) - .withExpiration(2)); - }; - } - - } ----- - -This sample configuration reuses the `Cluster` that was created through auto-configuration. - - - -[[boot-features-caching-provider-redis]] -==== Redis -If https://redis.io/[Redis] is available and configured, a `RedisCacheManager` is auto-configured. -It is possible to create additional caches on startup by setting the configprop:spring.cache.cache-names[] property and cache defaults can be configured by using `spring.cache.redis.*` properties. -For instance, the following configuration creates `cache1` and `cache2` caches with a _time to live_ of 10 minutes: - -[source,properties,indent=0,configprops] ----- - spring.cache.cache-names=cache1,cache2 - spring.cache.redis.time-to-live=600000 ----- - -NOTE: By default, a key prefix is added so that, if two separate caches use the same key, Redis does not have overlapping keys and cannot return invalid values. -We strongly recommend keeping this setting enabled if you create your own `RedisCacheManager`. - -TIP: You can take full control of the configuration by adding a `RedisCacheConfiguration` `@Bean` of your own. -This can be useful if you're looking for customizing the serialization strategy. - - - -[[boot-features-caching-provider-caffeine]] -==== Caffeine -https://github.com/ben-manes/caffeine[Caffeine] is a Java 8 rewrite of Guava's cache that supersedes support for Guava. -If Caffeine is present, a `CaffeineCacheManager` (provided by the `spring-boot-starter-cache` "`Starter`") is auto-configured. -Caches can be created on startup by setting the configprop:spring.cache.cache-names[] property and can be customized by one of the following (in the indicated order): - -. A cache spec defined by `spring.cache.caffeine.spec` -. A `com.github.benmanes.caffeine.cache.CaffeineSpec` bean is defined -. A `com.github.benmanes.caffeine.cache.Caffeine` bean is defined - -For instance, the following configuration creates `cache1` and `cache2` caches with a maximum size of 500 and a _time to live_ of 10 minutes - -[source,properties,indent=0,configprops] ----- - spring.cache.cache-names=cache1,cache2 - spring.cache.caffeine.spec=maximumSize=500,expireAfterAccess=600s ----- - -If a `com.github.benmanes.caffeine.cache.CacheLoader` bean is defined, it is automatically associated to the `CaffeineCacheManager`. -Since the `CacheLoader` is going to be associated with _all_ caches managed by the cache manager, it must be defined as `CacheLoader`. -The auto-configuration ignores any other generic type. - - - -[[boot-features-caching-provider-simple]] -==== Simple -If none of the other providers can be found, a simple implementation using a `ConcurrentHashMap` as the cache store is configured. -This is the default if no caching library is present in your application. -By default, caches are created as needed, but you can restrict the list of available caches by setting the `cache-names` property. -For instance, if you want only `cache1` and `cache2` caches, set the `cache-names` property as follows: - -[source,properties,indent=0,configprops] ----- - spring.cache.cache-names=cache1,cache2 ----- - -If you do so and your application uses a cache not listed, then it fails at runtime when the cache is needed, but not on startup. -This is similar to the way the "real" cache providers behave if you use an undeclared cache. - - - -[[boot-features-caching-provider-none]] -==== None -When `@EnableCaching` is present in your configuration, a suitable cache configuration is expected as well. -If you need to disable caching altogether in certain environments, force the cache type to `none` to use a no-op implementation, as shown in the following example: - -[source,properties,indent=0,configprops] ----- - spring.cache.type=none ----- - - - -[[boot-features-messaging]] -== Messaging -The Spring Framework provides extensive support for integrating with messaging systems, from simplified use of the JMS API using `JmsTemplate` to a complete infrastructure to receive messages asynchronously. -Spring AMQP provides a similar feature set for the Advanced Message Queuing Protocol. -Spring Boot also provides auto-configuration options for `RabbitTemplate` and RabbitMQ. -Spring WebSocket natively includes support for STOMP messaging, and Spring Boot has support for that through starters and a small amount of auto-configuration. -Spring Boot also has support for Apache Kafka. - - - -[[boot-features-jms]] -=== JMS -The `javax.jms.ConnectionFactory` interface provides a standard method of creating a `javax.jms.Connection` for interacting with a JMS broker. -Although Spring needs a `ConnectionFactory` to work with JMS, you generally need not use it directly yourself and can instead rely on higher level messaging abstractions. -(See the {spring-framework-docs}integration.html#jms[relevant section] of the Spring Framework reference documentation for details.) -Spring Boot also auto-configures the necessary infrastructure to send and receive messages. - - - -[[boot-features-activemq]] -==== ActiveMQ Support -When https://activemq.apache.org/[ActiveMQ] is available on the classpath, Spring Boot can also configure a `ConnectionFactory`. -If the broker is present, an embedded broker is automatically started and configured (provided no broker URL is specified through configuration). - -NOTE: If you use `spring-boot-starter-activemq`, the necessary dependencies to connect or embed an ActiveMQ instance are provided, as is the Spring infrastructure to integrate with JMS. - -ActiveMQ configuration is controlled by external configuration properties in `+spring.activemq.*+`. -For example, you might declare the following section in `application.properties`: - -[source,properties,indent=0,configprops] ----- - spring.activemq.broker-url=tcp://192.168.1.210:9876 - spring.activemq.user=admin - spring.activemq.password=secret ----- - -By default, a `CachingConnectionFactory` wraps the native `ConnectionFactory` with sensible settings that you can control by external configuration properties in `+spring.jms.*+`: - -[source,properties,indent=0,configprops] ----- - spring.jms.cache.session-cache-size=5 ----- - -If you'd rather use native pooling, you can do so by adding a dependency to `org.messaginghub:pooled-jms` and configuring the `JmsPoolConnectionFactory` accordingly, as shown in the following example: - -[source,properties,indent=0,configprops] ----- - spring.activemq.pool.enabled=true - spring.activemq.pool.max-connections=50 ----- - -TIP: See {spring-boot-autoconfigure-module-code}/jms/activemq/ActiveMQProperties.java[`ActiveMQProperties`] for more of the supported options. -You can also register an arbitrary number of beans that implement `ActiveMQConnectionFactoryCustomizer` for more advanced customizations. - -By default, ActiveMQ creates a destination if it does not yet exist so that destinations are resolved against their provided names. - - - -[[boot-features-artemis]] -==== Artemis Support -Spring Boot can auto-configure a `ConnectionFactory` when it detects that https://activemq.apache.org/artemis/[Artemis] is available on the classpath. -If the broker is present, an embedded broker is automatically started and configured (unless the mode property has been explicitly set). -The supported modes are `embedded` (to make explicit that an embedded broker is required and that an error should occur if the broker is not available on the classpath) and `native` (to connect to a broker using the `netty` transport protocol). -When the latter is configured, Spring Boot configures a `ConnectionFactory` that connects to a broker running on the local machine with the default settings. - -NOTE: If you use `spring-boot-starter-artemis`, the necessary dependencies to connect to an existing Artemis instance are provided, as well as the Spring infrastructure to integrate with JMS. -Adding `org.apache.activemq:artemis-jms-server` to your application lets you use embedded mode. - -Artemis configuration is controlled by external configuration properties in `+spring.artemis.*+`. -For example, you might declare the following section in `application.properties`: - -[source,properties,indent=0,configprops] ----- - spring.artemis.mode=native - spring.artemis.host=192.168.1.210 - spring.artemis.port=9876 - spring.artemis.user=admin - spring.artemis.password=secret ----- - -When embedding the broker, you can choose if you want to enable persistence and list the destinations that should be made available. -These can be specified as a comma-separated list to create them with the default options, or you can define bean(s) of type `org.apache.activemq.artemis.jms.server.config.JMSQueueConfiguration` or `org.apache.activemq.artemis.jms.server.config.TopicConfiguration`, for advanced queue and topic configurations, respectively. - -By default, a `CachingConnectionFactory` wraps the native `ConnectionFactory` with sensible settings that you can control by external configuration properties in `+spring.jms.*+`: - -[source,properties,indent=0,configprops] ----- - spring.jms.cache.session-cache-size=5 ----- - -If you'd rather use native pooling, you can do so by adding a dependency to `org.messaginghub:pooled-jms` and configuring the `JmsPoolConnectionFactory` accordingly, as shown in the following example: - -[source,properties,indent=0,configprops] ----- - spring.artemis.pool.enabled=true - spring.artemis.pool.max-connections=50 ----- - -See {spring-boot-autoconfigure-module-code}/jms/artemis/ArtemisProperties.java[`ArtemisProperties`] for more supported options. - -No JNDI lookup is involved, and destinations are resolved against their names, using either the `name` attribute in the Artemis configuration or the names provided through configuration. - - - -[[boot-features-jms-jndi]] -==== Using a JNDI ConnectionFactory -If you are running your application in an application server, Spring Boot tries to locate a JMS `ConnectionFactory` by using JNDI. -By default, the `java:/JmsXA` and `java:/XAConnectionFactory` location are checked. -You can use the configprop:spring.jms.jndi-name[] property if you need to specify an alternative location, as shown in the following example: - -[source,properties,indent=0,configprops] ----- - spring.jms.jndi-name=java:/MyConnectionFactory ----- - - - -[[boot-features-using-jms-sending]] -==== Sending a Message -Spring's `JmsTemplate` is auto-configured, and you can autowire it directly into your own beans, as shown in the following example: - -[source,java,indent=0] ----- - import org.springframework.beans.factory.annotation.Autowired; - import org.springframework.jms.core.JmsTemplate; - import org.springframework.stereotype.Component; - - @Component - public class MyBean { - - private final JmsTemplate jmsTemplate; - - @Autowired - public MyBean(JmsTemplate jmsTemplate) { - this.jmsTemplate = jmsTemplate; - } - - // ... - - } ----- - -NOTE: {spring-framework-api}/jms/core/JmsMessagingTemplate.html[`JmsMessagingTemplate`] can be injected in a similar manner. -If a `DestinationResolver` or a `MessageConverter` bean is defined, it is associated automatically to the auto-configured `JmsTemplate`. - - - -[[boot-features-using-jms-receiving]] -==== Receiving a Message -When the JMS infrastructure is present, any bean can be annotated with `@JmsListener` to create a listener endpoint. -If no `JmsListenerContainerFactory` has been defined, a default one is configured automatically. -If a `DestinationResolver` or a `MessageConverter` beans is defined, it is associated automatically to the default factory. - -By default, the default factory is transactional. -If you run in an infrastructure where a `JtaTransactionManager` is present, it is associated to the listener container by default. -If not, the `sessionTransacted` flag is enabled. -In that latter scenario, you can associate your local data store transaction to the processing of an incoming message by adding `@Transactional` on your listener method (or a delegate thereof). -This ensures that the incoming message is acknowledged, once the local transaction has completed. -This also includes sending response messages that have been performed on the same JMS session. - -The following component creates a listener endpoint on the `someQueue` destination: - -[source,java,indent=0] ----- - @Component - public class MyBean { - - @JmsListener(destination = "someQueue") - public void processMessage(String content) { - // ... - } - - } ----- - -TIP: See {spring-framework-api}/jms/annotation/EnableJms.html[the Javadoc of `@EnableJms`] for more details. - -If you need to create more `JmsListenerContainerFactory` instances or if you want to override the default, Spring Boot provides a `DefaultJmsListenerContainerFactoryConfigurer` that you can use to initialize a `DefaultJmsListenerContainerFactory` with the same settings as the one that is auto-configured. - -For instance, the following example exposes another factory that uses a specific `MessageConverter`: - -[source,java,indent=0] ----- - @Configuration(proxyBeanMethods = false) - static class JmsConfiguration { - - @Bean - public DefaultJmsListenerContainerFactory myFactory( - DefaultJmsListenerContainerFactoryConfigurer configurer) { - DefaultJmsListenerContainerFactory factory = - new DefaultJmsListenerContainerFactory(); - configurer.configure(factory, connectionFactory()); - factory.setMessageConverter(myMessageConverter()); - return factory; - } - - } ----- - -Then you can use the factory in any `@JmsListener`-annotated method as follows: - -[source,java,indent=0] -[subs="verbatim,quotes"] ----- - @Component - public class MyBean { - - @JmsListener(destination = "someQueue", **containerFactory="myFactory"**) - public void processMessage(String content) { - // ... - } - - } ----- - - - -[[boot-features-amqp]] -=== AMQP -The Advanced Message Queuing Protocol (AMQP) is a platform-neutral, wire-level protocol for message-oriented middleware. -The Spring AMQP project applies core Spring concepts to the development of AMQP-based messaging solutions. -Spring Boot offers several conveniences for working with AMQP through RabbitMQ, including the `spring-boot-starter-amqp` "`Starter`". - - - -[[boot-features-rabbitmq]] -==== RabbitMQ support -https://www.rabbitmq.com/[RabbitMQ] is a lightweight, reliable, scalable, and portable message broker based on the AMQP protocol. -Spring uses `RabbitMQ` to communicate through the AMQP protocol. - -RabbitMQ configuration is controlled by external configuration properties in `+spring.rabbitmq.*+`. -For example, you might declare the following section in `application.properties`: - -[source,properties,indent=0,configprops] ----- - spring.rabbitmq.host=localhost - spring.rabbitmq.port=5672 - spring.rabbitmq.username=admin - spring.rabbitmq.password=secret ----- - -Alternatively, you could configure the same connection using the `addresses` attributes: - -[source,properties,indent=0] ----- - spring.rabbitmq.addresses=amqp://admin:secret@localhost ----- - -If a `ConnectionNameStrategy` bean exists in the context, it will be automatically used to name connections created by the auto-configured `ConnectionFactory`. -See {spring-boot-autoconfigure-module-code}/amqp/RabbitProperties.java[`RabbitProperties`] for more of the supported options. - -TIP: See https://spring.io/blog/2010/06/14/understanding-amqp-the-protocol-used-by-rabbitmq/[Understanding AMQP, the protocol used by RabbitMQ] for more details. - - - -[[boot-features-using-amqp-sending]] -==== Sending a Message -Spring's `AmqpTemplate` and `AmqpAdmin` are auto-configured, and you can autowire them directly into your own beans, as shown in the following example: - -[source,java,indent=0] ----- - import org.springframework.amqp.core.AmqpAdmin; - import org.springframework.amqp.core.AmqpTemplate; - import org.springframework.beans.factory.annotation.Autowired; - import org.springframework.stereotype.Component; - - @Component - public class MyBean { - - private final AmqpAdmin amqpAdmin; - private final AmqpTemplate amqpTemplate; - - @Autowired - public MyBean(AmqpAdmin amqpAdmin, AmqpTemplate amqpTemplate) { - this.amqpAdmin = amqpAdmin; - this.amqpTemplate = amqpTemplate; - } - - // ... - - } ----- - -NOTE: {spring-amqp-api}/rabbit/core/RabbitMessagingTemplate.html[`RabbitMessagingTemplate`] can be injected in a similar manner. -If a `MessageConverter` bean is defined, it is associated automatically to the auto-configured `AmqpTemplate`. - -If necessary, any `org.springframework.amqp.core.Queue` that is defined as a bean is automatically used to declare a corresponding queue on the RabbitMQ instance. - -To retry operations, you can enable retries on the `AmqpTemplate` (for example, in the event that the broker connection is lost): - -[source,properties,indent=0,configprops] ----- - spring.rabbitmq.template.retry.enabled=true - spring.rabbitmq.template.retry.initial-interval=2s ----- - -Retries are disabled by default. -You can also customize the `RetryTemplate` programmatically by declaring a `RabbitRetryTemplateCustomizer` bean. - - - -[[boot-features-using-amqp-receiving]] -==== Receiving a Message -When the Rabbit infrastructure is present, any bean can be annotated with `@RabbitListener` to create a listener endpoint. -If no `RabbitListenerContainerFactory` has been defined, a default `SimpleRabbitListenerContainerFactory` is automatically configured and you can switch to a direct container using the configprop:spring.rabbitmq.listener.type[] property. -If a `MessageConverter` or a `MessageRecoverer` bean is defined, it is automatically associated with the default factory. - -The following sample component creates a listener endpoint on the `someQueue` queue: - -[source,java,indent=0] ----- - @Component - public class MyBean { - - @RabbitListener(queues = "someQueue") - public void processMessage(String content) { - // ... - } - - } ----- - -TIP: See {spring-amqp-api}/rabbit/annotation/EnableRabbit.html[the Javadoc of `@EnableRabbit`] for more details. - -If you need to create more `RabbitListenerContainerFactory` instances or if you want to override the default, Spring Boot provides a `SimpleRabbitListenerContainerFactoryConfigurer` and a `DirectRabbitListenerContainerFactoryConfigurer` that you can use to initialize a `SimpleRabbitListenerContainerFactory` and a `DirectRabbitListenerContainerFactory` with the same settings as the factories used by the auto-configuration. - -TIP: It does not matter which container type you chose. -Those two beans are exposed by the auto-configuration. - -For instance, the following configuration class exposes another factory that uses a specific `MessageConverter`: - -[source,java,indent=0] ----- - @Configuration(proxyBeanMethods = false) - static class RabbitConfiguration { - - @Bean - public SimpleRabbitListenerContainerFactory myFactory( - SimpleRabbitListenerContainerFactoryConfigurer configurer) { - SimpleRabbitListenerContainerFactory factory = - new SimpleRabbitListenerContainerFactory(); - configurer.configure(factory, connectionFactory); - factory.setMessageConverter(myMessageConverter()); - return factory; - } - - } ----- - -Then you can use the factory in any `@RabbitListener`-annotated method, as follows: - -[source,java,indent=0] -[subs="verbatim,quotes"] ----- - @Component - public class MyBean { - - @RabbitListener(queues = "someQueue", **containerFactory="myFactory"**) - public void processMessage(String content) { - // ... - } - - } ----- - -You can enable retries to handle situations where your listener throws an exception. -By default, `RejectAndDontRequeueRecoverer` is used, but you can define a `MessageRecoverer` of your own. -When retries are exhausted, the message is rejected and either dropped or routed to a dead-letter exchange if the broker is configured to do so. -By default, retries are disabled. -You can also customize the `RetryTemplate` programmatically by declaring a `RabbitRetryTemplateCustomizer` bean. - -IMPORTANT: By default, if retries are disabled and the listener throws an exception, the delivery is retried indefinitely. -You can modify this behavior in two ways: Set the `defaultRequeueRejected` property to `false` so that zero re-deliveries are attempted or throw an `AmqpRejectAndDontRequeueException` to signal the message should be rejected. -The latter is the mechanism used when retries are enabled and the maximum number of delivery attempts is reached. - - - -[[boot-features-kafka]] -=== Apache Kafka Support -https://kafka.apache.org/[Apache Kafka] is supported by providing auto-configuration of the `spring-kafka` project. - -Kafka configuration is controlled by external configuration properties in `spring.kafka.*`. -For example, you might declare the following section in `application.properties`: - -[source,properties,indent=0,configprops] ----- - spring.kafka.bootstrap-servers=localhost:9092 - spring.kafka.consumer.group-id=myGroup ----- - -TIP: To create a topic on startup, add a bean of type `NewTopic`. -If the topic already exists, the bean is ignored. - -See {spring-boot-autoconfigure-module-code}/kafka/KafkaProperties.java[`KafkaProperties`] for more supported options. - - - -[[boot-features-kafka-sending-a-message]] -==== Sending a Message -Spring's `KafkaTemplate` is auto-configured, and you can autowire it directly in your own beans, as shown in the following example: - -[source,java,indent=0] ----- -@Component -public class MyBean { - - private final KafkaTemplate kafkaTemplate; - - @Autowired - public MyBean(KafkaTemplate kafkaTemplate) { - this.kafkaTemplate = kafkaTemplate; - } - - // ... - -} ----- - -NOTE: If the property configprop:spring.kafka.producer.transaction-id-prefix[] is defined, a `KafkaTransactionManager` is automatically configured. -Also, if a `RecordMessageConverter` bean is defined, it is automatically associated to the auto-configured `KafkaTemplate`. - - - -[[boot-features-kafka-receiving-a-message]] -==== Receiving a Message -When the Apache Kafka infrastructure is present, any bean can be annotated with `@KafkaListener` to create a listener endpoint. -If no `KafkaListenerContainerFactory` has been defined, a default one is automatically configured with keys defined in `spring.kafka.listener.*`. - -The following component creates a listener endpoint on the `someTopic` topic: - -[source,java,indent=0] ----- - @Component - public class MyBean { - - @KafkaListener(topics = "someTopic") - public void processMessage(String content) { - // ... - } - - } ----- - -If a `KafkaTransactionManager` bean is defined, it is automatically associated to the container factory. -Similarly, if a `ErrorHandler`, `AfterRollbackProcessor` or `ConsumerAwareRebalanceListener` bean is defined, it is automatically associated to the default factory. - -Depending on the listener type, a `RecordMessageConverter` or `BatchMessageConverter` bean is associated to the default factory. -If only a `RecordMessageConverter` bean is present for a batch listener, it is wrapped in a `BatchMessageConverter`. - -TIP: A custom `ChainedKafkaTransactionManager` must be marked `@Primary` as it usually references the auto-configured `KafkaTransactionManager` bean. - - - -[[boot-features-kafka-streams]] -==== Kafka Streams -Spring for Apache Kafka provides a factory bean to create a `StreamsBuilder` object and manage the lifecycle of its streams. -Spring Boot auto-configures the required `KafkaStreamsConfiguration` bean as long as `kafka-streams` is on the classpath and Kafka Streams is enabled via the `@EnableKafkaStreams` annotation. - -Enabling Kafka Streams means that the application id and bootstrap servers must be set. -The former can be configured using `spring.kafka.streams.application-id`, defaulting to `spring.application.name` if not set. -The latter can be set globally or specifically overridden just for streams. - -Several additional properties are available using dedicated properties; other arbitrary Kafka properties can be set using the `spring.kafka.streams.properties` namespace. -See also <> for more information. - -To use the factory bean, simply wire `StreamsBuilder` into your `@Bean` as shown in the following example: - -[source,java,indent=0] ----- -include::{code-examples}/kafka/KafkaStreamsBeanExample.java[tag=configuration] ----- - -By default, the streams managed by the `StreamBuilder` object it creates are started automatically. -You can customize this behaviour using the configprop:spring.kafka.streams.auto-startup[] property. - - - -[[boot-features-kafka-extra-props]] -==== Additional Kafka Properties -The properties supported by auto configuration are shown in <>. -Note that, for the most part, these properties (hyphenated or camelCase) map directly to the Apache Kafka dotted properties. -Refer to the Apache Kafka documentation for details. - -The first few of these properties apply to all components (producers, consumers, admins, and streams) but can be specified at the component level if you wish to use different values. -Apache Kafka designates properties with an importance of HIGH, MEDIUM, or LOW. -Spring Boot auto-configuration supports all HIGH importance properties, some selected MEDIUM and LOW properties, and any properties that do not have a default value. - -Only a subset of the properties supported by Kafka are available directly through the `KafkaProperties` class. -If you wish to configure the producer or consumer with additional properties that are not directly supported, use the following properties: - -[source,properties,indent=0,configprops] ----- - spring.kafka.properties.prop.one=first - spring.kafka.admin.properties.prop.two=second - spring.kafka.consumer.properties.prop.three=third - spring.kafka.producer.properties.prop.four=fourth - spring.kafka.streams.properties.prop.five=fifth ----- - -This sets the common `prop.one` Kafka property to `first` (applies to producers, consumers and admins), the `prop.two` admin property to `second`, the `prop.three` consumer property to `third`, the `prop.four` producer property to `fourth` and the `prop.five` streams property to `fifth`. - -You can also configure the Spring Kafka `JsonDeserializer` as follows: - -[source,properties,indent=0,configprops] ----- - spring.kafka.consumer.value-deserializer=org.springframework.kafka.support.serializer.JsonDeserializer - spring.kafka.consumer.properties.spring.json.value.default.type=com.example.Invoice - spring.kafka.consumer.properties.spring.json.trusted.packages=com.example,org.acme ----- - -Similarly, you can disable the `JsonSerializer` default behavior of sending type information in headers: - -[source,properties,indent=0,configprops] ----- - spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer - spring.kafka.producer.properties.spring.json.add.type.headers=false ----- - -IMPORTANT: Properties set in this way override any configuration item that Spring Boot explicitly supports. - - - -[[boot-features-embedded-kafka]] -==== Testing with Embedded Kafka -Spring for Apache Kafka provides a convenient way to test projects with an embedded Apache Kafka broker. -To use this feature, annotate a test class with `@EmbeddedKafka` from the `spring-kafka-test` module. -For more information, please see the Spring for Apache Kafka https://docs.spring.io/spring-kafka/docs/current/reference/html/#embedded-kafka-annotation[reference manual]. - -To make Spring Boot auto-configuration work with the aforementioned embedded Apache Kafka broker, you need to remap a system property for embedded broker addresses (populated by the `EmbeddedKafkaBroker`) into the Spring Boot configuration property for Apache Kafka. -There are several ways to do that: - -* Provide a system property to map embedded broker addresses into configprop:spring.kafka.bootstrap-servers[] in the test class: - -[source,java,indent=0] ----- -static { - System.setProperty(EmbeddedKafkaBroker.BROKER_LIST_PROPERTY, "spring.kafka.bootstrap-servers"); -} ----- - -* Configure a property name on the `@EmbeddedKafka` annotation: - -[source,java,indent=0] ----- -@EmbeddedKafka(topics = "someTopic", - bootstrapServersProperty = "spring.kafka.bootstrap-servers") ----- - -* Use a placeholder in configuration properties: - -[source,properties,indent=0,configprops] ----- -spring.kafka.bootstrap-servers=${spring.embedded.kafka.brokers} ----- - - - -[[boot-features-resttemplate]] -== Calling REST Services with `RestTemplate` -If you need to call remote REST services from your application, you can use the Spring Framework's {spring-framework-api}/web/client/RestTemplate.html[`RestTemplate`] class. -Since `RestTemplate` instances often need to be customized before being used, Spring Boot does not provide any single auto-configured `RestTemplate` bean. -It does, however, auto-configure a `RestTemplateBuilder`, which can be used to create `RestTemplate` instances when needed. -The auto-configured `RestTemplateBuilder` ensures that sensible `HttpMessageConverters` are applied to `RestTemplate` instances. - -The following code shows a typical example: - -[source,java,indent=0] ----- - @Service - public class MyService { - - private final RestTemplate restTemplate; - - public MyService(RestTemplateBuilder restTemplateBuilder) { - this.restTemplate = restTemplateBuilder.build(); - } - - public Details someRestCall(String name) { - return this.restTemplate.getForObject("/{name}/details", Details.class, name); - } - - } ----- - -TIP: `RestTemplateBuilder` includes a number of useful methods that can be used to quickly configure a `RestTemplate`. -For example, to add BASIC auth support, you can use `builder.basicAuthentication("user", "password").build()`. - - - -[[boot-features-resttemplate-customization]] -=== RestTemplate Customization -There are three main approaches to `RestTemplate` customization, depending on how broadly you want the customizations to apply. - -To make the scope of any customizations as narrow as possible, inject the auto-configured `RestTemplateBuilder` and then call its methods as required. -Each method call returns a new `RestTemplateBuilder` instance, so the customizations only affect this use of the builder. - -To make an application-wide, additive customization, use a `RestTemplateCustomizer` bean. -All such beans are automatically registered with the auto-configured `RestTemplateBuilder` and are applied to any templates that are built with it. - -The following example shows a customizer that configures the use of a proxy for all hosts except `192.168.0.5`: - -[source,java,indent=0] ----- -include::{code-examples}/web/client/RestTemplateProxyCustomizationExample.java[tag=customizer] ----- - -Finally, the most extreme (and rarely used) option is to create your own `RestTemplateBuilder` bean. -Doing so switches off the auto-configuration of a `RestTemplateBuilder` and prevents any `RestTemplateCustomizer` beans from being used. - - - -[[boot-features-webclient]] -== Calling REST Services with `WebClient` -If you have Spring WebFlux on your classpath, you can also choose to use `WebClient` to call remote REST services. -Compared to `RestTemplate`, this client has a more functional feel and is fully reactive. -You can learn more about the `WebClient` in the dedicated {spring-framework-docs}web-reactive.html#webflux-client[section in the Spring Framework docs]. - -Spring Boot creates and pre-configures a `WebClient.Builder` for you; it is strongly advised to inject it in your components and use it to create `WebClient` instances. -Spring Boot is configuring that builder to share HTTP resources, reflect codecs setup in the same fashion as the server ones (see <>), and more. - -The following code shows a typical example: - -[source,java,indent=0] ----- - @Service - public class MyService { - - private final WebClient webClient; - - public MyService(WebClient.Builder webClientBuilder) { - this.webClient = webClientBuilder.baseUrl("https://example.org").build(); - } - - public Mono
    someRestCall(String name) { - return this.webClient.get().uri("/{name}/details", name) - .retrieve().bodyToMono(Details.class); - } - - } ----- - - - -[[boot-features-webclient-runtime]] -=== WebClient Runtime -Spring Boot will auto-detect which `ClientHttpConnector` to use to drive `WebClient`, depending on the libraries available on the application classpath. -For now, Reactor Netty and Jetty RS client are supported. - -The `spring-boot-starter-webflux` starter depends on `io.projectreactor.netty:reactor-netty` by default, which brings both server and client implementations. -If you choose to use Jetty as a reactive server instead, you should add a dependency on the Jetty Reactive HTTP client library, `org.eclipse.jetty:jetty-reactive-httpclient`. -Using the same technology for server and client has it advantages, as it will automatically share HTTP resources between client and server. - -Developers can override the resource configuration for Jetty and Reactor Netty by providing a custom `ReactorResourceFactory` or `JettyResourceFactory` bean - this will be applied to both clients and servers. - -If you wish to override that choice for the client, you can define your own `ClientHttpConnector` bean and have full control over the client configuration. - -You can learn more about the {spring-framework-docs}web-reactive.html#webflux-client-builder[`WebClient` configuration options in the Spring Framework reference documentation]. - - - -[[boot-features-webclient-customization]] -=== WebClient Customization -There are three main approaches to `WebClient` customization, depending on how broadly you want the customizations to apply. - -To make the scope of any customizations as narrow as possible, inject the auto-configured `WebClient.Builder` and then call its methods as required. -`WebClient.Builder` instances are stateful: Any change on the builder is reflected in all clients subsequently created with it. -If you want to create several clients with the same builder, you can also consider cloning the builder with `WebClient.Builder other = builder.clone();`. - -To make an application-wide, additive customization to all `WebClient.Builder` instances, you can declare `WebClientCustomizer` beans and change the `WebClient.Builder` locally at the point of injection. - -Finally, you can fall back to the original API and use `WebClient.create()`. -In that case, no auto-configuration or `WebClientCustomizer` is applied. - - - -[[boot-features-validation]] -== Validation -The method validation feature supported by Bean Validation 1.1 is automatically enabled as long as a JSR-303 implementation (such as Hibernate validator) is on the classpath. -This lets bean methods be annotated with `javax.validation` constraints on their parameters and/or on their return value. -Target classes with such annotated methods need to be annotated with the `@Validated` annotation at the type level for their methods to be searched for inline constraint annotations. - -For instance, the following service triggers the validation of the first argument, making sure its size is between 8 and 10: - -[source,java,indent=0] ----- - @Service - @Validated - public class MyBean { - - public Archive findByCodeAndAuthor(@Size(min = 8, max = 10) String code, - Author author) { - ... - } - - } ----- - - - -[[boot-features-email]] -== Sending Email -The Spring Framework provides an easy abstraction for sending email by using the `JavaMailSender` interface, and Spring Boot provides auto-configuration for it as well as a starter module. - -TIP: See the {spring-framework-docs}integration.html#mail[reference documentation] for a detailed explanation of how you can use `JavaMailSender`. - -If `spring.mail.host` and the relevant libraries (as defined by `spring-boot-starter-mail`) are available, a default `JavaMailSender` is created if none exists. -The sender can be further customized by configuration items from the `spring.mail` namespace. -See {spring-boot-autoconfigure-module-code}/mail/MailProperties.java[`MailProperties`] for more details. - -In particular, certain default timeout values are infinite, and you may want to change that to avoid having a thread blocked by an unresponsive mail server, as shown in the following example: - -[source,properties,indent=0,configprops] ----- - spring.mail.properties.mail.smtp.connectiontimeout=5000 - spring.mail.properties.mail.smtp.timeout=3000 - spring.mail.properties.mail.smtp.writetimeout=5000 ----- - -It is also possible to configure a `JavaMailSender` with an existing `Session` from JNDI: - -[source,properties,indent=0,configprops] ----- - spring.mail.jndi-name=mail/Session ----- - -When a `jndi-name` is set, it takes precedence over all other Session-related settings. - - - -[[boot-features-jta]] -== Distributed Transactions with JTA -Spring Boot supports distributed JTA transactions across multiple XA resources by using either an https://www.atomikos.com/[Atomikos] or https://github.com/bitronix/btm[Bitronix] embedded transaction manager. -JTA transactions are also supported when deploying to a suitable Java EE Application Server. - -When a JTA environment is detected, Spring's `JtaTransactionManager` is used to manage transactions. -Auto-configured JMS, DataSource, and JPA beans are upgraded to support XA transactions. -You can use standard Spring idioms, such as `@Transactional`, to participate in a distributed transaction. -If you are within a JTA environment and still want to use local transactions, you can set the configprop:spring.jta.enabled[] property to `false` to disable the JTA auto-configuration. - - - -[[boot-features-jta-atomikos]] -=== Using an Atomikos Transaction Manager -https://www.atomikos.com/[Atomikos] is a popular open source transaction manager which can be embedded into your Spring Boot application. -You can use the `spring-boot-starter-jta-atomikos` starter to pull in the appropriate Atomikos libraries. -Spring Boot auto-configures Atomikos and ensures that appropriate `depends-on` settings are applied to your Spring beans for correct startup and shutdown ordering. - -By default, Atomikos transaction logs are written to a `transaction-logs` directory in your application's home directory (the directory in which your application jar file resides). -You can customize the location of this directory by setting a configprop:spring.jta.log-dir[] property in your `application.properties` file. -Properties starting with `spring.jta.atomikos.properties` can also be used to customize the Atomikos `UserTransactionServiceImp`. -See the {spring-boot-module-api}/jta/atomikos/AtomikosProperties.html[`AtomikosProperties` Javadoc] for complete details. - -NOTE: To ensure that multiple transaction managers can safely coordinate the same resource managers, each Atomikos instance must be configured with a unique ID. -By default, this ID is the IP address of the machine on which Atomikos is running. -To ensure uniqueness in production, you should configure the configprop:spring.jta.transaction-manager-id[] property with a different value for each instance of your application. - - - -[[boot-features-jta-bitronix]] -=== Using a Bitronix Transaction Manager -https://github.com/bitronix/btm[Bitronix] is a popular open-source JTA transaction manager implementation. -You can use the `spring-boot-starter-jta-bitronix` starter to add the appropriate Bitronix dependencies to your project. -As with Atomikos, Spring Boot automatically configures Bitronix and post-processes your beans to ensure that startup and shutdown ordering is correct. - -By default, Bitronix transaction log files (`part1.btm` and `part2.btm`) are written to a `transaction-logs` directory in your application home directory. -You can customize the location of this directory by setting the configprop:spring.jta.log-dir[] property. -Properties starting with `spring.jta.bitronix.properties` are also bound to the `bitronix.tm.Configuration` bean, allowing for complete customization. -See the https://github.com/bitronix/btm/wiki/Transaction-manager-configuration[Bitronix documentation] for details. - -NOTE: To ensure that multiple transaction managers can safely coordinate the same resource managers, each Bitronix instance must be configured with a unique ID. -By default, this ID is the IP address of the machine on which Bitronix is running. -To ensure uniqueness in production, you should configure the configprop:spring.jta.transaction-manager-id[] property with a different value for each instance of your application. - - - -[[boot-features-jta-javaee]] -=== Using a Java EE Managed Transaction Manager -If you package your Spring Boot application as a `war` or `ear` file and deploy it to a Java EE application server, you can use your application server's built-in transaction manager. -Spring Boot tries to auto-configure a transaction manager by looking at common JNDI locations (`java:comp/UserTransaction`, `java:comp/TransactionManager`, and so on). -If you use a transaction service provided by your application server, you generally also want to ensure that all resources are managed by the server and exposed over JNDI. -Spring Boot tries to auto-configure JMS by looking for a `ConnectionFactory` at the JNDI path (`java:/JmsXA` or `java:/XAConnectionFactory`), and you can use the <> to configure your `DataSource`. - - - -[[boot-features-jta-mixed-jms]] -=== Mixing XA and Non-XA JMS Connections -When using JTA, the primary JMS `ConnectionFactory` bean is XA-aware and participates in distributed transactions. -In some situations, you might want to process certain JMS messages by using a non-XA `ConnectionFactory`. -For example, your JMS processing logic might take longer than the XA timeout. - -If you want to use a non-XA `ConnectionFactory`, you can inject the `nonXaJmsConnectionFactory` bean rather than the `@Primary` `jmsConnectionFactory` bean. -For consistency, the `jmsConnectionFactory` bean is also provided by using the bean alias `xaJmsConnectionFactory`. - -The following example shows how to inject `ConnectionFactory` instances: - -[source,java,indent=0,subs="verbatim,quotes,attributes"] ----- - // Inject the primary (XA aware) ConnectionFactory - @Autowired - private ConnectionFactory defaultConnectionFactory; - - // Inject the XA aware ConnectionFactory (uses the alias and injects the same as above) - @Autowired - @Qualifier("xaJmsConnectionFactory") - private ConnectionFactory xaConnectionFactory; - - // Inject the non-XA aware ConnectionFactory - @Autowired - @Qualifier("nonXaJmsConnectionFactory") - private ConnectionFactory nonXaConnectionFactory; ----- - - - -[[boot-features-jta-supporting-alternative-embedded]] -=== Supporting an Alternative Embedded Transaction Manager -The {spring-boot-module-code}/jms/XAConnectionFactoryWrapper.java[`XAConnectionFactoryWrapper`] and {spring-boot-module-code}/jdbc/XADataSourceWrapper.java[`XADataSourceWrapper`] interfaces can be used to support alternative embedded transaction managers. -The interfaces are responsible for wrapping `XAConnectionFactory` and `XADataSource` beans and exposing them as regular `ConnectionFactory` and `DataSource` beans, which transparently enroll in the distributed transaction. -DataSource and JMS auto-configuration use JTA variants, provided you have a `JtaTransactionManager` bean and appropriate XA wrapper beans registered within your `ApplicationContext`. - -The {spring-boot-module-code}/jta/bitronix/BitronixXAConnectionFactoryWrapper.java[BitronixXAConnectionFactoryWrapper] and {spring-boot-module-code}/jta/bitronix/BitronixXADataSourceWrapper.java[BitronixXADataSourceWrapper] provide good examples of how to write XA wrappers. - - - -[[boot-features-hazelcast]] -== Hazelcast -If https://hazelcast.com/[Hazelcast] is on the classpath and a suitable configuration is found, Spring Boot auto-configures a `HazelcastInstance` that you can inject in your application. - -If you define a `com.hazelcast.config.Config` bean, Spring Boot uses that. -If your configuration defines an instance name, Spring Boot tries to locate an existing instance rather than creating a new one. - -If you define a `com.hazelcast.config.Config` bean, Spring Boot uses that. -If your configuration defines an instance name, Spring Boot tries to locate an existing instance rather than creating a new one. - -You could also specify the Hazelcast configuration file to use through configuration, as shown in the following example: - -[source,properties,indent=0,configprops] ----- - spring.hazelcast.config=classpath:config/my-hazelcast.xml ----- - -Otherwise, Spring Boot tries to find the Hazelcast configuration from the default locations: `hazelcast.xml` in the working directory or at the root of the classpath, or a `.yaml` counterpart in the same locations. -We also check if the `hazelcast.config` system property is set. -See the https://docs.hazelcast.org/docs/latest/manual/html-single/[Hazelcast documentation] for more details. - -If `hazelcast-client` is present on the classpath, Spring Boot first attempts to create a client by checking the following configuration options: - -* The presence of a `com.hazelcast.client.config.ClientConfig` bean. -* A configuration file defined by the configprop:spring.hazelcast.config[] property. -* The presence of the `hazelcast.client.config` system property. -* A `hazelcast-client.xml` in the working directory or at the root of the classpath. -* A `hazelcast-client.yaml` in the working directory or at the root of the classpath. - -NOTE: Spring Boot also has <>. -If caching is enabled, the `HazelcastInstance` is automatically wrapped in a `CacheManager` implementation. - - - -[[boot-features-quartz]] -== Quartz Scheduler -Spring Boot offers several conveniences for working with the https://www.quartz-scheduler.org/[Quartz scheduler], including the `spring-boot-starter-quartz` "`Starter`". -If Quartz is available, a `Scheduler` is auto-configured (through the `SchedulerFactoryBean` abstraction). - -Beans of the following types are automatically picked up and associated with the `Scheduler`: - -* `JobDetail`: defines a particular Job. - `JobDetail` instances can be built with the `JobBuilder` API. -* `Calendar`. -* `Trigger`: defines when a particular job is triggered. - -By default, an in-memory `JobStore` is used. -However, it is possible to configure a JDBC-based store if a `DataSource` bean is available in your application and if the configprop:spring.quartz.job-store-type[] property is configured accordingly, as shown in the following example: - -[source,properties,indent=0,configprops] ----- - spring.quartz.job-store-type=jdbc ----- - -When the JDBC store is used, the schema can be initialized on startup, as shown in the following example: - -[source,properties,indent=0,configprops] ----- - spring.quartz.jdbc.initialize-schema=always ----- - -WARNING: By default, the database is detected and initialized by using the standard scripts provided with the Quartz library. -These scripts drop existing tables, deleting all triggers on every restart. -It is also possible to provide a custom script by setting the configprop:spring.quartz.jdbc.schema[] property. - -To have Quartz use a `DataSource` other than the application's main `DataSource`, declare a `DataSource` bean, annotating its `@Bean` method with `@QuartzDataSource`. -Doing so ensures that the Quartz-specific `DataSource` is used by both the `SchedulerFactoryBean` and for schema initialization. - -By default, jobs created by configuration will not overwrite already registered jobs that have been read from a persistent job store. -To enable overwriting existing job definitions set the configprop:spring.quartz.overwrite-existing-jobs[] property. - -Quartz Scheduler configuration can be customized using `spring.quartz` properties and `SchedulerFactoryBeanCustomizer` beans, which allow programmatic `SchedulerFactoryBean` customization. -Advanced Quartz configuration properties can be customized using `spring.quartz.properties.*`. - -NOTE: In particular, an `Executor` bean is not associated with the scheduler as Quartz offers a way to configure the scheduler via `spring.quartz.properties`. -If you need to customize the task executor, consider implementing `SchedulerFactoryBeanCustomizer`. - -Jobs can define setters to inject data map properties. -Regular beans can also be injected in a similar manner, as shown in the following example: - -[source,java,indent=0] ----- - public class SampleJob extends QuartzJobBean { - - private MyService myService; - - private String name; - - // Inject "MyService" bean - public void setMyService(MyService myService) { ... } - - // Inject the "name" job data property - public void setName(String name) { ... } - - @Override - protected void executeInternal(JobExecutionContext context) - throws JobExecutionException { - ... - } - - } ----- - - - -[[boot-features-task-execution-scheduling]] -== Task Execution and Scheduling -In the absence of an `Executor` bean in the context, Spring Boot auto-configures a `ThreadPoolTaskExecutor` with sensible defaults that can be automatically associated to asynchronous task execution (`@EnableAsync`) and Spring MVC asynchronous request processing. - -[TIP] -==== -If you have defined a custom `Executor` in the context, regular task execution (i.e. `@EnableAsync`) will use it transparently but the Spring MVC support will not be configured as it requires an `AsyncTaskExecutor` implementation (named `applicationTaskExecutor`). -Depending on your target arrangement, you could change your `Executor` into a `ThreadPoolTaskExecutor` or define both a `ThreadPoolTaskExecutor` and an `AsyncConfigurer` wrapping your custom `Executor`. - -The auto-configured `TaskExecutorBuilder` allows you to easily create instances that reproduce what the auto-configuration does by default. -==== - -The thread pool uses 8 core threads that can grow and shrink according to the load. -Those default settings can be fine-tuned using the `spring.task.execution` namespace as shown in the following example: - -[source,properties,indent=0,configprops] ----- - spring.task.execution.pool.max-size=16 - spring.task.execution.pool.queue-capacity=100 - spring.task.execution.pool.keep-alive=10s ----- - -This changes the thread pool to use a bounded queue so that when the queue is full (100 tasks), the thread pool increases to maximum 16 threads. -Shrinking of the pool is more aggressive as threads are reclaimed when they are idle for 10 seconds (rather than 60 seconds by default). - -A `ThreadPoolTaskScheduler` can also be auto-configured if need to be associated to scheduled task execution (`@EnableScheduling`). -The thread pool uses one thread by default and those settings can be fine-tuned using the `spring.task.scheduling` namespace. - -Both a `TaskExecutorBuilder` bean and a `TaskSchedulerBuilder` bean are made available in the context if a custom executor or scheduler needs to be created. - - - -[[boot-features-integration]] -== Spring Integration -Spring Boot offers several conveniences for working with {spring-integration}[Spring Integration], including the `spring-boot-starter-integration` "`Starter`". -Spring Integration provides abstractions over messaging and also other transports such as HTTP, TCP, and others. -If Spring Integration is available on your classpath, it is initialized through the `@EnableIntegration` annotation. - -Spring Boot also configures some features that are triggered by the presence of additional Spring Integration modules. -If `spring-integration-jmx` is also on the classpath, message processing statistics are published over JMX. -If `spring-integration-jdbc` is available, the default database schema can be created on startup, as shown in the following line: - -[source,properties,indent=0,configprops] ----- - spring.integration.jdbc.initialize-schema=always ----- - -See the {spring-boot-autoconfigure-module-code}/integration/IntegrationAutoConfiguration.java[`IntegrationAutoConfiguration`] and {spring-boot-autoconfigure-module-code}/integration/IntegrationProperties.java[`IntegrationProperties`] classes for more details. - -By default, if a Micrometer `meterRegistry` bean is present, Spring Integration metrics will be managed by Micrometer. -If you wish to use legacy Spring Integration metrics, add a `DefaultMetricsFactory` bean to the application context. - - - -[[boot-features-session]] -== Spring Session -Spring Boot provides {spring-session}[Spring Session] auto-configuration for a wide range of data stores. -When building a Servlet web application, the following stores can be auto-configured: - -* JDBC -* Redis -* Hazelcast -* MongoDB - -When building a reactive web application, the following stores can be auto-configured: - -* Redis -* MongoDB - -If a single Spring Session module is present on the classpath, Spring Boot uses that store implementation automatically. -If you have more than one implementation, you must choose the {spring-boot-autoconfigure-module-code}/session/StoreType.java[`StoreType`] that you wish to use to store the sessions. -For instance, to use JDBC as the back-end store, you can configure your application as follows: - -[source,properties,indent=0,configprops] ----- - spring.session.store-type=jdbc ----- - -TIP: You can disable Spring Session by setting the `store-type` to `none`. - -Each store has specific additional settings. -For instance, it is possible to customize the name of the table for the JDBC store, as shown in the following example: - -[source,properties,indent=0,configprops] ----- - spring.session.jdbc.table-name=SESSIONS ----- - -For setting the timeout of the session you can use the configprop:spring.session.timeout[] property. -If that property is not set, the auto-configuration falls back to the value of configprop:server.servlet.session.timeout[]. - - - -[[boot-features-jmx]] -== Monitoring and Management over JMX -Java Management Extensions (JMX) provide a standard mechanism to monitor and manage applications. -Spring Boot exposes the most suitable `MBeanServer` as a bean with an ID of `mbeanServer`. -Any of your beans that are annotated with Spring JMX annotations (`@ManagedResource`, `@ManagedAttribute`, or `@ManagedOperation`) are exposed to it. - -If your platform provides a standard `MBeanServer`, Spring Boot will use that and default to the VM `MBeanServer` if necessary. -If all that fails, a new `MBeanServer` will be created. - -See the {spring-boot-autoconfigure-module-code}/jmx/JmxAutoConfiguration.java[`JmxAutoConfiguration`] class for more details. - - - -[[boot-features-testing]] -== Testing -Spring Boot provides a number of utilities and annotations to help when testing your application. -Test support is provided by two modules: `spring-boot-test` contains core items, and `spring-boot-test-autoconfigure` supports auto-configuration for tests. - -Most developers use the `spring-boot-starter-test` "`Starter`", which imports both Spring Boot test modules as well as JUnit Jupiter, AssertJ, Hamcrest, and a number of other useful libraries. - -[TIP] -==== -The starter also brings the vintage engine so that you can run both JUnit 4 and JUnit 5 tests. -If you have migrated your tests to JUnit 5, you should exclude JUnit 4 support, as shown in the following example: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - org.springframework.boot - spring-boot-starter-test - test - - - org.junit.vintage - junit-vintage-engine - - - ----- -==== - - - -[[boot-features-test-scope-dependencies]] -=== Test Scope Dependencies -The `spring-boot-starter-test` "`Starter`" (in the `test` `scope`) contains the following provided libraries: - -* https://junit.org/junit5[JUnit 5] (including the vintage engine for backward compatibility with JUnit 4: The de-facto standard for unit testing Java applications. -* {spring-framework-docs}testing.html#integration-testing[Spring Test] & Spring Boot Test: Utilities and integration test support for Spring Boot applications. -* https://joel-costigliola.github.io/assertj/[AssertJ]: A fluent assertion library. -* https://github.com/hamcrest/JavaHamcrest[Hamcrest]: A library of matcher objects (also known as constraints or predicates). -* https://mockito.github.io[Mockito]: A Java mocking framework. -* https://github.com/skyscreamer/JSONassert[JSONassert]: An assertion library for JSON. -* https://github.com/jayway/JsonPath[JsonPath]: XPath for JSON. - -We generally find these common libraries to be useful when writing tests. -If these libraries do not suit your needs, you can add additional test dependencies of your own. - - - -[[boot-features-testing-spring-applications]] -=== Testing Spring Applications -One of the major advantages of dependency injection is that it should make your code easier to unit test. -You can instantiate objects by using the `new` operator without even involving Spring. -You can also use _mock objects_ instead of real dependencies. - -Often, you need to move beyond unit testing and start integration testing (with a Spring `ApplicationContext`). -It is useful to be able to perform integration testing without requiring deployment of your application or needing to connect to other infrastructure. - -The Spring Framework includes a dedicated test module for such integration testing. -You can declare a dependency directly to `org.springframework:spring-test` or use the `spring-boot-starter-test` "`Starter`" to pull it in transitively. - -If you have not used the `spring-test` module before, you should start by reading the {spring-framework-docs}testing.html#testing[relevant section] of the Spring Framework reference documentation. - - - -[[boot-features-testing-spring-boot-applications]] -=== Testing Spring Boot Applications -A Spring Boot application is a Spring `ApplicationContext`, so nothing very special has to be done to test it beyond what you would normally do with a vanilla Spring context. - -NOTE: External properties, logging, and other features of Spring Boot are installed in the context by default only if you use `SpringApplication` to create it. - -Spring Boot provides a `@SpringBootTest` annotation, which can be used as an alternative to the standard `spring-test` `@ContextConfiguration` annotation when you need Spring Boot features. -The annotation works by <>. -In addition to `@SpringBootTest` a number of other annotations are also provided for <> of an application. - -TIP: If you are using JUnit 4, don't forget to also add `@RunWith(SpringRunner.class)` to your test, otherwise the annotations will be ignored. -If you are using JUnit 5, there's no need to add the equivalent `@ExtendWith(SpringExtension.class)` as `@SpringBootTest` and the other `@…Test` annotations are already annotated with it. - -By default, `@SpringBootTest` will not start a server. -You can use the `webEnvironment` attribute of `@SpringBootTest` to further refine how your tests run: - -* `MOCK`(Default) : Loads a web `ApplicationContext` and provides a mock web environment. - Embedded servers are not started when using this annotation. - If a web environment is not available on your classpath, this mode transparently falls back to creating a regular non-web `ApplicationContext`. - It can be used in conjunction with <> for mock-based testing of your web application. -* `RANDOM_PORT`: Loads a `WebServerApplicationContext` and provides a real web environment. - Embedded servers are started and listen on a random port. -* `DEFINED_PORT`: Loads a `WebServerApplicationContext` and provides a real web environment. - Embedded servers are started and listen on a defined port (from your `application.properties`) or on the default port of `8080`. -* `NONE`: Loads an `ApplicationContext` by using `SpringApplication` but does not provide _any_ web environment (mock or otherwise). - -NOTE: If your test is `@Transactional`, it rolls back the transaction at the end of each test method by default. -However, as using this arrangement with either `RANDOM_PORT` or `DEFINED_PORT` implicitly provides a real servlet environment, the HTTP client and server run in separate threads and, thus, in separate transactions. -Any transaction initiated on the server does not roll back in this case. - -NOTE: `@SpringBootTest` with `webEnvironment = WebEnvironment.RANDOM_PORT` will also start the management server on a separate random port if your application uses a different port for the management server. - - - -[[boot-features-testing-spring-boot-applications-detecting-web-app-type]] -==== Detecting Web Application Type -If Spring MVC is available, a regular MVC-based application context is configured. -If you have only Spring WebFlux, we'll detect that and configure a WebFlux-based application context instead. - -If both are present, Spring MVC takes precedence. -If you want to test a reactive web application in this scenario, you must set the configprop:spring.main.web-application-type[] property: - -[source,java,indent=0] ----- - @SpringBootTest(properties = "spring.main.web-application-type=reactive") - class MyWebFluxTests { ... } ----- - - - -[[boot-features-testing-spring-boot-applications-detecting-config]] -==== Detecting Test Configuration -If you are familiar with the Spring Test Framework, you may be used to using `@ContextConfiguration(classes=...)` in order to specify which Spring `@Configuration` to load. -Alternatively, you might have often used nested `@Configuration` classes within your test. - -When testing Spring Boot applications, this is often not required. -Spring Boot's `@*Test` annotations search for your primary configuration automatically whenever you do not explicitly define one. - -The search algorithm works up from the package that contains the test until it finds a class annotated with `@SpringBootApplication` or `@SpringBootConfiguration`. -As long as you <> in a sensible way, your main configuration is usually found. - -[NOTE] -==== -If you use a <>, you should avoid adding configuration settings that are specific to a particular area on the <>. - -The underlying component scan configuration of `@SpringBootApplication` defines exclude filters that are used to make sure slicing works as expected. -If you are using an explicit `@ComponentScan` directive on your `@SpringBootApplication`-annotated class, be aware that those filters will be disabled. -If you are using slicing, you should define them again. -==== - -If you want to customize the primary configuration, you can use a nested `@TestConfiguration` class. -Unlike a nested `@Configuration` class, which would be used instead of your application's primary configuration, a nested `@TestConfiguration` class is used in addition to your application's primary configuration. - -NOTE: Spring's test framework caches application contexts between tests. -Therefore, as long as your tests share the same configuration (no matter how it is discovered), the potentially time-consuming process of loading the context happens only once. - - - -[[boot-features-testing-spring-boot-applications-excluding-config]] -==== Excluding Test Configuration -If your application uses component scanning (for example, if you use `@SpringBootApplication` or `@ComponentScan`), you may find top-level configuration classes that you created only for specific tests accidentally get picked up everywhere. - -As we <>, `@TestConfiguration` can be used on an inner class of a test to customize the primary configuration. -When placed on a top-level class, `@TestConfiguration` indicates that classes in `src/test/java` should not be picked up by scanning. -You can then import that class explicitly where it is required, as shown in the following example: - -[source,java,indent=0] ----- - @SpringBootTest - @Import(MyTestsConfiguration.class) - class MyTests { - - @Test - void exampleTest() { - ... - } - - } ----- - -NOTE: If you directly use `@ComponentScan` (that is, not through `@SpringBootApplication`) you need to register the `TypeExcludeFilter` with it. -See {spring-boot-module-api}/context/TypeExcludeFilter.html[the Javadoc] for details. - - - - -[[boot-features-testing-spring-boot-application-arguments]] -==== Using Application Arguments -If your application expects <>, you can -have `@SpringBootTest` inject them using the `args` attribute. - -[source,java,indent=0] ----- -include::{code-examples}/test/context/ApplicationArgumentsExampleTests.java[tag=example] ----- - - - -[[boot-features-testing-spring-boot-applications-testing-with-mock-environment]] -==== Testing with a mock environment -By default, `@SpringBootTest` does not start the server. -If you have web endpoints that you want to test against this mock environment, you can additionally configure {spring-framework-docs}/testing.html#spring-mvc-test-framework[`MockMvc`] as shown in the following example: - -[source,java,indent=0] ----- -include::{code-examples}/test/web/MockMvcExampleTests.java[tag=test-mock-mvc] ----- - -TIP: If you want to focus only on the web layer and not start a complete `ApplicationContext`, consider <>. - -Alternatively, you can configure a {spring-framework-docs}testing.html#webtestclient-tests[`WebTestClient`] as shown in the following example: - -[source,java,indent=0] ----- -include::{code-examples}/test/web/MockWebTestClientExampleTests.java[tag=test-mock-web-test-client] ----- - -[TIP] -==== -Testing within a mocked environment is usually faster than running with a full Servlet container. -However, since mocking occurs at the Spring MVC layer, code that relies on lower-level Servlet container behavior cannot be directly tested with MockMvc. - -For example, Spring Boot's error handling is based on the "`error page`" support provided by the Servlet container. -This means that, whilst you can test your MVC layer throws and handles exceptions as expected, you cannot directly test that a specific <> is rendered. -If you need to test these lower-level concerns, you can start a fully running server as described in the next section. -==== - - - -[[boot-features-testing-spring-boot-applications-testing-with-running-server]] -==== Testing with a running server -If you need to start a full running server, we recommend that you use random ports. -If you use `@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT)`, an available port is picked at random each time your test runs. - -The `@LocalServerPort` annotation can be used to <> into your test. -For convenience, tests that need to make REST calls to the started server can additionally `@Autowire` a {spring-framework-docs}testing.html#webtestclient-tests[`WebTestClient`], which resolves relative links to the running server and comes with a dedicated API for verifying responses, as shown in the following example: - -[source,java,indent=0] ----- -include::{code-examples}/test/web/RandomPortWebTestClientExampleTests.java[tag=test-random-port] ----- - -This setup requires `spring-webflux` on the classpath. -If you can't or won't add webflux, Spring Boot also provides a `TestRestTemplate` facility: - -[source,java,indent=0] ----- -include::{code-examples}/test/web/RandomPortTestRestTemplateExampleTests.java[tag=test-random-port] ----- - - - -[[boot-features-testing-spring-boot-applications-customizing-web-test-client]] -==== Customizing WebTestClient -To customize the `WebTestClient` bean, configure a `WebTestClientBuilderCustomizer` bean. -Any such beans are called with the `WebTestClient.Builder` that is used to create the `WebTestClient`. - - - -[[boot-features-testing-spring-boot-applications-jmx]] -==== Using JMX -As the test context framework caches context, JMX is disabled by default to prevent identical components to register on the same domain. -If such test needs access to an `MBeanServer`, consider marking it dirty as well: - -[source,java,indent=0] ----- -include::{test-examples}/jmx/SampleJmxTests.java[tag=test] ----- - - - -[[boot-features-testing-spring-boot-applications-mocking-beans]] -==== Mocking and Spying Beans -When running tests, it is sometimes necessary to mock certain components within your application context. -For example, you may have a facade over some remote service that is unavailable during development. -Mocking can also be useful when you want to simulate failures that might be hard to trigger in a real environment. - -Spring Boot includes a `@MockBean` annotation that can be used to define a Mockito mock for a bean inside your `ApplicationContext`. -You can use the annotation to add new beans or replace a single existing bean definition. -The annotation can be used directly on test classes, on fields within your test, or on `@Configuration` classes and fields. -When used on a field, the instance of the created mock is also injected. -Mock beans are automatically reset after each test method. - -[NOTE] -==== -If your test uses one of Spring Boot's test annotations (such as `@SpringBootTest`), this feature is automatically enabled. -To use this feature with a different arrangement, a listener must be explicitly added, as shown in the following example: - -[source,java,indent=0] ----- - @TestExecutionListeners(MockitoTestExecutionListener.class) ----- - -==== - -The following example replaces an existing `RemoteService` bean with a mock implementation: - -[source,java,indent=0] ----- - import org.junit.jupiter.api.Test; - import org.springframework.beans.factory.annotation.*; - import org.springframework.boot.test.context.*; - import org.springframework.boot.test.mock.mockito.*; - - import static org.assertj.core.api.Assertions.*; - import static org.mockito.BDDMockito.*; - - @SpringBootTest - class MyTests { - - @MockBean - private RemoteService remoteService; - - @Autowired - private Reverser reverser; - - @Test - void exampleTest() { - // RemoteService has been injected into the reverser bean - given(this.remoteService.someCall()).willReturn("mock"); - String reverse = reverser.reverseSomeCall(); - assertThat(reverse).isEqualTo("kcom"); - } - - } ----- - -NOTE: `@MockBean` cannot be used to mock the behavior of a bean that's exercised during application context refresh. -By the time the test is executed, the application context refresh has completed and it is too late to configure the mocked behavior. -We recommend using a `@Bean` method to create and configure the mock in this situation. - -Additionally, you can use `@SpyBean` to wrap any existing bean with a Mockito `spy`. -See the {spring-boot-test-module-api}/mock/mockito/SpyBean.html[Javadoc] for full details. - -NOTE: While Spring's test framework caches application contexts between tests and reuses a context for tests sharing the same configuration, the use of `@MockBean` or `@SpyBean` influences the cache key, which will most likely increase the number of contexts. - -TIP: If you are using `@SpyBean` to spy on a bean with `@Cacheable` methods that refer to parameters by name, your application must be compiled with `-parameters`. -This ensures that the parameter names are available to the caching infrastructure once the bean has been spied upon. - - - -[[boot-features-testing-spring-boot-applications-testing-autoconfigured-tests]] -==== Auto-configured Tests -Spring Boot's auto-configuration system works well for applications but can sometimes be a little too much for tests. -It often helps to load only the parts of the configuration that are required to test a "`slice`" of your application. -For example, you might want to test that Spring MVC controllers are mapping URLs correctly, and you do not want to involve database calls in those tests, or you might want to test JPA entities, and you are not interested in the web layer when those tests run. - -The `spring-boot-test-autoconfigure` module includes a number of annotations that can be used to automatically configure such "`slices`". -Each of them works in a similar way, providing a `@...Test` annotation that loads the `ApplicationContext` and one or more `@AutoConfigure...` annotations that can be used to customize auto-configuration settings. - -NOTE: Each slice restricts component scan to appropriate components and loads a very restricted set of auto-configuration classes. -If you need to exclude one of them, most `@...Test` annotations provide an `excludeAutoConfiguration` attribute. -Alternatively, you can use `@ImportAutoConfiguration#exclude`. - -NOTE: Including multiple "`slices`" by using several `@...Test` annotations in one test is not supported. -If you need multiple "`slices`", pick one of the `@...Test` annotations and include the `@AutoConfigure...` annotations of the other "`slices`" by hand. - -TIP: It is also possible to use the `@AutoConfigure...` annotations with the standard `@SpringBootTest` annotation. -You can use this combination if you are not interested in "`slicing`" your application but you want some of the auto-configured test beans. - - - -[[boot-features-testing-spring-boot-applications-testing-autoconfigured-json-tests]] -==== Auto-configured JSON Tests -To test that object JSON serialization and deserialization is working as expected, you can use the `@JsonTest` annotation. -`@JsonTest` auto-configures the available supported JSON mapper, which can be one of the following libraries: - -* Jackson `ObjectMapper`, any `@JsonComponent` beans and any Jackson ``Module``s -* `Gson` -* `Jsonb` - -TIP: A list of the auto-configurations that are enabled by `@JsonTest` can be <>. - -If you need to configure elements of the auto-configuration, you can use the `@AutoConfigureJsonTesters` annotation. - -Spring Boot includes AssertJ-based helpers that work with the JSONAssert and JsonPath libraries to check that JSON appears as expected. -The `JacksonTester`, `GsonTester`, `JsonbTester`, and `BasicJsonTester` classes can be used for Jackson, Gson, Jsonb, and Strings respectively. -Any helper fields on the test class can be `@Autowired` when using `@JsonTest`. -The following example shows a test class for Jackson: - -[source,java,indent=0] ----- - import org.junit.jupiter.api.Test; - import org.springframework.beans.factory.annotation.*; - import org.springframework.boot.test.autoconfigure.json.*; - import org.springframework.boot.test.context.*; - import org.springframework.boot.test.json.*; - - import static org.assertj.core.api.Assertions.*; - - @JsonTest - class MyJsonTests { - - @Autowired - private JacksonTester json; - - @Test - void testSerialize() throws Exception { - VehicleDetails details = new VehicleDetails("Honda", "Civic"); - // Assert against a `.json` file in the same package as the test - assertThat(this.json.write(details)).isEqualToJson("expected.json"); - // Or use JSON path based assertions - assertThat(this.json.write(details)).hasJsonPathStringValue("@.make"); - assertThat(this.json.write(details)).extractingJsonPathStringValue("@.make") - .isEqualTo("Honda"); - } - - @Test - void testDeserialize() throws Exception { - String content = "{\"make\":\"Ford\",\"model\":\"Focus\"}"; - assertThat(this.json.parse(content)) - .isEqualTo(new VehicleDetails("Ford", "Focus")); - assertThat(this.json.parseObject(content).getMake()).isEqualTo("Ford"); - } - - } ----- - -NOTE: JSON helper classes can also be used directly in standard unit tests. -To do so, call the `initFields` method of the helper in your `@Before` method if you do not use `@JsonTest`. - -If you're using Spring Boot's AssertJ-based helpers to assert on a number value at a given JSON path, you might not be able to use `isEqualTo` depending on the type. -Instead, you can use AssertJ's `satisfies` to assert that the value matches the given condition. -For instance, the following example asserts that the actual number is a float value close to `0.15` within an offset of `0.01`. - -[source,java,indent=0] ----- -assertThat(json.write(message)) - .extractingJsonPathNumberValue("@.test.numberValue") - .satisfies((number) -> assertThat(number.floatValue()).isCloseTo(0.15f, within(0.01f))); ----- - - - -[[boot-features-testing-spring-boot-applications-testing-autoconfigured-mvc-tests]] -==== Auto-configured Spring MVC Tests -To test whether Spring MVC controllers are working as expected, use the `@WebMvcTest` annotation. -`@WebMvcTest` auto-configures the Spring MVC infrastructure and limits scanned beans to `@Controller`, `@ControllerAdvice`, `@JsonComponent`, `Converter`, `GenericConverter`, `Filter`, `HandlerInterceptor`, `WebMvcConfigurer`, and `HandlerMethodArgumentResolver`. -Regular `@Component` beans are not scanned when using this annotation. - -TIP: A list of the auto-configuration settings that are enabled by `@WebMvcTest` can be <>. - -TIP: If you need to register extra components, such as the Jackson `Module`, you can import additional configuration classes by using `@Import` on your test. - -Often, `@WebMvcTest` is limited to a single controller and is used in combination with `@MockBean` to provide mock implementations for required collaborators. - -`@WebMvcTest` also auto-configures `MockMvc`. -Mock MVC offers a powerful way to quickly test MVC controllers without needing to start a full HTTP server. - -TIP: You can also auto-configure `MockMvc` in a non-`@WebMvcTest` (such as `@SpringBootTest`) by annotating it with `@AutoConfigureMockMvc`. -The following example uses `MockMvc`: - -[source,java,indent=0] ----- - import org.junit.jupiter.api.*; - import org.springframework.beans.factory.annotation.*; - import org.springframework.boot.test.autoconfigure.web.servlet.*; - import org.springframework.boot.test.mock.mockito.*; - - import static org.assertj.core.api.Assertions.*; - import static org.mockito.BDDMockito.*; - import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; - import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - - @WebMvcTest(UserVehicleController.class) - class MyControllerTests { - - @Autowired - private MockMvc mvc; - - @MockBean - private UserVehicleService userVehicleService; - - @Test - void testExample() throws Exception { - given(this.userVehicleService.getVehicleDetails("sboot")) - .willReturn(new VehicleDetails("Honda", "Civic")); - this.mvc.perform(get("/sboot/vehicle").accept(MediaType.TEXT_PLAIN)) - .andExpect(status().isOk()).andExpect(content().string("Honda Civic")); - } - - } ----- - -TIP: If you need to configure elements of the auto-configuration (for example, when servlet filters should be applied) you can use attributes in the `@AutoConfigureMockMvc` annotation. - -If you use HtmlUnit or Selenium, auto-configuration also provides an HtmlUnit `WebClient` bean and/or a Selenium `WebDriver` bean. -The following example uses HtmlUnit: - -[source,java,indent=0] ----- - import com.gargoylesoftware.htmlunit.*; - import org.junit.jupiter.api.*; - import org.springframework.beans.factory.annotation.*; - import org.springframework.boot.test.autoconfigure.web.servlet.*; - import org.springframework.boot.test.mock.mockito.*; - - import static org.assertj.core.api.Assertions.*; - import static org.mockito.BDDMockito.*; - - @WebMvcTest(UserVehicleController.class) - class MyHtmlUnitTests { - - @Autowired - private WebClient webClient; - - @MockBean - private UserVehicleService userVehicleService; - - @Test - void testExample() throws Exception { - given(this.userVehicleService.getVehicleDetails("sboot")) - .willReturn(new VehicleDetails("Honda", "Civic")); - HtmlPage page = this.webClient.getPage("/sboot/vehicle.html"); - assertThat(page.getBody().getTextContent()).isEqualTo("Honda Civic"); - } - - } ----- - -NOTE: By default, Spring Boot puts `WebDriver` beans in a special "`scope`" to ensure that the driver exits after each test and that a new instance is injected. -If you do not want this behavior, you can add `@Scope("singleton")` to your `WebDriver` `@Bean` definition. - -WARNING: The `webDriver` scope created by Spring Boot will replace any user defined scope of the same name. -If you define your own `webDriver` scope you may find it stops working when you use `@WebMvcTest`. - -If you have Spring Security on the classpath, `@WebMvcTest` will also scan `WebSecurityConfigurer` beans. -Instead of disabling security completely for such tests, you can use Spring Security's test support. -More details on how to use Spring Security's `MockMvc` support can be found in this _<>_ how-to section. - -TIP: Sometimes writing Spring MVC tests is not enough; Spring Boot can help you run <>. - - - -[[boot-features-testing-spring-boot-applications-testing-autoconfigured-webflux-tests]] -==== Auto-configured Spring WebFlux Tests -To test that {spring-framework-docs}/web-reactive.html[Spring WebFlux] controllers are working as expected, you can use the `@WebFluxTest` annotation. -`@WebFluxTest` auto-configures the Spring WebFlux infrastructure and limits scanned beans to `@Controller`, `@ControllerAdvice`, `@JsonComponent`, `Converter`, `GenericConverter`, `WebFilter`, and `WebFluxConfigurer`. -Regular `@Component` beans are not scanned when the `@WebFluxTest` annotation is used. - -TIP: A list of the auto-configurations that are enabled by `@WebFluxTest` can be <>. - -TIP: If you need to register extra components, such as Jackson `Module`, you can import additional configuration classes using `@Import` on your test. - -Often, `@WebFluxTest` is limited to a single controller and used in combination with the `@MockBean` annotation to provide mock implementations for required collaborators. - -`@WebFluxTest` also auto-configures {spring-framework-docs}testing.html#webtestclient[`WebTestClient`], which offers a powerful way to quickly test WebFlux controllers without needing to start a full HTTP server. - -TIP: You can also auto-configure `WebTestClient` in a non-`@WebFluxTest` (such as `@SpringBootTest`) by annotating it with `@AutoConfigureWebTestClient`. -The following example shows a class that uses both `@WebFluxTest` and a `WebTestClient`: - -[source,java,indent=0] ----- - import org.junit.jupiter.api.Test; - - import org.springframework.beans.factory.annotation.Autowired; - import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; - import org.springframework.http.MediaType; - import org.springframework.test.web.reactive.server.WebTestClient; - - @WebFluxTest(UserVehicleController.class) - class MyControllerTests { - - @Autowired - private WebTestClient webClient; - - @MockBean - private UserVehicleService userVehicleService; - - @Test - void testExample() throws Exception { - given(this.userVehicleService.getVehicleDetails("sboot")) - .willReturn(new VehicleDetails("Honda", "Civic")); - this.webClient.get().uri("/sboot/vehicle").accept(MediaType.TEXT_PLAIN) - .exchange() - .expectStatus().isOk() - .expectBody(String.class).isEqualTo("Honda Civic"); - } - - } ----- - -TIP: This setup is only supported by WebFlux applications as using `WebTestClient` in a mocked web application only works with WebFlux at the moment. - -NOTE: `@WebFluxTest` cannot detect routes registered via the functional web framework. -For testing `RouterFunction` beans in the context, consider importing your `RouterFunction` yourself via `@Import` or using `@SpringBootTest`. - -NOTE: `@WebFluxTest` cannot detect custom security configuration registered via a `@Bean` of type `SecurityWebFilterChain`. -To include that in your test, you will need to import the configuration that registers the bean via `@Import` or use `@SpringBootTest`. - -TIP: Sometimes writing Spring WebFlux tests is not enough; Spring Boot can help you run <>. - - - -[[boot-features-testing-spring-boot-applications-testing-autoconfigured-jpa-test]] -==== Auto-configured Data JPA Tests -You can use the `@DataJpaTest` annotation to test JPA applications. -By default, it scans for `@Entity` classes and configures Spring Data JPA repositories. -If an embedded database is available on the classpath, it configures one as well. -Regular `@Component` beans are not loaded into the `ApplicationContext`. - -TIP: A list of the auto-configuration settings that are enabled by `@DataJpaTest` can be <>. - -By default, data JPA tests are transactional and roll back at the end of each test. -See the {spring-framework-docs}testing.html#testcontext-tx-enabling-transactions[relevant section] in the Spring Framework Reference Documentation for more details. -If that is not what you want, you can disable transaction management for a test or for the whole class as follows: - -[source,java,indent=0] ----- - import org.junit.jupiter.api.Test; - import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; - import org.springframework.transaction.annotation.Propagation; - import org.springframework.transaction.annotation.Transactional; - - @DataJpaTest - @Transactional(propagation = Propagation.NOT_SUPPORTED) - class ExampleNonTransactionalTests { - - } ----- - -Data JPA tests may also inject a {spring-boot-test-autoconfigure-module-code}/orm/jpa/TestEntityManager.java[`TestEntityManager`] bean, which provides an alternative to the standard JPA `EntityManager` that is specifically designed for tests. -If you want to use `TestEntityManager` outside of `@DataJpaTest` instances, you can also use the `@AutoConfigureTestEntityManager` annotation. -A `JdbcTemplate` is also available if you need that. -The following example shows the `@DataJpaTest` annotation in use: - -[source,java,indent=0] ----- - import org.junit.jupiter.api.Test; - import org.springframework.boot.test.autoconfigure.orm.jpa.*; - - import static org.assertj.core.api.Assertions.*; - - @DataJpaTest - class ExampleRepositoryTests { - - @Autowired - private TestEntityManager entityManager; - - @Autowired - private UserRepository repository; - - @Test - void testExample() throws Exception { - this.entityManager.persist(new User("sboot", "1234")); - User user = this.repository.findByUsername("sboot"); - assertThat(user.getUsername()).isEqualTo("sboot"); - assertThat(user.getVin()).isEqualTo("1234"); - } - - } ----- - -In-memory embedded databases generally work well for tests, since they are fast and do not require any installation. -If, however, you prefer to run tests against a real database you can use the `@AutoConfigureTestDatabase` annotation, as shown in the following example: - -[source,java,indent=0] ----- - @DataJpaTest - @AutoConfigureTestDatabase(replace=Replace.NONE) - class ExampleRepositoryTests { - - // ... - - } ----- - - - -[[boot-features-testing-spring-boot-applications-testing-autoconfigured-jdbc-test]] -==== Auto-configured JDBC Tests -`@JdbcTest` is similar to `@DataJpaTest` but is for tests that only require a `DataSource` and do not use Spring Data JDBC. -By default, it configures an in-memory embedded database and a `JdbcTemplate`. -Regular `@Component` beans are not loaded into the `ApplicationContext`. - -TIP: A list of the auto-configurations that are enabled by `@JdbcTest` can be <>. - -By default, JDBC tests are transactional and roll back at the end of each test. -See the {spring-framework-docs}testing.html#testcontext-tx-enabling-transactions[relevant section] in the Spring Framework Reference Documentation for more details. -If that is not what you want, you can disable transaction management for a test or for the whole class, as follows: - -[source,java,indent=0] ----- - import org.junit.jupiter.api.Test; - import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; - import org.springframework.transaction.annotation.Propagation; - import org.springframework.transaction.annotation.Transactional; - - @JdbcTest - @Transactional(propagation = Propagation.NOT_SUPPORTED) - class ExampleNonTransactionalTests { - - } ----- - -If you prefer your test to run against a real database, you can use the `@AutoConfigureTestDatabase` annotation in the same way as for `DataJpaTest`. -(See "<>".) - - - -[[boot-features-testing-spring-boot-applications-testing-autoconfigured-data-jdbc-test]] -==== Auto-configured Data JDBC Tests -`@DataJdbcTest` is similar to `@JdbcTest` but is for tests that use Spring Data JDBC repositories. -By default, it configures an in-memory embedded database, a `JdbcTemplate`, and Spring Data JDBC repositories. -Regular `@Component` beans are not loaded into the `ApplicationContext`. - -TIP: A list of the auto-configurations that are enabled by `@DataJdbcTest` can be <>. - -By default, Data JDBC tests are transactional and roll back at the end of each test. -See the {spring-framework-docs}testing.html#testcontext-tx-enabling-transactions[relevant section] in the Spring Framework Reference Documentation for more details. -If that is not what you want, you can disable transaction management for a test or for the whole test class as <>. - -If you prefer your test to run against a real database, you can use the `@AutoConfigureTestDatabase` annotation in the same way as for `DataJpaTest`. -(See "<>".) - - - -[[boot-features-testing-spring-boot-applications-testing-autoconfigured-jooq-test]] -==== Auto-configured jOOQ Tests -You can use `@JooqTest` in a similar fashion as `@JdbcTest` but for jOOQ-related tests. -As jOOQ relies heavily on a Java-based schema that corresponds with the database schema, the existing `DataSource` is used. -If you want to replace it with an in-memory database, you can use `@AutoConfigureTestDatabase` to override those settings. -(For more about using jOOQ with Spring Boot, see "<>", earlier in this chapter.) -Regular `@Component` beans are not loaded into the `ApplicationContext`. - -TIP: A list of the auto-configurations that are enabled by `@JooqTest` can be <>. - -`@JooqTest` configures a `DSLContext`. -Regular `@Component` beans are not loaded into the `ApplicationContext`. -The following example shows the `@JooqTest` annotation in use: - -[source,java,indent=0] ----- - import org.jooq.DSLContext; - import org.junit.jupiter.api.Test; - import org.springframework.boot.test.autoconfigure.jooq.JooqTest; - - @JooqTest - class ExampleJooqTests { - - @Autowired - private DSLContext dslContext; - } ----- - -JOOQ tests are transactional and roll back at the end of each test by default. -If that is not what you want, you can disable transaction management for a test or for the whole test class as <>. - - - -[[boot-features-testing-spring-boot-applications-testing-autoconfigured-mongo-test]] -==== Auto-configured Data MongoDB Tests -You can use `@DataMongoTest` to test MongoDB applications. -By default, it configures an in-memory embedded MongoDB (if available), configures a `MongoTemplate`, scans for `@Document` classes, and configures Spring Data MongoDB repositories. -Regular `@Component` beans are not loaded into the `ApplicationContext`. -(For more about using MongoDB with Spring Boot, see "<>", earlier in this chapter.) - -TIP: A list of the auto-configuration settings that are enabled by `@DataMongoTest` can be <>. - -The following class shows the `@DataMongoTest` annotation in use: - -[source,java,indent=0] ----- - import org.springframework.beans.factory.annotation.Autowired; - import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest; - import org.springframework.data.mongodb.core.MongoTemplate; - - @DataMongoTest - class ExampleDataMongoTests { - - @Autowired - private MongoTemplate mongoTemplate; - - // - } ----- - -In-memory embedded MongoDB generally works well for tests, since it is fast and does not require any developer installation. -If, however, you prefer to run tests against a real MongoDB server, you should exclude the embedded MongoDB auto-configuration, as shown in the following example: - -[source,java,indent=0] ----- - import org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration; - import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest; - - @DataMongoTest(excludeAutoConfiguration = EmbeddedMongoAutoConfiguration.class) - class ExampleDataMongoNonEmbeddedTests { - - } ----- - - - -[[boot-features-testing-spring-boot-applications-testing-autoconfigured-neo4j-test]] -==== Auto-configured Data Neo4j Tests -You can use `@DataNeo4jTest` to test Neo4j applications. -By default, it uses an in-memory embedded Neo4j (if the embedded driver is available), scans for `@NodeEntity` classes, and configures Spring Data Neo4j repositories. -Regular `@Component` beans are not loaded into the `ApplicationContext`. -(For more about using Neo4J with Spring Boot, see "<>", earlier in this chapter.) - -TIP: A list of the auto-configuration settings that are enabled by `@DataNeo4jTest` can be <>. - -The following example shows a typical setup for using Neo4J tests in Spring Boot: - -[source,java,indent=0] ----- - import org.springframework.beans.factory.annotation.Autowired; - import org.springframework.boot.test.autoconfigure.data.neo4j.DataNeo4jTest; - - @DataNeo4jTest - class ExampleDataNeo4jTests { - - @Autowired - private YourRepository repository; - - // - } ----- - -By default, Data Neo4j tests are transactional and roll back at the end of each test. -See the {spring-framework-docs}testing.html#testcontext-tx-enabling-transactions[relevant section] in the Spring Framework Reference Documentation for more details. -If that is not what you want, you can disable transaction management for a test or for the whole class, as follows: - -[source,java,indent=0] ----- - import org.springframework.boot.test.autoconfigure.data.neo4j.DataNeo4jTest; - import org.springframework.transaction.annotation.Propagation; - import org.springframework.transaction.annotation.Transactional; - - @DataNeo4jTest - @Transactional(propagation = Propagation.NOT_SUPPORTED) - class ExampleNonTransactionalTests { - - } ----- - - - -[[boot-features-testing-spring-boot-applications-testing-autoconfigured-redis-test]] -==== Auto-configured Data Redis Tests -You can use `@DataRedisTest` to test Redis applications. -By default, it scans for `@RedisHash` classes and configures Spring Data Redis repositories. -Regular `@Component` beans are not loaded into the `ApplicationContext`. -(For more about using Redis with Spring Boot, see "<>", earlier in this chapter.) - -TIP: A list of the auto-configuration settings that are enabled by `@DataRedisTest` can be <>. - -The following example shows the `@DataRedisTest` annotation in use: - -[source,java,indent=0] ----- - import org.springframework.beans.factory.annotation.Autowired; - import org.springframework.boot.test.autoconfigure.data.redis.DataRedisTest; - - @DataRedisTest - class ExampleDataRedisTests { - - @Autowired - private YourRepository repository; - - // - } ----- - - - -[[boot-features-testing-spring-boot-applications-testing-autoconfigured-ldap-test]] -==== Auto-configured Data LDAP Tests -You can use `@DataLdapTest` to test LDAP applications. -By default, it configures an in-memory embedded LDAP (if available), configures an `LdapTemplate`, scans for `@Entry` classes, and configures Spring Data LDAP repositories. -Regular `@Component` beans are not loaded into the `ApplicationContext`. -(For more about using LDAP with Spring Boot, see "<>", earlier in this chapter.) - -TIP: A list of the auto-configuration settings that are enabled by `@DataLdapTest` can be <>. - -The following example shows the `@DataLdapTest` annotation in use: - -[source,java,indent=0] ----- - import org.springframework.beans.factory.annotation.Autowired; - import org.springframework.boot.test.autoconfigure.data.ldap.DataLdapTest; - import org.springframework.ldap.core.LdapTemplate; - - @DataLdapTest - class ExampleDataLdapTests { - - @Autowired - private LdapTemplate ldapTemplate; - - // - } ----- - -In-memory embedded LDAP generally works well for tests, since it is fast and does not require any developer installation. -If, however, you prefer to run tests against a real LDAP server, you should exclude the embedded LDAP auto-configuration, as shown in the following example: - -[source,java,indent=0] ----- - import org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapAutoConfiguration; - import org.springframework.boot.test.autoconfigure.data.ldap.DataLdapTest; - - @DataLdapTest(excludeAutoConfiguration = EmbeddedLdapAutoConfiguration.class) - class ExampleDataLdapNonEmbeddedTests { - - } ----- - - - -[[boot-features-testing-spring-boot-applications-testing-autoconfigured-rest-client]] -==== Auto-configured REST Clients -You can use the `@RestClientTest` annotation to test REST clients. -By default, it auto-configures Jackson, GSON, and Jsonb support, configures a `RestTemplateBuilder`, and adds support for `MockRestServiceServer`. -Regular `@Component` beans are not loaded into the `ApplicationContext`. - -TIP: A list of the auto-configuration settings that are enabled by `@RestClientTest` can be <>. - -The specific beans that you want to test should be specified by using the `value` or `components` attribute of `@RestClientTest`, as shown in the following example: - -[source,java,indent=0] ----- - @RestClientTest(RemoteVehicleDetailsService.class) - class ExampleRestClientTest { - - @Autowired - private RemoteVehicleDetailsService service; - - @Autowired - private MockRestServiceServer server; - - @Test - void getVehicleDetailsWhenResultIsSuccessShouldReturnDetails() - throws Exception { - this.server.expect(requestTo("/greet/details")) - .andRespond(withSuccess("hello", MediaType.TEXT_PLAIN)); - String greeting = this.service.callRestService(); - assertThat(greeting).isEqualTo("hello"); - } - - } ----- - - - -[[boot-features-testing-spring-boot-applications-testing-autoconfigured-rest-docs]] -==== Auto-configured Spring REST Docs Tests -You can use the `@AutoConfigureRestDocs` annotation to use {spring-restdocs}[Spring REST Docs] in your tests with Mock MVC, REST Assured, or WebTestClient. -It removes the need for the JUnit extension in Spring REST Docs. - -`@AutoConfigureRestDocs` can be used to override the default output directory (`target/generated-snippets` if you are using Maven or `build/generated-snippets` if you are using Gradle). -It can also be used to configure the host, scheme, and port that appears in any documented URIs. - - - -[[boot-features-testing-spring-boot-applications-testing-autoconfigured-rest-docs-mock-mvc]] -===== Auto-configured Spring REST Docs Tests with Mock MVC -`@AutoConfigureRestDocs` customizes the `MockMvc` bean to use Spring REST Docs. -You can inject it by using `@Autowired` and use it in your tests as you normally would when using Mock MVC and Spring REST Docs, as shown in the following example: - -[source,java,indent=0] ----- - import org.junit.jupiter.api.Test; - - import org.springframework.beans.factory.annotation.Autowired; - import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; - import org.springframework.http.MediaType; - import org.springframework.test.web.servlet.MockMvc; - - import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; - import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; - import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - - @WebMvcTest(UserController.class) - @AutoConfigureRestDocs - class UserDocumentationTests { - - @Autowired - private MockMvc mvc; - - @Test - void listUsers() throws Exception { - this.mvc.perform(get("/users").accept(MediaType.TEXT_PLAIN)) - .andExpect(status().isOk()) - .andDo(document("list-users")); - } - - } ----- - -If you require more control over Spring REST Docs configuration than offered by the attributes of `@AutoConfigureRestDocs`, you can use a `RestDocsMockMvcConfigurationCustomizer` bean, as shown in the following example: - -[source,java,indent=0] ----- - @TestConfiguration - static class CustomizationConfiguration - implements RestDocsMockMvcConfigurationCustomizer { - - @Override - public void customize(MockMvcRestDocumentationConfigurer configurer) { - configurer.snippets().withTemplateFormat(TemplateFormats.markdown()); - } - - } ----- - -If you want to make use of Spring REST Docs support for a parameterized output directory, you can create a `RestDocumentationResultHandler` bean. -The auto-configuration calls `alwaysDo` with this result handler, thereby causing each `MockMvc` call to automatically generate the default snippets. -The following example shows a `RestDocumentationResultHandler` being defined: - -[source,java,indent=0] ----- - @TestConfiguration(proxyBeanMethods = false) - static class ResultHandlerConfiguration { - - @Bean - public RestDocumentationResultHandler restDocumentation() { - return MockMvcRestDocumentation.document("{method-name}"); - } - - } ----- - - - -[[boot-features-testing-spring-boot-applications-testing-autoconfigured-rest-docs-web-test-client]] -===== Auto-configured Spring REST Docs Tests with WebTestClient -`@AutoConfigureRestDocs` can also be used with `WebTestClient`. -You can inject it by using `@Autowired` and use it in your tests as you normally would when using `@WebFluxTest` and Spring REST Docs, as shown in the following example: - -[source,java,indent=0] ----- -include::{code-examples}/test/autoconfigure/restdocs/webclient/UsersDocumentationTests.java[tag=source] ----- - -If you require more control over Spring REST Docs configuration than offered by the attributes of `@AutoConfigureRestDocs`, you can use a `RestDocsWebTestClientConfigurationCustomizer` bean, as shown in the following example: - -[source,java,indent=0] ----- -include::{code-examples}/test/autoconfigure/restdocs/webclient/AdvancedConfigurationExample.java[tag=configuration] ----- - - - -[[boot-features-testing-spring-boot-applications-testing-autoconfigured-rest-docs-rest-assured]] -===== Auto-configured Spring REST Docs Tests with REST Assured -`@AutoConfigureRestDocs` makes a `RequestSpecification` bean, preconfigured to use Spring REST Docs, available to your tests. -You can inject it by using `@Autowired` and use it in your tests as you normally would when using REST Assured and Spring REST Docs, as shown in the following example: - -[source,java,indent=0] ----- -include::{code-examples}/test/autoconfigure/restdocs/restassured/UserDocumentationTests.java[tag=source] ----- - -If you require more control over Spring REST Docs configuration than offered by the attributes of `@AutoConfigureRestDocs`, a `RestDocsRestAssuredConfigurationCustomizer` bean can be used, as shown in the following example: - -[source,java,indent=0] ----- -include::{code-examples}/test/autoconfigure/restdocs/restassured/AdvancedConfigurationExample.java[tag=configuration] ----- - - - -[[boot-features-testing-spring-boot-applications-testing-auto-configured-additional-auto-config]] -==== Additional Auto-configuration and Slicing -Each slice provides one or more `@AutoConfigure...` annotations that namely defines the auto-configurations that should be included as part of a slice. -Additional auto-configurations can be added by creating a custom `@AutoConfigure...` annotation or simply by adding `@ImportAutoConfiguration` to the test as shown in the following example: - -[source,java,indent=0] ----- - @JdbcTest - @ImportAutoConfiguration(IntegrationAutoConfiguration.class) - class ExampleJdbcTests { - - } ----- - -NOTE: Make sure to not use the regular `@Import` annotation to import auto-configurations as they are handled in a specific way by Spring Boot. - - - -[[boot-features-testing-spring-boot-applications-testing-user-configuration]] -==== User Configuration and Slicing -If you <> in a sensible way, your `@SpringBootApplication` class is <> as the configuration of your tests. - -It then becomes important not to litter the application's main class with configuration settings that are specific to a particular area of its functionality. - -Assume that you are using Spring Batch and you rely on the auto-configuration for it. -You could define your `@SpringBootApplication` as follows: - -[source,java,indent=0] ----- - @SpringBootApplication - @EnableBatchProcessing - public class SampleApplication { ... } ----- - -Because this class is the source configuration for the test, any slice test actually tries to start Spring Batch, which is definitely not what you want to do. -A recommended approach is to move that area-specific configuration to a separate `@Configuration` class at the same level as your application, as shown in the following example: - -[source,java,indent=0] ----- - @Configuration(proxyBeanMethods = false) - @EnableBatchProcessing - public class BatchConfiguration { ... } ----- - -NOTE: Depending on the complexity of your application, you may either have a single `@Configuration` class for your customizations or one class per domain area. -The latter approach lets you enable it in one of your tests, if necessary, with the `@Import` annotation. - -Test slices exclude `@Configuration` classes from scanning. -For example, for a `@WebMvcTest`, the following configuration will not include the given `WebMvcConfigurer` bean in the application context loaded by the test slice: - -[source,java,indent=0] ----- - @Configuration - public class WebConfiguration { - @Bean - public WebMvcConfigurer testConfigurer() { - return new WebMvcConfigurer() { - ... - }; - } - } ----- - -The configuration below will, however, cause the custom `WebMvcConfigurer` to be loaded by the test slice. - -[source,java,indent=0] ----- - @Component - public class TestWebMvcConfigurer implements WebMvcConfigurer { - ... - } ----- - -Another source of confusion is classpath scanning. -Assume that, while you structured your code in a sensible way, you need to scan an additional package. -Your application may resemble the following code: - -[source,java,indent=0] ----- - @SpringBootApplication - @ComponentScan({ "com.example.app", "org.acme.another" }) - public class SampleApplication { ... } ----- - -Doing so effectively overrides the default component scan directive with the side effect of scanning those two packages regardless of the slice that you chose. -For instance, a `@DataJpaTest` seems to suddenly scan components and user configurations of your application. -Again, moving the custom directive to a separate class is a good way to fix this issue. - -TIP: If this is not an option for you, you can create a `@SpringBootConfiguration` somewhere in the hierarchy of your test so that it is used instead. -Alternatively, you can specify a source for your test, which disables the behavior of finding a default one. - - - -[[boot-features-testing-spring-boot-applications-with-spock]] -==== Using Spock to Test Spring Boot Applications -If you wish to use Spock to test a Spring Boot application, you should add a dependency on Spock's `spock-spring` module to your application's build. -`spock-spring` integrates Spring's test framework into Spock. -It is recommended that you use Spock 1.2 or later to benefit from a number of improvements to Spock's Spring Framework and Spring Boot integration. -See http://spockframework.org/spock/docs/1.2/modules.html#_spring_module[the documentation for Spock's Spring module] for further details. - - - -[[boot-features-test-utilities]] -=== Test Utilities -A few test utility classes that are generally useful when testing your application are packaged as part of `spring-boot`. - - - -[[boot-features-configfileapplicationcontextinitializer-test-utility]] -==== ConfigFileApplicationContextInitializer -`ConfigFileApplicationContextInitializer` is an `ApplicationContextInitializer` that you can apply to your tests to load Spring Boot `application.properties` files. -You can use it when you do not need the full set of features provided by `@SpringBootTest`, as shown in the following example: - -[source,java,indent=0] ----- - @ContextConfiguration(classes = Config.class, - initializers = ConfigFileApplicationContextInitializer.class) ----- - -NOTE: Using `ConfigFileApplicationContextInitializer` alone does not provide support for `@Value("${...}")` injection. -Its only job is to ensure that `application.properties` files are loaded into Spring's `Environment`. -For `@Value` support, you need to either additionally configure a `PropertySourcesPlaceholderConfigurer` or use `@SpringBootTest`, which auto-configures one for you. - - - -[[boot-features-test-property-values]] -==== TestPropertyValues -`TestPropertyValues` lets you quickly add properties to a `ConfigurableEnvironment` or `ConfigurableApplicationContext`. -You can call it with `key=value` strings, as follows: - -[source,java,indent=0] ----- - TestPropertyValues.of("org=Spring", "name=Boot").applyTo(env); ----- - - - -[[boot-features-output-capture-test-utility]] -==== OutputCapture -`OutputCapture` is a JUnit `Extension` that you can use to capture `System.out` and `System.err` output. -To use add `@ExtendWith(OutputCaptureExtension.class)` and inject `CapturedOutput` as an argument to your test class constructor or test method as follows: - -[source,java,indent=0] ----- -include::{test-examples}/test/system/OutputCaptureTests.java[tag=test] ----- - - - -[[boot-features-rest-templates-test-utility]] -==== TestRestTemplate -`TestRestTemplate` is a convenience alternative to Spring's `RestTemplate` that is useful in integration tests. -You can get a vanilla template or one that sends Basic HTTP authentication (with a username and password). -In either case, the template behaves in a test-friendly way by not throwing exceptions on server-side errors. - -TIP: Spring Framework 5.0 provides a new `WebTestClient` that works for <> and both <>. -It provides a fluent API for assertions, unlike `TestRestTemplate`. - -It is recommended, but not mandatory, to use the Apache HTTP Client (version 4.3.2 or better). -If you have that on your classpath, the `TestRestTemplate` responds by configuring the client appropriately. -If you do use Apache's HTTP client, some additional test-friendly features are enabled: - -* Redirects are not followed (so you can assert the response location). -* Cookies are ignored (so the template is stateless). - -`TestRestTemplate` can be instantiated directly in your integration tests, as shown in the following example: - -[source,java,indent=0] ----- - public class MyTest { - - private TestRestTemplate template = new TestRestTemplate(); - - @Test - public void testRequest() throws Exception { - HttpHeaders headers = this.template.getForEntity( - "https://myhost.example.com/example", String.class).getHeaders(); - assertThat(headers.getLocation()).hasHost("other.example.com"); - } - - } ----- - -Alternatively, if you use the `@SpringBootTest` annotation with `WebEnvironment.RANDOM_PORT` or `WebEnvironment.DEFINED_PORT`, you can inject a fully configured `TestRestTemplate` and start using it. -If necessary, additional customizations can be applied through the `RestTemplateBuilder` bean. -Any URLs that do not specify a host and port automatically connect to the embedded server, as shown in the following example: - -[source,java,indent=0] ----- -include::{test-examples}/web/client/SampleWebClientTests.java[tag=test] ----- - - - -[[boot-features-websockets]] -== WebSockets -Spring Boot provides WebSockets auto-configuration for embedded Tomcat, Jetty, and Undertow. -If you deploy a war file to a standalone container, Spring Boot assumes that the container is responsible for the configuration of its WebSocket support. - -Spring Framework provides {spring-framework-docs}web.html#websocket[rich WebSocket support] for MVC web applications that can be easily accessed through the `spring-boot-starter-websocket` module. - -WebSocket support is also available for {spring-framework-docs}web-reactive.html#webflux-websocket[reactive web applications] and requires to include the WebSocket API alongside `spring-boot-starter-webflux`: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - javax.websocket - javax.websocket-api - ----- - - - -[[boot-features-webservices]] -== Web Services -Spring Boot provides Web Services auto-configuration so that all you must do is define your `Endpoints`. - -The {spring-webservices-docs}[Spring Web Services features] can be easily accessed with the `spring-boot-starter-webservices` module. - -`SimpleWsdl11Definition` and `SimpleXsdSchema` beans can be automatically created for your WSDLs and XSDs respectively. -To do so, configure their location, as shown in the following example: - - -[source,properties,indent=0,configprops] ----- - spring.webservices.wsdl-locations=classpath:/wsdl ----- - - - -[[boot-features-webservices-template]] -=== Calling Web Services with `WebServiceTemplate` -If you need to call remote Web services from your application, you can use the {spring-webservices-docs}#client-web-service-template[`WebServiceTemplate`] class. -Since `WebServiceTemplate` instances often need to be customized before being used, Spring Boot does not provide any single auto-configured `WebServiceTemplate` bean. -It does, however, auto-configure a `WebServiceTemplateBuilder`, which can be used to create `WebServiceTemplate` instances when needed. - -The following code shows a typical example: - -[source,java,indent=0] ----- - @Service - public class MyService { - - private final WebServiceTemplate webServiceTemplate; - - public MyService(WebServiceTemplateBuilder webServiceTemplateBuilder) { - this.webServiceTemplate = webServiceTemplateBuilder.build(); - } - - public DetailsResp someWsCall(DetailsReq detailsReq) { - return (DetailsResp) this.webServiceTemplate.marshalSendAndReceive(detailsReq, new SoapActionCallback(ACTION)); - } - - } ----- - -By default, `WebServiceTemplateBuilder` detects a suitable HTTP-based `WebServiceMessageSender` using the available HTTP client libraries on the classpath. -You can also customize read and connection timeouts as follows: - -[source,java,indent=0] ----- - @Bean - public WebServiceTemplate webServiceTemplate(WebServiceTemplateBuilder builder) { - return builder.messageSenders(new HttpWebServiceMessageSenderBuilder() - .setConnectTimeout(5000).setReadTimeout(2000).build()).build(); - } ----- - - - -[[boot-features-developing-auto-configuration]] -== Creating Your Own Auto-configuration -If you work in a company that develops shared libraries, or if you work on an open-source or commercial library, you might want to develop your own auto-configuration. -Auto-configuration classes can be bundled in external jars and still be picked-up by Spring Boot. - -Auto-configuration can be associated to a "`starter`" that provides the auto-configuration code as well as the typical libraries that you would use with it. -We first cover what you need to know to build your own auto-configuration and then we move on to the <>. - -TIP: A https://github.com/snicoll-demos/spring-boot-master-auto-configuration[demo project] is available to showcase how you can create a starter step-by-step. - - - -[[boot-features-understanding-auto-configured-beans]] -=== Understanding Auto-configured Beans -Under the hood, auto-configuration is implemented with standard `@Configuration` classes. -Additional `@Conditional` annotations are used to constrain when the auto-configuration should apply. -Usually, auto-configuration classes use `@ConditionalOnClass` and `@ConditionalOnMissingBean` annotations. -This ensures that auto-configuration applies only when relevant classes are found and when you have not declared your own `@Configuration`. - -You can browse the source code of {spring-boot-autoconfigure-module-code}[`spring-boot-autoconfigure`] to see the `@Configuration` classes that Spring provides (see the {spring-boot-code}/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories[`META-INF/spring.factories`] file). - - - -[[boot-features-locating-auto-configuration-candidates]] -=== Locating Auto-configuration Candidates -Spring Boot checks for the presence of a `META-INF/spring.factories` file within your published jar. -The file should list your configuration classes under the `EnableAutoConfiguration` key, as shown in the following example: - -[indent=0] ----- - org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ - com.mycorp.libx.autoconfigure.LibXAutoConfiguration,\ - com.mycorp.libx.autoconfigure.LibXWebAutoConfiguration ----- - -NOTE: Auto-configurations must be loaded that way _only_. -Make sure that they are defined in a specific package space and that they are never the target of component scanning. -Furthermore, auto-configuration classes should not enable component scanning to find additional components. -Specific ``@Import``s should be used instead. - -You can use the {spring-boot-autoconfigure-module-code}/AutoConfigureAfter.java[`@AutoConfigureAfter`] or {spring-boot-autoconfigure-module-code}/AutoConfigureBefore.java[`@AutoConfigureBefore`] annotations if your configuration needs to be applied in a specific order. -For example, if you provide web-specific configuration, your class may need to be applied after `WebMvcAutoConfiguration`. - -If you want to order certain auto-configurations that should not have any direct knowledge of each other, you can also use `@AutoConfigureOrder`. -That annotation has the same semantic as the regular `@Order` annotation but provides a dedicated order for auto-configuration classes. - - - -[[boot-features-condition-annotations]] -=== Condition Annotations -You almost always want to include one or more `@Conditional` annotations on your auto-configuration class. -The `@ConditionalOnMissingBean` annotation is one common example that is used to allow developers to override auto-configuration if they are not happy with your defaults. - -Spring Boot includes a number of `@Conditional` annotations that you can reuse in your own code by annotating `@Configuration` classes or individual `@Bean` methods. -These annotations include: - -* <> -* <> -* <> -* <> -* <> -* <> - - - -[[boot-features-class-conditions]] -==== Class Conditions -The `@ConditionalOnClass` and `@ConditionalOnMissingClass` annotations let `@Configuration` classes be included based on the presence or absence of specific classes. -Due to the fact that annotation metadata is parsed by using https://asm.ow2.org/[ASM], you can use the `value` attribute to refer to the real class, even though that class might not actually appear on the running application classpath. -You can also use the `name` attribute if you prefer to specify the class name by using a `String` value. - -This mechanism does not apply the same way to `@Bean` methods where typically the return type is the target of the condition: before the condition on the method applies, the JVM will have loaded the class and potentially processed method references which will fail if the class is not present. - -To handle this scenario, a separate `@Configuration` class can be used to isolate the condition, as shown in the following example: - -[source,java,indent=0] ----- - @Configuration(proxyBeanMethods = false) - // Some conditions - public class MyAutoConfiguration { - - // Auto-configured beans - - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(EmbeddedAcmeService.class) - static class EmbeddedConfiguration { - - @Bean - @ConditionalOnMissingBean - public EmbeddedAcmeService embeddedAcmeService() { ... } - - } - - } ----- - -TIP: If you use `@ConditionalOnClass` or `@ConditionalOnMissingClass` as a part of a meta-annotation to compose your own composed annotations, you must use `name` as referring to the class in such a case is not handled. - - - -[[boot-features-bean-conditions]] -==== Bean Conditions -The `@ConditionalOnBean` and `@ConditionalOnMissingBean` annotations let a bean be included based on the presence or absence of specific beans. -You can use the `value` attribute to specify beans by type or `name` to specify beans by name. -The `search` attribute lets you limit the `ApplicationContext` hierarchy that should be considered when searching for beans. - -When placed on a `@Bean` method, the target type defaults to the return type of the method, as shown in the following example: - -[source,java,indent=0] ----- - @Configuration(proxyBeanMethods = false) - public class MyAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - public MyService myService() { ... } - - } ----- - -In the preceding example, the `myService` bean is going to be created if no bean of type `MyService` is already contained in the `ApplicationContext`. - -TIP: You need to be very careful about the order in which bean definitions are added, as these conditions are evaluated based on what has been processed so far. -For this reason, we recommend using only `@ConditionalOnBean` and `@ConditionalOnMissingBean` annotations on auto-configuration classes (since these are guaranteed to load after any user-defined bean definitions have been added). - -NOTE: `@ConditionalOnBean` and `@ConditionalOnMissingBean` do not prevent `@Configuration` classes from being created. -The only difference between using these conditions at the class level and marking each contained `@Bean` method with the annotation is that the former prevents registration of the `@Configuration` class as a bean if the condition does not match. - - - -[[boot-features-property-conditions]] -==== Property Conditions -The `@ConditionalOnProperty` annotation lets configuration be included based on a Spring Environment property. -Use the `prefix` and `name` attributes to specify the property that should be checked. -By default, any property that exists and is not equal to `false` is matched. -You can also create more advanced checks by using the `havingValue` and `matchIfMissing` attributes. - - - -[[boot-features-resource-conditions]] -==== Resource Conditions -The `@ConditionalOnResource` annotation lets configuration be included only when a specific resource is present. -Resources can be specified by using the usual Spring conventions, as shown in the following example: `file:/home/user/test.dat`. - - - -[[boot-features-web-application-conditions]] -==== Web Application Conditions -The `@ConditionalOnWebApplication` and `@ConditionalOnNotWebApplication` annotations let configuration be included depending on whether the application is a "`web application`". -A servlet-based web application is any application that uses a Spring `WebApplicationContext`, defines a `session` scope, or has a `ConfigurableWebEnvironment`. -A reactive web application is any application that uses a `ReactiveWebApplicationContext`, or has a `ConfigurableReactiveWebEnvironment`. - - - -[[boot-features-spel-conditions]] -==== SpEL Expression Conditions -The `@ConditionalOnExpression` annotation lets configuration be included based on the result of a {spring-framework-docs}core.html#expressions[SpEL expression]. - - - -[[boot-features-test-autoconfig]] -=== Testing your Auto-configuration -An auto-configuration can be affected by many factors: user configuration (`@Bean` definition and `Environment` customization), condition evaluation (presence of a particular library), and others. -Concretely, each test should create a well defined `ApplicationContext` that represents a combination of those customizations. -`ApplicationContextRunner` provides a great way to achieve that. - -`ApplicationContextRunner` is usually defined as a field of the test class to gather the base, common configuration. -The following example makes sure that `UserServiceAutoConfiguration` is always invoked: - -[source,java,indent=0] ----- -include::{test-examples}/autoconfigure/UserServiceAutoConfigurationTests.java[tag=runner] ----- - -TIP: If multiple auto-configurations have to be defined, there is no need to order their declarations as they are invoked in the exact same order as when running the application. - -Each test can use the runner to represent a particular use case. -For instance, the sample below invokes a user configuration (`UserConfiguration`) and checks that the auto-configuration backs off properly. -Invoking `run` provides a callback context that can be used with `Assert4J`. - -[source,java,indent=0] ----- -include::{test-examples}/autoconfigure/UserServiceAutoConfigurationTests.java[tag=test-user-config] ----- - -It is also possible to easily customize the `Environment`, as shown in the following example: - -[source,java,indent=0] ----- -include::{test-examples}/autoconfigure/UserServiceAutoConfigurationTests.java[tag=test-env] ----- - -The runner can also be used to display the `ConditionEvaluationReport`. -The report can be printed at `INFO` or `DEBUG` level. -The following example shows how to use the `ConditionEvaluationReportLoggingListener` to print the report in auto-configuration tests. - -[source,java,indent=0] ----- - @Test - public void autoConfigTest { - ConditionEvaluationReportLoggingListener initializer = new ConditionEvaluationReportLoggingListener( - LogLevel.INFO); - ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withInitializer(initializer).run((context) -> { - // Do something... - }); - } ----- - - - -==== Simulating a Web Context -If you need to test an auto-configuration that only operates in a Servlet or Reactive web application context, use the `WebApplicationContextRunner` or `ReactiveWebApplicationContextRunner` respectively. - - - -==== Overriding the Classpath -It is also possible to test what happens when a particular class and/or package is not present at runtime. -Spring Boot ships with a `FilteredClassLoader` that can easily be used by the runner. -In the following example, we assert that if `UserService` is not present, the auto-configuration is properly disabled: - -[source,java,indent=0] ----- -include::{test-examples}/autoconfigure/UserServiceAutoConfigurationTests.java[tag=test-classloader] ----- - - - -[[boot-features-custom-starter]] -=== Creating Your Own Starter -A full Spring Boot starter for a library may contain the following components: - -* The `autoconfigure` module that contains the auto-configuration code. -* The `starter` module that provides a dependency to the `autoconfigure` module as well as the library and any additional dependencies that are typically useful. -In a nutshell, adding the starter should provide everything needed to start using that library. - -TIP: You may combine the auto-configuration code and the dependency management in a single module if you do not need to separate those two concerns. - - - -[[boot-features-custom-starter-naming]] -==== Naming -You should make sure to provide a proper namespace for your starter. -Do not start your module names with `spring-boot`, even if you use a different Maven `groupId`. -We may offer official support for the thing you auto-configure in the future. - -As a rule of thumb, you should name a combined module after the starter. -For example, assume that you are creating a starter for "acme" and that you name the auto-configure module `acme-spring-boot-autoconfigure` and the starter `acme-spring-boot-starter`. -If you only have one module that combines the two, name it `acme-spring-boot-starter`. - - - -[[boot-features-custom-starter-configuration-keys]] -==== Configuration keys -If your starter provides configuration keys, use a unique namespace for them. -In particular, do not include your keys in the namespaces that Spring Boot uses (such as `server`, `management`, `spring`, and so on). -If you use the same namespace, we may modify these namespaces in the future in ways that break your modules. -As a rule of thumb, prefix all your keys with a namespace that you own (e.g. `acme`). - -Make sure that configuration keys are documented by adding field javadoc for each property, as shown in the following example: - -[source,java,indent=0] ----- - @ConfigurationProperties("acme") - public class AcmeProperties { - - /** - * Whether to check the location of acme resources. - */ - private boolean checkLocation = true; - - /** - * Timeout for establishing a connection to the acme server. - */ - private Duration loginTimeout = Duration.ofSeconds(3); - - // getters & setters - - } ----- - -NOTE: You should only use simple text with `@ConfigurationProperties` field Javadoc, since they are not processed before being added to the JSON. - -Here are some rules we follow internally to make sure descriptions are consistent: - -* Do not start the description by "The" or "A". -* For `boolean` types, start the description with "Whether" or "Enable". -* For collection-based types, start the description with "Comma-separated list" -* Use `java.time.Duration` rather than `long` and describe the default unit if it differs from milliseconds, e.g. "If a duration suffix is not specified, seconds will be used". -* Do not provide the default value in the description unless it has to be determined at runtime. - -Make sure to <> so that IDE assistance is available for your keys as well. -You may want to review the generated metadata (`META-INF/spring-configuration-metadata.json`) to make sure your keys are properly documented. -Using your own starter in a compatible IDE is also a good idea to validate that quality of the metadata. - - - -[[boot-features-custom-starter-module-autoconfigure]] -==== `autoconfigure` Module -The `autoconfigure` module contains everything that is necessary to get started with the library. -It may also contain configuration key definitions (such as `@ConfigurationProperties`) and any callback interface that can be used to further customize how the components are initialized. - -TIP: You should mark the dependencies to the library as optional so that you can include the `autoconfigure` module in your projects more easily. -If you do it that way, the library is not provided and, by default, Spring Boot backs off. - -Spring Boot uses an annotation processor to collect the conditions on auto-configurations in a metadata file (`META-INF/spring-autoconfigure-metadata.properties`). -If that file is present, it is used to eagerly filter auto-configurations that do not match, which will improve startup time. -It is recommended to add the following dependency in a module that contains auto-configurations: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - org.springframework.boot - spring-boot-autoconfigure-processor - true - ----- - -With Gradle 4.5 and earlier, the dependency should be declared in the `compileOnly` configuration, as shown in the following example: - -[source,groovy,indent=0,subs="verbatim,quotes,attributes"] ----- - dependencies { - compileOnly "org.springframework.boot:spring-boot-autoconfigure-processor" - } ----- - -With Gradle 4.6 and later, the dependency should be declared in the `annotationProcessor` configuration, as shown in the following example: - -[source,groovy,indent=0,subs="verbatim,quotes,attributes"] ----- - dependencies { - annotationProcessor "org.springframework.boot:spring-boot-autoconfigure-processor" - } ----- - - - -[[boot-features-custom-starter-module-starter]] -==== Starter Module -The starter is really an empty jar. -Its only purpose is to provide the necessary dependencies to work with the library. -You can think of it as an opinionated view of what is required to get started. - -Do not make assumptions about the project in which your starter is added. -If the library you are auto-configuring typically requires other starters, mention them as well. -Providing a proper set of _default_ dependencies may be hard if the number of optional dependencies is high, as you should avoid including dependencies that are unnecessary for a typical usage of the library. -In other words, you should not include optional dependencies. - -NOTE: Either way, your starter must reference the core Spring Boot starter (`spring-boot-starter`) directly or indirectly (i.e. no need to add it if your starter relies on another starter). -If a project is created with only your custom starter, Spring Boot's core features will be honoured by the presence of the core starter. - - - -[[boot-features-kotlin]] -== Kotlin support -https://kotlinlang.org[Kotlin] is a statically-typed language targeting the JVM (and other platforms) which allows writing concise and elegant code while providing {kotlin-docs}java-interop.html[interoperability] with existing libraries written in Java. - -Spring Boot provides Kotlin support by leveraging the support in other Spring projects such as Spring Framework, Spring Data, and Reactor. -See the {spring-framework-docs}languages.html#kotlin[Spring Framework Kotlin support documentation] for more information. - -The easiest way to start with Spring Boot and Kotlin is to follow https://spring.io/guides/tutorials/spring-boot-kotlin/[this comprehensive tutorial]. -You can create new Kotlin projects via https://start.spring.io/#!language=kotlin[start.spring.io]. -Feel free to join the #spring channel of https://slack.kotlinlang.org/[Kotlin Slack] or ask a question with the `spring` and `kotlin` tags on https://stackoverflow.com/questions/tagged/spring+kotlin[Stack Overflow] if you need support. - - - -[[boot-features-kotlin-requirements]] -=== Requirements -Spring Boot supports Kotlin 1.3.x. -To use Kotlin, `org.jetbrains.kotlin:kotlin-stdlib` and `org.jetbrains.kotlin:kotlin-reflect` must be present on the classpath. -The `kotlin-stdlib` variants `kotlin-stdlib-jdk7` and `kotlin-stdlib-jdk8` can also be used. - -Since https://discuss.kotlinlang.org/t/classes-final-by-default/166[Kotlin classes are final by default], you are likely to want to configure {kotlin-docs}compiler-plugins.html#spring-support[kotlin-spring] plugin in order to automatically open Spring-annotated classes so that they can be proxied. - -https://github.com/FasterXML/jackson-module-kotlin[Jackson's Kotlin module] is required for serializing / deserializing JSON data in Kotlin. -It is automatically registered when found on the classpath. -A warning message is logged if Jackson and Kotlin are present but the Jackson Kotlin module is not. - -TIP: These dependencies and plugins are provided by default if one bootstraps a Kotlin project on https://start.spring.io/#!language=kotlin[start.spring.io]. - - - -[[boot-features-kotlin-null-safety]] -=== Null-safety -One of Kotlin's key features is {kotlin-docs}null-safety.html[null-safety]. -It deals with `null` values at compile time rather than deferring the problem to runtime and encountering a `NullPointerException`. -This helps to eliminate a common source of bugs without paying the cost of wrappers like `Optional`. -Kotlin also allows using functional constructs with nullable values as described in this https://www.baeldung.com/kotlin-null-safety[comprehensive guide to null-safety in Kotlin]. - -Although Java does not allow one to express null-safety in its type system, Spring Framework, Spring Data, and Reactor now provide null-safety of their API via tooling-friendly annotations. -By default, types from Java APIs used in Kotlin are recognized as {kotlin-docs}java-interop.html#null-safety-and-platform-types[platform types] for which null-checks are relaxed. -{kotlin-docs}java-interop.html#jsr-305-support[Kotlin's support for JSR 305 annotations] combined with nullability annotations provide null-safety for the related Spring API in Kotlin. - -The JSR 305 checks can be configured by adding the `-Xjsr305` compiler flag with the following options: `-Xjsr305={strict|warn|ignore}`. -The default behavior is the same as `-Xjsr305=warn`. -The `strict` value is required to have null-safety taken in account in Kotlin types inferred from Spring API but should be used with the knowledge that Spring API nullability declaration could evolve even between minor releases and more checks may be added in the future). - -WARNING: Generic type arguments, varargs and array elements nullability are not yet supported. -See https://jira.spring.io/browse/SPR-15942[SPR-15942] for up-to-date information. -Also be aware that Spring Boot's own API is {github-issues}10712[not yet annotated]. - - - -[[boot-features-kotlin-api]] -=== Kotlin API - - - -[[boot-features-kotlin-api-runapplication]] -==== runApplication -Spring Boot provides an idiomatic way to run an application with `runApplication(*args)` as shown in the following example: - -[source,kotlin,indent=0] ----- - import org.springframework.boot.autoconfigure.SpringBootApplication - import org.springframework.boot.runApplication - - @SpringBootApplication - class MyApplication - - fun main(args: Array) { - runApplication(*args) - } ----- - -This is a drop-in replacement for `SpringApplication.run(MyApplication::class.java, *args)`. -It also allows customization of the application as shown in the following example: - -[source,kotlin,indent=0] ----- - runApplication(*args) { - setBannerMode(OFF) - } ----- - - - -[[boot-features-kotlin-api-extensions]] -==== Extensions -Kotlin {kotlin-docs}extensions.html[extensions] provide the ability to extend existing classes with additional functionality. -The Spring Boot Kotlin API makes use of these extensions to add new Kotlin specific conveniences to existing APIs. - -`TestRestTemplate` extensions, similar to those provided by Spring Framework for `RestOperations` in Spring Framework, are provided. -Among other things, the extensions make it possible to take advantage of Kotlin reified type parameters. - - - -[[boot-features-kotlin-dependency-management]] -=== Dependency management -In order to avoid mixing different versions of Kotlin dependencies on the classpath, Spring Boot imports the Kotlin BOM. - -With Maven, the Kotlin version can be customized via the `kotlin.version` property and plugin management is provided for `kotlin-maven-plugin`. -With Gradle, the Spring Boot plugin automatically aligns the `kotlin.version` with the version of the Kotlin plugin. - -Spring Boot also manages the version of Coroutines dependencies by importing the Kotlin Coroutines BOM. -The version can be customized via the `kotlin-coroutines.version` property. - -TIP: `org.jetbrains.kotlinx:kotlinx-coroutines-reactor` dependency is provided by default if one bootstraps a Kotlin project with at least one reactive dependency on https://start.spring.io/#!language=kotlin[start.spring.io]. - - -[[boot-features-kotlin-configuration-properties]] -=== `@ConfigurationProperties` -`@ConfigurationProperties` when used in combination with <> supports classes with immutable `val` properties as shown in the following example: - -[source,kotlin,indent=0] ----- -@ConstructorBinding -@ConfigurationProperties("example.kotlin") -data class KotlinExampleProperties( - val name: String, - val description: String, - val myService: MyService) { - - data class MyService( - val apiToken: String, - val uri: URI - ) -} ----- - -TIP: To generate <> using the annotation processor, {kotlin-docs}kapt.html[`kapt` should be configured] with the `spring-boot-configuration-processor` dependency. -Note that some features (such as detecting the default value or deprecated items) are not working due to limitations in the model kapt provides. - - - -[[boot-features-kotlin-testing]] -=== Testing -While it is possible to use JUnit 4 to test Kotlin code, JUnit 5 is provided by default and is recommended. -JUnit 5 enables a test class to be instantiated once and reused for all of the class's tests. -This makes it possible to use `@BeforeClass` and `@AfterClass` annotations on non-static methods, which is a good fit for Kotlin. - -JUnit 5 is the default and the vintage engine is provided for backward compatibility with JUnit 4. -If you don't use it, exclude `org.junit.vintange:junit-vintage-engine`. -You also need to {junit5-docs}/#writing-tests-test-instance-lifecycle-changing-default[switch test instance lifecycle to "per-class"]. - -To mock Kotlin classes, https://mockk.io/[MockK] is recommended. -If you need the `Mockk` equivalent of the Mockito specific <>, you can use https://github.com/Ninja-Squad/springmockk[SpringMockK] which provides similar `@MockkBean` and `@SpykBean` annotations. - - - -[[boot-features-kotlin-resources]] -=== Resources - - - -[[boot-features-kotlin-resources-further-reading]] -==== Further reading -* {kotlin-docs}[Kotlin language reference] -* https://slack.kotlinlang.org/[Kotlin Slack] (with a dedicated #spring channel) -* https://stackoverflow.com/questions/tagged/spring+kotlin[Stackoverflow with `spring` and `kotlin` tags] -* https://try.kotlinlang.org/[Try Kotlin in your browser] -* https://blog.jetbrains.com/kotlin/[Kotlin blog] -* https://kotlin.link/[Awesome Kotlin] -* https://spring.io/guides/tutorials/spring-boot-kotlin/[Tutorial: building web applications with Spring Boot and Kotlin] -* https://spring.io/blog/2016/02/15/developing-spring-boot-applications-with-kotlin[Developing Spring Boot applications with Kotlin] -* https://spring.io/blog/2016/03/20/a-geospatial-messenger-with-kotlin-spring-boot-and-postgresql[A Geospatial Messenger with Kotlin, Spring Boot and PostgreSQL] -* https://spring.io/blog/2017/01/04/introducing-kotlin-support-in-spring-framework-5-0[Introducing Kotlin support in Spring Framework 5.0] -* https://spring.io/blog/2017/08/01/spring-framework-5-kotlin-apis-the-functional-way[Spring Framework 5 Kotlin APIs, the functional way] - - - -[[boot-features-kotlin-resources-examples]] -==== Examples -* https://github.com/sdeleuze/spring-boot-kotlin-demo[spring-boot-kotlin-demo]: regular Spring Boot + Spring Data JPA project -* https://github.com/mixitconf/mixit[mixit]: Spring Boot 2 + WebFlux + Reactive Spring Data MongoDB -* https://github.com/sdeleuze/spring-kotlin-fullstack[spring-kotlin-fullstack]: WebFlux Kotlin fullstack example with Kotlin2js for frontend instead of JavaScript or TypeScript -* https://github.com/spring-petclinic/spring-petclinic-kotlin[spring-petclinic-kotlin]: Kotlin version of the Spring PetClinic Sample Application -* https://github.com/sdeleuze/spring-kotlin-deepdive[spring-kotlin-deepdive]: a step by step migration for Boot 1.0 + Java to Boot 2.0 + Kotlin -* https://github.com/sdeleuze/spring-boot-coroutines-demo[spring-boot-coroutines-demo]: Coroutines sample project - - - -[[boot-features-whats-next]] -== What to Read Next -If you want to learn more about any of the classes discussed in this section, you can check out the {spring-boot-api}[Spring Boot API documentation] or you can browse the {spring-boot-code}[source code directly]. -If you have specific questions, take a look at the <> section. - -If you are comfortable with Spring Boot's core features, you can continue on and read about <>. diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-reference.pdfadoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-reference.pdfadoc deleted file mode 100644 index 1e5720829ff0..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-reference.pdfadoc +++ /dev/null @@ -1,28 +0,0 @@ -[[spring-boot-reference-documentation]] -= Spring Boot Reference Documentation -Phillip Webb, Dave Syer, Josh Long, Stéphane Nicoll, Rob Winch, Andy Wilkinson, Marcel Overdijk, Christian Dupuis, Sébastien Deleuze, Michael Simons, Vedran Pavić, Jay Bryant, Madhura Bhave -:docinfo: shared -include::attributes.adoc[] - -include::legal.adoc[leveloffset=+1] -include::documentation-overview.adoc[leveloffset=+1] -include::getting-started.adoc[leveloffset=+1] -include::using-spring-boot.adoc[leveloffset=+1] -include::spring-boot-features.adoc[leveloffset=+1] -include::production-ready-features.adoc[leveloffset=+1] -include::deployment.adoc[leveloffset=+1] -include::spring-boot-cli.adoc[leveloffset=+1] -include::build-tool-plugins.adoc[leveloffset=+1] -include::howto.adoc[leveloffset=+1] - - - -[[appendix]] -== Appendices - -include::appendix-application-properties.adoc[leveloffset=+2] -include::appendix-configuration-metadata.adoc[leveloffset=+2] -include::appendix-auto-configuration-classes.adoc[leveloffset=+2] -include::appendix-test-auto-configuration.adoc[leveloffset=+2] -include::appendix-executable-jar-format.adoc[leveloffset=+2] -include::appendix-dependency-versions.adoc[leveloffset=+2] diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/using-spring-boot.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/using-spring-boot.adoc deleted file mode 100644 index be9a1121faab..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/using-spring-boot.adoc +++ /dev/null @@ -1,1039 +0,0 @@ -[[using-boot]] -= Using Spring Boot -include::attributes.adoc[] - -This section goes into more detail about how you should use Spring Boot. -It covers topics such as build systems, auto-configuration, and how to run your applications. -We also cover some Spring Boot best practices. -Although there is nothing particularly special about Spring Boot (it is just another library that you can consume), there are a few recommendations that, when followed, make your development process a little easier. - -If you are starting out with Spring Boot, you should probably read the _<>_ guide before diving into this section. - - - -[[using-boot-build-systems]] -== Build Systems -It is strongly recommended that you choose a build system that supports <> and that can consume artifacts published to the "`Maven Central`" repository. -We would recommend that you choose Maven or Gradle. -It is possible to get Spring Boot to work with other build systems (Ant, for example), but they are not particularly well supported. - - - -[[using-boot-dependency-management]] -=== Dependency Management -Each release of Spring Boot provides a curated list of dependencies that it supports. -In practice, you do not need to provide a version for any of these dependencies in your build configuration, as Spring Boot manages that for you. -When you upgrade Spring Boot itself, these dependencies are upgraded as well in a consistent way. - -NOTE: You can still specify a version and override Spring Boot's recommendations if you need to do so. - -The curated list contains all the spring modules that you can use with Spring Boot as well as a refined list of third party libraries. -The list is available as a standard <> that can be used with both <> and <>. - -WARNING: Each release of Spring Boot is associated with a base version of the Spring Framework. -We **highly** recommend that you not specify its version. - - - -[[using-boot-maven]] -=== Maven -Maven users can inherit from the `spring-boot-starter-parent` project to obtain sensible defaults. -The parent project provides the following features: - -* Java 1.8 as the default compiler level. -* UTF-8 source encoding. -* A <>, inherited from the spring-boot-dependencies pom, that manages the versions of common dependencies. -This dependency management lets you omit tags for those dependencies when used in your own pom. -* An execution of the {spring-boot-maven-plugin-docs}/repackage-mojo.html[`repackage` goal] with a `repackage` execution id. -* Sensible https://maven.apache.org/plugins/maven-resources-plugin/examples/filter.html[resource filtering]. -* Sensible plugin configuration (https://www.mojohaus.org/exec-maven-plugin/[exec plugin], https://github.com/ktoso/maven-git-commit-id-plugin[Git commit ID], and https://maven.apache.org/plugins/maven-shade-plugin/[shade]). -* Sensible resource filtering for `application.properties` and `application.yml` including profile-specific files (for example, `application-dev.properties` and `application-dev.yml`) - -Note that, since the `application.properties` and `application.yml` files accept Spring style placeholders (`${...}`), the Maven filtering is changed to use `@..@` placeholders. -(You can override that by setting a Maven property called `resource.delimiter`.) - - - -[[using-boot-maven-parent-pom]] -==== Inheriting the Starter Parent -To configure your project to inherit from the `spring-boot-starter-parent`, set the `parent` as follows: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - - org.springframework.boot - spring-boot-starter-parent - {spring-boot-version} - ----- - -NOTE: You should need to specify only the Spring Boot version number on this dependency. -If you import additional starters, you can safely omit the version number. - -With that setup, you can also override individual dependencies by overriding a property in your own project. -For instance, to upgrade to another Spring Data release train, you would add the following to your `pom.xml`: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - Fowler-SR2 - ----- - -TIP: Check the {spring-boot-code}/spring-boot-project/spring-boot-dependencies/pom.xml[`spring-boot-dependencies` pom] for a list of supported properties. - - - -[[using-boot-maven-without-a-parent]] -==== Using Spring Boot without the Parent POM -Not everyone likes inheriting from the `spring-boot-starter-parent` POM. -You may have your own corporate standard parent that you need to use or you may prefer to explicitly declare all your Maven configuration. - -If you do not want to use the `spring-boot-starter-parent`, you can still keep the benefit of the dependency management (but not the plugin management) by using a `scope=import` dependency, as follows: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - - - - org.springframework.boot - spring-boot-dependencies - {spring-boot-version} - pom - import - - - ----- - -The preceding sample setup does not let you override individual dependencies by using a property, as explained above. -To achieve the same result, you need to add an entry in the `dependencyManagement` of your project **before** the `spring-boot-dependencies` entry. -For instance, to upgrade to another Spring Data release train, you could add the following element to your `pom.xml`: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - - - - org.springframework.data - spring-data-releasetrain - Fowler-SR2 - pom - import - - - org.springframework.boot - spring-boot-dependencies - {spring-boot-version} - pom - import - - - ----- - -NOTE: In the preceding example, we specify a _BOM_, but any dependency type can be overridden in the same way. - - - -[[using-boot-maven-plugin]] -==== Using the Spring Boot Maven Plugin -Spring Boot includes a <> that can package the project as an executable jar. -Add the plugin to your `` section if you want to use it, as shown in the following example: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - - - org.springframework.boot - spring-boot-maven-plugin - - - ----- - -NOTE: If you use the Spring Boot starter parent pom, you need to add only the plugin. -There is no need to configure it unless you want to change the settings defined in the parent. - - - -[[using-boot-gradle]] -=== Gradle -To learn about using Spring Boot with Gradle, please refer to the documentation for Spring Boot's Gradle plugin: - -* Reference ({spring-boot-gradle-plugin-docs}[HTML] and {spring-boot-gradle-plugin-pdfdocs}[PDF]) -* {spring-boot-gradle-plugin-api}[API] - - - -[[using-boot-ant]] -=== Ant -It is possible to build a Spring Boot project using Apache Ant+Ivy. -The `spring-boot-antlib` "`AntLib`" module is also available to help Ant create executable jars. - -To declare dependencies, a typical `ivy.xml` file looks something like the following example: - -[source,xml,indent=0] ----- - - - - - - - - - - ----- - -A typical `build.xml` looks like the following example: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ----- - -TIP: If you do not want to use the `spring-boot-antlib` module, see the _<>_ "`How-to`" . - - - -[[using-boot-starter]] -=== Starters -Starters are a set of convenient dependency descriptors that you can include in your application. -You get a one-stop shop for all the Spring and related technologies that you need without having to hunt through sample code and copy-paste loads of dependency descriptors. -For example, if you want to get started using Spring and JPA for database access, include the `spring-boot-starter-data-jpa` dependency in your project. - -The starters contain a lot of the dependencies that you need to get a project up and running quickly and with a consistent, supported set of managed transitive dependencies. - -.What's in a name -**** -All **official** starters follow a similar naming pattern; `+spring-boot-starter-*+`, where `+*+` is a particular type of application. -This naming structure is intended to help when you need to find a starter. -The Maven integration in many IDEs lets you search dependencies by name. -For example, with the appropriate Eclipse or STS plugin installed, you can press `ctrl-space` in the POM editor and type "`spring-boot-starter`" for a complete list. - -As explained in the "`<>`" section, third party starters should not start with `spring-boot`, as it is reserved for official Spring Boot artifacts. -Rather, a third-party starter typically starts with the name of the project. -For example, a third-party starter project called `thirdpartyproject` would typically be named `thirdpartyproject-spring-boot-starter`. -**** - -The following application starters are provided by Spring Boot under the `org.springframework.boot` group: - -.Spring Boot application starters -include::{generated-resources-root}/application-starters.adoc[] - -In addition to the application starters, the following starters can be used to add _<>_ features: - -.Spring Boot production starters -include::{generated-resources-root}/production-starters.adoc[] - -Finally, Spring Boot also includes the following starters that can be used if you want to exclude or swap specific technical facets: - -.Spring Boot technical starters -include::{generated-resources-root}/technical-starters.adoc[] - -TIP: For a list of additional community contributed starters, see the {spring-boot-master-code}/spring-boot-project/spring-boot-starters/README.adoc[README file] in the `spring-boot-starters` module on GitHub. - - - -[[using-boot-structuring-your-code]] -== Structuring Your Code -Spring Boot does not require any specific code layout to work. -However, there are some best practices that help. - - - -[[using-boot-using-the-default-package]] -=== Using the "`default`" Package -When a class does not include a `package` declaration, it is considered to be in the "`default package`". -The use of the "`default package`" is generally discouraged and should be avoided. -It can cause particular problems for Spring Boot applications that use the `@ComponentScan`, `@ConfigurationPropertiesScan`, `@EntityScan`, or `@SpringBootApplication` annotations, since every class from every jar is read. - -TIP: We recommend that you follow Java's recommended package naming conventions and use a reversed domain name (for example, `com.example.project`). - - - -[[using-boot-locating-the-main-class]] -=== Locating the Main Application Class -We generally recommend that you locate your main application class in a root package above other classes. -The <> is often placed on your main class, and it implicitly defines a base "`search package`" for certain items. -For example, if you are writing a JPA application, the package of the `@SpringBootApplication` annotated class is used to search for `@Entity` items. -Using a root package also allows component scan to apply only on your project. - -TIP: If you don't want to use `@SpringBootApplication`, the `@EnableAutoConfiguration` and `@ComponentScan` annotations that it imports defines that behaviour so you can also use those instead. - -The following listing shows a typical layout: - -[indent=0] ----- - com - +- example - +- myapplication - +- Application.java - | - +- customer - | +- Customer.java - | +- CustomerController.java - | +- CustomerService.java - | +- CustomerRepository.java - | - +- order - +- Order.java - +- OrderController.java - +- OrderService.java - +- OrderRepository.java ----- - -The `Application.java` file would declare the `main` method, along with the basic `@SpringBootApplication`, as follows: - -[source,java,indent=0] ----- - package com.example.myapplication; - - import org.springframework.boot.SpringApplication; - import org.springframework.boot.autoconfigure.SpringBootApplication; - - @SpringBootApplication - public class Application { - - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } - - } ----- - - - -[[using-boot-configuration-classes]] -== Configuration Classes -Spring Boot favors Java-based configuration. -Although it is possible to use `SpringApplication` with XML sources, we generally recommend that your primary source be a single `@Configuration` class. -Usually the class that defines the `main` method is a good candidate as the primary `@Configuration`. - -TIP: Many Spring configuration examples have been published on the Internet that use XML configuration. -If possible, always try to use the equivalent Java-based configuration. -Searching for `+Enable*+` annotations can be a good starting point. - - - -[[using-boot-importing-configuration]] -=== Importing Additional Configuration Classes -You need not put all your `@Configuration` into a single class. -The `@Import` annotation can be used to import additional configuration classes. -Alternatively, you can use `@ComponentScan` to automatically pick up all Spring components, including `@Configuration` classes. - - - -[[using-boot-importing-xml-configuration]] -=== Importing XML Configuration -If you absolutely must use XML based configuration, we recommend that you still start with a `@Configuration` class. -You can then use an `@ImportResource` annotation to load XML configuration files. - - - -[[using-boot-auto-configuration]] -== Auto-configuration -Spring Boot auto-configuration attempts to automatically configure your Spring application based on the jar dependencies that you have added. -For example, if `HSQLDB` is on your classpath, and you have not manually configured any database connection beans, then Spring Boot auto-configures an in-memory database. - -You need to opt-in to auto-configuration by adding the `@EnableAutoConfiguration` or `@SpringBootApplication` annotations to one of your `@Configuration` classes. - -TIP: You should only ever add one `@SpringBootApplication` or `@EnableAutoConfiguration` annotation. -We generally recommend that you add one or the other to your primary `@Configuration` class only. - - - -[[using-boot-replacing-auto-configuration]] -=== Gradually Replacing Auto-configuration -Auto-configuration is non-invasive. -At any point, you can start to define your own configuration to replace specific parts of the auto-configuration. -For example, if you add your own `DataSource` bean, the default embedded database support backs away. - -If you need to find out what auto-configuration is currently being applied, and why, start your application with the `--debug` switch. -Doing so enables debug logs for a selection of core loggers and logs a conditions report to the console. - - - -[[using-boot-disabling-specific-auto-configuration]] -=== Disabling Specific Auto-configuration Classes -If you find that specific auto-configuration classes that you do not want are being applied, you can use the exclude attribute of `@EnableAutoConfiguration` to disable them, as shown in the following example: - -[source,java,indent=0] ----- - import org.springframework.boot.autoconfigure.*; - import org.springframework.boot.autoconfigure.jdbc.*; - import org.springframework.context.annotation.*; - - @Configuration(proxyBeanMethods = false) - @EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class}) - public class MyConfiguration { - } ----- - -If the class is not on the classpath, you can use the `excludeName` attribute of the annotation and specify the fully qualified name instead. -Finally, you can also control the list of auto-configuration classes to exclude by using the configprop:spring.autoconfigure.exclude[] property. - -TIP: You can define exclusions both at the annotation level and by using the property. - -NOTE: Even though auto-configuration classes are `public`, the only aspect of the class that is considered public API is the name of the class which can be used for disabling the auto-configuration. -The actual contents of those classes, such as nested configuration classes or bean methods are for internal use only and we do not recommend using those directly. - - - -[[using-boot-spring-beans-and-dependency-injection]] -== Spring Beans and Dependency Injection -You are free to use any of the standard Spring Framework techniques to define your beans and their injected dependencies. -For simplicity, we often find that using `@ComponentScan` (to find your beans) and using `@Autowired` (to do constructor injection) works well. - -If you structure your code as suggested above (locating your application class in a root package), you can add `@ComponentScan` without any arguments. -All of your application components (`@Component`, `@Service`, `@Repository`, `@Controller` etc.) are automatically registered as Spring Beans. - -The following example shows a `@Service` Bean that uses constructor injection to obtain a required `RiskAssessor` bean: - -[source,java,indent=0] ----- - package com.example.service; - - import org.springframework.beans.factory.annotation.Autowired; - import org.springframework.stereotype.Service; - - @Service - public class DatabaseAccountService implements AccountService { - - private final RiskAssessor riskAssessor; - - @Autowired - public DatabaseAccountService(RiskAssessor riskAssessor) { - this.riskAssessor = riskAssessor; - } - - // ... - - } ----- - -If a bean has one constructor, you can omit the `@Autowired`, as shown in the following example: - -[source,java,indent=0] ----- - @Service - public class DatabaseAccountService implements AccountService { - - private final RiskAssessor riskAssessor; - - public DatabaseAccountService(RiskAssessor riskAssessor) { - this.riskAssessor = riskAssessor; - } - - // ... - - } ----- - -TIP: Notice how using constructor injection lets the `riskAssessor` field be marked as `final`, indicating that it cannot be subsequently changed. - - - -[[using-boot-using-springbootapplication-annotation]] -== Using the @SpringBootApplication Annotation -Many Spring Boot developers like their apps to use auto-configuration, component scan and be able to define extra configuration on their "application class". -A single `@SpringBootApplication` annotation can be used to enable those three features, that is: - -* `@EnableAutoConfiguration`: enable <> -* `@ComponentScan`: enable `@Component` scan on the package where the application is located (see <>) -* `@Configuration`: allow to register extra beans in the context or import additional configuration classes - -[source,java,indent=0] ----- - package com.example.myapplication; - - import org.springframework.boot.SpringApplication; - import org.springframework.boot.autoconfigure.SpringBootApplication; - - @SpringBootApplication // same as @Configuration @EnableAutoConfiguration @ComponentScan - public class Application { - - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } - - } ----- - -NOTE: `@SpringBootApplication` also provides aliases to customize the attributes of `@EnableAutoConfiguration` and `@ComponentScan`. - -[NOTE] -==== -None of these features are mandatory and you may choose to replace this single annotation by any of the features that it enables. -For instance, you may not want to use component scan or configuration properties scan in your application: - -[source,java,indent=0] ----- - package com.example.myapplication; - - import org.springframework.boot.SpringApplication; - import org.springframework.context.annotation.ComponentScan - import org.springframework.context.annotation.Configuration; - import org.springframework.context.annotation.Import; - - @Configuration(proxyBeanMethods = false) - @EnableAutoConfiguration - @Import({ MyConfig.class, MyAnotherConfig.class }) - public class Application { - - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } - - } ----- - -In this example, `Application` is just like any other Spring Boot application except that `@Component`-annotated classes and `@ConfigurationProperties`-annotated classes are not detected automatically and the user-defined beans are imported explicitly (see `@Import`). -==== - - - -[[using-boot-running-your-application]] -== Running Your Application -One of the biggest advantages of packaging your application as a jar and using an embedded HTTP server is that you can run your application as you would any other. -Debugging Spring Boot applications is also easy. -You do not need any special IDE plugins or extensions. - -NOTE: This section only covers jar based packaging. -If you choose to package your application as a war file, you should refer to your server and IDE documentation. - - - -[[using-boot-running-from-an-ide]] -=== Running from an IDE -You can run a Spring Boot application from your IDE as a simple Java application. -However, you first need to import your project. -Import steps vary depending on your IDE and build system. -Most IDEs can import Maven projects directly. -For example, Eclipse users can select `Import...` -> `Existing Maven Projects` from the `File` menu. - -If you cannot directly import your project into your IDE, you may be able to generate IDE metadata by using a build plugin. -Maven includes plugins for https://maven.apache.org/plugins/maven-eclipse-plugin/[Eclipse] and https://maven.apache.org/plugins/maven-idea-plugin/[IDEA]. -Gradle offers plugins for {gradle-docs}/userguide.html[various IDEs]. - -TIP: If you accidentally run a web application twice, you see a "`Port already in use`" error. -STS users can use the `Relaunch` button rather than the `Run` button to ensure that any existing instance is closed. - - - -[[using-boot-running-as-a-packaged-application]] -=== Running as a Packaged Application -If you use the Spring Boot Maven or Gradle plugins to create an executable jar, you can run your application using `java -jar`, as shown in the following example: - -[indent=0,subs="attributes"] ----- - $ java -jar target/myapplication-0.0.1-SNAPSHOT.jar ----- - -It is also possible to run a packaged application with remote debugging support enabled. -Doing so lets you attach a debugger to your packaged application, as shown in the following example: - -[indent=0,subs="attributes"] ----- - $ java -Xdebug -Xrunjdwp:server=y,transport=dt_socket,address=8000,suspend=n \ - -jar target/myapplication-0.0.1-SNAPSHOT.jar ----- - - - -[[using-boot-running-with-the-maven-plugin]] -=== Using the Maven Plugin -The Spring Boot Maven plugin includes a `run` goal that can be used to quickly compile and run your application. -Applications run in an exploded form, as they do in your IDE. -The following example shows a typical Maven command to run a Spring Boot application: - -[indent=0,subs="attributes"] ----- - $ mvn spring-boot:run ----- - -You might also want to use the `MAVEN_OPTS` operating system environment variable, as shown in the following example: - -[indent=0,subs="attributes"] ----- - $ export MAVEN_OPTS=-Xmx1024m ----- - - - -[[using-boot-running-with-the-gradle-plugin]] -=== Using the Gradle Plugin -The Spring Boot Gradle plugin also includes a `bootRun` task that can be used to run your application in an exploded form. -The `bootRun` task is added whenever you apply the `org.springframework.boot` and `java` plugins and is shown in the following example: - -[indent=0,subs="attributes"] ----- - $ gradle bootRun ----- - -You might also want to use the `JAVA_OPTS` operating system environment variable, as shown in the following example: - -[indent=0,subs="attributes"] ----- - $ export JAVA_OPTS=-Xmx1024m ----- - - - -[[using-boot-hot-swapping]] -=== Hot Swapping -Since Spring Boot applications are just plain Java applications, JVM hot-swapping should work out of the box. -JVM hot swapping is somewhat limited with the bytecode that it can replace. -For a more complete solution, https://jrebel.com/software/jrebel/[JRebel] can be used. - -The `spring-boot-devtools` module also includes support for quick application restarts. -See the <> section later in this chapter and the <> for details. - - - -[[using-boot-devtools]] -== Developer Tools -Spring Boot includes an additional set of tools that can make the application development experience a little more pleasant. -The `spring-boot-devtools` module can be included in any project to provide additional development-time features. -To include devtools support, add the module dependency to your build, as shown in the following listings for Maven and Gradle: - -.Maven -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - - org.springframework.boot - spring-boot-devtools - true - - ----- - -.Gradle -[source,groovy,indent=0,subs="attributes"] ----- - configurations { - developmentOnly - runtimeClasspath { - extendsFrom developmentOnly - } - } - dependencies { - developmentOnly("org.springframework.boot:spring-boot-devtools") - } ----- - -NOTE: Developer tools are automatically disabled when running a fully packaged application. -If your application is launched from `java -jar` or if it is started from a special classloader, then it is considered a "`production application`". -If that does not apply to you (i.e. if you run your application from a container), consider excluding devtools or set the `-Dspring.devtools.restart.enabled=false` system property. - -TIP: Flagging the dependency as optional in Maven or using a custom `developmentOnly` configuration in Gradle (as shown above) is a best practice that prevents devtools from being transitively applied to other modules that use your project. - -TIP: Repackaged archives do not contain devtools by default. -If you want to use a <>, you need to disable the `excludeDevtools` build property to include it. -The property is supported with both the Maven and Gradle plugins. - - - -[[using-boot-devtools-property-defaults]] -=== Property Defaults -Several of the libraries supported by Spring Boot use caches to improve performance. -For example, <> cache compiled templates to avoid repeatedly parsing template files. -Also, Spring MVC can add HTTP caching headers to responses when serving static resources. - -While caching is very beneficial in production, it can be counter-productive during development, preventing you from seeing the changes you just made in your application. -For this reason, spring-boot-devtools disables the caching options by default. - -Cache options are usually configured by settings in your `application.properties` file. -For example, Thymeleaf offers the configprop:spring.thymeleaf.cache[] property. -Rather than needing to set these properties manually, the `spring-boot-devtools` module automatically applies sensible development-time configuration. - -Because you need more information about web requests while developing Spring MVC and Spring WebFlux applications, developer tools will enable `DEBUG` logging for the `web` logging group. -This will give you information about the incoming request, which handler is processing it, the response outcome, etc. -If you wish to log all request details (including potentially sensitive information), you can turn on the configprop:spring.http.log-request-details[] configuration property. - -NOTE: If you don't want property defaults to be applied you can set configprop:spring.devtools.add-properties[] to `false` in your `application.properties`. - -TIP: For a complete list of the properties that are applied by the devtools, see {spring-boot-devtools-module-code}/env/DevToolsPropertyDefaultsPostProcessor.java[DevToolsPropertyDefaultsPostProcessor]. - - - -[[using-boot-devtools-restart]] -=== Automatic Restart -Applications that use `spring-boot-devtools` automatically restart whenever files on the classpath change. -This can be a useful feature when working in an IDE, as it gives a very fast feedback loop for code changes. -By default, any entry on the classpath that points to a folder is monitored for changes. -Note that certain resources, such as static assets and view templates, <>. - -.Triggering a restart -**** -As DevTools monitors classpath resources, the only way to trigger a restart is to update the classpath. -The way in which you cause the classpath to be updated depends on the IDE that you are using. -In Eclipse, saving a modified file causes the classpath to be updated and triggers a restart. -In IntelliJ IDEA, building the project (`Build +->+ Build Project`) has the same effect. -**** - -NOTE: As long as forking is enabled, you can also start your application by using the supported build plugins (Maven and Gradle), since DevTools needs an isolated application classloader to operate properly. -By default, the Gradle and Maven plugins fork the application process. - -TIP: Automatic restart works very well when used with LiveReload. -<> for details. -If you use JRebel, automatic restarts are disabled in favor of dynamic class reloading. -Other devtools features (such as LiveReload and property overrides) can still be used. - -NOTE: DevTools relies on the application context's shutdown hook to close it during a restart. -It does not work correctly if you have disabled the shutdown hook (`SpringApplication.setRegisterShutdownHook(false)`). - -NOTE: When deciding if an entry on the classpath should trigger a restart when it changes, DevTools automatically ignores projects named `spring-boot`, `spring-boot-devtools`, `spring-boot-autoconfigure`, `spring-boot-actuator`, and `spring-boot-starter`. - -NOTE: DevTools needs to customize the `ResourceLoader` used by the `ApplicationContext`. -If your application provides one already, it is going to be wrapped. -Direct override of the `getResource` method on the `ApplicationContext` is not supported. - -[[using-spring-boot-restart-vs-reload]] -.Restart vs Reload -**** -The restart technology provided by Spring Boot works by using two classloaders. -Classes that do not change (for example, those from third-party jars) are loaded into a _base_ classloader. -Classes that you are actively developing are loaded into a _restart_ classloader. -When the application is restarted, the _restart_ classloader is thrown away and a new one is created. -This approach means that application restarts are typically much faster than "`cold starts`", since the _base_ classloader is already available and populated. - -If you find that restarts are not quick enough for your applications or you encounter classloading issues, you could consider reloading technologies such as https://jrebel.com/software/jrebel/[JRebel] from ZeroTurnaround. -These work by rewriting classes as they are loaded to make them more amenable to reloading. -**** - - - -[[using-boot-devtools-restart-logging-condition-delta]] -==== Logging changes in condition evaluation -By default, each time your application restarts, a report showing the condition evaluation delta is logged. -The report shows the changes to your application's auto-configuration as you make changes such as adding or removing beans and setting configuration properties. - -To disable the logging of the report, set the following property: - -[indent=0] ----- - spring.devtools.restart.log-condition-evaluation-delta=false ----- - - -[[using-boot-devtools-restart-exclude]] -==== Excluding Resources -Certain resources do not necessarily need to trigger a restart when they are changed. -For example, Thymeleaf templates can be edited in-place. -By default, changing resources in `/META-INF/maven`, `/META-INF/resources`, `/resources`, `/static`, `/public`, or `/templates` does not trigger a restart but does trigger a <>. -If you want to customize these exclusions, you can use the configprop:spring.devtools.restart.exclude[] property. -For example, to exclude only `/static` and `/public` you would set the following property: - -[indent=0] ----- - spring.devtools.restart.exclude=static/**,public/** ----- - -TIP: If you want to keep those defaults and _add_ additional exclusions, use the configprop:spring.devtools.restart.additional-exclude[] property instead. - - - -[[using-boot-devtools-restart-additional-paths]] -==== Watching Additional Paths -You may want your application to be restarted or reloaded when you make changes to files that are not on the classpath. -To do so, use the configprop:spring.devtools.restart.additional-paths[] property to configure additional paths to watch for changes. -You can use the configprop:spring.devtools.restart.exclude[] property <> to control whether changes beneath the additional paths trigger a full restart or a <>. - - - -[[using-boot-devtools-restart-disable]] -==== Disabling Restart -If you do not want to use the restart feature, you can disable it by using the configprop:spring.devtools.restart.enabled[] property. -In most cases, you can set this property in your `application.properties` (doing so still initializes the restart classloader, but it does not watch for file changes). - -If you need to _completely_ disable restart support (for example, because it does not work with a specific library), you need to set the configprop:spring.devtools.restart.enabled[] `System` property to `false` before calling `SpringApplication.run(...)`, as shown in the following example: - -[source,java,indent=0] ----- - public static void main(String[] args) { - System.setProperty("spring.devtools.restart.enabled", "false"); - SpringApplication.run(MyApp.class, args); - } ----- - - - -[[using-boot-devtools-restart-triggerfile]] -==== Using a Trigger File -If you work with an IDE that continuously compiles changed files, you might prefer to trigger restarts only at specific times. -To do so, you can use a "`trigger file`", which is a special file that must be modified when you want to actually trigger a restart check. - -NOTE: Any update to the file will trigger a check, but restart only actually occurs if Devtools has detected it has something to do. - -To use a trigger file, set the configprop:spring.devtools.restart.trigger-file[] property to the name (excluding any path) of your trigger file. -The trigger file must appear somewhere on your classpath. - -For example, if you have a project with the following structure: - -[indent=0] ----- - src - +- main - +- resources - +- .reloadtrigger ----- - -Then your `trigger-file` property would be: - -[source,properties,indent=0,configprops] ----- - spring.devtools.restart.trigger-file=.reloadtrigger ----- - -Restarts will now only happen when the `src/main/resources/.reloadtrigger` is updated. - -TIP: You might want to set `spring.devtools.restart.trigger-file` as a <>, so that all your projects behave in the same way. - -Some IDEs have features that save you from needing to update your trigger file manually. -https://spring.io/tools[Spring Tools for Eclipse] and https://www.jetbrains.com/idea/[IntelliJ IDEA (Ultimate Edition)] both have such support. -With Spring Tools, you can use the "`reload`" button from the console view (as long as your `trigger-file` is named `.reloadtrigger`). -For IntelliJ, you can follow the https://www.jetbrains.com/help/idea/spring-boot.html#configure-application-update-policies-with-devtools[instructions in their documentation]. - - - -[[using-boot-devtools-customizing-classload]] -==== Customizing the Restart Classloader -As described earlier in the <> section, restart functionality is implemented by using two classloaders. -For most applications, this approach works well. -However, it can sometimes cause classloading issues. - -By default, any open project in your IDE is loaded with the "`restart`" classloader, and any regular `.jar` file is loaded with the "`base`" classloader. -If you work on a multi-module project, and not every module is imported into your IDE, you may need to customize things. -To do so, you can create a `META-INF/spring-devtools.properties` file. - -The `spring-devtools.properties` file can contain properties prefixed with `restart.exclude` and `restart.include`. -The `include` elements are items that should be pulled up into the "`restart`" classloader, and the `exclude` elements are items that should be pushed down into the "`base`" classloader. -The value of the property is a regex pattern that is applied to the classpath, as shown in the following example: - -[source,properties,indent=0] ----- - restart.exclude.companycommonlibs=/mycorp-common-[\\w\\d-\.]+\.jar - restart.include.projectcommon=/mycorp-myproj-[\\w\\d-\.]+\.jar ----- - -NOTE: All property keys must be unique. -As long as a property starts with `restart.include.` or `restart.exclude.` it is considered. - -TIP: All `META-INF/spring-devtools.properties` from the classpath are loaded. -You can package files inside your project, or in the libraries that the project consumes. - - - -[[using-boot-devtools-known-restart-limitations]] -==== Known Limitations -Restart functionality does not work well with objects that are deserialized by using a standard `ObjectInputStream`. -If you need to deserialize data, you may need to use Spring's `ConfigurableObjectInputStream` in combination with `Thread.currentThread().getContextClassLoader()`. - -Unfortunately, several third-party libraries deserialize without considering the context classloader. -If you find such a problem, you need to request a fix with the original authors. - - - -[[using-boot-devtools-livereload]] -=== LiveReload -The `spring-boot-devtools` module includes an embedded LiveReload server that can be used to trigger a browser refresh when a resource is changed. -LiveReload browser extensions are freely available for Chrome, Firefox and Safari from http://livereload.com/extensions/[livereload.com]. - -If you do not want to start the LiveReload server when your application runs, you can set the configprop:spring.devtools.livereload.enabled[] property to `false`. - -NOTE: You can only run one LiveReload server at a time. -Before starting your application, ensure that no other LiveReload servers are running. -If you start multiple applications from your IDE, only the first has LiveReload support. - - - -[[using-boot-devtools-globalsettings]] -=== Global Settings -You can configure global devtools settings by adding any of the following files to the `$HOME/.config/spring-boot` folder: - -. `spring-boot-devtools.properties` -. `spring-boot-devtools.yaml` -. `spring-boot-devtools.yml` - -Any properties added to these file apply to _all_ Spring Boot applications on your machine that use devtools. -For example, to configure restart to always use a <>, you would add the following property: - -.~/.config/spring-boot/spring-boot-devtools.properties -[source,properties,indent=0,configprops] ----- - spring.devtools.restart.trigger-file=.reloadtrigger ----- - -NOTE: If devtools configuration files are not found in `$HOME/.config/spring-boot`, the root of the `$HOME` folder is searched for the presence of a `.spring-boot-devtools.properties` file. -This allows you to share the devtools global configuration with applications that are on an older version of Spring Boot that does not support the `$HOME/.config/spring-boot` location. - -NOTE: Profiles activated in the above files will not affect the loading of <>. - - - -[[using-boot-devtools-remote]] -=== Remote Applications -The Spring Boot developer tools are not limited to local development. -You can also use several features when running applications remotely. -Remote support is opt-in as enabling it can be a security risk. -It should only be enabled when running on a trusted network or when secured with SSL. -If neither of these options is available to you, you should not use DevTools' remote support. -You should never enable support on a production deployment. - -To enable it, you need to make sure that `devtools` is included in the repackaged archive, as shown in the following listing: - -[source,xml,indent=0,subs="verbatim,quotes,attributes"] ----- - - - - org.springframework.boot - spring-boot-maven-plugin - - false - - - - ----- - -Then you need to set the configprop:spring.devtools.remote.secret[] property. -Like any important password or secret, the value should be unique and strong such that it cannot be guessed or brute-forced. - -Remote devtools support is provided in two parts: a server-side endpoint that accepts connections and a client application that you run in your IDE. -The server component is automatically enabled when the configprop:spring.devtools.remote.secret[] property is set. -The client component must be launched manually. - - - -==== Running the Remote Client Application -The remote client application is designed to be run from within your IDE. -You need to run `org.springframework.boot.devtools.RemoteSpringApplication` with the same classpath as the remote project that you connect to. -The application's single required argument is the remote URL to which it connects. - -For example, if you are using Eclipse or STS and you have a project named `my-app` that you have deployed to Cloud Foundry, you would do the following: - -* Select `Run Configurations...` from the `Run` menu. -* Create a new `Java Application` "`launch configuration`". -* Browse for the `my-app` project. -* Use `org.springframework.boot.devtools.RemoteSpringApplication` as the main class. -* Add `+++https://myapp.cfapps.io+++` to the `Program arguments` (or whatever your remote URL is). - -A running remote client might resemble the following listing: - -[indent=0,subs="attributes"] ----- - . ____ _ __ _ _ - /\\ / ___'_ __ _ _(_)_ __ __ _ ___ _ \ \ \ \ - ( ( )\___ | '_ | '_| | '_ \/ _` | | _ \___ _ __ ___| |_ ___ \ \ \ \ - \\/ ___)| |_)| | | | | || (_| []::::::[] / -_) ' \/ _ \ _/ -_) ) ) ) ) - ' |____| .__|_| |_|_| |_\__, | |_|_\___|_|_|_\___/\__\___|/ / / / - =========|_|==============|___/===================================/_/_/_/ - :: Spring Boot Remote :: {spring-boot-version} - - 2015-06-10 18:25:06.632 INFO 14938 --- [ main] o.s.b.devtools.RemoteSpringApplication : Starting RemoteSpringApplication on pwmbp with PID 14938 (/Users/pwebb/projects/spring-boot/code/spring-boot-devtools/target/classes started by pwebb in /Users/pwebb/projects/spring-boot/code/spring-boot-samples/spring-boot-sample-devtools) - 2015-06-10 18:25:06.671 INFO 14938 --- [ main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@2a17b7b6: startup date [Wed Jun 10 18:25:06 PDT 2015]; root of context hierarchy - 2015-06-10 18:25:07.043 WARN 14938 --- [ main] o.s.b.d.r.c.RemoteClientConfiguration : The connection to http://localhost:8080 is insecure. You should use a URL starting with 'https://'. - 2015-06-10 18:25:07.074 INFO 14938 --- [ main] o.s.b.d.a.OptionalLiveReloadServer : LiveReload server is running on port 35729 - 2015-06-10 18:25:07.130 INFO 14938 --- [ main] o.s.b.devtools.RemoteSpringApplication : Started RemoteSpringApplication in 0.74 seconds (JVM running for 1.105) ----- - -NOTE: Because the remote client is using the same classpath as the real application it can directly read application properties. -This is how the configprop:spring.devtools.remote.secret[] property is read and passed to the server for authentication. - -TIP: It is always advisable to use `https://` as the connection protocol, so that traffic is encrypted and passwords cannot be intercepted. - -TIP: If you need to use a proxy to access the remote application, configure the `spring.devtools.remote.proxy.host` and `spring.devtools.remote.proxy.port` properties. - - - -[[using-boot-devtools-remote-update]] -==== Remote Update -The remote client monitors your application classpath for changes in the same way as the <>. -Any updated resource is pushed to the remote application and (_if required_) triggers a restart. -This can be helpful if you iterate on a feature that uses a cloud service that you do not have locally. -Generally, remote updates and restarts are much quicker than a full rebuild and deploy cycle. - -NOTE: Files are only monitored when the remote client is running. -If you change a file before starting the remote client, it is not pushed to the remote server. - - - -[[configuring-file-system-watcher]] -==== Configuring File System Watcher -{spring-boot-devtools-module-code}/filewatch/FileSystemWatcher.java[FileSystemWatcher] works by polling the class changes with a certain time interval, and then waiting for a predefined quiet period to make sure there are no more changes. -The changes are then uploaded to the remote application. -On a slower development environment, it may happen that the quiet period is not enough, and the changes in the classes may be split into batches. -The server is restarted after the first batch of class changes is uploaded. -The next batch can’t be sent to the application, since the server is restarting. - -This is typically manifested by a warning in the `RemoteSpringApplication` logs about failing to upload some of the classes, and a consequent retry. -But it may also lead to application code inconsistency and failure to restart after the first batch of changes is uploaded. - -If you observe such problems constantly, try increasing the `spring.devtools.restart.poll-interval` and `spring.devtools.restart.quiet-period` parameters to the values that fit your development environment: - -[source,properties,indent=0,configprops] ----- - spring.devtools.restart.poll-interval=2s - spring.devtools.restart.quiet-period=1s ----- - -The monitored classpath folders are now polled every 2 seconds for changes, and a 1 second quiet period is maintained to make sure there are no additional class changes. - - - -[[using-boot-packaging-for-production]] -== Packaging Your Application for Production -Executable jars can be used for production deployment. -As they are self-contained, they are also ideally suited for cloud-based deployment. - -For additional "`production ready`" features, such as health, auditing, and metric REST or JMX end-points, consider adding `spring-boot-actuator`. -See _<>_ for details. - - - -[[using-boot-whats-next]] -== What to Read Next -You should now understand how you can use Spring Boot and some best practices that you should follow. -You can now go on to learn about specific _<>_ in depth, or you could skip ahead and read about the "`<>`" aspects of Spring Boot. diff --git a/spring-boot-project/spring-boot-docs/src/main/groovy/generateAutoConfigurationClassTables.groovy b/spring-boot-project/spring-boot-docs/src/main/groovy/generateAutoConfigurationClassTables.groovy deleted file mode 100644 index 5a94817491fc..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/groovy/generateAutoConfigurationClassTables.groovy +++ /dev/null @@ -1,41 +0,0 @@ -def processModule(File moduleDir, File generatedResourcesDir) { - def moduleName = moduleDir.name - def factoriesFile = new File(moduleDir, 'META-INF/spring.factories') - new File(generatedResourcesDir, "auto-configuration-classes-${moduleName}.adoc") - .withPrintWriter { - generateAutoConfigurationClassTable(moduleName, factoriesFile, it) - } -} - -def generateAutoConfigurationClassTable(String module, File factories, PrintWriter writer) { - writer.println '[cols="4,1"]' - writer.println '|===' - writer.println '| Configuration Class | Links' - - getAutoConfigurationClasses(factories).each { - writer.println '' - writer.println "| {spring-boot-code}/spring-boot-project/${module}/src/main/java/${it.path}.java[`${it.name}`]" - writer.println "| {spring-boot-api}/${it.path}.html[javadoc]" - } - - writer.println '|===' -} - -def getAutoConfigurationClasses(File factories) { - factories.withInputStream { - def properties = new Properties() - properties.load(it) - properties.get('org.springframework.boot.autoconfigure.EnableAutoConfiguration') - .split(',') - .collect { - def path = it.replace('.', '/') - def name = it.substring(it.lastIndexOf('.') + 1) - [ 'path': path, 'name': name] - } - .sort {a, b -> a.name.compareTo(b.name)} - } -} - -def autoConfigDir = new File(project.build.directory, 'auto-config') -def generatedResourcesDir = new File(project.build.directory, 'generated-resources') -autoConfigDir.eachDir { processModule(it, generatedResourcesDir) } diff --git a/spring-boot-project/spring-boot-docs/src/main/groovy/generateConfigurationPropertyTables.groovy b/spring-boot-project/spring-boot-docs/src/main/groovy/generateConfigurationPropertyTables.groovy deleted file mode 100644 index 1b5baa2fdc95..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/groovy/generateConfigurationPropertyTables.groovy +++ /dev/null @@ -1,81 +0,0 @@ -import org.springframework.boot.configurationdocs.ConfigurationMetadataDocumentWriter -import org.springframework.boot.configurationdocs.DocumentOptions -import org.springframework.core.io.UrlResource - -import java.nio.file.Path -import java.nio.file.Paths - -def getConfigMetadataInputStreams() { - def mainMetadata = getClass().getClassLoader().getResources("META-INF/spring-configuration-metadata.json") - def additionalMetadata = getClass().getClassLoader().getResources("META-INF/additional-spring-configuration-metadata.json") - def streams = [] - streams += mainMetadata.collect { new UrlResource(it).getInputStream() } - streams += additionalMetadata.collect { new UrlResource(it).getInputStream() } - return streams -} - -def generateConfigMetadataDocumentation() { - - def streams = getConfigMetadataInputStreams() - try { - Path outputPath = Paths.get(project.build.directory, 'generated-resources', 'config-docs') - def builder = DocumentOptions.builder(); - - builder - .addSection("core") - .withKeyPrefixes("debug", "trace", "logging", "spring.aop", "spring.application", - "spring.autoconfigure", "spring.banner", "spring.beaninfo", "spring.codec", "spring.config", - "spring.info", "spring.jmx", "spring.main", "spring.messages", "spring.pid", - "spring.profiles", "spring.quartz", "spring.reactor", "spring.task", - "spring.mandatory-file-encoding", "info", "spring.output.ansi.enabled") - .addSection("mail") - .withKeyPrefixes("spring.mail", "spring.sendgrid") - .addSection("cache") - .withKeyPrefixes("spring.cache") - .addSection("server") - .withKeyPrefixes("server") - .addSection("web") - .withKeyPrefixes("spring.hateoas", - "spring.http", "spring.servlet", "spring.jersey", - "spring.mvc", "spring.resources", "spring.webflux") - .addSection("json") - .withKeyPrefixes("spring.jackson", "spring.gson") - .addSection("rsocket") - .withKeyPrefixes("spring.rsocket") - .addSection("templating") - .withKeyPrefixes("spring.freemarker", "spring.groovy", "spring.mustache", "spring.thymeleaf") - .addOverride("spring.groovy.template.configuration", "See GroovyMarkupConfigurer") - .addSection("security") - .withKeyPrefixes("spring.security", "spring.ldap", "spring.session") - .addSection("data-migration") - .withKeyPrefixes("spring.flyway", "spring.liquibase") - .addSection("data") - .withKeyPrefixes("spring.couchbase", "spring.elasticsearch", "spring.h2", - "spring.influx", "spring.mongodb", "spring.redis", - "spring.dao", "spring.data", "spring.datasource", "spring.jooq", - "spring.jdbc", "spring.jpa") - .addOverride("spring.datasource.dbcp2", "Commons DBCP2 specific settings") - .addOverride("spring.datasource.tomcat", "Tomcat datasource specific settings") - .addOverride("spring.datasource.hikari", "Hikari specific settings") - .addSection("transaction") - .withKeyPrefixes("spring.jta", "spring.transaction") - .addSection("integration") - .withKeyPrefixes("spring.activemq", "spring.artemis", "spring.batch", - "spring.integration", "spring.jms", "spring.kafka", "spring.rabbitmq", "spring.hazelcast", - "spring.webservices") - .addSection("actuator") - .withKeyPrefixes("management") - .addSection("devtools") - .withKeyPrefixes("spring.devtools") - .addSection("testing") - .withKeyPrefixes("spring.test"); - - ConfigurationMetadataDocumentWriter writer = new ConfigurationMetadataDocumentWriter(); - writer.writeDocument(outputPath, builder.build(), streams.toArray(new InputStream[0])); - } - finally { - streams.each { it.close() } - } -} - -generateConfigMetadataDocumentation() \ No newline at end of file diff --git a/spring-boot-project/spring-boot-docs/src/main/groovy/generateStarterTables.groovy b/spring-boot-project/spring-boot-docs/src/main/groovy/generateStarterTables.groovy deleted file mode 100644 index b1c7d9a4ac75..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/groovy/generateStarterTables.groovy +++ /dev/null @@ -1,76 +0,0 @@ -import groovy.util.XmlSlurper - -def getStarters(File dir) { - def starters = [] - new File(project.build.directory, 'external-resources/starter-poms').eachDir { starterDir -> - def pom = new XmlSlurper().parse(new File(starterDir, 'pom.xml')) - def dependencies = getDependencies(pom) - if (isStarter(dependencies)) { - def name = pom.artifactId.text() - starters << [ - 'name': name, - 'description': postProcessDescription(pom.description.text()), - 'dependencies': dependencies, - 'pomUrl': "{spring-boot-code}/spring-boot-project/spring-boot-starters/$name/pom.xml" - ] - } - } - return starters.sort { it.name } -} - -boolean isApplicationStarter(def starter) { - !isTechnicalStarter(starter) && !isProductionStarter(starter) -} - -boolean isTechnicalStarter(def starter) { - starter.name != 'spring-boot-starter-test' && !isProductionStarter(starter) && - starter.dependencies.find { - it.startsWith('org.springframework.boot:spring-boot-starter') } == null -} - -boolean isProductionStarter(def starter) { - starter.name in ['spring-boot-starter-actuator'] -} - -boolean isStarter(def dependencies) { - !dependencies.empty -} - -def postProcessDescription(String description) { - addStarterCrossLinks(removeExtraWhitespace(description)) -} - -def removeExtraWhitespace(String input) { - input.replaceAll('\\s+', ' ') -} - -def addStarterCrossLinks(String input) { - input.replaceAll('(spring-boot-starter[A-Za-z-]*)', '<<$1,`$1`>>') -} - -def getDependencies(def pom) { - dependencies = [] - pom.dependencies.dependency.each { dependency -> - dependencies << "${dependency.groupId.text()}:${dependency.artifactId.text()}" - } - dependencies -} - -def writeTable(String name, def starters) { - new File(project.build.directory, "generated-resources/${name}.adoc").withPrintWriter { writer -> - writer.println '|===' - writer.println '| Name | Description | Pom' - starters.each { starter -> - writer.println '' - writer.println "| [[${starter.name}]]`${starter.name}`" - writer.println "| ${starter.description}" - writer.println "| ${starter.pomUrl}[Pom]" - } - writer.println '|===' - } -} - -def starters = getStarters(new File(project.build.directory, 'external-resources/starter-poms')) -writeTable('application-starters', starters.findAll { isApplicationStarter(it) }) -writeTable('production-starters', starters.findAll { isProductionStarter(it) }) -writeTable('technical-starters', starters.findAll { isTechnicalStarter(it) }) diff --git a/spring-boot-project/spring-boot-docs/src/main/groovy/generateTestSlicesTable.groovy b/spring-boot-project/spring-boot-docs/src/main/groovy/generateTestSlicesTable.groovy deleted file mode 100644 index 3da1826a0282..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/groovy/generateTestSlicesTable.groovy +++ /dev/null @@ -1,117 +0,0 @@ -import groovy.io.FileType - -import java.util.Properties - -import org.springframework.core.io.InputStreamResource -import org.springframework.core.type.AnnotationMetadata -import org.springframework.core.type.ClassMetadata -import org.springframework.core.type.classreading.MetadataReader -import org.springframework.core.type.classreading.MetadataReaderFactory -import org.springframework.core.type.classreading.SimpleMetadataReaderFactory -import org.springframework.util.ClassUtils -import org.springframework.util.StringUtils - -class Project { - - final List classFiles - - final Properties springFactories - - Project(File rootDirectory) { - this.springFactories = loadSpringFactories(rootDirectory) - this.classFiles = [] - rootDirectory.eachFileRecurse (FileType.FILES) { file -> - if (file.name.endsWith('.class')) { - classFiles << file - } - } - } - - private static Properties loadSpringFactories(File rootDirectory) { - Properties springFactories = new Properties() - new File(rootDirectory, 'META-INF/spring.factories').withInputStream { inputStream -> - springFactories.load(inputStream) - } - return springFactories - } -} - -class TestSlice { - - final String name - - final SortedSet importedAutoConfiguration - - TestSlice(String annotationName, Collection importedAutoConfiguration) { - this.name = ClassUtils.getShortName(annotationName) - this.importedAutoConfiguration = new TreeSet(importedAutoConfiguration) - } -} - -List createTestSlices(Project project) { - MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory() - project.classFiles - .findAll { classFile -> - classFile.name.endsWith('Test.class') - }.collect { classFile -> - createMetadataReader(metadataReaderFactory, classFile) - }.findAll { metadataReader -> - metadataReader.classMetadata.annotation - }.collect { metadataReader -> - createTestSlice(project.springFactories, metadataReader.classMetadata, metadataReader.annotationMetadata) - }.sort { - a, b -> a.name.compareTo b.name - } -} - -MetadataReader createMetadataReader(MetadataReaderFactory factory, File classFile) { - classFile.withInputStream { inputStream -> - factory.getMetadataReader(new InputStreamResource(inputStream)) - } -} - -TestSlice createTestSlice(Properties springFactories, ClassMetadata classMetadata, AnnotationMetadata annotationMetadata) { - new TestSlice(classMetadata.className, getImportedAutoConfiguration(springFactories, annotationMetadata)) -} - -Set getImportedAutoConfiguration(Properties springFactories, AnnotationMetadata annotationMetadata) { - Set importers = findMetaImporters(annotationMetadata) - if (annotationMetadata.isAnnotated('org.springframework.boot.autoconfigure.ImportAutoConfiguration')) { - importers.add(annotationMetadata.className) - } - importers - .collect { autoConfigurationImporter -> - StringUtils.commaDelimitedListToSet(springFactories.get(autoConfigurationImporter)) - }.flatten() -} - -Set findMetaImporters(AnnotationMetadata annotationMetadata) { - annotationMetadata.annotationTypes - .findAll { annotationType -> - isAutoConfigurationImporter(annotationType, annotationMetadata) - } -} - -boolean isAutoConfigurationImporter(String annotationType, AnnotationMetadata metadata) { - metadata.getMetaAnnotationTypes(annotationType).contains('org.springframework.boot.autoconfigure.ImportAutoConfiguration') -} - -void writeTestSlicesTable(List testSlices) { - new File(project.build.directory, "generated-resources/test-slice-auto-configuration.adoc").withPrintWriter { writer -> - writer.println '[cols="d,a"]' - writer.println '|===' - writer.println '| Test slice | Imported auto-configuration' - testSlices.each { testSlice -> - writer.println '' - writer.println "| `@${testSlice.name}`" - writer.print '| ' - testSlice.importedAutoConfiguration.each { - writer.println "`${it}`" - } - } - writer.println '|===' - } -} - -List testSlices = createTestSlices(new Project(new File(project.build.directory, 'test-auto-config'))) -writeTestSlicesTable(testSlices) diff --git a/spring-boot-project/spring-boot-docs/src/main/groovy/org/springframework/boot/docs/cli/usingthecli/run/WebApplication.groovy b/spring-boot-project/spring-boot-docs/src/main/groovy/org/springframework/boot/docs/cli/usingthecli/run/WebApplication.groovy new file mode 100644 index 000000000000..d034a7c062ee --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/groovy/org/springframework/boot/docs/cli/usingthecli/run/WebApplication.groovy @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.cli.usingthecli.run; + +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.RequestMapping + +// tag::code[] +@RestController +class WebApplication { + + @RequestMapping("/") + String home() { + "Hello World!" + } + +} +// end::code[] diff --git a/spring-boot-project/spring-boot-docs/src/main/groovy/org/springframework/boot/docs/cli/usingthecli/run/customdependencymanagement/multiple/CustomDependencyManagement.groovy b/spring-boot-project/spring-boot-docs/src/main/groovy/org/springframework/boot/docs/cli/usingthecli/run/customdependencymanagement/multiple/CustomDependencyManagement.groovy new file mode 100644 index 000000000000..39d19a3c976b --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/groovy/org/springframework/boot/docs/cli/usingthecli/run/customdependencymanagement/multiple/CustomDependencyManagement.groovy @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.cli.usingthecli.run.customdependencymanagement.multiple; + +import org.springframework.boot.groovy.DependencyManagementBom; + +// tag::code[] +@DependencyManagementBom([ + "com.example.custom-bom:1.0.0", + "com.example.another-bom:1.0.0"]) +// end::code[] +class CustomDependencyManagement { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/groovy/org/springframework/boot/docs/cli/usingthecli/run/customdependencymanagement/single/CustomDependencyManagement.groovy b/spring-boot-project/spring-boot-docs/src/main/groovy/org/springframework/boot/docs/cli/usingthecli/run/customdependencymanagement/single/CustomDependencyManagement.groovy new file mode 100644 index 000000000000..8184af333416 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/groovy/org/springframework/boot/docs/cli/usingthecli/run/customdependencymanagement/single/CustomDependencyManagement.groovy @@ -0,0 +1,26 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.cli.usingthecli.run.customdependencymanagement.single; + +import org.springframework.boot.groovy.DependencyManagementBom; + +// tag::code[] +@DependencyManagementBom("com.example.custom-bom:1.0.0") +// end::code[] +class CustomDependencyManagement { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/ExitCodeApplication.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/ExitCodeApplication.java deleted file mode 100644 index 813c6ed09283..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/ExitCodeApplication.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs; - -import org.springframework.boot.ExitCodeGenerator; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; - -/** - * Example configuration that illustrates the use of {@link ExitCodeGenerator}. - * - * @author Stephane Nicoll - */ -// tag::example[] -@SpringBootApplication -public class ExitCodeApplication { - - @Bean - public ExitCodeGenerator exitCodeGenerator() { - return () -> 42; - } - - public static void main(String[] args) { - System.exit(SpringApplication.exit(SpringApplication.run(ExitCodeApplication.class, args))); - } - -} -// end::example[] diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuate/metrics/MetricsFilterBeanExample.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuate/metrics/MetricsFilterBeanExample.java deleted file mode 100644 index 3c7be0c394b8..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuate/metrics/MetricsFilterBeanExample.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.actuate.metrics; - -import io.micrometer.core.instrument.config.MeterFilter; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * Example to show a {@link MeterFilter}. - * - * @author Phillip Webb - */ -public class MetricsFilterBeanExample { - - @Configuration(proxyBeanMethods = false) - public static class MetricsFilterExampleConfiguration { - - // tag::configuration[] - @Bean - public MeterFilter renameRegionTagMeterFilter() { - return MeterFilter.renameTag("com.example", "mytag.region", "mytag.area"); - } - // end::configuration[] - - } - -} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuate/metrics/MetricsMeterRegistryInjectionExample.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuate/metrics/MetricsMeterRegistryInjectionExample.java deleted file mode 100644 index efb36a2e415c..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuate/metrics/MetricsMeterRegistryInjectionExample.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.actuate.metrics; - -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; - -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Tags; - -/** - * Example to show injection and use of a {@link MeterRegistry}. - * - * @author Andy Wilkinson - */ -public class MetricsMeterRegistryInjectionExample { - - // tag::component[] - class Dictionary { - - private final List words = new CopyOnWriteArrayList<>(); - - Dictionary(MeterRegistry registry) { - registry.gaugeCollectionSize("dictionary.size", Tags.empty(), this.words); - } - - // … - - } - // end::component[] - -} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuate/metrics/SampleBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuate/metrics/SampleBean.java deleted file mode 100644 index 03ab56d0a513..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuate/metrics/SampleBean.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.actuate.metrics; - -import io.micrometer.core.instrument.Counter; -import io.micrometer.core.instrument.MeterRegistry; - -import org.springframework.stereotype.Component; - -/** - * Example to show manual usage of {@link MeterRegistry}. - * - * @author Stephane Nicoll - */ -// tag::example[] -@Component -public class SampleBean { - - private final Counter counter; - - public SampleBean(MeterRegistry registry) { - this.counter = registry.counter("received.messages"); - } - - public void handleMessage(String message) { - this.counter.increment(); - // handle message implementation - } - -} -// end::example[] diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/cloudfoundry/customcontextpath/MyCloudFoundryConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/cloudfoundry/customcontextpath/MyCloudFoundryConfiguration.java new file mode 100644 index 000000000000..3712cbc09add --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/cloudfoundry/customcontextpath/MyCloudFoundryConfiguration.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.actuator.cloudfoundry.customcontextpath; + +import java.io.IOException; +import java.util.Collections; + +import javax.servlet.GenericServlet; +import javax.servlet.Servlet; +import javax.servlet.ServletContainerInitializer; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.apache.catalina.Host; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.startup.Tomcat; + +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.boot.web.servlet.ServletContextInitializer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class MyCloudFoundryConfiguration { + + @Bean + public TomcatServletWebServerFactory servletWebServerFactory() { + return new TomcatServletWebServerFactory() { + + @Override + protected void prepareContext(Host host, ServletContextInitializer[] initializers) { + super.prepareContext(host, initializers); + StandardContext child = new StandardContext(); + child.addLifecycleListener(new Tomcat.FixContextListener()); + child.setPath("/cloudfoundryapplication"); + ServletContainerInitializer initializer = getServletContextInitializer(getContextPath()); + child.addServletContainerInitializer(initializer, Collections.emptySet()); + child.setCrossContext(true); + host.addChild(child); + } + + }; + } + + private ServletContainerInitializer getServletContextInitializer(String contextPath) { + return (classes, context) -> { + Servlet servlet = new GenericServlet() { + + @Override + public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { + ServletContext context = req.getServletContext().getContext(contextPath); + context.getRequestDispatcher("/cloudfoundryapplication").forward(req, res); + } + + }; + context.addServlet("cloudfoundry", servlet).addMapping("/*"); + }; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/endpoints/health/reactivehealthindicators/MyReactiveHealthIndicator.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/endpoints/health/reactivehealthindicators/MyReactiveHealthIndicator.java new file mode 100644 index 000000000000..e915e079d445 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/endpoints/health/reactivehealthindicators/MyReactiveHealthIndicator.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.actuator.endpoints.health.reactivehealthindicators; + +import reactor.core.publisher.Mono; + +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.ReactiveHealthIndicator; +import org.springframework.stereotype.Component; + +@Component +public class MyReactiveHealthIndicator implements ReactiveHealthIndicator { + + @Override + public Mono health() { + // @formatter:off + return doHealthCheck().onErrorResume((exception) -> + Mono.just(new Health.Builder().down(exception).build())); + // @formatter:on + } + + private Mono doHealthCheck() { + // perform some specific health check + return /**/ null; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/endpoints/health/writingcustomhealthindicators/MyHealthIndicator.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/endpoints/health/writingcustomhealthindicators/MyHealthIndicator.java new file mode 100644 index 000000000000..492a3013c69c --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/endpoints/health/writingcustomhealthindicators/MyHealthIndicator.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.actuator.endpoints.health.writingcustomhealthindicators; + +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.stereotype.Component; + +@Component +public class MyHealthIndicator implements HealthIndicator { + + @Override + public Health health() { + int errorCode = check(); + if (errorCode != 0) { + return Health.down().withDetail("Error Code", errorCode).build(); + } + return Health.up().build(); + } + + private int check() { + // perform some specific health check + return /**/ 0; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/endpoints/implementingcustom/CustomData.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/endpoints/implementingcustom/CustomData.java new file mode 100644 index 000000000000..89e2683ffad7 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/endpoints/implementingcustom/CustomData.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.actuator.endpoints.implementingcustom; + +class CustomData { + + private final String name; + + private final int counter; + + CustomData(String name, int counter) { + this.name = name; + this.counter = counter; + } + + String getName() { + return this.name; + } + + int getCounter() { + return this.counter; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/endpoints/implementingcustom/MyEndpoint.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/endpoints/implementingcustom/MyEndpoint.java new file mode 100644 index 000000000000..a1b9babab217 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/endpoints/implementingcustom/MyEndpoint.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.actuator.endpoints.implementingcustom; + +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.annotation.WriteOperation; + +@Endpoint(id = "custom") +public class MyEndpoint { + + // tag::read[] + @ReadOperation + public CustomData getData() { + return new CustomData("test", 5); + } + // end::read[] + + // tag::write[] + @WriteOperation + public void updateData(String name, int counter) { + // injects "test" and 42 + } + // end::write[] + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/endpoints/info/writingcustominfocontributors/MyInfoContributor.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/endpoints/info/writingcustominfocontributors/MyInfoContributor.java new file mode 100644 index 000000000000..d7449af66718 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/endpoints/info/writingcustominfocontributors/MyInfoContributor.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.actuator.endpoints.info.writingcustominfocontributors; + +import java.util.Collections; + +import org.springframework.boot.actuate.info.Info; +import org.springframework.boot.actuate.info.InfoContributor; +import org.springframework.stereotype.Component; + +@Component +public class MyInfoContributor implements InfoContributor { + + @Override + public void contribute(Info.Builder builder) { + builder.withDetail("example", Collections.singletonMap("key", "value")); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/endpoints/security/exposeall/MySecurityConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/endpoints/security/exposeall/MySecurityConfiguration.java new file mode 100644 index 000000000000..ba372dd74c6d --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/endpoints/security/exposeall/MySecurityConfiguration.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.actuator.endpoints.security.exposeall; + +import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration(proxyBeanMethods = false) +public class MySecurityConfiguration { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http.requestMatcher(EndpointRequest.toAnyEndpoint()) + .authorizeRequests((requests) -> requests.anyRequest().permitAll()); + return http.build(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/endpoints/security/typical/MySecurityConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/endpoints/security/typical/MySecurityConfiguration.java new file mode 100644 index 000000000000..0a963a0a6f86 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/endpoints/security/typical/MySecurityConfiguration.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.actuator.endpoints.security.typical; + +import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration(proxyBeanMethods = false) +public class MySecurityConfiguration { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http.requestMatcher(EndpointRequest.toAnyEndpoint()) + .authorizeRequests((requests) -> requests.anyRequest().hasRole("ENDPOINT_ADMIN")); + http.httpBasic(); + return http.build(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/customizing/MyMetricsFilterConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/customizing/MyMetricsFilterConfiguration.java new file mode 100644 index 000000000000..281b213e1705 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/customizing/MyMetricsFilterConfiguration.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.actuator.metrics.customizing; + +import io.micrometer.core.instrument.config.MeterFilter; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class MyMetricsFilterConfiguration { + + @Bean + public MeterFilter renameRegionTagMeterFilter() { + return MeterFilter.renameTag("com.example", "mytag.region", "mytag.area"); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/export/graphite/MyGraphiteConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/export/graphite/MyGraphiteConfiguration.java new file mode 100644 index 000000000000..19cabff85707 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/export/graphite/MyGraphiteConfiguration.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.actuator.metrics.export.graphite; + +import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.config.NamingConvention; +import io.micrometer.core.instrument.util.HierarchicalNameMapper; +import io.micrometer.graphite.GraphiteConfig; +import io.micrometer.graphite.GraphiteMeterRegistry; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class MyGraphiteConfiguration { + + @Bean + public GraphiteMeterRegistry graphiteMeterRegistry(GraphiteConfig config, Clock clock) { + return new GraphiteMeterRegistry(config, clock, this::toHierarchicalName); + } + + private String toHierarchicalName(Meter.Id id, NamingConvention convention) { + return /**/ HierarchicalNameMapper.DEFAULT.toHierarchicalName(id, convention); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/export/jmx/MyJmxConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/export/jmx/MyJmxConfiguration.java new file mode 100644 index 000000000000..06fefff59fde --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/export/jmx/MyJmxConfiguration.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.actuator.metrics.export.jmx; + +import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.config.NamingConvention; +import io.micrometer.core.instrument.util.HierarchicalNameMapper; +import io.micrometer.jmx.JmxConfig; +import io.micrometer.jmx.JmxMeterRegistry; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class MyJmxConfiguration { + + @Bean + public JmxMeterRegistry jmxMeterRegistry(JmxConfig config, Clock clock) { + return new JmxMeterRegistry(config, clock, this::toHierarchicalName); + } + + private String toHierarchicalName(Meter.Id id, NamingConvention convention) { + return /**/ HierarchicalNameMapper.DEFAULT.toHierarchicalName(id, convention); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/gettingstarted/commontags/MyMeterRegistryConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/gettingstarted/commontags/MyMeterRegistryConfiguration.java new file mode 100644 index 000000000000..d86b46a66443 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/gettingstarted/commontags/MyMeterRegistryConfiguration.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.actuator.metrics.gettingstarted.commontags; + +import io.micrometer.core.instrument.MeterRegistry; + +import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class MyMeterRegistryConfiguration { + + @Bean + public MeterRegistryCustomizer metricsCommonTags() { + return (registry) -> registry.config().commonTags("region", "us-east-1"); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/gettingstarted/specifictype/MyMeterRegistryConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/gettingstarted/specifictype/MyMeterRegistryConfiguration.java new file mode 100644 index 000000000000..3e32dfab23c3 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/gettingstarted/specifictype/MyMeterRegistryConfiguration.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.actuator.metrics.gettingstarted.specifictype; + +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.config.NamingConvention; +import io.micrometer.graphite.GraphiteMeterRegistry; + +import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class MyMeterRegistryConfiguration { + + @Bean + public MeterRegistryCustomizer graphiteMetricsNamingConvention() { + return (registry) -> registry.config().namingConvention(this::name); + } + + private String name(String name, Meter.Type type, String baseUnit) { + return /**/ NamingConvention.snakeCase.name(name, type, baseUnit); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/registeringcustom/Dictionary.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/registeringcustom/Dictionary.java new file mode 100644 index 000000000000..13d22726a822 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/registeringcustom/Dictionary.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.actuator.metrics.registeringcustom; + +import java.util.Collections; +import java.util.List; + +class Dictionary { + + static Dictionary load() { + return new Dictionary(); + } + + List getWords() { + return Collections.emptyList(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/registeringcustom/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/registeringcustom/MyBean.java new file mode 100644 index 000000000000..ec5bf8bc1303 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/registeringcustom/MyBean.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.actuator.metrics.registeringcustom; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; + +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + private final Dictionary dictionary; + + public MyBean(MeterRegistry registry) { + this.dictionary = Dictionary.load(); + registry.gauge("dictionary.size", Tags.empty(), this.dictionary.getWords().size()); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/registeringcustom/MyMeterBinderConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/registeringcustom/MyMeterBinderConfiguration.java new file mode 100644 index 000000000000..37d191005d8e --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/registeringcustom/MyMeterBinderConfiguration.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.actuator.metrics.registeringcustom; + +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.binder.MeterBinder; + +import org.springframework.context.annotation.Bean; + +public class MyMeterBinderConfiguration { + + @Bean + public MeterBinder queueSize(Queue queue) { + return (registry) -> Gauge.builder("queueSize", queue::size).register(registry); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/registeringcustom/Queue.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/registeringcustom/Queue.java new file mode 100644 index 000000000000..3c2001ebbd06 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/registeringcustom/Queue.java @@ -0,0 +1,25 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.actuator.metrics.registeringcustom; + +class Queue { + + int size() { + return 5; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/mongodb/command/CustomCommandTagsProvider.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/mongodb/command/CustomCommandTagsProvider.java new file mode 100644 index 000000000000..587446e18224 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/mongodb/command/CustomCommandTagsProvider.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.actuator.metrics.supported.mongodb.command; + +import com.mongodb.event.CommandEvent; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.binder.mongodb.MongoCommandTagsProvider; + +class CustomCommandTagsProvider implements MongoCommandTagsProvider { + + @Override + public Iterable commandTags(CommandEvent commandEvent) { + return java.util.Collections.emptyList(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/mongodb/command/MyCommandTagsProviderConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/mongodb/command/MyCommandTagsProviderConfiguration.java new file mode 100644 index 000000000000..066a9ebd0de4 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/mongodb/command/MyCommandTagsProviderConfiguration.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.actuator.metrics.supported.mongodb.command; + +import io.micrometer.core.instrument.binder.mongodb.MongoCommandTagsProvider; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class MyCommandTagsProviderConfiguration { + + @Bean + public MongoCommandTagsProvider customCommandTagsProvider() { + return new CustomCommandTagsProvider(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/mongodb/connectionpool/CustomConnectionPoolTagsProvider.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/mongodb/connectionpool/CustomConnectionPoolTagsProvider.java new file mode 100644 index 000000000000..ece9565a5a39 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/mongodb/connectionpool/CustomConnectionPoolTagsProvider.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.actuator.metrics.supported.mongodb.connectionpool; + +import com.mongodb.event.ConnectionPoolCreatedEvent; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.binder.mongodb.MongoConnectionPoolTagsProvider; + +public class CustomConnectionPoolTagsProvider implements MongoConnectionPoolTagsProvider { + + @Override + public Iterable connectionPoolTags(ConnectionPoolCreatedEvent event) { + return java.util.Collections.emptyList(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/mongodb/connectionpool/MyConnectionPoolTagsProviderConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/mongodb/connectionpool/MyConnectionPoolTagsProviderConfiguration.java new file mode 100644 index 000000000000..72bcadf50792 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/mongodb/connectionpool/MyConnectionPoolTagsProviderConfiguration.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.actuator.metrics.supported.mongodb.connectionpool; + +import io.micrometer.core.instrument.binder.mongodb.MongoConnectionPoolTagsProvider; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class MyConnectionPoolTagsProviderConfiguration { + + @Bean + public MongoConnectionPoolTagsProvider customConnectionPoolTagsProvider() { + return new CustomConnectionPoolTagsProvider(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/timedannotation/all/Address.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/timedannotation/all/Address.java new file mode 100644 index 000000000000..7b074590c1c4 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/timedannotation/all/Address.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.actuator.metrics.supported.timedannotation.all; + +class Address { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/timedannotation/all/MyController.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/timedannotation/all/MyController.java new file mode 100644 index 000000000000..772a17304728 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/timedannotation/all/MyController.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.actuator.metrics.supported.timedannotation.all; + +import java.util.List; + +import io.micrometer.core.annotation.Timed; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@Timed +public class MyController { + + @GetMapping("/api/addresses") + public List
    listAddress() { + return /**/ null; + } + + @GetMapping("/api/people") + public List listPeople() { + return /**/ null; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/timedannotation/all/Person.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/timedannotation/all/Person.java new file mode 100644 index 000000000000..ecdeef450926 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/timedannotation/all/Person.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.actuator.metrics.supported.timedannotation.all; + +class Person { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/timedannotation/change/Address.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/timedannotation/change/Address.java new file mode 100644 index 000000000000..818cd770dc64 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/timedannotation/change/Address.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.actuator.metrics.supported.timedannotation.change; + +class Address { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/timedannotation/change/MyController.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/timedannotation/change/MyController.java new file mode 100644 index 000000000000..a9579e4b711e --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/timedannotation/change/MyController.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.actuator.metrics.supported.timedannotation.change; + +import java.util.List; + +import io.micrometer.core.annotation.Timed; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@Timed +public class MyController { + + @GetMapping("/api/addresses") + public List
    listAddress() { + return /**/ null; + } + + @GetMapping("/api/people") + @Timed(extraTags = { "region", "us-east-1" }) + @Timed(value = "all.people", longTask = true) + public List listPeople() { + return /**/ null; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/timedannotation/change/Person.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/timedannotation/change/Person.java new file mode 100644 index 000000000000..e954534481c6 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/timedannotation/change/Person.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.actuator.metrics.supported.timedannotation.change; + +class Person { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/timedannotation/single/Address.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/timedannotation/single/Address.java new file mode 100644 index 000000000000..742794195916 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/timedannotation/single/Address.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.actuator.metrics.supported.timedannotation.single; + +class Address { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/timedannotation/single/MyController.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/timedannotation/single/MyController.java new file mode 100644 index 000000000000..f5822c2b2617 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/timedannotation/single/MyController.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.actuator.metrics.supported.timedannotation.single; + +import java.util.List; + +import io.micrometer.core.annotation.Timed; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class MyController { + + @GetMapping("/api/addresses") + public List
    listAddress() { + return /**/ null; + } + + @GetMapping("/api/people") + @Timed + public List listPeople() { + return /**/ null; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/timedannotation/single/Person.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/timedannotation/single/Person.java new file mode 100644 index 000000000000..9eb03399a0fc --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/metrics/supported/timedannotation/single/Person.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.actuator.metrics.supported.timedannotation.single; + +class Person { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/autoconfigure/UserService.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/autoconfigure/UserService.java deleted file mode 100644 index 4b089ca29ba8..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/autoconfigure/UserService.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.autoconfigure; - -/** - * Sample service. - * - * @author Stephane Nicoll - */ -public class UserService { - - private final String name; - - public UserService(String name) { - this.name = name; - } - - public String getName() { - return this.name; - } - -} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/autoconfigure/UserServiceAutoConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/autoconfigure/UserServiceAutoConfiguration.java deleted file mode 100644 index 7566b78ad5c2..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/autoconfigure/UserServiceAutoConfiguration.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.autoconfigure; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.docs.autoconfigure.UserServiceAutoConfiguration.UserProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * Sample auto-configuration. - * - * @author Stephane Nicoll - */ -@Configuration(proxyBeanMethods = false) -@ConditionalOnClass(UserService.class) -@EnableConfigurationProperties(UserProperties.class) -public class UserServiceAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - public UserService userService(UserProperties properties) { - return new UserService(properties.getName()); - } - - @ConfigurationProperties("user") - public static class UserProperties { - - private String name = "test"; - - public String getName() { - return this.name; - } - - public void setName(String name) { - this.name = name; - } - - } - -} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/builder/SpringApplicationBuilderExample.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/builder/SpringApplicationBuilderExample.java deleted file mode 100644 index d1944fff70bb..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/builder/SpringApplicationBuilderExample.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.builder; - -import org.springframework.boot.Banner; -import org.springframework.boot.builder.SpringApplicationBuilder; - -/** - * Examples of using {@link SpringApplicationBuilder}. - * - * @author Andy Wilkinson - */ -public class SpringApplicationBuilderExample { - - public void hierarchyWithDisabledBanner(String[] args) { - // @formatter:off - // tag::hierarchy[] - new SpringApplicationBuilder() - .sources(Parent.class) - .child(Application.class) - .bannerMode(Banner.Mode.OFF) - .run(args); - // end::hierarchy[] - // @formatter:on - } - - /** - * Parent application configuration. - */ - static class Parent { - - } - - /** - * Application configuration. - */ - static class Application { - - } - -} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/buildtoolplugins/otherbuildsystems/examplerepackageimplementation/MyBuildTool.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/buildtoolplugins/otherbuildsystems/examplerepackageimplementation/MyBuildTool.java new file mode 100644 index 000000000000..311a5ed9339f --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/buildtoolplugins/otherbuildsystems/examplerepackageimplementation/MyBuildTool.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.buildtoolplugins.otherbuildsystems.examplerepackageimplementation; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +import org.springframework.boot.loader.tools.Library; +import org.springframework.boot.loader.tools.LibraryCallback; +import org.springframework.boot.loader.tools.LibraryScope; +import org.springframework.boot.loader.tools.Repackager; + +public class MyBuildTool { + + public void build() throws IOException { + File sourceJarFile = /**/ null; + Repackager repackager = new Repackager(sourceJarFile); + repackager.setBackupSource(false); + repackager.repackage(this::getLibraries); + } + + private void getLibraries(LibraryCallback callback) throws IOException { + // Build system specific implementation, callback for each dependency + for (File nestedJar : getCompileScopeJars()) { + callback.library(new Library(nestedJar, LibraryScope.COMPILE)); + } + // ... + } + + private List getCompileScopeJars() { + return /**/ null; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/cloudfoundry/CloudFoundryCustomContextPathExample.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/cloudfoundry/CloudFoundryCustomContextPathExample.java deleted file mode 100644 index d1cbc15d5e45..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/cloudfoundry/CloudFoundryCustomContextPathExample.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.cloudfoundry; - -import java.io.IOException; -import java.util.Collections; - -import javax.servlet.GenericServlet; -import javax.servlet.Servlet; -import javax.servlet.ServletContainerInitializer; -import javax.servlet.ServletContext; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; - -import org.apache.catalina.Host; -import org.apache.catalina.core.StandardContext; -import org.apache.catalina.startup.Tomcat; - -import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; -import org.springframework.boot.web.servlet.ServletContextInitializer; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * Example configuration for custom context path in Cloud Foundry. - * - * @author Johnny Lim - */ -@Configuration(proxyBeanMethods = false) -public class CloudFoundryCustomContextPathExample { - - // tag::configuration[] - @Bean - public TomcatServletWebServerFactory servletWebServerFactory() { - return new TomcatServletWebServerFactory() { - - @Override - protected void prepareContext(Host host, ServletContextInitializer[] initializers) { - super.prepareContext(host, initializers); - StandardContext child = new StandardContext(); - child.addLifecycleListener(new Tomcat.FixContextListener()); - child.setPath("/cloudfoundryapplication"); - ServletContainerInitializer initializer = getServletContextInitializer(getContextPath()); - child.addServletContainerInitializer(initializer, Collections.emptySet()); - child.setCrossContext(true); - host.addChild(child); - } - - }; - } - - private ServletContainerInitializer getServletContextInitializer(String contextPath) { - return (c, context) -> { - Servlet servlet = new GenericServlet() { - - @Override - public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { - ServletContext context = req.getServletContext().getContext(contextPath); - context.getRequestDispatcher("/cloudfoundryapplication").forward(req, res); - } - - }; - context.addServlet("cloudfoundry", servlet).addMapping("/*"); - }; - } - // end::configuration[] - -} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/configurationmetadata/annotationprocessor/automaticmetadatageneration/MyMessagingProperties.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/configurationmetadata/annotationprocessor/automaticmetadatageneration/MyMessagingProperties.java new file mode 100644 index 000000000000..672f1e831077 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/configurationmetadata/annotationprocessor/automaticmetadatageneration/MyMessagingProperties.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.configurationmetadata.annotationprocessor.automaticmetadatageneration; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "my.messaging") +public class MyMessagingProperties { + + private List addresses = new ArrayList<>(Arrays.asList("a", "b")); + + private ContainerType containerType = ContainerType.SIMPLE; + + // @fold:on // getters/setters ... + public List getAddresses() { + return this.addresses; + } + + public void setAddresses(List addresses) { + this.addresses = addresses; + } + + public ContainerType getContainerType() { + return this.containerType; + } + + public void setContainerType(ContainerType containerType) { + this.containerType = containerType; + } + // @fold:off + + public enum ContainerType { + + SIMPLE, DIRECT + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/configurationmetadata/annotationprocessor/automaticmetadatageneration/MyServerProperties.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/configurationmetadata/annotationprocessor/automaticmetadatageneration/MyServerProperties.java new file mode 100644 index 000000000000..e5193aa70e06 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/configurationmetadata/annotationprocessor/automaticmetadatageneration/MyServerProperties.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.configurationmetadata.annotationprocessor.automaticmetadatageneration; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "my.server") +public class MyServerProperties { + + /** + * Name of the server. + */ + private String name; + + /** + * IP address to listen to. + */ + private String ip = "127.0.0.1"; + + /** + * Port to listener to. + */ + private int port = 9797; + + // @fold:on // getters/setters ... + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public String getIp() { + return this.ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public int getPort() { + return this.port; + } + + public void setPort(int port) { + this.port = port; + } + // fold:off + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/configurationmetadata/annotationprocessor/automaticmetadatageneration/nestedproperties/MyServerProperties.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/configurationmetadata/annotationprocessor/automaticmetadatageneration/nestedproperties/MyServerProperties.java new file mode 100644 index 000000000000..07f328bd8f20 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/configurationmetadata/annotationprocessor/automaticmetadatageneration/nestedproperties/MyServerProperties.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.configurationmetadata.annotationprocessor.automaticmetadatageneration.nestedproperties; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "my.server") +public class MyServerProperties { + + private String name; + + private Host host; + + // @fold:on // getters/setters ... + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public Host getHost() { + return this.host; + } + + public void setHost(Host host) { + this.host = host; + } + // @fold:off + + public static class Host { + + private String ip; + + private int port; + + // @fold:on // getters/setters ... + public String getIp() { + return this.ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public int getPort() { + return this.port; + } + + public void setPort(int port) { + this.port = port; + } + // @fold:off // getters/setters ... + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/configurationmetadata/format/group/MyProperties.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/configurationmetadata/format/group/MyProperties.java new file mode 100644 index 000000000000..100d19bb6368 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/configurationmetadata/format/group/MyProperties.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.configurationmetadata.format.group; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; + +@ConfigurationProperties("my.app") +public class MyProperties { + + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @Deprecated + @DeprecatedConfigurationProperty(replacement = "my.app.name") + public String getTarget() { + return this.name; + } + + @Deprecated + public void setTarget(String target) { + this.name = target; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/configurationmetadata/manualhints/valuehint/MyProperties.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/configurationmetadata/manualhints/valuehint/MyProperties.java new file mode 100644 index 000000000000..6c7bbad1261d --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/configurationmetadata/manualhints/valuehint/MyProperties.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.configurationmetadata.manualhints.valuehint; + +import java.util.Map; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("my") +public class MyProperties { + + private Map contexts; + + // @fold:on // getters/setters ... + public Map getContexts() { + return this.contexts; + } + + public void setContexts(Map contexts) { + this.contexts = contexts; + } + // @fold:off + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/context/EnvironmentPostProcessorExample.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/context/EnvironmentPostProcessorExample.java deleted file mode 100644 index 2bfef078e9e8..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/context/EnvironmentPostProcessorExample.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.context; - -import java.io.IOException; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.env.EnvironmentPostProcessor; -import org.springframework.boot.env.YamlPropertySourceLoader; -import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.PropertySource; -import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.Resource; - -/** - * An {@link EnvironmentPostProcessor} example that loads a YAML file. - * - * @author Stephane Nicoll - */ -// tag::example[] -public class EnvironmentPostProcessorExample implements EnvironmentPostProcessor { - - private final YamlPropertySourceLoader loader = new YamlPropertySourceLoader(); - - @Override - public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { - Resource path = new ClassPathResource("com/example/myapp/config.yml"); - PropertySource propertySource = loadYaml(path); - environment.getPropertySources().addLast(propertySource); - } - - private PropertySource loadYaml(Resource path) { - if (!path.exists()) { - throw new IllegalArgumentException("Resource " + path + " does not exist"); - } - try { - return this.loader.load("custom-resource", path).get(0); - } - catch (IOException ex) { - throw new IllegalStateException("Failed to load yaml configuration from " + path, ex); - } - } - -} -// end::example[] diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/context/embedded/TomcatLegacyCookieProcessorExample.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/context/embedded/TomcatLegacyCookieProcessorExample.java deleted file mode 100644 index f2bb5d298aa6..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/context/embedded/TomcatLegacyCookieProcessorExample.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.context.embedded; - -import org.apache.tomcat.util.http.LegacyCookieProcessor; - -import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; -import org.springframework.boot.web.server.WebServerFactoryCustomizer; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * Example configuration for configuring Tomcat with to use {@link LegacyCookieProcessor}. - * - * @author Andy Wilkinson - */ -public class TomcatLegacyCookieProcessorExample { - - /** - * Configuration class that declares the required {@link WebServerFactoryCustomizer}. - */ - @Configuration(proxyBeanMethods = false) - public static class LegacyCookieProcessorConfiguration { - - // tag::customizer[] - @Bean - public WebServerFactoryCustomizer cookieProcessorCustomizer() { - return (factory) -> factory - .addContextCustomizers((context) -> context.setCookieProcessor(new LegacyCookieProcessor())); - } - // end::customizer[] - - } - -} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/context/properties/bind/AppIoProperties.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/context/properties/bind/AppIoProperties.java deleted file mode 100644 index 4a89b76034eb..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/context/properties/bind/AppIoProperties.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.context.properties.bind; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.convert.DataSizeUnit; -import org.springframework.util.unit.DataSize; -import org.springframework.util.unit.DataUnit; - -/** - * A {@link ConfigurationProperties @ConfigurationProperties} example that uses - * {@link DataSize}. - * - * @author Stephane Nicoll - */ -// tag::example[] -@ConfigurationProperties("app.io") -public class AppIoProperties { - - @DataSizeUnit(DataUnit.MEGABYTES) - private DataSize bufferSize = DataSize.ofMegabytes(2); - - private DataSize sizeThreshold = DataSize.ofBytes(512); - - public DataSize getBufferSize() { - return this.bufferSize; - } - - public void setBufferSize(DataSize bufferSize) { - this.bufferSize = bufferSize; - } - - public DataSize getSizeThreshold() { - return this.sizeThreshold; - } - - public void setSizeThreshold(DataSize sizeThreshold) { - this.sizeThreshold = sizeThreshold; - } - -} -// end::example[] diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/context/properties/bind/AppSystemProperties.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/context/properties/bind/AppSystemProperties.java deleted file mode 100644 index 230d267033a4..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/context/properties/bind/AppSystemProperties.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.context.properties.bind; - -import java.time.Duration; -import java.time.temporal.ChronoUnit; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.convert.DurationUnit; - -/** - * A {@link ConfigurationProperties @ConfigurationProperties} example that uses - * {@link Duration}. - * - * @author Stephane Nicoll - */ -// tag::example[] -@ConfigurationProperties("app.system") -public class AppSystemProperties { - - @DurationUnit(ChronoUnit.SECONDS) - private Duration sessionTimeout = Duration.ofSeconds(30); - - private Duration readTimeout = Duration.ofMillis(1000); - - public Duration getSessionTimeout() { - return this.sessionTimeout; - } - - public void setSessionTimeout(Duration sessionTimeout) { - this.sessionTimeout = sessionTimeout; - } - - public Duration getReadTimeout() { - return this.readTimeout; - } - - public void setReadTimeout(Duration readTimeout) { - this.readTimeout = readTimeout; - } - -} -// end::example[] diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/deployment/cloud/cloudfoundry/bindingtoservices/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/deployment/cloud/cloudfoundry/bindingtoservices/MyBean.java new file mode 100644 index 000000000000..cf625dc4d92d --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/deployment/cloud/cloudfoundry/bindingtoservices/MyBean.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.deployment.cloud.cloudfoundry.bindingtoservices; + +import org.springframework.context.EnvironmentAware; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +@Component +public class MyBean implements EnvironmentAware { + + @SuppressWarnings("unused") + private String instanceId; + + @Override + public void setEnvironment(Environment environment) { + this.instanceId = environment.getProperty("vcap.application.instance_id"); + } + + // ... + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/devtools/restart/disable/MyApplication.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/devtools/restart/disable/MyApplication.java new file mode 100644 index 000000000000..44bc253f1a58 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/devtools/restart/disable/MyApplication.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.devtools.restart.disable; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class MyApplication { + + public static void main(String[] args) { + System.setProperty("spring.devtools.restart.enabled", "false"); + SpringApplication.run(MyApplication.class, args); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/elasticsearch/HibernateSearchElasticsearchExample.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/elasticsearch/HibernateSearchElasticsearchExample.java deleted file mode 100644 index 02a6922756f8..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/elasticsearch/HibernateSearchElasticsearchExample.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.elasticsearch; - -import javax.persistence.EntityManagerFactory; - -import org.springframework.boot.autoconfigure.data.jpa.EntityManagerFactoryDependsOnPostProcessor; -import org.springframework.stereotype.Component; - -/** - * Example configuration for configuring Hibernate to depend on Elasticsearch so that - * Hibernate Search can use Elasticsearch as its index manager. - * - * @author Andy Wilkinson - */ -public class HibernateSearchElasticsearchExample { - - // tag::configuration[] - /** - * {@link EntityManagerFactoryDependsOnPostProcessor} that ensures that - * {@link EntityManagerFactory} beans depend on the {@code elasticsearchClient} bean. - */ - @Component - static class ElasticsearchEntityManagerFactoryDependsOnPostProcessor - extends EntityManagerFactoryDependsOnPostProcessor { - - ElasticsearchEntityManagerFactoryDependsOnPostProcessor() { - super("elasticsearchClient"); - } - - } - // end::configuration[] - -} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/elasticsearch/jest/JestClientCustomizationExample.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/elasticsearch/jest/JestClientCustomizationExample.java deleted file mode 100644 index 764bfba40b46..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/elasticsearch/jest/JestClientCustomizationExample.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.elasticsearch.jest; - -import io.searchbox.client.config.HttpClientConfig; - -import org.springframework.boot.autoconfigure.elasticsearch.jest.HttpClientConfigBuilderCustomizer; - -/** - * Example configuration for using a {@link HttpClientConfigBuilderCustomizer} to - * configure additional HTTP settings. - * - * @author Stephane Nicoll - */ -public class JestClientCustomizationExample { - - /** - * A {@link HttpClientConfigBuilderCustomizer} that applies additional HTTP settings - * to the auto-configured jest client. - */ - // tag::customizer[] - static class HttpSettingsCustomizer implements HttpClientConfigBuilderCustomizer { - - @Override - public void customize(HttpClientConfig.Builder builder) { - builder.maxTotalConnection(100).defaultMaxTotalConnectionPerRoute(5); - } - - } - // end::customizer[] - -} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/caching/MyMathService.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/caching/MyMathService.java new file mode 100644 index 000000000000..1d1eb82f492f --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/caching/MyMathService.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.caching; + +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Component; + +@Component +public class MyMathService { + + @Cacheable("piDecimals") + public int computePiDecimal(int precision) { + /**/ return 0; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/caching/provider/MyCacheManagerConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/caching/provider/MyCacheManagerConfiguration.java new file mode 100644 index 000000000000..2d3ea03f37b8 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/caching/provider/MyCacheManagerConfiguration.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.caching.provider; + +import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizer; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class MyCacheManagerConfiguration { + + @Bean + public CacheManagerCustomizer cacheManagerCustomizer() { + return (cacheManager) -> cacheManager.setAllowNullValues(false); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/caching/provider/couchbase/MyCouchbaseCacheManagerConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/caching/provider/couchbase/MyCouchbaseCacheManagerConfiguration.java new file mode 100644 index 000000000000..d2b11e7d12bd --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/caching/provider/couchbase/MyCouchbaseCacheManagerConfiguration.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.caching.provider.couchbase; + +import java.time.Duration; + +import org.springframework.boot.autoconfigure.cache.CouchbaseCacheManagerBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.couchbase.cache.CouchbaseCacheConfiguration; + +@Configuration(proxyBeanMethods = false) +public class MyCouchbaseCacheManagerConfiguration { + + @Bean + public CouchbaseCacheManagerBuilderCustomizer myCouchbaseCacheManagerBuilderCustomizer() { + // @formatter:off + return (builder) -> builder + .withCacheConfiguration("cache1", CouchbaseCacheConfiguration + .defaultCacheConfig().entryExpiry(Duration.ofSeconds(10))) + .withCacheConfiguration("cache2", CouchbaseCacheConfiguration + .defaultCacheConfig().entryExpiry(Duration.ofMinutes(1))); + // @formatter:on + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/caching/provider/redis/MyRedisCacheManagerConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/caching/provider/redis/MyRedisCacheManagerConfiguration.java new file mode 100644 index 000000000000..582ef2e1f3e8 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/caching/provider/redis/MyRedisCacheManagerConfiguration.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.caching.provider.redis; + +import java.time.Duration; + +import org.springframework.boot.autoconfigure.cache.RedisCacheManagerBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; + +@Configuration(proxyBeanMethods = false) +public class MyRedisCacheManagerConfiguration { + + @Bean + public RedisCacheManagerBuilderCustomizer myRedisCacheManagerBuilderCustomizer() { + // @formatter:off + return (builder) -> builder + .withCacheConfiguration("cache1", RedisCacheConfiguration + .defaultCacheConfig().entryTtl(Duration.ofSeconds(10))) + .withCacheConfiguration("cache2", RedisCacheConfiguration + .defaultCacheConfig().entryTtl(Duration.ofMinutes(1))); + // @formatter:on + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingautoconfiguration/conditionannotations/beanconditions/MyAutoConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingautoconfiguration/conditionannotations/beanconditions/MyAutoConfiguration.java new file mode 100644 index 000000000000..2ac32c4f2abf --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingautoconfiguration/conditionannotations/beanconditions/MyAutoConfiguration.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingautoconfiguration.conditionannotations.beanconditions; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class MyAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public SomeService someService() { + return new SomeService(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingautoconfiguration/conditionannotations/beanconditions/SomeService.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingautoconfiguration/conditionannotations/beanconditions/SomeService.java new file mode 100644 index 000000000000..9335d3e90dfb --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingautoconfiguration/conditionannotations/beanconditions/SomeService.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingautoconfiguration.conditionannotations.beanconditions; + +public class SomeService { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingautoconfiguration/conditionannotations/classconditions/MyAutoConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingautoconfiguration/conditionannotations/classconditions/MyAutoConfiguration.java new file mode 100644 index 000000000000..3db21879f277 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingautoconfiguration/conditionannotations/classconditions/MyAutoConfiguration.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingautoconfiguration.conditionannotations.classconditions; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +// Some conditions ... +public class MyAutoConfiguration { + + // Auto-configured beans ... + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(SomeService.class) + public static class SomeServiceConfiguration { + + @Bean + @ConditionalOnMissingBean + public SomeService someService() { + return new SomeService(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingautoconfiguration/conditionannotations/classconditions/SomeService.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingautoconfiguration/conditionannotations/classconditions/SomeService.java new file mode 100644 index 000000000000..5d4c43208308 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingautoconfiguration/conditionannotations/classconditions/SomeService.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingautoconfiguration.conditionannotations.classconditions; + +class SomeService { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingautoconfiguration/customstarter/configurationkeys/AcmeProperties.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingautoconfiguration/customstarter/configurationkeys/AcmeProperties.java new file mode 100644 index 000000000000..efb304f41b49 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingautoconfiguration/customstarter/configurationkeys/AcmeProperties.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingautoconfiguration.customstarter.configurationkeys; + +import java.time.Duration; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("acme") +public class AcmeProperties { + + /** + * Whether to check the location of acme resources. + */ + private boolean checkLocation = true; + + /** + * Timeout for establishing a connection to the acme server. + */ + private Duration loginTimeout = Duration.ofSeconds(3); + + // @fold:on // getters/setters ... + public boolean isCheckLocation() { + return this.checkLocation; + } + + public void setCheckLocation(boolean checkLocation) { + this.checkLocation = checkLocation; + } + + public Duration getLoginTimeout() { + return this.loginTimeout; + } + + public void setLoginTimeout(Duration loginTimeout) { + this.loginTimeout = loginTimeout; + } + // @fold:off + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingautoconfiguration/testing/MyConditionEvaluationReportingTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingautoconfiguration/testing/MyConditionEvaluationReportingTests.java new file mode 100644 index 000000000000..08d2ba709a07 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingautoconfiguration/testing/MyConditionEvaluationReportingTests.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingautoconfiguration.testing; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener; +import org.springframework.boot.logging.LogLevel; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +class MyConditionEvaluationReportingTests { + + @Test + void autoConfigTest() { + // @formatter:off + new ApplicationContextRunner() + .withInitializer(new ConditionEvaluationReportLoggingListener(LogLevel.INFO)) + .run((context) -> { + // Test something... + }); + // @formatter:on + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingautoconfiguration/testing/MyService.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingautoconfiguration/testing/MyService.java new file mode 100644 index 000000000000..7db64935af30 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingautoconfiguration/testing/MyService.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingautoconfiguration.testing; + +public class MyService { + + private final String name; + + public MyService(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingautoconfiguration/testing/MyServiceAutoConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingautoconfiguration/testing/MyServiceAutoConfiguration.java new file mode 100644 index 000000000000..19343df832ab --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingautoconfiguration/testing/MyServiceAutoConfiguration.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingautoconfiguration.testing; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.docs.features.developingautoconfiguration.testing.MyServiceAutoConfiguration.UserProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(MyService.class) +@EnableConfigurationProperties(UserProperties.class) +public class MyServiceAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public MyService userService(UserProperties properties) { + return new MyService(properties.getName()); + } + + @ConfigurationProperties("user") + public static class UserProperties { + + private String name = "test"; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingautoconfiguration/testing/MyServiceAutoConfigurationTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingautoconfiguration/testing/MyServiceAutoConfigurationTests.java new file mode 100644 index 000000000000..03269ecf5524 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingautoconfiguration/testing/MyServiceAutoConfigurationTests.java @@ -0,0 +1,75 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingautoconfiguration.testing; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +class MyServiceAutoConfigurationTests { + + // tag::runner[] + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(MyServiceAutoConfiguration.class)); + + // end::runner[] + + // tag::test-env[] + @Test + void serviceNameCanBeConfigured() { + this.contextRunner.withPropertyValues("user.name=test123").run((context) -> { + assertThat(context).hasSingleBean(MyService.class); + assertThat(context.getBean(MyService.class).getName()).isEqualTo("test123"); + }); + } + // end::test-env[] + + // tag::test-classloader[] + @Test + void serviceIsIgnoredIfLibraryIsNotPresent() { + this.contextRunner.withClassLoader(new FilteredClassLoader(MyService.class)) + .run((context) -> assertThat(context).doesNotHaveBean("myService")); + } + // end::test-classloader[] + + // tag::test-user-config[] + @Test + void defaultServiceBacksOff() { + this.contextRunner.withUserConfiguration(UserConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(MyService.class); + assertThat(context).getBean("myCustomService").isSameAs(context.getBean(MyService.class)); + }); + } + + @Configuration(proxyBeanMethods = false) + static class UserConfiguration { + + @Bean + MyService myCustomService() { + return new MyService("mine"); + } + + } + // end::test-user-config[] + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/embeddedcontainer/applicationcontext/MyDemoBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/embeddedcontainer/applicationcontext/MyDemoBean.java new file mode 100644 index 000000000000..7cb0e0f27edb --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/embeddedcontainer/applicationcontext/MyDemoBean.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.embeddedcontainer.applicationcontext; + +import javax.servlet.ServletContext; + +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; +import org.springframework.web.context.WebApplicationContext; + +public class MyDemoBean implements ApplicationListener { + + private ServletContext servletContext; + + @Override + public void onApplicationEvent(ApplicationStartedEvent event) { + ApplicationContext applicationContext = event.getApplicationContext(); + this.servletContext = ((WebApplicationContext) applicationContext).getServletContext(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/embeddedcontainer/customizing/programmatic/MyTomcatWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/embeddedcontainer/customizing/programmatic/MyTomcatWebServerFactoryCustomizer.java new file mode 100644 index 000000000000..7a83567d3f82 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/embeddedcontainer/customizing/programmatic/MyTomcatWebServerFactoryCustomizer.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.embeddedcontainer.customizing.programmatic; + +import java.time.Duration; + +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.stereotype.Component; + +@Component +public class MyTomcatWebServerFactoryCustomizer implements WebServerFactoryCustomizer { + + @Override + public void customize(TomcatServletWebServerFactory server) { + server.addConnectorCustomizers((connector) -> connector.setAsyncTimeout(Duration.ofSeconds(20).toMillis())); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/embeddedcontainer/customizing/programmatic/MyWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/embeddedcontainer/customizing/programmatic/MyWebServerFactoryCustomizer.java new file mode 100644 index 000000000000..bad350242851 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/embeddedcontainer/customizing/programmatic/MyWebServerFactoryCustomizer.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.embeddedcontainer.customizing.programmatic; + +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; +import org.springframework.stereotype.Component; + +@Component +public class MyWebServerFactoryCustomizer implements WebServerFactoryCustomizer { + + @Override + public void customize(ConfigurableServletWebServerFactory server) { + server.setPort(9000); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/jersey/MyEndpoint.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/jersey/MyEndpoint.java new file mode 100644 index 000000000000..1678226e7b17 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/jersey/MyEndpoint.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.jersey; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.springframework.stereotype.Component; + +@Component +@Path("/hello") +public class MyEndpoint { + + @GET + public String message() { + return "Hello"; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/jersey/MyJerseyConfig.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/jersey/MyJerseyConfig.java new file mode 100644 index 000000000000..ccccd0391bfe --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/jersey/MyJerseyConfig.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.jersey; + +import org.glassfish.jersey.server.ResourceConfig; + +import org.springframework.stereotype.Component; + +@Component +public class MyJerseyConfig extends ResourceConfig { + + public MyJerseyConfig() { + register(MyEndpoint.class); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/Customer.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/Customer.java new file mode 100644 index 000000000000..cf088634b85f --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/Customer.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springmvc; + +class Customer { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/CustomerRepository.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/CustomerRepository.java new file mode 100644 index 000000000000..8c41b969046a --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/CustomerRepository.java @@ -0,0 +1,27 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springmvc; + +import java.util.List; + +import org.springframework.data.repository.CrudRepository; + +interface CustomerRepository extends CrudRepository { + + List findByUser(User user); + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/MyRestController.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/MyRestController.java new file mode 100644 index 000000000000..fbe815ef39ea --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/MyRestController.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springmvc; + +import java.util.List; + +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/users") +public class MyRestController { + + private final UserRepository userRepository; + + private final CustomerRepository customerRepository; + + public MyRestController(UserRepository userRepository, CustomerRepository customerRepository) { + this.userRepository = userRepository; + this.customerRepository = customerRepository; + } + + @GetMapping("/{userId}") + public User getUser(@PathVariable Long userId) { + return this.userRepository.findById(userId).get(); + } + + @GetMapping("/{userId}/customers") + public List getUserCustomers(@PathVariable Long userId) { + return this.userRepository.findById(userId).map(this.customerRepository::findByUser).get(); + } + + @DeleteMapping("/{userId}") + public void deleteUser(@PathVariable Long userId) { + this.userRepository.deleteById(userId); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/MyRoutingConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/MyRoutingConfiguration.java new file mode 100644 index 000000000000..27aa18b8e9bf --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/MyRoutingConfiguration.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springmvc; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.web.servlet.function.RequestPredicate; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.ServerResponse; + +import static org.springframework.web.servlet.function.RequestPredicates.accept; +import static org.springframework.web.servlet.function.RouterFunctions.route; + +@Configuration(proxyBeanMethods = false) +public class MyRoutingConfiguration { + + private static final RequestPredicate ACCEPT_JSON = accept(MediaType.APPLICATION_JSON); + + @Bean + public RouterFunction routerFunction(MyUserHandler userHandler) { + // @formatter:off + return route() + .GET("/{user}", ACCEPT_JSON, userHandler::getUser) + .GET("/{user}/customers", ACCEPT_JSON, userHandler::getUserCustomers) + .DELETE("/{user}", ACCEPT_JSON, userHandler::deleteUser) + .build(); + // @formatter:on + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/MyUserHandler.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/MyUserHandler.java new file mode 100644 index 000000000000..f83dc92334a1 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/MyUserHandler.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springmvc; + +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.function.ServerRequest; +import org.springframework.web.servlet.function.ServerResponse; + +@Component +public class MyUserHandler { + + public ServerResponse getUser(ServerRequest request) { + /**/ return ServerResponse.ok().build(); + } + + public ServerResponse getUserCustomers(ServerRequest request) { + /**/ return ServerResponse.ok().build(); + } + + public ServerResponse deleteUser(ServerRequest request) { + /**/ return ServerResponse.ok().build(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/User.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/User.java new file mode 100644 index 000000000000..f0e90bdf689f --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/User.java @@ -0,0 +1,27 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springmvc; + +import java.util.List; + +class User { + + List getCustomers() { + return null; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/UserRepository.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/UserRepository.java new file mode 100644 index 000000000000..c21fde369482 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/UserRepository.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springmvc; + +import org.springframework.data.repository.CrudRepository; + +interface UserRepository extends CrudRepository { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/cors/MyCorsConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/cors/MyCorsConfiguration.java new file mode 100644 index 000000000000..53be9963ffab --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/cors/MyCorsConfiguration.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springmvc.cors; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration(proxyBeanMethods = false) +public class MyCorsConfiguration { + + @Bean + public WebMvcConfigurer corsConfigurer() { + return new WebMvcConfigurer() { + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/api/**"); + } + + }; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/errorhandling/CustomException.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/errorhandling/CustomException.java new file mode 100644 index 000000000000..3f80217fa2db --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/errorhandling/CustomException.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springmvc.errorhandling; + +class CustomException extends RuntimeException { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/errorhandling/MyController.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/errorhandling/MyController.java new file mode 100644 index 000000000000..90a1d9dcac6c --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/errorhandling/MyController.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springmvc.errorhandling; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.boot.web.servlet.error.ErrorAttributes; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@Controller +public class MyController { + + @ExceptionHandler(CustomException.class) + String handleCustomException(HttpServletRequest request, CustomException ex) { + request.setAttribute(ErrorAttributes.ERROR_ATTRIBUTE, ex); + return "errorView"; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/errorhandling/MyControllerAdvice.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/errorhandling/MyControllerAdvice.java new file mode 100644 index 000000000000..c72fa6034f03 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/errorhandling/MyControllerAdvice.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springmvc.errorhandling; + +import javax.servlet.RequestDispatcher; +import javax.servlet.http.HttpServletRequest; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +@ControllerAdvice(basePackageClasses = SomeController.class) +public class MyControllerAdvice extends ResponseEntityExceptionHandler { + + @ResponseBody + @ExceptionHandler(MyException.class) + public ResponseEntity handleControllerException(HttpServletRequest request, Throwable ex) { + HttpStatus status = getStatus(request); + return new ResponseEntity<>(new MyErrorBody(status.value(), ex.getMessage()), status); + } + + private HttpStatus getStatus(HttpServletRequest request) { + Integer code = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); + HttpStatus status = HttpStatus.resolve(code); + return (status != null) ? status : HttpStatus.INTERNAL_SERVER_ERROR; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/errorhandling/MyErrorBody.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/errorhandling/MyErrorBody.java new file mode 100644 index 000000000000..8e275fc602a8 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/errorhandling/MyErrorBody.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springmvc.errorhandling; + +class MyErrorBody { + + MyErrorBody(int value, String message) { + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/errorhandling/MyException.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/errorhandling/MyException.java new file mode 100644 index 000000000000..e70c35bbf27d --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/errorhandling/MyException.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springmvc.errorhandling; + +class MyException extends RuntimeException { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/errorhandling/SomeController.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/errorhandling/SomeController.java new file mode 100644 index 000000000000..6bb3e593e1d8 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/errorhandling/SomeController.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springmvc.errorhandling; + +class SomeController { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/errorhandling/errorpages/MyErrorViewResolver.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/errorhandling/errorpages/MyErrorViewResolver.java new file mode 100644 index 000000000000..663e5c446370 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/errorhandling/errorpages/MyErrorViewResolver.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springmvc.errorhandling.errorpages; + +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver; +import org.springframework.http.HttpStatus; +import org.springframework.web.servlet.ModelAndView; + +public class MyErrorViewResolver implements ErrorViewResolver { + + @Override + public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map model) { + // Use the request or status to optionally return a ModelAndView + if (status == HttpStatus.INSUFFICIENT_STORAGE) { + // We could add custom model values here + new ModelAndView("myview"); + } + return null; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/errorhandling/errorpageswithoutspringmvc/MyErrorPagesConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/errorhandling/errorpageswithoutspringmvc/MyErrorPagesConfiguration.java new file mode 100644 index 000000000000..f609f23e5533 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/errorhandling/errorpageswithoutspringmvc/MyErrorPagesConfiguration.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springmvc.errorhandling.errorpageswithoutspringmvc; + +import org.springframework.boot.web.server.ErrorPage; +import org.springframework.boot.web.server.ErrorPageRegistrar; +import org.springframework.boot.web.server.ErrorPageRegistry; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; + +@Configuration(proxyBeanMethods = false) +public class MyErrorPagesConfiguration { + + @Bean + public ErrorPageRegistrar errorPageRegistrar() { + return this::registerErrorPages; + } + + private void registerErrorPages(ErrorPageRegistry registry) { + registry.addErrorPages(new ErrorPage(HttpStatus.BAD_REQUEST, "/400")); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/errorhandling/errorpageswithoutspringmvc/MyFilter.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/errorhandling/errorpageswithoutspringmvc/MyFilter.java new file mode 100644 index 000000000000..88932ee3cf65 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/errorhandling/errorpageswithoutspringmvc/MyFilter.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springmvc.errorhandling.errorpageswithoutspringmvc; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.springframework.web.filter.GenericFilterBean; + +class MyFilter extends GenericFilterBean { + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/errorhandling/errorpageswithoutspringmvc/MyFilterConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/errorhandling/errorpageswithoutspringmvc/MyFilterConfiguration.java new file mode 100644 index 000000000000..55e581858527 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/errorhandling/errorpageswithoutspringmvc/MyFilterConfiguration.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springmvc.errorhandling.errorpageswithoutspringmvc; + +import java.util.EnumSet; + +import javax.servlet.DispatcherType; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class MyFilterConfiguration { + + @Bean + public FilterRegistrationBean myFilter() { + FilterRegistrationBean registration = new FilterRegistrationBean<>(new MyFilter()); + // ... + registration.setDispatcherTypes(EnumSet.allOf(DispatcherType.class)); + return registration; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/json/MyJsonComponent.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/json/MyJsonComponent.java new file mode 100644 index 000000000000..9f739e3ed337 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/json/MyJsonComponent.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springmvc.json; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import org.springframework.boot.jackson.JsonComponent; + +@JsonComponent +public class MyJsonComponent { + + public static class Serializer extends JsonSerializer { + + @Override + public void serialize(MyObject value, JsonGenerator jgen, SerializerProvider serializers) throws IOException { + jgen.writeStartObject(); + jgen.writeStringField("name", value.getName()); + jgen.writeNumberField("age", value.getAge()); + jgen.writeEndObject(); + } + + } + + public static class Deserializer extends JsonDeserializer { + + @Override + public MyObject deserialize(JsonParser jsonParser, DeserializationContext ctxt) + throws IOException, JsonProcessingException { + ObjectCodec codec = jsonParser.getCodec(); + JsonNode tree = codec.readTree(jsonParser); + String name = tree.get("name").textValue(); + int age = tree.get("age").intValue(); + return new MyObject(name, age); + } + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/json/MyObject.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/json/MyObject.java new file mode 100644 index 000000000000..25cf41f1a476 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/json/MyObject.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springmvc.json; + +class MyObject { + + MyObject(String name, int age) { + } + + String getName() { + return null; + } + + Integer getAge() { + return null; + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/json/object/MyJsonComponent.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/json/object/MyJsonComponent.java new file mode 100644 index 000000000000..de7c4dc6924c --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/json/object/MyJsonComponent.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springmvc.json.object; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; + +import org.springframework.boot.jackson.JsonComponent; +import org.springframework.boot.jackson.JsonObjectDeserializer; +import org.springframework.boot.jackson.JsonObjectSerializer; + +@JsonComponent +public class MyJsonComponent { + + public static class Serializer extends JsonObjectSerializer { + + @Override + protected void serializeObject(MyObject value, JsonGenerator jgen, SerializerProvider provider) + throws IOException { + jgen.writeStringField("name", value.getName()); + jgen.writeNumberField("age", value.getAge()); + } + + } + + public static class Deserializer extends JsonObjectDeserializer { + + @Override + protected MyObject deserializeObject(JsonParser jsonParser, DeserializationContext context, ObjectCodec codec, + JsonNode tree) throws IOException { + String name = nullSafeValue(tree.get("name"), String.class); + int age = nullSafeValue(tree.get("age"), Integer.class); + return new MyObject(name, age); + } + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/json/object/MyObject.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/json/object/MyObject.java new file mode 100644 index 000000000000..906f2f2250f3 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/json/object/MyObject.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springmvc.json.object; + +class MyObject { + + MyObject(String name, int age) { + } + + String getName() { + return null; + } + + Integer getAge() { + return null; + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/messageconverters/AdditionalHttpMessageConverter.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/messageconverters/AdditionalHttpMessageConverter.java new file mode 100644 index 000000000000..3bf816eaaa9a --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/messageconverters/AdditionalHttpMessageConverter.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springmvc.messageconverters; + +import java.io.IOException; + +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.converter.AbstractHttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; + +class AdditionalHttpMessageConverter extends AbstractHttpMessageConverter { + + @Override + protected boolean supports(Class clazz) { + return false; + } + + @Override + protected Object readInternal(Class clazz, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException { + return null; + } + + @Override + protected void writeInternal(Object t, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException { + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/messageconverters/AnotherHttpMessageConverter.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/messageconverters/AnotherHttpMessageConverter.java new file mode 100644 index 000000000000..57c8abbbef95 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/messageconverters/AnotherHttpMessageConverter.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springmvc.messageconverters; + +class AnotherHttpMessageConverter extends AdditionalHttpMessageConverter { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/messageconverters/MyHttpMessageConvertersConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/messageconverters/MyHttpMessageConvertersConfiguration.java new file mode 100644 index 000000000000..50cc98cc3cf6 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springmvc/messageconverters/MyHttpMessageConvertersConfiguration.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springmvc.messageconverters; + +import org.springframework.boot.autoconfigure.http.HttpMessageConverters; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; + +@Configuration(proxyBeanMethods = false) +public class MyHttpMessageConvertersConfiguration { + + @Bean + public HttpMessageConverters customConverters() { + HttpMessageConverter additional = new AdditionalHttpMessageConverter(); + HttpMessageConverter another = new AnotherHttpMessageConverter(); + return new HttpMessageConverters(additional, another); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springwebflux/Customer.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springwebflux/Customer.java new file mode 100644 index 000000000000..411b1f8232ee --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springwebflux/Customer.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springwebflux; + +class Customer { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springwebflux/CustomerRepository.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springwebflux/CustomerRepository.java new file mode 100644 index 000000000000..595d2b21a56e --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springwebflux/CustomerRepository.java @@ -0,0 +1,27 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springwebflux; + +import reactor.core.publisher.Flux; + +import org.springframework.data.repository.reactive.ReactiveCrudRepository; + +interface CustomerRepository extends ReactiveCrudRepository { + + Flux findByUser(User user); + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springwebflux/MyRestController.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springwebflux/MyRestController.java new file mode 100644 index 000000000000..0aa35f2dbcf6 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springwebflux/MyRestController.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springwebflux; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/users") +public class MyRestController { + + private final UserRepository userRepository; + + private final CustomerRepository customerRepository; + + public MyRestController(UserRepository userRepository, CustomerRepository customerRepository) { + this.userRepository = userRepository; + this.customerRepository = customerRepository; + } + + @GetMapping("/{userId}") + public Mono getUser(@PathVariable Long userId) { + return this.userRepository.findById(userId); + } + + @GetMapping("/{userId}/customers") + public Flux getUserCustomers(@PathVariable Long userId) { + return this.userRepository.findById(userId).flatMapMany(this.customerRepository::findByUser); + } + + @DeleteMapping("/{userId}") + public Mono deleteUser(@PathVariable Long userId) { + return this.userRepository.deleteById(userId); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springwebflux/MyRoutingConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springwebflux/MyRoutingConfiguration.java new file mode 100644 index 000000000000..c2e3fc745f2b --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springwebflux/MyRoutingConfiguration.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springwebflux; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.server.RequestPredicate; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; + +import static org.springframework.web.reactive.function.server.RequestPredicates.accept; +import static org.springframework.web.reactive.function.server.RouterFunctions.route; + +@Configuration(proxyBeanMethods = false) +public class MyRoutingConfiguration { + + private static final RequestPredicate ACCEPT_JSON = accept(MediaType.APPLICATION_JSON); + + @Bean + public RouterFunction monoRouterFunction(MyUserHandler userHandler) { + // @formatter:off + return route() + .GET("/{user}", ACCEPT_JSON, userHandler::getUser) + .GET("/{user}/customers", ACCEPT_JSON, userHandler::getUserCustomers) + .DELETE("/{user}", ACCEPT_JSON, userHandler::deleteUser) + .build(); + // @formatter:on + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springwebflux/MyUserHandler.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springwebflux/MyUserHandler.java new file mode 100644 index 000000000000..49a31e2731f1 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springwebflux/MyUserHandler.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springwebflux; + +import reactor.core.publisher.Mono; + +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; + +@Component +public class MyUserHandler { + + public Mono getUser(ServerRequest request) { + /**/ return ServerResponse.ok().build(); + } + + public Mono getUserCustomers(ServerRequest request) { + /**/ return ServerResponse.ok().build(); + } + + public Mono deleteUser(ServerRequest request) { + /**/ return ServerResponse.ok().build(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springwebflux/User.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springwebflux/User.java new file mode 100644 index 000000000000..4337c215b6fa --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springwebflux/User.java @@ -0,0 +1,27 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springwebflux; + +import java.util.List; + +class User { + + List getCustomers() { + return null; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springwebflux/UserRepository.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springwebflux/UserRepository.java new file mode 100644 index 000000000000..7c4616a066a1 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springwebflux/UserRepository.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springwebflux; + +import org.springframework.data.repository.reactive.ReactiveCrudRepository; + +interface UserRepository extends ReactiveCrudRepository { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springwebflux/errorhandling/MyErrorWebExceptionHandler.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springwebflux/errorhandling/MyErrorWebExceptionHandler.java new file mode 100644 index 000000000000..93273dda74c9 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springwebflux/errorhandling/MyErrorWebExceptionHandler.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springwebflux.errorhandling; + +import reactor.core.publisher.Mono; + +import org.springframework.boot.autoconfigure.web.WebProperties.Resources; +import org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler; +import org.springframework.boot.web.reactive.error.ErrorAttributes; +import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.reactive.function.server.ServerResponse.BodyBuilder; + +@Component +public class MyErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler { + + public MyErrorWebExceptionHandler(ErrorAttributes errorAttributes, Resources resources, + ApplicationContext applicationContext) { + super(errorAttributes, resources, applicationContext); + } + + @Override + protected RouterFunction getRoutingFunction(ErrorAttributes errorAttributes) { + return RouterFunctions.route(this::acceptsXml, this::handleErrorAsXml); + } + + private boolean acceptsXml(ServerRequest request) { + return request.headers().accept().contains(MediaType.APPLICATION_XML); + } + + public Mono handleErrorAsXml(ServerRequest request) { + BodyBuilder builder = ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR); + // ... additional builder calls + return builder.build(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springwebflux/errorhandling/MyExceptionHandlingController.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springwebflux/errorhandling/MyExceptionHandlingController.java new file mode 100644 index 000000000000..b3e45caa1cfe --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springwebflux/errorhandling/MyExceptionHandlingController.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springwebflux.errorhandling; + +import org.springframework.boot.web.reactive.error.ErrorAttributes; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.reactive.result.view.Rendering; +import org.springframework.web.server.ServerWebExchange; + +@Controller +public class MyExceptionHandlingController { + + @GetMapping("/profile") + public Rendering userProfile() { + // ... + throw new IllegalStateException(); + } + + @ExceptionHandler(IllegalStateException.class) + public Rendering handleIllegalState(ServerWebExchange exchange, IllegalStateException exc) { + exchange.getAttributes().putIfAbsent(ErrorAttributes.ERROR_ATTRIBUTE, exc); + return Rendering.view("errorView").modelAttribute("message", exc.getMessage()).build(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springwebflux/httpcodecs/MyCodecsConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springwebflux/httpcodecs/MyCodecsConfiguration.java new file mode 100644 index 000000000000..04a5e1271520 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/developingwebapplications/springwebflux/httpcodecs/MyCodecsConfiguration.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingwebapplications.springwebflux.httpcodecs; + +import org.springframework.boot.web.codec.CodecCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.codec.ServerSentEventHttpMessageReader; + +@Configuration(proxyBeanMethods = false) +public class MyCodecsConfiguration { + + @Bean + public CodecCustomizer myCodecCustomizer() { + return (configurer) -> { + configurer.registerDefaults(false); + configurer.customCodecs().register(new ServerSentEventHttpMessageReader()); + // ... + }; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/MyBean.java new file mode 100644 index 000000000000..97f1c61c91d4 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/MyBean.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.externalconfig; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + @Value("${name}") + private String name; + + // ... + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/MyProperties.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/MyProperties.java new file mode 100644 index 000000000000..5af0cde4b674 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/MyProperties.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.constructorbinding; + +import java.net.InetAddress; +import java.util.List; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; +import org.springframework.boot.context.properties.bind.DefaultValue; + +@ConstructorBinding +@ConfigurationProperties("my.service") +public class MyProperties { + + // @fold:on // fields... + private final boolean enabled; + + private final InetAddress remoteAddress; + + private final Security security; + + // @fold:off + + public MyProperties(boolean enabled, InetAddress remoteAddress, Security security) { + this.enabled = enabled; + this.remoteAddress = remoteAddress; + this.security = security; + } + + // @fold:on // getters... + public boolean isEnabled() { + return this.enabled; + } + + public InetAddress getRemoteAddress() { + return this.remoteAddress; + } + + public Security getSecurity() { + return this.security; + } + // @fold:off + + public static class Security { + + // @fold:on // fields... + private final String username; + + private final String password; + + private final List roles; + + // @fold:off + + public Security(String username, String password, @DefaultValue("USER") List roles) { + this.username = username; + this.password = password; + this.roles = roles; + } + + // @fold:on // getters... + public String getUsername() { + return this.username; + } + + public String getPassword() { + return this.password; + } + + public List getRoles() { + return this.roles; + } + // @fold:off + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/nonnull/MyProperties.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/nonnull/MyProperties.java new file mode 100644 index 000000000000..11566766f765 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/constructorbinding/nonnull/MyProperties.java @@ -0,0 +1,84 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.constructorbinding.nonnull; + +import java.net.InetAddress; +import java.util.List; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; +import org.springframework.boot.context.properties.bind.DefaultValue; + +@ConstructorBinding +@ConfigurationProperties("my.service") +public class MyProperties { + + private final boolean enabled; + + private final InetAddress remoteAddress; + + private final Security security; + + // tag::code[] + public MyProperties(boolean enabled, InetAddress remoteAddress, @DefaultValue Security security) { + this.enabled = enabled; + this.remoteAddress = remoteAddress; + this.security = security; + } + // end::code[] + + public boolean isEnabled() { + return this.enabled; + } + + public InetAddress getRemoteAddress() { + return this.remoteAddress; + } + + public Security getSecurity() { + return this.security; + } + + public static class Security { + + private final String username; + + private final String password; + + private final List roles; + + public Security(String username, String password, @DefaultValue("USER") List roles) { + this.username = username; + this.password = password; + this.roles = roles; + } + + public String getUsername() { + return this.username; + } + + public String getPassword() { + return this.password; + } + + public List getRoles() { + return this.roles; + } + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/conversion/datasizes/constructorbinding/MyProperties.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/conversion/datasizes/constructorbinding/MyProperties.java new file mode 100644 index 000000000000..27c1c1dde05c --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/conversion/datasizes/constructorbinding/MyProperties.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.conversion.datasizes.constructorbinding; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; +import org.springframework.boot.context.properties.bind.DefaultValue; +import org.springframework.boot.convert.DataSizeUnit; +import org.springframework.util.unit.DataSize; +import org.springframework.util.unit.DataUnit; + +@ConfigurationProperties("my") +@ConstructorBinding +public class MyProperties { + + // @fold:on // fields... + private final DataSize bufferSize; + + private final DataSize sizeThreshold; + + // @fold:off + public MyProperties(@DataSizeUnit(DataUnit.MEGABYTES) @DefaultValue("2MB") DataSize bufferSize, + @DefaultValue("512B") DataSize sizeThreshold) { + this.bufferSize = bufferSize; + this.sizeThreshold = sizeThreshold; + } + + // @fold:on // getters... + public DataSize getBufferSize() { + return this.bufferSize; + } + + public DataSize getSizeThreshold() { + return this.sizeThreshold; + } + // @fold:off + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/conversion/datasizes/javabeanbinding/MyProperties.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/conversion/datasizes/javabeanbinding/MyProperties.java new file mode 100644 index 000000000000..05b9623268bc --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/conversion/datasizes/javabeanbinding/MyProperties.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.conversion.datasizes.javabeanbinding; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.convert.DataSizeUnit; +import org.springframework.util.unit.DataSize; +import org.springframework.util.unit.DataUnit; + +@ConfigurationProperties("my") +public class MyProperties { + + @DataSizeUnit(DataUnit.MEGABYTES) + private DataSize bufferSize = DataSize.ofMegabytes(2); + + private DataSize sizeThreshold = DataSize.ofBytes(512); + + // @fold:on // getters/setters... + public DataSize getBufferSize() { + return this.bufferSize; + } + + public void setBufferSize(DataSize bufferSize) { + this.bufferSize = bufferSize; + } + + public DataSize getSizeThreshold() { + return this.sizeThreshold; + } + + public void setSizeThreshold(DataSize sizeThreshold) { + this.sizeThreshold = sizeThreshold; + } + // @fold:off + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/conversion/durations/constructorbinding/MyProperties.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/conversion/durations/constructorbinding/MyProperties.java new file mode 100644 index 000000000000..d1d4183e966f --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/conversion/durations/constructorbinding/MyProperties.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.conversion.durations.constructorbinding; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; +import org.springframework.boot.context.properties.bind.DefaultValue; +import org.springframework.boot.convert.DurationUnit; + +@ConfigurationProperties("my") +@ConstructorBinding +public class MyProperties { + + // @fold:on // fields... + private final Duration sessionTimeout; + + private final Duration readTimeout; + + // @fold:off + public MyProperties(@DurationUnit(ChronoUnit.SECONDS) @DefaultValue("30s") Duration sessionTimeout, + @DefaultValue("1000ms") Duration readTimeout) { + this.sessionTimeout = sessionTimeout; + this.readTimeout = readTimeout; + } + + // @fold:on // getters... + public Duration getSessionTimeout() { + return this.sessionTimeout; + } + + public Duration getReadTimeout() { + return this.readTimeout; + } + // @fold:off + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/conversion/durations/javabeanbinding/MyProperties.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/conversion/durations/javabeanbinding/MyProperties.java new file mode 100644 index 000000000000..c9a0abea7ca7 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/conversion/durations/javabeanbinding/MyProperties.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.conversion.durations.javabeanbinding; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.convert.DurationUnit; + +@ConfigurationProperties("my") +public class MyProperties { + + @DurationUnit(ChronoUnit.SECONDS) + private Duration sessionTimeout = Duration.ofSeconds(30); + + private Duration readTimeout = Duration.ofMillis(1000); + + // @fold:on // getters / setters... + public Duration getSessionTimeout() { + return this.sessionTimeout; + } + + public void setSessionTimeout(Duration sessionTimeout) { + this.sessionTimeout = sessionTimeout; + } + + public Duration getReadTimeout() { + return this.readTimeout; + } + + public void setReadTimeout(Duration readTimeout) { + this.readTimeout = readTimeout; + } + // @fold:off + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/enablingannotatedtypes/MyApplication.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/enablingannotatedtypes/MyApplication.java new file mode 100644 index 000000000000..0756d581a687 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/enablingannotatedtypes/MyApplication.java @@ -0,0 +1,26 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.enablingannotatedtypes; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; + +@SpringBootApplication +@ConfigurationPropertiesScan({ "com.example.app", "com.example.another" }) +public class MyApplication { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/enablingannotatedtypes/MyConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/enablingannotatedtypes/MyConfiguration.java new file mode 100644 index 000000000000..5ec4ea18850d --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/enablingannotatedtypes/MyConfiguration.java @@ -0,0 +1,26 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.enablingannotatedtypes; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(SomeProperties.class) +public class MyConfiguration { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/enablingannotatedtypes/SomeProperties.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/enablingannotatedtypes/SomeProperties.java new file mode 100644 index 000000000000..674845bed6e3 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/enablingannotatedtypes/SomeProperties.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.enablingannotatedtypes; + +class SomeProperties { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/javabeanbinding/MyProperties.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/javabeanbinding/MyProperties.java new file mode 100644 index 000000000000..7d91ff68e9e7 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/javabeanbinding/MyProperties.java @@ -0,0 +1,93 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.javabeanbinding; + +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("my.service") +public class MyProperties { + + private boolean enabled; + + private InetAddress remoteAddress; + + private final Security security = new Security(); + + // @fold:on // getters / setters... + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public InetAddress getRemoteAddress() { + return this.remoteAddress; + } + + public void setRemoteAddress(InetAddress remoteAddress) { + this.remoteAddress = remoteAddress; + } + + public Security getSecurity() { + return this.security; + } + // @fold:off + + public static class Security { + + private String username; + + private String password; + + private List roles = new ArrayList<>(Collections.singleton("USER")); + + // @fold:on // getters / setters... + public String getUsername() { + return this.username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return this.password; + } + + public void setPassword(String password) { + this.password = password; + } + + public List getRoles() { + return this.roles; + } + + public void setRoles(List roles) { + this.roles = roles; + } + // @fold:off + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/mergingcomplextypes/list/MyPojo.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/mergingcomplextypes/list/MyPojo.java new file mode 100644 index 000000000000..a61ab9f0fc61 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/mergingcomplextypes/list/MyPojo.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.mergingcomplextypes.list; + +class MyPojo { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/mergingcomplextypes/list/MyProperties.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/mergingcomplextypes/list/MyProperties.java new file mode 100644 index 000000000000..0c5885cb4222 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/mergingcomplextypes/list/MyProperties.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.mergingcomplextypes.list; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("my") +public class MyProperties { + + private final List list = new ArrayList<>(); + + public List getList() { + return this.list; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/mergingcomplextypes/map/MyPojo.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/mergingcomplextypes/map/MyPojo.java new file mode 100644 index 000000000000..59975fb6d8a3 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/mergingcomplextypes/map/MyPojo.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.mergingcomplextypes.map; + +class MyPojo { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/mergingcomplextypes/map/MyProperties.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/mergingcomplextypes/map/MyProperties.java new file mode 100644 index 000000000000..771975786173 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/mergingcomplextypes/map/MyProperties.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.mergingcomplextypes.map; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties("my") +public class MyProperties { + + private final Map map = new LinkedHashMap<>(); + + public Map getMap() { + return this.map; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/relaxedbinding/MyPersonProperties.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/relaxedbinding/MyPersonProperties.java new file mode 100644 index 000000000000..74fb9d6bd55f --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/relaxedbinding/MyPersonProperties.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.relaxedbinding; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "my.main-project.person") +public class MyPersonProperties { + + private String firstName; + + public String getFirstName() { + return this.firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/thirdpartyconfiguration/AnotherComponent.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/thirdpartyconfiguration/AnotherComponent.java new file mode 100644 index 000000000000..c1c4aa55806a --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/thirdpartyconfiguration/AnotherComponent.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.thirdpartyconfiguration; + +class AnotherComponent { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/thirdpartyconfiguration/ThirdPartyConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/thirdpartyconfiguration/ThirdPartyConfiguration.java new file mode 100644 index 000000000000..b63271555683 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/thirdpartyconfiguration/ThirdPartyConfiguration.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.thirdpartyconfiguration; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class ThirdPartyConfiguration { + + @Bean + @ConfigurationProperties(prefix = "another") + public AnotherComponent anotherComponent() { + return new AnotherComponent(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/usingannotatedtypes/MyService.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/usingannotatedtypes/MyService.java new file mode 100644 index 000000000000..dfd2904d48c5 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/usingannotatedtypes/MyService.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.usingannotatedtypes; + +import org.springframework.stereotype.Service; + +@Service +public class MyService { + + private final SomeProperties properties; + + public MyService(SomeProperties properties) { + this.properties = properties; + } + + public void openConnection() { + Server server = new Server(this.properties.getRemoteAddress()); + server.start(); + // ... + } + + // ... + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/usingannotatedtypes/Server.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/usingannotatedtypes/Server.java new file mode 100644 index 000000000000..132bbe2bdc81 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/usingannotatedtypes/Server.java @@ -0,0 +1,27 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.usingannotatedtypes; + +class Server { + + Server(Object remoteAddress) { + } + + void start() { + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/usingannotatedtypes/SomeProperties.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/usingannotatedtypes/SomeProperties.java new file mode 100644 index 000000000000..cf46da4cffef --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/usingannotatedtypes/SomeProperties.java @@ -0,0 +1,25 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.usingannotatedtypes; + +class SomeProperties { + + Object getRemoteAddress() { + return null; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/validate/MyProperties.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/validate/MyProperties.java new file mode 100644 index 000000000000..138b8d4eccac --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/validate/MyProperties.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.validate; + +import java.net.InetAddress; + +import javax.validation.constraints.NotNull; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +@ConfigurationProperties("my.service") +@Validated +public class MyProperties { + + @NotNull + private InetAddress remoteAddress; + + // @fold:on // getters/setters... + public InetAddress getRemoteAddress() { + return this.remoteAddress; + } + + public void setRemoteAddress(InetAddress remoteAddress) { + this.remoteAddress = remoteAddress; + } + // @fold:off + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/validate/nested/MyProperties.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/validate/nested/MyProperties.java new file mode 100644 index 000000000000..fd87eeaf4678 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/validate/nested/MyProperties.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.validate.nested; + +import java.net.InetAddress; + +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +@ConfigurationProperties("my.service") +@Validated +public class MyProperties { + + @NotNull + private InetAddress remoteAddress; + + @Valid + private final Security security = new Security(); + + // @fold:on // getters/setters... + public InetAddress getRemoteAddress() { + return this.remoteAddress; + } + + public void setRemoteAddress(InetAddress remoteAddress) { + this.remoteAddress = remoteAddress; + } + + public Security getSecurity() { + return this.security; + } + // @fold:off + + public static class Security { + + @NotEmpty + private String username; + + // @fold:on // getters/setters... + public String getUsername() { + return this.username; + } + + public void setUsername(String username) { + this.username = username; + } + // @fold:off + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/jta/mixingxaandnonxaconnections/nonxa/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/jta/mixingxaandnonxaconnections/nonxa/MyBean.java new file mode 100644 index 000000000000..6eeede5fff59 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/jta/mixingxaandnonxaconnections/nonxa/MyBean.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.jta.mixingxaandnonxaconnections.nonxa; + +import javax.jms.ConnectionFactory; + +import org.springframework.beans.factory.annotation.Qualifier; + +public class MyBean { + + // tag::code[] + public MyBean(@Qualifier("nonXaJmsConnectionFactory") ConnectionFactory connectionFactory) { + // ... + } + // end::code[] + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/jta/mixingxaandnonxaconnections/primary/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/jta/mixingxaandnonxaconnections/primary/MyBean.java new file mode 100644 index 000000000000..b3441310be97 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/jta/mixingxaandnonxaconnections/primary/MyBean.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.jta.mixingxaandnonxaconnections.primary; + +import javax.jms.ConnectionFactory; + +public class MyBean { + + // tag::code[] + public MyBean(ConnectionFactory connectionFactory) { + // ... + } + // end::code[] + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/jta/mixingxaandnonxaconnections/xa/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/jta/mixingxaandnonxaconnections/xa/MyBean.java new file mode 100644 index 000000000000..72ca53e66edf --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/jta/mixingxaandnonxaconnections/xa/MyBean.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.jta.mixingxaandnonxaconnections.xa; + +import javax.jms.ConnectionFactory; + +import org.springframework.beans.factory.annotation.Qualifier; + +public class MyBean { + + // tag::code[] + public MyBean(@Qualifier("xaJmsConnectionFactory") ConnectionFactory connectionFactory) { + // ... + } + // end::code[] + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/amqp/receiving/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/amqp/receiving/MyBean.java new file mode 100644 index 000000000000..8b7657acbc7f --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/amqp/receiving/MyBean.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.messaging.amqp.receiving; + +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + @RabbitListener(queues = "someQueue") + public void processMessage(String content) { + // ... + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/amqp/receiving/custom/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/amqp/receiving/custom/MyBean.java new file mode 100644 index 000000000000..851f9dba3062 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/amqp/receiving/custom/MyBean.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.messaging.amqp.receiving.custom; + +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + @RabbitListener(queues = "someQueue", containerFactory = "myFactory") + public void processMessage(String content) { + // ... + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/amqp/receiving/custom/MyMessageConverter.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/amqp/receiving/custom/MyMessageConverter.java new file mode 100644 index 000000000000..ccc379d7252d --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/amqp/receiving/custom/MyMessageConverter.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.messaging.amqp.receiving.custom; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.support.converter.MessageConversionException; +import org.springframework.amqp.support.converter.MessageConverter; + +class MyMessageConverter implements MessageConverter { + + @Override + public Message toMessage(Object object, MessageProperties messageProperties) throws MessageConversionException { + return null; + } + + @Override + public Object fromMessage(Message message) throws MessageConversionException { + return null; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/amqp/receiving/custom/MyRabbitConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/amqp/receiving/custom/MyRabbitConfiguration.java new file mode 100644 index 000000000000..74d7064f4524 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/amqp/receiving/custom/MyRabbitConfiguration.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.messaging.amqp.receiving.custom; + +import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.boot.autoconfigure.amqp.SimpleRabbitListenerContainerFactoryConfigurer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class MyRabbitConfiguration { + + @Bean + public SimpleRabbitListenerContainerFactory myFactory(SimpleRabbitListenerContainerFactoryConfigurer configurer) { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + ConnectionFactory connectionFactory = getCustomConnectionFactory(); + configurer.configure(factory, connectionFactory); + factory.setMessageConverter(new MyMessageConverter()); + return factory; + } + + private ConnectionFactory getCustomConnectionFactory() { + return /**/ null; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/amqp/sending/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/amqp/sending/MyBean.java new file mode 100644 index 000000000000..7f6ac3e854c0 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/amqp/sending/MyBean.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.messaging.amqp.sending; + +import org.springframework.amqp.core.AmqpAdmin; +import org.springframework.amqp.core.AmqpTemplate; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + private final AmqpAdmin amqpAdmin; + + private final AmqpTemplate amqpTemplate; + + public MyBean(AmqpAdmin amqpAdmin, AmqpTemplate amqpTemplate) { + this.amqpAdmin = amqpAdmin; + this.amqpTemplate = amqpTemplate; + } + + // @fold:on // ... + public void someMethod() { + this.amqpAdmin.getQueueInfo("someQueue"); + } + + public void someOtherMethod() { + this.amqpTemplate.convertAndSend("hello"); + } + // @fold:off + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/jms/receiving/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/jms/receiving/MyBean.java new file mode 100644 index 000000000000..c9190c173c31 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/jms/receiving/MyBean.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.messaging.jms.receiving; + +import org.springframework.jms.annotation.JmsListener; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + @JmsListener(destination = "someQueue") + public void processMessage(String content) { + // ... + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/jms/receiving/custom/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/jms/receiving/custom/MyBean.java new file mode 100644 index 000000000000..3341edf2eefb --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/jms/receiving/custom/MyBean.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.messaging.jms.receiving.custom; + +import org.springframework.jms.annotation.JmsListener; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + @JmsListener(destination = "someQueue", containerFactory = "myFactory") + public void processMessage(String content) { + // ... + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/jms/receiving/custom/MyJmsConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/jms/receiving/custom/MyJmsConfiguration.java new file mode 100644 index 000000000000..1781fc3254cf --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/jms/receiving/custom/MyJmsConfiguration.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.messaging.jms.receiving.custom; + +import javax.jms.ConnectionFactory; + +import org.springframework.boot.autoconfigure.jms.DefaultJmsListenerContainerFactoryConfigurer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jms.config.DefaultJmsListenerContainerFactory; + +@Configuration(proxyBeanMethods = false) +public class MyJmsConfiguration { + + @Bean + public DefaultJmsListenerContainerFactory myFactory(DefaultJmsListenerContainerFactoryConfigurer configurer) { + DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory(); + ConnectionFactory connectionFactory = getCustomConnectionFactory(); + configurer.configure(factory, connectionFactory); + factory.setMessageConverter(new MyMessageConverter()); + return factory; + } + + private ConnectionFactory getCustomConnectionFactory() { + return /**/ null; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/jms/receiving/custom/MyMessageConverter.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/jms/receiving/custom/MyMessageConverter.java new file mode 100644 index 000000000000..aebd1beb8e35 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/jms/receiving/custom/MyMessageConverter.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.messaging.jms.receiving.custom; + +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.Session; + +import org.springframework.jms.support.converter.MessageConversionException; +import org.springframework.jms.support.converter.MessageConverter; + +class MyMessageConverter implements MessageConverter { + + @Override + public Message toMessage(Object object, Session session) throws JMSException, MessageConversionException { + return null; + } + + @Override + public Object fromMessage(Message message) throws JMSException, MessageConversionException { + return null; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/jms/sending/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/jms/sending/MyBean.java new file mode 100644 index 000000000000..70bbc53ae4f8 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/jms/sending/MyBean.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.messaging.jms.sending; + +import org.springframework.jms.core.JmsTemplate; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + private final JmsTemplate jmsTemplate; + + public MyBean(JmsTemplate jmsTemplate) { + this.jmsTemplate = jmsTemplate; + } + + // @fold:on // ... + public void someMethod() { + this.jmsTemplate.convertAndSend("hello"); + } + // @fold:off + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/kafka/embedded/annotation/MyTest.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/kafka/embedded/annotation/MyTest.java new file mode 100644 index 000000000000..64faa56163ac --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/kafka/embedded/annotation/MyTest.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.messaging.kafka.embedded.annotation; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.kafka.test.context.EmbeddedKafka; + +@SpringBootTest +@EmbeddedKafka(topics = "someTopic", bootstrapServersProperty = "spring.kafka.bootstrap-servers") +class MyTest { + + // ... + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/kafka/embedded/property/MyTest.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/kafka/embedded/property/MyTest.java new file mode 100644 index 000000000000..fb186d5ae308 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/kafka/embedded/property/MyTest.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.messaging.kafka.embedded.property; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.kafka.test.EmbeddedKafkaBroker; + +@SpringBootTest +class MyTest { + + // tag::code[] + static { + System.setProperty(EmbeddedKafkaBroker.BROKER_LIST_PROPERTY, "spring.kafka.bootstrap-servers"); + } + // end::code[] + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/kafka/receiving/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/kafka/receiving/MyBean.java new file mode 100644 index 000000000000..30cdbbf587f0 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/kafka/receiving/MyBean.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.messaging.kafka.receiving; + +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + @KafkaListener(topics = "someTopic") + public void processMessage(String content) { + // ... + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/kafka/sending/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/kafka/sending/MyBean.java new file mode 100644 index 000000000000..f01737e6f6e2 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/kafka/sending/MyBean.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.messaging.kafka.sending; + +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + private final KafkaTemplate kafkaTemplate; + + public MyBean(KafkaTemplate kafkaTemplate) { + this.kafkaTemplate = kafkaTemplate; + } + + // @fold:on // ... + public void someMethod() { + this.kafkaTemplate.send("someTopic", "Hello"); + } + // @fold:off + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/kafka/streams/MyKafkaStreamsConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/kafka/streams/MyKafkaStreamsConfiguration.java new file mode 100644 index 000000000000..cc35d5f85931 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/messaging/kafka/streams/MyKafkaStreamsConfiguration.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.messaging.kafka.streams; + +import org.apache.kafka.common.serialization.Serdes; +import org.apache.kafka.streams.KeyValue; +import org.apache.kafka.streams.StreamsBuilder; +import org.apache.kafka.streams.kstream.KStream; +import org.apache.kafka.streams.kstream.Produced; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.annotation.EnableKafkaStreams; +import org.springframework.kafka.support.serializer.JsonSerde; + +@Configuration(proxyBeanMethods = false) +@EnableKafkaStreams +public class MyKafkaStreamsConfiguration { + + @Bean + public KStream kStream(StreamsBuilder streamsBuilder) { + KStream stream = streamsBuilder.stream("ks1In"); + stream.map(this::uppercaseValue).to("ks1Out", Produced.with(Serdes.Integer(), new JsonSerde<>())); + return stream; + } + + private KeyValue uppercaseValue(Integer key, String value) { + return new KeyValue<>(key, value.toUpperCase()); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/cassandra/connecting/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/cassandra/connecting/MyBean.java new file mode 100644 index 000000000000..36cd9c4d3bf7 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/cassandra/connecting/MyBean.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.nosql.cassandra.connecting; + +import org.springframework.data.cassandra.core.CassandraTemplate; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + private final CassandraTemplate template; + + public MyBean(CassandraTemplate template) { + this.template = template; + } + + // @fold:on // ... + public long someMethod() { + return this.template.count(User.class); + } + // @fold:off + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/cassandra/connecting/User.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/cassandra/connecting/User.java new file mode 100644 index 000000000000..885b0fcc7ed3 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/cassandra/connecting/User.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.nosql.cassandra.connecting; + +class User { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/couchbase/repositories/CouchbaseProperties.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/couchbase/repositories/CouchbaseProperties.java new file mode 100644 index 000000000000..e6bc0bfb7dd3 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/couchbase/repositories/CouchbaseProperties.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.nosql.couchbase.repositories; + +class CouchbaseProperties { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/couchbase/repositories/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/couchbase/repositories/MyBean.java new file mode 100644 index 000000000000..496ef0f4351c --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/couchbase/repositories/MyBean.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.nosql.couchbase.repositories; + +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + private final CouchbaseTemplate template; + + public MyBean(CouchbaseTemplate template) { + this.template = template; + } + + // @fold:on // ... + public String someMethod() { + return this.template.getBucketName(); + } + // @fold:off + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/couchbase/repositories/MyConverter.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/couchbase/repositories/MyConverter.java new file mode 100644 index 000000000000..742852ef7f84 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/couchbase/repositories/MyConverter.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.nosql.couchbase.repositories; + +import org.springframework.core.convert.converter.Converter; + +class MyConverter implements Converter { + + @Override + public Boolean convert(CouchbaseProperties value) { + return true; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/couchbase/repositories/MyCouchbaseConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/couchbase/repositories/MyCouchbaseConfiguration.java new file mode 100644 index 000000000000..39b294b53ac2 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/couchbase/repositories/MyCouchbaseConfiguration.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.nosql.couchbase.repositories; + +import org.assertj.core.util.Arrays; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.couchbase.config.BeanNames; +import org.springframework.data.couchbase.core.convert.CouchbaseCustomConversions; + +@Configuration(proxyBeanMethods = false) +public class MyCouchbaseConfiguration { + + @Bean(BeanNames.COUCHBASE_CUSTOM_CONVERSIONS) + public CouchbaseCustomConversions myCustomConversions() { + return new CouchbaseCustomConversions(Arrays.asList(new MyConverter())); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/elasticsearch/connectingusingspringdata/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/elasticsearch/connectingusingspringdata/MyBean.java new file mode 100644 index 000000000000..9ef558570372 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/elasticsearch/connectingusingspringdata/MyBean.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.nosql.elasticsearch.connectingusingspringdata; + +import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + private final ElasticsearchRestTemplate template; + + public MyBean(ElasticsearchRestTemplate template) { + this.template = template; + } + + // @fold:on // ... + public boolean someMethod(String id) { + return this.template.exists(id, User.class); + } + // @fold:off + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/elasticsearch/connectingusingspringdata/User.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/elasticsearch/connectingusingspringdata/User.java new file mode 100644 index 000000000000..8f9bce68abec --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/elasticsearch/connectingusingspringdata/User.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.nosql.elasticsearch.connectingusingspringdata; + +class User { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/ldap/repositories/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/ldap/repositories/MyBean.java new file mode 100644 index 000000000000..9093c58d49e1 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/ldap/repositories/MyBean.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.nosql.ldap.repositories; + +import java.util.List; + +import org.springframework.ldap.core.LdapTemplate; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + private final LdapTemplate template; + + public MyBean(LdapTemplate template) { + this.template = template; + } + + // @fold:on // ... + public List someMethod() { + return this.template.findAll(User.class); + } + // @fold:off + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/ldap/repositories/User.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/ldap/repositories/User.java new file mode 100644 index 000000000000..bac79a879d24 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/ldap/repositories/User.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.nosql.ldap.repositories; + +class User { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/mongodb/connecting/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/mongodb/connecting/MyBean.java new file mode 100644 index 000000000000..863ae5bab896 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/mongodb/connecting/MyBean.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.nosql.mongodb.connecting; + +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import org.bson.Document; + +import org.springframework.data.mongodb.MongoDatabaseFactory; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + private final MongoDatabaseFactory mongo; + + public MyBean(MongoDatabaseFactory mongo) { + this.mongo = mongo; + } + + // @fold:on // ... + public MongoCollection someMethod() { + MongoDatabase db = this.mongo.getMongoDatabase(); + return db.getCollection("users"); + } + // @fold:off + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/mongodb/repositories/City.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/mongodb/repositories/City.java new file mode 100644 index 000000000000..7d23f3f6b5de --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/mongodb/repositories/City.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.nosql.mongodb.repositories; + +public class City { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/mongodb/repositories/CityRepository.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/mongodb/repositories/CityRepository.java new file mode 100644 index 000000000000..d6f6fe1b8333 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/mongodb/repositories/CityRepository.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.nosql.mongodb.repositories; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.repository.Repository; + +public interface CityRepository extends Repository { + + Page findAll(Pageable pageable); + + City findByNameAndStateAllIgnoringCase(String name, String state); + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/mongodb/template/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/mongodb/template/MyBean.java new file mode 100644 index 000000000000..09e104b580f0 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/mongodb/template/MyBean.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.nosql.mongodb.template; + +import com.mongodb.client.MongoCollection; +import org.bson.Document; + +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + private final MongoTemplate mongoTemplate; + + public MyBean(MongoTemplate mongoTemplate) { + this.mongoTemplate = mongoTemplate; + } + + // @fold:on // ... + public MongoCollection someMethod() { + return this.mongoTemplate.getCollection("users"); + } + // @fold:off + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/neo4j/connecting/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/neo4j/connecting/MyBean.java new file mode 100644 index 000000000000..87abe298fa87 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/neo4j/connecting/MyBean.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.nosql.neo4j.connecting; + +import org.neo4j.driver.Driver; +import org.neo4j.driver.Session; +import org.neo4j.driver.Values; + +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + private final Driver driver; + + public MyBean(Driver driver) { + this.driver = driver; + } + + // @fold:on // ... + public String someMethod(String message) { + try (Session session = this.driver.session()) { + return session.writeTransaction((transaction) -> transaction + .run("CREATE (a:Greeting) SET a.message = $message RETURN a.message + ', from node ' + id(a)", + Values.parameters("message", message)) + .single().get(0).asString()); + } + } + // @fold:off + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/neo4j/repositories/City.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/neo4j/repositories/City.java new file mode 100644 index 000000000000..ec5740768be4 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/neo4j/repositories/City.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.nosql.neo4j.repositories; + +public class City { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/neo4j/repositories/CityRepository.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/neo4j/repositories/CityRepository.java new file mode 100644 index 000000000000..8c58ae4888dd --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/neo4j/repositories/CityRepository.java @@ -0,0 +1,27 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.nosql.neo4j.repositories; + +import java.util.Optional; + +import org.springframework.data.neo4j.repository.Neo4jRepository; + +public interface CityRepository extends Neo4jRepository { + + Optional findOneByNameAndState(String name, String state); + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/neo4j/repositories/MyNeo4jConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/neo4j/repositories/MyNeo4jConfiguration.java new file mode 100644 index 000000000000..f9ee179f5777 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/neo4j/repositories/MyNeo4jConfiguration.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.nosql.neo4j.repositories; + +import org.neo4j.driver.Driver; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; +import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager; + +@Configuration(proxyBeanMethods = false) +public class MyNeo4jConfiguration { + + @Bean + public ReactiveNeo4jTransactionManager reactiveTransactionManager(Driver driver, + ReactiveDatabaseSelectionProvider databaseNameProvider) { + return new ReactiveNeo4jTransactionManager(driver, databaseNameProvider); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/redis/connecting/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/redis/connecting/MyBean.java new file mode 100644 index 000000000000..0e1506dd4908 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/redis/connecting/MyBean.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.nosql.redis.connecting; + +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + private final StringRedisTemplate template; + + public MyBean(StringRedisTemplate template) { + this.template = template; + } + + // @fold:on // ... + public Boolean someMethod() { + return this.template.hasKey("spring"); + } + // @fold:off + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/solr/connecting/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/solr/connecting/MyBean.java new file mode 100644 index 000000000000..8bdbd28c7db6 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/nosql/solr/connecting/MyBean.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.nosql.solr.connecting; + +import java.io.IOException; + +import org.apache.solr.client.solrj.SolrClient; +import org.apache.solr.client.solrj.SolrServerException; +import org.apache.solr.client.solrj.response.SolrPingResponse; + +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + private final SolrClient solr; + + public MyBean(SolrClient solr) { + this.solr = solr; + } + + // @fold:on // ... + public SolrPingResponse someMethod() throws SolrServerException, IOException { + return this.solr.ping("users"); + } + // @fold:off + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/profiles/ProductionConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/profiles/ProductionConfiguration.java new file mode 100644 index 000000000000..d21a37612c71 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/profiles/ProductionConfiguration.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.profiles; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +@Configuration(proxyBeanMethods = false) +@Profile("production") +public class ProductionConfiguration { + + // ... + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/quartz/MySampleJob.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/quartz/MySampleJob.java new file mode 100644 index 000000000000..9a74343c7b2d --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/quartz/MySampleJob.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.quartz; + +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; + +import org.springframework.scheduling.quartz.QuartzJobBean; + +public class MySampleJob extends QuartzJobBean { + + // @fold:on // fields ... + private MyService myService; + + private String name; + + // @fold:off + + // Inject "MyService" bean + public void setMyService(MyService myService) { + this.myService = myService; + } + + // Inject the "name" job data property + public void setName(String name) { + this.name = name; + } + + @Override + protected void executeInternal(JobExecutionContext context) throws JobExecutionException { + this.myService.someMethod(context.getFireTime(), this.name); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/quartz/MyService.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/quartz/MyService.java new file mode 100644 index 000000000000..afb1cc893396 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/quartz/MyService.java @@ -0,0 +1,26 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.quartz; + +import java.util.Date; + +class MyService { + + void someMethod(Date date, String name) { + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/resttemplate/Details.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/resttemplate/Details.java new file mode 100644 index 000000000000..e7872ed67f5a --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/resttemplate/Details.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.resttemplate; + +class Details { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/resttemplate/MyService.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/resttemplate/MyService.java new file mode 100644 index 000000000000..8ddf50b19f26 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/resttemplate/MyService.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.resttemplate; + +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +public class MyService { + + private final RestTemplate restTemplate; + + public MyService(RestTemplateBuilder restTemplateBuilder) { + this.restTemplate = restTemplateBuilder.build(); + } + + public Details someRestCall(String name) { + return this.restTemplate.getForObject("/{name}/details", Details.class, name); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/resttemplate/customization/MyRestTemplateBuilderConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/resttemplate/customization/MyRestTemplateBuilderConfiguration.java new file mode 100644 index 000000000000..95f0dc7fc4d8 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/resttemplate/customization/MyRestTemplateBuilderConfiguration.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.resttemplate.customization; + +import java.time.Duration; + +import org.springframework.boot.autoconfigure.web.client.RestTemplateBuilderConfigurer; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class MyRestTemplateBuilderConfiguration { + + @Bean + public RestTemplateBuilder restTemplateBuilder(RestTemplateBuilderConfigurer configurer) { + return configurer.configure(new RestTemplateBuilder()).setConnectTimeout(Duration.ofSeconds(5)) + .setReadTimeout(Duration.ofSeconds(2)); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/resttemplate/customization/MyRestTemplateCustomizer.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/resttemplate/customization/MyRestTemplateCustomizer.java new file mode 100644 index 000000000000..0c09950c0caf --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/resttemplate/customization/MyRestTemplateCustomizer.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.resttemplate.customization; + +import org.apache.http.HttpException; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.client.HttpClient; +import org.apache.http.conn.routing.HttpRoutePlanner; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.DefaultProxyRoutePlanner; +import org.apache.http.protocol.HttpContext; + +import org.springframework.boot.web.client.RestTemplateCustomizer; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; + +public class MyRestTemplateCustomizer implements RestTemplateCustomizer { + + @Override + public void customize(RestTemplate restTemplate) { + HttpRoutePlanner routePlanner = new CustomRoutePlanner(new HttpHost("proxy.example.com")); + HttpClient httpClient = HttpClientBuilder.create().setRoutePlanner(routePlanner).build(); + restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory(httpClient)); + } + + static class CustomRoutePlanner extends DefaultProxyRoutePlanner { + + CustomRoutePlanner(HttpHost proxy) { + super(proxy); + } + + @Override + public HttpHost determineProxy(HttpHost target, HttpRequest request, HttpContext context) throws HttpException { + if (target.getHostName().equals("192.168.0.5")) { + return null; + } + return super.determineProxy(target, request, context); + } + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/rsocket/requester/MyService.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/rsocket/requester/MyService.java new file mode 100644 index 000000000000..5ce67277231b --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/rsocket/requester/MyService.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.rsocket.requester; + +import reactor.core.publisher.Mono; + +import org.springframework.messaging.rsocket.RSocketRequester; +import org.springframework.stereotype.Service; + +@Service +public class MyService { + + private final RSocketRequester rsocketRequester; + + public MyService(RSocketRequester.Builder rsocketRequesterBuilder) { + this.rsocketRequester = rsocketRequesterBuilder.tcp("example.org", 9898); + } + + public Mono someRSocketCall(String name) { + return this.rsocketRequester.route("user").data(name).retrieveMono(User.class); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/rsocket/requester/User.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/rsocket/requester/User.java new file mode 100644 index 000000000000..e461d995ed9e --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/rsocket/requester/User.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.rsocket.requester; + +class User { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/security/oauth2/client/MyOAuthClientConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/security/oauth2/client/MyOAuthClientConfiguration.java new file mode 100644 index 000000000000..6e15db5f2e05 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/security/oauth2/client/MyOAuthClientConfiguration.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.security.oauth2.client; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration(proxyBeanMethods = false) +public class MyOAuthClientConfiguration { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http.authorizeRequests().anyRequest().authenticated(); + http.oauth2Login().redirectionEndpoint().baseUri("custom-callback"); + return http.build(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/security/springwebflux/MyWebFluxSecurityConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/security/springwebflux/MyWebFluxSecurityConfiguration.java new file mode 100644 index 000000000000..05dca638ec2b --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/security/springwebflux/MyWebFluxSecurityConfiguration.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.security.springwebflux; + +import org.springframework.boot.autoconfigure.security.reactive.PathRequest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.web.server.SecurityWebFilterChain; + +@Configuration(proxyBeanMethods = false) +public class MyWebFluxSecurityConfiguration { + + @Bean + public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http.authorizeExchange((spec) -> { + spec.matchers(PathRequest.toStaticResources().atCommonLocations()).permitAll(); + spec.pathMatchers("/foo", "/bar").authenticated(); + }); + http.formLogin(); + return http.build(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/springapplication/MyApplication.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/springapplication/MyApplication.java new file mode 100644 index 000000000000..24f018ad490a --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/springapplication/MyApplication.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.springapplication; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class MyApplication { + + public static void main(String[] args) { + SpringApplication.run(MyApplication.class, args); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/springapplication/applicationarguments/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/springapplication/applicationarguments/MyBean.java new file mode 100644 index 000000000000..a1f5f99a6774 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/springapplication/applicationarguments/MyBean.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.springapplication.applicationarguments; + +import java.util.List; + +import org.springframework.boot.ApplicationArguments; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + public MyBean(ApplicationArguments args) { + boolean debug = args.containsOption("debug"); + List files = args.getNonOptionArgs(); + if (debug) { + System.out.println(files); + } + // if run with "--debug logfile.txt" prints ["logfile.txt"] + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/springapplication/applicationavailability/managing/CacheCompletelyBrokenException.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/springapplication/applicationavailability/managing/CacheCompletelyBrokenException.java new file mode 100644 index 000000000000..4b0dc860334e --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/springapplication/applicationavailability/managing/CacheCompletelyBrokenException.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.springapplication.applicationavailability.managing; + +class CacheCompletelyBrokenException extends RuntimeException { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/springapplication/applicationavailability/managing/MyLocalCacheVerifier.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/springapplication/applicationavailability/managing/MyLocalCacheVerifier.java new file mode 100644 index 000000000000..7debf1c67a6d --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/springapplication/applicationavailability/managing/MyLocalCacheVerifier.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.springapplication.applicationavailability.managing; + +import org.springframework.boot.availability.AvailabilityChangeEvent; +import org.springframework.boot.availability.LivenessState; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +@Component +public class MyLocalCacheVerifier { + + private final ApplicationEventPublisher eventPublisher; + + public MyLocalCacheVerifier(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + + public void checkLocalCache() { + try { + // ... + } + catch (CacheCompletelyBrokenException ex) { + AvailabilityChangeEvent.publish(this.eventPublisher, ex, LivenessState.BROKEN); + } + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/springapplication/applicationavailability/managing/MyReadinessStateExporter.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/springapplication/applicationavailability/managing/MyReadinessStateExporter.java new file mode 100644 index 000000000000..db7127eec29c --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/springapplication/applicationavailability/managing/MyReadinessStateExporter.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.springapplication.applicationavailability.managing; + +import org.springframework.boot.availability.AvailabilityChangeEvent; +import org.springframework.boot.availability.ReadinessState; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +@Component +public class MyReadinessStateExporter { + + @EventListener + public void onStateChange(AvailabilityChangeEvent event) { + switch (event.getState()) { + case ACCEPTING_TRAFFIC: + // create file /tmp/healthy + break; + case REFUSING_TRAFFIC: + // remove file /tmp/healthy + break; + } + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/springapplication/applicationexit/MyApplication.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/springapplication/applicationexit/MyApplication.java new file mode 100644 index 000000000000..64951ba2a447 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/springapplication/applicationexit/MyApplication.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.springapplication.applicationexit; + +import org.springframework.boot.ExitCodeGenerator; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +@SpringBootApplication +public class MyApplication { + + @Bean + public ExitCodeGenerator exitCodeGenerator() { + return () -> 42; + } + + public static void main(String[] args) { + System.exit(SpringApplication.exit(SpringApplication.run(MyApplication.class, args))); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/springapplication/commandlinerunner/MyCommandLineRunner.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/springapplication/commandlinerunner/MyCommandLineRunner.java new file mode 100644 index 000000000000..953d69dcc825 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/springapplication/commandlinerunner/MyCommandLineRunner.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.springapplication.commandlinerunner; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +@Component +public class MyCommandLineRunner implements CommandLineRunner { + + @Override + public void run(String... args) { + // Do something... + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/springapplication/customizingspringapplication/MyApplication.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/springapplication/customizingspringapplication/MyApplication.java new file mode 100644 index 000000000000..198966f711c5 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/springapplication/customizingspringapplication/MyApplication.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.springapplication.customizingspringapplication; + +import org.springframework.boot.Banner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class MyApplication { + + public static void main(String[] args) { + SpringApplication application = new SpringApplication(MyApplication.class); + application.setBannerMode(Banner.Mode.OFF); + application.run(args); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/springapplication/fluentbuilderapi/MyApplication.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/springapplication/fluentbuilderapi/MyApplication.java new file mode 100644 index 000000000000..0ef7f8ed4266 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/springapplication/fluentbuilderapi/MyApplication.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.springapplication.fluentbuilderapi; + +import org.springframework.boot.Banner; +import org.springframework.boot.builder.SpringApplicationBuilder; + +public class MyApplication { + + public void hierarchyWithDisabledBanner(String[] args) { + // @formatter:off + // tag::code[] + new SpringApplicationBuilder() + .sources(Parent.class) + .child(Application.class) + .bannerMode(Banner.Mode.OFF) + .run(args); + // end::code[] + // @formatter:on + } + + static class Parent { + + } + + static class Application { + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/springapplication/startuptracking/MyApplication.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/springapplication/startuptracking/MyApplication.java new file mode 100644 index 000000000000..a5dc14cbe5ba --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/springapplication/startuptracking/MyApplication.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.springapplication.startuptracking; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup; + +@SpringBootApplication +public class MyApplication { + + public static void main(String[] args) { + SpringApplication application = new SpringApplication(MyApplication.class); + application.setApplicationStartup(new BufferingApplicationStartup(2048)); + application.run(args); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/sql/h2webconsole/springsecurity/DevProfileSecurityConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/sql/h2webconsole/springsecurity/DevProfileSecurityConfiguration.java new file mode 100644 index 000000000000..88eb90d875c1 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/sql/h2webconsole/springsecurity/DevProfileSecurityConfiguration.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.sql.h2webconsole.springsecurity; + +import org.springframework.boot.autoconfigure.security.servlet.PathRequest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Profile("dev") +@Configuration(proxyBeanMethods = false) +public class DevProfileSecurityConfiguration { + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + SecurityFilterChain h2ConsoleSecurityFilterChain(HttpSecurity http) throws Exception { + // @formatter:off + return http.requestMatcher(PathRequest.toH2Console()) + // ... configuration for authorization + .csrf().disable() + .headers().frameOptions().sameOrigin().and() + .build(); + // @formatter:on + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/sql/jdbctemplate/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/sql/jdbctemplate/MyBean.java new file mode 100644 index 000000000000..272d42d56b93 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/sql/jdbctemplate/MyBean.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.sql.jdbctemplate; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + private final JdbcTemplate jdbcTemplate; + + public MyBean(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public void doSomething() { + /* @chomp:line this.jdbcTemplate ... */ this.jdbcTemplate.execute("delete from customer"); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/sql/jooq/dslcontext/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/sql/jooq/dslcontext/MyBean.java new file mode 100644 index 000000000000..7e4eb66cd02f --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/sql/jooq/dslcontext/MyBean.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.sql.jooq.dslcontext; + +import java.util.GregorianCalendar; +import java.util.List; + +import org.jooq.DSLContext; + +import org.springframework.stereotype.Component; + +import static org.springframework.boot.docs.features.sql.jooq.dslcontext.Tables.AUTHOR; + +@Component +public class MyBean { + + private final DSLContext create; + + public MyBean(DSLContext dslContext) { + this.create = dslContext; + } + + // tag::method[] + public List authorsBornAfter1980() { + // @formatter:off + return this.create.selectFrom(AUTHOR) + .where(AUTHOR.DATE_OF_BIRTH.greaterThan(new GregorianCalendar(1980, 0, 1))) + .fetch(AUTHOR.DATE_OF_BIRTH); + // @formatter:on + } // end::method[] + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/sql/jooq/dslcontext/Tables.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/sql/jooq/dslcontext/Tables.java new file mode 100644 index 000000000000..1e00193a3c17 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/sql/jooq/dslcontext/Tables.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.sql.jooq.dslcontext; + +import java.util.GregorianCalendar; + +import org.jooq.Name; +import org.jooq.Table; +import org.jooq.TableField; +import org.jooq.impl.TableImpl; +import org.jooq.impl.TableRecordImpl; + +abstract class Tables { + + static final TAuthor AUTHOR = null; + + abstract class TAuthor extends TableImpl { + + TAuthor(Name name) { + super(name); + } + + public final TableField DATE_OF_BIRTH = null; + + } + + abstract class TAuthorRecord extends TableRecordImpl { + + TAuthorRecord(Table table) { + super(table); + } + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/sql/jpaandspringdata/entityclasses/City.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/sql/jpaandspringdata/entityclasses/City.java new file mode 100644 index 000000000000..0936e9151778 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/sql/jpaandspringdata/entityclasses/City.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.sql.jpaandspringdata.entityclasses; + +import java.io.Serializable; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; + +@Entity +public class City implements Serializable { + + @Id + @GeneratedValue + private Long id; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String state; + + // ... additional members, often include @OneToMany mappings + + protected City() { + // no-args constructor required by JPA spec + // this one is protected since it shouldn't be used directly + } + + public City(String name, String state) { + this.name = name; + this.state = state; + } + + public String getName() { + return this.name; + } + + public String getState() { + return this.state; + } + + // ... etc + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/sql/jpaandspringdata/repositories/CityRepository.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/sql/jpaandspringdata/repositories/CityRepository.java new file mode 100644 index 000000000000..aa5816da6967 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/sql/jpaandspringdata/repositories/CityRepository.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.sql.jpaandspringdata.repositories; + +import org.springframework.boot.docs.features.sql.jpaandspringdata.entityclasses.City; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.repository.Repository; + +public interface CityRepository extends Repository { + + Page findAll(Pageable pageable); + + City findByNameAndStateAllIgnoringCase(String name, String state); + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/sql/r2dbc/MyPostgresR2dbcConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/sql/r2dbc/MyPostgresR2dbcConfiguration.java new file mode 100644 index 000000000000..5e64e08cc812 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/sql/r2dbc/MyPostgresR2dbcConfiguration.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.sql.r2dbc; + +import java.util.HashMap; +import java.util.Map; + +import io.r2dbc.postgresql.PostgresqlConnectionFactoryProvider; + +import org.springframework.boot.autoconfigure.r2dbc.ConnectionFactoryOptionsBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class MyPostgresR2dbcConfiguration { + + @Bean + public ConnectionFactoryOptionsBuilderCustomizer postgresCustomizer() { + Map options = new HashMap<>(); + options.put("lock_timeout", "30s"); + options.put("statement_timeout", "60s"); + return (builder) -> builder.option(PostgresqlConnectionFactoryProvider.OPTIONS, options); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/sql/r2dbc/MyR2dbcConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/sql/r2dbc/MyR2dbcConfiguration.java new file mode 100644 index 000000000000..09483ad34050 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/sql/r2dbc/MyR2dbcConfiguration.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.sql.r2dbc; + +import io.r2dbc.spi.ConnectionFactoryOptions; + +import org.springframework.boot.autoconfigure.r2dbc.ConnectionFactoryOptionsBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class MyR2dbcConfiguration { + + @Bean + public ConnectionFactoryOptionsBuilderCustomizer connectionFactoryPortCustomizer() { + return (builder) -> builder.option(ConnectionFactoryOptions.PORT, 5432); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/sql/r2dbc/repositories/City.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/sql/r2dbc/repositories/City.java new file mode 100644 index 000000000000..0219fda90264 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/sql/r2dbc/repositories/City.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.sql.r2dbc.repositories; + +public class City { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/sql/r2dbc/repositories/CityRepository.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/sql/r2dbc/repositories/CityRepository.java new file mode 100644 index 000000000000..801d128f41b1 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/sql/r2dbc/repositories/CityRepository.java @@ -0,0 +1,27 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.sql.r2dbc.repositories; + +import reactor.core.publisher.Mono; + +import org.springframework.data.repository.Repository; + +public interface CityRepository extends Repository { + + Mono findByNameAndStateAllIgnoringCase(String name, String state); + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/sql/r2dbc/usingdatabaseclient/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/sql/r2dbc/usingdatabaseclient/MyBean.java new file mode 100644 index 000000000000..0456a2018aab --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/sql/r2dbc/usingdatabaseclient/MyBean.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.sql.r2dbc.usingdatabaseclient; + +import java.util.Map; + +import reactor.core.publisher.Flux; + +import org.springframework.r2dbc.core.DatabaseClient; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + private final DatabaseClient databaseClient; + + public MyBean(DatabaseClient databaseClient) { + this.databaseClient = databaseClient; + } + + // @fold:on // ... + public Flux> someMethod() { + return this.databaseClient.sql("select * from user").fetch().all(); + } + // @fold:off + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/additionalautoconfigurationandslicing/MyJdbcTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/additionalautoconfigurationandslicing/MyJdbcTests.java new file mode 100644 index 000000000000..b2f9aadc368e --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/additionalautoconfigurationandslicing/MyJdbcTests.java @@ -0,0 +1,27 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.additionalautoconfigurationandslicing; + +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; + +@JdbcTest +@ImportAutoConfiguration(IntegrationAutoConfiguration.class) +class MyJdbcTests { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredjdbc/MyTransactionalTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredjdbc/MyTransactionalTests.java new file mode 100644 index 000000000000..944e1ca34c60 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredjdbc/MyTransactionalTests.java @@ -0,0 +1,27 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredjdbc; + +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +@JdbcTest +@Transactional(propagation = Propagation.NOT_SUPPORTED) +class MyTransactionalTests { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredjooq/MyJooqTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredjooq/MyJooqTests.java new file mode 100644 index 000000000000..0f06dbbacb08 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredjooq/MyJooqTests.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredjooq; + +import org.jooq.DSLContext; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jooq.JooqTest; + +@JooqTest +class MyJooqTests { + + @Autowired + @SuppressWarnings("unused") + private DSLContext dslContext; + + // ... + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredrestclient/MyRestClientTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredrestclient/MyRestClientTests.java new file mode 100644 index 000000000000..101250ccce17 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredrestclient/MyRestClientTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredrestclient; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.client.RestClientTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.client.MockRestServiceServer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +@RestClientTest(RemoteVehicleDetailsService.class) +class MyRestClientTests { + + @Autowired + private RemoteVehicleDetailsService service; + + @Autowired + private MockRestServiceServer server; + + @Test + void getVehicleDetailsWhenResultIsSuccessShouldReturnDetails() throws Exception { + this.server.expect(requestTo("/greet/details")).andRespond(withSuccess("hello", MediaType.TEXT_PLAIN)); + String greeting = this.service.callRestService(); + assertThat(greeting).isEqualTo("hello"); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredrestclient/RemoteVehicleDetailsService.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredrestclient/RemoteVehicleDetailsService.java new file mode 100644 index 000000000000..6bfd672ac480 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredrestclient/RemoteVehicleDetailsService.java @@ -0,0 +1,25 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredrestclient; + +class RemoteVehicleDetailsService { + + String callRestService() { + return "hello"; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdatacassandra/MyDataCassandraTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdatacassandra/MyDataCassandraTests.java new file mode 100644 index 000000000000..639686461cf9 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdatacassandra/MyDataCassandraTests.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredspringdatacassandra; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.data.cassandra.DataCassandraTest; + +@DataCassandraTest +class MyDataCassandraTests { + + @Autowired + @SuppressWarnings("unused") + private SomeRepository repository; + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdatacassandra/SomeRepository.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdatacassandra/SomeRepository.java new file mode 100644 index 000000000000..7cf8ce720303 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdatacassandra/SomeRepository.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredspringdatacassandra; + +interface SomeRepository { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdatajpa/MyNonTransactionalTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdatajpa/MyNonTransactionalTests.java new file mode 100644 index 000000000000..90ba61f25529 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdatajpa/MyNonTransactionalTests.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredspringdatajpa; + +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +@DataJpaTest +@Transactional(propagation = Propagation.NOT_SUPPORTED) +class MyNonTransactionalTests { + + // ... + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdatajpa/withdb/MyRepositoryTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdatajpa/withdb/MyRepositoryTests.java new file mode 100644 index 000000000000..16aca2b2822e --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdatajpa/withdb/MyRepositoryTests.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredspringdatajpa.withdb; + +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +@DataJpaTest +@AutoConfigureTestDatabase(replace = Replace.NONE) +class MyRepositoryTests { + + // ... + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdatajpa/withoutdb/MyRepositoryTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdatajpa/withoutdb/MyRepositoryTests.java new file mode 100644 index 000000000000..b122f1ac087f --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdatajpa/withoutdb/MyRepositoryTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredspringdatajpa.withoutdb; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +class MyRepositoryTests { + + @Autowired + private TestEntityManager entityManager; + + @Autowired + private UserRepository repository; + + @Test + void testExample() throws Exception { + this.entityManager.persist(new User("sboot", "1234")); + User user = this.repository.findByUsername("sboot"); + assertThat(user.getUsername()).isEqualTo("sboot"); + assertThat(user.getEmployeeNumber()).isEqualTo("1234"); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdatajpa/withoutdb/User.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdatajpa/withoutdb/User.java new file mode 100644 index 000000000000..c3656b8129cf --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdatajpa/withoutdb/User.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredspringdatajpa.withoutdb; + +class User { + + User(String username, String employeeNumber) { + } + + String getEmployeeNumber() { + return null; + } + + String getUsername() { + return null; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdatajpa/withoutdb/UserRepository.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdatajpa/withoutdb/UserRepository.java new file mode 100644 index 000000000000..5b2e6963be6c --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdatajpa/withoutdb/UserRepository.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredspringdatajpa.withoutdb; + +interface UserRepository { + + User findByUsername(String username); + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdataldap/inmemory/MyDataLdapTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdataldap/inmemory/MyDataLdapTests.java new file mode 100644 index 000000000000..cb95fd2df009 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdataldap/inmemory/MyDataLdapTests.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredspringdataldap.inmemory; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.data.ldap.DataLdapTest; +import org.springframework.ldap.core.LdapTemplate; + +@DataLdapTest +class MyDataLdapTests { + + @Autowired + @SuppressWarnings("unused") + private LdapTemplate ldapTemplate; + + // ... + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdataldap/server/MyDataLdapTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdataldap/server/MyDataLdapTests.java new file mode 100644 index 000000000000..81a21ddec4e9 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdataldap/server/MyDataLdapTests.java @@ -0,0 +1,27 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredspringdataldap.server; + +import org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapAutoConfiguration; +import org.springframework.boot.test.autoconfigure.data.ldap.DataLdapTest; + +@DataLdapTest(excludeAutoConfiguration = EmbeddedLdapAutoConfiguration.class) +class MyDataLdapTests { + + // ... + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdatamongodb/withdb/MyDataMongoDbTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdatamongodb/withdb/MyDataMongoDbTests.java new file mode 100644 index 000000000000..e75413153156 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdatamongodb/withdb/MyDataMongoDbTests.java @@ -0,0 +1,27 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredspringdatamongodb.withdb; + +import org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration; +import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest; + +@DataMongoTest(excludeAutoConfiguration = EmbeddedMongoAutoConfiguration.class) +class MyDataMongoDbTests { + + // ... + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdatamongodb/withoutdb/MyDataMongoDbTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdatamongodb/withoutdb/MyDataMongoDbTests.java new file mode 100644 index 000000000000..e342f484d326 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdatamongodb/withoutdb/MyDataMongoDbTests.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredspringdatamongodb.withoutdb; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest; +import org.springframework.data.mongodb.core.MongoTemplate; + +@DataMongoTest +class MyDataMongoDbTests { + + @Autowired + @SuppressWarnings("unused") + private MongoTemplate mongoTemplate; + + // ... + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdataneo4j/nopropagation/MyDataNeo4jTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdataneo4j/nopropagation/MyDataNeo4jTests.java new file mode 100644 index 000000000000..ff923a9e83ce --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdataneo4j/nopropagation/MyDataNeo4jTests.java @@ -0,0 +1,27 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredspringdataneo4j.nopropagation; + +import org.springframework.boot.test.autoconfigure.data.neo4j.DataNeo4jTest; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +@DataNeo4jTest +@Transactional(propagation = Propagation.NOT_SUPPORTED) +class MyDataNeo4jTests { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdataneo4j/propagation/MyDataNeo4jTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdataneo4j/propagation/MyDataNeo4jTests.java new file mode 100644 index 000000000000..f7a4cfe592c2 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdataneo4j/propagation/MyDataNeo4jTests.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredspringdataneo4j.propagation; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.data.neo4j.DataNeo4jTest; + +@DataNeo4jTest +class MyDataNeo4jTests { + + @Autowired + @SuppressWarnings("unused") + private SomeRepository repository; + + // ... + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdataneo4j/propagation/SomeRepository.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdataneo4j/propagation/SomeRepository.java new file mode 100644 index 000000000000..b905dbe206a5 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdataneo4j/propagation/SomeRepository.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredspringdataneo4j.propagation; + +interface SomeRepository { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdataredis/MyDataRedisTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdataredis/MyDataRedisTests.java new file mode 100644 index 000000000000..d5a58d5a1789 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdataredis/MyDataRedisTests.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredspringdataredis; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.data.redis.DataRedisTest; + +@DataRedisTest +class MyDataRedisTests { + + @Autowired + @SuppressWarnings("unused") + private SomeRepository repository; + + // ... + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdataredis/SomeRepository.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdataredis/SomeRepository.java new file mode 100644 index 000000000000..1a792e60dd71 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringdataredis/SomeRepository.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredspringdataredis; + +interface SomeRepository { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringrestdocs/withmockmvc/MyRestDocsConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringrestdocs/withmockmvc/MyRestDocsConfiguration.java new file mode 100644 index 000000000000..149dc47d4d94 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringrestdocs/withmockmvc/MyRestDocsConfiguration.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredspringrestdocs.withmockmvc; + +import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentationConfigurer; +import org.springframework.restdocs.templates.TemplateFormats; + +@TestConfiguration(proxyBeanMethods = false) +public class MyRestDocsConfiguration implements RestDocsMockMvcConfigurationCustomizer { + + @Override + public void customize(MockMvcRestDocumentationConfigurer configurer) { + configurer.snippets().withTemplateFormat(TemplateFormats.markdown()); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringrestdocs/withmockmvc/MyResultHandlerConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringrestdocs/withmockmvc/MyResultHandlerConfiguration.java new file mode 100644 index 000000000000..51a9d31f0716 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringrestdocs/withmockmvc/MyResultHandlerConfiguration.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredspringrestdocs.withmockmvc; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; + +@TestConfiguration(proxyBeanMethods = false) +public class MyResultHandlerConfiguration { + + @Bean + public RestDocumentationResultHandler restDocumentation() { + return MockMvcRestDocumentation.document("{method-name}"); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringrestdocs/withmockmvc/MyUserDocumentationTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringrestdocs/withmockmvc/MyUserDocumentationTests.java new file mode 100644 index 000000000000..5e26ca555c34 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringrestdocs/withmockmvc/MyUserDocumentationTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredspringrestdocs.withmockmvc; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(UserController.class) +@AutoConfigureRestDocs +class MyUserDocumentationTests { + + @Autowired + private MockMvc mvc; + + @Test + void listUsers() throws Exception { + // @formatter:off + this.mvc.perform(get("/users").accept(MediaType.TEXT_PLAIN)) + .andExpect(status().isOk()) + .andDo(document("list-users")); + // @formatter:on + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringrestdocs/withmockmvc/UserController.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringrestdocs/withmockmvc/UserController.java new file mode 100644 index 000000000000..f920a838057e --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringrestdocs/withmockmvc/UserController.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredspringrestdocs.withmockmvc; + +class UserController { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringrestdocs/withrestassured/MyRestDocsConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringrestdocs/withrestassured/MyRestDocsConfiguration.java new file mode 100644 index 000000000000..f83b77190b80 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringrestdocs/withrestassured/MyRestDocsConfiguration.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredspringrestdocs.withrestassured; + +import org.springframework.boot.test.autoconfigure.restdocs.RestDocsRestAssuredConfigurationCustomizer; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.restdocs.restassured3.RestAssuredRestDocumentationConfigurer; +import org.springframework.restdocs.templates.TemplateFormats; + +@TestConfiguration(proxyBeanMethods = false) +public class MyRestDocsConfiguration implements RestDocsRestAssuredConfigurationCustomizer { + + @Override + public void customize(RestAssuredRestDocumentationConfigurer configurer) { + configurer.snippets().withTemplateFormat(TemplateFormats.markdown()); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringrestdocs/withrestassured/MyUserDocumentationTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringrestdocs/withrestassured/MyUserDocumentationTests.java new file mode 100644 index 000000000000..9f969cf180f8 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringrestdocs/withrestassured/MyUserDocumentationTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredspringrestdocs.withrestassured; + +import io.restassured.specification.RequestSpecification; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.web.server.LocalServerPort; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.is; +import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@AutoConfigureRestDocs +class MyUserDocumentationTests { + + @Test + void listUsers(@Autowired RequestSpecification documentationSpec, @LocalServerPort int port) { + // @formatter:off + given(documentationSpec) + .filter(document("list-users")) + .when() + .port(port) + .get("/") + .then().assertThat() + .statusCode(is(200)); + // @formatter:on + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringrestdocs/withwebtestclient/MyRestDocsConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringrestdocs/withwebtestclient/MyRestDocsConfiguration.java new file mode 100644 index 000000000000..b1d7694ceb39 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringrestdocs/withwebtestclient/MyRestDocsConfiguration.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredspringrestdocs.withwebtestclient; + +import org.springframework.boot.test.autoconfigure.restdocs.RestDocsWebTestClientConfigurationCustomizer; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentationConfigurer; + +@TestConfiguration(proxyBeanMethods = false) +public class MyRestDocsConfiguration implements RestDocsWebTestClientConfigurationCustomizer { + + @Override + public void customize(WebTestClientRestDocumentationConfigurer configurer) { + configurer.snippets().withEncoding("UTF-8"); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringrestdocs/withwebtestclient/MyUsersDocumentationTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringrestdocs/withwebtestclient/MyUsersDocumentationTests.java new file mode 100644 index 000000000000..a83b763b9403 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringrestdocs/withwebtestclient/MyUsersDocumentationTests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredspringrestdocs.withwebtestclient; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document; + +@WebFluxTest +@AutoConfigureRestDocs +class MyUsersDocumentationTests { + + @Autowired + private WebTestClient webTestClient; + + @Test + void listUsers() { + // @formatter:off + this.webTestClient + .get().uri("/") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .consumeWith(document("list-users")); + // @formatter:on + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringrestdocs/withwebtestclient/MyWebTestClientBuilderCustomizerConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringrestdocs/withwebtestclient/MyWebTestClientBuilderCustomizerConfiguration.java new file mode 100644 index 000000000000..8713104daacd --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredspringrestdocs/withwebtestclient/MyWebTestClientBuilderCustomizerConfiguration.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredspringrestdocs.withwebtestclient; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.web.reactive.server.WebTestClientBuilderCustomizer; +import org.springframework.context.annotation.Bean; + +import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document; + +@TestConfiguration(proxyBeanMethods = false) +public class MyWebTestClientBuilderCustomizerConfiguration { + + @Bean + public WebTestClientBuilderCustomizer restDocumentation() { + return (builder) -> builder.entityExchangeResultConsumer(document("{method-name}")); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredwebservices/MyWebServiceClientTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredwebservices/MyWebServiceClientTests.java new file mode 100644 index 000000000000..ede71492df0f --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredwebservices/MyWebServiceClientTests.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredwebservices; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.webservices.client.WebServiceClientTest; +import org.springframework.ws.test.client.MockWebServiceServer; +import org.springframework.xml.transform.StringSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.ws.test.client.RequestMatchers.payload; +import static org.springframework.ws.test.client.ResponseCreators.withPayload; + +@WebServiceClientTest(SomeWebService.class) +class MyWebServiceClientTests { + + @Autowired + private MockWebServiceServer server; + + @Autowired + private SomeWebService someWebService; + + @Test + void mockServerCall() { + // @formatter:off + this.server + .expect(payload(new StringSource(""))) + .andRespond(withPayload(new StringSource("200"))); + assertThat(this.someWebService.test()) + .extracting(Response::getStatus) + .isEqualTo(200); + // @formatter:on + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredwebservices/Request.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredwebservices/Request.java new file mode 100644 index 000000000000..f497c2bb7171 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredwebservices/Request.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredwebservices; + +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement(name = "request") +class Request { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredwebservices/Response.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredwebservices/Response.java new file mode 100644 index 000000000000..4d20f7f4839f --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredwebservices/Response.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredwebservices; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement(name = "response") +@XmlAccessorType(XmlAccessType.FIELD) +class Response { + + private int status; + + int getStatus() { + return this.status; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredwebservices/SomeWebService.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredwebservices/SomeWebService.java new file mode 100644 index 000000000000..fda489b2536a --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredwebservices/SomeWebService.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredwebservices; + +import org.springframework.boot.webservices.client.WebServiceTemplateBuilder; +import org.springframework.stereotype.Service; +import org.springframework.ws.client.core.WebServiceTemplate; + +@Service +public class SomeWebService { + + private final WebServiceTemplate webServiceTemplate; + + public SomeWebService(WebServiceTemplateBuilder builder) { + this.webServiceTemplate = builder.build(); + } + + public Response test() { + return (Response) this.webServiceTemplate.marshalSendAndReceive("https://example.com", new Request()); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/detectingwebapptype/MyWebFluxTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/detectingwebapptype/MyWebFluxTests.java new file mode 100644 index 000000000000..6aebffbd26fe --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/detectingwebapptype/MyWebFluxTests.java @@ -0,0 +1,26 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.detectingwebapptype; + +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest(properties = "spring.main.web-application-type=reactive") +class MyWebFluxTests { + + // ... + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/excludingconfiguration/MyTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/excludingconfiguration/MyTests.java new file mode 100644 index 000000000000..34ddeb8edefa --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/excludingconfiguration/MyTests.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.excludingconfiguration; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; + +@SpringBootTest +@Import(MyTestsConfiguration.class) +class MyTests { + + @Test + void exampleTest() { + // ... + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/excludingconfiguration/MyTestsConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/excludingconfiguration/MyTestsConfiguration.java new file mode 100644 index 000000000000..be779949850c --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/excludingconfiguration/MyTestsConfiguration.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.excludingconfiguration; + +class MyTestsConfiguration { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/jmx/MyJmxTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/jmx/MyJmxTests.java new file mode 100644 index 000000000000..ba544a77c2a6 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/jmx/MyJmxTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.jmx; + +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(SpringExtension.class) +@SpringBootTest(properties = "spring.jmx.enabled=true") +@DirtiesContext +class MyJmxTests { + + @Autowired + private MBeanServer mBeanServer; + + @Test + void exampleTest() throws MalformedObjectNameException { + assertThat(this.mBeanServer.getDomains()).contains("java.lang"); + // ... + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/jmx/SampleApp.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/jmx/SampleApp.java new file mode 100644 index 000000000000..e6d101f0c226 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/jmx/SampleApp.java @@ -0,0 +1,27 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.jmx; + +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration; + +@SpringBootConfiguration +@ImportAutoConfiguration(JmxAutoConfiguration.class) +public class SampleApp { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/jsontests/MyJsonAssertJTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/jsontests/MyJsonAssertJTests.java new file mode 100644 index 000000000000..3f108b520525 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/jsontests/MyJsonAssertJTests.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.jsontests; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.json.JacksonTester; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.within; + +@JsonTest +class MyJsonAssertJTests { + + @Autowired + private JacksonTester json; + + // tag::code[] + @Test + void someTest() throws Exception { + SomeObject value = new SomeObject(0.152f); + assertThat(this.json.write(value)).extractingJsonPathNumberValue("@.test.numberValue") + .satisfies((number) -> assertThat(number.floatValue()).isCloseTo(0.15f, within(0.01f))); + } + // end::code[] + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/jsontests/MyJsonTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/jsontests/MyJsonTests.java new file mode 100644 index 000000000000..027bb69a97a1 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/jsontests/MyJsonTests.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.jsontests; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.json.JsonTest; +import org.springframework.boot.test.json.JacksonTester; + +import static org.assertj.core.api.Assertions.assertThat; + +@JsonTest +class MyJsonTests { + + @Autowired + private JacksonTester json; + + @Test + void serialize() throws Exception { + VehicleDetails details = new VehicleDetails("Honda", "Civic"); + // Assert against a `.json` file in the same package as the test + assertThat(this.json.write(details)).isEqualToJson("expected.json"); + // Or use JSON path based assertions + assertThat(this.json.write(details)).hasJsonPathStringValue("@.make"); + assertThat(this.json.write(details)).extractingJsonPathStringValue("@.make").isEqualTo("Honda"); + } + + @Test + void deserialize() throws Exception { + String content = "{\"make\":\"Ford\",\"model\":\"Focus\"}"; + assertThat(this.json.parse(content)).isEqualTo(new VehicleDetails("Ford", "Focus")); + assertThat(this.json.parseObject(content).getMake()).isEqualTo("Ford"); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/jsontests/SomeObject.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/jsontests/SomeObject.java new file mode 100644 index 000000000000..b24cd2a98dd6 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/jsontests/SomeObject.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.jsontests; + +class SomeObject { + + SomeObject(float value) { + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/jsontests/VehicleDetails.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/jsontests/VehicleDetails.java new file mode 100644 index 000000000000..8737b8fbaf9d --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/jsontests/VehicleDetails.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.jsontests; + +class VehicleDetails { + + private final String make; + + private final String model; + + VehicleDetails(String make, String model) { + this.make = make; + this.model = model; + } + + String getMake() { + return this.make; + } + + String getModel() { + return this.model; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/mockingbeans/bean/MyTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/mockingbeans/bean/MyTests.java new file mode 100644 index 000000000000..3afcdda54819 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/mockingbeans/bean/MyTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.mockingbeans.bean; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; + +@SpringBootTest +class MyTests { + + @Autowired + private Reverser reverser; + + @MockBean + private RemoteService remoteService; + + @Test + void exampleTest() { + given(this.remoteService.getValue()).willReturn("spring"); + String reverse = this.reverser.getReverseValue(); // Calls injected RemoteService + assertThat(reverse).isEqualTo("gnirps"); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/mockingbeans/bean/RemoteService.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/mockingbeans/bean/RemoteService.java new file mode 100644 index 000000000000..1c0792316415 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/mockingbeans/bean/RemoteService.java @@ -0,0 +1,25 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.mockingbeans.bean; + +class RemoteService { + + Object getValue() { + return null; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/mockingbeans/bean/Reverser.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/mockingbeans/bean/Reverser.java new file mode 100644 index 000000000000..f98a35807f30 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/mockingbeans/bean/Reverser.java @@ -0,0 +1,25 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.mockingbeans.bean; + +class Reverser { + + String getReverseValue() { + return null; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/mockingbeans/listener/MyConfig.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/mockingbeans/listener/MyConfig.java new file mode 100644 index 000000000000..c0a7835cac27 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/mockingbeans/listener/MyConfig.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.mockingbeans.listener; + +class MyConfig { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/mockingbeans/listener/MyTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/mockingbeans/listener/MyTests.java new file mode 100644 index 000000000000..66f43b2f3d5a --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/mockingbeans/listener/MyTests.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.mockingbeans.listener; + +import org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener; +import org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; + +@ContextConfiguration(classes = MyConfig.class) +@TestExecutionListeners({ MockitoTestExecutionListener.class, ResetMocksTestExecutionListener.class }) +class MyTests { + + // ... + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/springmvctests/MyControllerTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/springmvctests/MyControllerTests.java new file mode 100644 index 000000000000..cce533a272be --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/springmvctests/MyControllerTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.springmvctests; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(UserVehicleController.class) +class MyControllerTests { + + @Autowired + private MockMvc mvc; + + @MockBean + private UserVehicleService userVehicleService; + + @Test + void testExample() throws Exception { + // @formatter:off + given(this.userVehicleService.getVehicleDetails("sboot")) + .willReturn(new VehicleDetails("Honda", "Civic")); + this.mvc.perform(get("/sboot/vehicle").accept(MediaType.TEXT_PLAIN)) + .andExpect(status().isOk()) + .andExpect(content().string("Honda Civic")); + // @formatter:on + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/springmvctests/MyHtmlUnitTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/springmvctests/MyHtmlUnitTests.java new file mode 100644 index 000000000000..5d0b4b5bc976 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/springmvctests/MyHtmlUnitTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.springmvctests; + +import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; + +@WebMvcTest(UserVehicleController.class) +class MyHtmlUnitTests { + + @Autowired + private WebClient webClient; + + @MockBean + private UserVehicleService userVehicleService; + + @Test + void testExample() throws Exception { + given(this.userVehicleService.getVehicleDetails("sboot")).willReturn(new VehicleDetails("Honda", "Civic")); + HtmlPage page = this.webClient.getPage("/sboot/vehicle.html"); + assertThat(page.getBody().getTextContent()).isEqualTo("Honda Civic"); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/springmvctests/UserVehicleController.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/springmvctests/UserVehicleController.java new file mode 100644 index 000000000000..504d724e10cf --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/springmvctests/UserVehicleController.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.springmvctests; + +class UserVehicleController { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/springmvctests/UserVehicleService.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/springmvctests/UserVehicleService.java new file mode 100644 index 000000000000..f7b4f7c9fadc --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/springmvctests/UserVehicleService.java @@ -0,0 +1,25 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.springmvctests; + +class UserVehicleService { + + VehicleDetails getVehicleDetails(String name) { + return null; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/springmvctests/VehicleDetails.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/springmvctests/VehicleDetails.java new file mode 100644 index 000000000000..73b81924fc71 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/springmvctests/VehicleDetails.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.springmvctests; + +class VehicleDetails { + + VehicleDetails(String make, String model) { + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/springwebfluxtests/MyControllerTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/springwebfluxtests/MyControllerTests.java new file mode 100644 index 000000000000..914e4bcb0561 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/springwebfluxtests/MyControllerTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.springwebfluxtests; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.mockito.BDDMockito.given; + +@WebFluxTest(UserVehicleController.class) +class MyControllerTests { + + @Autowired + private WebTestClient webClient; + + @MockBean + private UserVehicleService userVehicleService; + + @Test + void testExample() throws Exception { + // @formatter:off + given(this.userVehicleService.getVehicleDetails("sboot")) + .willReturn(new VehicleDetails("Honda", "Civic")); + this.webClient.get().uri("/sboot/vehicle").accept(MediaType.TEXT_PLAIN).exchange() + .expectStatus().isOk() + .expectBody(String.class).isEqualTo("Honda Civic"); + // @formatter:on + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/springwebfluxtests/UserVehicleController.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/springwebfluxtests/UserVehicleController.java new file mode 100644 index 000000000000..4e2460933226 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/springwebfluxtests/UserVehicleController.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.springwebfluxtests; + +class UserVehicleController { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/springwebfluxtests/UserVehicleService.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/springwebfluxtests/UserVehicleService.java new file mode 100644 index 000000000000..c571ac3dd96d --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/springwebfluxtests/UserVehicleService.java @@ -0,0 +1,25 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.springwebfluxtests; + +class UserVehicleService { + + VehicleDetails getVehicleDetails(String name) { + return null; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/springwebfluxtests/VehicleDetails.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/springwebfluxtests/VehicleDetails.java new file mode 100644 index 000000000000..0645577d06d4 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/springwebfluxtests/VehicleDetails.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.springwebfluxtests; + +class VehicleDetails { + + VehicleDetails(String make, String model) { + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/userconfigurationandslicing/MyApplication.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/userconfigurationandslicing/MyApplication.java new file mode 100644 index 000000000000..0527e209cc7c --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/userconfigurationandslicing/MyApplication.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.userconfigurationandslicing; + +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +@EnableBatchProcessing +public class MyApplication { + + // ... + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/userconfigurationandslicing/MyBatchConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/userconfigurationandslicing/MyBatchConfiguration.java new file mode 100644 index 000000000000..10de7c44bd40 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/userconfigurationandslicing/MyBatchConfiguration.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.userconfigurationandslicing; + +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@EnableBatchProcessing +public class MyBatchConfiguration { + + // ... + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/userconfigurationandslicing/MyWebConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/userconfigurationandslicing/MyWebConfiguration.java new file mode 100644 index 000000000000..a4f7aba1a5f1 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/userconfigurationandslicing/MyWebConfiguration.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.userconfigurationandslicing; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration(proxyBeanMethods = false) +public class MyWebConfiguration { + + @Bean + public WebMvcConfigurer testConfigurer() { + return new WebMvcConfigurer() { + // ... + }; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/userconfigurationandslicing/MyWebMvcConfigurer.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/userconfigurationandslicing/MyWebMvcConfigurer.java new file mode 100644 index 000000000000..30e4c013232a --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/userconfigurationandslicing/MyWebMvcConfigurer.java @@ -0,0 +1,27 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.userconfigurationandslicing; + +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Component +public class MyWebMvcConfigurer implements WebMvcConfigurer { + + // ... + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/userconfigurationandslicing/scan/MyApplication.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/userconfigurationandslicing/scan/MyApplication.java new file mode 100644 index 000000000000..b1828362527f --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/userconfigurationandslicing/scan/MyApplication.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.userconfigurationandslicing.scan; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; + +@SpringBootApplication +@ComponentScan({ "com.example.app", "com.example.another" }) +public class MyApplication { + + // ... + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/usingapplicationarguments/MyApplicationArgumentTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/usingapplicationarguments/MyApplicationArgumentTests.java new file mode 100644 index 000000000000..c362c7ebf5a0 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/usingapplicationarguments/MyApplicationArgumentTests.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.usingapplicationarguments; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(args = "--app.test=one") +class MyApplicationArgumentTests { + + @Test + void applicationArgumentsPopulated(@Autowired ApplicationArguments args) { + assertThat(args.getOptionNames()).containsOnly("app.test"); + assertThat(args.getOptionValues("app.test")).containsOnly("one"); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/withmockenvironment/MyMockMvcTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/withmockenvironment/MyMockMvcTests.java new file mode 100644 index 000000000000..b89b927673b5 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/withmockenvironment/MyMockMvcTests.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.withmockenvironment; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +class MyMockMvcTests { + + @Test + void exampleTest(@Autowired MockMvc mvc) throws Exception { + mvc.perform(get("/")).andExpect(status().isOk()).andExpect(content().string("Hello World")); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/withmockenvironment/MyMockWebTestClientTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/withmockenvironment/MyMockWebTestClientTests.java new file mode 100644 index 000000000000..4fcd698b73dc --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/withmockenvironment/MyMockWebTestClientTests.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.withmockenvironment; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.reactive.server.WebTestClient; + +@SpringBootTest +@AutoConfigureWebTestClient +class MyMockWebTestClientTests { + + @Test + void exampleTest(@Autowired WebTestClient webClient) { + // @formatter:off + webClient + .get().uri("/") + .exchange() + .expectStatus().isOk() + .expectBody(String.class).isEqualTo("Hello World"); + // @formatter:on + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/withrunningserver/MyRandomPortTestRestTemplateTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/withrunningserver/MyRandomPortTestRestTemplateTests.java new file mode 100644 index 000000000000..8ed9f3f416b3 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/withrunningserver/MyRandomPortTestRestTemplateTests.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.withrunningserver; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +class MyRandomPortTestRestTemplateTests { + + @Test + void exampleTest(@Autowired TestRestTemplate restTemplate) { + String body = restTemplate.getForObject("/", String.class); + assertThat(body).isEqualTo("Hello World"); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/withrunningserver/MyRandomPortWebTestClientTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/withrunningserver/MyRandomPortWebTestClientTests.java new file mode 100644 index 000000000000..c7f660b67d54 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/withrunningserver/MyRandomPortWebTestClientTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.withrunningserver; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.web.reactive.server.WebTestClient; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +class MyRandomPortWebTestClientTests { + + @Test + void exampleTest(@Autowired WebTestClient webClient) { + // @formatter:off + webClient + .get().uri("/") + .exchange() + .expectStatus().isOk() + .expectBody(String.class).isEqualTo("Hello World"); + // @formatter:on + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/utilities/configdataapplicationcontextinitializer/Config.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/utilities/configdataapplicationcontextinitializer/Config.java new file mode 100644 index 000000000000..499411882218 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/utilities/configdataapplicationcontextinitializer/Config.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.utilities.configdataapplicationcontextinitializer; + +class Config { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/utilities/configdataapplicationcontextinitializer/MyConfigFileTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/utilities/configdataapplicationcontextinitializer/MyConfigFileTests.java new file mode 100644 index 000000000000..3799f6ed52df --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/utilities/configdataapplicationcontextinitializer/MyConfigFileTests.java @@ -0,0 +1,27 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.utilities.configdataapplicationcontextinitializer; + +import org.springframework.boot.test.context.ConfigDataApplicationContextInitializer; +import org.springframework.test.context.ContextConfiguration; + +@ContextConfiguration(classes = Config.class, initializers = ConfigDataApplicationContextInitializer.class) +class MyConfigFileTests { + + // ... + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/utilities/outputcapture/MyOutputCaptureTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/utilities/outputcapture/MyOutputCaptureTests.java new file mode 100644 index 000000000000..589eef16a658 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/utilities/outputcapture/MyOutputCaptureTests.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.utilities.outputcapture; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(OutputCaptureExtension.class) +class MyOutputCaptureTests { + + @Test + void testName(CapturedOutput output) { + System.out.println("Hello World!"); + assertThat(output).contains("World"); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/utilities/testpropertyvalues/MyEnvironmentTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/utilities/testpropertyvalues/MyEnvironmentTests.java new file mode 100644 index 000000000000..c40bd3addbbe --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/utilities/testpropertyvalues/MyEnvironmentTests.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.utilities.testpropertyvalues; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.mock.env.MockEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; + +class MyEnvironmentTests { + + @Test + void testPropertySources() { + MockEnvironment environment = new MockEnvironment(); + TestPropertyValues.of("org=Spring", "name=Boot").applyTo(environment); + assertThat(environment.getProperty("name")).isEqualTo("Boot"); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/utilities/testresttemplate/MySpringBootTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/utilities/testresttemplate/MySpringBootTests.java new file mode 100644 index 000000000000..f4dc7bb95705 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/utilities/testresttemplate/MySpringBootTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.utilities.testresttemplate; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpHeaders; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +class MySpringBootTests { + + @Autowired + private TestRestTemplate template; + + @Test + void testRequest() { + HttpHeaders headers = this.template.getForEntity("/example", String.class).getHeaders(); + assertThat(headers.getLocation()).hasHost("other.example.com"); + } + + @TestConfiguration(proxyBeanMethods = false) + static class RestTemplateBuilderConfiguration { + + @Bean + RestTemplateBuilder restTemplateBuilder() { + return new RestTemplateBuilder().setConnectTimeout(Duration.ofSeconds(1)) + .setReadTimeout(Duration.ofSeconds(1)); + } + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/utilities/testresttemplate/MySpringBootTestsConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/utilities/testresttemplate/MySpringBootTestsConfiguration.java new file mode 100644 index 000000000000..9cbe6f25a551 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/utilities/testresttemplate/MySpringBootTestsConfiguration.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.utilities.testresttemplate; + +import java.net.URI; + +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@SpringBootConfiguration(proxyBeanMethods = false) +@ImportAutoConfiguration({ ServletWebServerFactoryAutoConfiguration.class, DispatcherServletAutoConfiguration.class, + JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class }) +public class MySpringBootTestsConfiguration { + + @RestController + private static class ExampleController { + + @RequestMapping("/example") + ResponseEntity example() { + return ResponseEntity.ok().location(URI.create("https://other.example.com/example")).body("test"); + } + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/utilities/testresttemplate/MyTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/utilities/testresttemplate/MyTests.java new file mode 100644 index 000000000000..04c9210217d8 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/utilities/testresttemplate/MyTests.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.utilities.testresttemplate; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; + +class MyTests { + + private TestRestTemplate template = new TestRestTemplate(); + + @Test + void testRequest() throws Exception { + ResponseEntity headers = this.template.getForEntity("https://myhost.example.com/example", String.class); + assertThat(headers.getHeaders().getLocation()).hasHost("other.example.com"); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/validation/Archive.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/validation/Archive.java new file mode 100644 index 000000000000..70ea4b82a706 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/validation/Archive.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.validation; + +class Archive { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/validation/Author.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/validation/Author.java new file mode 100644 index 000000000000..2c4ce61feca1 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/validation/Author.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.validation; + +class Author { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/validation/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/validation/MyBean.java new file mode 100644 index 000000000000..65c4957b49ca --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/validation/MyBean.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.validation; + +import javax.validation.constraints.Size; + +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +@Service +@Validated +public class MyBean { + + public Archive findByCodeAndAuthor(@Size(min = 8, max = 10) String code, Author author) { + return /**/ null; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/webclient/MyService.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/webclient/MyService.java new file mode 100644 index 000000000000..0d0d863da253 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/webclient/MyService.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.webclient; + +import org.neo4j.cypherdsl.core.Relationship.Details; +import reactor.core.publisher.Mono; + +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; + +@Service +public class MyService { + + private final WebClient webClient; + + public MyService(WebClient.Builder webClientBuilder) { + this.webClient = webClientBuilder.baseUrl("https://example.org").build(); + } + + public Mono
    someRestCall(String name) { + return this.webClient.get().uri("/{name}/details", name).retrieve().bodyToMono(Details.class); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/webservices/template/MyService.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/webservices/template/MyService.java new file mode 100644 index 000000000000..4c4ac1345cf5 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/webservices/template/MyService.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.webservices.template; + +import org.springframework.boot.webservices.client.WebServiceTemplateBuilder; +import org.springframework.stereotype.Service; +import org.springframework.ws.client.core.WebServiceTemplate; +import org.springframework.ws.soap.client.core.SoapActionCallback; + +@Service +public class MyService { + + private final WebServiceTemplate webServiceTemplate; + + public MyService(WebServiceTemplateBuilder webServiceTemplateBuilder) { + this.webServiceTemplate = webServiceTemplateBuilder.build(); + } + + public SomeResponse someWsCall(SomeRequest detailsReq) { + return (SomeResponse) this.webServiceTemplate.marshalSendAndReceive(detailsReq, + new SoapActionCallback("https://ws.example.com/action")); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/webservices/template/MyWebServiceTemplateConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/webservices/template/MyWebServiceTemplateConfiguration.java new file mode 100644 index 000000000000..cfe3953dc140 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/webservices/template/MyWebServiceTemplateConfiguration.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.webservices.template; + +import java.time.Duration; + +import org.springframework.boot.webservices.client.HttpWebServiceMessageSenderBuilder; +import org.springframework.boot.webservices.client.WebServiceTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.ws.client.core.WebServiceTemplate; +import org.springframework.ws.transport.WebServiceMessageSender; + +@Configuration(proxyBeanMethods = false) +public class MyWebServiceTemplateConfiguration { + + @Bean + public WebServiceTemplate webServiceTemplate(WebServiceTemplateBuilder builder) { + // @formatter:off + WebServiceMessageSender sender = new HttpWebServiceMessageSenderBuilder() + .setConnectTimeout(Duration.ofSeconds(5)) + .setReadTimeout(Duration.ofSeconds(2)) + .build(); + return builder.messageSenders(sender).build(); + // @formatter:on + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/webservices/template/SomeRequest.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/webservices/template/SomeRequest.java new file mode 100644 index 000000000000..2c149b0fc484 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/webservices/template/SomeRequest.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.webservices.template; + +class SomeRequest { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/webservices/template/SomeResponse.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/webservices/template/SomeResponse.java new file mode 100644 index 000000000000..fb635c745572 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/webservices/template/SomeResponse.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.webservices.template; + +class SomeResponse { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/gettingstarted/firstapplication/code/MyApplication.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/gettingstarted/firstapplication/code/MyApplication.java new file mode 100644 index 000000000000..c05e4abf4508 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/gettingstarted/firstapplication/code/MyApplication.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.gettingstarted.firstapplication.code; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@EnableAutoConfiguration +public class MyApplication { + + @RequestMapping("/") + String home() { + return "Hello World!"; + } + + public static void main(String[] args) { + SpringApplication.run(MyApplication.class, args); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/actuator/maphealthindicatorstometrics/MetricsHealthMicrometerExport.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/actuator/maphealthindicatorstometrics/MetricsHealthMicrometerExport.java new file mode 100644 index 000000000000..6b53dcc476b3 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/actuator/maphealthindicatorstometrics/MetricsHealthMicrometerExport.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.actuator.maphealthindicatorstometrics; + +public class MetricsHealthMicrometerExport { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/actuator/maphealthindicatorstometrics/MyHealthMetricsExportConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/actuator/maphealthindicatorstometrics/MyHealthMetricsExportConfiguration.java new file mode 100644 index 000000000000..9af5092ee0d3 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/actuator/maphealthindicatorstometrics/MyHealthMetricsExportConfiguration.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.actuator.maphealthindicatorstometrics; + +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; + +import org.springframework.boot.actuate.health.HealthEndpoint; +import org.springframework.boot.actuate.health.Status; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class MyHealthMetricsExportConfiguration { + + public MyHealthMetricsExportConfiguration(MeterRegistry registry, HealthEndpoint healthEndpoint) { + // This example presumes common tags (such as the app) are applied elsewhere + Gauge.builder("health", healthEndpoint, this::getStatusCode).strongReference(true).register(registry); + } + + private int getStatusCode(HealthEndpoint health) { + Status status = health.health().getStatus(); + if (Status.UP.equals(status)) { + return 3; + } + if (Status.OUT_OF_SERVICE.equals(status)) { + return 2; + } + if (Status.DOWN.equals(status)) { + return 1; + } + return 0; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/application/customizetheenvironmentorapplicationcontext/MyEnvironmentPostProcessor.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/application/customizetheenvironmentorapplicationcontext/MyEnvironmentPostProcessor.java new file mode 100644 index 000000000000..f2ef2cfbb54d --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/application/customizetheenvironmentorapplicationcontext/MyEnvironmentPostProcessor.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.application.customizetheenvironmentorapplicationcontext; + +import java.io.IOException; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.boot.env.YamlPropertySourceLoader; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.PropertySource; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; + +public class MyEnvironmentPostProcessor implements EnvironmentPostProcessor { + + private final YamlPropertySourceLoader loader = new YamlPropertySourceLoader(); + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + Resource path = new ClassPathResource("com/example/myapp/config.yml"); + PropertySource propertySource = loadYaml(path); + environment.getPropertySources().addLast(propertySource); + } + + private PropertySource loadYaml(Resource path) { + Assert.isTrue(path.exists(), () -> "Resource " + path + " does not exist"); + try { + return this.loader.load("custom-resource", path).get(0); + } + catch (IOException ex) { + throw new IllegalStateException("Failed to load yaml configuration from " + path, ex); + } + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/configureacomponentthatisusedbyjpa/ElasticsearchEntityManagerFactoryDependsOnPostProcessor.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/configureacomponentthatisusedbyjpa/ElasticsearchEntityManagerFactoryDependsOnPostProcessor.java new file mode 100644 index 000000000000..b9bb55de1c7f --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/configureacomponentthatisusedbyjpa/ElasticsearchEntityManagerFactoryDependsOnPostProcessor.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.dataaccess.configureacomponentthatisusedbyjpa; + +import javax.persistence.EntityManagerFactory; + +import org.springframework.boot.autoconfigure.orm.jpa.EntityManagerFactoryDependsOnPostProcessor; +import org.springframework.stereotype.Component; + +/** + * {@link EntityManagerFactoryDependsOnPostProcessor} that ensures that + * {@link EntityManagerFactory} beans depend on the {@code elasticsearchClient} bean. + */ +@Component +public class ElasticsearchEntityManagerFactoryDependsOnPostProcessor + extends EntityManagerFactoryDependsOnPostProcessor { + + public ElasticsearchEntityManagerFactoryDependsOnPostProcessor() { + super("elasticsearchClient"); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/configurecustomdatasource/builder/MyDataSourceConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/configurecustomdatasource/builder/MyDataSourceConfiguration.java new file mode 100644 index 000000000000..97a3dfd1da6d --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/configurecustomdatasource/builder/MyDataSourceConfiguration.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.dataaccess.configurecustomdatasource.builder; + +import javax.sql.DataSource; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class MyDataSourceConfiguration { + + @Bean + @ConfigurationProperties("app.datasource") + public DataSource dataSource() { + return DataSourceBuilder.create().build(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/configurecustomdatasource/configurable/MyDataSourceConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/configurecustomdatasource/configurable/MyDataSourceConfiguration.java new file mode 100644 index 000000000000..17b8a8079c03 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/configurecustomdatasource/configurable/MyDataSourceConfiguration.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.dataaccess.configurecustomdatasource.configurable; + +import com.zaxxer.hikari.HikariDataSource; + +import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +@Configuration(proxyBeanMethods = false) +public class MyDataSourceConfiguration { + + @Bean + @Primary + @ConfigurationProperties("app.datasource") + public DataSourceProperties dataSourceProperties() { + return new DataSourceProperties(); + } + + @Bean + @ConfigurationProperties("app.datasource.configuration") + public HikariDataSource dataSource(DataSourceProperties properties) { + return properties.initializeDataSourceBuilder().type(HikariDataSource.class).build(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/configurecustomdatasource/custom/MyDataSourceConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/configurecustomdatasource/custom/MyDataSourceConfiguration.java new file mode 100644 index 000000000000..72104f971ab3 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/configurecustomdatasource/custom/MyDataSourceConfiguration.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.dataaccess.configurecustomdatasource.custom; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class MyDataSourceConfiguration { + + @Bean + @ConfigurationProperties(prefix = "app.datasource") + public SomeDataSource dataSource() { + return new SomeDataSource(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/configurecustomdatasource/custom/SomeDataSource.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/configurecustomdatasource/custom/SomeDataSource.java new file mode 100644 index 000000000000..13324df2637e --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/configurecustomdatasource/custom/SomeDataSource.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.dataaccess.configurecustomdatasource.custom; + +public class SomeDataSource { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/configurecustomdatasource/simple/MyDataSourceConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/configurecustomdatasource/simple/MyDataSourceConfiguration.java new file mode 100644 index 000000000000..1c98791a8176 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/configurecustomdatasource/simple/MyDataSourceConfiguration.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.dataaccess.configurecustomdatasource.simple; + +import com.zaxxer.hikari.HikariDataSource; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class MyDataSourceConfiguration { + + @Bean + @ConfigurationProperties("app.datasource") + public HikariDataSource dataSource() { + return DataSourceBuilder.create().type(HikariDataSource.class).build(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/configurehibernatenamingstrategy/spring/MyHibernateConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/configurehibernatenamingstrategy/spring/MyHibernateConfiguration.java new file mode 100644 index 000000000000..c82b766fdfd8 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/configurehibernatenamingstrategy/spring/MyHibernateConfiguration.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.dataaccess.configurehibernatenamingstrategy.spring; + +import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; + +import org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class MyHibernateConfiguration { + + @Bean + public SpringPhysicalNamingStrategy caseSensitivePhysicalNamingStrategy() { + return new SpringPhysicalNamingStrategy() { + + @Override + protected boolean isCaseInsensitive(JdbcEnvironment jdbcEnvironment) { + return false; + } + + }; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/configurehibernatenamingstrategy/standard/MyHibernateConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/configurehibernatenamingstrategy/standard/MyHibernateConfiguration.java new file mode 100644 index 000000000000..67ffd0736996 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/configurehibernatenamingstrategy/standard/MyHibernateConfiguration.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.dataaccess.configurehibernatenamingstrategy.standard; + +import org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +class MyHibernateConfiguration { + + @Bean + PhysicalNamingStrategyStandardImpl caseSensitivePhysicalNamingStrategy() { + return new PhysicalNamingStrategyStandardImpl(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/configurehibernatesecondlevelcaching/MyHibernateSecondLevelCacheConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/configurehibernatesecondlevelcaching/MyHibernateSecondLevelCacheConfiguration.java new file mode 100644 index 000000000000..476d742c4931 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/configurehibernatesecondlevelcaching/MyHibernateSecondLevelCacheConfiguration.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.dataaccess.configurehibernatesecondlevelcaching; + +import org.hibernate.cache.jcache.ConfigSettings; + +import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer; +import org.springframework.cache.jcache.JCacheCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class MyHibernateSecondLevelCacheConfiguration { + + @Bean + public HibernatePropertiesCustomizer hibernateSecondLevelCacheCustomizer(JCacheCacheManager cacheManager) { + return (properties) -> properties.put(ConfigSettings.CACHE_MANAGER, cacheManager.getCacheManager()); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/configuretwodatasources/MyCompleteDataSourcesConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/configuretwodatasources/MyCompleteDataSourcesConfiguration.java new file mode 100644 index 000000000000..f25837de7274 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/configuretwodatasources/MyCompleteDataSourcesConfiguration.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.dataaccess.configuretwodatasources; + +import com.zaxxer.hikari.HikariDataSource; +import org.apache.commons.dbcp2.BasicDataSource; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +@Configuration(proxyBeanMethods = false) +public class MyCompleteDataSourcesConfiguration { + + @Bean + @Primary + @ConfigurationProperties("app.datasource.first") + public DataSourceProperties firstDataSourceProperties() { + return new DataSourceProperties(); + } + + @Bean + @Primary + @ConfigurationProperties("app.datasource.first.configuration") + public HikariDataSource firstDataSource(DataSourceProperties firstDataSourceProperties) { + return firstDataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build(); + } + + @Bean + @ConfigurationProperties("app.datasource.second") + public DataSourceProperties secondDataSourceProperties() { + return new DataSourceProperties(); + } + + @Bean + @ConfigurationProperties("app.datasource.second.configuration") + public BasicDataSource secondDataSource( + @Qualifier("secondDataSourceProperties") DataSourceProperties secondDataSourceProperties) { + return secondDataSourceProperties.initializeDataSourceBuilder().type(BasicDataSource.class).build(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/configuretwodatasources/MyDataSourcesConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/configuretwodatasources/MyDataSourcesConfiguration.java new file mode 100644 index 000000000000..c88ba5a7274d --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/configuretwodatasources/MyDataSourcesConfiguration.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.dataaccess.configuretwodatasources; + +import com.zaxxer.hikari.HikariDataSource; +import org.apache.commons.dbcp2.BasicDataSource; + +import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +@Configuration(proxyBeanMethods = false) +public class MyDataSourcesConfiguration { + + @Bean + @Primary + @ConfigurationProperties("app.datasource.first") + public DataSourceProperties firstDataSourceProperties() { + return new DataSourceProperties(); + } + + @Bean + @Primary + @ConfigurationProperties("app.datasource.first.configuration") + public HikariDataSource firstDataSource(DataSourceProperties firstDataSourceProperties) { + return firstDataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build(); + } + + @Bean + @ConfigurationProperties("app.datasource.second") + public BasicDataSource secondDataSource() { + return DataSourceBuilder.create().type(BasicDataSource.class).build(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/separateentitydefinitionsfromspringconfiguration/City.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/separateentitydefinitionsfromspringconfiguration/City.java new file mode 100644 index 000000000000..b9c97900204f --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/separateentitydefinitionsfromspringconfiguration/City.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.dataaccess.separateentitydefinitionsfromspringconfiguration; + +class City { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/separateentitydefinitionsfromspringconfiguration/MyApplication.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/separateentitydefinitionsfromspringconfiguration/MyApplication.java new file mode 100644 index 000000000000..8627bafc1369 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/separateentitydefinitionsfromspringconfiguration/MyApplication.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.dataaccess.separateentitydefinitionsfromspringconfiguration; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@EnableAutoConfiguration +@EntityScan(basePackageClasses = City.class) +public class MyApplication { + + // ... + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/usemultipleentitymanagers/Customer.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/usemultipleentitymanagers/Customer.java new file mode 100644 index 000000000000..b03ad31d2786 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/usemultipleentitymanagers/Customer.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.dataaccess.usemultipleentitymanagers; + +public class Customer { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/usemultipleentitymanagers/CustomerConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/usemultipleentitymanagers/CustomerConfiguration.java new file mode 100644 index 000000000000..6092d4c32bf4 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/usemultipleentitymanagers/CustomerConfiguration.java @@ -0,0 +1,26 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.dataaccess.usemultipleentitymanagers; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +@Configuration(proxyBeanMethods = false) +@EnableJpaRepositories(basePackageClasses = Customer.class, entityManagerFactoryRef = "secondEntityManagerFactory") +public class CustomerConfiguration { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/usemultipleentitymanagers/MyEntityManagerFactoryConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/usemultipleentitymanagers/MyEntityManagerFactoryConfiguration.java new file mode 100644 index 000000000000..11f898027b27 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/usemultipleentitymanagers/MyEntityManagerFactoryConfiguration.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.dataaccess.usemultipleentitymanagers; + +import javax.sql.DataSource; + +import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.orm.jpa.JpaVendorAdapter; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; + +@Configuration(proxyBeanMethods = false) +public class MyEntityManagerFactoryConfiguration { + + @Bean + @ConfigurationProperties("app.jpa.first") + public JpaProperties firstJpaProperties() { + return new JpaProperties(); + } + + @Bean + public LocalContainerEntityManagerFactoryBean firstEntityManagerFactory(DataSource firstDataSource, + JpaProperties firstJpaProperties) { + EntityManagerFactoryBuilder builder = createEntityManagerFactoryBuilder(firstJpaProperties); + return builder.dataSource(firstDataSource).packages(Order.class).persistenceUnit("firstDs").build(); + } + + private EntityManagerFactoryBuilder createEntityManagerFactoryBuilder(JpaProperties jpaProperties) { + JpaVendorAdapter jpaVendorAdapter = createJpaVendorAdapter(jpaProperties); + return new EntityManagerFactoryBuilder(jpaVendorAdapter, jpaProperties.getProperties(), null); + } + + private JpaVendorAdapter createJpaVendorAdapter(JpaProperties jpaProperties) { + // ... map JPA properties as needed + return new HibernateJpaVendorAdapter(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/usemultipleentitymanagers/Order.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/usemultipleentitymanagers/Order.java new file mode 100644 index 000000000000..333f4f615fd2 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/usemultipleentitymanagers/Order.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.dataaccess.usemultipleentitymanagers; + +class Order { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/usemultipleentitymanagers/OrderConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/usemultipleentitymanagers/OrderConfiguration.java new file mode 100644 index 000000000000..72f00ba0fb48 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/dataaccess/usemultipleentitymanagers/OrderConfiguration.java @@ -0,0 +1,26 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.dataaccess.usemultipleentitymanagers; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +@Configuration(proxyBeanMethods = false) +@EnableJpaRepositories(basePackageClasses = Order.class, entityManagerFactoryRef = "firstEntityManagerFactory") +public class OrderConfiguration { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/httpclients/webclientreactornettycustomization/MyReactorNettyClientConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/httpclients/webclientreactornettycustomization/MyReactorNettyClientConfiguration.java new file mode 100644 index 000000000000..8b67fb450e23 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/httpclients/webclientreactornettycustomization/MyReactorNettyClientConfiguration.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.httpclients.webclientreactornettycustomization; + +import io.netty.channel.ChannelOption; +import io.netty.handler.timeout.ReadTimeoutHandler; +import reactor.netty.http.client.HttpClient; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.http.client.reactive.ReactorResourceFactory; + +@Configuration(proxyBeanMethods = false) +public class MyReactorNettyClientConfiguration { + + @Bean + ClientHttpConnector clientHttpConnector(ReactorResourceFactory resourceFactory) { + // @formatter:off + HttpClient httpClient = HttpClient.create(resourceFactory.getConnectionProvider()) + .runOn(resourceFactory.getLoopResources()) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 60000) + .doOnConnected((connection) -> connection.addHandlerLast(new ReadTimeoutHandler(60))); + return new ReactorClientHttpConnector(httpClient); + // @formatter:on + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/alongsideanotherwebframework/Endpoint.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/alongsideanotherwebframework/Endpoint.java new file mode 100644 index 000000000000..8a9e8579beb6 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/alongsideanotherwebframework/Endpoint.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.jersey.alongsideanotherwebframework; + +class Endpoint { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/alongsideanotherwebframework/JerseyConfig.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/alongsideanotherwebframework/JerseyConfig.java new file mode 100644 index 000000000000..b64eae6ff31d --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/alongsideanotherwebframework/JerseyConfig.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.jersey.alongsideanotherwebframework; + +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.servlet.ServletProperties; + +import org.springframework.stereotype.Component; + +@Component +public class JerseyConfig extends ResourceConfig { + + public JerseyConfig() { + register(Endpoint.class); + property(ServletProperties.FILTER_FORWARD_ON_404, true); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/springsecurity/Endpoint.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/springsecurity/Endpoint.java new file mode 100644 index 000000000000..240f4d36bfab --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/springsecurity/Endpoint.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.jersey.springsecurity; + +class Endpoint { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/springsecurity/JerseySetStatusOverSendErrorConfig.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/springsecurity/JerseySetStatusOverSendErrorConfig.java new file mode 100644 index 000000000000..e7e9b63ba473 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/jersey/springsecurity/JerseySetStatusOverSendErrorConfig.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.jersey.springsecurity; + +import java.util.Collections; + +import org.glassfish.jersey.server.ResourceConfig; + +import org.springframework.stereotype.Component; + +@Component +public class JerseySetStatusOverSendErrorConfig extends ResourceConfig { + + public JerseySetStatusOverSendErrorConfig() { + register(Endpoint.class); + setProperties(Collections.singletonMap("jersey.config.server.response.setStatusOverSendError", true)); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/messaging/disabletransactedjmssession/MyJmsConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/messaging/disabletransactedjmssession/MyJmsConfiguration.java new file mode 100644 index 000000000000..b11cb791266d --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/messaging/disabletransactedjmssession/MyJmsConfiguration.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.messaging.disabletransactedjmssession; + +import javax.jms.ConnectionFactory; + +import org.springframework.boot.autoconfigure.jms.DefaultJmsListenerContainerFactoryConfigurer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jms.config.DefaultJmsListenerContainerFactory; + +@Configuration(proxyBeanMethods = false) +public class MyJmsConfiguration { + + @Bean + public DefaultJmsListenerContainerFactory jmsListenerContainerFactory(ConnectionFactory connectionFactory, + DefaultJmsListenerContainerFactoryConfigurer configurer) { + DefaultJmsListenerContainerFactory listenerFactory = new DefaultJmsListenerContainerFactory(); + configurer.configure(listenerFactory, connectionFactory); + listenerFactory.setTransactionManager(null); + listenerFactory.setSessionTransacted(false); + return listenerFactory; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/propertiesandconfiguration/externalizeconfiguration/application/MyApplication.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/propertiesandconfiguration/externalizeconfiguration/application/MyApplication.java new file mode 100644 index 000000000000..7df228f5e2f6 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/propertiesandconfiguration/externalizeconfiguration/application/MyApplication.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.propertiesandconfiguration.externalizeconfiguration.application; + +import org.springframework.boot.Banner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class MyApplication { + + public static void main(String[] args) { + SpringApplication application = new SpringApplication(MyApplication.class); + application.setBannerMode(Banner.Mode.OFF); + application.run(args); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/propertiesandconfiguration/externalizeconfiguration/builder/MyApplication.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/propertiesandconfiguration/externalizeconfiguration/builder/MyApplication.java new file mode 100644 index 000000000000..ebb33d245233 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/propertiesandconfiguration/externalizeconfiguration/builder/MyApplication.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.propertiesandconfiguration.externalizeconfiguration.builder; + +import org.springframework.boot.Banner; +import org.springframework.boot.builder.SpringApplicationBuilder; + +public class MyApplication { + + public static void main(String[] args) { + // @formatter:off + new SpringApplicationBuilder() + .bannerMode(Banner.Mode.OFF) + .sources(MyApplication.class) + .run(args); + // @formatter:on + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/security/enablehttps/MySecurityConfig.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/security/enablehttps/MySecurityConfig.java new file mode 100644 index 000000000000..a02a677ea6b5 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/security/enablehttps/MySecurityConfig.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.security.enablehttps; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class MySecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + // Customize the application security ... + http.requiresChannel().anyRequest().requiresSecure(); + return http.build(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/springmvc/writejsonrestservice/MyController.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/springmvc/writejsonrestservice/MyController.java new file mode 100644 index 000000000000..43e03a9d8e2e --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/springmvc/writejsonrestservice/MyController.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.springmvc.writejsonrestservice; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class MyController { + + @RequestMapping("/thing") + public MyThing thing() { + return new MyThing(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/springmvc/writejsonrestservice/MyThing.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/springmvc/writejsonrestservice/MyThing.java new file mode 100644 index 000000000000..ed28185e30f5 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/springmvc/writejsonrestservice/MyThing.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.springmvc.writejsonrestservice; + +public class MyThing { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/springmvc/writexmlrestservice/MyThing.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/springmvc/writexmlrestservice/MyThing.java new file mode 100644 index 000000000000..7b69c1ee6267 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/springmvc/writexmlrestservice/MyThing.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.springmvc.writexmlrestservice; + +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement +public class MyThing { + + private String name; + + // @fold:on // getters/setters ... + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + // @fold:off + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/testing/slicetests/MyConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/testing/slicetests/MyConfiguration.java new file mode 100644 index 000000000000..e6d24d1bbe09 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/testing/slicetests/MyConfiguration.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.testing.slicetests; + +import org.apache.commons.dbcp2.BasicDataSource; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration(proxyBeanMethods = false) +public class MyConfiguration { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http.authorizeRequests().anyRequest().authenticated(); + return http.build(); + } + + @Bean + @ConfigurationProperties("app.datasource.second") + public BasicDataSource secondDataSource() { + return DataSourceBuilder.create().type(BasicDataSource.class).build(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/testing/slicetests/MyDatasourceConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/testing/slicetests/MyDatasourceConfiguration.java new file mode 100644 index 000000000000..70506c4ee427 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/testing/slicetests/MyDatasourceConfiguration.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.testing.slicetests; + +import org.apache.commons.dbcp2.BasicDataSource; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class MyDatasourceConfiguration { + + @Bean + @ConfigurationProperties("app.datasource.second") + public BasicDataSource secondDataSource() { + return DataSourceBuilder.create().type(BasicDataSource.class).build(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/testing/slicetests/MySecurityConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/testing/slicetests/MySecurityConfiguration.java new file mode 100644 index 000000000000..866ab0fd4cd7 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/testing/slicetests/MySecurityConfiguration.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.testing.slicetests; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration(proxyBeanMethods = false) +public class MySecurityConfiguration { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http.authorizeRequests().anyRequest().authenticated(); + return http.build(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/testing/testcontainers/dynamicproperties/MyIntegrationTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/testing/testcontainers/dynamicproperties/MyIntegrationTests.java new file mode 100644 index 000000000000..d49df7c7509e --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/testing/testcontainers/dynamicproperties/MyIntegrationTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.testing.testcontainers.dynamicproperties; + +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.Neo4jContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; + +@SpringBootTest +@Testcontainers +class MyIntegrationTests { + + @Container + static Neo4jContainer neo4j = new Neo4jContainer<>("neo4j:4.2"); + + @Test + void myTest() { + // ... + } + + @DynamicPropertySource + static void neo4jProperties(DynamicPropertyRegistry registry) { + registry.add("spring.neo4j.uri", neo4j::getBoltUrl); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/testing/testcontainers/vanilla/MyIntegrationTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/testing/testcontainers/vanilla/MyIntegrationTests.java new file mode 100644 index 000000000000..30f4a5f719fc --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/testing/testcontainers/vanilla/MyIntegrationTests.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.testing.testcontainers.vanilla; + +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.Neo4jContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +@Testcontainers +class MyIntegrationTests { + + @Container + static Neo4jContainer neo4j = new Neo4jContainer<>("neo4j:4.2"); + + @Test + void myTest() { + // ... + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/testing/withspringsecurity/MySecurityTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/testing/withspringsecurity/MySecurityTests.java new file mode 100644 index 000000000000..b932490b8af4 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/testing/withspringsecurity/MySecurityTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.testing.withspringsecurity; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; + +@WebMvcTest(UserController.class) +class MySecurityTests { + + @Autowired + private MockMvc mvc; + + @Test + @WithMockUser(roles = "ADMIN") + void requestProtectedUrlWithUser() throws Exception { + this.mvc.perform(get("/")); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/testing/withspringsecurity/UserController.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/testing/withspringsecurity/UserController.java new file mode 100644 index 000000000000..96fa823791cb --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/testing/withspringsecurity/UserController.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.testing.withspringsecurity; + +class UserController { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/traditionaldeployment/convertexistingapplication/MyApplication.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/traditionaldeployment/convertexistingapplication/MyApplication.java new file mode 100644 index 000000000000..ec886c4f598d --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/traditionaldeployment/convertexistingapplication/MyApplication.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.traditionaldeployment.convertexistingapplication; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; + +@SpringBootApplication +public class MyApplication extends SpringBootServletInitializer { + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + // Customize the application or call application.sources(...) to add sources + // Since our example is itself a @Configuration class (via @SpringBootApplication) + // we actually don't need to override this method. + return application; + } + + // tag::main[] + public static void main(String[] args) { + SpringApplication.run(MyApplication.class, args); + } + // end::main[] + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/traditionaldeployment/convertexistingapplication/both/MyApplication.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/traditionaldeployment/convertexistingapplication/both/MyApplication.java new file mode 100644 index 000000000000..1967af66f92b --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/traditionaldeployment/convertexistingapplication/both/MyApplication.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.traditionaldeployment.convertexistingapplication.both; + +import org.springframework.boot.Banner; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; + +@SpringBootApplication +public class MyApplication extends SpringBootServletInitializer { + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { + return customizerBuilder(builder); + } + + public static void main(String[] args) { + customizerBuilder(new SpringApplicationBuilder()).run(args); + } + + private static SpringApplicationBuilder customizerBuilder(SpringApplicationBuilder builder) { + return builder.sources(MyApplication.class).bannerMode(Banner.Mode.OFF); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/traditionaldeployment/war/MyApplication.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/traditionaldeployment/war/MyApplication.java new file mode 100644 index 000000000000..bb8164ff7d71 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/traditionaldeployment/war/MyApplication.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.traditionaldeployment.war; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; + +@SpringBootApplication +public class MyApplication extends SpringBootServletInitializer { + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + return application.sources(MyApplication.class); + } + + public static void main(String[] args) { + SpringApplication.run(MyApplication.class, args); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/traditionaldeployment/weblogic/MyApplication.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/traditionaldeployment/weblogic/MyApplication.java new file mode 100644 index 000000000000..b46e47be68b3 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/traditionaldeployment/weblogic/MyApplication.java @@ -0,0 +1,26 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.traditionaldeployment.weblogic; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +import org.springframework.web.WebApplicationInitializer; + +@SpringBootApplication +public class MyApplication extends SpringBootServletInitializer implements WebApplicationInitializer { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/webserver/addservletfilterlistener/springbean/disable/MyFilter.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/webserver/addservletfilterlistener/springbean/disable/MyFilter.java new file mode 100644 index 000000000000..9c448efa72b5 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/webserver/addservletfilterlistener/springbean/disable/MyFilter.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.webserver.addservletfilterlistener.springbean.disable; + +import javax.servlet.Filter; + +public abstract class MyFilter implements Filter { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/webserver/addservletfilterlistener/springbean/disable/MyFilterConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/webserver/addservletfilterlistener/springbean/disable/MyFilterConfiguration.java new file mode 100644 index 000000000000..bc728ac53fe9 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/webserver/addservletfilterlistener/springbean/disable/MyFilterConfiguration.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.webserver.addservletfilterlistener.springbean.disable; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class MyFilterConfiguration { + + @Bean + public FilterRegistrationBean registration(MyFilter filter) { + FilterRegistrationBean registration = new FilterRegistrationBean<>(filter); + registration.setEnabled(false); + return registration; + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/webserver/configure/MyTomcatWebServerCustomizer.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/webserver/configure/MyTomcatWebServerCustomizer.java new file mode 100644 index 000000000000..14df9fc80b25 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/webserver/configure/MyTomcatWebServerCustomizer.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.webserver.configure; + +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.stereotype.Component; + +@Component +public class MyTomcatWebServerCustomizer implements WebServerFactoryCustomizer { + + @Override + public void customize(TomcatServletWebServerFactory factory) { + // customize the factory here + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/webserver/createwebsocketendpointsusingserverendpoint/MyWebSocketConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/webserver/createwebsocketendpointsusingserverendpoint/MyWebSocketConfiguration.java new file mode 100644 index 000000000000..63948c342b47 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/webserver/createwebsocketendpointsusingserverendpoint/MyWebSocketConfiguration.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.webserver.createwebsocketendpointsusingserverendpoint; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.server.standard.ServerEndpointExporter; + +@Configuration(proxyBeanMethods = false) +public class MyWebSocketConfiguration { + + @Bean + public ServerEndpointExporter serverEndpointExporter() { + return new ServerEndpointExporter(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/webserver/discoverport/MyWebIntegrationTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/webserver/discoverport/MyWebIntegrationTests.java new file mode 100644 index 000000000000..520e6e3b1aa0 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/webserver/discoverport/MyWebIntegrationTests.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.webserver.discoverport; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.web.server.LocalServerPort; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +public class MyWebIntegrationTests { + + @LocalServerPort + int port; + + // ... + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/webserver/enablemultipleconnectorsintomcat/MyTomcatConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/webserver/enablemultipleconnectorsintomcat/MyTomcatConfiguration.java new file mode 100644 index 000000000000..218a9cc9d674 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/webserver/enablemultipleconnectorsintomcat/MyTomcatConfiguration.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.webserver.enablemultipleconnectorsintomcat; + +import java.io.IOException; +import java.net.URL; + +import org.apache.catalina.connector.Connector; +import org.apache.coyote.http11.Http11NioProtocol; + +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.ResourceUtils; + +@Configuration(proxyBeanMethods = false) +public class MyTomcatConfiguration { + + @Bean + public WebServerFactoryCustomizer sslConnectorCustomizer() { + return (tomcat) -> tomcat.addAdditionalTomcatConnectors(createSslConnector()); + } + + private Connector createSslConnector() { + Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol"); + Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler(); + try { + URL keystore = ResourceUtils.getURL("keystore"); + URL truststore = ResourceUtils.getURL("truststore"); + connector.setScheme("https"); + connector.setSecure(true); + connector.setPort(8443); + protocol.setSSLEnabled(true); + protocol.setKeystoreFile(keystore.toString()); + protocol.setKeystorePass("changeit"); + protocol.setTruststoreFile(truststore.toString()); + protocol.setTruststorePass("changeit"); + protocol.setKeyAlias("apitester"); + return connector; + } + catch (IOException ex) { + throw new IllegalStateException("Fail to create ssl connector", ex); + } + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/webserver/enablemultiplelistenersinundertow/MyUndertowConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/webserver/enablemultiplelistenersinundertow/MyUndertowConfiguration.java new file mode 100644 index 000000000000..35ab674a03c3 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/webserver/enablemultiplelistenersinundertow/MyUndertowConfiguration.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.webserver.enablemultiplelistenersinundertow; + +import io.undertow.Undertow.Builder; + +import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class MyUndertowConfiguration { + + @Bean + public WebServerFactoryCustomizer undertowListenerCustomizer() { + return (factory) -> factory.addBuilderCustomizers(this::addHttpListener); + } + + private Builder addHttpListener(Builder builder) { + return builder.addHttpListener(8080, "0.0.0.0"); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/webserver/usetomcatlegacycookieprocessor/MyLegacyCookieProcessorConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/webserver/usetomcatlegacycookieprocessor/MyLegacyCookieProcessorConfiguration.java new file mode 100644 index 000000000000..fb1702f99dd6 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/webserver/usetomcatlegacycookieprocessor/MyLegacyCookieProcessorConfiguration.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.webserver.usetomcatlegacycookieprocessor; + +import org.apache.tomcat.util.http.LegacyCookieProcessor; + +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class MyLegacyCookieProcessorConfiguration { + + @Bean + public WebServerFactoryCustomizer cookieProcessorCustomizer() { + return (factory) -> factory + .addContextCustomizers((context) -> context.setCookieProcessor(new LegacyCookieProcessor())); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/jdbc/BasicDataSourceExample.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/jdbc/BasicDataSourceExample.java deleted file mode 100644 index 674c361e64a5..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/jdbc/BasicDataSourceExample.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.jdbc; - -import javax.sql.DataSource; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.jdbc.DataSourceBuilder; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * Example configuration for configuring a very basic custom {@link DataSource}. - * - * @author Stephane Nicoll - */ -public class BasicDataSourceExample { - - /** - * A configuration that exposes an empty {@link DataSource}. - */ - @Configuration(proxyBeanMethods = false) - public static class BasicDataSourceConfiguration { - - // tag::configuration[] - @Bean - @ConfigurationProperties("app.datasource") - public DataSource dataSource() { - return DataSourceBuilder.create().build(); - } - // end::configuration[] - - } - -} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/jdbc/CompleteTwoDataSourcesExample.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/jdbc/CompleteTwoDataSourcesExample.java deleted file mode 100644 index 7526b6331de5..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/jdbc/CompleteTwoDataSourcesExample.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.jdbc; - -import com.zaxxer.hikari.HikariDataSource; -import org.apache.commons.dbcp2.BasicDataSource; - -import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; - -/** - * Example configuration for configuring two data sources with what Spring Boot does in - * auto-configuration. - * - * @author Stephane Nicoll - */ -public class CompleteTwoDataSourcesExample { - - /** - * A complete configuration that exposes two data sources. - */ - @Configuration - public static class CompleteDataSourcesConfiguration { - - // tag::configuration[] - @Bean - @Primary - @ConfigurationProperties("app.datasource.first") - public DataSourceProperties firstDataSourceProperties() { - return new DataSourceProperties(); - } - - @Bean - @Primary - @ConfigurationProperties("app.datasource.first.configuration") - public HikariDataSource firstDataSource() { - return firstDataSourceProperties().initializeDataSourceBuilder().type(HikariDataSource.class).build(); - } - - @Bean - @ConfigurationProperties("app.datasource.second") - public DataSourceProperties secondDataSourceProperties() { - return new DataSourceProperties(); - } - - @Bean - @ConfigurationProperties("app.datasource.second.configuration") - public BasicDataSource secondDataSource() { - return secondDataSourceProperties().initializeDataSourceBuilder().type(BasicDataSource.class).build(); - } - // end::configuration[] - - } - -} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/jdbc/ConfigurableDataSourceExample.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/jdbc/ConfigurableDataSourceExample.java deleted file mode 100644 index e9517f752f71..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/jdbc/ConfigurableDataSourceExample.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.jdbc; - -import javax.sql.DataSource; - -import com.zaxxer.hikari.HikariDataSource; - -import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; - -/** - * Example configuration for configuring a configurable custom {@link DataSource}. - * - * @author Stephane Nicoll - */ -public class ConfigurableDataSourceExample { - - /** - * A configuration that defines dedicated settings and reuses - * {@link DataSourceProperties}. - */ - @Configuration(proxyBeanMethods = false) - public static class ConfigurableDataSourceConfiguration { - - // tag::configuration[] - @Bean - @Primary - @ConfigurationProperties("app.datasource") - public DataSourceProperties dataSourceProperties() { - return new DataSourceProperties(); - } - - @Bean - @ConfigurationProperties("app.datasource.configuration") - public HikariDataSource dataSource(DataSourceProperties properties) { - return properties.initializeDataSourceBuilder().type(HikariDataSource.class).build(); - } - // end::configuration[] - - } - -} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/jdbc/SimpleDataSourceExample.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/jdbc/SimpleDataSourceExample.java deleted file mode 100644 index 7dec69c4b2f5..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/jdbc/SimpleDataSourceExample.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.jdbc; - -import javax.sql.DataSource; - -import com.zaxxer.hikari.HikariDataSource; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.jdbc.DataSourceBuilder; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * Example configuration for configuring a simple {@link DataSource}. - * - * @author Stephane Nicoll - */ -public class SimpleDataSourceExample { - - /** - * A simple configuration that exposes dedicated settings. - */ - @Configuration(proxyBeanMethods = false) - public static class SimpleDataSourceConfiguration { - - // tag::configuration[] - @Bean - @ConfigurationProperties("app.datasource") - public HikariDataSource dataSource() { - return DataSourceBuilder.create().type(HikariDataSource.class).build(); - } - // end::configuration[] - - } - -} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/jdbc/SimpleTwoDataSourcesExample.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/jdbc/SimpleTwoDataSourcesExample.java deleted file mode 100644 index 568d465da0c6..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/jdbc/SimpleTwoDataSourcesExample.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.jdbc; - -import javax.sql.DataSource; - -import com.zaxxer.hikari.HikariDataSource; -import org.apache.commons.dbcp2.BasicDataSource; - -import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.jdbc.DataSourceBuilder; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; - -/** - * Example configuration for configuring a configurable secondary {@link DataSource} while - * keeping the auto-configuration defaults for the primary one. - * - * @author Stephane Nicoll - */ -public class SimpleTwoDataSourcesExample { - - /** - * A simple configuration that exposes two data sources. - */ - @Configuration - public static class SimpleDataSourcesConfiguration { - - // tag::configuration[] - @Bean - @Primary - @ConfigurationProperties("app.datasource.first") - public DataSourceProperties firstDataSourceProperties() { - return new DataSourceProperties(); - } - - @Bean - @Primary - @ConfigurationProperties("app.datasource.first.configuration") - public HikariDataSource firstDataSource() { - return firstDataSourceProperties().initializeDataSourceBuilder().type(HikariDataSource.class).build(); - } - - @Bean - @ConfigurationProperties("app.datasource.second") - public BasicDataSource secondDataSource() { - return DataSourceBuilder.create().type(BasicDataSource.class).build(); - } - // end::configuration[] - - } - -} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/jersey/JerseySetStatusOverSendErrorExample.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/jersey/JerseySetStatusOverSendErrorExample.java deleted file mode 100644 index 0874f9f0e7ca..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/jersey/JerseySetStatusOverSendErrorExample.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.jersey; - -import java.util.Collections; - -import javax.servlet.http.HttpServletResponse; - -import org.glassfish.jersey.server.ResourceConfig; - -import org.springframework.stereotype.Component; - -/** - * Example configuration for a Jersey {@link ResourceConfig} configured to use - * {@link HttpServletResponse#setStatus(int)} rather than - * {@link HttpServletResponse#sendError(int)}. - * - * @author Andy Wilkinson - */ -public class JerseySetStatusOverSendErrorExample { - - // tag::resource-config[] - @Component - public class JerseyConfig extends ResourceConfig { - - public JerseyConfig() { - register(Endpoint.class); - setProperties(Collections.singletonMap("jersey.config.server.response.setStatusOverSendError", true)); - } - - } - // end::resource-config[] - - static class Endpoint { - - } - -} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/jpa/HibernateSecondLevelCacheExample.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/jpa/HibernateSecondLevelCacheExample.java deleted file mode 100644 index 75c9bce2027f..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/jpa/HibernateSecondLevelCacheExample.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.jpa; - -import org.hibernate.cache.jcache.ConfigSettings; - -import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer; -import org.springframework.cache.jcache.JCacheCacheManager; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * Example configuration of using JCache and Hibernate to enable second level caching. - * - * @author Stephane Nicoll - */ -// tag::configuration[] -@Configuration(proxyBeanMethods = false) -public class HibernateSecondLevelCacheExample { - - @Bean - public HibernatePropertiesCustomizer hibernateSecondLevelCacheCustomizer(JCacheCacheManager cacheManager) { - return (properties) -> properties.put(ConfigSettings.CACHE_MANAGER, cacheManager.getCacheManager()); - } - -} -// end::configuration[] diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/kafka/KafkaStreamsBeanExample.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/kafka/KafkaStreamsBeanExample.java deleted file mode 100644 index ebad610febf7..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/kafka/KafkaStreamsBeanExample.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.kafka; - -import org.apache.kafka.common.serialization.Serdes; -import org.apache.kafka.streams.KeyValue; -import org.apache.kafka.streams.StreamsBuilder; -import org.apache.kafka.streams.kstream.KStream; -import org.apache.kafka.streams.kstream.Produced; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.kafka.annotation.EnableKafkaStreams; -import org.springframework.kafka.support.serializer.JsonSerde; - -/** - * Example to show usage of {@link StreamsBuilder}. - * - * @author Stephane Nicoll - */ -public class KafkaStreamsBeanExample { - - // tag::configuration[] - @Configuration(proxyBeanMethods = false) - @EnableKafkaStreams - public static class KafkaStreamsExampleConfiguration { - - @Bean - public KStream kStream(StreamsBuilder streamsBuilder) { - KStream stream = streamsBuilder.stream("ks1In"); - stream.map((k, v) -> new KeyValue<>(k, v.toUpperCase())).to("ks1Out", - Produced.with(Serdes.Integer(), new JsonSerde<>())); - return stream; - } - - } - // end::configuration[] - -} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/test/autoconfigure/restdocs/restassured/AdvancedConfigurationExample.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/test/autoconfigure/restdocs/restassured/AdvancedConfigurationExample.java deleted file mode 100644 index 1e3247b893fc..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/test/autoconfigure/restdocs/restassured/AdvancedConfigurationExample.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.test.autoconfigure.restdocs.restassured; - -import org.springframework.boot.test.autoconfigure.restdocs.RestDocsRestAssuredConfigurationCustomizer; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.restdocs.restassured3.RestAssuredRestDocumentationConfigurer; -import org.springframework.restdocs.templates.TemplateFormats; - -public class AdvancedConfigurationExample { - - // tag::configuration[] - @TestConfiguration(proxyBeanMethods = false) - public static class CustomizationConfiguration implements RestDocsRestAssuredConfigurationCustomizer { - - @Override - public void customize(RestAssuredRestDocumentationConfigurer configurer) { - configurer.snippets().withTemplateFormat(TemplateFormats.markdown()); - } - - } - // end::configuration[] - -} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/test/autoconfigure/restdocs/restassured/UserDocumentationTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/test/autoconfigure/restdocs/restassured/UserDocumentationTests.java deleted file mode 100644 index 84f2ec13230d..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/test/autoconfigure/restdocs/restassured/UserDocumentationTests.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.test.autoconfigure.restdocs.restassured; - -// tag::source[] -import io.restassured.specification.RequestSpecification; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.boot.web.server.LocalServerPort; - -import static io.restassured.RestAssured.given; -import static org.hamcrest.Matchers.is; -import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document; - -@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -@AutoConfigureRestDocs -class UserDocumentationTests { - - @Test - void listUsers(@Autowired RequestSpecification documentationSpec, @LocalServerPort int port) { - given(documentationSpec).filter(document("list-users")).when().port(port).get("/").then().assertThat() - .statusCode(is(200)); - } - -} -// end::source[] diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/test/autoconfigure/restdocs/webclient/AdvancedConfigurationExample.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/test/autoconfigure/restdocs/webclient/AdvancedConfigurationExample.java deleted file mode 100644 index a5b574a59dcb..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/test/autoconfigure/restdocs/webclient/AdvancedConfigurationExample.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.test.autoconfigure.restdocs.webclient; - -import org.springframework.boot.test.autoconfigure.restdocs.RestDocsWebTestClientConfigurationCustomizer; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentationConfigurer; - -public class AdvancedConfigurationExample { - - // tag::configuration[] - @TestConfiguration(proxyBeanMethods = false) - public static class CustomizationConfiguration implements RestDocsWebTestClientConfigurationCustomizer { - - @Override - public void customize(WebTestClientRestDocumentationConfigurer configurer) { - configurer.snippets().withEncoding("UTF-8"); - } - - } - // end::configuration[] - -} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/test/autoconfigure/restdocs/webclient/UsersDocumentationTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/test/autoconfigure/restdocs/webclient/UsersDocumentationTests.java deleted file mode 100644 index 7f06bc090c13..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/test/autoconfigure/restdocs/webclient/UsersDocumentationTests.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.test.autoconfigure.restdocs.webclient; - -// tag::source[] -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; -import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; -import org.springframework.test.web.reactive.server.WebTestClient; - -import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document; - -@WebFluxTest -@AutoConfigureRestDocs -class UsersDocumentationTests { - - @Autowired - private WebTestClient webTestClient; - - @Test - void listUsers() { - this.webTestClient.get().uri("/").exchange().expectStatus().isOk().expectBody() - .consumeWith(document("list-users")); - } - -} -// end::source[] diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/test/context/ApplicationArgumentsExampleTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/test/context/ApplicationArgumentsExampleTests.java deleted file mode 100644 index 79625817166c..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/test/context/ApplicationArgumentsExampleTests.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.test.context; - -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.test.context.SpringBootTest; - -import static org.assertj.core.api.Assertions.assertThat; - -// tag::example[] -@SpringBootTest(args = "--app.test=one") -class ApplicationArgumentsExampleTests { - - @Test - void applicationArgumentsPopulated(@Autowired ApplicationArguments args) { - assertThat(args.getOptionNames()).containsOnly("app.test"); - assertThat(args.getOptionValues("app.test")).containsOnly("one"); - } - -} -// end::example[] diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/test/web/MockMvcExampleTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/test/web/MockMvcExampleTests.java deleted file mode 100644 index e6e6738363ed..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/test/web/MockMvcExampleTests.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.test.web; - -// tag::test-mock-mvc[] - -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.web.servlet.MockMvc; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@SpringBootTest -@AutoConfigureMockMvc -class MockMvcExampleTests { - - @Test - void exampleTest(@Autowired MockMvc mvc) throws Exception { - mvc.perform(get("/")).andExpect(status().isOk()).andExpect(content().string("Hello World")); - } - -} -// end::test-mock-mvc[] diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/test/web/MockWebTestClientExampleTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/test/web/MockWebTestClientExampleTests.java deleted file mode 100644 index 883f5ebd3822..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/test/web/MockWebTestClientExampleTests.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.test.web; - -// tag::test-mock-web-test-client[] - -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.web.reactive.server.WebTestClient; - -@SpringBootTest -@AutoConfigureWebTestClient -class MockWebTestClientExampleTests { - - @Test - void exampleTest(@Autowired WebTestClient webClient) { - webClient.get().uri("/").exchange().expectStatus().isOk().expectBody(String.class).isEqualTo("Hello World"); - } - -} -// end::test-mock-web-test-client[] diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/test/web/RandomPortTestRestTemplateExampleTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/test/web/RandomPortTestRestTemplateExampleTests.java deleted file mode 100644 index 32a93ad3d10c..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/test/web/RandomPortTestRestTemplateExampleTests.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.test.web; - -// tag::test-random-port[] - -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.boot.test.web.client.TestRestTemplate; - -import static org.assertj.core.api.Assertions.assertThat; - -@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -class RandomPortTestRestTemplateExampleTests { - - @Test - void exampleTest(@Autowired TestRestTemplate restTemplate) { - String body = restTemplate.getForObject("/", String.class); - assertThat(body).isEqualTo("Hello World"); - } - -} -// end::test-random-port[] diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/test/web/RandomPortWebTestClientExampleTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/test/web/RandomPortWebTestClientExampleTests.java deleted file mode 100644 index 9a800a3f3e46..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/test/web/RandomPortWebTestClientExampleTests.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.test.web; - -// tag::test-random-port[] - -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.test.web.reactive.server.WebTestClient; - -@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -class RandomPortWebTestClientExampleTests { - - @Test - void exampleTest(@Autowired WebTestClient webClient) { - webClient.get().uri("/").exchange().expectStatus().isOk().expectBody(String.class).isEqualTo("Hello World"); - } - -} -// end::test-random-port[] diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/autoconfiguration/disablingspecific/MyApplication.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/autoconfiguration/disablingspecific/MyApplication.java new file mode 100644 index 000000000000..8288ceb90d1b --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/autoconfiguration/disablingspecific/MyApplication.java @@ -0,0 +1,25 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.using.autoconfiguration.disablingspecific; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; + +@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class }) +public class MyApplication { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/springbeansanddependencyinjection/multipleconstructors/AccountService.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/springbeansanddependencyinjection/multipleconstructors/AccountService.java new file mode 100644 index 000000000000..7a7f367207a4 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/springbeansanddependencyinjection/multipleconstructors/AccountService.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.using.springbeansanddependencyinjection.multipleconstructors; + +public interface AccountService { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/springbeansanddependencyinjection/multipleconstructors/MyAccountService.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/springbeansanddependencyinjection/multipleconstructors/MyAccountService.java new file mode 100644 index 000000000000..7425c7de4401 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/springbeansanddependencyinjection/multipleconstructors/MyAccountService.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.using.springbeansanddependencyinjection.multipleconstructors; + +import java.io.PrintStream; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class MyAccountService implements AccountService { + + @SuppressWarnings("unused") + private final RiskAssessor riskAssessor; + + @SuppressWarnings("unused") + private final PrintStream out; + + @Autowired + public MyAccountService(RiskAssessor riskAssessor) { + this.riskAssessor = riskAssessor; + this.out = System.out; + } + + public MyAccountService(RiskAssessor riskAssessor, PrintStream out) { + this.riskAssessor = riskAssessor; + this.out = out; + } + + // ... + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/springbeansanddependencyinjection/multipleconstructors/RiskAssessor.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/springbeansanddependencyinjection/multipleconstructors/RiskAssessor.java new file mode 100644 index 000000000000..fcc89452d651 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/springbeansanddependencyinjection/multipleconstructors/RiskAssessor.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.using.springbeansanddependencyinjection.multipleconstructors; + +public interface RiskAssessor { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/springbeansanddependencyinjection/singleconstructor/AccountService.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/springbeansanddependencyinjection/singleconstructor/AccountService.java new file mode 100644 index 000000000000..676d061fa900 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/springbeansanddependencyinjection/singleconstructor/AccountService.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.using.springbeansanddependencyinjection.singleconstructor; + +public interface AccountService { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/springbeansanddependencyinjection/singleconstructor/MyAccountService.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/springbeansanddependencyinjection/singleconstructor/MyAccountService.java new file mode 100644 index 000000000000..f885f9c75067 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/springbeansanddependencyinjection/singleconstructor/MyAccountService.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.using.springbeansanddependencyinjection.singleconstructor; + +import org.springframework.stereotype.Service; + +@Service +public class MyAccountService implements AccountService { + + @SuppressWarnings("unused") + private final RiskAssessor riskAssessor; + + public MyAccountService(RiskAssessor riskAssessor) { + this.riskAssessor = riskAssessor; + } + + // ... + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/springbeansanddependencyinjection/singleconstructor/RiskAssessor.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/springbeansanddependencyinjection/singleconstructor/RiskAssessor.java new file mode 100644 index 000000000000..7a55aea32a41 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/springbeansanddependencyinjection/singleconstructor/RiskAssessor.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.using.springbeansanddependencyinjection.singleconstructor; + +public interface RiskAssessor { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/structuringyourcode/locatingthemainclass/MyApplication.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/structuringyourcode/locatingthemainclass/MyApplication.java new file mode 100644 index 000000000000..38542621e495 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/structuringyourcode/locatingthemainclass/MyApplication.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.using.structuringyourcode.locatingthemainclass; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class MyApplication { + + public static void main(String[] args) { + SpringApplication.run(MyApplication.class, args); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/usingthespringbootapplicationannotation/individualannotations/AnotherConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/usingthespringbootapplicationannotation/individualannotations/AnotherConfiguration.java new file mode 100644 index 000000000000..c8137d43484b --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/usingthespringbootapplicationannotation/individualannotations/AnotherConfiguration.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.using.usingthespringbootapplicationannotation.individualannotations; + +public class AnotherConfiguration { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/usingthespringbootapplicationannotation/individualannotations/MyApplication.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/usingthespringbootapplicationannotation/individualannotations/MyApplication.java new file mode 100644 index 000000000000..9f8b87e820f4 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/usingthespringbootapplicationannotation/individualannotations/MyApplication.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.using.usingthespringbootapplicationannotation.individualannotations; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.Import; + +@SpringBootConfiguration(proxyBeanMethods = false) +@EnableAutoConfiguration +@Import({ SomeConfiguration.class, AnotherConfiguration.class }) +public class MyApplication { + + public static void main(String[] args) { + SpringApplication.run(MyApplication.class, args); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/usingthespringbootapplicationannotation/individualannotations/SomeConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/usingthespringbootapplicationannotation/individualannotations/SomeConfiguration.java new file mode 100644 index 000000000000..c44268156931 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/usingthespringbootapplicationannotation/individualannotations/SomeConfiguration.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.using.usingthespringbootapplicationannotation.individualannotations; + +public class SomeConfiguration { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/usingthespringbootapplicationannotation/springapplication/MyApplication.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/usingthespringbootapplicationannotation/springapplication/MyApplication.java new file mode 100644 index 000000000000..ff2c5d3d3668 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/using/usingthespringbootapplicationannotation/springapplication/MyApplication.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.using.usingthespringbootapplicationannotation.springapplication; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +// Same as @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan +@SpringBootApplication +public class MyApplication { + + public static void main(String[] args) { + SpringApplication.run(MyApplication.class, args); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/client/RestTemplateProxyCustomizationExample.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/client/RestTemplateProxyCustomizationExample.java deleted file mode 100644 index ae9a13c66f59..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/client/RestTemplateProxyCustomizationExample.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.web.client; - -import org.apache.http.HttpException; -import org.apache.http.HttpHost; -import org.apache.http.HttpRequest; -import org.apache.http.client.HttpClient; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.impl.conn.DefaultProxyRoutePlanner; -import org.apache.http.protocol.HttpContext; - -import org.springframework.boot.web.client.RestTemplateCustomizer; -import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; -import org.springframework.web.client.RestTemplate; - -/** - * Example configuration for using a {@link RestTemplateCustomizer} to configure a proxy. - * - * @author Andy Wilkinson - */ -public class RestTemplateProxyCustomizationExample { - - /** - * A {@link RestTemplateCustomizer} that applies an HttpComponents-based request - * factory that is configured to use a proxy. - */ - // tag::customizer[] - static class ProxyCustomizer implements RestTemplateCustomizer { - - @Override - public void customize(RestTemplate restTemplate) { - HttpHost proxy = new HttpHost("proxy.example.com"); - HttpClient httpClient = HttpClientBuilder.create().setRoutePlanner(new DefaultProxyRoutePlanner(proxy) { - - @Override - public HttpHost determineProxy(HttpHost target, HttpRequest request, HttpContext context) - throws HttpException { - if (target.getHostName().equals("192.168.0.5")) { - return null; - } - return super.determineProxy(target, request, context); - } - - }).build(); - restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory(httpClient)); - } - - } - // end::customizer[] - -} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/reactive/function/client/ReactorNettyClientCustomizationExample.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/reactive/function/client/ReactorNettyClientCustomizationExample.java deleted file mode 100644 index 008e7da9448a..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/reactive/function/client/ReactorNettyClientCustomizationExample.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.web.reactive.function.client; - -import io.netty.channel.ChannelOption; -import io.netty.handler.timeout.ReadTimeoutHandler; -import reactor.netty.http.client.HttpClient; -import reactor.netty.tcp.TcpClient; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.client.reactive.ClientHttpConnector; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.http.client.reactive.ReactorResourceFactory; -import org.springframework.web.reactive.function.client.WebClient; - -/** - * Example configuration for customizing the Reactor Netty-based {@link WebClient}. - * - * @author Andy Wilkinson - */ -@Configuration(proxyBeanMethods = false) -public class ReactorNettyClientCustomizationExample { - - // tag::custom-http-connector[] - @Bean - ClientHttpConnector clientHttpConnector(ReactorResourceFactory resourceFactory) { - TcpClient tcpClient = TcpClient.create(resourceFactory.getConnectionProvider()) - .runOn(resourceFactory.getLoopResources()).option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 60000) - .doOnConnected((connection) -> connection.addHandlerLast(new ReadTimeoutHandler(60))); - return new ReactorClientHttpConnector(HttpClient.from(tcpClient)); - } - // end::custom-http-connector[] - -} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/security/CustomWebFluxSecurityExample.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/security/CustomWebFluxSecurityExample.java deleted file mode 100644 index 9db671e57ec8..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/security/CustomWebFluxSecurityExample.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.web.security; - -import org.springframework.boot.autoconfigure.security.reactive.PathRequest; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.web.server.ServerHttpSecurity; -import org.springframework.security.web.server.SecurityWebFilterChain; - -/** - * Example configuration for customizing security rules for a WebFlux application. - * - * @author Madhura Bhave - */ -@Configuration(proxyBeanMethods = false) -public class CustomWebFluxSecurityExample { - - // @formatter:off - // tag::configuration[] - @Bean - public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { - return http - .authorizeExchange() - .matchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() - .pathMatchers("/foo", "/bar") - .authenticated().and() - .formLogin().and() - .build(); - } - // end::configuration[] - // @formatter:on - -} diff --git a/spring-boot-project/spring-boot-docs/src/main/xslt/dependencyVersions.xsl b/spring-boot-project/spring-boot-docs/src/main/xslt/dependencyVersions.xsl deleted file mode 100644 index 1920ac831b39..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/xslt/dependencyVersions.xsl +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - |=== - | Group ID | Artifact ID | Version - - - - - | ` - - ` - | ` - - ` - | - - - - |=== - - - diff --git a/spring-boot-project/spring-boot-docs/src/main/xslt/versionProperties.xsl b/spring-boot-project/spring-boot-docs/src/main/xslt/versionProperties.xsl deleted file mode 100644 index f08f48949a6a..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/xslt/versionProperties.xsl +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - flattenedpom.version. - - . - - = - - - - - - diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/autoconfigure/UserServiceAutoConfigurationTests.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/autoconfigure/UserServiceAutoConfigurationTests.java deleted file mode 100644 index ddff07b9cf01..000000000000 --- a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/autoconfigure/UserServiceAutoConfigurationTests.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.autoconfigure; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.FilteredClassLoader; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link UserServiceAutoConfiguration}. - * - * @author Stephane Nicoll - */ -class UserServiceAutoConfigurationTests { - - // tag::runner[] - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(UserServiceAutoConfiguration.class)); - - // end::runner[] - - // tag::test-env[] - @Test - void serviceNameCanBeConfigured() { - this.contextRunner.withPropertyValues("user.name=test123").run((context) -> { - assertThat(context).hasSingleBean(UserService.class); - assertThat(context.getBean(UserService.class).getName()).isEqualTo("test123"); - }); - } - // end::test-env[] - - // tag::test-classloader[] - @Test - void serviceIsIgnoredIfLibraryIsNotPresent() { - this.contextRunner.withClassLoader(new FilteredClassLoader(UserService.class)) - .run((context) -> assertThat(context).doesNotHaveBean("userService")); - } - // end::test-classloader[] - - // tag::test-user-config[] - @Test - void defaultServiceBacksOff() { - this.contextRunner.withUserConfiguration(UserConfiguration.class).run((context) -> { - assertThat(context).hasSingleBean(UserService.class); - assertThat(context).getBean("myUserService").isSameAs(context.getBean(UserService.class)); - }); - } - - @Configuration(proxyBeanMethods = false) - static class UserConfiguration { - - @Bean - UserService myUserService() { - return new UserService("mine"); - } - - } - // end::test-user-config[] - -} diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/builder/SpringApplicationBuilderExampleTests.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/builder/SpringApplicationBuilderExampleTests.java deleted file mode 100644 index 3da2ba10d3e7..000000000000 --- a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/builder/SpringApplicationBuilderExampleTests.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.builder; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.boot.test.system.CapturedOutput; -import org.springframework.boot.test.system.OutputCaptureExtension; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link SpringApplicationBuilderExample}. - * - * @author Andy Wilkinson - */ -@ExtendWith(OutputCaptureExtension.class) -class SpringApplicationBuilderExampleTests { - - @Test - void contextHierarchyWithDisabledBanner(CapturedOutput output) { - System.setProperty("spring.main.web-application-type", "none"); - try { - new SpringApplicationBuilderExample().hierarchyWithDisabledBanner(new String[0]); - assertThat(output).doesNotContain(":: Spring Boot ::"); - } - finally { - System.clearProperty("spring.main.web-application-type"); - } - } - -} diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/context/EnvironmentPostProcessorExampleTests.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/context/EnvironmentPostProcessorExampleTests.java deleted file mode 100644 index 2dc4ff7f6f7c..000000000000 --- a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/context/EnvironmentPostProcessorExampleTests.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.context; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.SpringApplication; -import org.springframework.core.env.StandardEnvironment; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link EnvironmentPostProcessorExample}. - * - * @author Stephane Nicoll - */ -class EnvironmentPostProcessorExampleTests { - - private final StandardEnvironment environment = new StandardEnvironment(); - - @Test - void applyEnvironmentPostProcessor() { - assertThat(this.environment.containsProperty("test.foo.bar")).isFalse(); - new EnvironmentPostProcessorExample().postProcessEnvironment(this.environment, new SpringApplication()); - assertThat(this.environment.containsProperty("test.foo.bar")).isTrue(); - assertThat(this.environment.getProperty("test.foo.bar")).isEqualTo("value"); - } - -} diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/context/embedded/TomcatLegacyCookieProcessorExampleTests.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/context/embedded/TomcatLegacyCookieProcessorExampleTests.java deleted file mode 100644 index 5fd9dd02e3c3..000000000000 --- a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/context/embedded/TomcatLegacyCookieProcessorExampleTests.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.context.embedded; - -import org.apache.catalina.Context; -import org.apache.tomcat.util.http.LegacyCookieProcessor; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.docs.context.embedded.TomcatLegacyCookieProcessorExample.LegacyCookieProcessorConfiguration; -import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; -import org.springframework.boot.web.embedded.tomcat.TomcatWebServer; -import org.springframework.boot.web.server.WebServerFactoryCustomizerBeanPostProcessor; -import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link TomcatLegacyCookieProcessorExample}. - * - * @author Andy Wilkinson - */ -class TomcatLegacyCookieProcessorExampleTests { - - @Test - void cookieProcessorIsCustomized() { - ServletWebServerApplicationContext applicationContext = (ServletWebServerApplicationContext) new SpringApplication( - TestConfiguration.class, LegacyCookieProcessorConfiguration.class).run(); - Context context = (Context) ((TomcatWebServer) applicationContext.getWebServer()).getTomcat().getHost() - .findChildren()[0]; - assertThat(context.getCookieProcessor()).isInstanceOf(LegacyCookieProcessor.class); - } - - @Configuration(proxyBeanMethods = false) - static class TestConfiguration { - - @Bean - TomcatServletWebServerFactory tomcatFactory() { - return new TomcatServletWebServerFactory(0); - } - - @Bean - WebServerFactoryCustomizerBeanPostProcessor postProcessor() { - return new WebServerFactoryCustomizerBeanPostProcessor(); - } - - } - -} diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/context/properties/bind/AppSystemPropertiesTests.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/context/properties/bind/AppSystemPropertiesTests.java deleted file mode 100644 index 10b6997bc6f1..000000000000 --- a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/context/properties/bind/AppSystemPropertiesTests.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.context.properties.bind; - -import java.time.Duration; -import java.util.function.Consumer; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.test.context.assertj.AssertableApplicationContext; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.boot.test.context.runner.ContextConsumer; -import org.springframework.context.annotation.Configuration; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link AppSystemProperties}. - * - * @author Stephane Nicoll - */ -class AppSystemPropertiesTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withUserConfiguration(Config.class); - - @Test - void bindWithDefaultUnit() { - this.contextRunner.withPropertyValues("app.system.session-timeout=40", "app.system.read-timeout=5000") - .run(assertBinding((properties) -> { - assertThat(properties.getSessionTimeout()).isEqualTo(Duration.ofSeconds(40)); - assertThat(properties.getReadTimeout()).isEqualTo(Duration.ofMillis(5000)); - })); - } - - @Test - void bindWithExplicitUnit() { - this.contextRunner.withPropertyValues("app.system.session-timeout=1h", "app.system.read-timeout=5s") - .run(assertBinding((properties) -> { - assertThat(properties.getSessionTimeout()).isEqualTo(Duration.ofMinutes(60)); - assertThat(properties.getReadTimeout()).isEqualTo(Duration.ofMillis(5000)); - })); - } - - @Test - void bindWithIso8601Format() { - this.contextRunner.withPropertyValues("app.system.session-timeout=PT15S", "app.system.read-timeout=PT0.5S") - .run(assertBinding((properties) -> { - assertThat(properties.getSessionTimeout()).isEqualTo(Duration.ofSeconds(15)); - assertThat(properties.getReadTimeout()).isEqualTo(Duration.ofMillis(500)); - })); - } - - private ContextConsumer assertBinding(Consumer properties) { - return (context) -> { - assertThat(context).hasSingleBean(AppSystemProperties.class); - properties.accept(context.getBean(AppSystemProperties.class)); - }; - } - - @Configuration(proxyBeanMethods = false) - @EnableConfigurationProperties(AppSystemProperties.class) - static class Config { - - } - -} diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/features/developingautoconfiguration/testing/MyServiceAutoConfigurationTestsTests.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/features/developingautoconfiguration/testing/MyServiceAutoConfigurationTestsTests.java new file mode 100644 index 000000000000..b48f2d147a02 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/features/developingautoconfiguration/testing/MyServiceAutoConfigurationTestsTests.java @@ -0,0 +1,26 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.developingautoconfiguration.testing; + +/** + * Tests for {@link MyServiceAutoConfigurationTests}. + * + * @author Stephane Nicoll + */ +class MyServiceAutoConfigurationTestsTests extends MyServiceAutoConfigurationTests { + +} diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/conversion/durations/constructorbinding/MyPropertiesTests.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/conversion/durations/constructorbinding/MyPropertiesTests.java new file mode 100644 index 000000000000..6e3d9c6a3cda --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/conversion/durations/constructorbinding/MyPropertiesTests.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.conversion.durations.constructorbinding; + +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.ContextConsumer; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MyProperties}. + * + * @author Stephane Nicoll + */ +class MyPropertiesTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withUserConfiguration(Config.class); + + @Test + void bindWithDefaultUnit() { + this.contextRunner.withPropertyValues("my.session-timeout=40", "my.read-timeout=5000") + .run(assertBinding((properties) -> { + assertThat(properties.getSessionTimeout()).hasSeconds(40); + assertThat(properties.getReadTimeout()).hasMillis(5000); + })); + } + + @Test + void bindWithExplicitUnit() { + this.contextRunner.withPropertyValues("my.session-timeout=1h", "my.read-timeout=5s") + .run(assertBinding((properties) -> { + assertThat(properties.getSessionTimeout()).hasMinutes(60); + assertThat(properties.getReadTimeout()).hasMillis(5000); + })); + } + + @Test + void bindWithIso8601Format() { + this.contextRunner.withPropertyValues("my.session-timeout=PT15S", "my.read-timeout=PT0.5S") + .run(assertBinding((properties) -> { + assertThat(properties.getSessionTimeout()).hasSeconds(15); + assertThat(properties.getReadTimeout()).hasMillis(500); + })); + } + + private ContextConsumer assertBinding(Consumer properties) { + return (context) -> { + assertThat(context).hasSingleBean(MyProperties.class); + properties.accept(context.getBean(MyProperties.class)); + }; + } + + @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties(MyProperties.class) + static class Config { + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/conversion/durations/javabeanbinding/MyPropertiesTests.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/conversion/durations/javabeanbinding/MyPropertiesTests.java new file mode 100644 index 000000000000..d5652489ffc1 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/features/externalconfig/typesafeconfigurationproperties/conversion/durations/javabeanbinding/MyPropertiesTests.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.externalconfig.typesafeconfigurationproperties.conversion.durations.javabeanbinding; + +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.ContextConsumer; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MyProperties}. + * + * @author Stephane Nicoll + */ +class MyPropertiesTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withUserConfiguration(Config.class); + + @Test + void bindWithDefaultUnit() { + this.contextRunner.withPropertyValues("my.session-timeout=40", "my.read-timeout=5000") + .run(assertBinding((properties) -> { + assertThat(properties.getSessionTimeout()).hasSeconds(40); + assertThat(properties.getReadTimeout()).hasMillis(5000); + })); + } + + @Test + void bindWithExplicitUnit() { + this.contextRunner.withPropertyValues("my.session-timeout=1h", "my.read-timeout=5s") + .run(assertBinding((properties) -> { + assertThat(properties.getSessionTimeout()).hasMinutes(60); + assertThat(properties.getReadTimeout()).hasMillis(5000); + })); + } + + @Test + void bindWithIso8601Format() { + this.contextRunner.withPropertyValues("my.session-timeout=PT15S", "my.read-timeout=PT0.5S") + .run(assertBinding((properties) -> { + assertThat(properties.getSessionTimeout()).hasSeconds(15); + assertThat(properties.getReadTimeout()).hasMillis(500); + })); + } + + private ContextConsumer assertBinding(Consumer properties) { + return (context) -> { + assertThat(context).hasSingleBean(MyProperties.class); + properties.accept(context.getBean(MyProperties.class)); + }; + } + + @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties(MyProperties.class) + static class Config { + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/features/springapplication/fluentbuilderapi/MyApplicationTests.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/features/springapplication/fluentbuilderapi/MyApplicationTests.java new file mode 100644 index 000000000000..d78dda2622e7 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/features/springapplication/fluentbuilderapi/MyApplicationTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.springapplication.fluentbuilderapi; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MyApplication}. + * + * @author Andy Wilkinson + */ +@ExtendWith(OutputCaptureExtension.class) +class MyApplicationTests { + + @Test + void contextHierarchyWithDisabledBanner(CapturedOutput output) { + System.setProperty("spring.main.web-application-type", "none"); + try { + new MyApplication().hierarchyWithDisabledBanner(new String[0]); + assertThat(output).doesNotContain(":: Spring Boot ::"); + } + finally { + System.clearProperty("spring.main.web-application-type"); + } + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/features/testing/springbootapplications/jmx/MyJmxTestsTests.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/features/testing/springbootapplications/jmx/MyJmxTestsTests.java new file mode 100644 index 000000000000..fb9e56d21734 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/features/testing/springbootapplications/jmx/MyJmxTestsTests.java @@ -0,0 +1,26 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.jmx; + +/** + * Tests for SampleJmxTests + * + * @author Stephane Nicoll + */ +class MyJmxTestsTests extends MyJmxTests { + +} diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/features/testing/utilities/outputcapture/MyOutputCaptureTestsTests.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/features/testing/utilities/outputcapture/MyOutputCaptureTestsTests.java new file mode 100644 index 000000000000..d7f587b283b3 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/features/testing/utilities/outputcapture/MyOutputCaptureTestsTests.java @@ -0,0 +1,26 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.utilities.outputcapture; + +/** + * Tests for {@link MyOutputCaptureTests}. + * + * @author Stephane Nicoll + */ +class MyOutputCaptureTestsTests extends MyOutputCaptureTests { + +} diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/features/testing/utilities/testresttemplate/MySpringBootTestsTests.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/features/testing/utilities/testresttemplate/MySpringBootTestsTests.java new file mode 100644 index 000000000000..d91bc5fb7e9a --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/features/testing/utilities/testresttemplate/MySpringBootTestsTests.java @@ -0,0 +1,26 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.utilities.testresttemplate; + +/** + * Tests for {@link MySpringBootTests}. + * + * @author Stephane Nicoll + */ +class MySpringBootTestsTests extends MySpringBootTests { + +} diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/howto/actuator/maphealthindicatorstometrics/MetricsHealthMicrometerExportTests.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/howto/actuator/maphealthindicatorstometrics/MetricsHealthMicrometerExportTests.java new file mode 100644 index 000000000000..3671f7eb37a3 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/howto/actuator/maphealthindicatorstometrics/MetricsHealthMicrometerExportTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.actuator.maphealthindicatorstometrics; + +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MetricsHealthMicrometerExport}. + * + * @author Phillip Webb + */ +@SpringBootTest +class MetricsHealthMicrometerExportTests { + + @Autowired + private MeterRegistry registry; + + @Test + void registryExportsHealth() { + Gauge gauge = this.registry.get("health").gauge(); + assertThat(gauge.value()).isEqualTo(2); + } + + @Configuration(proxyBeanMethods = false) + @Import(MyHealthMetricsExportConfiguration.class) + @ImportAutoConfiguration(classes = { HealthContributorAutoConfiguration.class, MetricsAutoConfiguration.class, + HealthEndpointAutoConfiguration.class }) + static class Config { + + @Bean + MetricsHealthMicrometerExport example() { + return new MetricsHealthMicrometerExport(); + } + + @Bean + SimpleMeterRegistry simpleMeterRegistry() { + return new SimpleMeterRegistry(); + } + + @Bean + HealthIndicator outOfService() { + return () -> new Health.Builder().outOfService().build(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/howto/dataaccess/SampleApp.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/howto/dataaccess/SampleApp.java new file mode 100644 index 000000000000..230ccc2ad7cb --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/howto/dataaccess/SampleApp.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.dataaccess; + +import javax.sql.DataSource; + +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; + +/** + * A sample {@link SpringBootConfiguration @ConfigurationProperties} that only enables the + * auto-configuration for the {@link DataSource}. + * + * @author Stephane Nicoll + */ +@SpringBootConfiguration +@ImportAutoConfiguration(DataSourceAutoConfiguration.class) +class SampleApp { + +} diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/howto/dataaccess/configurecustomdatasource/MyDataSourceConfigurationTests.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/howto/dataaccess/configurecustomdatasource/MyDataSourceConfigurationTests.java new file mode 100644 index 000000000000..d88554d67ce5 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/howto/dataaccess/configurecustomdatasource/MyDataSourceConfigurationTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.dataaccess.configurecustomdatasource; + +import java.sql.SQLException; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.docs.howto.dataaccess.configurecustomdatasource.builder.MyDataSourceConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for {@link MyDataSourceConfiguration}. + * + * @author Stephane Nicoll + */ +@ExtendWith(SpringExtension.class) +@SpringBootTest(properties = "app.datasource.jdbcUrl=jdbc:h2:mem:basic;DB_CLOSE_DELAY=-1") +@Import(MyDataSourceConfiguration.class) +class MyDataSourceConfigurationTests { + + @Autowired + private ApplicationContext context; + + @Test + void validateConfiguration() throws SQLException { + assertThat(this.context.getBeansOfType(DataSource.class)).hasSize(1); + DataSource dataSource = this.context.getBean(DataSource.class); + assertThat(dataSource.getConnection().getMetaData().getURL()).isEqualTo("jdbc:h2:mem:basic"); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/howto/dataaccess/configurecustomdatasource/configurable/MyDataSourceConfigurationTests.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/howto/dataaccess/configurecustomdatasource/configurable/MyDataSourceConfigurationTests.java new file mode 100644 index 000000000000..4a9f09c21c0c --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/howto/dataaccess/configurecustomdatasource/configurable/MyDataSourceConfigurationTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.dataaccess.configurecustomdatasource.configurable; + +import java.sql.SQLException; + +import javax.sql.DataSource; + +import com.zaxxer.hikari.HikariDataSource; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for {@link MyDataSourceConfiguration}. + * + * @author Stephane Nicoll + */ +@ExtendWith(SpringExtension.class) +@SpringBootTest(properties = { "app.datasource.url=jdbc:h2:mem:configurable;DB_CLOSE_DELAY=-1", + "app.datasource.configuration.maximum-pool-size=42" }) +@Import(MyDataSourceConfiguration.class) +class MyDataSourceConfigurationTests { + + @Autowired + private ApplicationContext context; + + @Test + void validateConfiguration() throws SQLException { + assertThat(this.context.getBeansOfType(DataSource.class)).hasSize(1); + HikariDataSource dataSource = this.context.getBean(HikariDataSource.class); + assertThat(dataSource.getConnection().getMetaData().getURL()).isEqualTo("jdbc:h2:mem:configurable"); + assertThat(dataSource.getMaximumPoolSize()).isEqualTo(42); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/howto/dataaccess/configurecustomdatasource/simple/MyDataSourceConfigurationTests.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/howto/dataaccess/configurecustomdatasource/simple/MyDataSourceConfigurationTests.java new file mode 100644 index 000000000000..bf717145aac1 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/howto/dataaccess/configurecustomdatasource/simple/MyDataSourceConfigurationTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.dataaccess.configurecustomdatasource.simple; + +import java.sql.SQLException; + +import javax.sql.DataSource; + +import com.zaxxer.hikari.HikariDataSource; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for {@link MyDataSourceConfiguration}. + * + * @author Stephane Nicoll + */ +@ExtendWith(SpringExtension.class) +@SpringBootTest(properties = { "app.datasource.jdbc-url=jdbc:h2:mem:simple;DB_CLOSE_DELAY=-1", + "app.datasource.maximum-pool-size=42" }) +@Import(MyDataSourceConfiguration.class) +class MyDataSourceConfigurationTests { + + @Autowired + private ApplicationContext context; + + @Test + void validateConfiguration() throws SQLException { + assertThat(this.context.getBeansOfType(DataSource.class)).hasSize(1); + HikariDataSource dataSource = this.context.getBean(HikariDataSource.class); + assertThat(dataSource.getConnection().getMetaData().getURL()).isEqualTo("jdbc:h2:mem:simple"); + assertThat(dataSource.getMaximumPoolSize()).isEqualTo(42); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/howto/dataaccess/configuretwodatasources/MyCompleteDataSourcesConfigurationTests.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/howto/dataaccess/configuretwodatasources/MyCompleteDataSourcesConfigurationTests.java new file mode 100644 index 000000000000..aa3be89a0cd4 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/howto/dataaccess/configuretwodatasources/MyCompleteDataSourcesConfigurationTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.dataaccess.configuretwodatasources; + +import java.sql.SQLException; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MyCompleteDataSourcesConfiguration}. + * + * @author Stephane Nicoll + */ +@ExtendWith(SpringExtension.class) +@SpringBootTest +@Import(MyCompleteDataSourcesConfiguration.class) +class MyCompleteDataSourcesConfigurationTests { + + @Autowired + private ApplicationContext context; + + @Test + void validateConfiguration() throws SQLException { + assertThat(this.context.getBeansOfType(DataSource.class)).hasSize(2); + DataSource dataSource = this.context.getBean(DataSource.class); + assertThat(this.context.getBean("firstDataSource")).isSameAs(dataSource); + assertThat(dataSource.getConnection().getMetaData().getURL()).startsWith("jdbc:h2:mem:"); + DataSource secondDataSource = this.context.getBean("secondDataSource", DataSource.class); + assertThat(secondDataSource.getConnection().getMetaData().getURL()).startsWith("jdbc:h2:mem:"); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/howto/dataaccess/configuretwodatasources/MyDataSourcesConfigurationTests.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/howto/dataaccess/configuretwodatasources/MyDataSourcesConfigurationTests.java new file mode 100644 index 000000000000..6034bae7659d --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/howto/dataaccess/configuretwodatasources/MyDataSourcesConfigurationTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.dataaccess.configuretwodatasources; + +import java.sql.SQLException; + +import javax.sql.DataSource; + +import org.apache.commons.dbcp2.BasicDataSource; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MyDataSourcesConfiguration}. + * + * @author Stephane Nicoll + */ +@ExtendWith(SpringExtension.class) +@SpringBootTest(properties = { "app.datasource.second.url=jdbc:h2:mem:bar;DB_CLOSE_DELAY=-1", + "app.datasource.second.max-total=42" }) +@Import(MyDataSourcesConfiguration.class) +class MyDataSourcesConfigurationTests { + + @Autowired + private ApplicationContext context; + + @Test + void validateConfiguration() throws SQLException { + assertThat(this.context.getBeansOfType(DataSource.class)).hasSize(2); + DataSource dataSource = this.context.getBean(DataSource.class); + assertThat(this.context.getBean("firstDataSource")).isSameAs(dataSource); + assertThat(dataSource.getConnection().getMetaData().getURL()).startsWith("jdbc:h2:mem:"); + BasicDataSource secondDataSource = this.context.getBean("secondDataSource", BasicDataSource.class); + assertThat(secondDataSource.getUrl()).isEqualTo("jdbc:h2:mem:bar;DB_CLOSE_DELAY=-1"); + assertThat(secondDataSource.getMaxTotal()).isEqualTo(42); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/howto/springbootapplication/MyEnvironmentPostProcessorTests.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/howto/springbootapplication/MyEnvironmentPostProcessorTests.java new file mode 100644 index 000000000000..3b271f94cc86 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/howto/springbootapplication/MyEnvironmentPostProcessorTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.springbootapplication; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.docs.howto.application.customizetheenvironmentorapplicationcontext.MyEnvironmentPostProcessor; +import org.springframework.core.env.StandardEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MyEnvironmentPostProcessor}. + * + * @author Stephane Nicoll + */ +class MyEnvironmentPostProcessorTests { + + private final StandardEnvironment environment = new StandardEnvironment(); + + @Test + void applyEnvironmentPostProcessor() { + assertThat(this.environment.containsProperty("test.foo.bar")).isFalse(); + new MyEnvironmentPostProcessor().postProcessEnvironment(this.environment, new SpringApplication()); + assertThat(this.environment.containsProperty("test.foo.bar")).isTrue(); + assertThat(this.environment.getProperty("test.foo.bar")).isEqualTo("value"); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/howto/webserver/usetomcatlegacycookieprocessor/MyLegacyCookieProcessorConfigurationTests.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/howto/webserver/usetomcatlegacycookieprocessor/MyLegacyCookieProcessorConfigurationTests.java new file mode 100644 index 000000000000..29fde8b168ba --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/howto/webserver/usetomcatlegacycookieprocessor/MyLegacyCookieProcessorConfigurationTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.howto.webserver.usetomcatlegacycookieprocessor; + +import org.apache.catalina.Context; +import org.apache.tomcat.util.http.LegacyCookieProcessor; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.boot.web.embedded.tomcat.TomcatWebServer; +import org.springframework.boot.web.server.WebServerFactoryCustomizerBeanPostProcessor; +import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MyLegacyCookieProcessorConfiguration}. + * + * @author Andy Wilkinson + */ +class MyLegacyCookieProcessorConfigurationTests { + + @Test + void cookieProcessorIsCustomized() { + ServletWebServerApplicationContext applicationContext = (ServletWebServerApplicationContext) new SpringApplication( + TestConfiguration.class, MyLegacyCookieProcessorConfiguration.class).run(); + Context context = (Context) ((TomcatWebServer) applicationContext.getWebServer()).getTomcat().getHost() + .findChildren()[0]; + assertThat(context.getCookieProcessor()).isInstanceOf(LegacyCookieProcessor.class); + } + + @Configuration(proxyBeanMethods = false) + static class TestConfiguration { + + @Bean + TomcatServletWebServerFactory tomcatFactory() { + return new TomcatServletWebServerFactory(0); + } + + @Bean + WebServerFactoryCustomizerBeanPostProcessor postProcessor() { + return new WebServerFactoryCustomizerBeanPostProcessor(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/jdbc/BasicDataSourceExampleTests.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/jdbc/BasicDataSourceExampleTests.java deleted file mode 100644 index 2d441f5c4069..000000000000 --- a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/jdbc/BasicDataSourceExampleTests.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.jdbc; - -import java.sql.SQLException; - -import javax.sql.DataSource; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Import; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Test for {@link BasicDataSourceExample}. - * - * @author Stephane Nicoll - */ -@ExtendWith(SpringExtension.class) -@SpringBootTest(properties = "app.datasource.jdbcUrl=jdbc:h2:mem:basic;DB_CLOSE_DELAY=-1") -@Import(BasicDataSourceExample.BasicDataSourceConfiguration.class) -class BasicDataSourceExampleTests { - - @Autowired - private ApplicationContext context; - - @Test - void validateConfiguration() throws SQLException { - assertThat(this.context.getBeansOfType(DataSource.class)).hasSize(1); - DataSource dataSource = this.context.getBean(DataSource.class); - assertThat(dataSource.getConnection().getMetaData().getURL()).isEqualTo("jdbc:h2:mem:basic"); - } - -} diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/jdbc/CompleteTwoDataSourcesExampleTests.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/jdbc/CompleteTwoDataSourcesExampleTests.java deleted file mode 100644 index 10a801536b2b..000000000000 --- a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/jdbc/CompleteTwoDataSourcesExampleTests.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.jdbc; - -import java.sql.SQLException; - -import javax.sql.DataSource; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Import; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link CompleteTwoDataSourcesExample}. - * - * @author Stephane Nicoll - */ -@ExtendWith(SpringExtension.class) -@SpringBootTest -@Import(CompleteTwoDataSourcesExample.CompleteDataSourcesConfiguration.class) -class CompleteTwoDataSourcesExampleTests { - - @Autowired - private ApplicationContext context; - - @Test - void validateConfiguration() throws SQLException { - assertThat(this.context.getBeansOfType(DataSource.class)).hasSize(2); - DataSource dataSource = this.context.getBean(DataSource.class); - assertThat(this.context.getBean("firstDataSource")).isSameAs(dataSource); - assertThat(dataSource.getConnection().getMetaData().getURL()).startsWith("jdbc:h2:mem:"); - DataSource secondDataSource = this.context.getBean("secondDataSource", DataSource.class); - assertThat(secondDataSource.getConnection().getMetaData().getURL()).startsWith("jdbc:h2:mem:"); - } - -} diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/jdbc/ConfigurableDataSourceExampleTests.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/jdbc/ConfigurableDataSourceExampleTests.java deleted file mode 100644 index 0e290c19f434..000000000000 --- a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/jdbc/ConfigurableDataSourceExampleTests.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.jdbc; - -import java.sql.SQLException; - -import javax.sql.DataSource; - -import com.zaxxer.hikari.HikariDataSource; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Import; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Test for {@link SimpleDataSourceExample}. - * - * @author Stephane Nicoll - */ -@ExtendWith(SpringExtension.class) -@SpringBootTest(properties = { "app.datasource.url=jdbc:h2:mem:configurable;DB_CLOSE_DELAY=-1", - "app.datasource.configuration.maximum-pool-size=42" }) -@Import(ConfigurableDataSourceExample.ConfigurableDataSourceConfiguration.class) -class ConfigurableDataSourceExampleTests { - - @Autowired - private ApplicationContext context; - - @Test - void validateConfiguration() throws SQLException { - assertThat(this.context.getBeansOfType(DataSource.class)).hasSize(1); - HikariDataSource dataSource = this.context.getBean(HikariDataSource.class); - assertThat(dataSource.getConnection().getMetaData().getURL()).isEqualTo("jdbc:h2:mem:configurable"); - assertThat(dataSource.getMaximumPoolSize()).isEqualTo(42); - } - -} diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/jdbc/SampleApp.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/jdbc/SampleApp.java deleted file mode 100644 index 442293507327..000000000000 --- a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/jdbc/SampleApp.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.jdbc; - -import javax.sql.DataSource; - -import org.springframework.boot.SpringBootConfiguration; -import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; - -/** - * A sample {@link SpringBootConfiguration @ConfigurationProperties} that only enables the - * auto-configuration for the {@link DataSource}. - * - * @author Stephane Nicoll - */ -@SpringBootConfiguration -@ImportAutoConfiguration(DataSourceAutoConfiguration.class) -class SampleApp { - -} diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/jdbc/SimpleDataSourceExampleTests.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/jdbc/SimpleDataSourceExampleTests.java deleted file mode 100644 index f64d9b876560..000000000000 --- a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/jdbc/SimpleDataSourceExampleTests.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.jdbc; - -import java.sql.SQLException; - -import javax.sql.DataSource; - -import com.zaxxer.hikari.HikariDataSource; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Import; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Test for {@link SimpleDataSourceExample}. - * - * @author Stephane Nicoll - */ -@ExtendWith(SpringExtension.class) -@SpringBootTest(properties = { "app.datasource.jdbc-url=jdbc:h2:mem:simple;DB_CLOSE_DELAY=-1", - "app.datasource.maximum-pool-size=42" }) -@Import(SimpleDataSourceExample.SimpleDataSourceConfiguration.class) -class SimpleDataSourceExampleTests { - - @Autowired - private ApplicationContext context; - - @Test - void validateConfiguration() throws SQLException { - assertThat(this.context.getBeansOfType(DataSource.class)).hasSize(1); - HikariDataSource dataSource = this.context.getBean(HikariDataSource.class); - assertThat(dataSource.getConnection().getMetaData().getURL()).isEqualTo("jdbc:h2:mem:simple"); - assertThat(dataSource.getMaximumPoolSize()).isEqualTo(42); - } - -} diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/jdbc/SimpleTwoDataSourcesExampleTests.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/jdbc/SimpleTwoDataSourcesExampleTests.java deleted file mode 100644 index 1866436a3915..000000000000 --- a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/jdbc/SimpleTwoDataSourcesExampleTests.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.jdbc; - -import java.sql.SQLException; - -import javax.sql.DataSource; - -import org.apache.commons.dbcp2.BasicDataSource; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Import; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link SimpleTwoDataSourcesExample}. - * - * @author Stephane Nicoll - */ -@ExtendWith(SpringExtension.class) -@SpringBootTest(properties = { "app.datasource.second.url=jdbc:h2:mem:bar;DB_CLOSE_DELAY=-1", - "app.datasource.second.max-total=42" }) -@Import(SimpleTwoDataSourcesExample.SimpleDataSourcesConfiguration.class) -class SimpleTwoDataSourcesExampleTests { - - @Autowired - private ApplicationContext context; - - @Test - void validateConfiguration() throws SQLException { - assertThat(this.context.getBeansOfType(DataSource.class)).hasSize(2); - DataSource dataSource = this.context.getBean(DataSource.class); - assertThat(this.context.getBean("firstDataSource")).isSameAs(dataSource); - assertThat(dataSource.getConnection().getMetaData().getURL()).startsWith("jdbc:h2:mem:"); - BasicDataSource secondDataSource = this.context.getBean("secondDataSource", BasicDataSource.class); - assertThat(secondDataSource.getUrl()).isEqualTo("jdbc:h2:mem:bar;DB_CLOSE_DELAY=-1"); - assertThat(secondDataSource.getMaxTotal()).isEqualTo(42); - } - -} diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/jmx/SampleApp.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/jmx/SampleApp.java deleted file mode 100644 index ea5ecad61ff7..000000000000 --- a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/jmx/SampleApp.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.jmx; - -import org.springframework.boot.SpringBootConfiguration; -import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration; - -/** - * A sample {@link SpringBootConfiguration @ConfigurationProperties} that only enables JMX - * auto-configuration. - * - * @author Stephane Nicoll - */ -@SpringBootConfiguration -@ImportAutoConfiguration(JmxAutoConfiguration.class) -public class SampleApp { - -} diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/jmx/SampleJmxTests.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/jmx/SampleJmxTests.java deleted file mode 100644 index c469e6daa32e..000000000000 --- a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/jmx/SampleJmxTests.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.jmx; - -import javax.management.MBeanServer; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -/** - * Example integration test that uses JMX. - * - * @author Stephane Nicoll - */ -@SuppressWarnings("unused") -// tag::test[] -@ExtendWith(SpringExtension.class) -@SpringBootTest(properties = "spring.jmx.enabled=true") -@DirtiesContext -class SampleJmxTests { - - @Autowired - private MBeanServer mBeanServer; - - @Test - void exampleTest() { - // ... - } - -} -// end::test[] diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/test/system/OutputCaptureTests.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/test/system/OutputCaptureTests.java deleted file mode 100644 index 8f9a3731fcee..000000000000 --- a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/test/system/OutputCaptureTests.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.test.system; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.boot.test.system.CapturedOutput; -import org.springframework.boot.test.system.OutputCaptureExtension; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Sample showcasing the use of {@link CapturedOutput}. - * - * @author Stephane Nicoll - */ -// tag::test[] -@ExtendWith(OutputCaptureExtension.class) -class OutputCaptureTests { - - @Test - void testName(CapturedOutput output) { - System.out.println("Hello World!"); - assertThat(output).contains("World"); - } - -} -// end::test[] diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/web/client/SampleWebClientConfiguration.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/web/client/SampleWebClientConfiguration.java deleted file mode 100644 index f96e46538bac..000000000000 --- a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/web/client/SampleWebClientConfiguration.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.web.client; - -import java.net.URI; - -import org.springframework.boot.SpringBootConfiguration; -import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; -import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; -import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; -import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -/** - * A sample {@link SpringBootConfiguration @ConfigurationProperties} with an example - * controller. - * - * @author Stephane Nicoll - */ -@SpringBootConfiguration -@ImportAutoConfiguration({ ServletWebServerFactoryAutoConfiguration.class, DispatcherServletAutoConfiguration.class, - JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class }) -class SampleWebClientConfiguration { - - @RestController - private static class ExampleController { - - @RequestMapping("/example") - ResponseEntity example() { - return ResponseEntity.ok().location(URI.create("https://other.example.com/example")).body("test"); - } - - } - -} diff --git a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/web/client/SampleWebClientTests.java b/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/web/client/SampleWebClientTests.java deleted file mode 100644 index 117b892a3bda..000000000000 --- a/spring-boot-project/spring-boot-docs/src/test/java/org/springframework/boot/docs/web/client/SampleWebClientTests.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.docs.web.client; - -import java.time.Duration; - -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.boot.web.client.RestTemplateBuilder; -import org.springframework.context.annotation.Bean; -import org.springframework.http.HttpHeaders; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Example integration test that uses {@link TestRestTemplate}. - * - * @author Stephane Nicoll - */ -// tag::test[] -@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -class SampleWebClientTests { - - @Autowired - private TestRestTemplate template; - - @Test - void testRequest() { - HttpHeaders headers = this.template.getForEntity("/example", String.class).getHeaders(); - assertThat(headers.getLocation()).hasHost("other.example.com"); - } - - @TestConfiguration(proxyBeanMethods = false) - static class Config { - - @Bean - RestTemplateBuilder restTemplateBuilder() { - return new RestTemplateBuilder().setConnectTimeout(Duration.ofSeconds(1)) - .setReadTimeout(Duration.ofSeconds(1)); - } - - } - -} -// end::test[] diff --git a/spring-boot-project/spring-boot-parent/build.gradle b/spring-boot-project/spring-boot-parent/build.gradle new file mode 100644 index 000000000000..72cc1cbc3e04 --- /dev/null +++ b/spring-boot-project/spring-boot-parent/build.gradle @@ -0,0 +1,184 @@ +plugins { + id "org.springframework.boot.bom" + id "org.springframework.boot.conventions" + id "org.springframework.boot.deployed" +} + +description = "Spring Boot Parent" + +bom { + upgrade { + policy = "same-major-version" + gitHub { + issueLabels = ["type: task"] + } + } + library("Android JSON", "0.0.20131108.vaadin1") { + group("com.vaadin.external.google") { + modules = [ + "android-json" + ] + } + } + library("Commons Compress", "1.21") { + group("org.apache.commons") { + modules = [ + "commons-compress" + ] + } + } + library("Commons FileUpload", "1.4") { + group("commons-fileupload") { + modules = [ + "commons-fileupload" + ] + } + } + library("Jakarta Inject", "1.0.5") { + group("jakarta.inject") { + modules = [ + "jakarta.inject-api" + ] + } + } + library("JLine", "2.11") { + prohibit("[2.12,)") { + because "it contains breaking changes" + } + group("jline") { + modules = [ + "jline" + ] + } + } + library("JNA", "5.7.0") { + group("net.java.dev.jna") { + modules = [ + "jna-platform" + ] + } + } + library("JOpt Simple", "5.0.4") { + group("net.sf.jopt-simple") { + modules = [ + "jopt-simple" + ] + } + } + library("Maven", "3.6.3") { + group("org.apache.maven") { + modules = [ + "maven-plugin-api", + "maven-resolver-provider", + "maven-settings-builder" + ] + } + } + library("Maven Common Artifact Filters", "3.2.0") { + group("org.apache.maven.shared") { + modules = [ + "maven-common-artifact-filters" + ] + } + } + library("Maven Invoker", "3.1.0") { + group("org.apache.maven.shared") { + modules = [ + "maven-invoker" + ] + } + } + library("Maven Plugin Tools", "3.6.0") { + group("org.apache.maven.plugin-tools") { + modules = [ + "maven-plugin-annotations" + ] + } + } + library("Maven Resolver", "1.6.1") { + group("org.apache.maven.resolver") { + modules = [ + "maven-resolver-connector-basic", + "maven-resolver-impl", + "maven-resolver-transport-file", + "maven-resolver-transport-http" + ] + } + } + library("Maven Shade Plugin", "3.2.4") { + group("org.apache.maven.plugins") { + modules = [ + "maven-shade-plugin" + ] + } + } + library("MockK", "1.10.6") { + group("io.mockk") { + modules = [ + "mockk" + ] + } + } + library("Plexus Build API", "0.0.7") { + group("org.sonatype.plexus") { + modules = [ + "plexus-build-api" + ] + } + } + library("Plexus Sec Dispatcher", "1.4") { + group("org.sonatype.plexus") { + modules = [ + "plexus-sec-dispatcher" + ] + } + } + library("Simple JNDI", "0.23.0") { + group("com.github.h-thurow") { + modules = [ + "simple-jndi" + ] + } + } + library("Sisu", "2.6.0") { + group("org.sonatype.sisu") { + modules = [ + "sisu-inject-plexus" + ] + } + } + library("Spock Framework", "2.0-groovy-3.0") { + group("org.spockframework") { + modules = [ + "spock-core", + "spock-spring" + ] + } + } + library("TestNG", "6.14.3") { + group("org.testng") { + modules = [ + "testng" + ] + } + } + library("Spring Asciidoctor Extensions", "0.6.0") { + group("io.spring.asciidoctor") { + modules = [ + "spring-asciidoctor-extensions-spring-boot", + "spring-asciidoctor-extensions-section-ids" + ] + } + } + library("Testcontainers", "1.16.2") { + group("org.testcontainers") { + imports = [ + "testcontainers-bom" + ] + } + } +} + +dependencies { + api(enforcedPlatform(project(":spring-boot-project:spring-boot-dependencies"))) +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-parent/pom.xml b/spring-boot-project/spring-boot-parent/pom.xml deleted file mode 100644 index 7fcc10524a53..000000000000 --- a/spring-boot-project/spring-boot-parent/pom.xml +++ /dev/null @@ -1,563 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-dependencies - ${revision} - ../spring-boot-dependencies - - spring-boot-parent - pom - Spring Boot Parent - Spring Boot Parent - - Pivotal Software, Inc. - https://spring.io - - - ${basedir}/../.. - false - https://github.com/spring-projects/spring-boot - scm:git:git://github.com/spring-projects/spring-boot.git - scm:git:ssh://git@github.com/spring-projects/spring-boot.git - 1.8 - UTF-8 - UTF-8 - 3.5.4 - 1.1.1 - 1.3-groovy-2.5 - 0.3.0.RELEASE - 0.1.3.RELEASE - https://repo.spring.io/release/io/spring/docresources/spring-doc-resources/${spring-doc-resources.version}/spring-doc-resources-${spring-doc-resources.version}.zip - 1.12.2 - 6.14.3 - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - Github - https://github.com/spring-projects/spring-boot/issues - - - - - - org.springframework.boot - spring-boot-test-support - ${revision} - - - - - log4j - log4j - 1.2.17 - - - commons-fileupload - commons-fileupload - 1.4 - - - io.mockk - mockk - 1.9.3 - - - org.sonatype.plexus - plexus-sec-dispatcher - 1.4 - - - org.sonatype.sisu - sisu-inject-plexus - 2.6.0 - - - com.vaadin.external.google - android-json - 0.0.20131108.vaadin1 - - - jline - jline - 2.11 - - - net.sf.jopt-simple - jopt-simple - 5.0.4 - - - org.apache.commons - commons-compress - 1.19 - - - org.apache.ivy - ivy - 2.4.0 - - - org.apache.maven - maven-archiver - 3.4.0 - - - org.apache.maven - maven-artifact - ${maven.version} - - - org.apache.maven - maven-core - ${maven.version} - - - org.apache.maven - maven-model - ${maven.version} - - - org.apache.maven - maven-plugin-api - ${maven.version} - - - org.apache.maven - maven-settings - ${maven.version} - - - org.apache.maven - maven-settings-builder - ${maven.version} - - - org.apache.maven - maven-model-builder - ${maven.version} - - - org.apache.maven - maven-resolver-provider - ${maven.version} - - - org.apache.maven.resolver - maven-resolver-connector-basic - ${maven-resolver.version} - - - org.apache.maven.resolver - maven-resolver-transport-file - ${maven-resolver.version} - - - org.apache.maven.resolver - maven-resolver-transport-http - ${maven-resolver.version} - - - org.apache.maven.resolver - maven-resolver-impl - ${maven-resolver.version} - - - org.apache.maven.shared - maven-common-artifact-filters - 3.1.0 - - - org.apache.maven.plugins - maven-shade-plugin - ${maven-shade-plugin.version} - - - org.apache.maven.plugin-tools - maven-plugin-annotations - 3.6.0 - - - org.codehaus.plexus - plexus-archiver - 3.7.0 - - - org.codehaus.plexus - plexus-utils - 3.1.0 - - - org.sonatype.plexus - plexus-build-api - 0.0.7 - - - org.spockframework - spock-core - ${spock.version} - - - org.spockframework - spock-spring - ${spock.version} - - - org.testcontainers - testcontainers-bom - ${testcontainers.version} - import - pom - - - org.testng - testng - ${testng.version} - - - org.zeroturnaround - zt-zip - 1.13 - - - - - - - org.junit.jupiter - junit-jupiter - test - - - org.junit.vintage - junit-vintage-engine - test - - - org.hamcrest - hamcrest-core - - - - - org.mockito - mockito-junit-jupiter - test - - - org.assertj - assertj-core - test - - - org.mockito - mockito-core - test - - - org.hamcrest - hamcrest - test - - - org.springframework - spring-test - test - - - - - - - com.googlecode.maven-download-plugin - download-maven-plugin - 1.4.2 - - - org.jetbrains.kotlin - kotlin-maven-plugin - ${kotlin.version} - - ${java.version} - true - - - - org.asciidoctor - asciidoctor-maven-plugin - 1.6.0 - - - org.asciidoctor - asciidoctorj-pdf - 1.5.0-alpha.18 - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - - - integration-test - verify - - - - - - org.apache.maven.plugins - maven-plugin-plugin - 3.6.0 - - - org.apache.maven.plugins - maven-site-plugin - - - org.basepom.maven - duplicate-finder-maven-plugin - 1.3.0 - - - org.codehaus.cargo - cargo-maven2-plugin - 1.7.7 - - - org.codehaus.gmavenplus - gmavenplus-plugin - 1.8.0 - - - - - - org.codehaus.mojo - build-helper-maven-plugin - - - regex-property - - regex-property - - - modulename - ${project.artifactId} - - - . - true - - - - - - org.codehaus.mojo - flatten-maven-plugin - true - - - - flatten - process-resources - - flatten - - - true - oss - - expand - remove - remove - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - ${java.version} - ${java.version} - true - - - - org.apache.maven.plugins - maven-enforcer-plugin - - - enforce-rules - - enforce - - - - - - com.zaxxer:HikariCP-* - org.hamcrest:hamcrest-* - javax.*:*:* - - - javax.batch:*:* - javax.cache:*:* - javax.inject:*:* - javax.money:*:* - - true - - - [1.8,) - - - [3.5.0,) - - - main.basedir - - - project.name - - - project.description - - - true - - - true - - - - - - org.apache.maven.plugins - maven-jar-plugin - - - false - - false - false - - - ${project.name} - ${modulename} - ${project.version} - Spring - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - - **/*Tests.java - - - **/Abstract*.java - - - file:/dev/./urandom - true - - -Xmx1024m - false - true - alphabetical - - - - org.apache.maven.plugins - maven-war-plugin - - - org.apache.maven.plugins - maven-source-plugin - - - attach-sources - - jar-no-fork - - - - - - - - - fast - - - fast - - - - true - - - - full - - - full - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - - ${java.version} - - - - attach-javadocs - - jar - - true - - - - - org.apache.commons - commons-lang3 - 3.7 - - - - - - - - eclipse.profile - - - m2e.version - - - - false - false - false - false - - - - diff --git a/spring-boot-project/spring-boot-properties-migrator/build.gradle b/spring-boot-project/spring-boot-properties-migrator/build.gradle new file mode 100644 index 000000000000..cd9da1f879ac --- /dev/null +++ b/spring-boot-project/spring-boot-properties-migrator/build.gradle @@ -0,0 +1,17 @@ +plugins { + id "java-library" + id "org.springframework.boot.conventions" + id "org.springframework.boot.deployed" +} + +description = "Spring Boot Properties Migrator" + +dependencies { + api(project(":spring-boot-project:spring-boot")) + api(project(":spring-boot-project:spring-boot-tools:spring-boot-configuration-metadata")) + + testImplementation(project(":spring-boot-project:spring-boot-test")) + testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.assertj:assertj-core") + testImplementation("org.springframework:spring-test") +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-properties-migrator/pom.xml b/spring-boot-project/spring-boot-properties-migrator/pom.xml deleted file mode 100644 index da34e62b70e3..000000000000 --- a/spring-boot-project/spring-boot-properties-migrator/pom.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-parent - ${revision} - ../spring-boot-parent - - spring-boot-properties-migrator - Spring Boot Properties Migrator - Spring Boot Properties Migrator - - ${basedir}/../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - - org.springframework.boot - spring-boot - - - org.springframework.boot - spring-boot-configuration-metadata - - - - org.springframework.boot - spring-boot-test - test - - - diff --git a/spring-boot-project/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationReport.java b/spring-boot-project/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationReport.java index d31b2e4781bd..d420fa2ddb5c 100644 --- a/spring-boot-project/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationReport.java +++ b/spring-boot-project/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationReport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -71,7 +71,7 @@ String getErrorReport() { "%nThe use of configuration keys that are no longer supported was found in the environment:%n%n")); append(report, content); report.append(String.format("%n")); - report.append("Please refer to the migration guide or reference guide for potential alternatives."); + report.append("Please refer to the release notes or reference guide for potential alternatives."); report.append(String.format("%n")); return report.toString(); } diff --git a/spring-boot-project/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationReporter.java b/spring-boot-project/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationReporter.java index d60dbbf431ca..d8d99d2c64ed 100644 --- a/spring-boot-project/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationReporter.java +++ b/spring-boot-project/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationReporter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,6 @@ import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty; import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepository; -import org.springframework.boot.configurationmetadata.Deprecation; import org.springframework.boot.context.properties.source.ConfigurationProperty; import org.springframework.boot.context.properties.source.ConfigurationPropertyName; import org.springframework.boot.context.properties.source.ConfigurationPropertySource; @@ -62,7 +61,8 @@ class PropertiesMigrationReporter { */ PropertiesMigrationReport getReport() { PropertiesMigrationReport report = new PropertiesMigrationReport(); - Map> properties = getMatchingProperties(deprecatedFilter()); + Map> properties = getMatchingProperties( + ConfigurationMetadataProperty::isDeprecated); if (properties.isEmpty()) { return report; } @@ -98,16 +98,14 @@ private Map> getMatchingProperties( MultiValueMap result = new LinkedMultiValueMap<>(); List candidates = this.allProperties.values().stream().filter(filter) .collect(Collectors.toList()); - getPropertySourcesAsMap().forEach((name, source) -> { - candidates.forEach((metadata) -> { - ConfigurationProperty configurationProperty = source - .getConfigurationProperty(ConfigurationPropertyName.of(metadata.getId())); - if (configurationProperty != null) { - result.add(name, new PropertyMigration(configurationProperty, metadata, - determineReplacementMetadata(metadata))); - } - }); - }); + getPropertySourcesAsMap().forEach((name, source) -> candidates.forEach((metadata) -> { + ConfigurationProperty configurationProperty = source + .getConfigurationProperty(ConfigurationPropertyName.of(metadata.getId())); + if (configurationProperty != null) { + result.add(name, + new PropertyMigration(configurationProperty, metadata, determineReplacementMetadata(metadata))); + } + })); return result; } @@ -131,11 +129,6 @@ private ConfigurationMetadataProperty detectMapValueReplacement(String fullId) { return null; } - private Predicate deprecatedFilter() { - return (property) -> property.getDeprecation() != null - && property.getDeprecation().getLevel() == Deprecation.Level.ERROR; - } - private Map getPropertySourcesAsMap() { Map map = new LinkedHashMap<>(); for (ConfigurationPropertySource source : ConfigurationPropertySources.get(this.environment)) { diff --git a/spring-boot-project/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertyMigration.java b/spring-boot-project/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertyMigration.java index 3e38b154ee9d..1a891894cb79 100644 --- a/spring-boot-project/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertyMigration.java +++ b/spring-boot-project/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertyMigration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -127,10 +127,7 @@ String determineReason() { return String.format("Reason: Replacement key '%s' uses an incompatible target type", deprecation.getReplacement()); } - else { - return String.format("Reason: No metadata found for replacement key '%s'", - deprecation.getReplacement()); - } + return String.format("Reason: No metadata found for replacement key '%s'", deprecation.getReplacement()); } return "Reason: none"; } diff --git a/spring-boot-project/spring-boot-properties-migrator/src/test/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationListenerTests.java b/spring-boot-project/spring-boot-properties-migrator/src/test/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationListenerTests.java index e041809d5abb..6bad1ac9bb7e 100644 --- a/spring-boot-project/spring-boot-properties-migrator/src/test/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationListenerTests.java +++ b/spring-boot-project/spring-boot-properties-migrator/src/test/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationListenerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,10 +47,10 @@ void closeContext() { @Test void sampleReport(CapturedOutput output) { - this.context = createSampleApplication().run("--banner.charset=UTF8"); - assertThat(output).contains("commandLineArgs").contains("spring.banner.charset") + this.context = createSampleApplication().run("--logging.file=test.log"); + assertThat(output).contains("commandLineArgs").contains("logging.file.name") .contains("Each configuration key has been temporarily mapped") - .doesNotContain("Please refer to the migration guide"); + .doesNotContain("Please refer to the release notes"); } private SpringApplication createSampleApplication() { diff --git a/spring-boot-project/spring-boot-properties-migrator/src/test/resources/metadata/type-conversion-metadata.json b/spring-boot-project/spring-boot-properties-migrator/src/test/resources/metadata/type-conversion-metadata.json index 3c247d1ebdf8..2119d57dca50 100644 --- a/spring-boot-project/spring-boot-properties-migrator/src/test/resources/metadata/type-conversion-metadata.json +++ b/spring-boot-project/spring-boot-properties-migrator/src/test/resources/metadata/type-conversion-metadata.json @@ -20,8 +20,7 @@ "name": "test.cache-seconds", "type": "java.lang.Integer", "deprecation": { - "replacement": "test.cache", - "level": "error" + "replacement": "test.cache" } }, { diff --git a/spring-boot-project/spring-boot-starters/README.adoc b/spring-boot-project/spring-boot-starters/README.adoc index 3b8cc5671c9c..fa3ca8afda29 100644 --- a/spring-boot-project/spring-boot-starters/README.adoc +++ b/spring-boot-project/spring-boot-starters/README.adoc @@ -4,7 +4,7 @@ Spring Boot Starters are a set of convenient dependency descriptors that you can in your application. You get a one-stop-shop for all the Spring and related technology that you need without having to hunt through sample code and copy paste loads of dependency descriptors. For example, if you want to get started using Spring and -JPA for database access just include the `spring-boot-starter-data-jpa` dependency in +JPA for database access include the `spring-boot-starter-data-jpa` dependency in your project, and you are good to go. For complete details see the @@ -12,7 +12,7 @@ https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#using-boot == Community Contributions If you create a starter for a technology that is not already in the standard list we can -list it here. Just send a pull request for this page. +list it here. To ask us to do so, please open a pull request that updates this page. WARNING: While the https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#using-boot-starter[reference documentation] @@ -22,8 +22,11 @@ do as they were designed before this was clarified. |=== | Name | Location -| https://camel.apache.org/spring-boot.html[Apache Camel] -| https://github.com/apache/camel/tree/master/components/camel-spring-boot +| AOProfiling (Aspect-oriented profiling) +| https://github.com/rechnerherz/aoprofiling-spring-boot-starter + +| https://camel.apache.org/camel-spring-boot/latest/spring-boot.html[Apache Camel] +| https://github.com/apache/camel-spring-boot | https://cxf.apache.org/docs/springboot.html[Apache CXF] | https://github.com/apache/cxf @@ -31,6 +34,9 @@ do as they were designed before this was clarified. | https://qpid.apache.org/components/jms/[Apache Qpid] | https://github.com/amqphub/amqp-10-jms-spring-boot +| https://rocketmq.apache.org/[Apache RocketMQ] +| https://github.com/ThierrySquirrel/rocketmq-spring-boot-starter + | https://wicket.apache.org/[Apache Wicket] | https://github.com/MarcGiffing/wicket-spring-boot @@ -49,6 +55,9 @@ do as they were designed before this was clarified. | https://docs.microsoft.com/en-us/azure/application-insights/app-insights-overview[Azure Application Insights] | https://github.com/Microsoft/ApplicationInsights-Java/tree/master/azure-application-insights-spring-boot-starter +| https://github.com/bitcoin/bitcoin[Bitcoin] +| https://github.com/theborakompanioni/bitcoin-spring-boot-starter + | https://github.com/vladimir-bukhtoyarov/bucket4j/[Bucket4j] | https://github.com/MarcGiffing/bucket4j-spring-boot-starter @@ -67,6 +76,9 @@ do as they were designed before this was clarified. | DataSource decorating (https://github.com/p6spy/p6spy[P6Spy], https://github.com/ttddyy/datasource-proxy[datasource-proxy], https://github.com/vladmihalcea/flexy-pool[FlexyPool]) | https://github.com/gavlyukovskiy/spring-boot-data-source-decorator +| https://github.com/Allurx/desensitization[desensitization] +| https://github.com/Allurx/desensitization-spring-boot + | https://github.com/docker-java/docker-java/[Docker Java] and https://github.com/spotify/docker-client/[Docker Client] | https://github.com/jliu666/docker-api-spring-boot @@ -76,6 +88,9 @@ do as they were designed before this was clarified. | Elegant Error Handling for Spring Boot | https://github.com/alimate/errors-spring-boot-starter +| https://elide.io/[Elide] +| https://github.com/yahoo/elide/tree/master/elide-spring/elide-spring-boot-starter + | ErroREST exception handler | https://github.com/mkopylec/errorest-spring-boot-starter @@ -86,13 +101,13 @@ do as they were designed before this was clarified. | https://github.com/mkopylec/recaptcha-spring-boot-starter | https://graphql.org/[GraphQL] and https://github.com/graphql/graphiql[GraphiQL] with https://github.com/graphql-java/[GraphQL Java] -| https://github.com/graphql-java/graphql-spring-boot +| https://github.com/graphql-java-kickstart/graphql-spring-boot | https://javaee.github.io/grizzly/[Grizzly] | https://github.com/dabla/grizzly-spring-boot-starter | https://www.grpc.io/[gRPC] -| https://github.com/LogNet/grpc-spring-boot-starter +| https://github.com/LogNet/grpc-spring-boot-starter & https://github.com/yidongnan/grpc-spring-boot-starter | https://ha-jdbc.github.io/[HA JDBC] | https://github.com/lievendoclo/hajdbc-spring-boot @@ -106,6 +121,12 @@ do as they were designed before this was clarified. | Hiatus for Spring Boot | https://github.com/jihor/hiatus-spring-boot +| https://www.hyperledger.org/use/fabric[Hyperledger Fabric] +| https://github.com/bxforce/hyperledger-fabric-spring-boot + +| https://www.ibm.com/products/mq[IBM MQ] +| https://github.com/ibm-messaging/mq-jms-spring + | https://infinispan.org/[Infinispan] | https://github.com/infinispan/infinispan-spring-boot @@ -118,6 +139,9 @@ do as they were designed before this was clarified. | https://javers.org[JaVers] | https://github.com/javers/javers +| https://www.jobrunr.io[JobRunr] +| https://github.com/jobrunr/jobrunr + | https://github.com/sbraconnier/jodconverter[JODConverter] | https://github.com/sbraconnier/jodconverter @@ -130,6 +154,9 @@ do as they were designed before this was clarified. | https://logback.qos.ch/access.html[Logback-access] | https://github.com/akihyro/logback-access-spring-boot-starter +| https://github.com/mulesoft/mule[Mule 4] +| https://github.com/hawkore/mule4-spring-boot-starter + | https://github.com/mybatis/mybatis-3[MyBatis] | https://github.com/mybatis/mybatis-spring-boot @@ -139,15 +166,24 @@ do as they were designed before this was clarified. | https://developer.nexmo.com/[Nexmo] | https://github.com/nexmo/nexmo-spring-boot-starter +| https://funthomas424242.github.io/nitrite-spring-boot-starter/[Nitrite Database] +| https://github.com/FunThomas424242/nitrite-spring-boot-starter + | https://github.com/nutzam/nutz[Nutz] | https://github.com/nutzam/nutzmore +| https://groupe-sii.github.io/ogham/[Ogham] +| https://github.com/groupe-sii/ogham/tree/master/ogham-spring-boot-starter-all, https://github.com/groupe-sii/ogham/tree/master/ogham-spring-boot-starter-email, and https://github.com/groupe-sii/ogham/tree/master/ogham-spring-boot-starter-sms + | https://square.github.io/okhttp/[OkHttp] | https://github.com/freefair/okhttp-spring-boot | https://developer.okta.com/[Okta] | https://github.com/okta/okta-spring-boot +| https://www.optaplanner.org/[OptaPlanner] +| https://github.com/kiegroup/optaplanner/tree/master/optaplanner-spring-integration/optaplanner-spring-boot-starter + | https://orika-mapper.github.io/orika-docs/[Orika] | https://github.com/akihyro/orika-spring-boot-starter @@ -157,6 +193,9 @@ do as they were designed before this was clarified. | https://picocli.info/[picocli] | https://github.com/remkop/picocli/tree/master/picocli-spring-boot-starter +| https://www.quickfixj.org/[quickfixj] +| https://github.com/gevoulga/spring-boot-quickfixj + | https://www.rabbitmq.com/[RabbitMQ] (Advanced usage) | https://github.com/societe-generale/rabbitmq-advanced-spring-boot-starter @@ -197,7 +236,7 @@ do as they were designed before this was clarified. | https://github.com/Catalysts/structurizr-extensions | https://vaadin.com/[Vaadin] -| https://github.com/vaadin/spring/tree/master/vaadin-spring-boot-starter +| https://github.com/vaadin/platform/tree/master/vaadin-spring-boot-starter | https://github.com/valiktor/valiktor[Valiktor] | https://github.com/valiktor/valiktor/tree/master/valiktor-spring/valiktor-spring-boot-starter @@ -208,4 +247,7 @@ do as they were designed before this was clarified. | https://alexo.github.io/wro4j/[Wro4j] | https://github.com/michael-simons/wro4j-spring-boot-starter +| https://github.com/knowm/XChange[XChange] +| https://github.com/cassandre-tech/cassandre-trading-bot + |=== diff --git a/spring-boot-project/spring-boot-starters/pom.xml b/spring-boot-project/spring-boot-starters/pom.xml deleted file mode 100644 index 6bffde0ba0fb..000000000000 --- a/spring-boot-project/spring-boot-starters/pom.xml +++ /dev/null @@ -1,174 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-parent - ${revision} - ../spring-boot-parent - - spring-boot-starters - pom - Spring Boot Starters - Spring Boot Starters - - ${basedir}/../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - spring-boot-starter - spring-boot-starter-activemq - spring-boot-starter-amqp - spring-boot-starter-aop - spring-boot-starter-artemis - spring-boot-starter-batch - spring-boot-starter-cache - spring-boot-starter-cloud-connectors - spring-boot-starter-data-cassandra - spring-boot-starter-data-cassandra-reactive - spring-boot-starter-data-couchbase - spring-boot-starter-data-couchbase-reactive - spring-boot-starter-data-elasticsearch - spring-boot-starter-data-jdbc - spring-boot-starter-data-jpa - spring-boot-starter-data-ldap - spring-boot-starter-data-mongodb - spring-boot-starter-data-mongodb-reactive - spring-boot-starter-data-neo4j - spring-boot-starter-data-redis - spring-boot-starter-data-redis-reactive - spring-boot-starter-data-rest - spring-boot-starter-data-solr - spring-boot-starter-freemarker - spring-boot-starter-groovy-templates - spring-boot-starter-hateoas - spring-boot-starter-integration - spring-boot-starter-jdbc - spring-boot-starter-jersey - spring-boot-starter-jetty - spring-boot-starter-jooq - spring-boot-starter-json - spring-boot-starter-jta-atomikos - spring-boot-starter-jta-bitronix - spring-boot-starter-logging - spring-boot-starter-log4j2 - spring-boot-starter-mail - spring-boot-starter-mustache - spring-boot-starter-actuator - spring-boot-starter-oauth2-client - spring-boot-starter-oauth2-resource-server - spring-boot-starter-parent - spring-boot-starter-quartz - spring-boot-starter-reactor-netty - spring-boot-starter-rsocket - spring-boot-starter-security - spring-boot-starter-test - spring-boot-starter-thymeleaf - spring-boot-starter-tomcat - spring-boot-starter-undertow - spring-boot-starter-validation - spring-boot-starter-web - spring-boot-starter-webflux - spring-boot-starter-websocket - spring-boot-starter-web-services - - - - - org.apache.maven.plugins - maven-enforcer-plugin - - - enforce-rules - - enforce - - - - - - commons-logging:*:* - org.hibernate:hibernate-validator:* - - true - - - - true - - - - - - maven-assembly-plugin - false - - - assemble-starter-poms - generate-resources - - single - - - - src/main/assembly/starter-poms-assembly.xml - - - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - - - checkstyle-validation - validate - - check - - - true - - - - - - org.apache.maven.plugins - maven-source-plugin - - true - - - - org.basepom.maven - duplicate-finder-maven-plugin - - - duplicate-dependencies - validate - - check - - - true - - .*module-info - - - changelog.txt - about.html - - - - - - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-activemq/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-activemq/build.gradle new file mode 100644 index 000000000000..224b4ae390db --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-activemq/build.gradle @@ -0,0 +1,16 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for JMS messaging using Apache ActiveMQ" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api("org.springframework:spring-jms") + api("org.apache.activemq:activemq-broker") { + exclude group: "org.apache.geronimo.specs", module: "geronimo-j2ee-management_1.1_spec" + exclude group: "org.apache.geronimo.specs", module: "geronimo-jms_1.1_spec" + } + api("jakarta.jms:jakarta.jms-api") + api("jakarta.management.j2ee:jakarta.management.j2ee-api") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-activemq/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-activemq/pom.xml deleted file mode 100644 index 11bfe14e5be7..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-activemq/pom.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-activemq - Spring Boot ActiveMQ Starter - Starter for JMS messaging using Apache ActiveMQ - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - org.springframework - spring-jms - - - org.apache.activemq - activemq-broker - - - geronimo-jms_1.1_spec - org.apache.geronimo.specs - - - - - jakarta.jms - jakarta.jms-api - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-actuator/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-actuator/build.gradle new file mode 100644 index 000000000000..4c4d4c90896e --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-actuator/build.gradle @@ -0,0 +1,11 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Spring Boot's Actuator which provides production ready features to help you monitor and manage your application" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api(project(":spring-boot-project:spring-boot-actuator-autoconfigure")) + api("io.micrometer:micrometer-core") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-actuator/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-actuator/pom.xml deleted file mode 100644 index c381b6c5df3e..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-actuator/pom.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-actuator - Spring Boot Actuator Starter - Starter for using Spring Boot's Actuator which provides production - ready features to help you monitor and manage your application - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - org.springframework.boot - spring-boot-actuator-autoconfigure - - - io.micrometer - micrometer-core - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-amqp/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-amqp/build.gradle new file mode 100644 index 000000000000..11866780060d --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-amqp/build.gradle @@ -0,0 +1,11 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Spring AMQP and Rabbit MQ" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api("org.springframework:spring-messaging") + api("org.springframework.amqp:spring-rabbit") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-amqp/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-amqp/pom.xml deleted file mode 100644 index d3d470ad7093..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-amqp/pom.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-amqp - Spring Boot AMQP Starter - Starter for using Spring AMQP and Rabbit MQ - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - org.springframework - spring-messaging - - - org.springframework.amqp - spring-rabbit - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-aop/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-aop/build.gradle new file mode 100644 index 000000000000..895dc5ecacb4 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-aop/build.gradle @@ -0,0 +1,11 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for aspect-oriented programming with Spring AOP and AspectJ" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api("org.springframework:spring-aop") + api("org.aspectj:aspectjweaver") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-aop/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-aop/pom.xml deleted file mode 100644 index bb697fb54fba..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-aop/pom.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-aop - Spring Boot AOP Starter - Starter for aspect-oriented programming with Spring AOP and AspectJ - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - org.springframework - spring-aop - - - org.aspectj - aspectjweaver - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-artemis/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-artemis/build.gradle new file mode 100644 index 000000000000..46b50d3b723b --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-artemis/build.gradle @@ -0,0 +1,17 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for JMS messaging using Apache Artemis" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api("jakarta.jms:jakarta.jms-api") + api("jakarta.json:jakarta.json-api") + api("org.springframework:spring-jms") + api("org.apache.activemq:artemis-jms-client") { + exclude group: "commons-logging", module: "commons-logging" + exclude group: "org.apache.geronimo.specs", module: "geronimo-jms_2.0_spec" + exclude group: "org.apache.geronimo.specs", module: "geronimo-json_1.0_spec" + } +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-artemis/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-artemis/pom.xml deleted file mode 100644 index e9d8dffdddd4..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-artemis/pom.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-artemis - Spring Boot Artemis Starter - Starter for JMS messaging using Apache Artemis - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - org.springframework - spring-jms - - - org.apache.activemq - artemis-jms-client - - - geronimo-jms_2.0_spec - org.apache.geronimo.specs - - - - - jakarta.jms - jakarta.jms-api - - - jakarta.json - jakarta.json-api - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-batch/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-batch/build.gradle new file mode 100644 index 000000000000..7f27a2af8806 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-batch/build.gradle @@ -0,0 +1,11 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Spring Batch" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-jdbc")) + api("org.springframework.batch:spring-batch-core") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-batch/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-batch/pom.xml deleted file mode 100644 index 24d24994959c..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-batch/pom.xml +++ /dev/null @@ -1,68 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-batch - Spring Boot Batch Starter - Starter for using Spring Batch - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - org.springframework.boot - spring-boot-starter-jdbc - - - org.springframework.batch - spring-batch-core - - - - - - org.basepom.maven - duplicate-finder-maven-plugin - - - duplicate-dependencies - validate - - check - - - - - - - xpp3 - xpp3_min - - - xmlpull - xmlpull - - - - - - - - - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-cache/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-cache/build.gradle new file mode 100644 index 000000000000..bb7fb8485716 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-cache/build.gradle @@ -0,0 +1,10 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Spring Framework's caching support" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api("org.springframework:spring-context-support") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-cache/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-cache/pom.xml deleted file mode 100644 index 45c6796929d3..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-cache/pom.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-cache - Spring Boot Cache Starter - Starter for using Spring Framework's caching support - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - org.springframework - spring-context-support - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-cloud-connectors/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-cloud-connectors/pom.xml deleted file mode 100644 index 012ad1756700..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-cloud-connectors/pom.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-cloud-connectors - Spring Boot Spring Cloud Connectors Starter - Starter for using Spring Cloud Connectors which simplifies connecting - to services in cloud platforms like Cloud Foundry and Heroku. Deprecated in - favor of Java CFEnv - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - org.springframework.cloud - spring-cloud-spring-service-connector - - - org.springframework.cloud - spring-cloud-cloudfoundry-connector - - - org.springframework.cloud - spring-cloud-heroku-connector - - - org.springframework.cloud - spring-cloud-localconfig-connector - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-cassandra-reactive/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-cassandra-reactive/build.gradle new file mode 100644 index 000000000000..e619b60eef57 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-cassandra-reactive/build.gradle @@ -0,0 +1,12 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Cassandra distributed database and Spring Data Cassandra Reactive" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api("org.springframework:spring-tx") + api("org.springframework.data:spring-data-cassandra") + api("io.projectreactor:reactor-core") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-cassandra-reactive/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-cassandra-reactive/pom.xml deleted file mode 100644 index 8202656b75eb..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-cassandra-reactive/pom.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-data-cassandra-reactive - Spring Boot Data Cassandra Reactive Starter - Starter for using Cassandra distributed database and Spring Data - Cassandra Reactive - - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - org.springframework - spring-tx - - - org.springframework.data - spring-data-cassandra - - - org.slf4j - jcl-over-slf4j - - - - - io.projectreactor - reactor-core - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-cassandra/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-cassandra/build.gradle new file mode 100644 index 000000000000..def14ae72964 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-cassandra/build.gradle @@ -0,0 +1,11 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Cassandra distributed database and Spring Data Cassandra" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api("org.springframework:spring-tx") + api("org.springframework.data:spring-data-cassandra") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-cassandra/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-cassandra/pom.xml deleted file mode 100644 index 12524d9c2d1f..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-cassandra/pom.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-data-cassandra - Spring Boot Data Cassandra Starter - Starter for using Cassandra distributed database and Spring Data - Cassandra - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - org.springframework - spring-tx - - - org.springframework.data - spring-data-cassandra - - - org.slf4j - jcl-over-slf4j - - - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-couchbase-reactive/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-couchbase-reactive/build.gradle new file mode 100644 index 000000000000..ec8c533472e7 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-couchbase-reactive/build.gradle @@ -0,0 +1,12 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Couchbase document-oriented database and Spring Data Couchbase Reactive" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api("io.projectreactor:reactor-core") + api("io.reactivex:rxjava-reactive-streams") + api("org.springframework.data:spring-data-couchbase") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-couchbase-reactive/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-couchbase-reactive/pom.xml deleted file mode 100644 index 6e159e0447ad..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-couchbase-reactive/pom.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-data-couchbase-reactive - Spring Boot Data Couchbase Reactive Starter - Starter for using Couchbase document-oriented database and Spring Data - Couchbase Reactive - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - org.springframework.data - spring-data-couchbase - - - org.slf4j - jcl-over-slf4j - - - com.couchbase.mock - CouchbaseMock - - - com.couchbase.client - encryption - - - - - io.projectreactor - reactor-core - - - io.reactivex - rxjava-reactive-streams - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-couchbase/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-couchbase/build.gradle new file mode 100644 index 000000000000..17742429df21 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-couchbase/build.gradle @@ -0,0 +1,10 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Couchbase document-oriented database and Spring Data Couchbase" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api("org.springframework.data:spring-data-couchbase") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-couchbase/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-couchbase/pom.xml deleted file mode 100644 index 62f40f704acc..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-couchbase/pom.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - 4.0.0 - - spring-boot-starters - org.springframework.boot - ${revision} - - spring-boot-starter-data-couchbase - Spring Boot Data Couchbase Starter - Starter for using Couchbase document-oriented database and Spring Data - Couchbase - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - org.springframework.data - spring-data-couchbase - - - org.slf4j - jcl-over-slf4j - - - com.couchbase.mock - CouchbaseMock - - - com.couchbase.client - encryption - - - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-elasticsearch/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-elasticsearch/build.gradle new file mode 100644 index 000000000000..850aa79197ea --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-elasticsearch/build.gradle @@ -0,0 +1,12 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Elasticsearch search and analytics engine and Spring Data Elasticsearch" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api("org.springframework.data:spring-data-elasticsearch") { + exclude group: "org.elasticsearch.client", module: "transport" + } +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-elasticsearch/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-elasticsearch/pom.xml deleted file mode 100644 index 18e4b779b797..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-elasticsearch/pom.xml +++ /dev/null @@ -1,68 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-data-elasticsearch - Spring Boot Data Elasticsearch Starter - Starter for using Elasticsearch search and analytics engine and Spring - Data Elasticsearch - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - org.springframework.data - spring-data-elasticsearch - - - org.slf4j - jcl-over-slf4j - - - org.apache.logging.log4j - log4j-core - - - - - - - - org.basepom.maven - duplicate-finder-maven-plugin - - - duplicate-dependencies - validate - - check - - - - org.joda.time.base.BaseDateTime - .*module-info - - - changelog.txt - - - - - - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-jdbc/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-jdbc/build.gradle new file mode 100644 index 000000000000..8ecfbed5f2e1 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-jdbc/build.gradle @@ -0,0 +1,10 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Spring Data JDBC" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-jdbc")) + api("org.springframework.data:spring-data-jdbc") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-jdbc/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-jdbc/pom.xml deleted file mode 100644 index 0e6e909c8c18..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-jdbc/pom.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-data-jdbc - Spring Boot Data JDBC Starter - Starter for using Spring Data JDBC - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter-jdbc - - - org.springframework.data - spring-data-jdbc - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-jpa/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-jpa/build.gradle new file mode 100644 index 000000000000..6f7bd7c91c31 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-jpa/build.gradle @@ -0,0 +1,22 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Spring Data JPA with Hibernate" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-aop")) + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-jdbc")) + api("jakarta.transaction:jakarta.transaction-api") + api("jakarta.persistence:jakarta.persistence-api") + api("org.hibernate:hibernate-core") { + exclude group: "javax.activation", module: "javax.activation-api" + exclude group: "javax.persistence", module: "javax.persistence-api" + exclude group: "javax.xml.bind", module: "jaxb-api" + exclude group: "org.jboss.spec.javax.transaction", module: "jboss-transaction-api_1.2_spec" + } + api("org.springframework.data:spring-data-jpa") { + exclude group: "org.aspectj", module: "aspectjrt" + } + api("org.springframework:spring-aspects") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-jpa/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-jpa/pom.xml deleted file mode 100644 index 1a4c0ba8fcbb..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-jpa/pom.xml +++ /dev/null @@ -1,83 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-data-jpa - Spring Boot Data JPA Starter - Starter for using Spring Data JPA with Hibernate - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter-aop - - - org.springframework.boot - spring-boot-starter-jdbc - - - jakarta.activation - jakarta.activation-api - - - jakarta.persistence - jakarta.persistence-api - - - jakarta.transaction - jakarta.transaction-api - - - org.hibernate - hibernate-core - - - org.jboss.spec.javax.transaction - jboss-transaction-api_1.2_spec - - - javax.activation - javax.activation-api - - - javax.persistence - javax.persistence-api - - - javax.xml.bind - jaxb-api - - - - - org.springframework.data - spring-data-jpa - - - org.aspectj - aspectjrt - - - org.slf4j - jcl-over-slf4j - - - - - org.springframework - spring-aspects - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-ldap/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-ldap/build.gradle new file mode 100644 index 000000000000..041112d8af69 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-ldap/build.gradle @@ -0,0 +1,10 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Spring Data LDAP" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api("org.springframework.data:spring-data-ldap") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-ldap/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-ldap/pom.xml deleted file mode 100644 index fbdda52b11a9..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-ldap/pom.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - 4.0.0 - - spring-boot-starters - org.springframework.boot - ${revision} - - spring-boot-starter-data-ldap - Spring Boot Data LDAP Starter - Starter for using Spring Data LDAP - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - org.springframework.data - spring-data-ldap - - - org.slf4j - jcl-over-slf4j - - - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-mongodb-reactive/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-mongodb-reactive/build.gradle new file mode 100644 index 000000000000..a1e5636e97c6 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-mongodb-reactive/build.gradle @@ -0,0 +1,12 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using MongoDB document-oriented database and Spring Data MongoDB Reactive" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api("io.projectreactor:reactor-core") + api("org.mongodb:mongodb-driver-reactivestreams") + api("org.springframework.data:spring-data-mongodb") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-mongodb-reactive/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-mongodb-reactive/pom.xml deleted file mode 100644 index 54162c3f1a65..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-mongodb-reactive/pom.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-data-mongodb-reactive - Spring Boot Data MongoDB Reactive Starter - Starter for using MongoDB document-oriented database and Spring Data - MongoDB Reactive - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - org.springframework.data - spring-data-mongodb - - - org.mongodb - mongo-java-driver - - - org.slf4j - jcl-over-slf4j - - - - - org.mongodb - mongodb-driver - - - org.mongodb - mongodb-driver-async - - - org.mongodb - mongodb-driver-reactivestreams - - - io.projectreactor - reactor-core - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-mongodb/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-mongodb/build.gradle new file mode 100644 index 000000000000..dfeb2fd6acbe --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-mongodb/build.gradle @@ -0,0 +1,11 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using MongoDB document-oriented database and Spring Data MongoDB" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api("org.mongodb:mongodb-driver-sync") + api("org.springframework.data:spring-data-mongodb") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-mongodb/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-mongodb/pom.xml deleted file mode 100644 index 0c6824c04d4d..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-mongodb/pom.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-data-mongodb - Spring Boot Data MongoDB Starter - Starter for using MongoDB document-oriented database and Spring Data - MongoDB - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - org.mongodb - mongodb-driver - - - org.springframework.data - spring-data-mongodb - - - org.mongodb - mongo-java-driver - - - org.slf4j - jcl-over-slf4j - - - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-neo4j/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-neo4j/build.gradle new file mode 100644 index 000000000000..cc50d8e552a3 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-neo4j/build.gradle @@ -0,0 +1,10 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Neo4j graph database and Spring Data Neo4j" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api("org.springframework.data:spring-data-neo4j") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-neo4j/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-neo4j/pom.xml deleted file mode 100644 index 5beaa0a27145..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-neo4j/pom.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-data-neo4j - Spring Boot Data Neo4j Starter - Starter for using Neo4j graph database and Spring Data Neo4j - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - org.springframework.data - spring-data-neo4j - - - org.slf4j - jcl-over-slf4j - - - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-r2dbc/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-r2dbc/build.gradle new file mode 100644 index 000000000000..2c35202593ba --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-r2dbc/build.gradle @@ -0,0 +1,12 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Spring Data R2DBC" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api("org.springframework.data:spring-data-r2dbc") + api("io.r2dbc:r2dbc-spi") + api("io.r2dbc:r2dbc-pool") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-redis-reactive/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-redis-reactive/build.gradle new file mode 100644 index 000000000000..d66f98dcfc40 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-redis-reactive/build.gradle @@ -0,0 +1,9 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Redis key-value data store with Spring Data Redis reactive and the Lettuce client" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-data-redis")) +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-redis-reactive/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-redis-reactive/pom.xml deleted file mode 100644 index adef6cad1ba2..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-redis-reactive/pom.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-data-redis-reactive - Spring Boot Data Redis Reactive Starter - Starter for using Redis key-value data store with Spring Data Redis - reactive and the Lettuce client - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter-data-redis - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-redis/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-redis/build.gradle new file mode 100644 index 000000000000..11f150cd1eec --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-redis/build.gradle @@ -0,0 +1,11 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Redis key-value data store with Spring Data Redis and the Lettuce client" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api("org.springframework.data:spring-data-redis") + api("io.lettuce:lettuce-core") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-redis/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-redis/pom.xml deleted file mode 100644 index 84b5b9e36105..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-redis/pom.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-data-redis - Spring Boot Data Redis Starter - Starter for using Redis key-value data store with Spring Data Redis and - the Lettuce client - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - org.springframework.data - spring-data-redis - - - org.slf4j - jcl-over-slf4j - - - - - io.lettuce - lettuce-core - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-rest/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-rest/build.gradle new file mode 100644 index 000000000000..1fce3609bd10 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-rest/build.gradle @@ -0,0 +1,10 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for exposing Spring Data repositories over REST using Spring Data REST" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-web")) + api("org.springframework.data:spring-data-rest-webmvc") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-rest/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-rest/pom.xml deleted file mode 100644 index fb21cd4b301b..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-rest/pom.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-data-rest - Spring Boot Data REST Starter - Starter for exposing Spring Data repositories over REST using Spring - Data REST - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.data - spring-data-rest-webmvc - - - org.slf4j - jcl-over-slf4j - - - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-solr/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-solr/pom.xml deleted file mode 100644 index 87c474c8ad3c..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-solr/pom.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-data-solr - Spring Boot Data Solr Starter - Starter for using the Apache Solr search platform with Spring Data - Solr - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - org.apache.solr - solr-solrj - - - org.springframework.data - spring-data-solr - - - org.slf4j - jcl-over-slf4j - - - - - org.apache.httpcomponents - httpmime - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-freemarker/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-freemarker/build.gradle new file mode 100644 index 000000000000..47930b9c569b --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-freemarker/build.gradle @@ -0,0 +1,11 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for building MVC web applications using FreeMarker views" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api("org.freemarker:freemarker") + api("org.springframework:spring-context-support") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-freemarker/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-freemarker/pom.xml deleted file mode 100644 index f480476e4425..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-freemarker/pom.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-freemarker - Spring Boot FreeMarker Starter - Starter for building MVC web applications using FreeMarker views - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - org.freemarker - freemarker - - - org.springframework - spring-context-support - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-groovy-templates/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-groovy-templates/build.gradle new file mode 100644 index 000000000000..45bd1056726a --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-groovy-templates/build.gradle @@ -0,0 +1,10 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for building MVC web applications using Groovy Templates views" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-web")) + api("org.codehaus.groovy:groovy-templates") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-groovy-templates/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-groovy-templates/pom.xml deleted file mode 100644 index 0ab1b3459e5c..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-groovy-templates/pom.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-groovy-templates - Spring Boot Groovy Templates Starter - Starter for building MVC web applications using Groovy Templates views - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter-web - - - org.codehaus.groovy - groovy-templates - - - - - - org.basepom.maven - duplicate-finder-maven-plugin - - - duplicate-dependencies - validate - - check - - - - - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-hateoas/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-hateoas/build.gradle new file mode 100644 index 000000000000..a4d3560b1b43 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-hateoas/build.gradle @@ -0,0 +1,10 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for building hypermedia-based RESTful web application with Spring MVC and Spring HATEOAS" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-web")) + api("org.springframework.hateoas:spring-hateoas") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-hateoas/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-hateoas/pom.xml deleted file mode 100644 index d23fa1563ad7..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-hateoas/pom.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-hateoas - Spring Boot HATEOAS Starter - Starter for building hypermedia-based RESTful web application with - Spring MVC and Spring HATEOAS - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.hateoas - spring-hateoas - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-integration/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-integration/build.gradle new file mode 100644 index 000000000000..0ce04e823dbd --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-integration/build.gradle @@ -0,0 +1,10 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Spring Integration" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-aop")) + api("org.springframework.integration:spring-integration-core") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-integration/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-integration/pom.xml deleted file mode 100644 index 6918380b3f42..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-integration/pom.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-integration - Spring Boot Integration Starter - Starter for using Spring Integration - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter-aop - - - org.springframework.integration - spring-integration-core - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-jdbc/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-jdbc/build.gradle new file mode 100644 index 000000000000..352c3570e924 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-jdbc/build.gradle @@ -0,0 +1,11 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using JDBC with the HikariCP connection pool" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api("com.zaxxer:HikariCP") + api("org.springframework:spring-jdbc") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-jdbc/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-jdbc/pom.xml deleted file mode 100644 index 5c1a9978583f..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-jdbc/pom.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-jdbc - Spring Boot JDBC Starter - Starter for using JDBC with the HikariCP connection pool - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - com.zaxxer - HikariCP - - - org.springframework - spring-jdbc - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-jersey/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-jersey/build.gradle new file mode 100644 index 000000000000..86856716c047 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-jersey/build.gradle @@ -0,0 +1,26 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for building RESTful web applications using JAX-RS and Jersey. An alternative to spring-boot-starter-web" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-json")) + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-tomcat")) + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-validation")) + api("org.springframework:spring-web") + api("org.glassfish.jersey.core:jersey-server") + api("org.glassfish.jersey.containers:jersey-container-servlet-core") + api("org.glassfish.jersey.containers:jersey-container-servlet") + api("org.glassfish.jersey.ext:jersey-bean-validation") { + exclude group: "jakarta.el", module: "jakarta.el-api" + exclude group: "org.glassfish", module: "jakarta.el" + } + api("org.glassfish.jersey.ext:jersey-spring5") + api("org.glassfish.jersey.media:jersey-media-json-jackson") +} + +checkRuntimeClasspathForConflicts { + ignore { name -> name.startsWith("org/aopalliance/intercept/") } + ignore { name -> name.startsWith("org/aopalliance/aop/") } +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-jersey/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-jersey/pom.xml deleted file mode 100644 index ee33ee1264e2..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-jersey/pom.xml +++ /dev/null @@ -1,176 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-jersey - Spring Boot Jersey Starter - Starter for building RESTful web applications using JAX-RS and Jersey. - An alternative to spring-boot-starter-web - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter-json - - - org.springframework.boot - spring-boot-starter-tomcat - - - org.springframework.boot - spring-boot-starter-validation - - - jakarta.annotation - jakarta.annotation-api - - - jakarta.ws.rs - jakarta.ws.rs-api - - - org.springframework - spring-web - - - org.glassfish.jersey.core - jersey-server - - - javax.validation - validation-api - - - - - org.glassfish.jersey.containers - jersey-container-servlet-core - - - org.glassfish.hk2.external - jakarta.inject - - - - - org.glassfish.jersey.containers - jersey-container-servlet - - - javax.ws.rs - javax.ws.rs-api - - - - - org.glassfish.jersey.ext - jersey-bean-validation - - - javax.validation - validation-api - - - org.glassfish - jakarta.el - - - org.hibernate - hibernate-validator - - - jakarta.el - jakarta.el-api - - - - - org.glassfish.jersey.ext - jersey-spring5 - - - org.jvnet - tiger-types - - - org.glassfish.hk2.external - bean-validator - - - org.hibernate - hibernate-validator - - - - - org.glassfish.jersey.media - jersey-media-json-jackson - - - jakarta.activation - jakarta.activation-api - - - - - jakarta.xml.bind - jakarta.xml.bind-api - - - - - - org.basepom.maven - duplicate-finder-maven-plugin - - - duplicate-dependencies - validate - - check - - - - org.aopalliance.* - javax.annotation.* - .*module-info - - - - - - - - - - jdk11+ - - [11,) - - - - jakarta.xml.bind - jakarta.xml.bind-api - - - jakarta.activation - jakarta.activation-api - - - - - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-jetty/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-jetty/build.gradle new file mode 100644 index 000000000000..a771c7c82f4d --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-jetty/build.gradle @@ -0,0 +1,25 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Jetty as the embedded servlet container. An alternative to spring-boot-starter-tomcat" + +dependencies { + api("jakarta.servlet:jakarta.servlet-api") + api("jakarta.websocket:jakarta.websocket-api") + api("org.apache.tomcat.embed:tomcat-embed-el") + api("org.eclipse.jetty:jetty-servlets") + api("org.eclipse.jetty:jetty-webapp") { + exclude group: "javax.servlet", module: "javax.servlet-api" + } + api("org.eclipse.jetty.websocket:websocket-server") { + exclude group: "javax.servlet", module: "javax.servlet-api" + } + api("org.eclipse.jetty.websocket:javax-websocket-server-impl") { + exclude group: "javax.annotation", module: "javax.annotation-api" + exclude group: "javax.servlet", module: "javax.servlet-api" + exclude group: "javax.websocket", module: "javax.websocket-api" + exclude group: "javax.websocket", module: "javax.websocket-client-api" + exclude group: "org.eclipse.jetty", module: "jetty-jndi" + } +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-jetty/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-jetty/pom.xml deleted file mode 100644 index f9bf5d3a8634..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-jetty/pom.xml +++ /dev/null @@ -1,108 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-jetty - Spring Boot Jetty Starter - Starter for using Jetty as the embedded servlet container. An - alternative to spring-boot-starter-tomcat - - ${basedir}/../../.. - 3.1.0 - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - jakarta.servlet - jakarta.servlet-api - - - jakarta.websocket - jakarta.websocket-api - - - org.eclipse.jetty - jetty-servlets - - - org.eclipse.jetty - jetty-webapp - - - javax.servlet - javax.servlet-api - - - - - org.eclipse.jetty.websocket - websocket-server - - - javax.servlet - javax.servlet-api - - - - - org.eclipse.jetty.websocket - javax-websocket-server-impl - - - javax.annotation - javax.annotation-api - - - javax.websocket - javax.websocket-client-api - - - javax.websocket - javax.websocket-api - - - org.eclipse.jetty - jetty-jndi - - - - - org.mortbay.jasper - apache-el - - - - - - org.basepom.maven - duplicate-finder-maven-plugin - - - duplicate-dependencies - validate - - check - - - - .*module-info - - - about.html - - - - - - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-jooq/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-jooq/build.gradle new file mode 100644 index 000000000000..734f55a80327 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-jooq/build.gradle @@ -0,0 +1,16 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using jOOQ to access SQL databases. An alternative to spring-boot-starter-data-jpa or spring-boot-starter-jdbc" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-jdbc")) + api("jakarta.activation:jakarta.activation-api") + api("jakarta.xml.bind:jakarta.xml.bind-api") + api("org.springframework:spring-tx") + api("org.jooq:jooq") { + exclude group: "javax.activation", module: "javax.activation-api" + exclude group: "javax.xml.bind", module: "jaxb-api" + } +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-jooq/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-jooq/pom.xml deleted file mode 100644 index 0d938f17ddcd..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-jooq/pom.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-jooq - Spring Boot JOOQ Starter - Starter for using jOOQ to access SQL databases. An alternative to - spring-boot-starter-data-jpa or spring-boot-starter-jdbc - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter-jdbc - - - jakarta.activation - jakarta.activation-api - - - jakarta.xml.bind - jakarta.xml.bind-api - - - org.springframework - spring-tx - - - org.jooq - jooq - - - javax.activation - javax.activation-api - - - javax.xml.bind - jaxb-api - - - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-json/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-json/build.gradle new file mode 100644 index 000000000000..67ec39087e25 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-json/build.gradle @@ -0,0 +1,14 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for reading and writing json" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api("org.springframework:spring-web") + api("com.fasterxml.jackson.core:jackson-databind") + api("com.fasterxml.jackson.datatype:jackson-datatype-jdk8") + api("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") + api("com.fasterxml.jackson.module:jackson-module-parameter-names") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-json/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-json/pom.xml deleted file mode 100644 index ba198fc5f178..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-json/pom.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-json - Spring Boot Json Starter - Starter for reading and writing json - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - org.springframework - spring-web - - - com.fasterxml.jackson.core - jackson-databind - - - com.fasterxml.jackson.datatype - jackson-datatype-jdk8 - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - - - com.fasterxml.jackson.module - jackson-module-parameter-names - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-jta-atomikos/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-jta-atomikos/build.gradle new file mode 100644 index 000000000000..cc9c208b503e --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-jta-atomikos/build.gradle @@ -0,0 +1,13 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for JTA transactions using Atomikos" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api("com.atomikos:transactions-jms") + api("com.atomikos:transactions-jta") + api("com.atomikos:transactions-jdbc") + api("jakarta.transaction:jakarta.transaction-api") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-jta-atomikos/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-jta-atomikos/pom.xml deleted file mode 100644 index 952efd70e16f..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-jta-atomikos/pom.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-jta-atomikos - Spring Boot Atomikos JTA Starter - Starter for JTA transactions using Atomikos - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - com.atomikos - transactions-jms - - - com.atomikos - transactions-jta - - - org.apache.geronimo.specs - geronimo-jta_1.0.1B_spec - - - - - com.atomikos - transactions-jdbc - - - jakarta.transaction - jakarta.transaction-api - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-jta-bitronix/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-jta-bitronix/pom.xml deleted file mode 100644 index 374205006a1b..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-jta-bitronix/pom.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-jta-bitronix - Spring Boot Bitronix JTA Starter - Starter for JTA transactions using Bitronix - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - jakarta.jms - jakarta.jms-api - - - jakarta.transaction - jakarta.transaction-api - - - org.codehaus.btm - btm - - - javax.transaction - jta - - - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-log4j2/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-log4j2/build.gradle new file mode 100644 index 000000000000..a7336d6c952e --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-log4j2/build.gradle @@ -0,0 +1,12 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Log4j2 for logging. An alternative to spring-boot-starter-logging" + +dependencies { + api("org.apache.logging.log4j:log4j-slf4j-impl") + api("org.apache.logging.log4j:log4j-core") + api("org.apache.logging.log4j:log4j-jul") + api("org.slf4j:jul-to-slf4j") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-log4j2/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-log4j2/pom.xml deleted file mode 100644 index 3f6bb33c49fc..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-log4j2/pom.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-log4j2 - Spring Boot Log4j 2 Starter - Starter for using Log4j2 for logging. An alternative to - spring-boot-starter-logging - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.apache.logging.log4j - log4j-slf4j-impl - - - org.apache.logging.log4j - log4j-core - - - org.apache.logging.log4j - log4j-jul - - - org.slf4j - jul-to-slf4j - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-logging/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-logging/build.gradle new file mode 100644 index 000000000000..ac7d3a5681ec --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-logging/build.gradle @@ -0,0 +1,11 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for logging using Logback. Default logging starter" + +dependencies { + api("ch.qos.logback:logback-classic") + api("org.apache.logging.log4j:log4j-to-slf4j") + api("org.slf4j:jul-to-slf4j") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-logging/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-logging/pom.xml deleted file mode 100644 index 80dc82f26f82..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-logging/pom.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-logging - Spring Boot Logging Starter - Starter for logging using Logback. Default logging starter - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - ch.qos.logback - logback-classic - - - org.apache.logging.log4j - log4j-to-slf4j - - - org.slf4j - jul-to-slf4j - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-mail/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-mail/build.gradle new file mode 100644 index 000000000000..07ae46662546 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-mail/build.gradle @@ -0,0 +1,11 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Java Mail and Spring Framework's email sending support" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api("org.springframework:spring-context-support") + api("com.sun.mail:jakarta.mail") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-mail/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-mail/pom.xml deleted file mode 100644 index 2378dfa55156..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-mail/pom.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-mail - Spring Boot Mail Starter - Starter for using Java Mail and Spring Framework's email sending - support - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - org.springframework - spring-context-support - - - com.sun.mail - jakarta.mail - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-mustache/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-mustache/build.gradle new file mode 100644 index 000000000000..5a05079f5727 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-mustache/build.gradle @@ -0,0 +1,10 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for building web applications using Mustache views" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api("com.samskivert:jmustache") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-mustache/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-mustache/pom.xml deleted file mode 100644 index c71dcaa464e3..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-mustache/pom.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-mustache - Spring Boot Mustache Starter - Starter for building web applications using Mustache views - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - com.samskivert - jmustache - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-oauth2-client/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-oauth2-client/build.gradle new file mode 100644 index 000000000000..8c7d6af3a7aa --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-oauth2-client/build.gradle @@ -0,0 +1,13 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Spring Security's OAuth2/OpenID Connect client features" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api("org.springframework.security:spring-security-config") + api("org.springframework.security:spring-security-core") + api("org.springframework.security:spring-security-oauth2-client") + api("org.springframework.security:spring-security-oauth2-jose") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-oauth2-client/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-oauth2-client/pom.xml deleted file mode 100644 index 63ecdd1d49e5..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-oauth2-client/pom.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-oauth2-client - Spring Boot OAuth2/OpenID Connect Client Starter - Starter for using Spring Security's OAuth2/OpenID Connect client features - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - com.sun.mail - jakarta.mail - - - org.springframework.security - spring-security-config - - - org.springframework.security - spring-security-core - - - org.springframework.security - spring-security-oauth2-client - - - com.sun.mail - javax.mail - - - - - org.springframework.security - spring-security-oauth2-jose - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-oauth2-resource-server/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-oauth2-resource-server/build.gradle new file mode 100644 index 000000000000..a91da01d8d68 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-oauth2-resource-server/build.gradle @@ -0,0 +1,13 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Spring Security's OAuth2 resource server features" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api("org.springframework.security:spring-security-config") + api("org.springframework.security:spring-security-core") + api("org.springframework.security:spring-security-oauth2-resource-server") + api("org.springframework.security:spring-security-oauth2-jose") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-oauth2-resource-server/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-oauth2-resource-server/pom.xml deleted file mode 100644 index 0a4f791f0f8d..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-oauth2-resource-server/pom.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-oauth2-resource-server - Spring Boot OAuth2 Resource Server Starter - Starter for using Spring Security's OAuth2 resource server features - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - org.springframework.security - spring-security-config - - - org.springframework.security - spring-security-core - - - org.springframework.security - spring-security-oauth2-resource-server - - - org.springframework.security - spring-security-oauth2-jose - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-parent/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-parent/build.gradle new file mode 100644 index 000000000000..6a10c7c13d82 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-parent/build.gradle @@ -0,0 +1,222 @@ +plugins { + id "org.springframework.boot.conventions" + id "org.springframework.boot.deployed" + id "org.springframework.boot.maven-repository" +} + +description = "Parent pom providing dependency and plugin management for applications built with Maven" + +publishing.publications.withType(MavenPublication) { + pom.withXml { xml -> + def root = xml.asNode() + root.groupId.replaceNode { + parent { + delegate.groupId("${project.group}") + delegate.artifactId("spring-boot-dependencies") + delegate.version("${project.version}") + } + } + root.remove(root.version) + root.description.plus { + properties { + delegate."java.version"('1.8') + delegate."resource.delimiter"('@') + delegate."maven.compiler.source"('${java.version}') + delegate."maven.compiler.target"('${java.version}') + delegate."project.build.sourceEncoding"('UTF-8') + delegate."project.reporting.outputEncoding"('UTF-8') + } + } + root.scm.plus { + build { + resources { + resource { + delegate.directory('${basedir}/src/main/resources') + delegate.filtering('true') + includes { + delegate.include('**/application*.yml') + delegate.include('**/application*.yaml') + delegate.include('**/application*.properties') + } + } + resource { + delegate.directory('${basedir}/src/main/resources') + excludes { + delegate.exclude('**/application*.yml') + delegate.exclude('**/application*.yaml') + delegate.exclude('**/application*.properties') + } + } + } + pluginManagement { + plugins { + plugin { + delegate.groupId('org.jetbrains.kotlin') + delegate.artifactId('kotlin-maven-plugin') + delegate.version('${kotlin.version}') + configuration { + delegate.jvmTarget('${java.version}') + delegate.javaParameters('true') + } + executions { + execution { + delegate.id('compile') + delegate.phase('compile') + goals { + delegate.goal('compile') + } + } + execution { + delegate.id('test-compile') + delegate.phase('test-compile') + goals { + delegate.goal('test-compile') + } + } + } + } + plugin { + delegate.groupId('org.apache.maven.plugins') + delegate.artifactId('maven-compiler-plugin') + configuration { + delegate.parameters('true') + } + } + plugin { + delegate.groupId('org.apache.maven.plugins') + delegate.artifactId('maven-failsafe-plugin') + executions { + execution { + goals { + delegate.goal('integration-test') + delegate.goal('verify') + } + } + } + configuration { + delegate.classesDirectory('${project.build.outputDirectory}') + } + } + plugin { + delegate.groupId('org.apache.maven.plugins') + delegate.artifactId('maven-jar-plugin') + configuration { + archive { + manifest { + delegate.mainClass('${start-class}') + delegate.addDefaultImplementationEntries('true') + } + } + } + } + plugin { + delegate.groupId('org.apache.maven.plugins') + delegate.artifactId('maven-war-plugin') + configuration { + archive { + manifest { + delegate.mainClass('${start-class}') + delegate.addDefaultImplementationEntries('true') + } + } + } + } + plugin { + delegate.groupId('org.apache.maven.plugins') + delegate.artifactId('maven-resources-plugin') + configuration { + delegate.propertiesEncoding('${project.build.sourceEncoding}') + delimiters { + delegate.delimiter('${resource.delimiter}') + } + delegate.useDefaultDelimiters('false') + } + } + plugin { + delegate.groupId('pl.project13.maven') + delegate.artifactId('git-commit-id-plugin') + executions { + execution { + goals { + delegate.goal('revision') + } + } + } + configuration { + delegate.verbose('true') + delegate.dateFormat("yyyy-MM-dd'T'HH:mm:ssZ") + delegate.generateGitPropertiesFile('true') + delegate.generateGitPropertiesFilename('${project.build.outputDirectory}/git.properties') + } + } + plugin { + delegate.groupId('org.springframework.boot') + delegate.artifactId('spring-boot-maven-plugin') + executions { + execution { + delegate.id('repackage') + goals { + delegate.goal('repackage') + } + } + } + configuration { + delegate.mainClass('${start-class}') + } + } + plugin { + delegate.groupId('org.apache.maven.plugins') + delegate.artifactId('maven-shade-plugin') + configuration { + delegate.keepDependenciesWithProvidedScope('true') + delegate.createDependencyReducedPom('true') + filters { + filter { + delegate.artifact('*:*') + excludes { + delegate.exclude('META-INF/*.SF') + delegate.exclude('META-INF/*.DSA') + delegate.exclude('META-INF/*.RSA') + } + } + } + } + delegate.dependencies { + dependency { + delegate.groupId('org.springframework.boot') + delegate.artifactId('spring-boot-maven-plugin') + delegate.version("${project.version}") + } + } + executions { + execution { + delegate.phase('package') + goals { + delegate.goal('shade') + } + configuration { + transformers { + transformer(implementation: 'org.apache.maven.plugins.shade.resource.AppendingTransformer') { + delegate.resource('META-INF/spring.handlers') + } + transformer(implementation: 'org.springframework.boot.maven.PropertiesMergingResourceTransformer') { + delegate.resource('META-INF/spring.factories') + } + transformer(implementation: 'org.apache.maven.plugins.shade.resource.AppendingTransformer') { + delegate.resource('META-INF/spring.schemas') + } + transformer(implementation: 'org.apache.maven.plugins.shade.resource.ServicesResourceTransformer') + transformer(implementation: 'org.apache.maven.plugins.shade.resource.ManifestResourceTransformer') { + delegate.mainClass('${start-class}') + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-parent/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-parent/pom.xml deleted file mode 100644 index 23b32d80e758..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-parent/pom.xml +++ /dev/null @@ -1,297 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-dependencies - ${revision} - ../../spring-boot-dependencies - - spring-boot-starter-parent - pom - Spring Boot Starter Parent - Parent pom providing dependency and plugin management for applications - built with Maven - - ${basedir}/../../.. - 1.8 - @ - UTF-8 - UTF-8 - ${java.version} - ${java.version} - - - - - - ${basedir}/src/main/resources - true - - **/application*.yml - **/application*.yaml - **/application*.properties - - - - ${basedir}/src/main/resources - - **/application*.yml - **/application*.yaml - **/application*.properties - - - - - - - - org.jetbrains.kotlin - kotlin-maven-plugin - ${kotlin.version} - - ${java.version} - true - - - - compile - compile - - compile - - - - test-compile - test-compile - - test-compile - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - true - - - - org.apache.maven.plugins - maven-failsafe-plugin - - - - integration-test - verify - - - - - ${project.build.outputDirectory} - - - - org.apache.maven.plugins - maven-jar-plugin - - - - ${start-class} - true - - - - - - org.apache.maven.plugins - maven-war-plugin - - - - ${start-class} - true - - - - - - org.codehaus.mojo - exec-maven-plugin - - ${start-class} - - - - org.apache.maven.plugins - maven-resources-plugin - - - ${resource.delimiter} - - false - - - - pl.project13.maven - git-commit-id-plugin - - - - revision - - - - - true - yyyy-MM-dd'T'HH:mm:ssZ - true - ${project.build.outputDirectory}/git.properties - - - - - org.springframework.boot - spring-boot-maven-plugin - - - repackage - - repackage - - - - - ${start-class} - - - - - org.apache.maven.plugins - maven-shade-plugin - - true - true - - - *:* - - META-INF/*.SF - META-INF/*.DSA - META-INF/*.RSA - - - - - - - org.springframework.boot - spring-boot-maven-plugin - ${revision} - - - - - package - - shade - - - - - META-INF/spring.handlers - - - META-INF/spring.factories - - - META-INF/spring.schemas - - - - ${start-class} - - - - - - - - - - - org.codehaus.mojo - flatten-maven-plugin - false - - - - flatten - process-resources - - flatten - - - true - - expand - keep - keep - expand - keep - keep - keep - keep - - - - - flatten-clean - clean - - clean - - - - - - org.codehaus.mojo - xml-maven-plugin - false - - - - post-process-flattened-pom - process-resources - - transform - - - - - ${project.basedir} - ${project.basedir} - .flattened-pom.xml - src/main/xslt/post-process-flattened-pom.xsl - - - indent - yes - - - - - - - - - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-parent/src/main/xslt/post-process-flattened-pom.xsl b/spring-boot-project/spring-boot-starters/spring-boot-starter-parent/src/main/xslt/post-process-flattened-pom.xsl deleted file mode 100644 index dff3f1f8493b..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-parent/src/main/xslt/post-process-flattened-pom.xsl +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-quartz/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-quartz/build.gradle new file mode 100644 index 000000000000..a0e033852e0c --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-quartz/build.gradle @@ -0,0 +1,12 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using the Quartz scheduler" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api("org.springframework:spring-context-support") + api("org.springframework:spring-tx") + api("org.quartz-scheduler:quartz") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-quartz/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-quartz/pom.xml deleted file mode 100644 index 7edae6ae0d4c..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-quartz/pom.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-quartz - Spring Boot Quartz Starter - Starter for using the Quartz scheduler - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - org.springframework - spring-context-support - - - org.springframework - spring-tx - - - org.quartz-scheduler - quartz - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-reactor-netty/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-reactor-netty/build.gradle new file mode 100644 index 000000000000..9a3b34e2b4b5 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-reactor-netty/build.gradle @@ -0,0 +1,9 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Reactor Netty as the embedded reactive HTTP server." + +dependencies { + api("io.projectreactor.netty:reactor-netty-http") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-reactor-netty/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-reactor-netty/pom.xml deleted file mode 100644 index fa72cb97ddb1..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-reactor-netty/pom.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-reactor-netty - Spring Boot Reactor Netty Starter - Starter for using Reactor Netty as the embedded reactive HTTP server. - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - io.projectreactor.netty - reactor-netty - - - org.glassfish - jakarta.el - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-rsocket/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-rsocket/build.gradle new file mode 100644 index 000000000000..f4a11678fcee --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-rsocket/build.gradle @@ -0,0 +1,15 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for building RSocket clients and servers" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-json")) + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-reactor-netty")) + api("com.fasterxml.jackson.dataformat:jackson-dataformat-cbor") + api("io.rsocket:rsocket-core") + api("io.rsocket:rsocket-transport-netty") + api("org.springframework:spring-messaging") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-rsocket/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-rsocket/pom.xml deleted file mode 100644 index b83a43efcecb..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-rsocket/pom.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-rsocket - Spring Boot RSocket Starter - Starter for building RSocket clients and servers. - - ${basedir}/../../.. - - - - org.springframework.boot - spring-boot-starter - - - org.springframework - spring-messaging - - - io.rsocket - rsocket-core - - - io.rsocket - rsocket-transport-netty - - - org.springframework.boot - spring-boot-starter-reactor-netty - - - org.springframework.boot - spring-boot-starter-json - - - com.fasterxml.jackson.dataformat - jackson-dataformat-cbor - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-security/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-security/build.gradle new file mode 100644 index 000000000000..c6e63f44efa3 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-security/build.gradle @@ -0,0 +1,12 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Spring Security" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api("org.springframework:spring-aop") + api("org.springframework.security:spring-security-config") + api("org.springframework.security:spring-security-web") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-security/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-security/pom.xml deleted file mode 100644 index 7b3112de86a4..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-security/pom.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-security - Spring Boot Security Starter - Starter for using Spring Security - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - org.springframework - spring-aop - - - org.springframework.security - spring-security-config - - - org.springframework.security - spring-security-web - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-test/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-test/build.gradle new file mode 100644 index 000000000000..74ba78aab07a --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-test/build.gradle @@ -0,0 +1,24 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for testing Spring Boot applications with libraries including JUnit Jupiter, Hamcrest and Mockito" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api(project(":spring-boot-project:spring-boot-test")) + api(project(":spring-boot-project:spring-boot-test-autoconfigure")) + api("com.jayway.jsonpath:json-path") + api("jakarta.xml.bind:jakarta.xml.bind-api") + api("org.assertj:assertj-core") + api("org.hamcrest:hamcrest") + api("org.junit.jupiter:junit-jupiter") + api("org.mockito:mockito-core") + api("org.mockito:mockito-junit-jupiter") + api("org.skyscreamer:jsonassert") + api("org.springframework:spring-core") + api("org.springframework:spring-test") + api("org.xmlunit:xmlunit-core") { + exclude group: "javax.xml.bind", module: "jaxb-api" + } +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-test/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-test/pom.xml deleted file mode 100644 index 03f9a83d7b5b..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-test/pom.xml +++ /dev/null @@ -1,129 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-test - Spring Boot Test Starter - Starter for testing Spring Boot applications with libraries including - JUnit, Hamcrest and Mockito - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - org.springframework.boot - spring-boot-test - - - org.springframework.boot - spring-boot-test-autoconfigure - - - com.jayway.jsonpath - json-path - - - jakarta.xml.bind - jakarta.xml.bind-api - - - org.junit.jupiter - junit-jupiter - - - org.junit.vintage - junit-vintage-engine - - - org.hamcrest - hamcrest-core - - - - - org.mockito - mockito-junit-jupiter - - - org.assertj - assertj-core - - - org.hamcrest - hamcrest - - - org.mockito - mockito-core - - - org.skyscreamer - jsonassert - - - org.springframework - spring-core - - - org.springframework - spring-test - - - org.xmlunit - xmlunit-core - - - javax.xml.bind - jaxb-api - - - - - - - - org.basepom.maven - duplicate-finder-maven-plugin - - - duplicate-dependencies - validate - - check - - - - - - - org.ow2.asm - asm - - - net.minidev - accessors-smart - - - - - - - - - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-thymeleaf/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-thymeleaf/build.gradle new file mode 100644 index 000000000000..a3a0cefe185b --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-thymeleaf/build.gradle @@ -0,0 +1,11 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for building MVC web applications using Thymeleaf views" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api("org.thymeleaf:thymeleaf-spring5") + api("org.thymeleaf.extras:thymeleaf-extras-java8time") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-thymeleaf/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-thymeleaf/pom.xml deleted file mode 100644 index 303066ffb246..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-thymeleaf/pom.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-thymeleaf - Spring Boot Thymeleaf Starter - Starter for building MVC web applications using Thymeleaf views - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - org.thymeleaf - thymeleaf-spring5 - - - org.thymeleaf.extras - thymeleaf-extras-java8time - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-tomcat/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-tomcat/build.gradle new file mode 100644 index 000000000000..8f57c34c352e --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-tomcat/build.gradle @@ -0,0 +1,16 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Tomcat as the embedded servlet container. Default servlet container starter used by spring-boot-starter-web" + +dependencies { + api("jakarta.annotation:jakarta.annotation-api") + api("org.apache.tomcat.embed:tomcat-embed-core") { + exclude group: "org.apache.tomcat", module: "tomcat-annotations-api" + } + api("org.apache.tomcat.embed:tomcat-embed-el") + api("org.apache.tomcat.embed:tomcat-embed-websocket") { + exclude group: "org.apache.tomcat", module: "tomcat-annotations-api" + } +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-tomcat/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-tomcat/pom.xml deleted file mode 100644 index 679367648466..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-tomcat/pom.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-tomcat - Spring Boot Tomcat Starter - Starter for using Tomcat as the embedded servlet container. Default - servlet container starter used by spring-boot-starter-web - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - jakarta.annotation - jakarta.annotation-api - - - org.apache.tomcat.embed - tomcat-embed-core - - - org.apache.tomcat - tomcat-annotations-api - - - - - org.apache.tomcat.embed - tomcat-embed-el - - - org.apache.tomcat.embed - tomcat-embed-websocket - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-undertow/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-undertow/build.gradle new file mode 100644 index 000000000000..9c4d02300bcb --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-undertow/build.gradle @@ -0,0 +1,21 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Undertow as the embedded servlet container. An alternative to spring-boot-starter-tomcat" + +dependencies { + api("io.undertow:undertow-core") + api("io.undertow:undertow-servlet") { + exclude group: "org.jboss.spec.javax.annotation", module: "jboss-annotations-api_1.3_spec" + exclude group: "org.jboss.spec.javax.servlet", module: "jboss-servlet-api_4.0_spec" + } + api("io.undertow:undertow-websockets-jsr") { + exclude group: "org.jboss.spec.javax.annotation", module: "jboss-annotations-api_1.3_spec" + exclude group: "org.jboss.spec.javax.servlet", module: "jboss-servlet-api_4.0_spec" + exclude group: "org.jboss.spec.javax.websocket", module: "jboss-websocket-api_1.1_spec" + } + api("jakarta.servlet:jakarta.servlet-api") + api("jakarta.websocket:jakarta.websocket-api") + api("org.apache.tomcat.embed:tomcat-embed-el") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-undertow/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-undertow/pom.xml deleted file mode 100644 index 83d5a29bdc75..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-undertow/pom.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-undertow - Spring Boot Undertow Starter - Starter for using Undertow as the embedded servlet container. An - alternative to spring-boot-starter-tomcat - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - io.undertow - undertow-core - - - io.undertow - undertow-servlet - - - org.jboss.spec.javax.servlet - jboss-servlet-api_4.0_spec - - - - - io.undertow - undertow-websockets-jsr - - - jakarta.servlet - jakarta.servlet-api - - - org.glassfish - jakarta.el - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-validation/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-validation/build.gradle new file mode 100644 index 000000000000..bc4d306b3b42 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-validation/build.gradle @@ -0,0 +1,11 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Java Bean Validation with Hibernate Validator" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api("org.apache.tomcat.embed:tomcat-embed-el") + api("org.hibernate.validator:hibernate-validator") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-validation/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-validation/pom.xml deleted file mode 100644 index 542f96b06701..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-validation/pom.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-validation - Spring Boot Validation Starter - Starter for using Java Bean Validation with Hibernate - Validator - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - jakarta.validation - jakarta.validation-api - - - org.apache.tomcat.embed - tomcat-embed-el - - - org.hibernate.validator - hibernate-validator - - - javax.validation - validation-api - - - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-web-services/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-web-services/build.gradle new file mode 100644 index 000000000000..c8e2854b60b2 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-web-services/build.gradle @@ -0,0 +1,15 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Spring Web Services" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-web")) + api("com.sun.xml.messaging.saaj:saaj-impl") + api("jakarta.xml.ws:jakarta.xml.ws-api") { + exclude group: "jakarta.activation", module: "jakarta.activation-api" + } + api("org.springframework:spring-oxm") + api("org.springframework.ws:spring-ws-core") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-web-services/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-web-services/pom.xml deleted file mode 100644 index 8e191dae28e0..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-web-services/pom.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-web-services - Spring Boot Web Services Starter - Starter for using Spring Web Services - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter-web - - - com.sun.xml.messaging.saaj - saaj-impl - - - javax.activation - activation - - - - - jakarta.xml.ws - jakarta.xml.ws-api - - - org.springframework - spring-oxm - - - org.springframework.ws - spring-ws-core - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-web/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-web/build.gradle new file mode 100644 index 000000000000..735883686b81 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-web/build.gradle @@ -0,0 +1,13 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for building web, including RESTful, applications using Spring MVC. Uses Tomcat as the default embedded container" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-json")) + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-tomcat")) + api("org.springframework:spring-web") + api("org.springframework:spring-webmvc") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-web/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-web/pom.xml deleted file mode 100644 index 94a0b6d4da0a..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-web/pom.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-web - Spring Boot Web Starter - Starter for building web, including RESTful, applications using Spring - MVC. Uses Tomcat as the default embedded container - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - org.springframework.boot - spring-boot-starter-json - - - org.springframework.boot - spring-boot-starter-tomcat - - - org.springframework.boot - spring-boot-starter-validation - - - org.apache.tomcat.embed - tomcat-embed-el - - - - - org.springframework - spring-web - - - org.springframework - spring-webmvc - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-webflux/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-webflux/build.gradle new file mode 100644 index 000000000000..2e81db32b648 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-webflux/build.gradle @@ -0,0 +1,13 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for building WebFlux applications using Spring Framework's Reactive Web support" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-json")) + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-reactor-netty")) + api("org.springframework:spring-web") + api("org.springframework:spring-webflux") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-webflux/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-webflux/pom.xml deleted file mode 100644 index b159eec6cb66..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-webflux/pom.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-webflux - Spring Boot WebFlux Starter - Starter for building WebFlux applications using Spring Framework's - Reactive Web support - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter - - - org.springframework.boot - spring-boot-starter-json - - - org.springframework.boot - spring-boot-starter-reactor-netty - - - org.springframework.boot - spring-boot-starter-validation - - - org.apache.tomcat.embed - tomcat-embed-el - - - - - org.springframework - spring-web - - - org.springframework - spring-webflux - - - org.synchronoss.cloud - nio-multipart-parser - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-websocket/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-websocket/build.gradle new file mode 100644 index 000000000000..ff04bb94b655 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-websocket/build.gradle @@ -0,0 +1,11 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for building WebSocket applications using Spring Framework's WebSocket support" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-web")) + api("org.springframework:spring-messaging") + api("org.springframework:spring-websocket") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-websocket/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter-websocket/pom.xml deleted file mode 100644 index 6e6755ce7d61..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-websocket/pom.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter-websocket - Spring Boot WebSocket Starter - Starter for building WebSocket applications using Spring Framework's - WebSocket support - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework - spring-messaging - - - org.springframework - spring-websocket - - - diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter/build.gradle new file mode 100644 index 000000000000..1e5a6fb6f762 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter/build.gradle @@ -0,0 +1,14 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Core starter, including auto-configuration support, logging and YAML" + +dependencies { + api(project(":spring-boot-project:spring-boot")) + api(project(":spring-boot-project:spring-boot-autoconfigure")) + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-logging")) + api("jakarta.annotation:jakarta.annotation-api") + api("org.springframework:spring-core") + api("org.yaml:snakeyaml") +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter/pom.xml b/spring-boot-project/spring-boot-starters/spring-boot-starter/pom.xml deleted file mode 100644 index a4029e66556c..000000000000 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter/pom.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starters - ${revision} - - spring-boot-starter - Spring Boot Starter - Core starter, including auto-configuration support, logging and YAML - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot - - - org.springframework.boot - spring-boot-autoconfigure - - - org.springframework.boot - spring-boot-starter-logging - - - jakarta.annotation - jakarta.annotation-api - - - org.springframework - spring-core - - - org.yaml - snakeyaml - runtime - - - diff --git a/spring-boot-project/spring-boot-starters/src/main/assembly/starter-poms-assembly.xml b/spring-boot-project/spring-boot-starters/src/main/assembly/starter-poms-assembly.xml deleted file mode 100644 index dc90e356e2ab..000000000000 --- a/spring-boot-project/spring-boot-starters/src/main/assembly/starter-poms-assembly.xml +++ /dev/null @@ -1,22 +0,0 @@ - - starter-poms - - zip - - false - - - - - - - **/pom.xml - - - - - - - diff --git a/spring-boot-project/spring-boot-test-autoconfigure/build.gradle b/spring-boot-project/spring-boot-test-autoconfigure/build.gradle new file mode 100644 index 000000000000..cf2508f87705 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/build.gradle @@ -0,0 +1,133 @@ +plugins { + id "java-library" + id "org.springframework.boot.conventions" + id "org.springframework.boot.deployed" + id "org.springframework.boot.optional-dependencies" +} + +description = "Spring Boot Test AutoConfigure" + +dependencies { + api(project(":spring-boot-project:spring-boot")) + api(project(":spring-boot-project:spring-boot-test")) + api(project(":spring-boot-project:spring-boot-autoconfigure")) + + optional("jakarta.json.bind:jakarta.json.bind-api") + optional("jakarta.persistence:jakarta.persistence-api") + optional("jakarta.servlet:jakarta.servlet-api") + optional("jakarta.transaction:jakarta.transaction-api") + optional("com.fasterxml.jackson.core:jackson-databind") + optional("com.google.code.gson:gson") + optional("com.jayway.jsonpath:json-path") + optional("com.sun.xml.messaging.saaj:saaj-impl") + optional("io.rest-assured:rest-assured") { + exclude group: "commons-logging", module: "commons-logging" + exclude group: "javax.activation", module: "activation" + exclude group: "javax.xml.bind", module: "jaxb-api" + } + optional("net.sourceforge.htmlunit:htmlunit") { + exclude group: "commons-logging", module: "commons-logging" + } + optional("org.hibernate:hibernate-core") { + exclude group: "commons-logging", module: "commons-logging" + exclude group: "javax.activation", module: "javax.activation-api" + exclude group: "javax.persistence", module: "javax.persistence-api" + exclude group: "javax.xml.bind", module: "jaxb-api" + exclude group: "org.jboss.spec.javax.transaction", module: "jboss-transaction-api_1.2_spec" + } + optional("org.junit.jupiter:junit-jupiter-api") + optional("org.seleniumhq.selenium:htmlunit-driver") { + exclude group: "commons-logging", module: "commons-logging" + } + optional("org.seleniumhq.selenium:selenium-api") + optional("org.springframework:spring-orm") + optional("org.springframework:spring-test") + optional("org.springframework:spring-web") + optional("org.springframework:spring-webmvc") + optional("org.springframework:spring-webflux") + optional("org.springframework.data:spring-data-cassandra") { + exclude group: "org.slf4j", module: "jcl-over-slf4j" + } + optional("org.springframework.data:spring-data-jdbc") + optional("org.springframework.data:spring-data-jpa") + optional("org.springframework.data:spring-data-ldap") + optional("org.springframework.data:spring-data-mongodb") + optional("org.springframework.data:spring-data-neo4j") + optional("org.springframework.data:spring-data-r2dbc") + optional("org.springframework.data:spring-data-redis") + optional("org.springframework.restdocs:spring-restdocs-mockmvc") { + exclude group: "javax.servlet", module: "javax.servlet-api" + } + optional("org.springframework.restdocs:spring-restdocs-restassured") { + exclude group: "commons-logging", module: "commons-logging" + exclude group: "javax.activation", module: "activation" + exclude group: "javax.xml.bind", module: "jaxb-api" + } + optional("org.springframework.restdocs:spring-restdocs-webtestclient") + optional("org.springframework.security:spring-security-config") + optional("org.springframework.security:spring-security-test") + optional("org.springframework.ws:spring-ws-core") + optional("org.springframework.ws:spring-ws-test") + optional("org.apache.tomcat.embed:tomcat-embed-core") + optional("org.mongodb:mongodb-driver-reactivestreams") + optional("org.mongodb:mongodb-driver-sync") + + testImplementation(project(":spring-boot-project:spring-boot-actuator")) + testImplementation(project(":spring-boot-project:spring-boot-actuator-autoconfigure")) + testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) + testImplementation("ch.qos.logback:logback-classic") + testImplementation("com.fasterxml.jackson.module:jackson-module-parameter-names") + testImplementation("com.h2database:h2") + testImplementation("com.unboundid:unboundid-ldapsdk") + testImplementation("io.lettuce:lettuce-core") + testImplementation("io.micrometer:micrometer-registry-prometheus") + testImplementation("io.projectreactor:reactor-core") + testImplementation("io.projectreactor:reactor-test") + testImplementation("io.r2dbc:r2dbc-h2") + testImplementation("jakarta.json:jakarta.json-api") + testImplementation("org.apache.commons:commons-pool2") + testImplementation("org.apache.johnzon:johnzon-jsonb") + testImplementation("org.apache.tomcat.embed:tomcat-embed-el") + testImplementation("org.aspectj:aspectjrt") + testImplementation("org.aspectj:aspectjweaver") + testImplementation("org.assertj:assertj-core") + testImplementation("org.awaitility:awaitility") + testImplementation("org.hibernate.validator:hibernate-validator") + testImplementation("org.hsqldb:hsqldb") + testImplementation("org.jooq:jooq") { + exclude group: "javax.xml.bind", module: "jaxb-api" + } + testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.junit.platform:junit-platform-engine") + testImplementation("org.junit.platform:junit-platform-launcher") + testImplementation("org.mockito:mockito-core") + testImplementation("org.mockito:mockito-junit-jupiter") + testImplementation("org.skyscreamer:jsonassert") + testImplementation("org.springframework.hateoas:spring-hateoas") + testImplementation("org.springframework.plugin:spring-plugin-core") + testImplementation("org.testcontainers:cassandra") + testImplementation("org.testcontainers:junit-jupiter") + testImplementation("org.testcontainers:mongodb") + testImplementation("org.testcontainers:neo4j") + testImplementation("org.testcontainers:testcontainers") + testImplementation("org.thymeleaf:thymeleaf") +} + +configurations { + configurationPropertiesMetadata +} + +artifacts { + configurationPropertiesMetadata new File(sourceSets.main.output.resourcesDir, "META-INF/spring-configuration-metadata.json"), { artifact -> + artifact.builtBy sourceSets.main.processResourcesTaskName + } +} + +test { + include "**/*Tests.class" +} + +task testSliceMetadata(type: org.springframework.boot.build.test.autoconfigure.TestSliceMetadata) { + sourceSet = sourceSets.main + outputFile = file("${buildDir}/test-slice-metadata.properties") +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/pom.xml b/spring-boot-project/spring-boot-test-autoconfigure/pom.xml deleted file mode 100644 index 1af6f9b2996c..000000000000 --- a/spring-boot-project/spring-boot-test-autoconfigure/pom.xml +++ /dev/null @@ -1,384 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-parent - ${revision} - ../spring-boot-parent - - spring-boot-test-autoconfigure - Spring Boot Test Auto-Configure - Spring Boot Test Auto-Configure - - ${basedir}/../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - - org.springframework.boot - spring-boot-test - - - org.springframework.boot - spring-boot-autoconfigure - - - - com.fasterxml.jackson.core - jackson-databind - true - - - com.google.code.gson - gson - true - - - com.jayway.jsonpath - json-path - true - - - io.rest-assured - rest-assured - true - - - javax.xml.bind - jaxb-api - - - javax.activation - activation - - - org.hamcrest - hamcrest-core - - - org.hamcrest - hamcrest-library - - - - - jakarta.json.bind - jakarta.json.bind-api - true - - - jakarta.persistence - jakarta.persistence-api - true - - - jakarta.servlet - jakarta.servlet-api - true - - - jakarta.transaction - jakarta.transaction-api - true - - - net.sourceforge.htmlunit - htmlunit - true - - - org.hibernate - hibernate-core - - - org.jboss.spec.javax.transaction - jboss-transaction-api_1.2_spec - - - javax.activation - javax.activation-api - - - javax.persistence - javax.persistence-api - - - javax.xml.bind - jaxb-api - - - true - - - org.junit.jupiter - junit-jupiter-api - true - - - org.seleniumhq.selenium - htmlunit-driver - true - - - org.seleniumhq.selenium - selenium-api - true - - - org.springframework - spring-orm - true - - - org.springframework - spring-test - true - - - org.springframework - spring-web - true - - - org.springframework - spring-webmvc - true - - - org.springframework - spring-webflux - true - - - org.springframework.data - spring-data-jdbc - true - - - org.springframework.data - spring-data-jpa - - - org.aspectj - aspectjrt - - - true - - - org.springframework.data - spring-data-ldap - true - - - org.springframework.data - spring-data-mongodb - true - - - org.springframework.data - spring-data-neo4j - true - - - org.springframework.data - spring-data-redis - true - - - org.springframework.restdocs - spring-restdocs-mockmvc - true - - - javax.servlet - javax.servlet-api - - - - - org.springframework.restdocs - spring-restdocs-restassured - true - - - org.springframework.restdocs - spring-restdocs-webtestclient - true - - - org.springframework.security - spring-security-config - true - - - org.springframework.security - spring-security-test - true - - - - org.springframework.boot - spring-boot-test-support - test - - - ch.qos.logback - logback-classic - test - - - com.fasterxml.jackson.module - jackson-module-parameter-names - test - - - com.h2database - h2 - test - - - com.unboundid - unboundid-ldapsdk - test - - - de.flapdoodle.embed - de.flapdoodle.embed.mongo - test - - - io.lettuce - lettuce-core - test - - - io.projectreactor - reactor-core - test - - - jakarta.json - jakarta.json-api - test - - - jakarta.validation - jakarta.validation-api - test - - - org.apache.commons - commons-pool2 - test - - - org.apache.johnzon - johnzon-jsonb - test - - - org.apache.tomcat.embed - tomcat-embed-core - true - - - org.apache.tomcat.embed - tomcat-embed-el - test - - - org.aspectj - aspectjrt - test - - - org.aspectj - aspectjweaver - test - - - org.hibernate.validator - hibernate-validator - test - - - javax.validation - validation-api - - - - - org.hsqldb - hsqldb - test - - - org.jooq - jooq - test - - - javax.xml.bind - jaxb-api - - - - - org.mongodb - mongodb-driver-async - true - - - org.mongodb - mongodb-driver-reactivestreams - true - - - org.skyscreamer - jsonassert - test - - - org.springframework.hateoas - spring-hateoas - test - - - org.testcontainers - junit-jupiter - test - - - org.testcontainers - neo4j - test - - - org.testcontainers - testcontainers - test - - - javax.annotation - javax.annotation-api - - - javax.xml.bind - jaxb-api - - - org.hamcrest - hamcrest-core - - - - - diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/OverrideAutoConfigurationContextCustomizerFactory.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/OverrideAutoConfigurationContextCustomizerFactory.java index ab9652cbfbde..cd66fcbd6127 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/OverrideAutoConfigurationContextCustomizerFactory.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/OverrideAutoConfigurationContextCustomizerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,12 +21,11 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.core.annotation.MergedAnnotations; -import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.ContextCustomizerFactory; import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.context.TestContextAnnotationUtils; /** * {@link ContextCustomizerFactory} to support @@ -39,8 +38,9 @@ class OverrideAutoConfigurationContextCustomizerFactory implements ContextCustom @Override public ContextCustomizer createContextCustomizer(Class testClass, List configurationAttributes) { - boolean enabled = MergedAnnotations.from(testClass, SearchStrategy.TYPE_HIERARCHY) - .get(OverrideAutoConfiguration.class).getValue("enabled", Boolean.class).orElse(true); + OverrideAutoConfiguration overrideAutoConfiguration = TestContextAnnotationUtils.findMergedAnnotation(testClass, + OverrideAutoConfiguration.class); + boolean enabled = (overrideAutoConfiguration != null) ? overrideAutoConfiguration.enabled() : true; return !enabled ? new DisableAutoConfigurationContextCustomizer() : null; } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/actuate/metrics/AutoConfigureMetrics.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/actuate/metrics/AutoConfigureMetrics.java new file mode 100644 index 000000000000..50e76a3098c0 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/actuate/metrics/AutoConfigureMetrics.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.actuate.metrics; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation that can be applied to a test class to enable auto-configuration for metrics + * exporters. + * + * @author Chris Bono + * @since 2.4.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface AutoConfigureMetrics { + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/actuate/metrics/MetricsExportContextCustomizerFactory.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/actuate/metrics/MetricsExportContextCustomizerFactory.java new file mode 100644 index 000000000000..01cf079e9bf9 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/actuate/metrics/MetricsExportContextCustomizerFactory.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.actuate.metrics; + +import java.util.List; + +import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.test.context.ContextConfigurationAttributes; +import org.springframework.test.context.ContextCustomizer; +import org.springframework.test.context.ContextCustomizerFactory; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.context.TestContextAnnotationUtils; + +/** + * {@link ContextCustomizerFactory} that globally disables metrics export unless + * {@link AutoConfigureMetrics} is set on the test class. + * + * @author Chris Bono + */ +class MetricsExportContextCustomizerFactory implements ContextCustomizerFactory { + + @Override + public ContextCustomizer createContextCustomizer(Class testClass, + List configAttributes) { + boolean disableMetricsExport = TestContextAnnotationUtils.findMergedAnnotation(testClass, + AutoConfigureMetrics.class) == null; + return disableMetricsExport ? new DisableMetricExportContextCustomizer() : null; + } + + static class DisableMetricExportContextCustomizer implements ContextCustomizer { + + @Override + public void customizeContext(ConfigurableApplicationContext context, + MergedContextConfiguration mergedContextConfiguration) { + TestPropertyValues.of("management.metrics.export.defaults.enabled=false", + "management.metrics.export.simple.enabled=true").applyTo(context); + } + + @Override + public boolean equals(Object obj) { + return (obj != null) && (getClass() == obj.getClass()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/actuate/metrics/package-info.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/actuate/metrics/package-info.java new file mode 100644 index 000000000000..6dfcc180e669 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/actuate/metrics/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for handling metrics in tests. + */ +package org.springframework.boot.test.autoconfigure.actuate.metrics; diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/cassandra/AutoConfigureDataCassandra.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/cassandra/AutoConfigureDataCassandra.java new file mode 100644 index 000000000000..3c070088dfb8 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/cassandra/AutoConfigureDataCassandra.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.data.cassandra; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; + +/** + * {@link ImportAutoConfiguration Auto-configuration imports} for typical Data Cassandra + * tests. Most tests should consider using {@link DataCassandraTest @DataCassandraTest} + * rather than using this annotation directly. + * + * @author Artsiom Yudovin + * @since 2.4.0 + * @see DataCassandraTest + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ImportAutoConfiguration +public @interface AutoConfigureDataCassandra { + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/cassandra/DataCassandraTest.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/cassandra/DataCassandraTest.java new file mode 100644 index 000000000000..37dd6c139635 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/cassandra/DataCassandraTest.java @@ -0,0 +1,105 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.data.cassandra; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.autoconfigure.OverrideAutoConfiguration; +import org.springframework.boot.test.autoconfigure.core.AutoConfigureCache; +import org.springframework.boot.test.autoconfigure.filter.TypeExcludeFilters; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.core.annotation.AliasFor; +import org.springframework.core.env.Environment; +import org.springframework.test.context.BootstrapWith; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +/** + * Annotation that can be used for a Cassandra test that focuses only on + * Cassandra components. + *

    + * Using this annotation will disable full auto-configuration and instead apply only + * configuration relevant to Cassandra tests. + *

    + * When using JUnit 4, this annotation should be used in combination with + * {@code @RunWith(SpringRunner.class)}. + * + * @author Artsiom Yudovin + * @author Stephane Nicoll + * @since 2.4.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@BootstrapWith(DataCassandraTestContextBootstrapper.class) +@ExtendWith(SpringExtension.class) +@OverrideAutoConfiguration(enabled = false) +@TypeExcludeFilters(DataCassandraTypeExcludeFilter.class) +@AutoConfigureCache +@AutoConfigureDataCassandra +@ImportAutoConfiguration +public @interface DataCassandraTest { + + /** + * Properties in form {@literal key=value} that should be added to the Spring + * {@link Environment} before the test runs. + * @return the properties to add + * @since 2.1.0 + */ + String[] properties() default {}; + + /** + * Determines if default filtering should be used with + * {@link SpringBootApplication @SpringBootApplication}. By default no beans are + * included. + * @see #includeFilters() + * @see #excludeFilters() + * @return if default filters should be used + */ + boolean useDefaultFilters() default true; + + /** + * A set of include filters which can be used to add otherwise filtered beans to the + * application context. + * @return include filters to apply + */ + Filter[] includeFilters() default {}; + + /** + * A set of exclude filters which can be used to filter beans that would otherwise be + * added to the application context. + * @return exclude filters to apply + */ + Filter[] excludeFilters() default {}; + + /** + * Auto-configuration exclusions that should be applied for this test. + * @return auto-configuration exclusions to apply + */ + @AliasFor(annotation = ImportAutoConfiguration.class, attribute = "exclude") + Class[] excludeAutoConfiguration() default {}; + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/cassandra/DataCassandraTestContextBootstrapper.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/cassandra/DataCassandraTestContextBootstrapper.java new file mode 100644 index 000000000000..c0fc1a76c25b --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/cassandra/DataCassandraTestContextBootstrapper.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.data.cassandra; + +import org.springframework.boot.test.context.SpringBootTestContextBootstrapper; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; +import org.springframework.test.context.TestContextBootstrapper; + +/** + * {@link TestContextBootstrapper} for {@link DataCassandraTest @DataCassandraTest} + * support. + * + * @author Artsiom Yudovin + */ +class DataCassandraTestContextBootstrapper extends SpringBootTestContextBootstrapper { + + @Override + protected String[] getProperties(Class testClass) { + return MergedAnnotations.from(testClass, SearchStrategy.INHERITED_ANNOTATIONS).get(DataCassandraTest.class) + .getValue("properties", String[].class).orElse(null); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/cassandra/DataCassandraTypeExcludeFilter.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/cassandra/DataCassandraTypeExcludeFilter.java new file mode 100644 index 000000000000..c4bae8479df4 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/cassandra/DataCassandraTypeExcludeFilter.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.data.cassandra; + +import org.springframework.boot.context.TypeExcludeFilter; +import org.springframework.boot.test.autoconfigure.filter.StandardAnnotationCustomizableTypeExcludeFilter; + +/** + * {@link TypeExcludeFilter} for {@link DataCassandraTest @DataCassandraTest}. + * + * @author Artsiom Yudovin + */ +class DataCassandraTypeExcludeFilter extends StandardAnnotationCustomizableTypeExcludeFilter { + + protected DataCassandraTypeExcludeFilter(Class testClass) { + super(testClass); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/cassandra/package-info.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/cassandra/package-info.java new file mode 100644 index 000000000000..ea5fb8c6fd87 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/cassandra/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for Data Cassandra tests. + */ +package org.springframework.boot.test.autoconfigure.data.cassandra; diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/neo4j/DataNeo4jTest.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/neo4j/DataNeo4jTest.java index 24e980506bbf..15bc29bf6a5c 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/neo4j/DataNeo4jTest.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/neo4j/DataNeo4jTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,9 +44,10 @@ * Using this annotation will disable full auto-configuration and instead apply only * configuration relevant to Neo4j tests. *

    - * By default, tests annotated with {@code @DataNeo4jTest} will use an embedded in-memory - * Neo4j process (if available). They will also be transactional with the usual - * test-related semantics (i.e. rollback by default). + * By default, tests annotated with {@code @DataNeo4jTest} are transactional with the + * usual test-related semantics (i.e. rollback by default). This feature is not supported + * with reactive access so this should be disabled by annotating the test class with + * {@code @Transactional(propagation = Propagation.NOT_SUPPORTED)}. *

    * When using JUnit 4, this annotation should be used in combination with * {@code @RunWith(SpringRunner.class)}. diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/r2dbc/AutoConfigureDataR2dbc.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/r2dbc/AutoConfigureDataR2dbc.java new file mode 100644 index 000000000000..23bdebe03d57 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/r2dbc/AutoConfigureDataR2dbc.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.data.r2dbc; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; + +/** + * {@link ImportAutoConfiguration Auto-configuration imports} for typical Data R2DBC + * tests. Most tests should consider using {@link DataR2dbcTest @DataR2dbcTest} rather + * than using this annotation directly. + * + * @author Mark Paluch + * @see DataR2dbcTest + * @since 2.3.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ImportAutoConfiguration +public @interface AutoConfigureDataR2dbc { + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/r2dbc/DataR2dbcTest.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/r2dbc/DataR2dbcTest.java new file mode 100644 index 000000000000..d49bbb805a41 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/r2dbc/DataR2dbcTest.java @@ -0,0 +1,102 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.data.r2dbc; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.autoconfigure.OverrideAutoConfiguration; +import org.springframework.boot.test.autoconfigure.filter.TypeExcludeFilters; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.core.annotation.AliasFor; +import org.springframework.core.env.Environment; +import org.springframework.test.context.BootstrapWith; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +/** + * Annotation that can be used for a R2DBC test that focuses only on Data + * R2DBC components. + *

    + * Using this annotation will disable full auto-configuration and instead apply only + * configuration relevant to Data R2DBC tests. + *

    + * When using JUnit 4, this annotation should be used in combination with + * {@code @RunWith(SpringRunner.class)}. + * + * @author Mark Paluch + * @author Stephane Nicoll + * @since 2.3.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@BootstrapWith(DataR2dbcTestContextBootstrapper.class) +@ExtendWith(SpringExtension.class) +@OverrideAutoConfiguration(enabled = false) +@TypeExcludeFilters(DataR2dbcTypeExcludeFilter.class) +@AutoConfigureDataR2dbc +@ImportAutoConfiguration +public @interface DataR2dbcTest { + + /** + * Properties in form {@literal key=value} that should be added to the Spring + * {@link Environment} before the test runs. + * @return the properties to add + */ + String[] properties() default {}; + + /** + * Determines if default filtering should be used with + * {@link SpringBootApplication @SpringBootApplication}. By default no beans are + * included. + * @see #includeFilters() + * @see #excludeFilters() + * @return if default filters should be used + */ + boolean useDefaultFilters() default true; + + /** + * A set of include filters which can be used to add otherwise filtered beans to the + * application context. + * @return include filters to apply + */ + Filter[] includeFilters() default {}; + + /** + * A set of exclude filters which can be used to filter beans that would otherwise be + * added to the application context. + * @return exclude filters to apply + */ + Filter[] excludeFilters() default {}; + + /** + * Auto-configuration exclusions that should be applied for this test. + * @return auto-configuration exclusions to apply + */ + @AliasFor(annotation = ImportAutoConfiguration.class, attribute = "exclude") + Class[] excludeAutoConfiguration() default {}; + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/r2dbc/DataR2dbcTestContextBootstrapper.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/r2dbc/DataR2dbcTestContextBootstrapper.java new file mode 100644 index 000000000000..3da527a33810 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/r2dbc/DataR2dbcTestContextBootstrapper.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.data.r2dbc; + +import org.springframework.boot.test.context.SpringBootTestContextBootstrapper; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; +import org.springframework.test.context.TestContextBootstrapper; + +/** + * {@link TestContextBootstrapper} for {@link DataR2dbcTest @DataR2dbcTest} support. + * + * @author Mark Paluch + */ +class DataR2dbcTestContextBootstrapper extends SpringBootTestContextBootstrapper { + + @Override + protected String[] getProperties(Class testClass) { + return MergedAnnotations.from(testClass, SearchStrategy.INHERITED_ANNOTATIONS).get(DataR2dbcTest.class) + .getValue("properties", String[].class).orElse(null); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/r2dbc/DataR2dbcTypeExcludeFilter.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/r2dbc/DataR2dbcTypeExcludeFilter.java new file mode 100644 index 000000000000..1c53ac01b577 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/r2dbc/DataR2dbcTypeExcludeFilter.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.data.r2dbc; + +import org.springframework.boot.context.TypeExcludeFilter; +import org.springframework.boot.test.autoconfigure.filter.StandardAnnotationCustomizableTypeExcludeFilter; + +/** + * {@link TypeExcludeFilter} for {@link DataR2dbcTest @DataR2dbcTest}. + * + * @author Mark Paluch + */ +class DataR2dbcTypeExcludeFilter extends StandardAnnotationCustomizableTypeExcludeFilter { + + DataR2dbcTypeExcludeFilter(Class testClass) { + super(testClass); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/r2dbc/package-info.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/r2dbc/package-info.java new file mode 100644 index 000000000000..2204f32d8376 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/data/r2dbc/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for Data R2DBC tests. + */ +package org.springframework.boot.test.autoconfigure.data.r2dbc; diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/AnnotationCustomizableTypeExcludeFilter.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/AnnotationCustomizableTypeExcludeFilter.java index d9670c1ce029..94640ff18f62 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/AnnotationCustomizableTypeExcludeFilter.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/AnnotationCustomizableTypeExcludeFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,10 +61,7 @@ protected boolean include(MetadataReader metadataReader, MetadataReaderFactory m metadataReaderFactory)) { return true; } - if (isUseDefaultFilters() && defaultInclude(metadataReader, metadataReaderFactory)) { - return true; - } - return false; + return isUseDefaultFilters() && defaultInclude(metadataReader, metadataReaderFactory); } protected boolean defaultInclude(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFilters.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFilters.java index 3b7d3b516609..2e0158a12dca 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFilters.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFilters.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -37,6 +38,7 @@ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented +@Inherited public @interface TypeExcludeFilters { /** diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFiltersContextCustomizerFactory.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFiltersContextCustomizerFactory.java index b93afdcfc47e..e0c748fd49d1 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFiltersContextCustomizerFactory.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFiltersContextCustomizerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,12 +21,11 @@ import java.util.List; import org.springframework.boot.context.TypeExcludeFilter; -import org.springframework.core.annotation.MergedAnnotation; -import org.springframework.core.annotation.MergedAnnotations; -import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.ContextCustomizerFactory; +import org.springframework.test.context.TestContextAnnotationUtils; +import org.springframework.test.context.TestContextAnnotationUtils.AnnotationDescriptor; import org.springframework.util.ObjectUtils; /** @@ -43,12 +42,13 @@ class TypeExcludeFiltersContextCustomizerFactory implements ContextCustomizerFac @Override public ContextCustomizer createContextCustomizer(Class testClass, List configurationAttributes) { - Class[] filterClasses = MergedAnnotations.from(testClass, SearchStrategy.INHERITED_ANNOTATIONS) - .get(TypeExcludeFilters.class).getValue(MergedAnnotation.VALUE, Class[].class).orElse(NO_FILTERS); + AnnotationDescriptor descriptor = TestContextAnnotationUtils + .findAnnotationDescriptor(testClass, TypeExcludeFilters.class); + Class[] filterClasses = (descriptor != null) ? descriptor.getAnnotation().value() : NO_FILTERS; if (ObjectUtils.isEmpty(filterClasses)) { return null; } - return createContextCustomizer(testClass, filterClasses); + return createContextCustomizer(descriptor.getRootDeclaringClass(), filterClasses); } @SuppressWarnings("unchecked") diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jdbc/AutoConfigureTestDatabase.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jdbc/AutoConfigureTestDatabase.java index 809e5dc71cae..e79b78ec0f43 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jdbc/AutoConfigureTestDatabase.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jdbc/AutoConfigureTestDatabase.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,10 +29,13 @@ import org.springframework.boot.jdbc.EmbeddedDatabaseConnection; import org.springframework.boot.test.autoconfigure.properties.PropertyMapping; import org.springframework.boot.test.autoconfigure.properties.SkipPropertyMapping; +import org.springframework.context.annotation.Primary; /** * Annotation that can be applied to a test class to configure a test database to use - * instead of any application defined or auto-configured {@link DataSource}. + * instead of the application-defined or auto-configured {@link DataSource}. In the case + * of multiple {@code DataSource} beans, only the {@link Primary @Primary} + * {@code DataSource} is considered. * * @author Phillip Webb * @since 1.5.0 @@ -47,15 +50,16 @@ public @interface AutoConfigureTestDatabase { /** - * Determines what type of existing DataSource beans can be replaced. + * Determines what type of existing DataSource bean can be replaced. * @return the type of existing DataSource to replace */ @PropertyMapping(skip = SkipPropertyMapping.ON_DEFAULT_VALUE) Replace replace() default Replace.ANY; /** - * The type of connection to be established when {@link #replace() replacing} the data - * source. By default will attempt to detect the connection based on the classpath. + * The type of connection to be established when {@link #replace() replacing} the + * DataSource. By default will attempt to detect the connection based on the + * classpath. * @return the type of connection to use */ EmbeddedDatabaseConnection connection() default EmbeddedDatabaseConnection.NONE; @@ -66,12 +70,12 @@ enum Replace { /** - * Replace any DataSource bean (auto-configured or manually defined). + * Replace the DataSource bean whether it was auto-configured or manually defined. */ ANY, /** - * Only replace auto-configured DataSource. + * Only replace the DataSource if it was auto-configured. */ AUTO_CONFIGURED, diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jdbc/TestDatabaseAutoConfiguration.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jdbc/TestDatabaseAutoConfiguration.java index bf5183206d01..2da486df0360 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jdbc/TestDatabaseAutoConfiguration.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jdbc/TestDatabaseAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,9 @@ package org.springframework.boot.test.autoconfigure.jdbc; +import java.util.HashMap; +import java.util.Map; + import javax.sql.DataSource; import org.apache.commons.logging.Log; @@ -40,7 +43,9 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; +import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; +import org.springframework.core.env.MapPropertySource; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; import org.springframework.util.Assert; @@ -168,6 +173,13 @@ private static class EmbeddedDataSourceFactory { EmbeddedDataSourceFactory(Environment environment) { this.environment = environment; + if (environment instanceof ConfigurableEnvironment) { + Map source = new HashMap<>(); + source.put("spring.datasource.schema-username", ""); + source.put("spring.sql.init.username", ""); + ((ConfigurableEnvironment) environment).getPropertySources() + .addFirst(new MapPropertySource("testDatabase", source)); + } } EmbeddedDatabase getEmbeddedDatabase() { diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/json/JsonTestersAutoConfiguration.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/json/JsonTestersAutoConfiguration.java index 4b512bef0c65..183657b91ec9 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/json/JsonTestersAutoConfiguration.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/json/JsonTestersAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessorAdapter; +import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -53,8 +53,8 @@ * * @author Phillip Webb * @author Eddú Meléndez - * @see AutoConfigureJsonTesters * @since 1.4.0 + * @see AutoConfigureJsonTesters */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(name = "org.assertj.core.api.Assert") @@ -163,7 +163,7 @@ public Class getObjectType() { /** * {@link BeanPostProcessor} used to initialize JSON testers. */ - static class JsonMarshalTestersBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter { + static class JsonMarshalTestersBeanPostProcessor implements InstantiationAwareBeanPostProcessor { @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/orm/jpa/DataJpaTest.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/orm/jpa/DataJpaTest.java index 7534c4fc5e6d..26de93fdb150 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/orm/jpa/DataJpaTest.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/orm/jpa/DataJpaTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,7 @@ import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.core.annotation.AliasFor; import org.springframework.core.env.Environment; +import org.springframework.data.repository.config.BootstrapMode; import org.springframework.test.context.BootstrapWith; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.transaction.annotation.Transactional; @@ -52,6 +53,10 @@ * {@link AutoConfigureTestDatabase @AutoConfigureTestDatabase} annotation can be used to * override these settings. *

    + * SQL queries are logged by default by setting the {@code spring.jpa.show-sql} property + * to {@code true}. This can be disabled using the {@link DataJpaTest#showSql() showSql} + * attribute. + *

    * If you are looking to load your full application configuration, but use an embedded * database, you should consider {@link SpringBootTest @SpringBootTest} combined with * {@link AutoConfigureTestDatabase @AutoConfigureTestDatabase} rather than this @@ -62,6 +67,7 @@ * * @author Phillip Webb * @author Artsiom Yudovin + * @author Scott Frederick * @since 1.4.0 * @see AutoConfigureDataJpa * @see AutoConfigureTestDatabase @@ -99,6 +105,14 @@ @PropertyMapping("spring.jpa.show-sql") boolean showSql() default true; + /** + * The {@link BootstrapMode} for the test repository support. Defaults to + * {@link BootstrapMode#DEFAULT}. + * @return the {@link BootstrapMode} to use for testing the repository + */ + @PropertyMapping("spring.data.jpa.repositories.bootstrap-mode") + BootstrapMode bootstrapMode() default BootstrapMode.DEFAULT; + /** * Determines if default filtering should be used with * {@link SpringBootApplication @SpringBootApplication}. By default no beans are diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/orm/jpa/TestEntityManager.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/orm/jpa/TestEntityManager.java index b770ca8fa29f..96201adc8740 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/orm/jpa/TestEntityManager.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/orm/jpa/TestEntityManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -234,7 +234,7 @@ public T getId(Object entity, Class idType) { */ public final EntityManager getEntityManager() { EntityManager manager = EntityManagerFactoryUtils.getTransactionalEntityManager(this.entityManagerFactory); - Assert.state(manager != null, "No transactional EntityManager found"); + Assert.state(manager != null, "No transactional EntityManager found, is your test running in a transactional?"); return manager; } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/properties/AnnotationsPropertySource.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/properties/AnnotationsPropertySource.java index 9e7d4dfee43f..d3432e3f3fc6 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/properties/AnnotationsPropertySource.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/properties/AnnotationsPropertySource.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.test.context.TestContextAnnotationUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -58,6 +59,11 @@ public AnnotationsPropertySource(String name, Class source) { private Map getProperties(Class source) { Map properties = new LinkedHashMap<>(); + getProperties(source, properties); + return properties; + } + + private void getProperties(Class source, Map properties) { MergedAnnotations.from(source, SearchStrategy.SUPERCLASS).stream() .filter(MergedAnnotationPredicates.unique(MergedAnnotation::getType)).forEach((annotation) -> { Class type = annotation.getType(); @@ -70,13 +76,15 @@ private Map getProperties(Class source) { collectProperties(prefix, defaultSkip, annotation, attribute, properties); } }); - return properties; + if (TestContextAnnotationUtils.searchEnclosingClass(source)) { + getProperties(source.getEnclosingClass(), properties); + } } - private void collectProperties(String prefix, SkipPropertyMapping defaultSkip, MergedAnnotation annotation, + private void collectProperties(String prefix, SkipPropertyMapping skip, MergedAnnotation annotation, Method attribute, Map properties) { MergedAnnotation attributeMapping = MergedAnnotations.from(attribute).get(PropertyMapping.class); - SkipPropertyMapping skip = attributeMapping.getValue("skip", SkipPropertyMapping.class).orElse(defaultSkip); + skip = attributeMapping.getValue("skip", SkipPropertyMapping.class).orElse(skip); if (skip == SkipPropertyMapping.YES) { return; } @@ -90,7 +98,7 @@ private void collectProperties(String prefix, SkipPropertyMapping defaultSkip, M } } String name = getName(prefix, attributeMapping, attribute); - putProperties(name, value.get(), properties); + putProperties(name, skip, value.get(), properties); } private String getName(String prefix, MergedAnnotation attributeMapping, Method attribute) { @@ -118,11 +126,18 @@ private String dotAppend(String prefix, String postfix) { return postfix; } - private void putProperties(String name, Object value, Map properties) { + private void putProperties(String name, SkipPropertyMapping defaultSkip, Object value, + Map properties) { if (ObjectUtils.isArray(value)) { Object[] array = ObjectUtils.toObjectArray(value); for (int i = 0; i < array.length; i++) { - properties.put(name + "[" + i + "]", array[i]); + putProperties(name + "[" + i + "]", defaultSkip, array[i], properties); + } + } + else if (value instanceof MergedAnnotation) { + MergedAnnotation annotation = (MergedAnnotation) value; + for (Method attribute : annotation.getType().getDeclaredMethods()) { + collectProperties(name, defaultSkip, (MergedAnnotation) value, attribute, properties); } } else { diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/restdocs/AutoConfigureRestDocs.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/restdocs/AutoConfigureRestDocs.java index 95625f15cbb0..905af8699260 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/restdocs/AutoConfigureRestDocs.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/restdocs/AutoConfigureRestDocs.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,21 +23,34 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import io.restassured.RestAssured; + import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.test.autoconfigure.properties.PropertyMapping; import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AliasFor; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.servlet.MockMvc; /** * Annotation that can be applied to a test class to enable and configure - * auto-configuration of Spring REST Docs. Allows configuration of the output directory - * and the host, scheme, and port of generated URIs. When further configuration is - * required a {@link RestDocsMockMvcConfigurationCustomizer} bean can be used. + * auto-configuration of Spring REST Docs. The auto-configuration sets up + * {@link MockMvc}-based testing of a servlet web application, {@link WebTestClient}-based + * testing of a reactive web application, or {@link RestAssured}-based testing of any web + * application over HTTP. + *

    + * Allows configuration of the output directory and the host, scheme, and port of + * generated URIs. When further configuration is required a + * {@link RestDocsMockMvcConfigurationCustomizer}, + * {@link RestDocsWebTestClientConfigurationCustomizer}, or + * {@link RestDocsRestAssuredConfigurationCustomizer} bean can be used. * * @author Andy Wilkinson * @since 1.4.0 * @see RestDocsAutoConfiguration * @see RestDocsMockMvcConfigurationCustomizer + * @see RestDocsWebTestClientConfigurationCustomizer + * @see RestDocsRestAssuredConfigurationCustomizer */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/restdocs/RestDocsWebTestClientBuilderCustomizer.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/restdocs/RestDocsWebTestClientBuilderCustomizer.java index 5c034717f9cf..b243b71549ec 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/restdocs/RestDocsWebTestClientBuilderCustomizer.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/restdocs/RestDocsWebTestClientBuilderCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ package org.springframework.boot.test.autoconfigure.restdocs; -import org.springframework.boot.test.autoconfigure.web.reactive.WebTestClientBuilderCustomizer; +import org.springframework.boot.test.web.reactive.server.WebTestClientBuilderCustomizer; import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentationConfigurer; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.util.StringUtils; @@ -61,7 +61,7 @@ private boolean isStandardPort(String scheme, Integer port) { if (port == null) { return true; } - return (scheme.equals("http") && port == 80) || (scheme.equals("https") && port == 443); + return ("http".equals(scheme) && port == 80) || ("https".equals(scheme) && port == 443); } } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/client/MockRestServiceServerAutoConfiguration.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/client/MockRestServiceServerAutoConfiguration.java index 2c47ef8f0960..ef95d7ec8cfe 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/client/MockRestServiceServerAutoConfiguration.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/client/MockRestServiceServerAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.io.IOException; import java.lang.reflect.Constructor; +import java.time.Duration; import java.util.Map; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -97,6 +98,11 @@ public void verify() { getDelegate().verify(); } + @Override + public void verify(Duration timeout) { + getDelegate().verify(timeout); + } + @Override public void reset() { Map expectationManagers = this.customizer.getExpectationManagers(); diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/reactive/WebFluxTest.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/reactive/WebFluxTest.java index ce45b2c8dfcb..ece41ac642d7 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/reactive/WebFluxTest.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/reactive/WebFluxTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,6 @@ import org.springframework.boot.test.autoconfigure.filter.TypeExcludeFilters; import org.springframework.boot.test.autoconfigure.json.AutoConfigureJson; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AliasFor; @@ -55,9 +54,10 @@ * {@link WebTestClient}. For more fine-grained control of WebTestClient the * {@link AutoConfigureWebTestClient @AutoConfigureWebTestClient} annotation can be used. *

    - * Typically {@code @WebFluxTest} is used in combination with {@link MockBean @MockBean} - * or {@link Import @Import} to create any collaborators required by your - * {@code @Controller} beans. + * Typically {@code @WebFluxTest} is used in combination with + * {@link org.springframework.boot.test.mock.mockito.MockBean @MockBean} or + * {@link Import @Import} to create any collaborators required by your {@code @Controller} + * beans. *

    * If you are looking to load your full application configuration and use WebTestClient, * you should consider {@link SpringBootTest @SpringBootTest} combined with diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/reactive/WebFluxTypeExcludeFilter.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/reactive/WebFluxTypeExcludeFilter.java index 5ca155d0c333..2a34559041a1 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/reactive/WebFluxTypeExcludeFilter.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/reactive/WebFluxTypeExcludeFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.GenericConverter; import org.springframework.stereotype.Controller; +import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.reactive.config.WebFluxConfigurer; @@ -43,6 +44,9 @@ public final class WebFluxTypeExcludeFilter extends StandardAnnotationCustomizab private static final Class[] NO_CONTROLLERS = {}; + private static final String[] OPTIONAL_INCLUDES = { "com.fasterxml.jackson.databind.Module", + "org.thymeleaf.dialect.IDialect" }; + private static final Set> DEFAULT_INCLUDES; static { @@ -54,6 +58,14 @@ public final class WebFluxTypeExcludeFilter extends StandardAnnotationCustomizab includes.add(GenericConverter.class); includes.add(WebExceptionHandler.class); includes.add(WebFilter.class); + for (String optionalInclude : OPTIONAL_INCLUDES) { + try { + includes.add(ClassUtils.forName(optionalInclude, null)); + } + catch (Exception ex) { + // Ignore + } + } DEFAULT_INCLUDES = Collections.unmodifiableSet(includes); } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/reactive/WebTestClientBuilderCustomizer.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/reactive/WebTestClientBuilderCustomizer.java deleted file mode 100644 index 750161e4d1fa..000000000000 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/reactive/WebTestClientBuilderCustomizer.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.test.autoconfigure.web.reactive; - -import org.springframework.test.web.reactive.server.WebTestClient.Builder; - -/** - * A customizer for a {@link Builder}. Any {@code WebTestClientBuilderCustomizer} beans - * found in the application context will be {@link #customize called} to customize the - * auto-configured {@link Builder}. - * - * @author Andy Wilkinson - * @since 2.0.0 - * @see WebTestClientAutoConfiguration - * @deprecated since 2.2 in favor of - * {@link org.springframework.boot.test.web.reactive.server.WebTestClientBuilderCustomizer} - */ -@FunctionalInterface -@Deprecated -public interface WebTestClientBuilderCustomizer - extends org.springframework.boot.test.web.reactive.server.WebTestClientBuilderCustomizer { - -} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/MockMvcAutoConfiguration.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/MockMvcAutoConfiguration.java index b02d466c25bd..e98291be3b01 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/MockMvcAutoConfiguration.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/MockMvcAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,8 +44,8 @@ * @author Phillip Webb * @author Andy Wilkinson * @author Stephane Nicoll - * @see AutoConfigureWebMvc * @since 1.4.0 + * @see AutoConfigureWebMvc */ @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java index 9d2e30223db1..20d4e0e6d04f 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -109,7 +109,7 @@ private LinesWriter getLinesWriter() { private void addFilters(ConfigurableMockMvcBuilder builder) { FilterRegistrationBeans registrations = new FilterRegistrationBeans(this.context); registrations.stream().map(AbstractFilterRegistrationBean.class::cast) - .filter(AbstractFilterRegistrationBean::isEnabled) + .filter(AbstractFilterRegistrationBean::isEnabled) .forEach((registration) -> addFilter(builder, registration)); } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverContextCustomizerFactory.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverContextCustomizerFactory.java index 2c2daad574cb..c9f836520d61 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverContextCustomizerFactory.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverContextCustomizerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTest.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTest.java index a65b2c56ccf5..57b53e2f2bf8 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTest.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,6 @@ import org.springframework.boot.test.autoconfigure.core.AutoConfigureCache; import org.springframework.boot.test.autoconfigure.filter.TypeExcludeFilters; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.Import; import org.springframework.core.annotation.AliasFor; @@ -56,7 +55,8 @@ * WebDriver). For more fine-grained control of MockMVC the * {@link AutoConfigureMockMvc @AutoConfigureMockMvc} annotation can be used. *

    - * Typically {@code @WebMvcTest} is used in combination with {@link MockBean @MockBean} or + * Typically {@code @WebMvcTest} is used in combination with + * {@link org.springframework.boot.test.mock.mockito.MockBean @MockBean} or * {@link Import @Import} to create any collaborators required by your {@code @Controller} * beans. *

    diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTypeExcludeFilter.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTypeExcludeFilter.java index 17a236862281..f2be9723b73e 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTypeExcludeFilter.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTypeExcludeFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,8 +49,9 @@ public final class WebMvcTypeExcludeFilter extends StandardAnnotationCustomizabl private static final Class[] NO_CONTROLLERS = {}; - private static final String[] OPTIONAL_INCLUDES = { - "org.springframework.security.config.annotation.web.WebSecurityConfigurer" }; + private static final String[] OPTIONAL_INCLUDES = { "com.fasterxml.jackson.databind.Module", + "org.springframework.security.config.annotation.web.WebSecurityConfigurer", + "org.springframework.security.web.SecurityFilterChain", "org.thymeleaf.dialect.IDialect" }; private static final Set> DEFAULT_INCLUDES; diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/webservices/client/AutoConfigureMockWebServiceServer.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/webservices/client/AutoConfigureMockWebServiceServer.java new file mode 100644 index 000000000000..0ab829870cb4 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/webservices/client/AutoConfigureMockWebServiceServer.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.webservices.client; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.autoconfigure.properties.PropertyMapping; +import org.springframework.ws.test.client.MockWebServiceServer; + +/** + * Annotation that can be applied to a test class to enable and configure + * auto-configuration of a single {@link MockWebServiceServer}. + * + * @author Dmytro Nosan + * @since 2.3.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ImportAutoConfiguration +@PropertyMapping("spring.test.webservice.client.mockserver") +public @interface AutoConfigureMockWebServiceServer { + + /** + * If {@link MockWebServiceServer} bean should be registered. Defaults to + * {@code true}. + * @return if mock support is enabled + */ + boolean enabled() default true; + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/webservices/client/AutoConfigureWebServiceClient.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/webservices/client/AutoConfigureWebServiceClient.java new file mode 100644 index 000000000000..11cddd90c8d3 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/webservices/client/AutoConfigureWebServiceClient.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.webservices.client; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.autoconfigure.properties.PropertyMapping; +import org.springframework.boot.webservices.client.WebServiceTemplateBuilder; +import org.springframework.ws.client.core.WebServiceTemplate; + +/** + * Annotation that can be applied to a test class to enable and configure + * auto-configuration of web service clients. + * + * @author Dmytro Nosan + * @since 2.3.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ImportAutoConfiguration +@PropertyMapping("spring.test.webservice.client") +public @interface AutoConfigureWebServiceClient { + + /** + * If a {@link WebServiceTemplate} bean should be registered. Defaults to + * {@code false} with the assumption that the {@link WebServiceTemplateBuilder} will + * be used. + * @return if a {@link WebServiceTemplate} bean should be added. + */ + boolean registerWebServiceTemplate() default false; + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/webservices/client/MockWebServiceServerAutoConfiguration.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/webservices/client/MockWebServiceServerAutoConfiguration.java new file mode 100644 index 000000000000..10a4cd403807 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/webservices/client/MockWebServiceServerAutoConfiguration.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.webservices.client; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.ws.client.core.WebServiceTemplate; +import org.springframework.ws.test.client.MockWebServiceMessageSender; +import org.springframework.ws.test.client.MockWebServiceServer; + +/** + * Auto-configuration for {@link MockWebServiceServer} support. + * + * @author Dmytro Nosan + * @since 2.3.0 + * @see AutoConfigureMockWebServiceServer + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnProperty(prefix = "spring.test.webservice.client.mockserver", name = "enabled") +@ConditionalOnClass({ MockWebServiceServer.class, WebServiceTemplate.class }) +public class MockWebServiceServerAutoConfiguration { + + @Bean + public TestMockWebServiceServer mockWebServiceServer() { + return new TestMockWebServiceServer(new MockWebServiceMessageSender()); + } + + @Bean + public MockWebServiceServerWebServiceTemplateCustomizer mockWebServiceServerWebServiceTemplateCustomizer( + TestMockWebServiceServer mockWebServiceServer) { + return new MockWebServiceServerWebServiceTemplateCustomizer(mockWebServiceServer); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/webservices/client/MockWebServiceServerTestExecutionListener.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/webservices/client/MockWebServiceServerTestExecutionListener.java new file mode 100644 index 000000000000..6e65ea6551d6 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/webservices/client/MockWebServiceServerTestExecutionListener.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.webservices.client; + +import org.springframework.context.ApplicationContext; +import org.springframework.core.Ordered; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.TestExecutionListener; +import org.springframework.test.context.support.AbstractTestExecutionListener; +import org.springframework.util.ClassUtils; +import org.springframework.ws.test.client.MockWebServiceServer; + +/** + * {@link TestExecutionListener} to {@code verify} and {@code reset} + * {@link MockWebServiceServer}. + * + * @author Dmytro Nosan + * @since 2.3.0 + */ +public class MockWebServiceServerTestExecutionListener extends AbstractTestExecutionListener { + + private static final String MOCK_SERVER_CLASS = "org.springframework.ws.test.client.MockWebServiceServer"; + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE - 100; + } + + @Override + public void afterTestMethod(TestContext testContext) { + if (isMockWebServiceServerPresent()) { + ApplicationContext applicationContext = testContext.getApplicationContext(); + String[] names = applicationContext.getBeanNamesForType(MockWebServiceServer.class, false, false); + for (String name : names) { + MockWebServiceServer mockServer = applicationContext.getBean(name, MockWebServiceServer.class); + mockServer.verify(); + mockServer.reset(); + } + } + } + + private boolean isMockWebServiceServerPresent() { + return ClassUtils.isPresent(MOCK_SERVER_CLASS, getClass().getClassLoader()); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/webservices/client/MockWebServiceServerWebServiceTemplateCustomizer.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/webservices/client/MockWebServiceServerWebServiceTemplateCustomizer.java new file mode 100644 index 000000000000..1edb211887d5 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/webservices/client/MockWebServiceServerWebServiceTemplateCustomizer.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.webservices.client; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.springframework.boot.webservices.client.WebServiceTemplateBuilder; +import org.springframework.boot.webservices.client.WebServiceTemplateCustomizer; +import org.springframework.util.Assert; +import org.springframework.ws.client.core.WebServiceTemplate; +import org.springframework.ws.test.client.MockWebServiceServer; + +/** + * {@link WebServiceTemplateCustomizer} that can be applied to a + * {@link WebServiceTemplateBuilder} instances to add {@link MockWebServiceServer} + * support. + * + * @author Dmytro Nosan + */ +class MockWebServiceServerWebServiceTemplateCustomizer implements WebServiceTemplateCustomizer { + + private final AtomicBoolean applied = new AtomicBoolean(); + + private final TestMockWebServiceServer mockServer; + + MockWebServiceServerWebServiceTemplateCustomizer(TestMockWebServiceServer mockServer) { + this.mockServer = mockServer; + } + + @Override + public void customize(WebServiceTemplate webServiceTemplate) { + Assert.state(!this.applied.getAndSet(true), "@WebServiceClientTest supports only a single WebServiceTemplate"); + webServiceTemplate.setMessageSender(this.mockServer.getMockMessageSender()); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/webservices/client/TestMockWebServiceServer.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/webservices/client/TestMockWebServiceServer.java new file mode 100644 index 000000000000..be76a76e4cca --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/webservices/client/TestMockWebServiceServer.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.webservices.client; + +import org.springframework.ws.test.client.MockWebServiceMessageSender; +import org.springframework.ws.test.client.MockWebServiceServer; + +/** + * Test {@link MockWebServiceServer} which provides access to the underlying + * {@link MockWebServiceMessageSender}. + * + * @author Dmytro Nosan + */ +final class TestMockWebServiceServer extends MockWebServiceServer { + + private final MockWebServiceMessageSender mockMessageSender; + + TestMockWebServiceServer(MockWebServiceMessageSender mockMessageSender) { + super(mockMessageSender); + this.mockMessageSender = mockMessageSender; + } + + MockWebServiceMessageSender getMockMessageSender() { + return this.mockMessageSender; + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/webservices/client/WebServiceClientExcludeFilter.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/webservices/client/WebServiceClientExcludeFilter.java new file mode 100644 index 000000000000..445bdd092118 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/webservices/client/WebServiceClientExcludeFilter.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.webservices.client; + +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.boot.context.TypeExcludeFilter; +import org.springframework.boot.test.autoconfigure.filter.StandardAnnotationCustomizableTypeExcludeFilter; + +/** + * {@link TypeExcludeFilter} for {@link WebServiceClientTest @WebServiceClientTest}. + * + * @author Dmytro Nosan + * @since 2.3.0 + */ +public final class WebServiceClientExcludeFilter + extends StandardAnnotationCustomizableTypeExcludeFilter { + + private final Class[] components; + + protected WebServiceClientExcludeFilter(Class testClass) { + super(testClass); + this.components = getAnnotation().getValue("components", Class[].class).orElseGet(() -> new Class[0]); + } + + @Override + protected Set> getComponentIncludes() { + return new LinkedHashSet<>(Arrays.asList(this.components)); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/webservices/client/WebServiceClientTemplateAutoConfiguration.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/webservices/client/WebServiceClientTemplateAutoConfiguration.java new file mode 100644 index 000000000000..8dfd08cc1a45 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/webservices/client/WebServiceClientTemplateAutoConfiguration.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.webservices.client; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.webservices.client.WebServiceTemplateAutoConfiguration; +import org.springframework.boot.webservices.client.WebServiceTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.ws.client.core.WebServiceTemplate; + +/** + * Auto-configuration for a web-client {@link WebServiceTemplate}. Used when + * {@link AutoConfigureWebServiceClient#registerWebServiceTemplate()} is {@code true}. + * + * @author Dmytro Nosan + * @since 2.3.0 + * @see AutoConfigureWebServiceClient + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnProperty(prefix = "spring.test.webservice.client", name = "register-web-service-template") +@AutoConfigureAfter(WebServiceTemplateAutoConfiguration.class) +@ConditionalOnClass(WebServiceTemplate.class) +@ConditionalOnBean(WebServiceTemplateBuilder.class) +public class WebServiceClientTemplateAutoConfiguration { + + @Bean + public WebServiceTemplate webServiceTemplate(WebServiceTemplateBuilder builder) { + return builder.build(); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/webservices/client/WebServiceClientTest.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/webservices/client/WebServiceClientTest.java new file mode 100644 index 000000000000..8b672974014f --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/webservices/client/WebServiceClientTest.java @@ -0,0 +1,129 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.webservices.client; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.autoconfigure.OverrideAutoConfiguration; +import org.springframework.boot.test.autoconfigure.core.AutoConfigureCache; +import org.springframework.boot.test.autoconfigure.filter.TypeExcludeFilters; +import org.springframework.boot.webservices.client.WebServiceTemplateBuilder; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.core.annotation.AliasFor; +import org.springframework.core.env.Environment; +import org.springframework.test.context.BootstrapWith; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.ws.client.core.WebServiceTemplate; +import org.springframework.ws.test.client.MockWebServiceServer; + +/** + * Annotation that can be used for a typical Spring web service client test. Can be used + * when a test focuses only on beans that use + * {@link WebServiceTemplateBuilder}. By default, tests annotated with + * {@code WebServiceClientTest} will also auto-configure a {@link MockWebServiceServer}. + *

    + * If you are testing a bean that doesn't use {@link WebServiceTemplateBuilder} but + * instead injects a {@link WebServiceTemplate} directly, you can add + * {@code @AutoConfigureWebServiceClient(registerWebServiceTemplate=true)}. + *

    + * When using JUnit 4, this annotation should be used in combination with + * {@code @RunWith(SpringRunner.class)}. + * + * @author Dmytro Nosan + * @since 2.3.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@BootstrapWith(WebServiceClientTestContextBootstrapper.class) +@ExtendWith(SpringExtension.class) +@OverrideAutoConfiguration(enabled = false) +@TypeExcludeFilters(WebServiceClientExcludeFilter.class) +@AutoConfigureCache +@AutoConfigureMockWebServiceServer +@AutoConfigureWebServiceClient +@ImportAutoConfiguration +public @interface WebServiceClientTest { + + /** + * Properties in form {@literal key=value} that should be added to the Spring + * {@link Environment} before the test runs. + * @return the properties to add + */ + String[] properties() default {}; + + /** + * Specifies the components to test. This is an alias of {@link #components()} which + * can be used for brevity if no other attributes are defined. See + * {@link #components()} for details. + * @see #components() + * @return the components to test + */ + @AliasFor("components") + Class[] value() default {}; + + /** + * Specifies the components to test. May be left blank if components will be manually + * imported or created directly. + * @see #value() + * @return the components to test + */ + @AliasFor("value") + Class[] components() default {}; + + /** + * Determines if default filtering should be used with + * {@link SpringBootApplication @SpringBootApplication}. By default only + * {@code @JsonComponent} and {@code Module} beans are included. + * @see #includeFilters() + * @see #excludeFilters() + * @return if default filters should be used + */ + boolean useDefaultFilters() default true; + + /** + * A set of include filters which can be used to add otherwise filtered beans to the + * application context. + * @return include filters to apply + */ + ComponentScan.Filter[] includeFilters() default {}; + + /** + * A set of exclude filters which can be used to filter beans that would otherwise be + * added to the application context. + * @return exclude filters to apply + */ + ComponentScan.Filter[] excludeFilters() default {}; + + /** + * Auto-configuration exclusions that should be applied for this test. + * @return auto-configuration exclusions to apply + */ + @AliasFor(annotation = ImportAutoConfiguration.class, attribute = "exclude") + Class[] excludeAutoConfiguration() default {}; + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/webservices/client/WebServiceClientTestContextBootstrapper.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/webservices/client/WebServiceClientTestContextBootstrapper.java new file mode 100644 index 000000000000..d3d4bc013291 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/webservices/client/WebServiceClientTestContextBootstrapper.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.webservices.client; + +import org.springframework.boot.test.context.SpringBootTestContextBootstrapper; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; +import org.springframework.test.context.TestContextBootstrapper; + +/** + * {@link TestContextBootstrapper} for {@link WebServiceClientTest @WebServiceClientTest} + * support. + * + * @author Dmytro Nosan + */ +class WebServiceClientTestContextBootstrapper extends SpringBootTestContextBootstrapper { + + @Override + protected String[] getProperties(Class testClass) { + return MergedAnnotations.from(testClass, SearchStrategy.INHERITED_ANNOTATIONS).get(WebServiceClientTest.class) + .getValue("properties", String[].class).orElse(null); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/webservices/client/package-info.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/webservices/client/package-info.java new file mode 100644 index 000000000000..cca66f26b3bf --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/webservices/client/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for web service clients. + */ +package org.springframework.boot.test.autoconfigure.webservices.client; diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring.factories index d05127f94215..d7d3e73cac42 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring.factories @@ -2,6 +2,14 @@ org.springframework.boot.test.autoconfigure.core.AutoConfigureCache=\ org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration +# AutoConfigureDataCassandra auto-configuration imports +org.springframework.boot.test.autoconfigure.data.cassandra.AutoConfigureDataCassandra=\ +org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\ +org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration,\ +org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveDataAutoConfiguration,\ +org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveRepositoriesAutoConfiguration,\ +org.springframework.boot.autoconfigure.data.cassandra.CassandraRepositoriesAutoConfiguration + # AutoConfigureDataJdbc auto-configuration imports org.springframework.boot.test.autoconfigure.data.jdbc.AutoConfigureDataJdbc=\ org.springframework.boot.autoconfigure.data.jdbc.JdbcRepositoriesAutoConfiguration,\ @@ -10,6 +18,7 @@ org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\ org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration,\ org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration,\ org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,\ +org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration,\ org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration # AutoConfigureDataJpa auto-configuration imports @@ -21,6 +30,7 @@ org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConf org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration,\ org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,\ org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\ +org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration,\ org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration # AutoConfigureDataLdap auto-configuration imports @@ -37,17 +47,33 @@ org.springframework.boot.autoconfigure.data.mongo.MongoReactiveRepositoriesAutoC org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration,\ org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\ org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration,\ -org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration +org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration,\ +org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration # AutoConfigureDataNeo4j auto-configuration imports org.springframework.boot.test.autoconfigure.data.neo4j.AutoConfigureDataNeo4j=\ +org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration,\ org.springframework.boot.autoconfigure.data.neo4j.Neo4jDataAutoConfiguration,\ +org.springframework.boot.autoconfigure.data.neo4j.Neo4jReactiveDataAutoConfiguration,\ +org.springframework.boot.autoconfigure.data.neo4j.Neo4jReactiveRepositoriesAutoConfiguration,\ org.springframework.boot.autoconfigure.data.neo4j.Neo4jRepositoriesAutoConfiguration,\ org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration +# AutoConfigureDataR2dbc auto-configuration imports +org.springframework.boot.test.autoconfigure.data.r2dbc.AutoConfigureDataR2dbc=\ +org.springframework.boot.autoconfigure.data.r2dbc.R2dbcRepositoriesAutoConfiguration,\ +org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration,\ +org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\ +org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,\ +org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration,\ +org.springframework.boot.autoconfigure.r2dbc.R2dbcTransactionManagerAutoConfiguration,\ +org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration,\ +org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration + # AutoConfigureDataRedis auto-configuration imports org.springframework.boot.test.autoconfigure.data.redis.AutoConfigureDataRedis=\ org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\ +org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\ org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration # AutoConfigureJdbc auto-configuration imports @@ -57,6 +83,7 @@ org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\ org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration,\ org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration,\ org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,\ +org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration,\ org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration # AutoConfigureTestDatabase auto-configuration imports @@ -71,6 +98,7 @@ org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\ org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration,\ org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration,\ org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,\ +org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration,\ org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration # AutoConfigureJson auto-configuration imports @@ -88,6 +116,8 @@ org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration # AutoConfigureWebClient auto-configuration imports org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient=\ +org.springframework.boot.autoconfigure.security.oauth2.client.reactive.ReactiveOAuth2ClientAutoConfiguration,\ +org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration,\ org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration,\ org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration,\ org.springframework.boot.test.autoconfigure.web.reactive.WebTestClientAutoConfiguration @@ -108,7 +138,10 @@ org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc=\ org.springframework.boot.test.autoconfigure.web.servlet.MockMvcAutoConfiguration,\ org.springframework.boot.test.autoconfigure.web.servlet.MockMvcWebClientAutoConfiguration,\ org.springframework.boot.test.autoconfigure.web.servlet.MockMvcWebDriverAutoConfiguration,\ +org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration,\ +org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration,\ org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,\ +org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration,\ org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration,\ org.springframework.boot.test.autoconfigure.web.servlet.MockMvcSecurityConfiguration @@ -151,8 +184,18 @@ org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration,\ org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration,\ org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration,\ org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration,\ +org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration,\ org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration +# AutoConfigureWebServiceClient +org.springframework.boot.test.autoconfigure.webservices.client.AutoConfigureWebServiceClient=\ +org.springframework.boot.test.autoconfigure.webservices.client.WebServiceClientTemplateAutoConfiguration,\ +org.springframework.boot.autoconfigure.webservices.client.WebServiceTemplateAutoConfiguration + +# AutoConfigureMockWebServiceServer +org.springframework.boot.test.autoconfigure.webservices.client.AutoConfigureMockWebServiceServer=\ +org.springframework.boot.test.autoconfigure.webservices.client.MockWebServiceServerAutoConfiguration + # DefaultTestExecutionListenersPostProcessors org.springframework.boot.test.context.DefaultTestExecutionListenersPostProcessor=\ org.springframework.boot.test.autoconfigure.SpringBootDependencyInjectionTestExecutionListener$PostProcessor @@ -160,6 +203,7 @@ org.springframework.boot.test.autoconfigure.SpringBootDependencyInjectionTestExe # Spring Test ContextCustomizerFactories org.springframework.test.context.ContextCustomizerFactory=\ org.springframework.boot.test.autoconfigure.OverrideAutoConfigurationContextCustomizerFactory,\ +org.springframework.boot.test.autoconfigure.actuate.metrics.MetricsExportContextCustomizerFactory,\ org.springframework.boot.test.autoconfigure.filter.TypeExcludeFiltersContextCustomizerFactory,\ org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizerFactory,\ org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory @@ -169,4 +213,5 @@ org.springframework.test.context.TestExecutionListener=\ org.springframework.boot.test.autoconfigure.restdocs.RestDocsTestExecutionListener,\ org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerResetTestExecutionListener,\ org.springframework.boot.test.autoconfigure.web.servlet.MockMvcPrintOnlyOnFailureTestExecutionListener,\ -org.springframework.boot.test.autoconfigure.web.servlet.WebDriverTestExecutionListener +org.springframework.boot.test.autoconfigure.web.servlet.WebDriverTestExecutionListener,\ +org.springframework.boot.test.autoconfigure.webservices.client.MockWebServiceServerTestExecutionListener diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/metrics/AutoConfigureMetricsMissingIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/metrics/AutoConfigureMetricsMissingIntegrationTests.java new file mode 100644 index 000000000000..3ac20e6bc1f4 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/metrics/AutoConfigureMetricsMissingIntegrationTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.actuate.metrics; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.prometheus.PrometheusMeterRegistry; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; +import org.springframework.core.env.Environment; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration test to verify behaviour when + * {@link AutoConfigureMetrics @AutoConfigureMetrics} is not present on the test class. + * + * @author Chris Bono + */ +@SpringBootTest +class AutoConfigureMetricsMissingIntegrationTests { + + @Test + void customizerRunsAndOnlyEnablesSimpleMeterRegistryWhenNoAnnotationPresent( + @Autowired ApplicationContext applicationContext) { + assertThat(applicationContext.getBean(MeterRegistry.class)).isInstanceOf(SimpleMeterRegistry.class); + assertThat(applicationContext.getBeansOfType(PrometheusMeterRegistry.class)).isEmpty(); + } + + @Test + void customizerRunsAndSetsExclusionPropertiesWhenNoAnnotationPresent(@Autowired Environment environment) { + assertThat(environment.getProperty("management.metrics.export.defaults.enabled")).isEqualTo("false"); + assertThat(environment.getProperty("management.metrics.export.simple.enabled")).isEqualTo("true"); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/metrics/AutoConfigureMetricsPresentIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/metrics/AutoConfigureMetricsPresentIntegrationTests.java new file mode 100644 index 000000000000..5767a073506e --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/metrics/AutoConfigureMetricsPresentIntegrationTests.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.actuate.metrics; + +import io.micrometer.prometheus.PrometheusMeterRegistry; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; +import org.springframework.core.env.Environment; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration test to verify behaviour when + * {@link AutoConfigureMetrics @AutoConfigureMetrics} is present on the test class. + * + * @author Chris Bono + */ +@SpringBootTest +@AutoConfigureMetrics +class AutoConfigureMetricsPresentIntegrationTests { + + @Test + void customizerDoesNotDisableAvailableMeterRegistriesWhenAnnotationPresent( + @Autowired ApplicationContext applicationContext) { + assertThat(applicationContext.getBeansOfType(PrometheusMeterRegistry.class)).hasSize(1); + } + + @Test + void customizerDoesNotSetExclusionPropertiesWhenAnnotationPresent(@Autowired Environment environment) { + assertThat(environment.containsProperty("management.metrics.export.enabled")).isFalse(); + assertThat(environment.containsProperty("management.metrics.export.simple.enabled")).isFalse(); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/metrics/AutoConfigureMetricsSpringBootApplication.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/metrics/AutoConfigureMetricsSpringBootApplication.java new file mode 100644 index 000000000000..c17d00d0873d --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/metrics/AutoConfigureMetricsSpringBootApplication.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.actuate.metrics; + +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; + +/** + * Example {@link SpringBootApplication @SpringBootApplication} for use with + * {@link AutoConfigureMetrics @AutoConfigureMetrics} tests. + * + * @author Chris Bono + */ +@SpringBootConfiguration +@EnableAutoConfiguration(exclude = CassandraAutoConfiguration.class) +class AutoConfigureMetricsSpringBootApplication { + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/metrics/MetricsExportContextCustomizerFactoryTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/metrics/MetricsExportContextCustomizerFactoryTests.java new file mode 100644 index 000000000000..000753cce086 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/metrics/MetricsExportContextCustomizerFactoryTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.actuate.metrics; + +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.test.context.ContextCustomizer; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AutoConfigureMetrics} and + * {@link MetricsExportContextCustomizerFactory} working together. + * + * @author Chris Bono + */ +class MetricsExportContextCustomizerFactoryTests { + + private final MetricsExportContextCustomizerFactory factory = new MetricsExportContextCustomizerFactory(); + + @Test + void getContextCustomizerWhenHasNoAnnotationShouldReturnCustomizer() { + ContextCustomizer customizer = this.factory.createContextCustomizer(NoAnnotation.class, + Collections.emptyList()); + assertThat(customizer).isNotNull(); + ConfigurableApplicationContext context = new GenericApplicationContext(); + customizer.customizeContext(context, null); + assertThat(context.getEnvironment().getProperty("management.metrics.export.defaults.enabled")) + .isEqualTo("false"); + assertThat(context.getEnvironment().getProperty("management.metrics.export.simple.enabled")).isEqualTo("true"); + } + + @Test + void getContextCustomizerWhenHasAnnotationShouldReturnNull() { + ContextCustomizer customizer = this.factory.createContextCustomizer(WithAnnotation.class, null); + assertThat(customizer).isNull(); + } + + @Test + void hashCodeAndEquals() { + ContextCustomizer customizer1 = this.factory.createContextCustomizer(NoAnnotation.class, null); + ContextCustomizer customizer2 = this.factory.createContextCustomizer(OtherWithNoAnnotation.class, null); + assertThat(customizer1.hashCode()).isEqualTo(customizer2.hashCode()); + assertThat(customizer1).isEqualTo(customizer1).isEqualTo(customizer2); + } + + static class NoAnnotation { + + } + + static class OtherWithNoAnnotation { + + } + + @AutoConfigureMetrics + static class WithAnnotation { + + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/cache/ImportsContextCustomizerFactoryWithAutoConfigurationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/cache/ImportsContextCustomizerFactoryWithAutoConfigurationTests.java index 5a07c6562656..7536126a7564 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/cache/ImportsContextCustomizerFactoryWithAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/cache/ImportsContextCustomizerFactoryWithAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,10 @@ import org.junit.jupiter.api.Test; import org.junit.platform.engine.discovery.DiscoverySelectors; +import org.junit.platform.launcher.Launcher; +import org.junit.platform.launcher.LauncherDiscoveryRequest; +import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; +import org.junit.platform.launcher.core.LauncherFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.AutoConfigurationPackage; @@ -28,9 +32,6 @@ import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.autoconfigure.orm.jpa.ExampleEntity; -import org.springframework.boot.testsupport.junit.platform.Launcher; -import org.springframework.boot.testsupport.junit.platform.LauncherDiscoveryRequest; -import org.springframework.boot.testsupport.junit.platform.LauncherDiscoveryRequestBuilder; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.test.context.ContextConfiguration; @@ -49,7 +50,7 @@ class ImportsContextCustomizerFactoryWithAutoConfigurationTests { static ApplicationContext contextFromTest; @Test - void testClassesThatHaveSameAnnotationsShareAContext() throws Throwable { + void testClassesThatHaveSameAnnotationsShareAContext() { executeTests(DataJpaTest1.class); ApplicationContext test1Context = contextFromTest; executeTests(DataJpaTest3.class); @@ -58,7 +59,7 @@ void testClassesThatHaveSameAnnotationsShareAContext() throws Throwable { } @Test - void testClassesThatOnlyHaveDifferingUnrelatedAnnotationsShareAContext() throws Throwable { + void testClassesThatOnlyHaveDifferingUnrelatedAnnotationsShareAContext() { executeTests(DataJpaTest1.class); ApplicationContext test1Context = contextFromTest; executeTests(DataJpaTest2.class); @@ -67,7 +68,7 @@ void testClassesThatOnlyHaveDifferingUnrelatedAnnotationsShareAContext() throws } @Test - void testClassesThatOnlyHaveDifferingPropertyMappedAnnotationAttributesDoNotShareAContext() throws Throwable { + void testClassesThatOnlyHaveDifferingPropertyMappedAnnotationAttributesDoNotShareAContext() { executeTests(DataJpaTest1.class); ApplicationContext test1Context = contextFromTest; executeTests(DataJpaTest4.class); @@ -75,11 +76,10 @@ void testClassesThatOnlyHaveDifferingPropertyMappedAnnotationAttributesDoNotShar assertThat(test1Context).isNotSameAs(test2Context); } - private void executeTests(Class testClass) throws Throwable { - ClassLoader classLoader = testClass.getClassLoader(); - LauncherDiscoveryRequest request = new LauncherDiscoveryRequestBuilder(classLoader) + private void executeTests(Class testClass) { + LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request() .selectors(DiscoverySelectors.selectClass(testClass)).build(); - Launcher launcher = new Launcher(testClass.getClassLoader()); + Launcher launcher = LauncherFactory.create(); launcher.execute(request); } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/cassandra/DataCassandraTestIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/cassandra/DataCassandraTestIntegrationTests.java new file mode 100644 index 000000000000..0b568c96bb31 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/cassandra/DataCassandraTestIntegrationTests.java @@ -0,0 +1,105 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.data.cassandra; + +import java.util.UUID; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.CqlSessionBuilder; +import org.junit.jupiter.api.Test; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.data.redis.ExampleService; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.testsupport.testcontainers.CassandraContainer; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.data.cassandra.core.CassandraTemplate; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Integration test for {@link DataCassandraTest @DataCassandraTest}. + * + * @author Artsiom Yudovin + */ +@DataCassandraTest(properties = { "spring.data.cassandra.local-datacenter=datacenter1", + "spring.data.cassandra.schema-action=create-if-not-exists", + "spring.data.cassandra.connection.connect-timeout=60s", + "spring.data.cassandra.connection.init-query-timeout=60s", "spring.data.cassandra.request.timeout=60s" }) +@Testcontainers(disabledWithoutDocker = true) +class DataCassandraTestIntegrationTests { + + @Container + static final CassandraContainer cassandra = new CassandraContainer(); + + @DynamicPropertySource + static void cassandraProperties(DynamicPropertyRegistry registry) { + registry.add("spring.data.cassandra.contact-points", + () -> cassandra.getHost() + ":" + cassandra.getFirstMappedPort()); + } + + @Autowired + private CassandraTemplate cassandraTemplate; + + @Autowired + private ExampleRepository exampleRepository; + + @Autowired + private ApplicationContext applicationContext; + + @Test + void didNotInjectExampleService() { + assertThatExceptionOfType(NoSuchBeanDefinitionException.class) + .isThrownBy(() -> this.applicationContext.getBean(ExampleService.class)); + } + + @Test + void testRepository() { + ExampleEntity entity = new ExampleEntity(); + entity.setDescription("Look, new @DataCassandraTest!"); + String id = UUID.randomUUID().toString(); + entity.setId(id); + ExampleEntity savedEntity = this.exampleRepository.save(entity); + ExampleEntity getEntity = this.cassandraTemplate.selectOneById(id, ExampleEntity.class); + assertThat(getEntity).isNotNull(); + assertThat(getEntity.getId()).isNotNull(); + assertThat(getEntity.getId()).isEqualTo(savedEntity.getId()); + this.exampleRepository.deleteAll(); + } + + @TestConfiguration(proxyBeanMethods = false) + static class KeyspaceTestConfiguration { + + @Bean + CqlSession cqlSession(CqlSessionBuilder cqlSessionBuilder) { + try (CqlSession session = cqlSessionBuilder.build()) { + session.execute("CREATE KEYSPACE IF NOT EXISTS boot_test" + + " WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };"); + } + return cqlSessionBuilder.withKeyspace("boot_test").build(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/cassandra/DataCassandraTestPropertiesIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/cassandra/DataCassandraTestPropertiesIntegrationTests.java new file mode 100644 index 000000000000..3083322d6bcd --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/cassandra/DataCassandraTestPropertiesIntegrationTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.data.cassandra; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.core.env.Environment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for the {@link DataCassandraTest#properties properties} attribute of + * {@link DataCassandraTest @DataCassandraTest}. + * + * @author Artsiom Yudovin + */ +@DataCassandraTest(properties = "spring.profiles.active=test") +class DataCassandraTestPropertiesIntegrationTests { + + @Autowired + private Environment environment; + + @Test + void environmentWithNewProfile() { + assertThat(this.environment.getActiveProfiles()).containsExactly("test"); + } + + @TestConfiguration(proxyBeanMethods = false) + static class CassandraMockConfiguration { + + @Bean + CqlSession cqlSession() { + DriverContext context = mock(DriverContext.class); + CodecRegistry codecRegistry = mock(CodecRegistry.class); + given(context.getCodecRegistry()).willReturn(codecRegistry); + CqlSession cqlSession = mock(CqlSession.class); + given(cqlSession.getContext()).willReturn(context); + return cqlSession; + } + + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/cassandra/DataCassandraTestWithIncludeFilterIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/cassandra/DataCassandraTestWithIncludeFilterIntegrationTests.java new file mode 100644 index 000000000000..14efc3a82eb8 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/cassandra/DataCassandraTestWithIncludeFilterIntegrationTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.data.cassandra; + +import java.util.UUID; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.CqlSessionBuilder; +import org.junit.jupiter.api.Test; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.testsupport.testcontainers.CassandraContainer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.stereotype.Service; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration test with custom include filter for + * {@link DataCassandraTest @DataCassandraTest}. + * + * @author Artsiom Yudovin + */ +@DataCassandraTest(includeFilters = @Filter(Service.class), + properties = { "spring.data.cassandra.local-datacenter=datacenter1", + "spring.data.cassandra.schema-action=create-if-not-exists", + "spring.data.cassandra.connection.connect-timeout=60s", + "spring.data.cassandra.connection.init-query-timeout=60s", + "spring.data.cassandra.request.timeout=60s" }) +@Testcontainers(disabledWithoutDocker = true) +class DataCassandraTestWithIncludeFilterIntegrationTests { + + @Container + static final CassandraContainer cassandra = new CassandraContainer(); + + @DynamicPropertySource + static void cassandraProperties(DynamicPropertyRegistry registry) { + registry.add("spring.data.cassandra.contact-points", + () -> cassandra.getHost() + ":" + cassandra.getFirstMappedPort()); + } + + @Autowired + private ExampleRepository exampleRepository; + + @Autowired + private ExampleService service; + + @Test + void testService() { + ExampleEntity exampleEntity = new ExampleEntity(); + exampleEntity.setDescription("Look, new @DataCassandraTest!"); + String id = UUID.randomUUID().toString(); + exampleEntity.setId(id); + this.exampleRepository.save(exampleEntity); + assertThat(this.service.hasRecord(exampleEntity)).isTrue(); + } + + @TestConfiguration(proxyBeanMethods = false) + static class KeyspaceTestConfiguration { + + @Bean + CqlSession cqlSession(CqlSessionBuilder cqlSessionBuilder) { + try (CqlSession session = cqlSessionBuilder.build()) { + session.execute("CREATE KEYSPACE IF NOT EXISTS boot_test" + + " WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };"); + } + return cqlSessionBuilder.withKeyspace("boot_test").build(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/cassandra/ExampleCassandraApplication.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/cassandra/ExampleCassandraApplication.java new file mode 100644 index 000000000000..a8459c979d6b --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/cassandra/ExampleCassandraApplication.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.data.cassandra; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Example {@link SpringBootApplication @SpringBootApplication} used with + * {@link DataCassandraTest @DataCassandraTest} tests. + * + * @author Artsiom Yudovin + */ +@SpringBootApplication +public class ExampleCassandraApplication { + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/cassandra/ExampleEntity.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/cassandra/ExampleEntity.java new file mode 100644 index 000000000000..17ea3354f091 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/cassandra/ExampleEntity.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.data.cassandra; + +import org.springframework.data.cassandra.core.mapping.PrimaryKey; +import org.springframework.data.cassandra.core.mapping.Table; + +/** + * Example graph used with {@link DataCassandraTest @DataCassandraTest} tests. + * + * @author Artsiom Yudovin + */ +@Table +public class ExampleEntity { + + @PrimaryKey + private String id; + + private String description; + + public String getId() { + return this.id; + } + + public void setId(String id) { + this.id = id; + } + + public String getDescription() { + return this.description; + } + + public void setDescription(String description) { + this.description = description; + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/cassandra/ExampleRepository.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/cassandra/ExampleRepository.java new file mode 100644 index 000000000000..261736170ac3 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/cassandra/ExampleRepository.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.data.cassandra; + +import org.springframework.data.cassandra.repository.CassandraRepository; + +/** + * Example repository used with {@link DataCassandraTest @DataCassandraTest} tests. + * + * @author Artsiom Yudovin + */ +interface ExampleRepository extends CassandraRepository { + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/cassandra/ExampleService.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/cassandra/ExampleService.java new file mode 100644 index 000000000000..1a5a49cde03a --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/cassandra/ExampleService.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.data.cassandra; + +import org.springframework.data.cassandra.core.CassandraTemplate; +import org.springframework.stereotype.Service; + +/** + * Example service used with {@link DataCassandraTest @DataCassandraTest} tests. + * + * @author Artsiom Yudovin + */ +@Service +public class ExampleService { + + private final CassandraTemplate cassandraTemplate; + + public ExampleService(CassandraTemplate cassandraTemplate) { + this.cassandraTemplate = cassandraTemplate; + } + + public boolean hasRecord(ExampleEntity entity) { + return this.cassandraTemplate.exists(entity.getId(), ExampleEntity.class); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/jdbc/DataJdbcTestIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/jdbc/DataJdbcTestIntegrationTests.java index 91e9c7d21a5b..81b8f7d03078 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/jdbc/DataJdbcTestIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/jdbc/DataJdbcTestIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,7 +39,7 @@ */ @DataJdbcTest @TestPropertySource( - properties = "spring.datasource.schema=classpath:org/springframework/boot/test/autoconfigure/data/jdbc/schema.sql") + properties = "spring.sql.init.schemaLocations=classpath:org/springframework/boot/test/autoconfigure/data/jdbc/schema.sql") class DataJdbcTestIntegrationTests { @Autowired diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/mongo/DataMongoTestIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/mongo/DataMongoTestIntegrationTests.java index dbf2152c7ea1..3a3e5cdd6ad7 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/mongo/DataMongoTestIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/mongo/DataMongoTestIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,20 @@ package org.springframework.boot.test.autoconfigure.data.mongo; +import java.time.Duration; + import org.junit.jupiter.api.Test; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; import org.springframework.context.ApplicationContext; import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -32,8 +40,13 @@ * @author Michael Simons */ @DataMongoTest +@Testcontainers(disabledWithoutDocker = true) class DataMongoTestIntegrationTests { + @Container + static final MongoDBContainer mongoDB = new MongoDBContainer(DockerImageNames.mongo()).withStartupAttempts(5) + .withStartupTimeout(Duration.ofMinutes(5)); + @Autowired private MongoTemplate mongoTemplate; @@ -58,4 +71,9 @@ void didNotInjectExampleService() { .isThrownBy(() -> this.applicationContext.getBean(ExampleService.class)); } + @DynamicPropertySource + static void mongoProperties(DynamicPropertyRegistry registry) { + registry.add("spring.data.mongodb.uri", mongoDB::getReplicaSetUrl); + } + } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/mongo/DataMongoTestReactiveIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/mongo/DataMongoTestReactiveIntegrationTests.java index 3b941f5c8bc9..65003b84707e 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/mongo/DataMongoTestReactiveIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/mongo/DataMongoTestReactiveIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,9 +19,15 @@ import java.time.Duration; import org.junit.jupiter.api.Test; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; import org.springframework.data.mongodb.core.ReactiveMongoTemplate; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; import static org.assertj.core.api.Assertions.assertThat; @@ -31,14 +37,24 @@ * @author Stephane Nicoll */ @DataMongoTest +@Testcontainers(disabledWithoutDocker = true) class DataMongoTestReactiveIntegrationTests { + @Container + static final MongoDBContainer mongoDB = new MongoDBContainer(DockerImageNames.mongo()).withStartupAttempts(5) + .withStartupTimeout(Duration.ofMinutes(5)); + @Autowired private ReactiveMongoTemplate mongoTemplate; @Autowired private ExampleReactiveRepository exampleRepository; + @DynamicPropertySource + static void mongoProperties(DynamicPropertyRegistry registry) { + registry.add("spring.data.mongodb.uri", mongoDB::getReplicaSetUrl); + } + @Test void testRepository() { ExampleDocument exampleDocument = new ExampleDocument(); diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/mongo/DataMongoTestWithIncludeFilterIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/mongo/DataMongoTestWithIncludeFilterIntegrationTests.java index 15d9d7e5b825..21e28dc89d1c 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/mongo/DataMongoTestWithIncludeFilterIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/mongo/DataMongoTestWithIncludeFilterIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,19 @@ package org.springframework.boot.test.autoconfigure.data.mongo; +import java.time.Duration; + import org.junit.jupiter.api.Test; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.stereotype.Service; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; import static org.assertj.core.api.Assertions.assertThat; @@ -30,11 +38,21 @@ * @author Michael Simons */ @DataMongoTest(includeFilters = @Filter(Service.class)) +@Testcontainers(disabledWithoutDocker = true) class DataMongoTestWithIncludeFilterIntegrationTests { + @Container + static final MongoDBContainer mongoDB = new MongoDBContainer(DockerImageNames.mongo()).withStartupAttempts(5) + .withStartupTimeout(Duration.ofMinutes(5)); + @Autowired private ExampleService service; + @DynamicPropertySource + static void mongoProperties(DynamicPropertyRegistry registry) { + registry.add("spring.data.mongodb.uri", mongoDB::getReplicaSetUrl); + } + @Test void testService() { assertThat(this.service.hasCollection("foobar")).isFalse(); diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/mongo/TransactionalDataMongoTestIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/mongo/TransactionalDataMongoTestIntegrationTests.java new file mode 100644 index 000000000000..a882d12fbfb6 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/mongo/TransactionalDataMongoTestIntegrationTests.java @@ -0,0 +1,105 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.data.mongo; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; +import org.springframework.context.annotation.Bean; +import org.springframework.data.mongodb.MongoDatabaseFactory; +import org.springframework.data.mongodb.MongoTransactionManager; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.transaction.annotation.Transactional; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for using {@link DataMongoTest @DataMongoTest} with transactions. + * + * @author Andy Wilkinson + */ +@DataMongoTest +@Transactional +@Testcontainers(disabledWithoutDocker = true) +class TransactionalDataMongoTestIntegrationTests { + + @Container + static final MongoDBContainer mongoDB = new MongoDBContainer(DockerImageNames.mongo()).withStartupAttempts(5) + .withStartupTimeout(Duration.ofMinutes(5)); + + @Autowired + private ExampleRepository exampleRepository; + + @Test + void testRepository() { + ExampleDocument exampleDocument = new ExampleDocument(); + exampleDocument.setText("Look, new @DataMongoTest!"); + exampleDocument = this.exampleRepository.save(exampleDocument); + assertThat(exampleDocument.getId()).isNotNull(); + } + + @DynamicPropertySource + static void mongoProperties(DynamicPropertyRegistry registry) { + registry.add("spring.data.mongodb.uri", mongoDB::getReplicaSetUrl); + } + + @TestConfiguration(proxyBeanMethods = false) + static class TransactionManagerConfiguration { + + @Bean + MongoTransactionManager mongoTransactionManager(MongoDatabaseFactory dbFactory) { + return new MongoTransactionManager(dbFactory); + } + + } + + @TestConfiguration(proxyBeanMethods = false) + static class MongoInitializationConfiguration { + + @Bean + MongoInitializer mongoInitializer(MongoTemplate template) { + return new MongoInitializer(template); + } + + static class MongoInitializer implements InitializingBean { + + private final MongoTemplate template; + + MongoInitializer(MongoTemplate template) { + this.template = template; + } + + @Override + public void afterPropertiesSet() throws Exception { + this.template.createCollection("exampleDocuments"); + } + + } + + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/neo4j/DataNeo4jTestIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/neo4j/DataNeo4jTestIntegrationTests.java index eba313f1ab75..fb783b4f84ea 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/neo4j/DataNeo4jTestIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/neo4j/DataNeo4jTestIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,19 +16,20 @@ package org.springframework.boot.test.autoconfigure.data.neo4j; +import java.time.Duration; + import org.junit.jupiter.api.Test; -import org.neo4j.ogm.session.Session; import org.testcontainers.containers.Neo4jContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextInitializer; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.test.context.ContextConfiguration; +import org.springframework.data.neo4j.core.Neo4jTemplate; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -40,16 +41,21 @@ * @author Stephane Nicoll * @author Michael Simons */ -@ContextConfiguration(initializers = DataNeo4jTestIntegrationTests.Initializer.class) @DataNeo4jTest @Testcontainers(disabledWithoutDocker = true) class DataNeo4jTestIntegrationTests { @Container - static final Neo4jContainer neo4j = new Neo4jContainer<>().withoutAuthentication(); + static final Neo4jContainer neo4j = new Neo4jContainer<>(DockerImageNames.neo4j()).withoutAuthentication() + .withStartupAttempts(5).withStartupTimeout(Duration.ofMinutes(10)); + + @DynamicPropertySource + static void neo4jProperties(DynamicPropertyRegistry registry) { + registry.add("spring.neo4j.uri", neo4j::getBoltUrl); + } @Autowired - private Session session; + private Neo4jTemplate neo4jTemplate; @Autowired private ExampleRepository exampleRepository; @@ -59,12 +65,11 @@ class DataNeo4jTestIntegrationTests { @Test void testRepository() { - ExampleGraph exampleGraph = new ExampleGraph(); - exampleGraph.setDescription("Look, new @DataNeo4jTest!"); + ExampleGraph exampleGraph = new ExampleGraph("Look, new @DataNeo4jTest!"); assertThat(exampleGraph.getId()).isNull(); ExampleGraph savedGraph = this.exampleRepository.save(exampleGraph); assertThat(savedGraph.getId()).isNotNull(); - assertThat(this.session.countEntitiesOfType(ExampleGraph.class)).isEqualTo(1); + assertThat(this.neo4jTemplate.count(ExampleGraph.class)).isEqualTo(1); } @Test @@ -73,14 +78,4 @@ void didNotInjectExampleService() { .isThrownBy(() -> this.applicationContext.getBean(ExampleService.class)); } - static class Initializer implements ApplicationContextInitializer { - - @Override - public void initialize(ConfigurableApplicationContext configurableApplicationContext) { - TestPropertyValues.of("spring.data.neo4j.uri=" + neo4j.getBoltUrl()) - .applyTo(configurableApplicationContext.getEnvironment()); - } - - } - } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/neo4j/DataNeo4jTestPropertiesIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/neo4j/DataNeo4jTestPropertiesIntegrationTests.java index 642c536f1364..e334c40563d5 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/neo4j/DataNeo4jTestPropertiesIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/neo4j/DataNeo4jTestPropertiesIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,17 +16,18 @@ package org.springframework.boot.test.autoconfigure.data.neo4j; +import java.time.Duration; + import org.junit.jupiter.api.Test; import org.testcontainers.containers.Neo4jContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.util.TestPropertyValues; -import org.springframework.context.ApplicationContextInitializer; -import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; import org.springframework.core.env.Environment; -import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; import static org.assertj.core.api.Assertions.assertThat; @@ -37,12 +38,17 @@ * @author Artsiom Yudovin */ @Testcontainers(disabledWithoutDocker = true) -@ContextConfiguration(initializers = DataNeo4jTestPropertiesIntegrationTests.Initializer.class) @DataNeo4jTest(properties = "spring.profiles.active=test") class DataNeo4jTestPropertiesIntegrationTests { @Container - static final Neo4jContainer neo4j = new Neo4jContainer<>().withoutAuthentication(); + static final Neo4jContainer neo4j = new Neo4jContainer<>(DockerImageNames.neo4j()).withoutAuthentication() + .withStartupAttempts(5).withStartupTimeout(Duration.ofMinutes(10)); + + @DynamicPropertySource + static void neo4jProperties(DynamicPropertyRegistry registry) { + registry.add("spring.neo4j.uri", neo4j::getBoltUrl); + } @Autowired private Environment environment; @@ -52,14 +58,4 @@ void environmentWithNewProfile() { assertThat(this.environment.getActiveProfiles()).containsExactly("test"); } - static class Initializer implements ApplicationContextInitializer { - - @Override - public void initialize(ConfigurableApplicationContext configurableApplicationContext) { - TestPropertyValues.of("spring.data.neo4j.uri=" + neo4j.getBoltUrl()) - .applyTo(configurableApplicationContext.getEnvironment()); - } - - } - } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/neo4j/DataNeo4jTestReactiveIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/neo4j/DataNeo4jTestReactiveIntegrationTests.java new file mode 100644 index 000000000000..2cc6479e9375 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/neo4j/DataNeo4jTestReactiveIntegrationTests.java @@ -0,0 +1,99 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.data.neo4j; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; +import org.neo4j.driver.Driver; +import org.testcontainers.containers.Neo4jContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; +import org.springframework.data.neo4j.core.ReactiveNeo4jTemplate; +import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Integration tests for {@link DataNeo4jTest @DataNeo4jTest} with reactive style. + * + * @author Michael J. Simons + * @author Scott Frederick + * @since 2.4.0 + */ +@DataNeo4jTest +@Transactional(propagation = Propagation.NOT_SUPPORTED) +@Testcontainers(disabledWithoutDocker = true) +class DataNeo4jTestReactiveIntegrationTests { + + @Container + static final Neo4jContainer neo4j = new Neo4jContainer<>(DockerImageNames.neo4j()).withoutAuthentication() + .withStartupAttempts(5).withStartupTimeout(Duration.ofMinutes(10)); + + @DynamicPropertySource + static void neo4jProperties(DynamicPropertyRegistry registry) { + registry.add("spring.neo4j.uri", neo4j::getBoltUrl); + } + + @Autowired + private ReactiveNeo4jTemplate neo4jTemplate; + + @Autowired + private ExampleReactiveRepository exampleRepository; + + @Autowired + private ApplicationContext applicationContext; + + @Test + void testRepository() { + Mono.just(new ExampleGraph("Look, new @DataNeo4jTest with reactive!")).flatMap(this.exampleRepository::save) + .as(StepVerifier::create).expectNextCount(1).verifyComplete(); + StepVerifier.create(this.neo4jTemplate.count(ExampleGraph.class)).expectNext(1L).verifyComplete(); + } + + @Test + void didNotInjectExampleService() { + assertThatExceptionOfType(NoSuchBeanDefinitionException.class) + .isThrownBy(() -> this.applicationContext.getBean(ExampleService.class)); + } + + @TestConfiguration(proxyBeanMethods = false) + static class ReactiveTransactionManagerConfiguration { + + @Bean + ReactiveNeo4jTransactionManager reactiveTransactionManager(Driver driver, + ReactiveDatabaseSelectionProvider databaseNameProvider) { + return new ReactiveNeo4jTransactionManager(driver, databaseNameProvider); + } + + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/neo4j/DataNeo4jTestWithIncludeFilterIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/neo4j/DataNeo4jTestWithIncludeFilterIntegrationTests.java index e82fba702ccd..69be31fe2f37 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/neo4j/DataNeo4jTestWithIncludeFilterIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/neo4j/DataNeo4jTestWithIncludeFilterIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,18 +16,19 @@ package org.springframework.boot.test.autoconfigure.data.neo4j; +import java.time.Duration; + import org.junit.jupiter.api.Test; import org.testcontainers.containers.Neo4jContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.util.TestPropertyValues; -import org.springframework.context.ApplicationContextInitializer; -import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.stereotype.Service; -import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; import static org.assertj.core.api.Assertions.assertThat; @@ -38,12 +39,17 @@ * @author Michael Simons */ @Testcontainers(disabledWithoutDocker = true) -@ContextConfiguration(initializers = DataNeo4jTestWithIncludeFilterIntegrationTests.Initializer.class) @DataNeo4jTest(includeFilters = @Filter(Service.class)) class DataNeo4jTestWithIncludeFilterIntegrationTests { @Container - static final Neo4jContainer neo4j = new Neo4jContainer<>().withoutAuthentication(); + static final Neo4jContainer neo4j = new Neo4jContainer<>(DockerImageNames.neo4j()).withoutAuthentication() + .withStartupAttempts(5).withStartupTimeout(Duration.ofMinutes(10)); + + @DynamicPropertySource + static void neo4jProperties(DynamicPropertyRegistry registry) { + registry.add("spring.neo4j.uri", neo4j::getBoltUrl); + } @Autowired private ExampleService service; @@ -53,14 +59,4 @@ void testService() { assertThat(this.service.hasNode(ExampleGraph.class)).isFalse(); } - static class Initializer implements ApplicationContextInitializer { - - @Override - public void initialize(ConfigurableApplicationContext configurableApplicationContext) { - TestPropertyValues.of("spring.data.neo4j.uri=" + neo4j.getBoltUrl()) - .applyTo(configurableApplicationContext.getEnvironment()); - } - - } - } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/neo4j/ExampleGraph.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/neo4j/ExampleGraph.java index 41d3fb2aa2bf..701249813056 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/neo4j/ExampleGraph.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/neo4j/ExampleGraph.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,17 +16,17 @@ package org.springframework.boot.test.autoconfigure.data.neo4j; -import org.neo4j.ogm.annotation.GeneratedValue; -import org.neo4j.ogm.annotation.Id; -import org.neo4j.ogm.annotation.NodeEntity; -import org.neo4j.ogm.annotation.Property; +import org.springframework.data.neo4j.core.schema.GeneratedValue; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.core.schema.Property; /** * Example graph used with {@link DataNeo4jTest @DataNeo4jTest} tests. * * @author Eddú Meléndez */ -@NodeEntity +@Node public class ExampleGraph { @Id @@ -36,6 +36,10 @@ public class ExampleGraph { @Property private String description; + public ExampleGraph(String description) { + this.description = description; + } + public Long getId() { return this.id; } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/neo4j/ExampleReactiveRepository.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/neo4j/ExampleReactiveRepository.java new file mode 100644 index 000000000000..059223eadeb2 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/neo4j/ExampleReactiveRepository.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.data.neo4j; + +import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; + +/** + * Example reactive repository used with {@link DataNeo4jTest @DataNeo4jTest} tests. + * + * @author Stephane Nicoll + */ +interface ExampleReactiveRepository extends ReactiveNeo4jRepository { + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/neo4j/ExampleService.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/neo4j/ExampleService.java index 58e1493848e2..619299a77b5f 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/neo4j/ExampleService.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/neo4j/ExampleService.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,26 +16,26 @@ package org.springframework.boot.test.autoconfigure.data.neo4j; -import org.neo4j.ogm.session.Session; - +import org.springframework.data.neo4j.core.Neo4jTemplate; import org.springframework.stereotype.Service; /** * Example service used with {@link DataNeo4jTest @DataNeo4jTest} tests. * * @author Eddú Meléndez + * @author Michael J. Simons */ @Service public class ExampleService { - private final Session session; + private final Neo4jTemplate neo4jTemplate; - public ExampleService(Session session) { - this.session = session; + public ExampleService(Neo4jTemplate neo4jTemplate) { + this.neo4jTemplate = neo4jTemplate; } public boolean hasNode(Class clazz) { - return this.session.countEntitiesOfType(clazz) == 1; + return this.neo4jTemplate.count(clazz) == 1; } } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/r2dbc/DataR2dbcTestIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/r2dbc/DataR2dbcTestIntegrationTests.java new file mode 100644 index 000000000000..2a6d9f9aed1f --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/r2dbc/DataR2dbcTestIntegrationTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.data.r2dbc; + +import io.r2dbc.spi.ConnectionFactory; +import org.junit.jupiter.api.Test; +import reactor.test.StepVerifier; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.r2dbc.core.DatabaseClient; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link DataR2dbcTest}. + * + * @author Mark Paluch + */ +@DataR2dbcTest( + properties = "spring.sql.init.schemaLocations=classpath:org/springframework/boot/test/autoconfigure/data/r2dbc/schema.sql") +class DataR2dbcTestIntegrationTests { + + @Autowired + private DatabaseClient databaseClient; + + @Autowired + private ConnectionFactory connectionFactory; + + @Autowired + private ApplicationContext applicationContext; + + @Test + void testDatabaseClient() { + this.databaseClient.sql("SELECT * FROM example").fetch().all().as(StepVerifier::create).verifyComplete(); + } + + @Test + void replacesDefinedConnectionFactoryWithEmbeddedDefault() { + String product = this.connectionFactory.getMetadata().getName(); + assertThat(product).isEqualTo("H2"); + } + + @Test + void registersExampleRepository() { + assertThat(this.applicationContext.getBeanNamesForType(ExampleRepository.class)).isNotEmpty(); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/r2dbc/DataR2dbcTestPropertiesIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/r2dbc/DataR2dbcTestPropertiesIntegrationTests.java new file mode 100644 index 000000000000..d518b252c332 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/r2dbc/DataR2dbcTestPropertiesIntegrationTests.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.data.r2dbc; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for the {@link DataR2dbcTest#properties properties} attribute of + * {@link DataR2dbcTest @DataR2dbcTest}. + * + * @author Mark Paluch + */ +@DataR2dbcTest(properties = "spring.profiles.active=test") +class DataR2dbcTestPropertiesIntegrationTests { + + @Autowired + private Environment environment; + + @Test + void environmentWithNewProfile() { + assertThat(this.environment.getActiveProfiles()).containsExactly("test"); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/r2dbc/Example.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/r2dbc/Example.java new file mode 100644 index 000000000000..891af2e16ff3 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/r2dbc/Example.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.data.r2dbc; + +import org.springframework.data.annotation.Id; +import org.springframework.data.relational.core.mapping.Table; + +/** + * Example entity used with {@link DataR2dbcTest} tests. + * + * @author Mark Paluch + */ +@Table +public class Example { + + @Id + String id; + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/r2dbc/ExampleR2dbcApplication.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/r2dbc/ExampleR2dbcApplication.java new file mode 100644 index 000000000000..b24787b2353f --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/r2dbc/ExampleR2dbcApplication.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.data.r2dbc; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Example {@link SpringBootApplication} used with {@link DataR2dbcTest} tests. + * + * @author Mark Paluch + */ +@SpringBootApplication +public class ExampleR2dbcApplication { + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/r2dbc/ExampleRepository.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/r2dbc/ExampleRepository.java new file mode 100644 index 000000000000..993a3cbf59ff --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/r2dbc/ExampleRepository.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.data.r2dbc; + +import org.springframework.data.repository.reactive.ReactiveCrudRepository; + +/** + * Example {@link ReactiveCrudRepository} used with {@link DataR2dbcTest} tests. + * + * @author Mark Paluch + */ +public interface ExampleRepository extends ReactiveCrudRepository { + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/redis/DataRedisTestIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/redis/DataRedisTestIntegrationTests.java index bd333ba47c4d..ef7d4b19a630 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/redis/DataRedisTestIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/redis/DataRedisTestIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,14 +25,12 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.boot.testsupport.testcontainers.RedisContainer; import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextInitializer; -import org.springframework.context.ConfigurableApplicationContext; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.core.RedisOperations; -import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -43,12 +41,13 @@ * @author Jayaram Pradhan */ @Testcontainers(disabledWithoutDocker = true) -@ContextConfiguration(initializers = DataRedisTestIntegrationTests.Initializer.class) @DataRedisTest class DataRedisTestIntegrationTests { + private static final Charset CHARSET = StandardCharsets.UTF_8; + @Container - public static RedisContainer redis = new RedisContainer(); + static RedisContainer redis = new RedisContainer(); @Autowired private RedisOperations operations; @@ -59,7 +58,11 @@ class DataRedisTestIntegrationTests { @Autowired private ApplicationContext applicationContext; - private static final Charset CHARSET = StandardCharsets.UTF_8; + @DynamicPropertySource + static void redisProperties(DynamicPropertyRegistry registry) { + registry.add("spring.redis.host", redis::getHost); + registry.add("spring.redis.port", redis::getFirstMappedPort); + } @Test void testRepository() { @@ -79,14 +82,4 @@ void didNotInjectExampleService() { .isThrownBy(() -> this.applicationContext.getBean(ExampleService.class)); } - static class Initializer implements ApplicationContextInitializer { - - @Override - public void initialize(ConfigurableApplicationContext configurableApplicationContext) { - TestPropertyValues.of("spring.redis.port=" + redis.getFirstMappedPort()) - .applyTo(configurableApplicationContext.getEnvironment()); - } - - } - } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/redis/DataRedisTestPropertiesIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/redis/DataRedisTestPropertiesIntegrationTests.java index b598d652d7fe..4f48974f0207 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/redis/DataRedisTestPropertiesIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/redis/DataRedisTestPropertiesIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,12 +21,10 @@ import org.testcontainers.junit.jupiter.Testcontainers; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.boot.testsupport.testcontainers.RedisContainer; -import org.springframework.context.ApplicationContextInitializer; -import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.env.Environment; -import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; import static org.assertj.core.api.Assertions.assertThat; @@ -37,7 +35,6 @@ * @author Artsiom Yudovin */ @Testcontainers(disabledWithoutDocker = true) -@ContextConfiguration(initializers = DataRedisTestPropertiesIntegrationTests.Initializer.class) @DataRedisTest(properties = "spring.profiles.active=test") class DataRedisTestPropertiesIntegrationTests { @@ -47,19 +44,15 @@ class DataRedisTestPropertiesIntegrationTests { @Autowired private Environment environment; + @DynamicPropertySource + static void redisProperties(DynamicPropertyRegistry registry) { + registry.add("spring.redis.host", redis::getHost); + registry.add("spring.redis.port", redis::getFirstMappedPort); + } + @Test void environmentWithNewProfile() { assertThat(this.environment.getActiveProfiles()).containsExactly("test"); } - static class Initializer implements ApplicationContextInitializer { - - @Override - public void initialize(ConfigurableApplicationContext configurableApplicationContext) { - TestPropertyValues.of("spring.redis.port=" + redis.getFirstMappedPort()) - .applyTo(configurableApplicationContext.getEnvironment()); - } - - } - } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/redis/DataRedisTestReactiveIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/redis/DataRedisTestReactiveIntegrationTests.java new file mode 100644 index 000000000000..8bfa677a1d5d --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/redis/DataRedisTestReactiveIntegrationTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.data.redis; + +import java.util.UUID; + +import org.junit.jupiter.api.Test; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import reactor.test.StepVerifier; + +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.testsupport.testcontainers.RedisContainer; +import org.springframework.context.ApplicationContext; +import org.springframework.data.redis.core.ReactiveRedisOperations; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Integration test for {@link DataRedisTest @DataRedisTest} using reactive operations. + * + * @author Stephane Nicoll + */ +@Testcontainers(disabledWithoutDocker = true) +@DataRedisTest +class DataRedisTestReactiveIntegrationTests { + + @Container + static RedisContainer redis = new RedisContainer(); + + @Autowired + private ReactiveRedisOperations operations; + + @Autowired + private ApplicationContext applicationContext; + + @DynamicPropertySource + static void redisProperties(DynamicPropertyRegistry registry) { + registry.add("spring.redis.host", redis::getHost); + registry.add("spring.redis.port", redis::getFirstMappedPort); + } + + @Test + void testRepository() { + String id = UUID.randomUUID().toString(); + StepVerifier.create(this.operations.opsForValue().set(id, "Hello World")).expectNext(Boolean.TRUE) + .verifyComplete(); + StepVerifier.create(this.operations.opsForValue().get(id)).expectNext("Hello World").verifyComplete(); + StepVerifier.create(this.operations.execute((action) -> action.serverCommands().flushDb())).expectNext("OK") + .verifyComplete(); + } + + @Test + void didNotInjectExampleService() { + assertThatExceptionOfType(NoSuchBeanDefinitionException.class) + .isThrownBy(() -> this.applicationContext.getBean(ExampleService.class)); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/redis/DataRedisTestWithIncludeFilterIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/redis/DataRedisTestWithIncludeFilterIntegrationTests.java index 3059fbb76992..bd4e7ecc93a9 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/redis/DataRedisTestWithIncludeFilterIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/redis/DataRedisTestWithIncludeFilterIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,13 +21,11 @@ import org.testcontainers.junit.jupiter.Testcontainers; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.boot.testsupport.testcontainers.RedisContainer; -import org.springframework.context.ApplicationContextInitializer; -import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.stereotype.Service; -import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; import static org.assertj.core.api.Assertions.assertThat; @@ -37,7 +35,6 @@ * @author Jayaram Pradhan */ @Testcontainers(disabledWithoutDocker = true) -@ContextConfiguration(initializers = DataRedisTestWithIncludeFilterIntegrationTests.Initializer.class) @DataRedisTest(includeFilters = @Filter(Service.class)) class DataRedisTestWithIncludeFilterIntegrationTests { @@ -50,6 +47,12 @@ class DataRedisTestWithIncludeFilterIntegrationTests { @Autowired private ExampleService service; + @DynamicPropertySource + static void redisProperties(DynamicPropertyRegistry registry) { + registry.add("spring.redis.host", redis::getHost); + registry.add("spring.redis.port", redis::getFirstMappedPort); + } + @Test void testService() { PersonHash personHash = new PersonHash(); @@ -59,14 +62,4 @@ void testService() { assertThat(this.service.hasRecord(savedEntity)).isTrue(); } - static class Initializer implements ApplicationContextInitializer { - - @Override - public void initialize(ConfigurableApplicationContext configurableApplicationContext) { - TestPropertyValues.of("spring.redis.port=" + redis.getFirstMappedPort()) - .applyTo(configurableApplicationContext.getEnvironment()); - } - - } - } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/redis/ExampleService.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/redis/ExampleService.java index 5e2f28f4704c..284bf4cd9eea 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/redis/ExampleService.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/redis/ExampleService.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ public class ExampleService { private static final Charset CHARSET = StandardCharsets.UTF_8; - private RedisOperations operations; + private final RedisOperations operations; public ExampleService(RedisOperations operations) { this.operations = operations; diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFiltersContextCustomizerFactoryTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFiltersContextCustomizerFactoryTests.java index 852a084b2829..a2424ef339e1 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFiltersContextCustomizerFactoryTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFiltersContextCustomizerFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.context.TypeExcludeFilter; +import org.springframework.boot.test.autoconfigure.filter.TypeExcludeFiltersContextCustomizerFactoryTests.EnclosingClass.WithEnclosingClassExcludeFilters; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.core.type.classreading.MetadataReader; @@ -55,6 +56,13 @@ void getContextCustomizerWhenHasAnnotationShouldReturnCustomizer() { assertThat(customizer).isNotNull(); } + @Test + void getContextCustomizerWhenEnclosingClassHasAnnotationShouldReturnCustomizer() { + ContextCustomizer customizer = this.factory.createContextCustomizer(WithEnclosingClassExcludeFilters.class, + null); + assertThat(customizer).isNotNull(); + } + @Test void hashCodeAndEquals() { ContextCustomizer customizer1 = this.factory.createContextCustomizer(WithExcludeFilters.class, null); @@ -88,6 +96,15 @@ static class WithExcludeFilters { } + @TypeExcludeFilters({ SimpleExclude.class, TestClassAwareExclude.class }) + static class EnclosingClass { + + class WithEnclosingClassExcludeFilters { + + } + + } + @TypeExcludeFilters({ TestClassAwareExclude.class, SimpleExclude.class }) static class WithSameExcludeFilters { diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/ExampleRepository.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/ExampleRepository.java index 643115ea94b2..76e28fd542b5 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/ExampleRepository.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/ExampleRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,8 +48,7 @@ public void save(ExampleEntity entity) { } public ExampleEntity findById(int id) { - return this.jdbcTemplate.queryForObject("select id, name from example where id =?", new Object[] { id }, - ROW_MAPPER); + return this.jdbcTemplate.queryForObject("select id, name from example where id =?", ROW_MAPPER, id); } public Collection findAll() { diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTestIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTestIntegrationTests.java index e0dd1f2b3d80..f7a5eed4fcbb 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTestIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTestIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ */ @JdbcTest @TestPropertySource( - properties = "spring.datasource.schema=classpath:org/springframework/boot/test/autoconfigure/jdbc/schema.sql") + properties = "spring.sql.init.schemaLocations=classpath:org/springframework/boot/test/autoconfigure/jdbc/schema.sql") class JdbcTestIntegrationTests { @Autowired diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTestWithAutoConfigureTestDatabaseReplaceAutoConfiguredIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTestWithAutoConfigureTestDatabaseReplaceAutoConfiguredIntegrationTests.java index 98a8ec7b566b..eebf2747143b 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTestWithAutoConfigureTestDatabaseReplaceAutoConfiguredIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTestWithAutoConfigureTestDatabaseReplaceAutoConfiguredIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,7 @@ */ @JdbcTest @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.AUTO_CONFIGURED, - connection = EmbeddedDatabaseConnection.HSQL) + connection = EmbeddedDatabaseConnection.HSQLDB) class JdbcTestWithAutoConfigureTestDatabaseReplaceAutoConfiguredIntegrationTests { @Autowired diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTestWithAutoConfigureTestDatabaseReplaceExplicitIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTestWithAutoConfigureTestDatabaseReplaceExplicitIntegrationTests.java index 68042165581c..86dad2a3e42c 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTestWithAutoConfigureTestDatabaseReplaceExplicitIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTestWithAutoConfigureTestDatabaseReplaceExplicitIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,7 +37,7 @@ * @author Stephane Nicoll */ @JdbcTest -@AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.HSQL) +@AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.HSQLDB) class JdbcTestWithAutoConfigureTestDatabaseReplaceExplicitIntegrationTests { @Autowired diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTestWithAutoConfigureTestDatabaseReplacePropertyAnyIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTestWithAutoConfigureTestDatabaseReplacePropertyAnyIntegrationTests.java index e737e2f3794c..3d0e51fa41c7 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTestWithAutoConfigureTestDatabaseReplacePropertyAnyIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTestWithAutoConfigureTestDatabaseReplacePropertyAnyIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,7 @@ * @author Stephane Nicoll */ @JdbcTest -@AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.HSQL) +@AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.HSQLDB) @TestPropertySource(properties = "spring.test.database.replace=ANY") class JdbcTestWithAutoConfigureTestDatabaseReplacePropertyAnyIntegrationTests { diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTestWithAutoConfigureTestDatabaseReplacePropertyAutoConfiguredIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTestWithAutoConfigureTestDatabaseReplacePropertyAutoConfiguredIntegrationTests.java index 2f20d407944f..538b9858cf8a 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTestWithAutoConfigureTestDatabaseReplacePropertyAutoConfiguredIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTestWithAutoConfigureTestDatabaseReplacePropertyAutoConfiguredIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,7 @@ * @author Stephane Nicoll */ @JdbcTest -@AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.HSQL) +@AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.HSQLDB) @TestPropertySource(properties = "spring.test.database.replace=AUTO_CONFIGURED") class JdbcTestWithAutoConfigureTestDatabaseReplacePropertyAutoConfiguredIntegrationTests { diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTestWithIncludeFilterIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTestWithIncludeFilterIntegrationTests.java index 46ac3afcb699..8fe180a93c7a 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTestWithIncludeFilterIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTestWithIncludeFilterIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,7 @@ */ @JdbcTest(includeFilters = @Filter(Repository.class)) @TestPropertySource( - properties = "spring.datasource.schema=classpath:org/springframework/boot/test/autoconfigure/jdbc/schema.sql") + properties = "spring.sql.init.schemaLocations=classpath:org/springframework/boot/test/autoconfigure/jdbc/schema.sql") class JdbcTestWithIncludeFilterIntegrationTests { @Autowired diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/json/app/ExampleJsonApplication.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/json/app/ExampleJsonApplication.java index 2e21f9bcbc90..7f63af309b7d 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/json/app/ExampleJsonApplication.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/json/app/ExampleJsonApplication.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.boot.test.autoconfigure.json.app; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; import org.springframework.boot.test.autoconfigure.json.JsonTest; /** @@ -25,7 +26,7 @@ * * @author Phillip Webb */ -@SpringBootApplication +@SpringBootApplication(exclude = CassandraAutoConfiguration.class) public class ExampleJsonApplication { } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/orm/jpa/DataJpaTestAttributesIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/orm/jpa/DataJpaTestAttributesIntegrationTests.java new file mode 100644 index 000000000000..92ccce3c9527 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/orm/jpa/DataJpaTestAttributesIntegrationTests.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.orm.jpa; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.data.repository.config.BootstrapMode; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for non-default attributes of {@link DataJpaTest @DataJpaTest}. + * + * @author Artsiom Yudovin + * @author Scott Frederick + */ +@DataJpaTest(properties = "spring.profiles.active=test", bootstrapMode = BootstrapMode.DEFERRED) +class DataJpaTestAttributesIntegrationTests { + + @Autowired + private Environment environment; + + @Test + void environmentWithNewProfile() { + assertThat(this.environment.getActiveProfiles()).containsExactly("test"); + } + + @Test + void bootstrapModeIsSet() { + assertThat(this.environment.getProperty("spring.data.jpa.repositories.bootstrap-mode")) + .isEqualTo(BootstrapMode.DEFERRED.name()); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/orm/jpa/DataJpaTestIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/orm/jpa/DataJpaTestIntegrationTests.java index 8324341391f8..723dd514eddd 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/orm/jpa/DataJpaTestIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/orm/jpa/DataJpaTestIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration; import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration; import org.springframework.context.ApplicationContext; +import org.springframework.data.repository.config.BootstrapMode; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.TestPropertySource; @@ -37,6 +38,7 @@ * * @author Phillip Webb * @author Andy Wilkinson + * @author Scott Frederick */ @DataJpaTest @TestPropertySource(properties = "spring.jpa.hibernate.use-new-id-generator-mappings=false") @@ -71,7 +73,7 @@ void testEntityManagerPersistAndGetId() { Long id = this.entities.persistAndGetId(new ExampleEntity("spring", "123"), Long.class); assertThat(id).isNotNull(); String reference = this.jdbcTemplate.queryForObject("SELECT REFERENCE FROM EXAMPLE_ENTITY WHERE ID = ?", - new Object[] { id }, String.class); + String.class, id); assertThat(reference).isEqualTo("123"); } @@ -106,4 +108,10 @@ void liquibaseAutoConfigurationWasImported() { assertThat(this.applicationContext).has(importedAutoConfiguration(LiquibaseAutoConfiguration.class)); } + @Test + void bootstrapModeIsDefaultByDefault() { + assertThat(this.applicationContext.getEnvironment().getProperty("spring.data.jpa.repositories.bootstrap-mode")) + .isEqualTo(BootstrapMode.DEFAULT.name()); + } + } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/orm/jpa/DataJpaTestPropertiesIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/orm/jpa/DataJpaTestPropertiesIntegrationTests.java deleted file mode 100644 index da8567836f4d..000000000000 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/orm/jpa/DataJpaTestPropertiesIntegrationTests.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.test.autoconfigure.orm.jpa; - -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.env.Environment; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for the {@link DataJpaTest#properties properties} attribute of - * {@link DataJpaTest @DataJpaTest}. - * - * @author Artsiom Yudovin - */ -@DataJpaTest(properties = "spring.profiles.active=test") -class DataJpaTestPropertiesIntegrationTests { - - @Autowired - private Environment environment; - - @Test - void environmentWithNewProfile() { - assertThat(this.environment.getActiveProfiles()).containsExactly("test"); - } - -} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/orm/jpa/DataJpaTestSchemaCredentialsIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/orm/jpa/DataJpaTestSchemaCredentialsIntegrationTests.java new file mode 100644 index 000000000000..acef5aefb10b --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/orm/jpa/DataJpaTestSchemaCredentialsIntegrationTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.orm.jpa; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link DataJpaTest @DataJpaTest} with schema credentials that + * should be ignored to allow the auto-configured test database to be used. + * + * @author Andy Wilkinson + */ +@DataJpaTest(properties = { "spring.sql.init.username=alice", "spring.sql.init.password=secret", + "spring.sql.init.schema-locations=classpath:org/springframework/boot/test/autoconfigure/orm/jpa/schema.sql" }) +class DataJpaTestSchemaCredentialsIntegrationTests { + + @Autowired + private DataSource dataSource; + + @Test + void replacesDefinedDataSourceWithEmbeddedDefault() throws Exception { + String product = this.dataSource.getConnection().getMetaData().getDatabaseProductName(); + assertThat(product).isEqualTo("H2"); + assertThat(new JdbcTemplate(this.dataSource).queryForList("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES", + String.class)).contains("EXAMPLE"); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/orm/jpa/TestEntityManagerTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/orm/jpa/TestEntityManagerTests.java index 15154b0cc31c..ed06824cbe77 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/orm/jpa/TestEntityManagerTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/orm/jpa/TestEntityManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,8 +22,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.orm.jpa.EntityManagerHolder; import org.springframework.transaction.support.TransactionSynchronizationManager; @@ -32,13 +33,14 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.verify; +import static org.mockito.BDDMockito.then; /** * Tests for {@link TestEntityManager}. * * @author Phillip Webb */ +@ExtendWith(MockitoExtension.class) class TestEntityManagerTests { @Mock @@ -54,9 +56,7 @@ class TestEntityManagerTests { @BeforeEach void setup() { - MockitoAnnotations.initMocks(this); this.testEntityManager = new TestEntityManager(this.entityManagerFactory); - given(this.entityManagerFactory.getPersistenceUnitUtil()).willReturn(this.persistenceUnitUtil); } @Test @@ -69,9 +69,10 @@ void createWhenEntityManagerIsNullShouldThrowException() { void persistAndGetIdShouldPersistAndGetId() { bindEntityManager(); TestEntity entity = new TestEntity(); + given(this.entityManagerFactory.getPersistenceUnitUtil()).willReturn(this.persistenceUnitUtil); given(this.persistenceUnitUtil.getIdentifier(entity)).willReturn(123); Object result = this.testEntityManager.persistAndGetId(entity); - verify(this.entityManager).persist(entity); + then(this.entityManager).should().persist(entity); assertThat(result).isEqualTo(123); } @@ -79,9 +80,10 @@ void persistAndGetIdShouldPersistAndGetId() { void persistAndGetIdForTypeShouldPersistAndGetId() { bindEntityManager(); TestEntity entity = new TestEntity(); + given(this.entityManagerFactory.getPersistenceUnitUtil()).willReturn(this.persistenceUnitUtil); given(this.persistenceUnitUtil.getIdentifier(entity)).willReturn(123); Integer result = this.testEntityManager.persistAndGetId(entity, Integer.class); - verify(this.entityManager).persist(entity); + then(this.entityManager).should().persist(entity); assertThat(result).isEqualTo(123); } @@ -90,7 +92,7 @@ void persistShouldPersist() { bindEntityManager(); TestEntity entity = new TestEntity(); TestEntity result = this.testEntityManager.persist(entity); - verify(this.entityManager).persist(entity); + then(this.entityManager).should().persist(entity); assertThat(result).isSameAs(entity); } @@ -99,8 +101,8 @@ void persistAndFlushShouldPersistAndFlush() { bindEntityManager(); TestEntity entity = new TestEntity(); TestEntity result = this.testEntityManager.persistAndFlush(entity); - verify(this.entityManager).persist(entity); - verify(this.entityManager).flush(); + then(this.entityManager).should().persist(entity); + then(this.entityManager).should().flush(); assertThat(result).isSameAs(entity); } @@ -109,11 +111,12 @@ void persistFlushFindShouldPersistAndFlushAndFind() { bindEntityManager(); TestEntity entity = new TestEntity(); TestEntity found = new TestEntity(); + given(this.entityManagerFactory.getPersistenceUnitUtil()).willReturn(this.persistenceUnitUtil); given(this.persistenceUnitUtil.getIdentifier(entity)).willReturn(123); given(this.entityManager.find(TestEntity.class, 123)).willReturn(found); TestEntity result = this.testEntityManager.persistFlushFind(entity); - verify(this.entityManager).persist(entity); - verify(this.entityManager).flush(); + then(this.entityManager).should().persist(entity); + then(this.entityManager).should().flush(); assertThat(result).isSameAs(found); } @@ -123,7 +126,7 @@ void mergeShouldMerge() { TestEntity entity = new TestEntity(); given(this.entityManager.merge(entity)).willReturn(entity); TestEntity result = this.testEntityManager.merge(entity); - verify(this.entityManager).merge(entity); + then(this.entityManager).should().merge(entity); assertThat(result).isSameAs(entity); } @@ -132,7 +135,7 @@ void removeShouldRemove() { bindEntityManager(); TestEntity entity = new TestEntity(); this.testEntityManager.remove(entity); - verify(this.entityManager).remove(entity); + then(this.entityManager).should().remove(entity); } @Test @@ -148,7 +151,7 @@ void findShouldFind() { void flushShouldFlush() { bindEntityManager(); this.testEntityManager.flush(); - verify(this.entityManager).flush(); + then(this.entityManager).should().flush(); } @Test @@ -156,14 +159,14 @@ void refreshShouldRefresh() { bindEntityManager(); TestEntity entity = new TestEntity(); this.testEntityManager.refresh(entity); - verify(this.entityManager).refresh(entity); + then(this.entityManager).should().refresh(entity); } @Test void clearShouldClear() { bindEntityManager(); this.testEntityManager.clear(); - verify(this.entityManager).clear(); + then(this.entityManager).should().clear(); } @Test @@ -171,12 +174,13 @@ void detachShouldDetach() { bindEntityManager(); TestEntity entity = new TestEntity(); this.testEntityManager.detach(entity); - verify(this.entityManager).detach(entity); + then(this.entityManager).should().detach(entity); } @Test void getIdForTypeShouldGetId() { TestEntity entity = new TestEntity(); + given(this.entityManagerFactory.getPersistenceUnitUtil()).willReturn(this.persistenceUnitUtil); given(this.persistenceUnitUtil.getIdentifier(entity)).willReturn(123); Integer result = this.testEntityManager.getId(entity, Integer.class); assertThat(result).isEqualTo(123); @@ -185,6 +189,7 @@ void getIdForTypeShouldGetId() { @Test void getIdForTypeWhenTypeIsWrongShouldThrowException() { TestEntity entity = new TestEntity(); + given(this.entityManagerFactory.getPersistenceUnitUtil()).willReturn(this.persistenceUnitUtil); given(this.persistenceUnitUtil.getIdentifier(entity)).willReturn(123); assertThatIllegalArgumentException().isThrownBy(() -> this.testEntityManager.getId(entity, Long.class)) .withMessageContaining("ID mismatch: Object of class [java.lang.Integer] " @@ -194,6 +199,7 @@ void getIdForTypeWhenTypeIsWrongShouldThrowException() { @Test void getIdShouldGetId() { TestEntity entity = new TestEntity(); + given(this.entityManagerFactory.getPersistenceUnitUtil()).willReturn(this.persistenceUnitUtil); given(this.persistenceUnitUtil.getIdentifier(entity)).willReturn(123); Object result = this.testEntityManager.getId(entity); assertThat(result).isEqualTo(123); diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/override/OverrideAutoConfigurationSpringBootApplication.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/override/OverrideAutoConfigurationSpringBootApplication.java index c519802da980..803e999ae410 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/override/OverrideAutoConfigurationSpringBootApplication.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/override/OverrideAutoConfigurationSpringBootApplication.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; import org.springframework.boot.test.autoconfigure.OverrideAutoConfiguration; /** @@ -28,7 +29,7 @@ * @author Andy Wilkinson */ @SpringBootConfiguration -@EnableAutoConfiguration +@EnableAutoConfiguration(exclude = CassandraAutoConfiguration.class) public class OverrideAutoConfigurationSpringBootApplication { } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/properties/AnnotationsPropertySourceTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/properties/AnnotationsPropertySourceTests.java index 2744df4d8bb3..d52d5730f1e3 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/properties/AnnotationsPropertySourceTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/properties/AnnotationsPropertySourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,8 +19,13 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.properties.AnnotationsPropertySourceTests.DeeplyNestedAnnotations.Level1; +import org.springframework.boot.test.autoconfigure.properties.AnnotationsPropertySourceTests.DeeplyNestedAnnotations.Level2; +import org.springframework.boot.test.autoconfigure.properties.AnnotationsPropertySourceTests.EnclosingClass.PropertyMappedAnnotationOnEnclosingClass; +import org.springframework.boot.test.autoconfigure.properties.AnnotationsPropertySourceTests.NestedAnnotations.Entry; import org.springframework.core.annotation.AliasFor; import static org.assertj.core.api.Assertions.assertThat; @@ -152,6 +157,14 @@ void typeLevelAnnotationOnSuperClass() { assertThat(source.getProperty("value")).isEqualTo("abc"); } + @Test + void typeLevelAnnotationOnEnclosingClass() { + AnnotationsPropertySource source = new AnnotationsPropertySource( + PropertyMappedAnnotationOnEnclosingClass.class); + assertThat(source.getPropertyNames()).containsExactly("value"); + assertThat(source.getProperty("value")).isEqualTo("abc"); + } + @Test void aliasedPropertyMappedAttributeOnSuperClass() { AnnotationsPropertySource source = new AnnotationsPropertySource( @@ -172,6 +185,26 @@ void enumValueNotMapped() { assertThat(source.containsProperty("testenum.value")).isFalse(); } + @Test + void nestedAnnotationsMapped() { + AnnotationsPropertySource source = new AnnotationsPropertySource(PropertyMappedWithNestedAnnotations.class); + assertThat(source.getProperty("testnested")).isNull(); + assertThat(source.getProperty("testnested.entries[0]")).isNull(); + assertThat(source.getProperty("testnested.entries[0].value")).isEqualTo("one"); + assertThat(source.getProperty("testnested.entries[1]")).isNull(); + assertThat(source.getProperty("testnested.entries[1].value")).isEqualTo("two"); + } + + @Test + void deeplyNestedAnnotationsMapped() { + AnnotationsPropertySource source = new AnnotationsPropertySource( + PropertyMappedWithDeeplyNestedAnnotations.class); + assertThat(source.getProperty("testdeeplynested")).isNull(); + assertThat(source.getProperty("testdeeplynested.level1")).isNull(); + assertThat(source.getProperty("testdeeplynested.level1.level2")).isNull(); + assertThat(source.getProperty("testdeeplynested.level1.level2.value")).isEqualTo("level2"); + } + static class NoAnnotation { } @@ -363,6 +396,15 @@ static class PropertyMappedAnnotationOnSuperClass extends TypeLevel { } + @TypeLevelAnnotation("abc") + static class EnclosingClass { + + class PropertyMappedAnnotationOnEnclosingClass { + + } + + } + static class AliasedPropertyMappedAnnotationOnSuperClass extends PropertyMappedAttributeWithAnAlias { } @@ -396,4 +438,61 @@ enum EnumItem { } + @Retention(RetentionPolicy.RUNTIME) + @PropertyMapping("testnested") + @interface NestedAnnotations { + + Entry[] entries(); + + @Retention(RetentionPolicy.RUNTIME) + @interface Entry { + + String value(); + + } + + } + + @NestedAnnotations(entries = { @Entry("one"), @Entry("two") }) + static class PropertyMappedWithNestedAnnotations { + + } + + @Retention(RetentionPolicy.RUNTIME) + @PropertyMapping("testdeeplynested") + @interface DeeplyNestedAnnotations { + + Level1 level1(); + + @Retention(RetentionPolicy.RUNTIME) + @interface Level1 { + + Level2 level2(); + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface Level2 { + + String value(); + + } + + } + + @DeeplyNestedAnnotations(level1 = @Level1(level2 = @Level2("level2"))) + static class PropertyMappedWithDeeplyNestedAnnotations { + + } + + @TypeLevelAnnotation("outer") + static class OuterWithTypeLevel { + + @Nested + static class NestedClass { + + } + + } + } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/properties/PropertyMappingContextCustomizerFactoryTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/properties/PropertyMappingContextCustomizerFactoryTests.java index a2d5197ae01d..535d8f6f9932 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/properties/PropertyMappingContextCustomizerFactoryTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/properties/PropertyMappingContextCustomizerFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,8 +32,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verifyNoInteractions; /** * Tests for {@link PropertyMappingContextCustomizerFactory}. @@ -53,7 +53,7 @@ void getContextCustomizerWhenHasNoMappingShouldNotAddPropertySource() { given(context.getEnvironment()).willReturn(environment); given(context.getBeanFactory()).willReturn(beanFactory); customizer.customizeContext(context, null); - verifyNoInteractions(environment); + then(environment).shouldHaveNoInteractions(); } @Test diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/restdocs/RestDocsTestApplication.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/restdocs/RestDocsTestApplication.java index b90c7e9706ef..dc26a4b1e12b 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/restdocs/RestDocsTestApplication.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/restdocs/RestDocsTestApplication.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,9 @@ package org.springframework.boot.test.autoconfigure.restdocs; +import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; /** @@ -24,7 +26,8 @@ * * @author Andy Wilkinson */ -@SpringBootApplication(exclude = SecurityAutoConfiguration.class) +@SpringBootApplication(exclude = { CassandraAutoConfiguration.class, SecurityAutoConfiguration.class, + ManagementWebSecurityAutoConfiguration.class }) public class RestDocsTestApplication { } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/restdocs/WebTestClientRestDocsAutoConfigurationAdvancedConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/restdocs/WebTestClientRestDocsAutoConfigurationAdvancedConfigurationIntegrationTests.java index eb870231310e..0eb874848125 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/restdocs/WebTestClientRestDocsAutoConfigurationAdvancedConfigurationIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/restdocs/WebTestClientRestDocsAutoConfigurationAdvancedConfigurationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,7 +61,7 @@ void deleteSnippets() { } @Test - void defaultSnippetsAreWritten() throws Exception { + void defaultSnippetsAreWritten() { this.webTestClient.get().uri("/").exchange().expectStatus().is2xxSuccessful().expectBody() .consumeWith(document("default-snippets")); File defaultSnippetsDir = new File(this.generatedSnippets, "default-snippets"); diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/restdocs/WebTestClientRestDocsAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/restdocs/WebTestClientRestDocsAutoConfigurationIntegrationTests.java index 979455ffbedc..c02e04d8e301 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/restdocs/WebTestClientRestDocsAutoConfigurationIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/restdocs/WebTestClientRestDocsAutoConfigurationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,7 +54,7 @@ void deleteSnippets() { } @Test - void defaultSnippetsAreWritten() throws Exception { + void defaultSnippetsAreWritten() { this.webTestClient.get().uri("/").exchange().expectStatus().is2xxSuccessful().expectBody() .consumeWith(document("default-snippets")); File defaultSnippetsDir = new File(this.generatedSnippets, "default-snippets"); diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AnotherExampleRestClient.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AnotherExampleRestClient.java index c551a600b495..e7452ca904e4 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AnotherExampleRestClient.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AnotherExampleRestClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ @Service public class AnotherExampleRestClient { - private RestTemplate restTemplate; + private final RestTemplate restTemplate; public AnotherExampleRestClient(RestTemplateBuilder builder) { this.restTemplate = builder.rootUri("https://example.com").build(); diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AutoConfigureMockRestServiceServerWithRootUriIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AutoConfigureMockRestServiceServerWithRootUriIntegrationTests.java new file mode 100644 index 000000000000..8021d1f7c0b2 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AutoConfigureMockRestServiceServerWithRootUriIntegrationTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.web.client; + +import io.micrometer.core.instrument.MeterRegistry; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +/** + * Tests for + * {@link AutoConfigureMockRestServiceServer @AutoConfigureMockRestServiceServer} with a + * {@link RestTemplate} configured with a root URI. + * + * @author Andy Wilkinson + */ +@SpringBootTest +@AutoConfigureMockRestServiceServer +class AutoConfigureMockRestServiceServerWithRootUriIntegrationTests { + + @Autowired + private RestTemplate restTemplate; + + @Autowired + private MockRestServiceServer server; + + @Autowired + MeterRegistry meterRegistry; + + @Test + void whenRestTemplateAppliesARootUriThenMockServerExpecationsAreStillMatched() { + this.server.expect(requestTo("/test")).andRespond(withSuccess("hello", MediaType.TEXT_HTML)); + ResponseEntity entity = this.restTemplate.getForEntity("/test", String.class); + assertThat(entity.getBody()).isEqualTo("hello"); + assertThat(this.meterRegistry.find("http.client.requests").tag("uri", "/rest/test").timer()).isNotNull(); + } + + @EnableAutoConfiguration(exclude = CassandraAutoConfiguration.class) + @Configuration(proxyBeanMethods = false) + static class RootUriConfiguration { + + @Bean + RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) { + return restTemplateBuilder.rootUri("/rest").build(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AutoConfigureWebClientWithRestTemplateIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AutoConfigureWebClientWithRestTemplateIntegrationTests.java index a8fa93e2ddc3..edb53b36e47c 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AutoConfigureWebClientWithRestTemplateIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AutoConfigureWebClientWithRestTemplateIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Configuration; @@ -57,7 +58,7 @@ void restTemplateTest() { } @Configuration(proxyBeanMethods = false) - @EnableAutoConfiguration + @EnableAutoConfiguration(exclude = CassandraAutoConfiguration.class) static class Config { } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/reactive/WebFluxTypeExcludeFilterTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/reactive/WebFluxTypeExcludeFilterTests.java index 66563d166661..6eff59b9fb2d 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/reactive/WebFluxTypeExcludeFilterTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/reactive/WebFluxTypeExcludeFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,9 @@ import java.io.IOException; +import com.fasterxml.jackson.databind.module.SimpleModule; import org.junit.jupiter.api.Test; +import org.thymeleaf.dialect.IDialect; import reactor.core.publisher.Mono; import org.springframework.context.annotation.ComponentScan.Filter; @@ -57,6 +59,8 @@ void matchWhenHasNoControllers() throws Exception { assertThat(excludes(filter, ExampleService.class)).isTrue(); assertThat(excludes(filter, ExampleRepository.class)).isTrue(); assertThat(excludes(filter, ExampleWebFilter.class)).isFalse(); + assertThat(excludes(filter, ExampleModule.class)).isFalse(); + assertThat(excludes(filter, ExampleDialect.class)).isFalse(); } @Test @@ -69,6 +73,8 @@ void matchWhenHasController() throws Exception { assertThat(excludes(filter, ExampleService.class)).isTrue(); assertThat(excludes(filter, ExampleRepository.class)).isTrue(); assertThat(excludes(filter, ExampleWebFilter.class)).isFalse(); + assertThat(excludes(filter, ExampleModule.class)).isFalse(); + assertThat(excludes(filter, ExampleDialect.class)).isFalse(); } @Test @@ -81,6 +87,8 @@ void matchNotUsingDefaultFilters() throws Exception { assertThat(excludes(filter, ExampleService.class)).isTrue(); assertThat(excludes(filter, ExampleRepository.class)).isTrue(); assertThat(excludes(filter, ExampleWebFilter.class)).isTrue(); + assertThat(excludes(filter, ExampleModule.class)).isTrue(); + assertThat(excludes(filter, ExampleDialect.class)).isTrue(); } @Test @@ -93,6 +101,8 @@ void matchWithIncludeFilter() throws Exception { assertThat(excludes(filter, ExampleService.class)).isTrue(); assertThat(excludes(filter, ExampleRepository.class)).isFalse(); assertThat(excludes(filter, ExampleWebFilter.class)).isFalse(); + assertThat(excludes(filter, ExampleModule.class)).isFalse(); + assertThat(excludes(filter, ExampleDialect.class)).isFalse(); } @Test @@ -105,6 +115,8 @@ void matchWithExcludeFilter() throws Exception { assertThat(excludes(filter, ExampleService.class)).isTrue(); assertThat(excludes(filter, ExampleRepository.class)).isTrue(); assertThat(excludes(filter, ExampleWebFilter.class)).isFalse(); + assertThat(excludes(filter, ExampleModule.class)).isFalse(); + assertThat(excludes(filter, ExampleDialect.class)).isFalse(); } private boolean excludes(WebFluxTypeExcludeFilter filter, Class type) throws IOException { @@ -175,4 +187,17 @@ public Mono filter(ServerWebExchange serverWebExchange, WebFilterChain web } + static class ExampleModule extends SimpleModule { + + } + + static class ExampleDialect implements IDialect { + + @Override + public String getName() { + return "example"; + } + + } + } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/reactive/WebTestClientAutoConfigurationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/reactive/WebTestClientAutoConfigurationTests.java index d530c4210153..df78ddd03357 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/reactive/WebTestClientAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/reactive/WebTestClientAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package org.springframework.boot.test.autoconfigure.web.reactive; import java.time.Duration; -import java.time.temporal.ChronoUnit; import java.util.List; import org.junit.jupiter.api.Test; @@ -39,8 +38,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link WebTestClientAutoConfiguration} @@ -50,7 +49,7 @@ */ class WebTestClientAutoConfigurationTests { - private ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(WebTestClientAutoConfiguration.class)); @Test @@ -66,7 +65,7 @@ void shouldCustomizeClientCodecs() { this.contextRunner.withUserConfiguration(CodecConfiguration.class).run((context) -> { assertThat(context).hasSingleBean(WebTestClient.class); assertThat(context).hasSingleBean(CodecCustomizer.class); - verify(context.getBean(CodecCustomizer.class)).customize(any(CodecConfigurer.class)); + then(context.getBean(CodecCustomizer.class)).should().customize(any(CodecConfigurer.class)); }); } @@ -75,8 +74,7 @@ void shouldCustomizeTimeout() { this.contextRunner.withUserConfiguration(BaseConfiguration.class) .withPropertyValues("spring.test.webtestclient.timeout=15m").run((context) -> { WebTestClient webTestClient = context.getBean(WebTestClient.class); - Object duration = ReflectionTestUtils.getField(webTestClient, "timeout"); - assertThat(duration).isEqualTo(Duration.of(15, ChronoUnit.MINUTES)); + assertThat(webTestClient).hasFieldOrPropertyWithValue("responseTimeout", Duration.ofMinutes(15)); }); } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/reactive/webclient/ExampleWebExceptionHandler.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/reactive/webclient/ExampleWebExceptionHandler.java index c25836c7a5b3..04a6695dc5c3 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/reactive/webclient/ExampleWebExceptionHandler.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/reactive/webclient/ExampleWebExceptionHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.test.autoconfigure.web.reactive.webclient; import reactor.core.publisher.Mono; diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/reactive/webclient/ExampleWebFluxApplication.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/reactive/webclient/ExampleWebFluxApplication.java index e20ef486679c..124d8c36a3eb 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/reactive/webclient/ExampleWebFluxApplication.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/reactive/webclient/ExampleWebFluxApplication.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.boot.test.autoconfigure.web.reactive.webclient; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; /** @@ -25,7 +26,7 @@ * * @author Stephane Nicoll */ -@SpringBootApplication +@SpringBootApplication(exclude = CassandraAutoConfiguration.class) public class ExampleWebFluxApplication { } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/reactive/webclient/WebFluxTestAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/reactive/webclient/WebFluxTestAutoConfigurationIntegrationTests.java index 2fab1ff8648b..2cef057e1db8 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/reactive/webclient/WebFluxTestAutoConfigurationIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/reactive/webclient/WebFluxTestAutoConfigurationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,8 @@ import org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration; import org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration; import org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration; +import org.springframework.boot.autoconfigure.security.oauth2.client.reactive.ReactiveOAuth2ClientAutoConfiguration; +import org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration; import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration; import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration; @@ -37,6 +39,7 @@ * @author Stephane Nicoll * @author Artsiom Yudovin * @author Ali Dehghani + * @author Madhura Bhave */ @WebFluxTest class WebFluxTestAutoConfigurationIntegrationTests { @@ -74,4 +77,15 @@ void errorWebFluxAutoConfigurationIsImported() { assertThat(this.applicationContext).has(importedAutoConfiguration(ErrorWebFluxAutoConfiguration.class)); } + @Test + void oAuth2ClientAutoConfigurationWasImported() { + assertThat(this.applicationContext).has(importedAutoConfiguration(ReactiveOAuth2ClientAutoConfiguration.class)); + } + + @Test + void oAuth2ResourceServerAutoConfigurationWasImported() { + assertThat(this.applicationContext) + .has(importedAutoConfiguration(ReactiveOAuth2ResourceServerAutoConfiguration.class)); + } + } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/reactive/webclient/WebTestClientSpringBootTestIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/reactive/webclient/WebTestClientSpringBootTestIntegrationTests.java index 64a9009a1df4..4ceb0b0b54b1 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/reactive/webclient/WebTestClientSpringBootTestIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/reactive/webclient/WebTestClientSpringBootTestIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,8 +37,9 @@ * * @author Stephane Nicoll */ -@SpringBootTest(properties = "spring.main.web-application-type=reactive", classes = { - WebTestClientSpringBootTestIntegrationTests.TestConfiguration.class, ExampleWebFluxApplication.class }) +@SpringBootTest(properties = "spring.main.web-application-type=reactive", + classes = { WebTestClientSpringBootTestIntegrationTests.TestConfiguration.class, + ExampleWebFluxApplication.class }) @AutoConfigureWebTestClient class WebTestClientSpringBootTestIntegrationTests { @@ -67,7 +68,7 @@ void shouldHaveRealService() { static class TestConfiguration { @Bean - SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception { + SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { http.authorizeExchange((exchanges) -> exchanges.anyExchange().permitAll()); return http.build(); } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/MockMvcAutoConfigurationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/MockMvcAutoConfigurationTests.java index 3ebf3c811e2b..eeaa6592c683 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/MockMvcAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/MockMvcAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.test.autoconfigure.web.servlet; import org.junit.jupiter.api.Test; @@ -31,7 +32,7 @@ */ class MockMvcAutoConfigurationTests { - private WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(MockMvcAutoConfiguration.class)); @Test diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizerTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizerTests.java index 63b343862440..4ae8388bc96a 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizerTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.test.autoconfigure.web.servlet; import java.util.ArrayList; diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTestAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTestAutoConfigurationIntegrationTests.java index 52eac84b328a..6e39d92300ce 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTestAutoConfigurationIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTestAutoConfigurationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,11 +22,13 @@ import org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration; import org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration; import org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration; +import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration; +import org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration; import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration; import org.springframework.context.ApplicationContext; import org.springframework.core.task.AsyncTaskExecutor; -import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import static org.assertj.core.api.Assertions.assertThat; @@ -37,6 +39,7 @@ * * @author Andy Wilkinson * @author Levi Puot Paul + * @author Madhura Bhave */ @WebMvcTest class WebMvcTestAutoConfigurationIntegrationTests { @@ -72,8 +75,23 @@ void taskExecutionAutoConfigurationWasImported() { @Test void asyncTaskExecutorWithApplicationTaskExecutor() { assertThat(this.applicationContext.getBeansOfType(AsyncTaskExecutor.class)).hasSize(1); - assertThat(ReflectionTestUtils.getField(this.applicationContext.getBean(RequestMappingHandlerAdapter.class), - "taskExecutor")).isSameAs(this.applicationContext.getBean("applicationTaskExecutor")); + assertThat(this.applicationContext.getBean(RequestMappingHandlerAdapter.class)).extracting("taskExecutor") + .isSameAs(this.applicationContext.getBean("applicationTaskExecutor")); + } + + @Test + void oAuth2ClientAutoConfigurationWasImported() { + assertThat(this.applicationContext).has(importedAutoConfiguration(OAuth2ClientAutoConfiguration.class)); + } + + @Test + void oAuth2ResourceServerAutoConfigurationWasImported() { + assertThat(this.applicationContext).has(importedAutoConfiguration(OAuth2ResourceServerAutoConfiguration.class)); + } + + @Test + void httpEncodingAutoConfigurationWasImported() { + assertThat(this.applicationContext).has(importedAutoConfiguration(HttpEncodingAutoConfiguration.class)); } } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTypeExcludeFilterTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTypeExcludeFilterTests.java index e9480855fcdf..ce8477be4223 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTypeExcludeFilterTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTypeExcludeFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,9 @@ import java.io.IOException; +import com.fasterxml.jackson.databind.module.SimpleModule; import org.junit.jupiter.api.Test; +import org.thymeleaf.dialect.IDialect; import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.FilterType; @@ -27,6 +29,7 @@ import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.web.SecurityFilterChain; import org.springframework.stereotype.Controller; import org.springframework.stereotype.Repository; import org.springframework.stereotype.Service; @@ -56,7 +59,10 @@ void matchWhenHasNoControllers() throws Exception { assertThat(excludes(filter, ExampleService.class)).isTrue(); assertThat(excludes(filter, ExampleRepository.class)).isTrue(); assertThat(excludes(filter, ExampleWebSecurityConfigurer.class)).isFalse(); + assertThat(excludes(filter, SecurityFilterChain.class)).isFalse(); assertThat(excludes(filter, ExampleHandlerInterceptor.class)).isFalse(); + assertThat(excludes(filter, ExampleModule.class)).isFalse(); + assertThat(excludes(filter, ExampleDialect.class)).isFalse(); } @Test @@ -70,7 +76,10 @@ void matchWhenHasController() throws Exception { assertThat(excludes(filter, ExampleService.class)).isTrue(); assertThat(excludes(filter, ExampleRepository.class)).isTrue(); assertThat(excludes(filter, ExampleWebSecurityConfigurer.class)).isFalse(); + assertThat(excludes(filter, SecurityFilterChain.class)).isFalse(); assertThat(excludes(filter, ExampleHandlerInterceptor.class)).isFalse(); + assertThat(excludes(filter, ExampleModule.class)).isFalse(); + assertThat(excludes(filter, ExampleDialect.class)).isFalse(); } @Test @@ -84,7 +93,10 @@ void matchNotUsingDefaultFilters() throws Exception { assertThat(excludes(filter, ExampleService.class)).isTrue(); assertThat(excludes(filter, ExampleRepository.class)).isTrue(); assertThat(excludes(filter, ExampleWebSecurityConfigurer.class)).isTrue(); + assertThat(excludes(filter, SecurityFilterChain.class)).isTrue(); assertThat(excludes(filter, ExampleHandlerInterceptor.class)).isTrue(); + assertThat(excludes(filter, ExampleModule.class)).isTrue(); + assertThat(excludes(filter, ExampleDialect.class)).isTrue(); } @Test @@ -98,6 +110,8 @@ void matchWithIncludeFilter() throws Exception { assertThat(excludes(filter, ExampleService.class)).isTrue(); assertThat(excludes(filter, ExampleRepository.class)).isFalse(); assertThat(excludes(filter, ExampleHandlerInterceptor.class)).isFalse(); + assertThat(excludes(filter, ExampleModule.class)).isFalse(); + assertThat(excludes(filter, ExampleDialect.class)).isFalse(); } @Test @@ -111,7 +125,10 @@ void matchWithExcludeFilter() throws Exception { assertThat(excludes(filter, ExampleService.class)).isTrue(); assertThat(excludes(filter, ExampleRepository.class)).isTrue(); assertThat(excludes(filter, ExampleWebSecurityConfigurer.class)).isFalse(); + assertThat(excludes(filter, SecurityFilterChain.class)).isFalse(); assertThat(excludes(filter, ExampleHandlerInterceptor.class)).isFalse(); + assertThat(excludes(filter, ExampleModule.class)).isFalse(); + assertThat(excludes(filter, ExampleDialect.class)).isFalse(); } private boolean excludes(WebMvcTypeExcludeFilter filter, Class type) throws IOException { @@ -185,4 +202,17 @@ static class ExampleHandlerInterceptor implements HandlerInterceptor { } + static class ExampleModule extends SimpleModule { + + } + + static class ExampleDialect implements IDialect { + + @Override + public String getName() { + return "example"; + } + + } + } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/AfterSecurityFilter.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/AfterSecurityFilter.java new file mode 100644 index 000000000000..86dcf3e538c9 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/AfterSecurityFilter.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.web.servlet.mockmvc; + +import java.io.IOException; +import java.security.Principal; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; + +import org.springframework.boot.autoconfigure.security.SecurityProperties; +import org.springframework.core.Ordered; + +/** + * {@link Filter} that is ordered to run after Spring Security's filter. + * + * @author Andy Wilkinson + */ +public class AfterSecurityFilter implements Filter, Ordered { + + @Override + public int getOrder() { + return SecurityProperties.DEFAULT_FILTER_ORDER + 1; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + Principal principal = ((HttpServletRequest) request).getUserPrincipal(); + if (principal == null) { + throw new ServletException("No user principal"); + } + response.getWriter().write(principal.getName()); + response.getWriter().flush(); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/AutoConfigureMockMvcSecurityFilterOrderingIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/AutoConfigureMockMvcSecurityFilterOrderingIntegrationTests.java new file mode 100644 index 000000000000..fdd388598405 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/AutoConfigureMockMvcSecurityFilterOrderingIntegrationTests.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.web.servlet.mockmvc; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Tests for {@link AutoConfigureMockMvc @AutoConfigureMockMvc} and the ordering of Spring + * Security's filter + * + * @author Andy Wilkinson + */ +@WebMvcTest +@WithMockUser(username = "user", password = "secret") +@Import(AfterSecurityFilter.class) +class AutoConfigureMockMvcSecurityFilterOrderingIntegrationTests { + + @Autowired + private MockMvc mvc; + + @Test + void afterSecurityFilterShouldFindAUserPrincipal() throws Exception { + this.mvc.perform(get("/one")).andExpect(status().isOk()).andExpect(content().string("user")); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/ExampleFilter.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/ExampleFilter.java index a4732f8e6271..089963432897 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/ExampleFilter.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/ExampleFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,9 @@ import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletResponse; +import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.core.Ordered; import org.springframework.stereotype.Component; /** @@ -35,7 +37,7 @@ * @author Phillip Webb */ @Component -public class ExampleFilter implements Filter { +public class ExampleFilter implements Filter, Ordered { @Override public void init(FilterConfig filterConfig) throws ServletException { @@ -52,4 +54,9 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha public void destroy() { } + @Override + public int getOrder() { + return SecurityProperties.DEFAULT_FILTER_ORDER - 1; + } + } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/ExampleWebMvcApplication.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/ExampleWebMvcApplication.java index 73513d58fdce..fc2f85bd7bb5 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/ExampleWebMvcApplication.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/ExampleWebMvcApplication.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.boot.test.autoconfigure.web.servlet.mockmvc; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; /** @@ -25,7 +26,7 @@ * * @author Phillip Webb */ -@SpringBootApplication +@SpringBootApplication(exclude = CassandraAutoConfiguration.class) public class ExampleWebMvcApplication { } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/HateoasController.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/HateoasController.java index 00ca2cce022d..d488867b3f5f 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/HateoasController.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/HateoasController.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import org.springframework.hateoas.EntityModel; import org.springframework.hateoas.Link; +import org.springframework.hateoas.LinkRelation; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -36,7 +37,7 @@ class HateoasController { @RequestMapping("/resource") EntityModel> resource() { - return new EntityModel<>(new HashMap<>(), new Link("self", "https://api.example.com")); + return EntityModel.of(new HashMap<>(), Link.of("self", LinkRelation.of("https://api.example.com"))); } @RequestMapping("/plain") diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/WebMvcTestAllControllersIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/WebMvcTestAllControllersIntegrationTests.java index e1cd442ef873..c5065ddd9946 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/WebMvcTestAllControllersIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/WebMvcTestAllControllersIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -70,7 +70,7 @@ void shouldRunValidationSuccess() throws Exception { } @Test - void shouldRunValidationFailure() throws Exception { + void shouldRunValidationFailure() { assertThatExceptionOfType(NestedServletException.class) .isThrownBy(() -> this.mvc.perform(get("/three/invalid"))) .withCauseInstanceOf(ConstraintViolationException.class); diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/WebMvcTestNestedIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/WebMvcTestNestedIntegrationTests.java new file mode 100644 index 000000000000..da07a8b3004f --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/WebMvcTestNestedIntegrationTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.web.servlet.mockmvc; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Tests for {@link WebMvcTest @WebMvcTest} using {@link Nested}. + * + * @author Andy Wilkinson + */ +@WebMvcTest(controllers = ExampleController2.class) +@WithMockUser +class WebMvcTestNestedIntegrationTests { + + @Autowired + private MockMvc mvc; + + @Test + void shouldNotFindController1() throws Exception { + this.mvc.perform(get("/one")).andExpect(status().isNotFound()); + } + + @Test + void shouldFindController2() throws Exception { + this.mvc.perform(get("/two")).andExpect(content().string("hellotwo")).andExpect(status().isOk()); + } + + @Nested + @WithMockUser + class NestedTests { + + @Test + void shouldNotFindController1() throws Exception { + WebMvcTestNestedIntegrationTests.this.mvc.perform(get("/one")).andExpect(status().isNotFound()); + } + + @Test + void shouldFindController2() throws Exception { + WebMvcTestNestedIntegrationTests.this.mvc.perform(get("/two")).andExpect(content().string("hellotwo")) + .andExpect(status().isOk()); + } + + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/WebMvcTestPrintDefaultIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/WebMvcTestPrintDefaultIntegrationTests.java index d932291a80bc..539cfda16731 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/WebMvcTestPrintDefaultIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/WebMvcTestPrintDefaultIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,15 +21,16 @@ import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.platform.engine.discovery.DiscoverySelectors; +import org.junit.platform.launcher.Launcher; +import org.junit.platform.launcher.LauncherDiscoveryRequest; +import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; +import org.junit.platform.launcher.core.LauncherFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; -import org.springframework.boot.testsupport.junit.platform.Launcher; -import org.springframework.boot.testsupport.junit.platform.LauncherDiscoveryRequest; -import org.springframework.boot.testsupport.junit.platform.LauncherDiscoveryRequestBuilder; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; @@ -45,26 +46,25 @@ * @author Andy Wilkinson */ @ExtendWith(OutputCaptureExtension.class) -@TestMethodOrder(MethodOrderer.Alphanumeric.class) +@TestMethodOrder(MethodOrderer.MethodName.class) class WebMvcTestPrintDefaultIntegrationTests { @Test - void shouldNotPrint(CapturedOutput output) throws Throwable { + void shouldNotPrint(CapturedOutput output) { executeTests(ShouldNotPrint.class); assertThat(output).doesNotContain("HTTP Method"); } @Test - void shouldPrint(CapturedOutput output) throws Throwable { + void shouldPrint(CapturedOutput output) { executeTests(ShouldPrint.class); assertThat(output).containsOnlyOnce("HTTP Method"); } - private void executeTests(Class testClass) throws Throwable { - ClassLoader classLoader = testClass.getClassLoader(); - LauncherDiscoveryRequest request = new LauncherDiscoveryRequestBuilder(classLoader) + private void executeTests(Class testClass) { + LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request() .selectors(DiscoverySelectors.selectClass(testClass)).build(); - Launcher launcher = new Launcher(testClass.getClassLoader()); + Launcher launcher = LauncherFactory.create(); launcher.execute(request); } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/WebMvcTestWebDriverCustomScopeIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/WebMvcTestWebDriverCustomScopeIntegrationTests.java index d56d4a1cb67b..66a5172f6dd3 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/WebMvcTestWebDriverCustomScopeIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/WebMvcTestWebDriverCustomScopeIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,7 +39,7 @@ * @author Phillip Webb */ @WebMvcTest -@TestMethodOrder(MethodOrderer.Alphanumeric.class) +@TestMethodOrder(MethodOrderer.MethodName.class) class WebMvcTestWebDriverCustomScopeIntegrationTests { // gh-7454 diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/WebMvcTestWebDriverIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/WebMvcTestWebDriverIntegrationTests.java index 9e9c341eb130..50faf940de1a 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/WebMvcTestWebDriverIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/mockmvc/WebMvcTestWebDriverIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,7 @@ */ @WebMvcTest @WithMockUser -@TestMethodOrder(MethodOrderer.Alphanumeric.class) +@TestMethodOrder(MethodOrderer.MethodName.class) class WebMvcTestWebDriverIntegrationTests { private static WebDriver previousWebDriver; diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/webservices/client/AutoConfigureMockWebServiceServerEnabledIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/webservices/client/AutoConfigureMockWebServiceServerEnabledIntegrationTests.java new file mode 100644 index 000000000000..3c5b35930b02 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/webservices/client/AutoConfigureMockWebServiceServerEnabledIntegrationTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.webservices.client; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.ws.test.client.MockWebServiceServer; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link AutoConfigureMockWebServiceServer @AutoConfigureMockWebServiceServer} + * with {@code enabled=false}. + * + * @author Dmytro Nosan + */ +@WebServiceClientTest +@AutoConfigureMockWebServiceServer(enabled = false) +class AutoConfigureMockWebServiceServerEnabledIntegrationTests { + + @Autowired + private ApplicationContext applicationContext; + + @Test + void mockWebServiceServerShouldNotBeRegistered() { + assertThatExceptionOfType(NoSuchBeanDefinitionException.class) + .isThrownBy(() -> this.applicationContext.getBean(MockWebServiceServer.class)); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/webservices/client/AutoConfigureWebServiceClientWebServiceTemplateIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/webservices/client/AutoConfigureWebServiceClientWebServiceTemplateIntegrationTests.java new file mode 100644 index 000000000000..bd7f5c2355ad --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/webservices/client/AutoConfigureWebServiceClientWebServiceTemplateIntegrationTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.webservices.client; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.ws.client.core.WebServiceTemplate; +import org.springframework.ws.test.client.MockWebServiceServer; +import org.springframework.xml.transform.StringSource; + +import static org.springframework.ws.test.client.RequestMatchers.payload; +import static org.springframework.ws.test.client.ResponseCreators.withPayload; + +/** + * Tests for {@link AutoConfigureWebServiceClient @AutoConfigureWebServiceClient} with + * {@code registerWebServiceTemplate=true}. + * + * @author Dmytro Nosan + */ +@SpringBootTest +@AutoConfigureWebServiceClient(registerWebServiceTemplate = true) +@AutoConfigureMockWebServiceServer +class AutoConfigureWebServiceClientWebServiceTemplateIntegrationTests { + + @Autowired + private WebServiceTemplate webServiceTemplate; + + @Autowired + private MockWebServiceServer server; + + @Test + void webServiceTemplateTest() { + this.server.expect(payload(new StringSource(""))) + .andRespond(withPayload(new StringSource(""))); + this.webServiceTemplate.marshalSendAndReceive("https://example.com", new Request()); + } + + @Configuration(proxyBeanMethods = false) + @Import(WebServiceMarshallerConfiguration.class) + static class Config { + + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/webservices/client/ExampleWebServiceClient.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/webservices/client/ExampleWebServiceClient.java new file mode 100644 index 000000000000..233313ea2f20 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/webservices/client/ExampleWebServiceClient.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.webservices.client; + +import org.springframework.boot.webservices.client.WebServiceTemplateBuilder; +import org.springframework.stereotype.Service; +import org.springframework.ws.client.core.WebServiceTemplate; + +/** + * Example web client used with {@link WebServiceClientTest @WebServiceClientTest} tests. + * + * @author Dmytro Nosan + */ +@Service +public class ExampleWebServiceClient { + + private final WebServiceTemplate webServiceTemplate; + + public ExampleWebServiceClient(WebServiceTemplateBuilder builder) { + this.webServiceTemplate = builder.build(); + } + + public Response test() { + return (Response) this.webServiceTemplate.marshalSendAndReceive("https://example.com", new Request()); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/webservices/client/ExampleWebServiceClientApplication.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/webservices/client/ExampleWebServiceClientApplication.java new file mode 100644 index 000000000000..7e74b64ba551 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/webservices/client/ExampleWebServiceClientApplication.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.webservices.client; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Import; + +/** + * Example {@link SpringBootApplication @SpringBootApplication} used with + * {@link WebServiceClientTest @WebServiceClientTest} tests. + * + * @author Dmytro Nosan + */ +@SpringBootApplication +@Import(WebServiceMarshallerConfiguration.class) +public class ExampleWebServiceClientApplication { + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/webservices/client/Request.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/webservices/client/Request.java new file mode 100644 index 000000000000..8fead68404dc --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/webservices/client/Request.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.webservices.client; + +import javax.xml.bind.annotation.XmlRootElement; + +/** + * Test request. + * + * @author Dmytro Nosan + */ +@XmlRootElement(name = "request") +class Request { + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/webservices/client/Response.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/webservices/client/Response.java new file mode 100644 index 000000000000..82e5f923e84d --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/webservices/client/Response.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.webservices.client; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * Test response. + * + * @author Dmytro Nosan + */ +@XmlRootElement(name = "response") +@XmlAccessorType(XmlAccessType.FIELD) +class Response { + + private int status; + + int getStatus() { + return this.status; + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/webservices/client/WebServiceClientIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/webservices/client/WebServiceClientIntegrationTests.java new file mode 100644 index 000000000000..2cb674e1cdc5 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/webservices/client/WebServiceClientIntegrationTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.webservices.client; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.ws.client.WebServiceTransportException; +import org.springframework.ws.test.client.MockWebServiceServer; +import org.springframework.ws.test.support.SourceAssertionError; +import org.springframework.xml.transform.StringSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.springframework.ws.test.client.RequestMatchers.connectionTo; +import static org.springframework.ws.test.client.RequestMatchers.payload; +import static org.springframework.ws.test.client.ResponseCreators.withError; +import static org.springframework.ws.test.client.ResponseCreators.withPayload; + +/** + * Tests for {@link WebServiceClientTest @WebServiceClientTest}. + * + * @author Dmytro Nosan + */ +@WebServiceClientTest(ExampleWebServiceClient.class) +class WebServiceClientIntegrationTests { + + @Autowired + private MockWebServiceServer server; + + @Autowired + private ExampleWebServiceClient client; + + @Test + void mockServerCall() { + this.server.expect(payload(new StringSource(""))) + .andRespond(withPayload(new StringSource("200"))); + assertThat(this.client.test()).extracting(Response::getStatus).isEqualTo(200); + } + + @Test + void mockServerCall1() { + this.server.expect(connectionTo("https://example1")).andRespond(withPayload(new StringSource(""))); + assertThatExceptionOfType(SourceAssertionError.class).isThrownBy(this.client::test) + .withMessageContaining("Unexpected connection expected"); + } + + @Test + void mockServerCall2() { + this.server.expect(payload(new StringSource(""))).andRespond(withError("Invalid Request")); + assertThatExceptionOfType(WebServiceTransportException.class).isThrownBy(this.client::test) + .withMessageContaining("Invalid Request"); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/webservices/client/WebServiceClientNoComponentIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/webservices/client/WebServiceClientNoComponentIntegrationTests.java new file mode 100644 index 000000000000..fdf01f2a1331 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/webservices/client/WebServiceClientNoComponentIntegrationTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.webservices.client; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.webservices.client.WebServiceTemplateBuilder; +import org.springframework.context.ApplicationContext; +import org.springframework.ws.test.client.MockWebServiceServer; +import org.springframework.xml.transform.StringSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.springframework.ws.test.client.RequestMatchers.payload; +import static org.springframework.ws.test.client.ResponseCreators.withPayload; + +/** + * Tests for {@link WebServiceClientTest @WebServiceClientTest} with no specific client. + * + * @author Dmytro Nosan + */ +@WebServiceClientTest +class WebServiceClientNoComponentIntegrationTests { + + @Autowired + private ApplicationContext applicationContext; + + @Autowired + private WebServiceTemplateBuilder webServiceTemplateBuilder; + + @Autowired + private MockWebServiceServer server; + + @Test + void exampleClientIsNotInjected() { + assertThatExceptionOfType(NoSuchBeanDefinitionException.class) + .isThrownBy(() -> this.applicationContext.getBean(ExampleWebServiceClient.class)); + } + + @Test + void manuallyCreateBean() { + ExampleWebServiceClient client = new ExampleWebServiceClient(this.webServiceTemplateBuilder); + this.server.expect(payload(new StringSource(""))) + .andRespond(withPayload(new StringSource("200"))); + assertThat(client.test()).extracting(Response::getStatus).isEqualTo(200); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/webservices/client/WebServiceClientPropertiesIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/webservices/client/WebServiceClientPropertiesIntegrationTests.java new file mode 100644 index 000000000000..b1d01217accb --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/webservices/client/WebServiceClientPropertiesIntegrationTests.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.webservices.client; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for the {@link WebServiceClientTest#properties properties} attribute of + * {@link WebServiceClientTest @WebServiceClientTest}. + * + * @author Dmytro Nosan + */ +@WebServiceClientTest(properties = "spring.profiles.active=test") +class WebServiceClientPropertiesIntegrationTests { + + @Autowired + private Environment environment; + + @Test + void environmentWithNewProfile() { + assertThat(this.environment.getActiveProfiles()).containsExactly("test"); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/webservices/client/WebServiceMarshallerConfiguration.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/webservices/client/WebServiceMarshallerConfiguration.java new file mode 100644 index 000000000000..559407b95903 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/webservices/client/WebServiceMarshallerConfiguration.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.webservices.client; + +import org.springframework.boot.webservices.client.WebServiceTemplateCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.oxm.Marshaller; +import org.springframework.oxm.Unmarshaller; +import org.springframework.oxm.jaxb.Jaxb2Marshaller; + +/** + * Test configuration to configure {@code Marshaller} and {@code Unmarshaller}. + * + * @author Dmytro Nosan + */ +@Configuration(proxyBeanMethods = false) +class WebServiceMarshallerConfiguration { + + @Bean + WebServiceTemplateCustomizer marshallerCustomizer(Marshaller marshaller) { + return (webServiceTemplate) -> webServiceTemplate.setMarshaller(marshaller); + } + + @Bean + WebServiceTemplateCustomizer unmarshallerCustomizer(Unmarshaller unmarshaller) { + return (webServiceTemplate) -> webServiceTemplate.setUnmarshaller(unmarshaller); + } + + @Bean + Jaxb2Marshaller createJaxbMarshaller() { + Jaxb2Marshaller jaxb2Marshaller = new Jaxb2Marshaller(); + jaxb2Marshaller.setClassesToBeBound(Request.class, Response.class); + return jaxb2Marshaller; + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/resources/org/springframework/boot/test/autoconfigure/data/r2dbc/schema.sql b/spring-boot-project/spring-boot-test-autoconfigure/src/test/resources/org/springframework/boot/test/autoconfigure/data/r2dbc/schema.sql new file mode 100644 index 000000000000..0d4a802b6b31 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/resources/org/springframework/boot/test/autoconfigure/data/r2dbc/schema.sql @@ -0,0 +1,2 @@ +drop table if exists example; +create table example (id int, name varchar); diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/resources/org/springframework/boot/test/autoconfigure/orm/jpa/schema.sql b/spring-boot-project/spring-boot-test-autoconfigure/src/test/resources/org/springframework/boot/test/autoconfigure/orm/jpa/schema.sql new file mode 100644 index 000000000000..e22c8c94cc26 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/resources/org/springframework/boot/test/autoconfigure/orm/jpa/schema.sql @@ -0,0 +1 @@ +CREATE TABLE example(identifier INT, name varchar(64)); diff --git a/spring-boot-project/spring-boot-test/build.gradle b/spring-boot-project/spring-boot-test/build.gradle new file mode 100644 index 000000000000..7571c2b34f98 --- /dev/null +++ b/spring-boot-project/spring-boot-test/build.gradle @@ -0,0 +1,60 @@ +plugins { + id "java-library" + id "org.jetbrains.kotlin.jvm" + id "org.springframework.boot.conventions" + id "org.springframework.boot.deployed" + id "org.springframework.boot.optional-dependencies" +} + +description = "Spring Boot Test" + +dependencies { + api(project(":spring-boot-project:spring-boot")) + + optional("com.fasterxml.jackson.core:jackson-databind") + optional("com.google.code.gson:gson") + optional("com.jayway.jsonpath:json-path") + optional("io.projectreactor.netty:reactor-netty-http") + optional("jakarta.json.bind:jakarta.json.bind-api") + optional("jakarta.servlet:jakarta.servlet-api") + optional("junit:junit") + optional("org.apache.httpcomponents:httpclient") { + exclude(group: "commons-logging", module: "commons-logging") + } + optional("org.assertj:assertj-core") + optional("org.hamcrest:hamcrest-core") + optional("org.hamcrest:hamcrest-library") + optional("org.jetbrains.kotlin:kotlin-stdlib") + optional("org.jetbrains.kotlin:kotlin-reflect") + optional("org.junit.jupiter:junit-jupiter-api") + optional("org.mockito:mockito-core") + optional("org.skyscreamer:jsonassert") + optional("org.seleniumhq.selenium:htmlunit-driver") { + exclude(group: "commons-logging", module: "commons-logging") + } + optional("org.seleniumhq.selenium:selenium-api") + optional("org.springframework:spring-test") + optional("org.springframework:spring-web") + optional("org.springframework:spring-webflux") + optional("net.sourceforge.htmlunit:htmlunit") { + exclude(group: "commons-logging", module: "commons-logging") + } + + testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) + testImplementation("io.mockk:mockk") + testImplementation("jakarta.json:jakarta.json-api") + testImplementation("ch.qos.logback:logback-classic") + testImplementation("org.apache.tomcat.embed:tomcat-embed-core") + testImplementation("org.codehaus.groovy:groovy") + testImplementation("org.codehaus.groovy:groovy-xml") + testImplementation("org.apache.johnzon:johnzon-jsonb") + testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.mockito:mockito-junit-jupiter") + testImplementation("org.slf4j:slf4j-api") + testImplementation("org.spockframework:spock-core") + testImplementation("org.springframework:spring-webmvc") + testImplementation("org.testng:testng") + + testRuntimeOnly("org.junit.vintage:junit-vintage-engine") +} + diff --git a/spring-boot-project/spring-boot-test/pom.xml b/spring-boot-project/spring-boot-test/pom.xml deleted file mode 100644 index 2e59fa5bdebf..000000000000 --- a/spring-boot-project/spring-boot-test/pom.xml +++ /dev/null @@ -1,288 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-parent - ${revision} - ../spring-boot-parent - - spring-boot-test - Spring Boot Test - Spring Boot Test - - ${basedir}/../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - - org.springframework.boot - spring-boot - - - - com.fasterxml.jackson.core - jackson-databind - true - - - com.google.code.gson - gson - true - - - com.jayway.jsonpath - json-path - true - - - io.projectreactor.netty - reactor-netty - true - - - jakarta.json.bind - jakarta.json.bind-api - true - - - jakarta.servlet - jakarta.servlet-api - true - - - junit - junit - true - - - org.hamcrest - hamcrest-core - - - - - org.apache.httpcomponents - httpclient - true - - - org.assertj - assertj-core - true - - - org.hamcrest - hamcrest - true - - - org.jetbrains.kotlin - kotlin-stdlib - true - - - org.jetbrains.kotlin - kotlin-reflect - true - - - org.junit.jupiter - junit-jupiter-api - true - - - org.mockito - mockito-core - true - - - org.skyscreamer - jsonassert - true - - - org.seleniumhq.selenium - htmlunit-driver - true - - - org.seleniumhq.selenium - selenium-api - true - - - org.springframework - spring-test - true - - - org.springframework - spring-web - true - - - org.springframework - spring-webflux - true - - - net.sourceforge.htmlunit - htmlunit - true - - - - org.springframework.boot - spring-boot-test-support - test - - - ch.qos.logback - logback-classic - test - - - io.mockk - mockk - test - - - jakarta.json - jakarta.json-api - test - - - org.apache.tomcat.embed - tomcat-embed-core - test - - - org.codehaus.groovy - groovy - test - - - org.codehaus.groovy - groovy-xml - true - test - - - org.apache.johnzon - johnzon-jsonb - test - - - org.slf4j - slf4j-api - test - - - org.spockframework - spock-core - test - - - org.springframework - spring-webmvc - test - - - org.testng - testng - test - - - - - - org.apache.maven.plugins - maven-jar-plugin - - - - test-jar - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - - org.apache.maven.surefire - surefire-junit-platform - ${maven-surefire-plugin.version} - - - - - org.jetbrains.kotlin - kotlin-maven-plugin - - - compile - compile - - compile - - - - ${project.basedir}/src/main/kotlin - ${project.basedir}/src/main/java - - - - - test-compile - test-compile - - test-compile - - - - ${project.basedir}/src/test/kotlin - ${project.basedir}/src/test/java - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - - default-compile - none - - - default-testCompile - none - - - java-compile - compile - - compile - - - - java-test-compile - test-compile - - testCompile - - - - - - - diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ConfigDataApplicationContextInitializer.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ConfigDataApplicationContextInitializer.java new file mode 100644 index 000000000000..a7b0adbe4b7b --- /dev/null +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ConfigDataApplicationContextInitializer.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.context; + +import org.springframework.boot.DefaultBootstrapContext; +import org.springframework.boot.DefaultPropertiesPropertySource; +import org.springframework.boot.context.config.ConfigData; +import org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor; +import org.springframework.boot.env.RandomValuePropertySource; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.test.context.ContextConfiguration; + +/** + * {@link ApplicationContextInitializer} that can be used with the + * {@link ContextConfiguration#initializers()} to trigger loading of {@link ConfigData} + * such as {@literal application.properties}. + * + * @author Phillip Webb + * @since 2.4.0 + * @see ConfigDataEnvironmentPostProcessor + */ +public class ConfigDataApplicationContextInitializer + implements ApplicationContextInitializer { + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + ConfigurableEnvironment environment = applicationContext.getEnvironment(); + RandomValuePropertySource.addToEnvironment(environment); + DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext(); + ConfigDataEnvironmentPostProcessor.applyTo(environment, applicationContext, bootstrapContext); + bootstrapContext.close(applicationContext); + DefaultPropertiesPropertySource.moveToEnd(environment); + } + +} diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ConfigFileApplicationContextInitializer.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ConfigFileApplicationContextInitializer.java index b94707093360..02f3ab0ac15b 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ConfigFileApplicationContextInitializer.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ConfigFileApplicationContextInitializer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.springframework.boot.test.context; -import org.springframework.boot.context.config.ConfigFileApplicationListener; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.test.context.ContextConfiguration; @@ -28,14 +27,17 @@ * * @author Phillip Webb * @since 1.4.0 - * @see ConfigFileApplicationListener + * @see org.springframework.boot.context.config.ConfigFileApplicationListener + * @deprecated since 2.4.0 for removal in 2.6.0 in favor of + * {@link ConfigDataApplicationContextInitializer} */ +@Deprecated public class ConfigFileApplicationContextInitializer implements ApplicationContextInitializer { @Override public void initialize(ConfigurableApplicationContext applicationContext) { - new ConfigFileApplicationListener() { + new org.springframework.boot.context.config.ConfigFileApplicationListener() { public void apply() { addPropertySources(applicationContext.getEnvironment(), applicationContext); addPostProcessors(applicationContext); diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/FilteredClassLoader.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/FilteredClassLoader.java index e8d577cf7065..5d6a8236c39f 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/FilteredClassLoader.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/FilteredClassLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,12 +20,14 @@ import java.io.InputStream; import java.net.URL; import java.net.URLClassLoader; +import java.security.ProtectionDomain; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.function.Predicate; +import org.springframework.core.SmartClassLoader; import org.springframework.core.io.ClassPathResource; /** @@ -37,7 +39,7 @@ * @author Roy Jacobs * @since 2.0.0 */ -public class FilteredClassLoader extends URLClassLoader { +public class FilteredClassLoader extends URLClassLoader implements SmartClassLoader { private final Collection> classesFilters; @@ -77,6 +79,7 @@ public FilteredClassLoader(ClassPathResource... hiddenResources) { * name of a class or a resource name. */ @SafeVarargs + @SuppressWarnings("varargs") public FilteredClassLoader(Predicate... filters) { this(Arrays.asList(filters), Arrays.asList(filters)); } @@ -128,6 +131,16 @@ public InputStream getResourceAsStream(String name) { return super.getResourceAsStream(name); } + @Override + public Class publicDefineClass(String name, byte[] b, ProtectionDomain protectionDomain) { + for (Predicate filter : this.classesFilters) { + if (filter.test(name)) { + throw new IllegalArgumentException(String.format("Defining class with name %s is not supported", name)); + } + } + return defineClass(name, b, 0, b.length, protectionDomain); + } + /** * Filter to restrict the classes that can be loaded. */ diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java index 3d33b764ce5e..97e1b3b4ffc3 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -222,6 +222,7 @@ static class ContextCustomizerKey { filters.add(new JavaLangAnnotationFilter()); filters.add(new KotlinAnnotationFilter()); filters.add(new SpockAnnotationFilter()); + filters.add(new JunitAnnotationFilter()); ANNOTATION_FILTERS = Collections.unmodifiableSet(filters); } @@ -384,4 +385,16 @@ public boolean isIgnored(Annotation annotation) { } + /** + * {@link AnnotationFilter} for JUnit annotations. + */ + private static final class JunitAnnotationFilter implements AnnotationFilter { + + @Override + public boolean isIgnored(Annotation annotation) { + return annotation.annotationType().getName().startsWith("org.junit."); + } + + } + } diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizerFactory.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizerFactory.java index 2cf04e1caae8..11c1889de8ac 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizerFactory.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,10 +22,11 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.core.annotation.MergedAnnotations; -import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.ContextCustomizerFactory; +import org.springframework.test.context.TestContextAnnotationUtils; +import org.springframework.test.context.TestContextAnnotationUtils.AnnotationDescriptor; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; @@ -41,9 +42,11 @@ class ImportsContextCustomizerFactory implements ContextCustomizerFactory { @Override public ContextCustomizer createContextCustomizer(Class testClass, List configAttributes) { - if (MergedAnnotations.from(testClass, SearchStrategy.TYPE_HIERARCHY).isPresent(Import.class)) { - assertHasNoBeanMethods(testClass); - return new ImportsContextCustomizer(testClass); + AnnotationDescriptor descriptor = TestContextAnnotationUtils.findAnnotationDescriptor(testClass, + Import.class); + if (descriptor != null) { + assertHasNoBeanMethods(descriptor.getRootDeclaringClass()); + return new ImportsContextCustomizer(descriptor.getRootDeclaringClass()); } return null; } diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java index ed385431a45a..95b27ca8cede 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,29 +18,35 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import org.springframework.beans.BeanUtils; +import org.springframework.boot.ApplicationContextFactory; +import org.springframework.boot.DefaultPropertiesPropertySource; import org.springframework.boot.SpringApplication; import org.springframework.boot.WebApplicationType; -import org.springframework.boot.context.properties.bind.Bindable; -import org.springframework.boot.context.properties.bind.Binder; -import org.springframework.boot.context.properties.source.ConfigurationPropertySource; -import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; +import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.mock.web.SpringBootMockServletContext; import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.boot.test.util.TestPropertyValues.Type; import org.springframework.boot.web.reactive.context.GenericReactiveWebApplicationContext; import org.springframework.boot.web.servlet.support.ServletContextApplicationContextInitializer; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.Ordered; +import org.springframework.core.PriorityOrdered; import org.springframework.core.SpringVersion; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; import org.springframework.core.annotation.Order; +import org.springframework.core.env.CommandLinePropertySource; import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySource; import org.springframework.core.env.StandardEnvironment; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.ResourceLoader; @@ -54,6 +60,7 @@ import org.springframework.test.context.web.WebMergedContextConfiguration; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; import org.springframework.web.context.support.GenericWebApplicationContext; @@ -76,12 +83,14 @@ * @author Andy Wilkinson * @author Stephane Nicoll * @author Madhura Bhave + * @author Scott Frederick * @since 1.4.0 * @see SpringBootTest */ public class SpringBootContextLoader extends AbstractContextLoader { - private static final String[] NO_ARGS = new String[0]; + private static final String[] PRIORITY_PROPERTY_SOURCES = { "configurationProperties", + DefaultPropertiesPropertySource.NAME, CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME }; @Override public ApplicationContext loadContext(MergedContextConfiguration config) throws Exception { @@ -95,16 +104,6 @@ public ApplicationContext loadContext(MergedContextConfiguration config) throws application.setMainApplicationClass(config.getTestClass()); application.addPrimarySources(Arrays.asList(configClasses)); application.getSources().addAll(Arrays.asList(configLocations)); - ConfigurableEnvironment environment = getEnvironment(); - if (!ObjectUtils.isEmpty(config.getActiveProfiles())) { - setActiveProfiles(environment, config.getActiveProfiles()); - } - ResourceLoader resourceLoader = (application.getResourceLoader() != null) ? application.getResourceLoader() - : new DefaultResourceLoader(getClass().getClassLoader()); - TestPropertySourceUtils.addPropertiesFilesToEnvironment(environment, resourceLoader, - config.getPropertySourceLocations()); - TestPropertySourceUtils.addInlinedPropertiesToEnvironment(environment, getInlinedProperties(config)); - application.setEnvironment(environment); List> initializers = getInitializers(config, application); if (config instanceof WebMergedContextConfiguration) { application.setWebApplicationType(WebApplicationType.SERVLET); @@ -115,14 +114,64 @@ public ApplicationContext loadContext(MergedContextConfiguration config) throws else if (config instanceof ReactiveWebMergedContextConfiguration) { application.setWebApplicationType(WebApplicationType.REACTIVE); if (!isEmbeddedWebEnvironment(config)) { - new ReactiveWebConfigurer().configure(application); + application.setApplicationContextFactory( + ApplicationContextFactory.of(GenericReactiveWebApplicationContext::new)); } } else { application.setWebApplicationType(WebApplicationType.NONE); } application.setInitializers(initializers); - return application.run(getArgs(config)); + boolean customEnvironent = ReflectionUtils.findMethod(getClass(), "getEnvironment") + .getDeclaringClass() != SpringBootContextLoader.class; + if (customEnvironent) { + ConfigurableEnvironment environment = getEnvironment(); + prepareEnvironment(config, application, environment, false); + application.setEnvironment(environment); + } + else { + application.addListeners(new PrepareEnvironmentListener(config)); + } + String[] args = SpringBootTestArgs.get(config.getContextCustomizers()); + return application.run(args); + } + + private void prepareEnvironment(MergedContextConfiguration config, SpringApplication application, + ConfigurableEnvironment environment, boolean applicationEnvironment) { + MutablePropertySources propertySources = environment.getPropertySources(); + List> priorityPropertySources = new ArrayList<>(); + if (applicationEnvironment) { + for (String priorityPropertySourceName : PRIORITY_PROPERTY_SOURCES) { + PropertySource priorityPropertySource = propertySources.get(priorityPropertySourceName); + if (priorityPropertySource != null) { + priorityPropertySources.add(priorityPropertySource); + propertySources.remove(priorityPropertySourceName); + } + } + } + setActiveProfiles(environment, config.getActiveProfiles(), applicationEnvironment); + ResourceLoader resourceLoader = (application.getResourceLoader() != null) ? application.getResourceLoader() + : new DefaultResourceLoader(null); + TestPropertySourceUtils.addPropertiesFilesToEnvironment(environment, resourceLoader, + config.getPropertySourceLocations()); + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(environment, getInlinedProperties(config)); + Collections.reverse(priorityPropertySources); + priorityPropertySources.forEach(propertySources::addFirst); + } + + private void setActiveProfiles(ConfigurableEnvironment environment, String[] profiles, + boolean applicationEnvironment) { + if (ObjectUtils.isEmpty(profiles)) { + return; + } + if (!applicationEnvironment) { + environment.setActiveProfiles(profiles); + } + String[] pairs = new String[profiles.length]; + for (int i = 0; i < profiles.length; i++) { + pairs[i] = "spring.profiles.active[" + i + "]=" + profiles[i]; + } + TestPropertyValues.of(pairs).applyTo(environment, Type.MAP, "active-test-profiles"); } /** @@ -143,31 +192,11 @@ protected ConfigurableEnvironment getEnvironment() { return new StandardEnvironment(); } - /** - * Return the application arguments to use. If no arguments are available, return an - * empty array. - * @param config the source context configuration - * @return the application arguments to use - * @see SpringApplication#run(String...) - */ - protected String[] getArgs(MergedContextConfiguration config) { - return MergedAnnotations.from(config.getTestClass(), SearchStrategy.TYPE_HIERARCHY).get(SpringBootTest.class) - .getValue("args", String[].class).orElse(NO_ARGS); - } - - private void setActiveProfiles(ConfigurableEnvironment environment, String[] profiles) { - TestPropertyValues.of("spring.profiles.active=" + StringUtils.arrayToCommaDelimitedString(profiles)) - .applyTo(environment); - } - protected String[] getInlinedProperties(MergedContextConfiguration config) { ArrayList properties = new ArrayList<>(); // JMX bean names will clash if the same bean is used in multiple contexts disableJmx(properties); properties.addAll(Arrays.asList(config.getPropertySourceProperties())); - if (!isEmbeddedWebEnvironment(config) && !hasCustomServerPort(properties)) { - properties.add("server.port=-1"); - } return StringUtils.toStringArray(properties); } @@ -175,16 +204,6 @@ private void disableJmx(List properties) { properties.add("spring.jmx.enabled=false"); } - private boolean hasCustomServerPort(List properties) { - Binder binder = new Binder(convertToConfigurationPropertySource(properties)); - return binder.bind("server.port", Bindable.of(String.class)).isBound(); - } - - private ConfigurationPropertySource convertToConfigurationPropertySource(List properties) { - return new MapConfigurationPropertySource( - TestPropertySourceUtils.convertInlinedPropertiesToMap(StringUtils.toStringArray(properties))); - } - /** * Return the {@link ApplicationContextInitializer initializers} that will be applied * to the context. By default this method will adapt {@link ContextCustomizer context @@ -262,13 +281,11 @@ protected String getResourceSuffix() { */ private static class WebConfigurer { - private static final Class WEB_CONTEXT_CLASS = GenericWebApplicationContext.class; - void configure(MergedContextConfiguration configuration, SpringApplication application, List> initializers) { WebMergedContextConfiguration webConfiguration = (WebMergedContextConfiguration) configuration; addMockServletContext(initializers, webConfiguration); - application.setApplicationContextClass(WEB_CONTEXT_CLASS); + application.setApplicationContextFactory((webApplicationType) -> new GenericWebApplicationContext()); } private void addMockServletContext(List> initializers, @@ -280,19 +297,6 @@ private void addMockServletContext(List> initia } - /** - * Inner class to configure {@link ReactiveWebMergedContextConfiguration}. - */ - private static class ReactiveWebConfigurer { - - private static final Class WEB_CONTEXT_CLASS = GenericReactiveWebApplicationContext.class; - - void configure(SpringApplication application) { - application.setApplicationContextClass(WEB_CONTEXT_CLASS); - } - - } - /** * Adapts a {@link ContextCustomizer} to a {@link ApplicationContextInitializer} so * that it can be triggered via {@link SpringApplication}. @@ -316,6 +320,9 @@ public void initialize(ConfigurableApplicationContext applicationContext) { } + /** + * {@link ApplicationContextInitializer} used to set the parent context. + */ @Order(Ordered.HIGHEST_PRECEDENCE) private static class ParentContextApplicationContextInitializer implements ApplicationContextInitializer { @@ -333,4 +340,28 @@ public void initialize(ConfigurableApplicationContext applicationContext) { } + /** + * {@link ApplicationListener} used to prepare the application created environment. + */ + private class PrepareEnvironmentListener + implements ApplicationListener, PriorityOrdered { + + private final MergedContextConfiguration config; + + PrepareEnvironmentListener(MergedContextConfiguration config) { + this.config = config; + } + + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE; + } + + @Override + public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { + prepareEnvironment(this.config, event.getSpringApplication(), event.getEnvironment(), true); + } + + } + } diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTest.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTest.java index 4ac2071f76fc..d069745393f4 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTest.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -101,6 +101,7 @@ * @return the application arguments to pass to the application under test. * @see ApplicationArguments * @see SpringApplication#run(String...) + * @since 2.2.0 */ String[] args() default {}; diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestArgs.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestArgs.java new file mode 100644 index 000000000000..750754cb25be --- /dev/null +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestArgs.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.context; + +import java.util.Arrays; +import java.util.Set; + +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.test.context.ContextCustomizer; +import org.springframework.test.context.MergedContextConfiguration; + +/** + * {@link ContextCustomizer} to track application arguments that are used in a + * {@link SpringBootTest}. The application arguments are taken into account when + * evaluating a {@link MergedContextConfiguration} to determine if a context can be shared + * between tests. + * + * @author Madhura Bhave + */ +class SpringBootTestArgs implements ContextCustomizer { + + private static final String[] NO_ARGS = new String[0]; + + private final String[] args; + + SpringBootTestArgs(Class testClass) { + this.args = MergedAnnotations.from(testClass, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY) + .get(SpringBootTest.class).getValue("args", String[].class).orElse(NO_ARGS); + } + + @Override + public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) { + } + + String[] getArgs() { + return this.args; + } + + @Override + public boolean equals(Object obj) { + return (obj != null) && (getClass() == obj.getClass()) + && Arrays.equals(this.args, ((SpringBootTestArgs) obj).args); + } + + @Override + public int hashCode() { + return Arrays.hashCode(this.args); + } + + /** + * Return the application arguments from the given customizers. + * @param customizers the customizers to check + * @return the application args or an empty array + */ + static String[] get(Set customizers) { + for (ContextCustomizer customizer : customizers) { + if (customizer instanceof SpringBootTestArgs) { + return ((SpringBootTestArgs) customizer).args; + } + } + return NO_ARGS; + } + +} diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestContextBootstrapper.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestContextBootstrapper.java index 9301f8649607..f521e0b815ce 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestContextBootstrapper.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestContextBootstrapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -40,10 +41,12 @@ import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfigurationAttributes; +import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.ContextHierarchy; import org.springframework.test.context.ContextLoader; import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.TestContext; +import org.springframework.test.context.TestContextAnnotationUtils; import org.springframework.test.context.TestContextBootstrapper; import org.springframework.test.context.TestExecutionListener; import org.springframework.test.context.support.DefaultTestContextBootstrapper; @@ -132,7 +135,7 @@ protected ContextLoader resolveContextLoader(Class testClass, } private void addConfigAttributesClasses(ContextConfigurationAttributes configAttributes, Class[] classes) { - List> combined = new ArrayList<>(Arrays.asList(classes)); + Set> combined = new LinkedHashSet<>(Arrays.asList(classes)); if (configAttributes.getClasses() != null) { combined.addAll(Arrays.asList(configAttributes.getClasses())); } @@ -316,8 +319,7 @@ protected String[] getProperties(Class testClass) { } protected SpringBootTest getAnnotation(Class testClass) { - return MergedAnnotations.from(testClass, SearchStrategy.INHERITED_ANNOTATIONS).get(SpringBootTest.class) - .synthesize(MergedAnnotation::isPresent).orElse(null); + return TestContextAnnotationUtils.findMergedAnnotation(testClass, SpringBootTest.class); } protected void verifyConfiguration(Class testClass) { @@ -355,11 +357,13 @@ protected final MergedContextConfiguration createModifiedConfig(MergedContextCon */ protected final MergedContextConfiguration createModifiedConfig(MergedContextConfiguration mergedConfig, Class[] classes, String[] propertySourceProperties) { + Set contextCustomizers = new LinkedHashSet<>(mergedConfig.getContextCustomizers()); + contextCustomizers.add(new SpringBootTestArgs(mergedConfig.getTestClass())); + contextCustomizers.add(new SpringBootTestWebEnvironment(mergedConfig.getTestClass())); return new MergedContextConfiguration(mergedConfig.getTestClass(), mergedConfig.getLocations(), classes, mergedConfig.getContextInitializerClasses(), mergedConfig.getActiveProfiles(), - mergedConfig.getPropertySourceLocations(), propertySourceProperties, - mergedConfig.getContextCustomizers(), mergedConfig.getContextLoader(), - getCacheAwareContextLoaderDelegate(), mergedConfig.getParent()); + mergedConfig.getPropertySourceLocations(), propertySourceProperties, contextCustomizers, + mergedConfig.getContextLoader(), getCacheAwareContextLoaderDelegate(), mergedConfig.getParent()); } } diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestWebEnvironment.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestWebEnvironment.java new file mode 100644 index 000000000000..39d91fdebf45 --- /dev/null +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestWebEnvironment.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.context; + +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.test.context.ContextCustomizer; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.context.TestContextAnnotationUtils; + +/** + * {@link ContextCustomizer} to track the web environment that is used in a + * {@link SpringBootTest}. The web environment is taken into account when evaluating a + * {@link MergedContextConfiguration} to determine if a context can be shared between + * tests. + * + * @author Andy Wilkinson + */ +class SpringBootTestWebEnvironment implements ContextCustomizer { + + private final WebEnvironment webEnvironment; + + SpringBootTestWebEnvironment(Class testClass) { + SpringBootTest sprintBootTest = TestContextAnnotationUtils.findMergedAnnotation(testClass, + SpringBootTest.class); + this.webEnvironment = (sprintBootTest != null) ? sprintBootTest.webEnvironment() : null; + } + + @Override + public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) { + } + + @Override + public boolean equals(Object obj) { + return (obj != null) && (getClass() == obj.getClass()) + && this.webEnvironment == ((SpringBootTestWebEnvironment) obj).webEnvironment; + } + + @Override + public int hashCode() { + return (this.webEnvironment != null) ? this.webEnvironment.hashCode() : 0; + } + +} diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/filter/TestTypeExcludeFilter.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/filter/TestTypeExcludeFilter.java index dbaf925c110e..696d3e29ab38 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/filter/TestTypeExcludeFilter.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/filter/TestTypeExcludeFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -62,6 +62,16 @@ public boolean match(MetadataReader metadataReader, MetadataReaderFactory metada return false; } + @Override + public boolean equals(Object obj) { + return (obj != null) && (getClass() == obj.getClass()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + private boolean isTestConfiguration(MetadataReader metadataReader) { return (metadataReader.getAnnotationMetadata().isAnnotated(TestComponent.class.getName())); } diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunner.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunner.java index 8e43214ec260..003b6d2e2eff 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunner.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunner.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,9 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionCustomizer; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.boot.context.annotation.Configurations; import org.springframework.boot.context.annotation.UserConfigurations; import org.springframework.boot.test.context.FilteredClassLoader; @@ -105,6 +107,8 @@ public abstract class AbstractApplicationContextRunner contextFactory; + private final boolean allowBeanDefinitionOverriding; + private final List> initializers; private final TestPropertyValues environmentProperties; @@ -124,13 +128,14 @@ public abstract class AbstractApplicationContextRunner contextFactory) { - this(contextFactory, Collections.emptyList(), TestPropertyValues.empty(), TestPropertyValues.empty(), null, - null, Collections.emptyList(), Collections.emptyList()); + this(contextFactory, false, Collections.emptyList(), TestPropertyValues.empty(), TestPropertyValues.empty(), + null, null, Collections.emptyList(), Collections.emptyList()); } /** * Create a new {@link AbstractApplicationContextRunner} instance. * @param contextFactory the factory used to create the actual context + * @param allowBeanDefinitionOverriding whether bean definition overriding is allowed * @param initializers the initializers * @param environmentProperties the environment properties * @param systemProperties the system properties @@ -139,7 +144,7 @@ protected AbstractApplicationContextRunner(Supplier contextFactory) { * @param beanRegistrations the bean registrations * @param configurations the configuration */ - protected AbstractApplicationContextRunner(Supplier contextFactory, + protected AbstractApplicationContextRunner(Supplier contextFactory, boolean allowBeanDefinitionOverriding, List> initializers, TestPropertyValues environmentProperties, TestPropertyValues systemProperties, ClassLoader classLoader, ApplicationContext parent, List> beanRegistrations, List configurations) { @@ -149,6 +154,7 @@ protected AbstractApplicationContextRunner(Supplier contextFactory, Assert.notNull(configurations, "Configurations must not be null"); Assert.notNull(initializers, "Initializers must not be null"); this.contextFactory = contextFactory; + this.allowBeanDefinitionOverriding = allowBeanDefinitionOverriding; this.initializers = Collections.unmodifiableList(initializers); this.environmentProperties = environmentProperties; this.systemProperties = systemProperties; @@ -159,15 +165,30 @@ protected AbstractApplicationContextRunner(Supplier contextFactory, } /** - * Add a {@link ApplicationContextInitializer} to be called when the context is + * Specify if bean definition overriding, by registering a definition with the same + * name as an existing definition, should be allowed. + * @param allowBeanDefinitionOverriding if bean overriding is allowed + * @return a new instance with the updated bean definition overriding policy + * @since 2.3.0 + * @see DefaultListableBeanFactory#setAllowBeanDefinitionOverriding(boolean) + */ + public SELF withAllowBeanDefinitionOverriding(boolean allowBeanDefinitionOverriding) { + return newInstance(this.contextFactory, allowBeanDefinitionOverriding, this.initializers, + this.environmentProperties, this.systemProperties, this.classLoader, this.parent, + this.beanRegistrations, this.configurations); + } + + /** + * Add an {@link ApplicationContextInitializer} to be called when the context is * created. * @param initializer the initializer to add * @return a new instance with the updated initializers */ public SELF withInitializer(ApplicationContextInitializer initializer) { Assert.notNull(initializer, "Initializer must not be null"); - return newInstance(this.contextFactory, add(this.initializers, initializer), this.environmentProperties, - this.systemProperties, this.classLoader, this.parent, this.beanRegistrations, this.configurations); + return newInstance(this.contextFactory, this.allowBeanDefinitionOverriding, add(this.initializers, initializer), + this.environmentProperties, this.systemProperties, this.classLoader, this.parent, + this.beanRegistrations, this.configurations); } /** @@ -181,8 +202,9 @@ public SELF withInitializer(ApplicationContextInitializer initializer * @see #withSystemProperties(String...) */ public SELF withPropertyValues(String... pairs) { - return newInstance(this.contextFactory, this.initializers, this.environmentProperties.and(pairs), - this.systemProperties, this.classLoader, this.parent, this.beanRegistrations, this.configurations); + return newInstance(this.contextFactory, this.allowBeanDefinitionOverriding, this.initializers, + this.environmentProperties.and(pairs), this.systemProperties, this.classLoader, this.parent, + this.beanRegistrations, this.configurations); } /** @@ -196,9 +218,9 @@ public SELF withPropertyValues(String... pairs) { * @see #withSystemProperties(String...) */ public SELF withSystemProperties(String... pairs) { - return newInstance(this.contextFactory, this.initializers, this.environmentProperties, - this.systemProperties.and(pairs), this.classLoader, this.parent, this.beanRegistrations, - this.configurations); + return newInstance(this.contextFactory, this.allowBeanDefinitionOverriding, this.initializers, + this.environmentProperties, this.systemProperties.and(pairs), this.classLoader, this.parent, + this.beanRegistrations, this.configurations); } /** @@ -209,8 +231,9 @@ public SELF withSystemProperties(String... pairs) { * @see FilteredClassLoader */ public SELF withClassLoader(ClassLoader classLoader) { - return newInstance(this.contextFactory, this.initializers, this.environmentProperties, this.systemProperties, - classLoader, this.parent, this.beanRegistrations, this.configurations); + return newInstance(this.contextFactory, this.allowBeanDefinitionOverriding, this.initializers, + this.environmentProperties, this.systemProperties, classLoader, this.parent, this.beanRegistrations, + this.configurations); } /** @@ -220,8 +243,9 @@ public SELF withClassLoader(ClassLoader classLoader) { * @return a new instance with the updated parent */ public SELF withParent(ApplicationContext parent) { - return newInstance(this.contextFactory, this.initializers, this.environmentProperties, this.systemProperties, - this.classLoader, parent, this.beanRegistrations, this.configurations); + return newInstance(this.contextFactory, this.allowBeanDefinitionOverriding, this.initializers, + this.environmentProperties, this.systemProperties, this.classLoader, parent, this.beanRegistrations, + this.configurations); } /** @@ -256,8 +280,8 @@ public SELF withBean(Class type, Object... constructorArgs) { * @return a new instance with the updated bean */ public SELF withBean(String name, Class type, Object... constructorArgs) { - return newInstance(this.contextFactory, this.initializers, this.environmentProperties, this.systemProperties, - this.classLoader, this.parent, + return newInstance(this.contextFactory, this.allowBeanDefinitionOverriding, this.initializers, + this.environmentProperties, this.systemProperties, this.classLoader, this.parent, add(this.beanRegistrations, new BeanRegistration<>(name, type, constructorArgs)), this.configurations); } @@ -296,8 +320,8 @@ public SELF withBean(Class type, Supplier supplier, BeanDefinitionCust */ public SELF withBean(String name, Class type, Supplier supplier, BeanDefinitionCustomizer... customizers) { - return newInstance(this.contextFactory, this.initializers, this.environmentProperties, this.systemProperties, - this.classLoader, this.parent, + return newInstance(this.contextFactory, this.allowBeanDefinitionOverriding, this.initializers, + this.environmentProperties, this.systemProperties, this.classLoader, this.parent, add(this.beanRegistrations, new BeanRegistration<>(name, type, supplier, customizers)), this.configurations); } @@ -319,8 +343,9 @@ public SELF withUserConfiguration(Class... configurationClasses) { */ public SELF withConfiguration(Configurations configurations) { Assert.notNull(configurations, "Configurations must not be null"); - return newInstance(this.contextFactory, this.initializers, this.environmentProperties, this.systemProperties, - this.classLoader, this.parent, this.beanRegistrations, add(this.configurations, configurations)); + return newInstance(this.contextFactory, this.allowBeanDefinitionOverriding, this.initializers, + this.environmentProperties, this.systemProperties, this.classLoader, this.parent, + this.beanRegistrations, add(this.configurations, configurations)); } /** @@ -339,7 +364,7 @@ private List add(List list, T element) { return result; } - protected abstract SELF newInstance(Supplier contextFactory, + protected abstract SELF newInstance(Supplier contextFactory, boolean allowBeanDefinitionOverriding, List> initializers, TestPropertyValues environmentProperties, TestPropertyValues systemProperties, ClassLoader classLoader, ApplicationContext parent, List> beanRegistrations, List configurations); @@ -353,14 +378,12 @@ protected abstract SELF newInstance(Supplier contextFactory, */ @SuppressWarnings("unchecked") public SELF run(ContextConsumer consumer) { - withContextClassLoader(this.classLoader, () -> { - this.systemProperties.applyToSystemProperties(() -> { - try (A context = createAssertableContext()) { - accept(consumer, context); - } - return null; - }); - }); + withContextClassLoader(this.classLoader, () -> this.systemProperties.applyToSystemProperties(() -> { + try (A context = createAssertableContext()) { + accept(consumer, context); + } + return null; + })); return (SELF) this; } @@ -391,6 +414,11 @@ private A createAssertableContext() { private C createAndLoadContext() { C context = this.contextFactory.get(); + ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); + if (beanFactory instanceof DefaultListableBeanFactory) { + ((DefaultListableBeanFactory) beanFactory) + .setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding); + } try { configureContext(context); return context; diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/ApplicationContextRunner.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/ApplicationContextRunner.java index a45fd753c23b..feb9416b6c0a 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/ApplicationContextRunner.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/ApplicationContextRunner.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -59,22 +59,24 @@ public ApplicationContextRunner(Supplier context } private ApplicationContextRunner(Supplier contextFactory, + boolean allowBeanDefinitionOverriding, List> initializers, TestPropertyValues environmentProperties, TestPropertyValues systemProperties, ClassLoader classLoader, ApplicationContext parent, List> beanRegistrations, List configurations) { - super(contextFactory, initializers, environmentProperties, systemProperties, classLoader, parent, - beanRegistrations, configurations); + super(contextFactory, allowBeanDefinitionOverriding, initializers, environmentProperties, systemProperties, + classLoader, parent, beanRegistrations, configurations); } @Override protected ApplicationContextRunner newInstance(Supplier contextFactory, + boolean allowBeanDefinitionOverriding, List> initializers, TestPropertyValues environmentProperties, TestPropertyValues systemProperties, ClassLoader classLoader, ApplicationContext parent, List> beanRegistrations, List configurations) { - return new ApplicationContextRunner(contextFactory, initializers, environmentProperties, systemProperties, - classLoader, parent, beanRegistrations, configurations); + return new ApplicationContextRunner(contextFactory, allowBeanDefinitionOverriding, initializers, + environmentProperties, systemProperties, classLoader, parent, beanRegistrations, configurations); } } diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/ContextConsumer.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/ContextConsumer.java index 3970a438d216..7a5849d07db0 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/ContextConsumer.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/ContextConsumer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,9 +22,9 @@ * Callback interface used to process an {@link ApplicationContext} with the ability to * throw a (checked) exception. * + * @param the application context type * @author Stephane Nicoll * @author Andy Wilkinson - * @param the application context type * @since 2.0.0 * @see AbstractApplicationContextRunner */ diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/ReactiveWebApplicationContextRunner.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/ReactiveWebApplicationContextRunner.java index 7f05a8743fd4..932e27ff8593 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/ReactiveWebApplicationContextRunner.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/ReactiveWebApplicationContextRunner.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -59,23 +59,24 @@ public ReactiveWebApplicationContextRunner(Supplier contextFactory, + boolean allowBeanDefinitionOverriding, List> initializers, TestPropertyValues environmentProperties, TestPropertyValues systemProperties, ClassLoader classLoader, ApplicationContext parent, List> beanRegistrations, List configurations) { - super(contextFactory, initializers, environmentProperties, systemProperties, classLoader, parent, - beanRegistrations, configurations); + super(contextFactory, allowBeanDefinitionOverriding, initializers, environmentProperties, systemProperties, + classLoader, parent, beanRegistrations, configurations); } @Override protected ReactiveWebApplicationContextRunner newInstance( - Supplier contextFactory, + Supplier contextFactory, boolean allowBeanDefinitionOverriding, List> initializers, TestPropertyValues environmentProperties, TestPropertyValues systemProperties, ClassLoader classLoader, ApplicationContext parent, List> beanRegistrations, List configurations) { - return new ReactiveWebApplicationContextRunner(contextFactory, initializers, environmentProperties, - systemProperties, classLoader, parent, beanRegistrations, configurations); + return new ReactiveWebApplicationContextRunner(contextFactory, allowBeanDefinitionOverriding, initializers, + environmentProperties, systemProperties, classLoader, parent, beanRegistrations, configurations); } } diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/WebApplicationContextRunner.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/WebApplicationContextRunner.java index 1df1bca5fc23..8f9c671cd0a1 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/WebApplicationContextRunner.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/WebApplicationContextRunner.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -63,22 +63,24 @@ public WebApplicationContextRunner(Supplier c } private WebApplicationContextRunner(Supplier contextFactory, + boolean allowBeanDefinitionOverriding, List> initializers, TestPropertyValues environmentProperties, TestPropertyValues systemProperties, ClassLoader classLoader, ApplicationContext parent, List> beanRegistrations, List configurations) { - super(contextFactory, initializers, environmentProperties, systemProperties, classLoader, parent, - beanRegistrations, configurations); + super(contextFactory, allowBeanDefinitionOverriding, initializers, environmentProperties, systemProperties, + classLoader, parent, beanRegistrations, configurations); } @Override protected WebApplicationContextRunner newInstance(Supplier contextFactory, + boolean allowBeanDefinitionOverriding, List> initializers, TestPropertyValues environmentProperties, TestPropertyValues systemProperties, ClassLoader classLoader, ApplicationContext parent, List> beanRegistrations, List configurations) { - return new WebApplicationContextRunner(contextFactory, initializers, environmentProperties, systemProperties, - classLoader, parent, beanRegistrations, configurations); + return new WebApplicationContextRunner(contextFactory, allowBeanDefinitionOverriding, initializers, + environmentProperties, systemProperties, classLoader, parent, beanRegistrations, configurations); } /** diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/json/BasicJsonTester.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/json/BasicJsonTester.java index ae593fbb8321..9b9bf84b3af1 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/json/BasicJsonTester.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/json/BasicJsonTester.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -81,7 +81,7 @@ public BasicJsonTester(Class resourceLoadClass, Charset charset) { * resources */ protected final void initialize(Class resourceLoadClass) { - this.initialize(resourceLoadClass, null); + initialize(resourceLoadClass, null); } /** diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/json/JacksonTester.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/json/JacksonTester.java index 79ba54bdf1b4..e382be57221b 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/json/JacksonTester.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/json/JacksonTester.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -165,7 +165,7 @@ public static void initFields(Object testInstance, ObjectFactory o * @return the new instance */ public JacksonTester forView(Class view) { - return new JacksonTester<>(this.getResourceLoadClass(), this.getType(), this.objectMapper, view); + return new JacksonTester<>(getResourceLoadClass(), getType(), this.objectMapper, view); } /** diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/DefinitionsParser.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/DefinitionsParser.java index 9028b70815d1..dd6591932a78 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/DefinitionsParser.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/DefinitionsParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; +import java.lang.reflect.TypeVariable; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; @@ -60,20 +61,20 @@ class DefinitionsParser { } void parse(Class source) { - parseElement(source); - ReflectionUtils.doWithFields(source, this::parseElement); + parseElement(source, null); + ReflectionUtils.doWithFields(source, (element) -> parseElement(element, source)); } - private void parseElement(AnnotatedElement element) { + private void parseElement(AnnotatedElement element, Class source) { MergedAnnotations annotations = MergedAnnotations.from(element, SearchStrategy.SUPERCLASS); annotations.stream(MockBean.class).map(MergedAnnotation::synthesize) - .forEach((annotation) -> parseMockBeanAnnotation(annotation, element)); + .forEach((annotation) -> parseMockBeanAnnotation(annotation, element, source)); annotations.stream(SpyBean.class).map(MergedAnnotation::synthesize) - .forEach((annotation) -> parseSpyBeanAnnotation(annotation, element)); + .forEach((annotation) -> parseSpyBeanAnnotation(annotation, element, source)); } - private void parseMockBeanAnnotation(MockBean annotation, AnnotatedElement element) { - Set typesToMock = getOrDeduceTypes(element, annotation.value()); + private void parseMockBeanAnnotation(MockBean annotation, AnnotatedElement element, Class source) { + Set typesToMock = getOrDeduceTypes(element, annotation.value(), source); Assert.state(!typesToMock.isEmpty(), () -> "Unable to deduce type to mock from " + element); if (StringUtils.hasLength(annotation.name())) { Assert.state(typesToMock.size() == 1, "The name attribute can only be used when mocking a single class"); @@ -86,8 +87,8 @@ private void parseMockBeanAnnotation(MockBean annotation, AnnotatedElement eleme } } - private void parseSpyBeanAnnotation(SpyBean annotation, AnnotatedElement element) { - Set typesToSpy = getOrDeduceTypes(element, annotation.value()); + private void parseSpyBeanAnnotation(SpyBean annotation, AnnotatedElement element, Class source) { + Set typesToSpy = getOrDeduceTypes(element, annotation.value(), source); Assert.state(!typesToSpy.isEmpty(), () -> "Unable to deduce type to spy from " + element); if (StringUtils.hasLength(annotation.name())) { Assert.state(typesToSpy.size() == 1, "The name attribute can only be used when spying a single class"); @@ -108,13 +109,15 @@ private void addDefinition(AnnotatedElement element, Definition definition, Stri } } - private Set getOrDeduceTypes(AnnotatedElement element, Class[] value) { + private Set getOrDeduceTypes(AnnotatedElement element, Class[] value, Class source) { Set types = new LinkedHashSet<>(); for (Class clazz : value) { types.add(ResolvableType.forClass(clazz)); } if (types.isEmpty() && element instanceof Field) { - types.add(ResolvableType.forField((Field) element)); + Field field = (Field) element; + types.add((field.getGenericType() instanceof TypeVariable) ? ResolvableType.forField(field, source) + : ResolvableType.forField(field)); } return types; } diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockBean.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockBean.java index 21f6c213befe..201a3e2e794e 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockBean.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,10 +36,12 @@ * used as a class level annotation or on fields in either {@code @Configuration} classes, * or test classes that are {@link RunWith @RunWith} the {@link SpringRunner}. *

    - * Mocks can be registered by type or by {@link #name() bean name}. Any existing single - * bean of the same type defined in the context will be replaced by the mock. If no - * existing bean is defined a new one will be added. Dependencies that are known to the - * application context but are not beans (such as those + * Mocks can be registered by type or by {@link #name() bean name}. When registered by + * type, any existing single bean of a matching type (including subclasses) in the context + * will be replaced by the mock. When registered by name, an existing bean can be + * specifically targeted for replacement by a mock. In either case, if no existing bean is + * defined a new one will be added. Dependencies that are known to the application context + * but are not beans (such as those * {@link org.springframework.beans.factory.config.ConfigurableListableBeanFactory#registerResolvableDependency(Class, Object) * registered directly}) will not be found and a mocked bean will be added to the context * alongside the existing dependency. diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoContextCustomizerFactory.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoContextCustomizerFactory.java index e013f82c1eee..2fef24d8b383 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoContextCustomizerFactory.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoContextCustomizerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.ContextCustomizerFactory; +import org.springframework.test.context.TestContextAnnotationUtils; /** * A {@link ContextCustomizerFactory} to add Mockito support. @@ -35,8 +36,15 @@ public ContextCustomizer createContextCustomizer(Class testClass, // We gather the explicit mock definitions here since they form part of the // MergedContextConfiguration key. Different mocks need to have a different key. DefinitionsParser parser = new DefinitionsParser(); - parser.parse(testClass); + parseDefinitions(testClass, parser); return new MockitoContextCustomizer(parser.getDefinitions()); } + private void parseDefinitions(Class testClass, DefinitionsParser parser) { + parser.parse(testClass); + if (TestContextAnnotationUtils.searchEnclosingClass(testClass)) { + parseDefinitions(testClass.getEnclosingClass(), parser); + } + } + } diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessor.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessor.java index b7cc40a47bc4..c87a9e6a711d 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessor.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,18 +16,18 @@ package org.springframework.boot.test.mock.mockito; -import java.beans.PropertyDescriptor; import java.lang.reflect.Field; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; -import org.springframework.aop.scope.ScopedObject; import org.springframework.aop.scope.ScopedProxyUtils; import org.springframework.beans.BeansException; import org.springframework.beans.PropertyValues; @@ -44,8 +44,9 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.ConstructorArgumentValues; import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder; -import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessorAdapter; +import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor; import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.beans.factory.support.DefaultBeanNameGenerator; @@ -76,8 +77,8 @@ * @author Andreas Neiser * @since 1.4.0 */ -public class MockitoPostProcessor extends InstantiationAwareBeanPostProcessorAdapter - implements BeanClassLoaderAware, BeanFactoryAware, BeanFactoryPostProcessor, Ordered { +public class MockitoPostProcessor implements InstantiationAwareBeanPostProcessor, BeanClassLoaderAware, + BeanFactoryAware, BeanFactoryPostProcessor, Ordered { private static final String BEAN_NAME = MockitoPostProcessor.class.getName(); @@ -248,9 +249,9 @@ private Set getExistingBeans(ConfigurableListableBeanFactory beanFactory } private Set getExistingBeans(ConfigurableListableBeanFactory beanFactory, ResolvableType type) { - Set beans = new LinkedHashSet<>(Arrays.asList(beanFactory.getBeanNamesForType(type))); + Set beans = new LinkedHashSet<>(Arrays.asList(beanFactory.getBeanNamesForType(type, true, false))); String typeName = type.resolve(Object.class).getName(); - for (String beanName : beanFactory.getBeanNamesForType(FactoryBean.class)) { + for (String beanName : beanFactory.getBeanNamesForType(FactoryBean.class, true, false)) { beanName = BeanFactoryUtils.transformedBeanName(beanName); BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName); if (typeName.equals(beanDefinition.getAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE))) { @@ -308,7 +309,7 @@ private String determinePrimaryCandidate(BeanDefinitionRegistry registry, Collec if (primaryBeanName != null) { throw new NoUniqueBeanDefinitionException(type.resolve(), candidateBeanNames.size(), "more than one 'primary' bean found among candidates: " - + Arrays.asList(candidateBeanNames)); + + Collections.singletonList(candidateBeanNames)); } primaryBeanName = candidateBeanName; } @@ -333,8 +334,8 @@ protected final Object createSpyIfNecessary(Object bean, String beanName) throws } @Override - public PropertyValues postProcessPropertyValues(PropertyValues pvs, PropertyDescriptor[] pds, final Object bean, - String beanName) throws BeansException { + public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) + throws BeansException { ReflectionUtils.doWithFields(bean.getClass(), (field) -> postProcessField(bean, field)); return pvs; } @@ -355,12 +356,13 @@ void inject(Field field, Object target, Definition definition) { private void inject(Field field, Object target, String beanName) { try { field.setAccessible(true); - Assert.state(ReflectionUtils.getField(field, target) == null, - () -> "The field " + field + " cannot have an existing value"); + Object existingValue = ReflectionUtils.getField(field, target); Object bean = this.beanFactory.getBean(beanName, field.getType()); - if (bean instanceof ScopedObject) { - bean = ((ScopedObject) bean).getTargetObject(); + if (existingValue == bean) { + return; } + Assert.state(existingValue == null, () -> "The existing value '" + existingValue + "' of field '" + field + + "' is not the same as the new value '" + bean + "'"); ReflectionUtils.setField(field, target, bean); } catch (Throwable ex) { @@ -425,14 +427,15 @@ private static BeanDefinition getOrAddBeanDefinition(BeanDefinitionRegistry regi } /** - * {@link BeanPostProcessor} to handle {@link SpyBean @SpyBean} definitions. - * Registered as a separate processor so that it can be ordered above AOP post - * processors. + * {@link BeanPostProcessor} to handle {@link SpyBean} definitions. Registered as a + * separate processor so that it can be ordered above AOP post processors. */ - static class SpyPostProcessor extends InstantiationAwareBeanPostProcessorAdapter implements PriorityOrdered { + static class SpyPostProcessor implements SmartInstantiationAwareBeanPostProcessor, PriorityOrdered { private static final String BEAN_NAME = SpyPostProcessor.class.getName(); + private final Map earlySpyReferences = new ConcurrentHashMap<>(16); + private final MockitoPostProcessor mockitoPostProcessor; SpyPostProcessor(MockitoPostProcessor mockitoPostProcessor) { @@ -446,22 +449,26 @@ public int getOrder() { @Override public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException { - return this.mockitoPostProcessor.createSpyIfNecessary(bean, getOriginalBeanNameIfScopedTarget(beanName)); + if (bean instanceof FactoryBean) { + return bean; + } + this.earlySpyReferences.put(getCacheKey(bean, beanName), bean); + return this.mockitoPostProcessor.createSpyIfNecessary(bean, beanName); } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { - if (bean instanceof FactoryBean || bean instanceof ScopedObject) { + if (bean instanceof FactoryBean) { return bean; } - return this.mockitoPostProcessor.createSpyIfNecessary(bean, getOriginalBeanNameIfScopedTarget(beanName)); + if (this.earlySpyReferences.remove(getCacheKey(bean, beanName)) != bean) { + return this.mockitoPostProcessor.createSpyIfNecessary(bean, beanName); + } + return bean; } - private String getOriginalBeanNameIfScopedTarget(String beanName) { - if (ScopedProxyUtils.isScopedTarget(beanName)) { - return beanName.substring("scopedTarget.".length()); - } - return beanName; + private String getCacheKey(Object bean, String beanName) { + return StringUtils.hasLength(beanName) ? beanName : bean.getClass().getName(); } static void register(BeanDefinitionRegistry registry) { diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoTestExecutionListener.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoTestExecutionListener.java index 674ea672f7bd..c1c6e417258f 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoTestExecutionListener.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoTestExecutionListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,16 +33,23 @@ import org.springframework.util.ReflectionUtils.FieldCallback; /** - * {@link TestExecutionListener} to trigger {@link MockitoAnnotations#initMocks(Object)} - * when {@link MockBean @MockBean} annotations are used. Primarily to allow - * {@link Captor @Captor} annotations. + * {@link TestExecutionListener} to enable {@link MockBean @MockBean} and + * {@link SpyBean @SpyBean} support. Also triggers + * {@link MockitoAnnotations#openMocks(Object)} when any Mockito annotations used, + * primarily to allow {@link Captor @Captor} annotations. + *

    + * To use the automatic reset support of {@code @MockBean} and {@code @SpyBean}, configure + * {@link ResetMocksTestExecutionListener} as well. * * @author Phillip Webb * @author Andy Wilkinson * @since 1.4.2 + * @see ResetMocksTestExecutionListener */ public class MockitoTestExecutionListener extends AbstractTestExecutionListener { + private static final String MOCKS_ATTRIBUTE_NAME = MockitoTestExecutionListener.class.getName() + ".mocks"; + @Override public final int getOrder() { return 1950; @@ -63,9 +70,17 @@ public void beforeTestMethod(TestContext testContext) throws Exception { } } + @Override + public void afterTestMethod(TestContext testContext) throws Exception { + Object mocks = testContext.getAttribute(MOCKS_ATTRIBUTE_NAME); + if (mocks instanceof AutoCloseable) { + ((AutoCloseable) mocks).close(); + } + } + private void initMocks(TestContext testContext) { if (hasMockitoAnnotations(testContext)) { - MockitoAnnotations.initMocks(testContext.getTestInstance()); + testContext.setAttribute(MOCKS_ATTRIBUTE_NAME, MockitoAnnotations.openMocks(testContext.getTestInstance())); } } diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/ResetMocksTestExecutionListener.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/ResetMocksTestExecutionListener.java index 27cce2d8663b..2033c2e648fa 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/ResetMocksTestExecutionListener.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/ResetMocksTestExecutionListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,10 +35,11 @@ /** * {@link TestExecutionListener} to reset any mock beans that have been marked with a - * {@link MockReset}. + * {@link MockReset}. Typically used alongside {@link MockitoTestExecutionListener}. * * @author Phillip Webb * @since 1.4.0 + * @see MockitoTestExecutionListener */ public class ResetMocksTestExecutionListener extends AbstractTestExecutionListener { diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/SpringBootMockResolver.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/SpringBootMockResolver.java new file mode 100644 index 000000000000..aaf37163e332 --- /dev/null +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/SpringBootMockResolver.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.mock.mockito; + +import org.mockito.plugins.MockResolver; + +import org.springframework.test.util.AopTestUtils; + +/** + * A {@link MockResolver} for testing Spring Boot applications with Mockito. Resolves + * mocks by returning the {@link AopTestUtils#getUltimateTargetObject(Object) ultimate + * target object} of the instance. + * + * @author Andy Wilkinson + * @since 2.4.0 + */ +public class SpringBootMockResolver implements MockResolver { + + @Override + public Object resolve(Object instance) { + return AopTestUtils.getUltimateTargetObject(instance); + } + +} diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/SpyBean.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/SpyBean.java index 7715875312f4..bf5146805cc1 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/SpyBean.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/SpyBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,9 +37,9 @@ * {@link RunWith @RunWith} the {@link SpringRunner}. *

    * Spies can be applied by type or by {@link #name() bean name}. All beans in the context - * of the same type will be wrapped with the spy. If no existing bean is defined a new one - * will be added. Dependencies that are known to the application context but are not beans - * (such as those + * of a matching type (including subclasses) will be wrapped with the spy. If no existing + * bean is defined a new one will be added. Dependencies that are known to the application + * context but are not beans (such as those * {@link org.springframework.beans.factory.config.ConfigurableListableBeanFactory#registerResolvableDependency(Class, Object) * registered directly}) will not be found and a spied bean will be added to the context * alongside the existing dependency. diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/SpyDefinition.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/SpyDefinition.java index 937230df23a4..dbfc28f6367d 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/SpyDefinition.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/SpyDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,9 @@ package org.springframework.boot.test.mock.mockito; +import java.lang.reflect.Proxy; + +import org.mockito.AdditionalAnswers; import org.mockito.MockSettings; import org.mockito.Mockito; import org.mockito.listeners.VerificationStartedEvent; @@ -95,12 +98,20 @@ T createSpy(String name, Object instance) { if (StringUtils.hasLength(name)) { settings.name(name); } - settings.spiedInstance(instance); - settings.defaultAnswer(Mockito.CALLS_REAL_METHODS); - if (this.isProxyTargetAware()) { + if (isProxyTargetAware()) { settings.verificationStartedListeners(new SpringAopBypassingVerificationStartedListener()); } - return (T) mock(instance.getClass(), settings); + Class toSpy; + if (Proxy.isProxyClass(instance.getClass())) { + settings.defaultAnswer(AdditionalAnswers.delegatesTo(instance)); + toSpy = this.typeToSpy.toClass(); + } + else { + settings.defaultAnswer(Mockito.CALLS_REAL_METHODS); + settings.spiedInstance(instance); + toSpy = instance.getClass(); + } + return (T) mock(toSpy, settings); } /** diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/web/SpringBootMockServletContext.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/web/SpringBootMockServletContext.java index 90b5fc5775a1..ec810bac3fd5 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/web/SpringBootMockServletContext.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/web/SpringBootMockServletContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.nio.file.Files; import org.springframework.core.io.FileSystemResourceLoader; import org.springframework.core.io.Resource; @@ -41,7 +42,7 @@ public class SpringBootMockServletContext extends MockServletContext { private final ResourceLoader resourceLoader; - private File emptyRootFolder; + private File emptyRootDirectory; public SpringBootMockServletContext(String resourceBasePath) { this(resourceBasePath, new FileSystemResourceLoader()); @@ -91,16 +92,14 @@ public URL getResource(String path) throws MalformedURLException { // Liquibase assumes that "/" always exists, if we don't have a directory // use a temporary location. try { - if (this.emptyRootFolder == null) { + if (this.emptyRootDirectory == null) { synchronized (this) { - File tempFolder = File.createTempFile("spr", "servlet"); - tempFolder.delete(); - tempFolder.mkdirs(); - tempFolder.deleteOnExit(); - this.emptyRootFolder = tempFolder; + File tempDirectory = Files.createTempDirectory("spr-servlet").toFile(); + tempDirectory.deleteOnExit(); + this.emptyRootDirectory = tempDirectory; } } - return this.emptyRootFolder.toURI().toURL(); + return this.emptyRootDirectory.toURI().toURL(); } catch (IOException ex) { // Ignore diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/rule/OutputCapture.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/rule/OutputCapture.java deleted file mode 100644 index 1d4b1f31ed40..000000000000 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/rule/OutputCapture.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.test.rule; - -import org.hamcrest.Matcher; -import org.junit.rules.TestRule; -import org.junit.runner.Description; -import org.junit.runners.model.Statement; - -import org.springframework.boot.test.system.OutputCaptureRule; - -/** - * JUnit {@code @Rule} to capture output from System.out and System.err. - * - * @author Phillip Webb - * @author Andy Wilkinson - * @since 1.4.0 - * @deprecated since 2.2.0 in favor of {@link OutputCaptureRule} - */ -@Deprecated -public class OutputCapture implements TestRule { - - private final OutputCaptureRule delegate = new OutputCaptureRule(); - - @Override - public Statement apply(Statement base, Description description) { - return this.delegate.apply(base, description); - } - - /** - * Discard all currently accumulated output. - */ - public void reset() { - this.delegate.reset(); - } - - public void flush() { - // Flushing is no longer necessary - } - - @Override - public String toString() { - return this.delegate.toString(); - } - - /** - * Verify that the output is matched by the supplied {@code matcher}. Verification is - * performed after the test method has executed. - * @param matcher the matcher - */ - public void expect(Matcher matcher) { - this.delegate.expect(matcher); - } - -} diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/rule/package-info.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/rule/package-info.java deleted file mode 100644 index 2034e0d961de..000000000000 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/rule/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Useful JUnit {@code @Rule} classes. - */ -package org.springframework.boot.test.rule; diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/system/OutputCaptureRule.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/system/OutputCaptureRule.java index 4eccfebd279e..50536bfa0e3b 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/system/OutputCaptureRule.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/system/OutputCaptureRule.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ import java.util.List; import org.hamcrest.Matcher; -import org.junit.Assert; +import org.hamcrest.MatcherAssert; import org.junit.Rule; import org.junit.rules.TestRule; import org.junit.runner.Description; @@ -70,7 +70,7 @@ public void evaluate() throws Throwable { try { if (!OutputCaptureRule.this.matchers.isEmpty()) { String output = OutputCaptureRule.this.delegate.toString(); - Assert.assertThat(output, allOf(OutputCaptureRule.this.matchers)); + MatcherAssert.assertThat(output, allOf(OutputCaptureRule.this.matchers)); } } finally { @@ -81,15 +81,6 @@ public void evaluate() throws Throwable { }; } - /** - * Resets the current capture session, clearing its captured output. - * @deprecated since 2.2.0 with no replacement - */ - @Deprecated - public void reset() { - OutputCaptureRule.this.delegate.reset(); - } - @Override public String getAll() { return this.delegate.getAll(); diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/util/TestPropertyValues.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/util/TestPropertyValues.java index e9ca8b6ede73..e72d26f48280 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/util/TestPropertyValues.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/util/TestPropertyValues.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import java.util.Map; import java.util.Objects; import java.util.concurrent.Callable; +import java.util.function.Function; import java.util.stream.Stream; import java.util.stream.StreamSupport; @@ -59,17 +60,62 @@ private TestPropertyValues(Map properties) { } /** - * Builder method to add more properties. + * Return a new {@link TestPropertyValues} instance with additional entries. + * Name-value pairs can be specified with colon (":") or equals ("=") separators. * @param pairs the property pairs to add * @return a new {@link TestPropertyValues} instance */ public TestPropertyValues and(String... pairs) { - return and(Arrays.stream(pairs).map(Pair::parse)); + return and(Arrays.stream(pairs), Pair::parse); } - private TestPropertyValues and(Stream pairs) { + /** + * Return a new {@link TestPropertyValues} instance with additional entries. + * Name-value pairs can be specified with colon (":") or equals ("=") separators. + * @param pairs the property pairs to add + * @return a new {@link TestPropertyValues} instance + * @since 2.4.0 + */ + public TestPropertyValues and(Iterable pairs) { + return (pairs != null) ? and(StreamSupport.stream(pairs.spliterator(), false)) : this; + } + + /** + * Return a new {@link TestPropertyValues} instance with additional entries. + * Name-value pairs can be specified with colon (":") or equals ("=") separators. + * @param pairs the property pairs to add + * @return a new {@link TestPropertyValues} instance + * @since 2.4.0 + */ + public TestPropertyValues and(Stream pairs) { + return (pairs != null) ? and(pairs, Pair::parse) : this; + } + + /** + * Return a new {@link TestPropertyValues} instance with additional entries. + * @param map the map of properties that need to be added to the environment + * @return a new {@link TestPropertyValues} instance + * @since 2.4.0 + */ + public TestPropertyValues and(Map map) { + return (map != null) ? and(map.entrySet().stream(), Pair::fromMapEntry) : this; + } + + /** + * Return a new {@link TestPropertyValues} instance with additional entries. + * @param the stream element type + * @param stream the elements that need to be added to the environment + * @param mapper a mapper function to convert an element from the stream into a + * {@link Pair} + * @return a new {@link TestPropertyValues} instance + * @since 2.4.0 + */ + public TestPropertyValues and(Stream stream, Function mapper) { + if (stream == null) { + return this; + } Map properties = new LinkedHashMap<>(this.properties); - pairs.filter(Objects::nonNull).forEach((pair) -> pair.addTo(properties)); + stream.map(mapper).filter(Objects::nonNull).forEach((pair) -> pair.addTo(properties)); return new TestPropertyValues(properties); } @@ -174,10 +220,7 @@ public static TestPropertyValues of(String... pairs) { * @return the new instance */ public static TestPropertyValues of(Iterable pairs) { - if (pairs == null) { - return empty(); - } - return of(StreamSupport.stream(pairs.spliterator(), false)); + return (pairs != null) ? of(StreamSupport.stream(pairs.spliterator(), false)) : empty(); } /** @@ -189,10 +232,30 @@ public static TestPropertyValues of(Iterable pairs) { * @return the new instance */ public static TestPropertyValues of(Stream pairs) { - if (pairs == null) { - return empty(); - } - return empty().and(pairs.map(Pair::parse)); + return (pairs != null) ? of(pairs, Pair::parse) : empty(); + } + + /** + * Return a new {@link TestPropertyValues} with the underlying map populated with the + * given map entries. + * @param map the map of properties that need to be added to the environment + * @return the new instance + */ + public static TestPropertyValues of(Map map) { + return (map != null) ? of(map.entrySet().stream(), Pair::fromMapEntry) : empty(); + } + + /** + * Return a new {@link TestPropertyValues} with the underlying map populated with the + * given stream. + * @param the stream element type + * @param stream the elements that need to be added to the environment + * @param mapper a mapper function to convert an element from the stream into a + * {@link Pair} + * @return the new instance + */ + public static TestPropertyValues of(Stream stream, Function mapper) { + return (stream != null) ? empty().and(stream, mapper) : empty(); } /** @@ -247,6 +310,14 @@ public static class Pair { private String value; + /** + * Create a new {@link Pair} instance. + * @param name the name + * @param value the value + * @deprecated since 2.4.0 for removal in 2.6.0 in favor of + * {@link #of(String, String)} + */ + @Deprecated public Pair(String name, String value) { Assert.hasLength(name, "Name must not be empty"); this.name = name; @@ -276,11 +347,28 @@ private static int getSeparatorIndex(String pair) { return Math.min(colonIndex, equalIndex); } - private static Pair of(String name, String value) { - if (StringUtils.isEmpty(name) && StringUtils.isEmpty(value)) { - return null; + /** + * Factory method to create a {@link Pair} from a {@code Map.Entry}. + * @param entry the map entry + * @return the {@link Pair} instance or {@code null} + * @since 2.4.0 + */ + public static Pair fromMapEntry(Map.Entry entry) { + return (entry != null) ? of(entry.getKey(), entry.getValue()) : null; + } + + /** + * Factory method to create a {@link Pair} from a name and value. + * @param name the name + * @param value the value + * @return the {@link Pair} instance or {@code null} + * @since 2.4.0 + */ + public static Pair of(String name, String value) { + if (StringUtils.hasLength(name) || StringUtils.hasLength(value)) { + return new Pair(name, value); } - return new Pair(name, value); + return null; } } @@ -309,7 +397,7 @@ public void close() { private String setOrClear(String name, String value) { Assert.notNull(name, "Name must not be null"); - if (StringUtils.isEmpty(value)) { + if (!StringUtils.hasLength(value)) { return (String) System.getProperties().remove(name); } return (String) System.getProperties().setProperty(name, value); diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/SpringBootTestRandomPortEnvironmentPostProcessor.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/SpringBootTestRandomPortEnvironmentPostProcessor.java index 052278c19492..5dc9fe4502c8 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/SpringBootTestRandomPortEnvironmentPostProcessor.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/SpringBootTestRandomPortEnvironmentPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.test.web; import java.util.Objects; diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/RootUriRequestExpectationManager.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/RootUriRequestExpectationManager.java index 8a0bc77985ae..9be8ceb63abc 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/RootUriRequestExpectationManager.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/RootUriRequestExpectationManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import java.io.OutputStream; import java.net.URI; import java.net.URISyntaxException; +import java.time.Duration; import org.springframework.boot.web.client.RootUriTemplateHandler; import org.springframework.http.client.ClientHttpRequest; @@ -111,6 +112,11 @@ public void verify() { this.expectationManager.verify(); } + @Override + public void verify(Duration timeout) { + this.expectationManager.verify(timeout); + } + @Override public void reset() { this.expectationManager.reset(); diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/TestRestTemplate.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/TestRestTemplate.java index ee877365db77..065d9c830151 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/TestRestTemplate.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/TestRestTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,6 +41,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.RequestEntity; +import org.springframework.http.RequestEntity.UriTemplateRequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.ClientHttpResponse; @@ -49,25 +50,28 @@ import org.springframework.web.client.DefaultResponseErrorHandler; import org.springframework.web.client.RequestCallback; import org.springframework.web.client.ResponseExtractor; -import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.DefaultUriBuilderFactory; import org.springframework.web.util.UriTemplateHandler; /** * Convenient alternative of {@link RestTemplate} that is suitable for integration tests. - * They are fault tolerant, and optionally can carry Basic authentication headers. If - * Apache Http Client 4.3.2 or better is available (recommended) it will be used as the - * client, and by default configured to ignore cookies and redirects. + * {@code TestRestTemplate} is fault tolerant. This means that 4xx and 5xx do not result + * in an exception being thrown and can instead be detected via the {@link ResponseEntity + * response entity} and its {@link ResponseEntity#getStatusCode() status code}. + *

    + * A {@code TestRestTemplate} can optionally carry Basic authentication headers. If Apache + * Http Client 4.3.2 or better is available (recommended) it will be used as the client, + * and by default configured to ignore cookies and redirects. *

    * Note: To prevent injection problems this class intentionally does not extend * {@link RestTemplate}. If you need access to the underlying {@link RestTemplate} use * {@link #getRestTemplate()}. *

    * If you are using the - * {@link org.springframework.boot.test.context.SpringBootTest @SpringBootTest} - * annotation, a {@link TestRestTemplate} is automatically available and can be - * {@code @Autowired} into your test. If you need customizations (for example to adding + * {@link org.springframework.boot.test.context.SpringBootTest @SpringBootTest} annotation + * with an embedded server, a {@link TestRestTemplate} is automatically available and can + * be {@code @Autowired} into your test. If you need customizations (for example to adding * additional message converters) use a {@link RestTemplateBuilder} {@code @Bean}. * * @author Dave Syer @@ -175,10 +179,9 @@ public String getRootUri() { * @param urlVariables the variables to expand the template * @param the type of the return value * @return the converted object - * @throws RestClientException on client-side HTTP error on client-side HTTP error * @see RestTemplate#getForObject(String, Class, Object...) */ - public T getForObject(String url, Class responseType, Object... urlVariables) throws RestClientException { + public T getForObject(String url, Class responseType, Object... urlVariables) { return this.restTemplate.getForObject(url, responseType, urlVariables); } @@ -192,11 +195,9 @@ public T getForObject(String url, Class responseType, Object... urlVariab * @param urlVariables the map containing variables for the URI template * @param the type of the return value * @return the converted object - * @throws RestClientException on client-side HTTP error * @see RestTemplate#getForObject(String, Class, Object...) */ - public T getForObject(String url, Class responseType, Map urlVariables) - throws RestClientException { + public T getForObject(String url, Class responseType, Map urlVariables) { return this.restTemplate.getForObject(url, responseType, urlVariables); } @@ -207,10 +208,9 @@ public T getForObject(String url, Class responseType, Map urlV * @param responseType the type of the return value * @param the type of the return value * @return the converted object - * @throws RestClientException on client-side HTTP error * @see RestTemplate#getForObject(java.net.URI, java.lang.Class) */ - public T getForObject(URI url, Class responseType) throws RestClientException { + public T getForObject(URI url, Class responseType) { return this.restTemplate.getForObject(applyRootUriIfNecessary(url), responseType); } @@ -224,12 +224,10 @@ public T getForObject(URI url, Class responseType) throws RestClientExcep * @param urlVariables the variables to expand the template * @param the type of the return value * @return the entity - * @throws RestClientException on client-side HTTP error * @see RestTemplate#getForEntity(java.lang.String, java.lang.Class, * java.lang.Object[]) */ - public ResponseEntity getForEntity(String url, Class responseType, Object... urlVariables) - throws RestClientException { + public ResponseEntity getForEntity(String url, Class responseType, Object... urlVariables) { return this.restTemplate.getForEntity(url, responseType, urlVariables); } @@ -243,11 +241,9 @@ public ResponseEntity getForEntity(String url, Class responseType, Obj * @param urlVariables the map containing variables for the URI template * @param the type of the return value * @return the converted object - * @throws RestClientException on client-side HTTP error * @see RestTemplate#getForEntity(java.lang.String, java.lang.Class, java.util.Map) */ - public ResponseEntity getForEntity(String url, Class responseType, Map urlVariables) - throws RestClientException { + public ResponseEntity getForEntity(String url, Class responseType, Map urlVariables) { return this.restTemplate.getForEntity(url, responseType, urlVariables); } @@ -258,10 +254,9 @@ public ResponseEntity getForEntity(String url, Class responseType, Map * @param responseType the type of the return value * @param the type of the return value * @return the converted object - * @throws RestClientException on client-side HTTP error * @see RestTemplate#getForEntity(java.net.URI, java.lang.Class) */ - public ResponseEntity getForEntity(URI url, Class responseType) throws RestClientException { + public ResponseEntity getForEntity(URI url, Class responseType) { return this.restTemplate.getForEntity(applyRootUriIfNecessary(url), responseType); } @@ -272,10 +267,9 @@ public ResponseEntity getForEntity(URI url, Class responseType) throws * @param url the URL * @param urlVariables the variables to expand the template * @return all HTTP headers of that resource - * @throws RestClientException on client-side HTTP error * @see RestTemplate#headForHeaders(java.lang.String, java.lang.Object[]) */ - public HttpHeaders headForHeaders(String url, Object... urlVariables) throws RestClientException { + public HttpHeaders headForHeaders(String url, Object... urlVariables) { return this.restTemplate.headForHeaders(url, urlVariables); } @@ -286,10 +280,9 @@ public HttpHeaders headForHeaders(String url, Object... urlVariables) throws Res * @param url the URL * @param urlVariables the map containing variables for the URI template * @return all HTTP headers of that resource - * @throws RestClientException on client-side HTTP error * @see RestTemplate#headForHeaders(java.lang.String, java.util.Map) */ - public HttpHeaders headForHeaders(String url, Map urlVariables) throws RestClientException { + public HttpHeaders headForHeaders(String url, Map urlVariables) { return this.restTemplate.headForHeaders(url, urlVariables); } @@ -297,10 +290,9 @@ public HttpHeaders headForHeaders(String url, Map urlVariables) throw * Retrieve all headers of the resource specified by the URL. * @param url the URL * @return all HTTP headers of that resource - * @throws RestClientException on client-side HTTP error * @see RestTemplate#headForHeaders(java.net.URI) */ - public HttpHeaders headForHeaders(URI url) throws RestClientException { + public HttpHeaders headForHeaders(URI url) { return this.restTemplate.headForHeaders(applyRootUriIfNecessary(url)); } @@ -317,12 +309,11 @@ public HttpHeaders headForHeaders(URI url) throws RestClientException { * @param request the Object to be POSTed, may be {@code null} * @param urlVariables the variables to expand the template * @return the value for the {@code Location} header - * @throws RestClientException on client-side HTTP error * @see HttpEntity * @see RestTemplate#postForLocation(java.lang.String, java.lang.Object, * java.lang.Object[]) */ - public URI postForLocation(String url, Object request, Object... urlVariables) throws RestClientException { + public URI postForLocation(String url, Object request, Object... urlVariables) { return this.restTemplate.postForLocation(url, request, urlVariables); } @@ -339,12 +330,11 @@ public URI postForLocation(String url, Object request, Object... urlVariables) t * @param request the Object to be POSTed, may be {@code null} * @param urlVariables the variables to expand the template * @return the value for the {@code Location} header - * @throws RestClientException on client-side HTTP error * @see HttpEntity * @see RestTemplate#postForLocation(java.lang.String, java.lang.Object, * java.util.Map) */ - public URI postForLocation(String url, Object request, Map urlVariables) throws RestClientException { + public URI postForLocation(String url, Object request, Map urlVariables) { return this.restTemplate.postForLocation(url, request, urlVariables); } @@ -358,11 +348,10 @@ public URI postForLocation(String url, Object request, Map urlVariabl * @param url the URL * @param request the Object to be POSTed, may be {@code null} * @return the value for the {@code Location} header - * @throws RestClientException on client-side HTTP error * @see HttpEntity * @see RestTemplate#postForLocation(java.net.URI, java.lang.Object) */ - public URI postForLocation(URI url, Object request) throws RestClientException { + public URI postForLocation(URI url, Object request) { return this.restTemplate.postForLocation(applyRootUriIfNecessary(url), request); } @@ -380,13 +369,11 @@ public URI postForLocation(URI url, Object request) throws RestClientException { * @param urlVariables the variables to expand the template * @param the type of the return value * @return the converted object - * @throws RestClientException on client-side HTTP error * @see HttpEntity * @see RestTemplate#postForObject(java.lang.String, java.lang.Object, * java.lang.Class, java.lang.Object[]) */ - public T postForObject(String url, Object request, Class responseType, Object... urlVariables) - throws RestClientException { + public T postForObject(String url, Object request, Class responseType, Object... urlVariables) { return this.restTemplate.postForObject(url, request, responseType, urlVariables); } @@ -404,13 +391,11 @@ public T postForObject(String url, Object request, Class responseType, Ob * @param urlVariables the variables to expand the template * @param the type of the return value * @return the converted object - * @throws RestClientException on client-side HTTP error * @see HttpEntity * @see RestTemplate#postForObject(java.lang.String, java.lang.Object, * java.lang.Class, java.util.Map) */ - public T postForObject(String url, Object request, Class responseType, Map urlVariables) - throws RestClientException { + public T postForObject(String url, Object request, Class responseType, Map urlVariables) { return this.restTemplate.postForObject(url, request, responseType, urlVariables); } @@ -425,11 +410,10 @@ public T postForObject(String url, Object request, Class responseType, Ma * @param responseType the type of the return value * @param the type of the return value * @return the converted object - * @throws RestClientException on client-side HTTP error * @see HttpEntity * @see RestTemplate#postForObject(java.net.URI, java.lang.Object, java.lang.Class) */ - public T postForObject(URI url, Object request, Class responseType) throws RestClientException { + public T postForObject(URI url, Object request, Class responseType) { return this.restTemplate.postForObject(applyRootUriIfNecessary(url), request, responseType); } @@ -447,13 +431,12 @@ public T postForObject(URI url, Object request, Class responseType) throw * @param urlVariables the variables to expand the template * @param the type of the return value * @return the converted object - * @throws RestClientException on client-side HTTP error * @see HttpEntity * @see RestTemplate#postForEntity(java.lang.String, java.lang.Object, * java.lang.Class, java.lang.Object[]) */ public ResponseEntity postForEntity(String url, Object request, Class responseType, - Object... urlVariables) throws RestClientException { + Object... urlVariables) { return this.restTemplate.postForEntity(url, request, responseType, urlVariables); } @@ -471,13 +454,12 @@ public ResponseEntity postForEntity(String url, Object request, Class * @param urlVariables the variables to expand the template * @param the type of the return value * @return the converted object - * @throws RestClientException on client-side HTTP error * @see HttpEntity * @see RestTemplate#postForEntity(java.lang.String, java.lang.Object, * java.lang.Class, java.util.Map) */ public ResponseEntity postForEntity(String url, Object request, Class responseType, - Map urlVariables) throws RestClientException { + Map urlVariables) { return this.restTemplate.postForEntity(url, request, responseType, urlVariables); } @@ -492,12 +474,10 @@ public ResponseEntity postForEntity(String url, Object request, Class * @param responseType the response type to return * @param the type of the return value * @return the converted object - * @throws RestClientException on client-side HTTP error * @see HttpEntity * @see RestTemplate#postForEntity(java.net.URI, java.lang.Object, java.lang.Class) */ - public ResponseEntity postForEntity(URI url, Object request, Class responseType) - throws RestClientException { + public ResponseEntity postForEntity(URI url, Object request, Class responseType) { return this.restTemplate.postForEntity(applyRootUriIfNecessary(url), request, responseType); } @@ -508,14 +488,16 @@ public ResponseEntity postForEntity(URI url, Object request, Class res *

    * The {@code request} parameter can be a {@link HttpEntity} in order to add * additional HTTP headers to the request. + *

    + * If you need to assert the request result consider using the + * {@link TestRestTemplate#exchange exchange} method. * @param url the URL * @param request the Object to be PUT, may be {@code null} * @param urlVariables the variables to expand the template - * @throws RestClientException on client-side HTTP error * @see HttpEntity * @see RestTemplate#put(java.lang.String, java.lang.Object, java.lang.Object[]) */ - public void put(String url, Object request, Object... urlVariables) throws RestClientException { + public void put(String url, Object request, Object... urlVariables) { this.restTemplate.put(url, request, urlVariables); } @@ -526,14 +508,16 @@ public void put(String url, Object request, Object... urlVariables) throws RestC *

    * The {@code request} parameter can be a {@link HttpEntity} in order to add * additional HTTP headers to the request. + *

    + * If you need to assert the request result consider using the + * {@link TestRestTemplate#exchange exchange} method. * @param url the URL * @param request the Object to be PUT, may be {@code null} * @param urlVariables the variables to expand the template - * @throws RestClientException on client-side HTTP error * @see HttpEntity * @see RestTemplate#put(java.lang.String, java.lang.Object, java.util.Map) */ - public void put(String url, Object request, Map urlVariables) throws RestClientException { + public void put(String url, Object request, Map urlVariables) { this.restTemplate.put(url, request, urlVariables); } @@ -542,13 +526,15 @@ public void put(String url, Object request, Map urlVariables) throws *

    * The {@code request} parameter can be a {@link HttpEntity} in order to add * additional HTTP headers to the request. + *

    + * If you need to assert the request result consider using the + * {@link TestRestTemplate#exchange exchange} method. * @param url the URL * @param request the Object to be PUT, may be {@code null} - * @throws RestClientException on client-side HTTP error * @see HttpEntity * @see RestTemplate#put(java.net.URI, java.lang.Object) */ - public void put(URI url, Object request) throws RestClientException { + public void put(URI url, Object request) { this.restTemplate.put(applyRootUriIfNecessary(url), request); } @@ -566,12 +552,10 @@ public void put(URI url, Object request) throws RestClientException { * @param uriVariables the variables to expand the template * @param the type of the return value * @return the converted object - * @throws RestClientException on client-side HTTP error * @since 1.4.4 * @see HttpEntity */ - public T patchForObject(String url, Object request, Class responseType, Object... uriVariables) - throws RestClientException { + public T patchForObject(String url, Object request, Class responseType, Object... uriVariables) { return this.restTemplate.patchForObject(url, request, responseType, uriVariables); } @@ -589,12 +573,10 @@ public T patchForObject(String url, Object request, Class responseType, O * @param uriVariables the variables to expand the template * @param the type of the return value * @return the converted object - * @throws RestClientException on client-side HTTP error * @since 1.4.4 * @see HttpEntity */ - public T patchForObject(String url, Object request, Class responseType, Map uriVariables) - throws RestClientException { + public T patchForObject(String url, Object request, Class responseType, Map uriVariables) { return this.restTemplate.patchForObject(url, request, responseType, uriVariables); } @@ -609,11 +591,10 @@ public T patchForObject(String url, Object request, Class responseType, M * @param responseType the type of the return value * @param the type of the return value * @return the converted object - * @throws RestClientException on client-side HTTP error * @since 1.4.4 * @see HttpEntity */ - public T patchForObject(URI url, Object request, Class responseType) throws RestClientException { + public T patchForObject(URI url, Object request, Class responseType) { return this.restTemplate.patchForObject(applyRootUriIfNecessary(url), request, responseType); } @@ -621,12 +602,14 @@ public T patchForObject(URI url, Object request, Class responseType) thro * Delete the resources at the specified URI. *

    * URI Template variables are expanded using the given URI variables, if any. + *

    + * If you need to assert the request result consider using the + * {@link TestRestTemplate#exchange exchange} method. * @param url the URL * @param urlVariables the variables to expand in the template - * @throws RestClientException on client-side HTTP error * @see RestTemplate#delete(java.lang.String, java.lang.Object[]) */ - public void delete(String url, Object... urlVariables) throws RestClientException { + public void delete(String url, Object... urlVariables) { this.restTemplate.delete(url, urlVariables); } @@ -634,22 +617,26 @@ public void delete(String url, Object... urlVariables) throws RestClientExceptio * Delete the resources at the specified URI. *

    * URI Template variables are expanded using the given map. + *

    + * If you need to assert the request result consider using the + * {@link TestRestTemplate#exchange exchange} method. * @param url the URL * @param urlVariables the variables to expand the template - * @throws RestClientException on client-side HTTP error * @see RestTemplate#delete(java.lang.String, java.util.Map) */ - public void delete(String url, Map urlVariables) throws RestClientException { + public void delete(String url, Map urlVariables) { this.restTemplate.delete(url, urlVariables); } /** * Delete the resources at the specified URL. + *

    + * If you need to assert the request result consider using the + * {@link TestRestTemplate#exchange exchange} method. * @param url the URL - * @throws RestClientException on client-side HTTP error * @see RestTemplate#delete(java.net.URI) */ - public void delete(URI url) throws RestClientException { + public void delete(URI url) { this.restTemplate.delete(applyRootUriIfNecessary(url)); } @@ -660,10 +647,9 @@ public void delete(URI url) throws RestClientException { * @param url the URL * @param urlVariables the variables to expand in the template * @return the value of the allow header - * @throws RestClientException on client-side HTTP error * @see RestTemplate#optionsForAllow(java.lang.String, java.lang.Object[]) */ - public Set optionsForAllow(String url, Object... urlVariables) throws RestClientException { + public Set optionsForAllow(String url, Object... urlVariables) { return this.restTemplate.optionsForAllow(url, urlVariables); } @@ -674,10 +660,9 @@ public Set optionsForAllow(String url, Object... urlVariables) throw * @param url the URL * @param urlVariables the variables to expand in the template * @return the value of the allow header - * @throws RestClientException on client-side HTTP error * @see RestTemplate#optionsForAllow(java.lang.String, java.util.Map) */ - public Set optionsForAllow(String url, Map urlVariables) throws RestClientException { + public Set optionsForAllow(String url, Map urlVariables) { return this.restTemplate.optionsForAllow(url, urlVariables); } @@ -685,10 +670,9 @@ public Set optionsForAllow(String url, Map urlVariables) * Return the value of the Allow header for the given URL. * @param url the URL * @return the value of the allow header - * @throws RestClientException on client-side HTTP error * @see RestTemplate#optionsForAllow(java.net.URI) */ - public Set optionsForAllow(URI url) throws RestClientException { + public Set optionsForAllow(URI url) { return this.restTemplate.optionsForAllow(applyRootUriIfNecessary(url)); } @@ -705,12 +689,11 @@ public Set optionsForAllow(URI url) throws RestClientException { * @param urlVariables the variables to expand in the template * @param the type of the return value * @return the response as entity - * @throws RestClientException on client-side HTTP error * @see RestTemplate#exchange(java.lang.String, org.springframework.http.HttpMethod, * org.springframework.http.HttpEntity, java.lang.Class, java.lang.Object[]) */ public ResponseEntity exchange(String url, HttpMethod method, HttpEntity requestEntity, - Class responseType, Object... urlVariables) throws RestClientException { + Class responseType, Object... urlVariables) { return this.restTemplate.exchange(url, method, requestEntity, responseType, urlVariables); } @@ -727,12 +710,11 @@ public ResponseEntity exchange(String url, HttpMethod method, HttpEntity< * @param urlVariables the variables to expand in the template * @param the type of the return value * @return the response as entity - * @throws RestClientException on client-side HTTP error * @see RestTemplate#exchange(java.lang.String, org.springframework.http.HttpMethod, * org.springframework.http.HttpEntity, java.lang.Class, java.util.Map) */ public ResponseEntity exchange(String url, HttpMethod method, HttpEntity requestEntity, - Class responseType, Map urlVariables) throws RestClientException { + Class responseType, Map urlVariables) { return this.restTemplate.exchange(url, method, requestEntity, responseType, urlVariables); } @@ -746,12 +728,11 @@ public ResponseEntity exchange(String url, HttpMethod method, HttpEntity< * @param responseType the type of the return value * @param the type of the return value * @return the response as entity - * @throws RestClientException on client-side HTTP error * @see RestTemplate#exchange(java.net.URI, org.springframework.http.HttpMethod, * org.springframework.http.HttpEntity, java.lang.Class) */ public ResponseEntity exchange(URI url, HttpMethod method, HttpEntity requestEntity, - Class responseType) throws RestClientException { + Class responseType) { return this.restTemplate.exchange(applyRootUriIfNecessary(url), method, requestEntity, responseType); } @@ -771,13 +752,12 @@ public ResponseEntity exchange(URI url, HttpMethod method, HttpEntity * @param urlVariables the variables to expand in the template * @param the type of the return value * @return the response as entity - * @throws RestClientException on client-side HTTP error * @see RestTemplate#exchange(java.lang.String, org.springframework.http.HttpMethod, * org.springframework.http.HttpEntity, * org.springframework.core.ParameterizedTypeReference, java.lang.Object[]) */ public ResponseEntity exchange(String url, HttpMethod method, HttpEntity requestEntity, - ParameterizedTypeReference responseType, Object... urlVariables) throws RestClientException { + ParameterizedTypeReference responseType, Object... urlVariables) { return this.restTemplate.exchange(url, method, requestEntity, responseType, urlVariables); } @@ -797,13 +777,12 @@ public ResponseEntity exchange(String url, HttpMethod method, HttpEntity< * @param urlVariables the variables to expand in the template * @param the type of the return value * @return the response as entity - * @throws RestClientException on client-side HTTP error * @see RestTemplate#exchange(java.lang.String, org.springframework.http.HttpMethod, * org.springframework.http.HttpEntity, * org.springframework.core.ParameterizedTypeReference, java.util.Map) */ public ResponseEntity exchange(String url, HttpMethod method, HttpEntity requestEntity, - ParameterizedTypeReference responseType, Map urlVariables) throws RestClientException { + ParameterizedTypeReference responseType, Map urlVariables) { return this.restTemplate.exchange(url, method, requestEntity, responseType, urlVariables); } @@ -822,13 +801,12 @@ public ResponseEntity exchange(String url, HttpMethod method, HttpEntity< * @param responseType the type of the return value * @param the type of the return value * @return the response as entity - * @throws RestClientException on client-side HTTP error * @see RestTemplate#exchange(java.net.URI, org.springframework.http.HttpMethod, * org.springframework.http.HttpEntity, * org.springframework.core.ParameterizedTypeReference) */ public ResponseEntity exchange(URI url, HttpMethod method, HttpEntity requestEntity, - ParameterizedTypeReference responseType) throws RestClientException { + ParameterizedTypeReference responseType) { return this.restTemplate.exchange(applyRootUriIfNecessary(url), method, requestEntity, responseType); } @@ -844,11 +822,9 @@ public ResponseEntity exchange(URI url, HttpMethod method, HttpEntity * @param responseType the type of the return value * @param the type of the return value * @return the response as entity - * @throws RestClientException on client-side HTTP error * @see RestTemplate#exchange(org.springframework.http.RequestEntity, java.lang.Class) */ - public ResponseEntity exchange(RequestEntity requestEntity, Class responseType) - throws RestClientException { + public ResponseEntity exchange(RequestEntity requestEntity, Class responseType) { return this.restTemplate.exchange(createRequestEntityWithRootAppliedUri(requestEntity), responseType); } @@ -865,12 +841,10 @@ public ResponseEntity exchange(RequestEntity requestEntity, Class r * @param responseType the type of the return value * @param the type of the return value * @return the response as entity - * @throws RestClientException on client-side HTTP error * @see RestTemplate#exchange(org.springframework.http.RequestEntity, * org.springframework.core.ParameterizedTypeReference) */ - public ResponseEntity exchange(RequestEntity requestEntity, ParameterizedTypeReference responseType) - throws RestClientException { + public ResponseEntity exchange(RequestEntity requestEntity, ParameterizedTypeReference responseType) { return this.restTemplate.exchange(createRequestEntityWithRootAppliedUri(requestEntity), responseType); } @@ -886,13 +860,12 @@ public ResponseEntity exchange(RequestEntity requestEntity, Parameteri * @param urlVariables the variables to expand in the template * @param the type of the return value * @return an arbitrary object, as returned by the {@link ResponseExtractor} - * @throws RestClientException on client-side HTTP error * @see RestTemplate#execute(java.lang.String, org.springframework.http.HttpMethod, * org.springframework.web.client.RequestCallback, * org.springframework.web.client.ResponseExtractor, java.lang.Object[]) */ public T execute(String url, HttpMethod method, RequestCallback requestCallback, - ResponseExtractor responseExtractor, Object... urlVariables) throws RestClientException { + ResponseExtractor responseExtractor, Object... urlVariables) { return this.restTemplate.execute(url, method, requestCallback, responseExtractor, urlVariables); } @@ -908,13 +881,12 @@ public T execute(String url, HttpMethod method, RequestCallback requestCallb * @param urlVariables the variables to expand in the template * @param the type of the return value * @return an arbitrary object, as returned by the {@link ResponseExtractor} - * @throws RestClientException on client-side HTTP error * @see RestTemplate#execute(java.lang.String, org.springframework.http.HttpMethod, * org.springframework.web.client.RequestCallback, * org.springframework.web.client.ResponseExtractor, java.util.Map) */ public T execute(String url, HttpMethod method, RequestCallback requestCallback, - ResponseExtractor responseExtractor, Map urlVariables) throws RestClientException { + ResponseExtractor responseExtractor, Map urlVariables) { return this.restTemplate.execute(url, method, requestCallback, responseExtractor, urlVariables); } @@ -927,13 +899,12 @@ public T execute(String url, HttpMethod method, RequestCallback requestCallb * @param responseExtractor object that extracts the return value from the response * @param the type of the return value * @return an arbitrary object, as returned by the {@link ResponseExtractor} - * @throws RestClientException on client-side HTTP error * @see RestTemplate#execute(java.net.URI, org.springframework.http.HttpMethod, * org.springframework.web.client.RequestCallback, * org.springframework.web.client.ResponseExtractor) */ public T execute(URI url, HttpMethod method, RequestCallback requestCallback, - ResponseExtractor responseExtractor) throws RestClientException { + ResponseExtractor responseExtractor) { return this.restTemplate.execute(applyRootUriIfNecessary(url), method, requestCallback, responseExtractor); } @@ -965,7 +936,7 @@ public TestRestTemplate withBasicAuth(String username, String password) { @SuppressWarnings({ "rawtypes", "unchecked" }) private RequestEntity createRequestEntityWithRootAppliedUri(RequestEntity requestEntity) { return new RequestEntity(requestEntity.getBody(), requestEntity.getHeaders(), requestEntity.getMethod(), - applyRootUriIfNecessary(requestEntity.getUrl()), requestEntity.getType()); + applyRootUriIfNecessary(resolveUri(requestEntity)), requestEntity.getType()); } private URI applyRootUriIfNecessary(URI uri) { @@ -976,6 +947,23 @@ private URI applyRootUriIfNecessary(URI uri) { return uri; } + private URI resolveUri(RequestEntity entity) { + if (entity instanceof UriTemplateRequestEntity) { + UriTemplateRequestEntity templatedUriEntity = (UriTemplateRequestEntity) entity; + if (templatedUriEntity.getVars() != null) { + return this.restTemplate.getUriTemplateHandler().expand(templatedUriEntity.getUriTemplate(), + templatedUriEntity.getVars()); + } + else if (templatedUriEntity.getVarsMap() != null) { + return this.restTemplate.getUriTemplateHandler().expand(templatedUriEntity.getUriTemplate(), + templatedUriEntity.getVarsMap()); + } + throw new IllegalStateException( + "No variables specified for URI template: " + templatedUriEntity.getUriTemplate()); + } + return entity.getUrl(); + } + /** * Options used to customize the Apache HTTP Client. */ diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/TestRestTemplateContextCustomizer.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/TestRestTemplateContextCustomizer.java index 5d7c7b1a0593..6c64f0fcceca 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/TestRestTemplateContextCustomizer.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/TestRestTemplateContextCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,6 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.client.TestRestTemplate.HttpClientOption; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory; @@ -38,11 +37,9 @@ import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.ConfigurationClassPostProcessor; import org.springframework.core.Ordered; -import org.springframework.core.annotation.MergedAnnotation; -import org.springframework.core.annotation.MergedAnnotations; -import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.context.TestContextAnnotationUtils; /** * {@link ContextCustomizer} for {@link TestRestTemplate}. @@ -55,10 +52,9 @@ class TestRestTemplateContextCustomizer implements ContextCustomizer { @Override public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedContextConfiguration) { - MergedAnnotation annotation = MergedAnnotations - .from(mergedContextConfiguration.getTestClass(), SearchStrategy.INHERITED_ANNOTATIONS) - .get(SpringBootTest.class); - if (annotation.getEnum("webEnvironment", WebEnvironment.class).isEmbedded()) { + SpringBootTest springBootTest = TestContextAnnotationUtils + .findMergedAnnotation(mergedContextConfiguration.getTestClass(), SpringBootTest.class); + if (springBootTest.webEnvironment().isEmbedded()) { registerTestRestTemplate(context); } } diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/TestRestTemplateContextCustomizerFactory.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/TestRestTemplateContextCustomizerFactory.java index 727340237601..60b8d2ce2b15 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/TestRestTemplateContextCustomizerFactory.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/TestRestTemplateContextCustomizerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,11 +19,10 @@ import java.util.List; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.core.annotation.MergedAnnotations; -import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.ContextCustomizerFactory; +import org.springframework.test.context.TestContextAnnotationUtils; /** * {@link ContextCustomizerFactory} for {@link TestRestTemplate}. @@ -36,11 +35,9 @@ class TestRestTemplateContextCustomizerFactory implements ContextCustomizerFacto @Override public ContextCustomizer createContextCustomizer(Class testClass, List configAttributes) { - MergedAnnotations annotations = MergedAnnotations.from(testClass, SearchStrategy.INHERITED_ANNOTATIONS); - if (annotations.isPresent(SpringBootTest.class)) { - return new TestRestTemplateContextCustomizer(); - } - return null; + SpringBootTest springBootTest = TestContextAnnotationUtils.findMergedAnnotation(testClass, + SpringBootTest.class); + return (springBootTest != null) ? new TestRestTemplateContextCustomizer() : null; } } diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/reactive/server/WebTestClientContextCustomizer.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/reactive/server/WebTestClientContextCustomizer.java index 40bf66265264..e03aac4e4992 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/reactive/server/WebTestClientContextCustomizer.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/reactive/server/WebTestClientContextCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,8 +30,8 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.boot.WebApplicationType; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.web.codec.CodecCustomizer; import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory; import org.springframework.context.ApplicationContext; @@ -39,13 +39,14 @@ import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.ConfigurationClassPostProcessor; import org.springframework.core.Ordered; -import org.springframework.core.annotation.MergedAnnotation; -import org.springframework.core.annotation.MergedAnnotations; -import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.context.TestContextAnnotationUtils; import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.context.WebApplicationContext; import org.springframework.web.reactive.function.client.ExchangeStrategies; /** @@ -57,9 +58,9 @@ class WebTestClientContextCustomizer implements ContextCustomizer { @Override public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) { - MergedAnnotation annotation = MergedAnnotations - .from(mergedConfig.getTestClass(), SearchStrategy.INHERITED_ANNOTATIONS).get(SpringBootTest.class); - if (annotation.getEnum("webEnvironment", WebEnvironment.class).isEmbedded()) { + SpringBootTest springBootTest = TestContextAnnotationUtils.findMergedAnnotation(mergedConfig.getTestClass(), + SpringBootTest.class); + if (springBootTest.webEnvironment().isEmbedded()) { registerWebTestClient(context); } } @@ -132,6 +133,10 @@ public static class WebTestClientFactory implements FactoryBean, private WebTestClient object; + private static final String SERVLET_APPLICATION_CONTEXT_CLASS = "org.springframework.web.context.WebApplicationContext"; + + private static final String REACTIVE_APPLICATION_CONTEXT_CLASS = "org.springframework.boot.web.reactive.context.ReactiveWebApplicationContext"; + @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; @@ -158,13 +163,49 @@ public WebTestClient getObject() throws Exception { private WebTestClient createWebTestClient() { boolean sslEnabled = isSslEnabled(this.applicationContext); String port = this.applicationContext.getEnvironment().getProperty("local.server.port", "8080"); - String baseUrl = (sslEnabled ? "https" : "http") + "://localhost:" + port; + String baseUrl = getBaseUrl(sslEnabled, port); WebTestClient.Builder builder = WebTestClient.bindToServer(); customizeWebTestClientBuilder(builder, this.applicationContext); customizeWebTestClientCodecs(builder, this.applicationContext); return builder.baseUrl(baseUrl).build(); } + private String getBaseUrl(boolean sslEnabled, String port) { + String basePath = deduceBasePath(); + String pathSegment = (StringUtils.hasText(basePath)) ? basePath : ""; + return (sslEnabled ? "https" : "http") + "://localhost:" + port + pathSegment; + } + + private String deduceBasePath() { + WebApplicationType webApplicationType = deduceFromApplicationContext(this.applicationContext.getClass()); + if (webApplicationType == WebApplicationType.REACTIVE) { + return this.applicationContext.getEnvironment().getProperty("spring.webflux.base-path"); + } + else if (webApplicationType == WebApplicationType.SERVLET) { + return ((WebApplicationContext) this.applicationContext).getServletContext().getContextPath(); + } + return null; + } + + static WebApplicationType deduceFromApplicationContext(Class applicationContextClass) { + if (isAssignable(SERVLET_APPLICATION_CONTEXT_CLASS, applicationContextClass)) { + return WebApplicationType.SERVLET; + } + if (isAssignable(REACTIVE_APPLICATION_CONTEXT_CLASS, applicationContextClass)) { + return WebApplicationType.REACTIVE; + } + return WebApplicationType.NONE; + } + + private static boolean isAssignable(String target, Class type) { + try { + return ClassUtils.resolveClassName(target, null).isAssignableFrom(type); + } + catch (Throwable ex) { + return false; + } + } + private boolean isSslEnabled(ApplicationContext context) { try { AbstractReactiveWebServerFactory webServerFactory = context diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/reactive/server/WebTestClientContextCustomizerFactory.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/reactive/server/WebTestClientContextCustomizerFactory.java index 8bb872104a66..e41fab860895 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/reactive/server/WebTestClientContextCustomizerFactory.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/reactive/server/WebTestClientContextCustomizerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,34 +19,46 @@ import java.util.List; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.core.annotation.MergedAnnotations; -import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.ContextCustomizerFactory; +import org.springframework.test.context.TestContextAnnotationUtils; import org.springframework.util.ClassUtils; /** * {@link ContextCustomizerFactory} for {@code WebTestClient}. * * @author Stephane Nicoll + * @author Andy Wilkinson */ class WebTestClientContextCustomizerFactory implements ContextCustomizerFactory { - private static final String WEB_TEST_CLIENT_CLASS = "org.springframework.web.reactive.function.client.WebClient"; + private static final boolean reactorClientPresent; + + private static final boolean jettyClientPresent; + + private static final boolean httpComponentsClientPresent; + + private static final boolean webClientPresent; + + static { + ClassLoader loader = WebTestClientContextCustomizerFactory.class.getClassLoader(); + reactorClientPresent = ClassUtils.isPresent("reactor.netty.http.client.HttpClient", loader); + jettyClientPresent = ClassUtils.isPresent("org.eclipse.jetty.client.HttpClient", loader); + httpComponentsClientPresent = ClassUtils + .isPresent("org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient", loader) + && ClassUtils.isPresent("org.apache.hc.core5.reactive.ReactiveDataConsumer", loader); + webClientPresent = ClassUtils.isPresent("org.springframework.web.reactive.function.client.WebClient", loader); + } @Override public ContextCustomizer createContextCustomizer(Class testClass, List configAttributes) { - MergedAnnotations annotations = MergedAnnotations.from(testClass, SearchStrategy.INHERITED_ANNOTATIONS); - if (isWebClientPresent() && annotations.isPresent(SpringBootTest.class)) { - return new WebTestClientContextCustomizer(); - } - return null; - } - - private boolean isWebClientPresent() { - return ClassUtils.isPresent(WEB_TEST_CLIENT_CLASS, getClass().getClassLoader()); + SpringBootTest springBootTest = TestContextAnnotationUtils.findMergedAnnotation(testClass, + SpringBootTest.class); + return (springBootTest != null && webClientPresent + && (reactorClientPresent || jettyClientPresent || httpComponentsClientPresent)) + ? new WebTestClientContextCustomizer() : null; } } diff --git a/spring-boot-project/spring-boot-test/src/main/resources/mockito-extensions/org.mockito.plugins.MockResolver b/spring-boot-project/spring-boot-test/src/main/resources/mockito-extensions/org.mockito.plugins.MockResolver new file mode 100644 index 000000000000..3646a4b77555 --- /dev/null +++ b/spring-boot-project/spring-boot-test/src/main/resources/mockito-extensions/org.mockito.plugins.MockResolver @@ -0,0 +1 @@ +org.springframework.boot.test.mock.mockito.SpringBootMockResolver \ No newline at end of file diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/AbstractSpringBootTestEmbeddedReactiveWebEnvironmentTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/AbstractSpringBootTestEmbeddedReactiveWebEnvironmentTests.java index b868f702a555..2aa871007d6d 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/AbstractSpringBootTestEmbeddedReactiveWebEnvironmentTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/AbstractSpringBootTestEmbeddedReactiveWebEnvironmentTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.boot.test.context; +import java.time.Duration; + import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; @@ -67,8 +69,8 @@ ReactiveWebApplicationContext getContext() { @Test void runAndTestHttpEndpoint() { assertThat(this.port).isNotEqualTo(8080).isNotEqualTo(0); - WebTestClient.bindToServer().baseUrl("http://localhost:" + this.port).build().get().uri("/").exchange() - .expectBody(String.class).isEqualTo("Hello World"); + WebTestClient.bindToServer().baseUrl("http://localhost:" + this.port).responseTimeout(Duration.ofMinutes(5)) + .build().get().uri("/").exchange().expectBody(String.class).isEqualTo("Hello World"); } @Test diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/ConfigDataApplicationContextInitializerTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/ConfigDataApplicationContextInitializerTests.java new file mode 100644 index 000000000000..6c0e03ce6f77 --- /dev/null +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/ConfigDataApplicationContextInitializerTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.context; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConfigDataApplicationContextInitializer}. + * + * @author Phillip Webb + */ +@ExtendWith(SpringExtension.class) +@DirtiesContext +@ContextConfiguration(classes = ConfigDataApplicationContextInitializerTests.Config.class, + initializers = ConfigDataApplicationContextInitializer.class) +class ConfigDataApplicationContextInitializerTests { + + @Autowired + private Environment environment; + + @Test + void initializerPopulatesEnvironment() { + assertThat(this.environment.getProperty("foo")).isEqualTo("bucket"); + } + + @Configuration(proxyBeanMethods = false) + static class Config { + + } + +} diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/ConfigDataApplicationContextInitializerWithLegacySwitchTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/ConfigDataApplicationContextInitializerWithLegacySwitchTests.java new file mode 100644 index 000000000000..9247c3449aaa --- /dev/null +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/ConfigDataApplicationContextInitializerWithLegacySwitchTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.context; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConfigDataApplicationContextInitializer}. + * + * @author Phillip Webb + */ +@ExtendWith(SpringExtension.class) +@DirtiesContext +@TestPropertySource(properties = "spring.config.use-legacy-processing=true") +@ContextConfiguration(classes = ConfigDataApplicationContextInitializerWithLegacySwitchTests.Config.class, + initializers = ConfigDataApplicationContextInitializer.class) +class ConfigDataApplicationContextInitializerWithLegacySwitchTests { + + @Autowired + private Environment environment; + + @Test + void initializerPopulatesEnvironment() { + assertThat(this.environment.getProperty("foo")).isEqualTo("bucket"); + } + + @Configuration(proxyBeanMethods = false) + static class Config { + + } + +} diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/ConfigFileApplicationContextInitializerTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/ConfigFileApplicationContextInitializerTests.java index 045549480711..ba81a9e802eb 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/ConfigFileApplicationContextInitializerTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/ConfigFileApplicationContextInitializerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,7 @@ * * @author Phillip Webb */ +@Deprecated @ExtendWith(SpringExtension.class) @DirtiesContext @ContextConfiguration(classes = ConfigFileApplicationContextInitializerTests.Config.class, diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/FilteredClassLoaderTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/FilteredClassLoaderTests.java index bfbb355dbf0e..a488d433e471 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/FilteredClassLoaderTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/FilteredClassLoaderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** * Tests for {@link FilteredClassLoader}. @@ -43,7 +44,7 @@ void loadClassWhenFilteredOnPackageShouldThrowClassNotFound() throws Exception { try (FilteredClassLoader classLoader = new FilteredClassLoader( FilteredClassLoaderTests.class.getPackage().getName())) { assertThatExceptionOfType(ClassNotFoundException.class) - .isThrownBy(() -> classLoader.loadClass(getClass().getName())); + .isThrownBy(() -> Class.forName(getClass().getName(), false, classLoader)); } } @@ -51,14 +52,14 @@ void loadClassWhenFilteredOnPackageShouldThrowClassNotFound() throws Exception { void loadClassWhenFilteredOnClassShouldThrowClassNotFound() throws Exception { try (FilteredClassLoader classLoader = new FilteredClassLoader(FilteredClassLoaderTests.class)) { assertThatExceptionOfType(ClassNotFoundException.class) - .isThrownBy(() -> classLoader.loadClass(getClass().getName())); + .isThrownBy(() -> Class.forName(getClass().getName(), false, classLoader)); } } @Test void loadClassWhenNotFilteredShouldLoadClass() throws Exception { FilteredClassLoader classLoader = new FilteredClassLoader((className) -> false); - Class loaded = classLoader.loadClass(getClass().getName()); + Class loaded = Class.forName(getClass().getName(), false, classLoader); assertThat(loaded.getName()).isEqualTo(getClass().getName()); classLoader.close(); } @@ -111,4 +112,13 @@ void loadResourceAsStreamWhenNotFilteredShouldLoadResource() throws Exception { } } + @Test + void publicDefineClassWhenFilteredThrowsException() throws Exception { + Class hiddenClass = FilteredClassLoaderTests.class; + try (FilteredClassLoader classLoader = new FilteredClassLoader(hiddenClass)) { + assertThatIllegalArgumentException() + .isThrownBy(() -> classLoader.publicDefineClass(hiddenClass.getName(), new byte[] {}, null)); + } + } + } diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/ImportsContextCustomizerTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/ImportsContextCustomizerTests.java index 01f048b7d602..4f8239bc3398 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/ImportsContextCustomizerTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/ImportsContextCustomizerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,8 @@ import java.util.Set; import kotlin.Metadata; +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; @@ -72,6 +74,12 @@ void customizersForTestClassesWithDifferentSpockLangAnnotationsAreEqual() { .isEqualTo(new ImportsContextCustomizer(SecondSpockLangAnnotatedTestClass.class)); } + @Test + void customizersForTestClassesWithDifferentJunitAnnotationsAreEqual() { + assertThat(new ImportsContextCustomizer(FirstJunitAnnotatedTestClass.class)) + .isEqualTo(new ImportsContextCustomizer(SecondJunitAnnotatedTestClass.class)); + } + @Import(TestImportSelector.class) @Indicator1 static class FirstImportSelectorAnnotatedClass { @@ -97,35 +105,53 @@ static class SecondDeterminableImportSelectorAnnotatedClass { } @Metadata(d2 = "foo") + @Import(TestImportSelector.class) static class FirstKotlinAnnotatedTestClass { } @Metadata(d2 = "bar") + @Import(TestImportSelector.class) 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 { + + } + + @Tag("test") + @Import(TestImportSelector.class) + static class SecondJunitAnnotatedTestClass { + + } + @Retention(RetentionPolicy.RUNTIME) @interface Indicator1 { diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootContextLoaderTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootContextLoaderTests.java index 3167940d3ae7..c0554f67992b 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootContextLoaderTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootContextLoaderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,24 @@ package org.springframework.boot.test.context; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Configuration; -import org.springframework.test.context.ContextConfiguration; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.PropertySource; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.TestContext; import org.springframework.test.context.TestContextManager; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.support.TestPropertySourceUtils; import org.springframework.test.util.ReflectionTestUtils; @@ -35,60 +43,116 @@ * Tests for {@link SpringBootContextLoader} * * @author Stephane Nicoll + * @author Scott Frederick + * @author Madhura Bhave */ class SpringBootContextLoaderTests { @Test void environmentPropertiesSimple() { - Map config = getEnvironmentProperties(SimpleConfig.class); + Map config = getMergedContextConfigurationProperties(SimpleConfig.class); assertKey(config, "key", "myValue"); assertKey(config, "anotherKey", "anotherValue"); } @Test void environmentPropertiesSimpleNonAlias() { - Map config = getEnvironmentProperties(SimpleConfigNonAlias.class); + Map config = getMergedContextConfigurationProperties(SimpleConfigNonAlias.class); assertKey(config, "key", "myValue"); assertKey(config, "anotherKey", "anotherValue"); } @Test void environmentPropertiesOverrideDefaults() { - Map config = getEnvironmentProperties(OverrideConfig.class); + Map config = getMergedContextConfigurationProperties(OverrideConfig.class); assertKey(config, "server.port", "2345"); } @Test void environmentPropertiesAppend() { - Map config = getEnvironmentProperties(AppendConfig.class); + Map config = getMergedContextConfigurationProperties(AppendConfig.class); assertKey(config, "key", "myValue"); assertKey(config, "otherKey", "otherValue"); } @Test void environmentPropertiesSeparatorInValue() { - Map config = getEnvironmentProperties(SameSeparatorInValue.class); + Map config = getMergedContextConfigurationProperties(SameSeparatorInValue.class); assertKey(config, "key", "my=Value"); assertKey(config, "anotherKey", "another:Value"); } @Test void environmentPropertiesAnotherSeparatorInValue() { - Map config = getEnvironmentProperties(AnotherSeparatorInValue.class); + Map config = getMergedContextConfigurationProperties(AnotherSeparatorInValue.class); assertKey(config, "key", "my:Value"); assertKey(config, "anotherKey", "another=Value"); } - @Test + @Test // gh-4384 @Disabled void environmentPropertiesNewLineInValue() { - // gh-4384 - Map config = getEnvironmentProperties(NewLineInValue.class); + Map config = getMergedContextConfigurationProperties(NewLineInValue.class); assertKey(config, "key", "myValue"); assertKey(config, "variables", "foo=FOO\n bar=BAR"); } - private Map getEnvironmentProperties(Class testClass) { + @Test + void noActiveProfiles() { + assertThat(getActiveProfiles(SimpleConfig.class)).isEmpty(); + } + + @Test + void multipleActiveProfiles() { + assertThat(getActiveProfiles(MultipleActiveProfiles.class)).containsExactly("profile1", "profile2"); + } + + @Test + void activeProfileWithComma() { + assertThat(getActiveProfiles(ActiveProfileWithComma.class)).containsExactly("profile1,2"); + } + + @Test + // gh-28776 + void testPropertyValuesShouldTakePrecedenceWhenInlinedPropertiesPresent() { + TestContext context = new ExposedTestContextManager(SimpleConfig.class).getExposedTestContext(); + StandardEnvironment environment = (StandardEnvironment) context.getApplicationContext().getEnvironment(); + TestPropertyValues.of("key=thisValue").applyTo(environment); + assertThat(environment.getProperty("key")).isEqualTo("thisValue"); + assertThat(environment.getPropertySources().get("active-test-profiles")).isNull(); + } + + @Test + void testPropertyValuesShouldTakePrecedenceWhenInlinedPropertiesPresentAndProfilesActive() { + TestContext context = new ExposedTestContextManager(ActiveProfileWithInlinedProperties.class) + .getExposedTestContext(); + StandardEnvironment environment = (StandardEnvironment) context.getApplicationContext().getEnvironment(); + TestPropertyValues.of("key=thisValue").applyTo(environment); + assertThat(environment.getProperty("key")).isEqualTo("thisValue"); + assertThat(environment.getPropertySources().get("active-test-profiles")).isNotNull(); + } + + @Test + void propertySourceOrdering() throws Exception { + TestContext context = new ExposedTestContextManager(PropertySourceOrdering.class).getExposedTestContext(); + ConfigurableEnvironment environment = (ConfigurableEnvironment) context.getApplicationContext() + .getEnvironment(); + List names = environment.getPropertySources().stream().map(PropertySource::getName) + .collect(Collectors.toList()); + String last = names.remove(names.size() - 1); + assertThat(names).containsExactly("configurationProperties", "commandLineArgs", "Inlined Test Properties", + "servletConfigInitParams", "servletContextInitParams", "systemProperties", "systemEnvironment", + "random"); + assertThat(last).startsWith("Config resource"); + } + + private String[] getActiveProfiles(Class testClass) { + TestContext testContext = new ExposedTestContextManager(testClass).getExposedTestContext(); + ApplicationContext applicationContext = testContext.getApplicationContext(); + return applicationContext.getEnvironment().getActiveProfiles(); + } + + private Map getMergedContextConfigurationProperties(Class testClass) { TestContext context = new ExposedTestContextManager(testClass).getExposedTestContext(); MergedContextConfiguration config = (MergedContextConfiguration) ReflectionTestUtils.getField(context, "mergedContextConfiguration"); @@ -100,48 +164,65 @@ private void assertKey(Map actual, String key, Object value) { assertThat(actual.get(key)).isEqualTo(value); } - @SpringBootTest({ "key=myValue", "anotherKey:anotherValue" }) - @ContextConfiguration(classes = Config.class) + @SpringBootTest(properties = { "key=myValue", "anotherKey:anotherValue" }, classes = Config.class) static class SimpleConfig { } - @SpringBootTest(properties = { "key=myValue", "anotherKey:anotherValue" }) - @ContextConfiguration(classes = Config.class) + @SpringBootTest(properties = { "key=myValue", "anotherKey:anotherValue" }, classes = Config.class) static class SimpleConfigNonAlias { } - @SpringBootTest("server.port=2345") - @ContextConfiguration(classes = Config.class) + @SpringBootTest(properties = "server.port=2345", classes = Config.class) static class OverrideConfig { } - @SpringBootTest({ "key=myValue", "otherKey=otherValue" }) - @ContextConfiguration(classes = Config.class) + @SpringBootTest(properties = { "key=myValue", "otherKey=otherValue" }, classes = Config.class) static class AppendConfig { } - @SpringBootTest({ "key=my=Value", "anotherKey:another:Value" }) - @ContextConfiguration(classes = Config.class) + @SpringBootTest(properties = { "key=my=Value", "anotherKey:another:Value" }, classes = Config.class) static class SameSeparatorInValue { } - @SpringBootTest({ "key=my:Value", "anotherKey:another=Value" }) - @ContextConfiguration(classes = Config.class) + @SpringBootTest(properties = { "key=my:Value", "anotherKey:another=Value" }, classes = Config.class) static class AnotherSeparatorInValue { } - @SpringBootTest({ "key=myValue", "variables=foo=FOO\n bar=BAR" }) - @ContextConfiguration(classes = Config.class) + @SpringBootTest(properties = { "key=myValue", "variables=foo=FOO\n bar=BAR" }, classes = Config.class) static class NewLineInValue { } + @SpringBootTest(classes = Config.class) + @ActiveProfiles({ "profile1", "profile2" }) + static class MultipleActiveProfiles { + + } + + @SpringBootTest(classes = Config.class) + @ActiveProfiles({ "profile1,2" }) + static class ActiveProfileWithComma { + + } + + @SpringBootTest(properties = { "key=myValue" }, classes = Config.class) + @ActiveProfiles({ "profile1,2" }) + static class ActiveProfileWithInlinedProperties { + + } + + @SpringBootTest(classes = Config.class, args = "args", properties = "one=1") + @TestPropertySource(properties = "two=2") + static class PropertySourceOrdering { + + } + @Configuration(proxyBeanMethods = false) static class Config { diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootTestWebEnvironmentDefinedPortTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootTestWebEnvironmentDefinedPortTests.java index a8611240e370..0b7f718daed5 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootTestWebEnvironmentDefinedPortTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootTestWebEnvironmentDefinedPortTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,7 @@ */ @DirtiesContext @SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT, properties = { "server.port=0", "value=123" }) -public class SpringBootTestWebEnvironmentDefinedPortTests extends AbstractSpringBootTestWebServerWebEnvironmentTests { +class SpringBootTestWebEnvironmentDefinedPortTests extends AbstractSpringBootTestWebServerWebEnvironmentTests { @Configuration(proxyBeanMethods = false) @EnableWebMvc diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootTestWithActiveProfilesAndEnvironmentPropertyTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootTestWithActiveProfilesAndEnvironmentPropertyTests.java new file mode 100644 index 000000000000..19ff569e93ec --- /dev/null +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootTestWithActiveProfilesAndEnvironmentPropertyTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.context; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link SpringBootTest @SpringBootTest} with an + * {@link ActiveProfiles @ActiveProfiles} annotation. + * + * @author Johnny Lim + * @author Phillip Webb + */ +@SpringBootTest +@ActiveProfiles({ "test1", "test2" }) +@ContextConfiguration(loader = SpringBootTestWithActiveProfilesAndEnvironmentPropertyTests.Loader.class) +class SpringBootTestWithActiveProfilesAndEnvironmentPropertyTests { + + @Autowired + private Environment environment; + + @Test + void getActiveProfiles() { + assertThat(this.environment.getActiveProfiles()).containsOnly("test1", "test2"); + } + + @Configuration + static class Config { + + } + + static class Loader extends SpringBootContextLoader { + + @Override + protected ConfigurableEnvironment getEnvironment() { + ConfigurableEnvironment environment = super.getEnvironment(); + MutablePropertySources sources = environment.getPropertySources(); + Map map = new LinkedHashMap<>(); + map.put("spring.profiles.active", "local"); + sources.addLast(new MapPropertySource("profiletest", map)); + return environment; + } + + } + +} diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootTestWithActiveProfilesAndSystemEnvironmentPropertyTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootTestWithActiveProfilesAndSystemEnvironmentPropertyTests.java new file mode 100644 index 000000000000..66b54c2caf51 --- /dev/null +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootTestWithActiveProfilesAndSystemEnvironmentPropertyTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.context; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySource; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link SpringBootTest @SpringBootTest} with an + * {@link ActiveProfiles @ActiveProfiles} annotation. + * + * @author Johnny Lim + * @author Phillip Webb + */ +@SpringBootTest +@ActiveProfiles({ "test1", "test2" }) +@ContextConfiguration(loader = SpringBootTestWithActiveProfilesAndSystemEnvironmentPropertyTests.Loader.class) +class SpringBootTestWithActiveProfilesAndSystemEnvironmentPropertyTests { + + @Autowired + private Environment environment; + + @Test + void getActiveProfiles() { + assertThat(this.environment.getActiveProfiles()).containsOnly("test1", "test2"); + } + + @Configuration + static class Config { + + } + + static class Loader extends SpringBootContextLoader { + + @Override + @SuppressWarnings("unchecked") + protected ConfigurableEnvironment getEnvironment() { + ConfigurableEnvironment environment = super.getEnvironment(); + MutablePropertySources sources = environment.getPropertySources(); + PropertySource source = sources.get(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME); + Map map = new LinkedHashMap<>((Map) source.getSource()); + map.put("SPRING_PROFILES_ACTIVE", "local"); + sources.replace(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, + new MapPropertySource(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, map)); + return environment; + } + + } + +} diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootTestWithCustomEnvironmentTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootTestWithCustomEnvironmentTests.java new file mode 100644 index 000000000000..24258f5af0d8 --- /dev/null +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootTestWithCustomEnvironmentTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.context; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.mock.env.MockEnvironment; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link SpringBootTest @SpringBootTest} with a custom + * {@link Environment}. + * + * @author Madhura Bhave + */ +@SpringBootTest +@ActiveProfiles({ "test1", "test2" }) +@ContextConfiguration(loader = SpringBootTestWithCustomEnvironmentTests.Loader.class) +class SpringBootTestWithCustomEnvironmentTests { + + @Autowired + private Environment environment; + + @Test + void getActiveProfiles() { + assertThat(this.environment).isInstanceOf(MockEnvironment.class); + assertThat(this.environment.getActiveProfiles()).containsOnly("test1", "test2"); + } + + @Configuration + static class Config { + + } + + static class Loader extends SpringBootContextLoader { + + @Override + protected ConfigurableEnvironment getEnvironment() { + return new MockEnvironment(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/assertj/ApplicationContextAssertProviderTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/assertj/ApplicationContextAssertProviderTests.java index 5931b9cff7f9..b77726ec5f3b 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/assertj/ApplicationContextAssertProviderTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/assertj/ApplicationContextAssertProviderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,8 +20,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; @@ -30,7 +31,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.mockito.Mockito.verify; +import static org.mockito.BDDMockito.then; /** * Tests for {@link ApplicationContextAssertProvider} and @@ -38,6 +39,7 @@ * * @author Phillip Webb */ +@ExtendWith(MockitoExtension.class) class ApplicationContextAssertProviderTests { @Mock @@ -51,7 +53,6 @@ class ApplicationContextAssertProviderTests { @BeforeEach void setup() { - MockitoAnnotations.initMocks(this); this.startupFailure = new RuntimeException(); this.mockContextSupplier = () -> this.mockContext; this.startupFailureSupplier = () -> { @@ -101,7 +102,7 @@ void getWhenContextStartsShouldReturnProxyThatCallsRealMethods() { ApplicationContextAssertProvider context = get(this.mockContextSupplier); assertThat((Object) context).isNotNull(); context.getBean("foo"); - verify(this.mockContext).getBean("foo"); + then(this.mockContext).should().getBean("foo"); } @Test @@ -185,7 +186,7 @@ void toStringWhenContextFailsToStartShouldReturnSimpleString() { void closeShouldCloseContext() { ApplicationContextAssertProvider context = get(this.mockContextSupplier); context.close(); - verify(this.mockContext).close(); + then(this.mockContext).should().close(); } private ApplicationContextAssertProvider get(Supplier contextSupplier) { diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/bootstrap/SpringBootTestContextBootstrapperTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/bootstrap/SpringBootTestContextBootstrapperTests.java index a64bdd0ca224..dc203560949b 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/bootstrap/SpringBootTestContextBootstrapperTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/bootstrap/SpringBootTestContextBootstrapperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,8 +23,12 @@ import org.springframework.boot.test.context.SpringBootTestContextBootstrapper; import org.springframework.test.context.BootstrapContext; import org.springframework.test.context.CacheAwareContextLoaderDelegate; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.context.TestContext; import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.util.ReflectionTestUtils; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -50,15 +54,63 @@ void springBootTestWithAMockWebEnvironmentCanBeUsedWithWebAppConfiguration() { buildTestContext(SpringBootTestMockWebEnvironmentAndWebAppConfiguration.class); } + @Test + void mergedContextConfigurationWhenArgsDifferentShouldNotBeConsideredEqual() { + TestContext context = buildTestContext(SpringBootTestArgsConfiguration.class); + MergedContextConfiguration contextConfiguration = getMergedContextConfiguration(context); + TestContext otherContext2 = buildTestContext(SpringBootTestOtherArgsConfiguration.class); + MergedContextConfiguration otherContextConfiguration = getMergedContextConfiguration(otherContext2); + assertThat(contextConfiguration).isNotEqualTo(otherContextConfiguration); + } + + @Test + void mergedContextConfigurationWhenArgsSameShouldBeConsideredEqual() { + TestContext context = buildTestContext(SpringBootTestArgsConfiguration.class); + MergedContextConfiguration contextConfiguration = getMergedContextConfiguration(context); + TestContext otherContext2 = buildTestContext(SpringBootTestSameArgsConfiguration.class); + MergedContextConfiguration otherContextConfiguration = getMergedContextConfiguration(otherContext2); + assertThat(contextConfiguration).isEqualTo(otherContextConfiguration); + } + + @Test + void mergedContextConfigurationWhenWebEnvironmentsDifferentShouldNotBeConsideredEqual() { + TestContext context = buildTestContext(SpringBootTestMockWebEnvironmentConfiguration.class); + MergedContextConfiguration contextConfiguration = getMergedContextConfiguration(context); + TestContext otherContext = buildTestContext(SpringBootTestDefinedPortWebEnvironmentConfiguration.class); + MergedContextConfiguration otherContextConfiguration = getMergedContextConfiguration(otherContext); + assertThat(contextConfiguration).isNotEqualTo(otherContextConfiguration); + } + + @Test + void mergedContextConfigurationWhenWebEnvironmentsSameShouldBeConsideredEqual() { + TestContext context = buildTestContext(SpringBootTestMockWebEnvironmentConfiguration.class); + MergedContextConfiguration contextConfiguration = getMergedContextConfiguration(context); + TestContext otherContext = buildTestContext(SpringBootTestAnotherMockWebEnvironmentConfiguration.class); + MergedContextConfiguration otherContextConfiguration = getMergedContextConfiguration(otherContext); + assertThat(contextConfiguration).isEqualTo(otherContextConfiguration); + } + + @Test + void mergedContextConfigurationClassesShouldNotContainDuplicates() { + TestContext context = buildTestContext(SpringBootTestClassesConfiguration.class); + MergedContextConfiguration contextConfiguration = getMergedContextConfiguration(context); + Class[] classes = contextConfiguration.getClasses(); + assertThat(classes).containsExactly(SpringBootTestContextBootstrapperExampleConfig.class); + } + @SuppressWarnings("rawtypes") - private void buildTestContext(Class testClass) { + private TestContext buildTestContext(Class testClass) { SpringBootTestContextBootstrapper bootstrapper = new SpringBootTestContextBootstrapper(); BootstrapContext bootstrapContext = mock(BootstrapContext.class); bootstrapper.setBootstrapContext(bootstrapContext); given((Class) bootstrapContext.getTestClass()).willReturn(testClass); CacheAwareContextLoaderDelegate contextLoaderDelegate = mock(CacheAwareContextLoaderDelegate.class); given(bootstrapContext.getCacheAwareContextLoaderDelegate()).willReturn(contextLoaderDelegate); - bootstrapper.buildTestContext(); + return bootstrapper.buildTestContext(); + } + + private MergedContextConfiguration getMergedContextConfiguration(TestContext context) { + return (MergedContextConfiguration) ReflectionTestUtils.getField(context, "mergedContextConfiguration"); } @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @@ -73,4 +125,39 @@ static class SpringBootTestMockWebEnvironmentAndWebAppConfiguration { } + @SpringBootTest(args = "--app.test=same") + static class SpringBootTestArgsConfiguration { + + } + + @SpringBootTest(webEnvironment = WebEnvironment.MOCK) + static class SpringBootTestMockWebEnvironmentConfiguration { + + } + + @SpringBootTest(webEnvironment = WebEnvironment.MOCK) + static class SpringBootTestAnotherMockWebEnvironmentConfiguration { + + } + + @SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT) + static class SpringBootTestDefinedPortWebEnvironmentConfiguration { + + } + + @SpringBootTest(args = "--app.test=same") + static class SpringBootTestSameArgsConfiguration { + + } + + @SpringBootTest(args = "--app.test=different") + static class SpringBootTestOtherArgsConfiguration { + + } + + @SpringBootTest(classes = SpringBootTestContextBootstrapperExampleConfig.class) + static class SpringBootTestClassesConfiguration { + + } + } diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/nestedtests/InheritedNestedTestConfigurationTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/nestedtests/InheritedNestedTestConfigurationTests.java new file mode 100644 index 000000000000..c898c6cbfd71 --- /dev/null +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/nestedtests/InheritedNestedTestConfigurationTests.java @@ -0,0 +1,107 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.context.nestedtests; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.nestedtests.InheritedNestedTestConfigurationTests.ActionPerformer; +import org.springframework.boot.test.context.nestedtests.InheritedNestedTestConfigurationTests.AppConfiguration; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.stereotype.Component; + +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.times; + +/** + * Tests for nested test configuration when the configuration is inherited from the + * enclosing class (the default behaviour). + * + * @author Andy Wilkinson + */ +@SpringBootTest(classes = AppConfiguration.class) +@Import(ActionPerformer.class) +class InheritedNestedTestConfigurationTests { + + @MockBean + Action action; + + @Autowired + ActionPerformer performer; + + @Test + void mockWasInvokedOnce() { + this.performer.run(); + then(this.action).should().perform(); + } + + @Test + void mockWasInvokedTwice() { + this.performer.run(); + this.performer.run(); + then(this.action).should(times(2)).perform(); + } + + @Nested + class InnerTests { + + @Test + void mockWasInvokedOnce() { + InheritedNestedTestConfigurationTests.this.performer.run(); + then(InheritedNestedTestConfigurationTests.this.action).should().perform(); + } + + @Test + void mockWasInvokedTwice() { + InheritedNestedTestConfigurationTests.this.performer.run(); + InheritedNestedTestConfigurationTests.this.performer.run(); + then(InheritedNestedTestConfigurationTests.this.action).should(times(2)).perform(); + } + + } + + @Component + static class ActionPerformer { + + private final Action action; + + ActionPerformer(Action action) { + this.action = action; + } + + void run() { + this.action.perform(); + } + + } + + public interface Action { + + void perform(); + + } + + @SpringBootConfiguration + static class AppConfiguration { + + } + +} diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunnerTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunnerTests.java index f87e2c0a0775..6f4e20b6434c 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunnerTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunnerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,11 @@ import com.google.gson.Gson; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.annotation.UserConfigurations; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.ApplicationContextAssertProvider; import org.springframework.context.ConfigurableApplicationContext; @@ -32,6 +36,7 @@ import org.springframework.context.annotation.ConditionContext; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; import org.springframework.core.env.Environment; import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.util.ClassUtils; @@ -139,24 +144,6 @@ void runWithUserBeanShouldRegisterBeanWithDefaultName() { get().withBean(String.class, () -> "foo").run((context) -> assertThat(context).hasBean("string")); } - @Test - void runWithUserBeanShouldBeRegisteredInOrder() { - get().withBean(String.class, () -> "one").withBean(String.class, () -> "two") - .withBean(String.class, () -> "three").run((context) -> { - assertThat(context).hasBean("string"); - assertThat(context.getBean("string")).isEqualTo("three"); - }); - } - - @Test - void runWithConfigurationsAndUserBeanShouldRegisterUserBeanLast() { - get().withUserConfiguration(FooConfig.class).withBean("foo", String.class, () -> "overridden") - .run((context) -> { - assertThat(context).hasBean("foo"); - assertThat(context.getBean("foo")).isEqualTo("overridden"); - }); - } - @Test void runWithMultipleConfigurationsShouldRegisterAllConfigurations() { get().withUserConfiguration(FooConfig.class).withConfiguration(UserConfigurations.of(BarConfig.class)) @@ -182,12 +169,49 @@ void runWithClassLoaderShouldSetClassLoaderOnConditionContext() { .run((context) -> assertThat(context).hasSingleBean(ConditionalConfig.class)); } + @Test + void consecutiveRunWithFilteredClassLoaderShouldHaveBeanWithLazyProperties() { + get().withClassLoader(new FilteredClassLoader(Gson.class)).withUserConfiguration(LazyConfig.class) + .run((context) -> assertThat(context).hasSingleBean(ExampleBeanWithLazyProperties.class)); + + get().withClassLoader(new FilteredClassLoader(Gson.class)).withUserConfiguration(LazyConfig.class) + .run((context) -> assertThat(context).hasSingleBean(ExampleBeanWithLazyProperties.class)); + } + @Test void thrownRuleWorksWithCheckedException() { get().run((context) -> assertThatIOException().isThrownBy(() -> throwCheckedException("Expected message")) .withMessageContaining("Expected message")); } + @Test + void runDisablesBeanOverridingByDefault() { + get().withUserConfiguration(FooConfig.class).withBean("foo", Integer.class, () -> 42).run((context) -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()).isInstanceOf(BeanDefinitionStoreException.class) + .hasMessageContaining("Invalid bean definition with name 'foo'") + .hasMessageContaining("@Bean definition illegally overridden by existing bean definition"); + }); + } + + @Test + void runWithUserBeanShouldBeRegisteredInOrder() { + get().withAllowBeanDefinitionOverriding(true).withBean(String.class, () -> "one") + .withBean(String.class, () -> "two").withBean(String.class, () -> "three").run((context) -> { + assertThat(context).hasBean("string"); + assertThat(context.getBean("string")).isEqualTo("three"); + }); + } + + @Test + void runWithConfigurationsAndUserBeanShouldRegisterUserBeanLast() { + get().withAllowBeanDefinitionOverriding(true).withUserConfiguration(FooConfig.class) + .withBean("foo", String.class, () -> "overridden").run((context) -> { + assertThat(context).hasBean("foo"); + assertThat(context.getBean("foo")).isEqualTo("overridden"); + }); + } + protected abstract T get(); private static void throwCheckedException(String message) throws IOException { @@ -230,6 +254,30 @@ static class ConditionalConfig { } + @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties(ExampleProperties.class) + static class LazyConfig { + + @Bean + ExampleBeanWithLazyProperties exampleBeanWithLazyProperties() { + return new ExampleBeanWithLazyProperties(); + } + + } + + static class ExampleBeanWithLazyProperties { + + @Autowired + @Lazy + ExampleProperties exampleProperties; + + } + + @ConfigurationProperties + public static class ExampleProperties { + + } + static class FilteredClassLoaderCondition implements Condition { @Override diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/json/JsonContentAssertTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/json/JsonContentAssertTests.java index ed5765661f9f..5d729cde6e41 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/json/JsonContentAssertTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/json/JsonContentAssertTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -116,7 +116,7 @@ void isEqualToWhenFileIsMatchingShouldPass() throws Exception { } @Test - void isEqualToWhenFileIsNotMatchingShouldFail() throws Exception { + void isEqualToWhenFileIsNotMatchingShouldFail() { assertThatExceptionOfType(AssertionError.class) .isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(createFile(DIFFERENT))); } @@ -199,7 +199,7 @@ void isEqualToJsonWhenFileIsMatchingShouldPass() throws Exception { } @Test - void isEqualToJsonWhenFileIsNotMatchingShouldFail() throws Exception { + void isEqualToJsonWhenFileIsNotMatchingShouldFail() { assertThatExceptionOfType(AssertionError.class) .isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualToJson(createFile(DIFFERENT))); } @@ -276,7 +276,7 @@ void isStrictlyEqualToJsonWhenFileIsMatchingShouldPass() throws Exception { } @Test - void isStrictlyEqualToJsonWhenFileIsNotMatchingShouldFail() throws Exception { + void isStrictlyEqualToJsonWhenFileIsNotMatchingShouldFail() { assertThatExceptionOfType(AssertionError.class) .isThrownBy(() -> assertThat(forJson(SOURCE)).isStrictlyEqualToJson(createFile(LENIENT_SAME))); } @@ -430,7 +430,7 @@ void isEqualToJsonWhenFileIsMatchingAndComparatorShouldPass() throws Exception { } @Test - void isEqualToJsonWhenFileIsNotMatchingAndComparatorShouldFail() throws Exception { + void isEqualToJsonWhenFileIsNotMatchingAndComparatorShouldFail() { assertThatExceptionOfType(AssertionError.class) .isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualToJson(createFile(DIFFERENT), COMPARATOR)); } @@ -496,7 +496,7 @@ void isNotEqualToWhenBytesAreNotMatchingShouldPass() { } @Test - void isNotEqualToWhenFileIsMatchingShouldFail() throws Exception { + void isNotEqualToWhenFileIsMatchingShouldFail() { assertThatExceptionOfType(AssertionError.class) .isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualTo(createFile(LENIENT_SAME))); } @@ -578,7 +578,7 @@ void isNotEqualToJsonWhenBytesAreNotMatchingShouldPass() { } @Test - void isNotEqualToJsonWhenFileIsMatchingShouldFail() throws Exception { + void isNotEqualToJsonWhenFileIsMatchingShouldFail() { assertThatExceptionOfType(AssertionError.class) .isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualToJson(createFile(LENIENT_SAME))); } @@ -655,7 +655,7 @@ void isNotStrictlyEqualToJsonWhenBytesAreNotMatchingShouldPass() { } @Test - void isNotStrictlyEqualToJsonWhenFileIsMatchingShouldFail() throws Exception { + void isNotStrictlyEqualToJsonWhenFileIsMatchingShouldFail() { assertThatExceptionOfType(AssertionError.class) .isThrownBy(() -> assertThat(forJson(SOURCE)).isNotStrictlyEqualToJson(createFile(SOURCE))); } @@ -732,7 +732,7 @@ void isNotEqualToJsonWhenBytesAreNotMatchingAndLenientShouldPass() { } @Test - void isNotEqualToJsonWhenFileIsMatchingAndLenientShouldFail() throws Exception { + void isNotEqualToJsonWhenFileIsMatchingAndLenientShouldFail() { assertThatExceptionOfType(AssertionError.class).isThrownBy( () -> assertThat(forJson(SOURCE)).isNotEqualToJson(createFile(LENIENT_SAME), JSONCompareMode.LENIENT)); } @@ -809,7 +809,7 @@ void isNotEqualToJsonWhenBytesAreNotMatchingAndComparatorShouldPass() { } @Test - void isNotEqualToJsonWhenFileIsMatchingAndComparatorShouldFail() throws Exception { + void isNotEqualToJsonWhenFileIsMatchingAndComparatorShouldFail() { assertThatExceptionOfType(AssertionError.class) .isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualToJson(createFile(LENIENT_SAME), COMPARATOR)); } diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/json/JsonbTesterTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/json/JsonbTesterTests.java index 29892aeb8feb..92237238c2c4 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/json/JsonbTesterTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/json/JsonbTesterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/AbstractMockBeanOnGenericExtensionTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/AbstractMockBeanOnGenericExtensionTests.java new file mode 100644 index 000000000000..8481e999bd58 --- /dev/null +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/AbstractMockBeanOnGenericExtensionTests.java @@ -0,0 +1,27 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.mock.mockito; + +/** + * Concrete implementation of {@link AbstractMockBeanOnGenericTests}. + * + * @author Madhura Bhave + */ +class AbstractMockBeanOnGenericExtensionTests extends + AbstractMockBeanOnGenericTests { + +} diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/AbstractMockBeanOnGenericTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/AbstractMockBeanOnGenericTests.java new file mode 100644 index 000000000000..76ae636c5749 --- /dev/null +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/AbstractMockBeanOnGenericTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.mock.mockito; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MockBean} with abstract class and generics. + * + * @author Madhura Bhave + */ +@SpringBootTest(classes = AbstractMockBeanOnGenericTests.TestConfiguration.class) +abstract class AbstractMockBeanOnGenericTests, U extends AbstractMockBeanOnGenericTests.Something> { + + @Autowired + private T thing; + + @MockBean + private U something; + + @Test + void mockBeanShouldResolveConcreteType() { + assertThat(this.something).isInstanceOf(SomethingImpl.class); + } + + abstract static class Thing { + + @Autowired + private T something; + + T getSomething() { + return this.something; + } + + void setSomething(T something) { + this.something = something; + } + + } + + static class SomethingImpl extends Something { + + } + + static class ThingImpl extends Thing { + + } + + static class Something { + + } + + @Configuration + static class TestConfiguration { + + @Bean + ThingImpl thing() { + return new ThingImpl(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockBeanContextCachingTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockBeanContextCachingTests.java new file mode 100644 index 000000000000..5d84902dda6f --- /dev/null +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockBeanContextCachingTests.java @@ -0,0 +1,128 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.mock.mockito; + +import java.util.Map; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTestContextBootstrapper; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.BootstrapContext; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate; +import org.springframework.test.context.cache.DefaultContextCache; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for application context caching when using {@link MockBean @MockBean}. + * + * @author Andy Wilkinson + */ +class MockBeanContextCachingTests { + + private final DefaultContextCache contextCache = new DefaultContextCache(); + + private final DefaultCacheAwareContextLoaderDelegate delegate = new DefaultCacheAwareContextLoaderDelegate( + this.contextCache); + + @AfterEach + @SuppressWarnings("unchecked") + void clearCache() { + Map contexts = (Map) ReflectionTestUtils + .getField(this.contextCache, "contextMap"); + for (ApplicationContext context : contexts.values()) { + if (context instanceof ConfigurableApplicationContext) { + ((ConfigurableApplicationContext) context).close(); + } + } + this.contextCache.clear(); + } + + @Test + void whenThereIsANormalBeanAndAMockBeanThenTwoContextsAreCreated() { + bootstrapContext(TestClass.class); + assertThat(this.contextCache.size()).isEqualTo(1); + bootstrapContext(MockedBeanTestClass.class); + assertThat(this.contextCache.size()).isEqualTo(2); + } + + @Test + void whenThereIsTheSameMockedBeanInEachTestClassThenOneContextIsCreated() { + bootstrapContext(MockedBeanTestClass.class); + assertThat(this.contextCache.size()).isEqualTo(1); + bootstrapContext(AnotherMockedBeanTestClass.class); + assertThat(this.contextCache.size()).isEqualTo(1); + } + + @SuppressWarnings("rawtypes") + private void bootstrapContext(Class testClass) { + SpringBootTestContextBootstrapper bootstrapper = new SpringBootTestContextBootstrapper(); + BootstrapContext bootstrapContext = mock(BootstrapContext.class); + given((Class) bootstrapContext.getTestClass()).willReturn(testClass); + bootstrapper.setBootstrapContext(bootstrapContext); + given(bootstrapContext.getCacheAwareContextLoaderDelegate()).willReturn(this.delegate); + TestContext testContext = bootstrapper.buildTestContext(); + testContext.getApplicationContext(); + } + + @SpringBootTest(classes = TestConfiguration.class) + static class TestClass { + + } + + @SpringBootTest(classes = TestConfiguration.class) + static class MockedBeanTestClass { + + @MockBean + private TestBean testBean; + + } + + @SpringBootTest(classes = TestConfiguration.class) + static class AnotherMockedBeanTestClass { + + @MockBean + private TestBean testBean; + + } + + @Configuration + static class TestConfiguration { + + @Bean + TestBean testBean() { + return new TestBean(); + } + + } + + static class TestBean { + + } + +} diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockBeanOnContextHierarchyIntegrationTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockBeanOnContextHierarchyIntegrationTests.java index ea0603b79d8e..df467bcedd3f 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockBeanOnContextHierarchyIntegrationTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockBeanOnContextHierarchyIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBeanOnContextHierarchyIntegrationTests.ChildConfig; import org.springframework.boot.test.mock.mockito.MockBeanOnContextHierarchyIntegrationTests.ParentConfig; @@ -73,7 +72,7 @@ static class ChildConfig implements ApplicationContextAware { private ApplicationContext context; @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + public void setApplicationContext(ApplicationContext applicationContext) { this.context = applicationContext; } diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockBeanOnTestFieldForExistingBeanWithQualifierIntegrationTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockBeanOnTestFieldForExistingBeanWithQualifierIntegrationTests.java index f86d58777258..30a54cab0ec6 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockBeanOnTestFieldForExistingBeanWithQualifierIntegrationTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockBeanOnTestFieldForExistingBeanWithQualifierIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.verify; +import static org.mockito.BDDMockito.then; /** * Test {@link MockBean @MockBean} on a test class field can be used to replace existing @@ -56,7 +56,7 @@ class MockBeanOnTestFieldForExistingBeanWithQualifierIntegrationTests { @Test void testMocking() { this.caller.sayGreeting(); - verify(this.service).greeting(); + then(this.service).should().greeting(); } @Test diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockBeanWithAopProxyTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockBeanWithAopProxyTests.java index cb9d4d16d18f..32c95776258f 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockBeanWithAopProxyTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockBeanWithAopProxyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,8 +37,8 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; /** * Test {@link MockBean @MockBean} when mixed with Spring AOP. @@ -60,9 +60,9 @@ void verifyShouldUseProxyTarget() { given(this.dateService.getDate(false)).willReturn(2L); Long d2 = this.dateService.getDate(false); assertThat(d2).isEqualTo(2L); - verify(this.dateService, times(2)).getDate(false); - verify(this.dateService, times(2)).getDate(eq(false)); - verify(this.dateService, times(2)).getDate(anyBoolean()); + then(this.dateService).should(times(2)).getDate(false); + then(this.dateService).should(times(2)).getDate(eq(false)); + then(this.dateService).should(times(2)).getDate(anyBoolean()); } @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockBeanWithSpringMethodRuleRepeatJUnit4IntegrationTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockBeanWithSpringMethodRuleRepeatJUnit4IntegrationTests.java new file mode 100644 index 000000000000..0c0e04cf7b3f --- /dev/null +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockBeanWithSpringMethodRuleRepeatJUnit4IntegrationTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.mock.mockito; + +import org.junit.AfterClass; +import org.junit.Rule; +import org.junit.Test; + +import org.springframework.test.annotation.Repeat; +import org.springframework.test.context.junit4.rules.SpringMethodRule; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MockBean} and {@link Repeat}. + * + * @author Andy Wilkinson + * @see gh-27693 + */ +public class MockBeanWithSpringMethodRuleRepeatJUnit4IntegrationTests { + + @Rule + public final SpringMethodRule springMethodRule = new SpringMethodRule(); + + @MockBean + private FirstService first; + + private static int invocations; + + @AfterClass + public static void afterClass() { + assertThat(invocations).isEqualTo(2); + } + + @Test + @Repeat(2) + public void repeatedTest() { + invocations++; + } + + interface FirstService { + + String greeting(); + + } + +} diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockitoContextCustomizerFactoryTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockitoContextCustomizerFactoryTests.java index 5077b7e3676e..8a8f7ad5b7ee 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockitoContextCustomizerFactoryTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockitoContextCustomizerFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,7 @@ package org.springframework.boot.test.mock.mockito; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.MockitoAnnotations; import org.springframework.test.context.ContextCustomizer; @@ -33,11 +31,6 @@ class MockitoContextCustomizerFactoryTests { private final MockitoContextCustomizerFactory factory = new MockitoContextCustomizerFactory(); - @BeforeEach - void setup() { - MockitoAnnotations.initMocks(this); - } - @Test void getContextCustomizerWithoutAnnotationReturnsCustomizer() { ContextCustomizer customizer = this.factory.createContextCustomizer(NoMockBeanAnnotation.class, null); diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessorTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessorTests.java index 0490be8196c1..36873c22b6d9 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessorTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,17 @@ package org.springframework.boot.test.mock.mockito; +import java.util.Map; + import org.junit.jupiter.api.Test; import org.mockito.Mockito; +import org.springframework.beans.BeanWrapper; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.boot.test.mock.mockito.example.ExampleService; import org.springframework.boot.test.mock.mockito.example.FailingExampleService; @@ -29,6 +35,9 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; +import org.springframework.core.Ordered; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.util.Assert; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; @@ -39,6 +48,7 @@ * @author Phillip Webb * @author Andy Wilkinson * @author Andreas Neiser + * @author Madhura Bhave */ class MockitoPostProcessorTests { @@ -123,6 +133,16 @@ void canSpyQualifiedBeanWithPrimaryBeanPresent() { assertThat(Mockito.mockingDetails(context.getBean("exampleQualified", ExampleService.class)).isSpy()).isTrue(); } + @Test + void postProcessorShouldNotTriggerEarlyInitialization() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(FactoryBeanRegisteringPostProcessor.class); + MockitoPostProcessor.register(context); + context.register(TestBeanFactoryPostProcessor.class); + context.register(EagerInitBean.class); + context.refresh(); + } + @Configuration(proxyBeanMethods = false) @MockBean(SomeInterface.class) static class MockedFactoryBean { @@ -258,6 +278,14 @@ ExampleService examplePrimary() { } + @Configuration(proxyBeanMethods = false) + static class EagerInitBean { + + @MockBean + private ExampleService service; + + } + static class TestFactoryBean implements FactoryBean { @Override @@ -277,6 +305,33 @@ public boolean isSingleton() { } + static class FactoryBeanRegisteringPostProcessor implements BeanFactoryPostProcessor, Ordered { + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { + RootBeanDefinition beanDefinition = new RootBeanDefinition(TestFactoryBean.class); + ((BeanDefinitionRegistry) beanFactory).registerBeanDefinition("test", beanDefinition); + } + + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE; + } + + } + + static class TestBeanFactoryPostProcessor implements BeanFactoryPostProcessor { + + @Override + @SuppressWarnings("unchecked") + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { + Map cache = (Map) ReflectionTestUtils.getField(beanFactory, + "factoryBeanInstanceCache"); + Assert.isTrue(cache.isEmpty(), "Early initialization of factory bean triggered."); + } + + } + interface SomeInterface { } diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockitoTestExecutionListenerTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockitoTestExecutionListenerTests.java index 3c1b94152aaf..05d8ed51fa36 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockitoTestExecutionListenerTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockitoTestExecutionListenerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,12 +19,12 @@ import java.io.InputStream; import java.lang.reflect.Field; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.context.ApplicationContext; import org.springframework.test.context.TestContext; @@ -34,15 +34,15 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; /** * Tests for {@link MockitoTestExecutionListener}. * * @author Phillip Webb */ +@ExtendWith(MockitoExtension.class) class MockitoTestExecutionListenerTests { private MockitoTestExecutionListener listener = new MockitoTestExecutionListener(); @@ -56,12 +56,6 @@ class MockitoTestExecutionListenerTests { @Captor private ArgumentCaptor fieldCaptor; - @BeforeEach - void setup() { - MockitoAnnotations.initMocks(this); - given(this.applicationContext.getBean(MockitoPostProcessor.class)).willReturn(this.postProcessor); - } - @Test void prepareTestInstanceShouldInitMockitoAnnotations() throws Exception { WithMockitoAnnotations instance = new WithMockitoAnnotations(); @@ -72,27 +66,31 @@ void prepareTestInstanceShouldInitMockitoAnnotations() throws Exception { @Test void prepareTestInstanceShouldInjectMockBean() throws Exception { + given(this.applicationContext.getBean(MockitoPostProcessor.class)).willReturn(this.postProcessor); WithMockBean instance = new WithMockBean(); - this.listener.prepareTestInstance(mockTestContext(instance)); - verify(this.postProcessor).inject(this.fieldCaptor.capture(), eq(instance), any(MockDefinition.class)); + TestContext testContext = mockTestContext(instance); + given(testContext.getApplicationContext()).willReturn(this.applicationContext); + this.listener.prepareTestInstance(testContext); + then(this.postProcessor).should().inject(this.fieldCaptor.capture(), eq(instance), any(MockDefinition.class)); assertThat(this.fieldCaptor.getValue().getName()).isEqualTo("mockBean"); } @Test void beforeTestMethodShouldDoNothingWhenDirtiesContextAttributeIsNotSet() throws Exception { - WithMockBean instance = new WithMockBean(); - this.listener.beforeTestMethod(mockTestContext(instance)); - verifyNoMoreInteractions(this.postProcessor); + this.listener.beforeTestMethod(mock(TestContext.class)); + then(this.postProcessor).shouldHaveNoMoreInteractions(); } @Test void beforeTestMethodShouldInjectMockBeanWhenDirtiesContextAttributeIsSet() throws Exception { + given(this.applicationContext.getBean(MockitoPostProcessor.class)).willReturn(this.postProcessor); WithMockBean instance = new WithMockBean(); TestContext mockTestContext = mockTestContext(instance); + given(mockTestContext.getApplicationContext()).willReturn(this.applicationContext); given(mockTestContext.getAttribute(DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE)) .willReturn(Boolean.TRUE); this.listener.beforeTestMethod(mockTestContext); - verify(this.postProcessor).inject(this.fieldCaptor.capture(), eq(instance), any(MockDefinition.class)); + then(this.postProcessor).should().inject(this.fieldCaptor.capture(), eq(instance), any(MockDefinition.class)); assertThat(this.fieldCaptor.getValue().getName()).isEqualTo("mockBean"); } @@ -101,7 +99,6 @@ private TestContext mockTestContext(Object instance) { TestContext testContext = mock(TestContext.class); given(testContext.getTestInstance()).willReturn(instance); given(testContext.getTestClass()).willReturn((Class) instance.getClass()); - given(testContext.getApplicationContext()).willReturn(this.applicationContext); return testContext; } diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/QualifierDefinitionTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/QualifierDefinitionTests.java index f8ba8b2cb472..53272bd00b79 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/QualifierDefinitionTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/QualifierDefinitionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,12 +20,12 @@ import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Field; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; @@ -36,13 +36,14 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.verify; +import static org.mockito.BDDMockito.then; /** * Tests for {@link QualifierDefinition}. * * @author Phillip Webb */ +@ExtendWith(MockitoExtension.class) class QualifierDefinitionTests { @Mock @@ -51,11 +52,6 @@ class QualifierDefinitionTests { @Captor private ArgumentCaptor descriptorCaptor; - @BeforeEach - void setup() { - MockitoAnnotations.initMocks(this); - } - @Test void forElementFieldIsNullShouldReturnNull() { assertThat(QualifierDefinition.forElement((Field) null)).isNull(); @@ -85,7 +81,7 @@ void matchesShouldCallBeanFactory() { Field field = ReflectionUtils.findField(ConfigA.class, "directQualifier"); QualifierDefinition qualifierDefinition = QualifierDefinition.forElement(field); qualifierDefinition.matches(this.beanFactory, "bean"); - verify(this.beanFactory).isAutowireCandidate(eq("bean"), this.descriptorCaptor.capture()); + then(this.beanFactory).should().isAutowireCandidate(eq("bean"), this.descriptorCaptor.capture()); assertThat(this.descriptorCaptor.getValue().getAnnotatedElement()).isEqualTo(field); } diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/ResetMocksTestExecutionListenerTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/ResetMocksTestExecutionListenerTests.java index 5f7728b23c94..a90256d1385d 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/ResetMocksTestExecutionListenerTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/ResetMocksTestExecutionListenerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ * @author Andy Wilkinson */ @ExtendWith(SpringExtension.class) -@TestMethodOrder(MethodOrderer.Alphanumeric.class) +@TestMethodOrder(MethodOrderer.MethodName.class) class ResetMocksTestExecutionListenerTests { @Autowired diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpringBootMockResolverIntegrationTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpringBootMockResolverIntegrationTests.java new file mode 100644 index 000000000000..0a1ce91cbc86 --- /dev/null +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpringBootMockResolverIntegrationTests.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.mock.mockito; + +import org.junit.jupiter.api.Test; +import org.mockito.internal.configuration.plugins.Plugins; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link SpringBootMockResolver}. + * + * @author Andy Wilkinson + */ +class SpringBootMockResolverIntegrationTests { + + @Test + void customMockResolverIsRegisteredWithMockito() { + assertThat(Plugins.getMockResolvers()).hasOnlyElementsOfType(SpringBootMockResolver.class); + } + +} diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnConfigurationClassForExistingBeanIntegrationTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnConfigurationClassForExistingBeanIntegrationTests.java index 924a39b7cfcb..e39877a32222 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnConfigurationClassForExistingBeanIntegrationTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnConfigurationClassForExistingBeanIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.verify; +import static org.mockito.BDDMockito.then; /** * Test {@link SpyBean @SpyBean} on a configuration class can be used to spy existing @@ -44,7 +44,7 @@ class SpyBeanOnConfigurationClassForExistingBeanIntegrationTests { @Test void testSpying() { assertThat(this.caller.sayGreeting()).isEqualTo("I say simple"); - verify(this.caller.getService()).greeting(); + then(this.caller.getService()).should().greeting(); } @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnConfigurationClassForNewBeanIntegrationTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnConfigurationClassForNewBeanIntegrationTests.java index 312de6761c73..3bccd8ead14e 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnConfigurationClassForNewBeanIntegrationTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnConfigurationClassForNewBeanIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.verify; +import static org.mockito.BDDMockito.then; /** * Test {@link SpyBean @SpyBean} on a configuration class can be used to inject new spy @@ -44,7 +44,7 @@ class SpyBeanOnConfigurationClassForNewBeanIntegrationTests { @Test void testSpying() { assertThat(this.caller.sayGreeting()).isEqualTo("I say simple"); - verify(this.caller.getService()).greeting(); + then(this.caller.getService()).should().greeting(); } @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnConfigurationFieldForExistingBeanIntegrationTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnConfigurationFieldForExistingBeanIntegrationTests.java index bab4a639350b..3b8a2163b15d 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnConfigurationFieldForExistingBeanIntegrationTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnConfigurationFieldForExistingBeanIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.verify; +import static org.mockito.BDDMockito.then; /** * Test {@link SpyBean @SpyBean} on a field on a {@code @Configuration} class can be used @@ -48,7 +48,7 @@ class SpyBeanOnConfigurationFieldForExistingBeanIntegrationTests { @Test void testSpying() { assertThat(this.caller.sayGreeting()).isEqualTo("I say simple"); - verify(this.config.exampleService).greeting(); + then(this.config.exampleService).should().greeting(); } @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnConfigurationFieldForNewBeanIntegrationTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnConfigurationFieldForNewBeanIntegrationTests.java index 4faca40b2164..447022fc65ca 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnConfigurationFieldForNewBeanIntegrationTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnConfigurationFieldForNewBeanIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.verify; +import static org.mockito.BDDMockito.then; /** * Test {@link SpyBean @SpyBean} on a field on a {@code @Configuration} class can be used @@ -47,7 +47,7 @@ class SpyBeanOnConfigurationFieldForNewBeanIntegrationTests { @Test void testSpying() { assertThat(this.caller.sayGreeting()).isEqualTo("I say simple"); - verify(this.config.exampleService).greeting(); + then(this.config.exampleService).should().greeting(); } @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnContextHierarchyIntegrationTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnContextHierarchyIntegrationTests.java index 37bf42d5e3eb..5d53081b2b83 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnContextHierarchyIntegrationTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnContextHierarchyIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.SpyBeanOnContextHierarchyIntegrationTests.ChildConfig; import org.springframework.boot.test.mock.mockito.SpyBeanOnContextHierarchyIntegrationTests.ParentConfig; @@ -74,7 +73,7 @@ static class ChildConfig implements ApplicationContextAware { private ApplicationContext context; @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + public void setApplicationContext(ApplicationContext applicationContext) { this.context = applicationContext; } diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestClassForExistingBeanIntegrationTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestClassForExistingBeanIntegrationTests.java index 702d6b642cad..d35570810bb2 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestClassForExistingBeanIntegrationTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestClassForExistingBeanIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.verify; +import static org.mockito.BDDMockito.then; /** * Test {@link SpyBean @SpyBean} on a test class can be used to replace existing beans. @@ -44,7 +44,7 @@ class SpyBeanOnTestClassForExistingBeanIntegrationTests { @Test void testSpying() { assertThat(this.caller.sayGreeting()).isEqualTo("I say simple"); - verify(this.caller.getService()).greeting(); + then(this.caller.getService()).should().greeting(); } @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestClassForNewBeanIntegrationTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestClassForNewBeanIntegrationTests.java index 1e73eb298236..ddc2fed14857 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestClassForNewBeanIntegrationTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestClassForNewBeanIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.verify; +import static org.mockito.BDDMockito.then; /** * Test {@link SpyBean @SpyBean} on a test class can be used to inject new spy instances. @@ -44,7 +44,7 @@ class SpyBeanOnTestClassForNewBeanIntegrationTests { @Test void testSpying() { assertThat(this.caller.sayGreeting()).isEqualTo("I say simple"); - verify(this.caller.getService()).greeting(); + then(this.caller.getService()).should().greeting(); } @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestFieldForExistingBeanCacheIntegrationTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestFieldForExistingBeanCacheIntegrationTests.java index 83392ea4b591..45cd5879baf4 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestFieldForExistingBeanCacheIntegrationTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestFieldForExistingBeanCacheIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.verify; +import static org.mockito.BDDMockito.then; /** * Test {@link SpyBean @SpyBean} on a test class field can be used to replace existing @@ -50,7 +50,7 @@ class SpyBeanOnTestFieldForExistingBeanCacheIntegrationTests { @Test void testSpying() { assertThat(this.caller.sayGreeting()).isEqualTo("I say simple"); - verify(this.caller.getService()).greeting(); + then(this.caller.getService()).should().greeting(); } } diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestFieldForExistingBeanIntegrationTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestFieldForExistingBeanIntegrationTests.java index e66f35180997..0043abd19779 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestFieldForExistingBeanIntegrationTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestFieldForExistingBeanIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.verify; +import static org.mockito.BDDMockito.then; /** * Test {@link SpyBean @SpyBean} on a test class field can be used to replace existing @@ -48,7 +48,7 @@ class SpyBeanOnTestFieldForExistingBeanIntegrationTests { @Test void testSpying() { assertThat(this.caller.sayGreeting()).isEqualTo("I say simple"); - verify(this.caller.getService()).greeting(); + then(this.caller.getService()).should().greeting(); } } diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestFieldForExistingBeanWithQualifierIntegrationTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestFieldForExistingBeanWithQualifierIntegrationTests.java index 8470a405bcbf..a758fdb8c359 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestFieldForExistingBeanWithQualifierIntegrationTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestFieldForExistingBeanWithQualifierIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.verify; +import static org.mockito.BDDMockito.then; /** * Test {@link SpyBean @SpyBean} on a test class field can be used to replace existing @@ -55,7 +55,7 @@ class SpyBeanOnTestFieldForExistingBeanWithQualifierIntegrationTests { @Test void testMocking() throws Exception { this.caller.sayGreeting(); - verify(this.service).greeting(); + then(this.service).should().greeting(); } @Test diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestFieldForExistingCircularBeansIntegrationTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestFieldForExistingCircularBeansIntegrationTests.java new file mode 100644 index 000000000000..f751e19e3f77 --- /dev/null +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestFieldForExistingCircularBeansIntegrationTests.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.mock.mockito; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.SpyBeanOnTestFieldForExistingCircularBeansIntegrationTests.SpyBeanOnTestFieldForExistingCircularBeansConfig; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.mockito.BDDMockito.then; + +/** + * Test {@link SpyBean @SpyBean} on a test class field can be used to replace existing + * beans with circular dependencies. + * + * @author Andy Wilkinson + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = SpyBeanOnTestFieldForExistingCircularBeansConfig.class) +class SpyBeanOnTestFieldForExistingCircularBeansIntegrationTests { + + @SpyBean + private One one; + + @Autowired + private Two two; + + @Test + void beanWithCircularDependenciesCanBeSpied() { + this.two.callOne(); + then(this.one).should().someMethod(); + } + + @Import({ One.class, Two.class }) + static class SpyBeanOnTestFieldForExistingCircularBeansConfig { + + } + + static class One { + + @Autowired + @SuppressWarnings("unused") + private Two two; + + void someMethod() { + + } + + } + + static class Two { + + @Autowired + private One one; + + void callOne() { + this.one.someMethod(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestFieldForExistingGenericBeanIntegrationTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestFieldForExistingGenericBeanIntegrationTests.java index c656bc13f858..d3034b61c827 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestFieldForExistingGenericBeanIntegrationTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestFieldForExistingGenericBeanIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.verify; +import static org.mockito.BDDMockito.then; /** * Test {@link SpyBean @SpyBean} on a test class field can be used to replace existing @@ -53,7 +53,7 @@ class SpyBeanOnTestFieldForExistingGenericBeanIntegrationTests { @Test void testSpying() { assertThat(this.caller.sayGreeting()).isEqualTo("I say 123 simple"); - verify(this.exampleService).greeting(); + then(this.exampleService).should().greeting(); } @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestFieldForExistingScopedBeanIntegrationTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestFieldForExistingScopedBeanIntegrationTests.java deleted file mode 100644 index eb413f3ef62d..000000000000 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestFieldForExistingScopedBeanIntegrationTests.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.test.mock.mockito; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.beans.factory.ObjectFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.config.CustomScopeConfigurer; -import org.springframework.boot.test.mock.mockito.SpyBeanOnTestFieldForExistingScopedBeanIntegrationTests.SpyBeanOnTestFieldForExistingScopedBeanConfig; -import org.springframework.boot.test.mock.mockito.example.ExampleService; -import org.springframework.boot.test.mock.mockito.example.ExampleServiceCaller; -import org.springframework.boot.test.mock.mockito.example.SimpleExampleService; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.Scope; -import org.springframework.context.annotation.ScopedProxyMode; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.verify; - -/** - * Test {@link SpyBean @SpyBean} on a test class field can be used to replace existing - * scoped beans. - * - * @author Andy Wilkinson - */ -@ExtendWith(SpringExtension.class) -@ContextConfiguration(classes = SpyBeanOnTestFieldForExistingScopedBeanConfig.class) -public class SpyBeanOnTestFieldForExistingScopedBeanIntegrationTests { - - @SpyBean - private ExampleService exampleService; - - @Autowired - private ExampleServiceCaller caller; - - @Test - void testSpying() { - assertThat(this.caller.sayGreeting()).isEqualTo("I say simple"); - verify(this.exampleService).greeting(); - } - - @Configuration(proxyBeanMethods = false) - @Import({ ExampleServiceCaller.class }) - static class SpyBeanOnTestFieldForExistingScopedBeanConfig { - - @Bean - @Scope(scopeName = "custom", proxyMode = ScopedProxyMode.TARGET_CLASS) - SimpleExampleService simpleExampleService() { - return new SimpleExampleService(); - } - - @Bean - static CustomScopeConfigurer customScopeConfigurer() { - CustomScopeConfigurer configurer = new CustomScopeConfigurer(); - configurer.addScope("custom", new org.springframework.beans.factory.config.Scope() { - - private Object bean; - - @Override - public Object resolveContextualObject(String key) { - throw new UnsupportedOperationException(); - } - - @Override - public Object remove(String name) { - throw new UnsupportedOperationException(); - } - - @Override - public void registerDestructionCallback(String name, Runnable callback) { - throw new UnsupportedOperationException(); - } - - @Override - public String getConversationId() { - throw new UnsupportedOperationException(); - } - - @Override - public Object get(String name, ObjectFactory objectFactory) { - if (this.bean == null) { - this.bean = objectFactory.getObject(); - } - return this.bean; - } - - }); - return configurer; - } - - } - -} diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestFieldForMultipleExistingBeansWithOnePrimaryIntegrationTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestFieldForMultipleExistingBeansWithOnePrimaryIntegrationTests.java index 83276bb8f1f8..6751f49f0cfb 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestFieldForMultipleExistingBeansWithOnePrimaryIntegrationTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestFieldForMultipleExistingBeansWithOnePrimaryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.verify; +import static org.mockito.BDDMockito.then; /** * Test {@link SpyBean @SpyBean} on a test class field can be used to inject a spy @@ -52,7 +52,7 @@ void testSpying() { assertThat(this.caller.sayGreeting()).isEqualTo("I say two"); assertThat(Mockito.mockingDetails(this.spy).getMockCreationSettings().getMockName().toString()) .isEqualTo("two"); - verify(this.spy).greeting(); + then(this.spy).should().greeting(); } @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestFieldForNewBeanIntegrationTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestFieldForNewBeanIntegrationTests.java index 91a1b8f59cc6..2cf35176d8a4 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestFieldForNewBeanIntegrationTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanOnTestFieldForNewBeanIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.verify; +import static org.mockito.BDDMockito.then; /** * Test {@link SpyBean @SpyBean} on a test class field can be used to inject new spy @@ -47,7 +47,7 @@ class SpyBeanOnTestFieldForNewBeanIntegrationTests { @Test void testSpying() { assertThat(this.caller.sayGreeting()).isEqualTo("I say simple"); - verify(this.caller.getService()).greeting(); + then(this.caller.getService()).should().greeting(); } @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanWithAopProxyAndNotProxyTargetAwareTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanWithAopProxyAndNotProxyTargetAwareTests.java index 8b67ebbf9cad..38b5439a3925 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanWithAopProxyAndNotProxyTargetAwareTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanWithAopProxyAndNotProxyTargetAwareTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,9 +35,8 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; /** * Test {@link SpyBean @SpyBean} when mixed with Spring AOP. @@ -54,7 +53,7 @@ class SpyBeanWithAopProxyAndNotProxyTargetAwareTests { @Test void verifyShouldUseProxyTarget() { this.dateService.getDate(false); - verify(this.dateService, times(1)).getDate(false); + then(this.dateService).should().getDate(false); assertThatExceptionOfType(UnfinishedVerificationException.class).isThrownBy(() -> reset(this.dateService)); } diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanWithAopProxyTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanWithAopProxyTests.java index 14dfacc187d6..2c0fd0e2a88b 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanWithAopProxyTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanWithAopProxyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,8 +36,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; +import static org.mockito.BDDMockito.then; /** * Test {@link SpyBean @SpyBean} when mixed with Spring AOP. @@ -57,9 +56,9 @@ void verifyShouldUseProxyTarget() throws Exception { Thread.sleep(200); Long d2 = this.dateService.getDate(false); assertThat(d1).isEqualTo(d2); - verify(this.dateService, times(1)).getDate(false); - verify(this.dateService, times(1)).getDate(eq(false)); - verify(this.dateService, times(1)).getDate(anyBoolean()); + then(this.dateService).should().getDate(false); + then(this.dateService).should().getDate(eq(false)); + then(this.dateService).should().getDate(anyBoolean()); } @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanWithDirtiesContextClassModeBeforeMethodIntegrationTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanWithDirtiesContextClassModeBeforeMethodIntegrationTests.java index d141dcbf426a..875deecc1e94 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanWithDirtiesContextClassModeBeforeMethodIntegrationTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanWithDirtiesContextClassModeBeforeMethodIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ import org.springframework.test.annotation.DirtiesContext.ClassMode; import org.springframework.test.context.junit.jupiter.SpringExtension; -import static org.mockito.Mockito.verify; +import static org.mockito.BDDMockito.then; /** * Integration tests for using {@link SpyBean @SpyBean} with @@ -49,7 +49,7 @@ class SpyBeanWithDirtiesContextClassModeBeforeMethodIntegrationTests { @Test void testSpying() throws Exception { this.caller.sayGreeting(); - verify(this.exampleService).greeting(); + then(this.exampleService).should().greeting(); } @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanWithJdkProxyTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanWithJdkProxyTests.java new file mode 100644 index 000000000000..002ca14decd0 --- /dev/null +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/SpyBeanWithJdkProxyTests.java @@ -0,0 +1,105 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.mock.mockito; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; + +/** + * Tests for {@link SpyBean @SpyBean} with a JDK proxy. + * + * @author Andy Wilkinson + */ +@ExtendWith(SpringExtension.class) +class SpyBeanWithJdkProxyTests { + + @Autowired + private ExampleService service; + + @SpyBean + private ExampleRepository repository; + + @Test + void jdkProxyCanBeSpied() throws Exception { + Example example = this.service.find("id"); + assertThat(example.id).isEqualTo("id"); + then(this.repository).should().find("id"); + } + + @Configuration(proxyBeanMethods = false) + @Import(ExampleService.class) + static class Config { + + @Bean + ExampleRepository dateService() { + return (ExampleRepository) Proxy.newProxyInstance(getClass().getClassLoader(), + new Class[] { ExampleRepository.class }, new InvocationHandler() { + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + return new Example((String) args[0]); + } + + }); + } + + } + + static class ExampleService { + + private final ExampleRepository repository; + + ExampleService(ExampleRepository repository) { + this.repository = repository; + } + + Example find(String id) { + return this.repository.find(id); + } + + } + + interface ExampleRepository { + + Example find(String id); + + } + + static class Example { + + private final String id; + + Example(String id) { + this.id = id; + } + + } + +} diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/rule/OutputCaptureTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/rule/OutputCaptureTests.java deleted file mode 100644 index 68d9774e5f57..000000000000 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/rule/OutputCaptureTests.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.test.rule; - -import org.junit.Rule; -import org.junit.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link OutputCapture}. - * - * @author Roland Weisleder - */ -@Deprecated -public class OutputCaptureTests { - - @Rule - public OutputCapture outputCapture = new OutputCapture(); - - @Test - public void toStringShouldReturnAllCapturedOutput() { - System.out.println("Hello World"); - assertThat(this.outputCapture.toString()).contains("Hello World"); - } - - @Test - public void reset() { - System.out.println("Hello"); - this.outputCapture.reset(); - System.out.println("World"); - assertThat(this.outputCapture.toString()).doesNotContain("Hello").contains("World"); - } - -} diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/system/OutputCaptureTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/system/OutputCaptureTests.java index 02fe67f483a3..a18b398541a3 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/system/OutputCaptureTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/system/OutputCaptureTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -69,7 +69,7 @@ void pushWhenEmptyStartsCapture() { } @Test - void pushWhenHasExistingStartesNewCapture() { + void pushWhenHasExistingStartsNewCapture() { System.out.print("A"); this.output.push(); System.out.print("B"); diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/system/OutputExtensionExtendWithTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/system/OutputExtensionExtendWithTests.java index 2cae90d697e3..23119c85fdd1 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/system/OutputExtensionExtendWithTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/system/OutputExtensionExtendWithTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.test.system; import org.junit.jupiter.api.Test; diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/util/ApplicationContextTestUtilsTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/util/ApplicationContextTestUtilsTests.java index 68e2c5ba5d91..13aaaf2cec71 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/util/ApplicationContextTestUtilsTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/util/ApplicationContextTestUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,8 +22,8 @@ import org.springframework.context.ConfigurableApplicationContext; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link ApplicationContextTestUtils}. @@ -50,10 +50,10 @@ void closeContextAndParent() { given(mock.getParent()).willReturn(parent); given(parent.getParent()).willReturn(null); ApplicationContextTestUtils.closeAll(mock); - verify(mock).getParent(); - verify(mock).close(); - verify(parent).getParent(); - verify(parent).close(); + then(mock).should().getParent(); + then(mock).should().close(); + then(parent).should().getParent(); + then(parent).should().close(); } } diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/util/TestPropertyValuesTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/util/TestPropertyValuesTests.java index d99b49a068a3..e007aa2f693d 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/util/TestPropertyValuesTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/util/TestPropertyValuesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,14 @@ package org.springframework.boot.test.util; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Stream; + import org.junit.jupiter.api.Test; +import org.springframework.boot.test.util.TestPropertyValues.Pair; import org.springframework.boot.test.util.TestPropertyValues.Type; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.PropertySource; @@ -25,6 +31,7 @@ import org.springframework.core.env.SystemEnvironmentPropertySource; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; /** * Tests for {@link TestPropertyValues}. @@ -36,6 +43,47 @@ class TestPropertyValuesTests { private final ConfigurableEnvironment environment = new StandardEnvironment(); + @Test + void ofStringArrayCreatesValues() { + TestPropertyValues.of("spring:boot", "version:latest").applyTo(this.environment); + assertThat(this.environment.getProperty("spring")).isEqualTo("boot"); + assertThat(this.environment.getProperty("version")).isEqualTo("latest"); + } + + @Test + void ofIterableCreatesValues() { + TestPropertyValues.of(Arrays.asList("spring:boot", "version:latest")).applyTo(this.environment); + assertThat(this.environment.getProperty("spring")).isEqualTo("boot"); + assertThat(this.environment.getProperty("version")).isEqualTo("latest"); + } + + @Test + void ofStreamCreatesValues() { + TestPropertyValues.of(Stream.of("spring:boot", "version:latest")).applyTo(this.environment); + assertThat(this.environment.getProperty("spring")).isEqualTo("boot"); + assertThat(this.environment.getProperty("version")).isEqualTo("latest"); + } + + @Test + void ofMapCreatesValues() { + Map map = new LinkedHashMap<>(); + map.put("spring", "boot"); + map.put("version", "latest"); + TestPropertyValues.of(map).applyTo(this.environment); + assertThat(this.environment.getProperty("spring")).isEqualTo("boot"); + assertThat(this.environment.getProperty("version")).isEqualTo("latest"); + } + + @Test + void ofMappedStreamCreatesValues() { + TestPropertyValues.of(Stream.of("spring|boot", "version|latest"), (string) -> { + String[] split = string.split("\\|"); + return Pair.of(split[0], split[1]); + }).applyTo(this.environment); + assertThat(this.environment.getProperty("spring")).isEqualTo("boot"); + assertThat(this.environment.getProperty("version")).isEqualTo("latest"); + } + @Test void applyToEnvironmentShouldAttachConfigurationPropertySource() { TestPropertyValues.of("foo.bar=baz").applyTo(this.environment); @@ -133,4 +181,23 @@ void applyToSystemPropertiesWhenValueIsNullShouldRemoveProperty() { } } + @Test + void pairOfCreatesPair() { + Map map = new LinkedHashMap<>(); + Pair.of("spring", "boot").addTo(map); + assertThat(map).containsOnly(entry("spring", "boot")); + } + + @Test + void pairOfWhenNameAndValueAreEmptyReturnsNull() { + assertThat(Pair.of("", "")).isNull(); + } + + @Test + void pairFromMapEntryCreatesPair() { + Map map = new LinkedHashMap<>(); + Pair.fromMapEntry(entry("spring", "boot")).addTo(map); + assertThat(map).containsOnly(entry("spring", "boot")); + } + } diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/SpringBootTestRandomPortEnvironmentPostProcessorTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/SpringBootTestRandomPortEnvironmentPostProcessorTests.java index e2e36f15d8dd..578f50b655a7 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/SpringBootTestRandomPortEnvironmentPostProcessorTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/SpringBootTestRandomPortEnvironmentPostProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.test.web; import java.util.Collections; diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/LocalHostUriTemplateHandlerTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/LocalHostUriTemplateHandlerTests.java index ad1f67f2ebce..85d0e57b849a 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/LocalHostUriTemplateHandlerTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/LocalHostUriTemplateHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,8 +28,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link LocalHostUriTemplateHandler}. @@ -99,7 +99,7 @@ void expandShouldUseCustomHandler() { given(uriTemplateHandler.expand("https://localhost:8080/", uriVariables)).willReturn(uri); LocalHostUriTemplateHandler handler = new LocalHostUriTemplateHandler(environment, "https", uriTemplateHandler); assertThat(handler.expand("/", uriVariables)).isEqualTo(uri); - verify(uriTemplateHandler).expand("https://localhost:8080/", uriVariables); + then(uriTemplateHandler).should().expand("https://localhost:8080/", uriVariables); } } diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/NoTestRestTemplateBeanChecker.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/NoTestRestTemplateBeanChecker.java index 4f0ecc7bc8bc..2fb9be5158b6 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/NoTestRestTemplateBeanChecker.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/NoTestRestTemplateBeanChecker.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.springframework.boot.test.web.client; -import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.BeanFactoryUtils; @@ -33,7 +32,7 @@ class NoTestRestTemplateBeanChecker implements ImportSelector, BeanFactoryAware { @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + public void setBeanFactory(BeanFactory beanFactory) { assertThat(BeanFactoryUtils.beanNamesForTypeIncludingAncestors((ListableBeanFactory) beanFactory, TestRestTemplate.class)).isEmpty(); } diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/RootUriRequestExpectationManagerTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/RootUriRequestExpectationManagerTests.java index 9de4bf90f536..2fd99419ca4a 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/RootUriRequestExpectationManagerTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/RootUriRequestExpectationManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,10 +20,11 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.http.client.ClientHttpRequest; @@ -39,8 +40,8 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; @@ -49,6 +50,7 @@ * * @author Phillip Webb */ +@ExtendWith(MockitoExtension.class) class RootUriRequestExpectationManagerTests { private String uri = "https://example.com"; @@ -63,7 +65,6 @@ class RootUriRequestExpectationManagerTests { @BeforeEach void setup() { - MockitoAnnotations.initMocks(this); this.manager = new RootUriRequestExpectationManager(this.uri, this.delegate); } @@ -84,7 +85,7 @@ void expectRequestShouldDelegateToExpectationManager() { ExpectedCount count = ExpectedCount.once(); RequestMatcher requestMatcher = mock(RequestMatcher.class); this.manager.expectRequest(count, requestMatcher); - verify(this.delegate).expectRequest(count, requestMatcher); + then(this.delegate).should().expectRequest(count, requestMatcher); } @Test @@ -92,7 +93,7 @@ void validateRequestWhenUriDoesNotStartWithRootUriShouldDelegateToExpectationMan ClientHttpRequest request = mock(ClientHttpRequest.class); given(request.getURI()).willReturn(new URI("https://spring.io/test")); this.manager.validateRequest(request); - verify(this.delegate).validateRequest(request); + then(this.delegate).should().validateRequest(request); } @Test @@ -100,7 +101,7 @@ void validateRequestWhenUriStartsWithRootUriShouldReplaceUri() throws Exception ClientHttpRequest request = mock(ClientHttpRequest.class); given(request.getURI()).willReturn(new URI(this.uri + "/hello")); this.manager.validateRequest(request); - verify(this.delegate).validateRequest(this.requestCaptor.capture()); + then(this.delegate).should().validateRequest(this.requestCaptor.capture()); HttpRequestWrapper actual = (HttpRequestWrapper) this.requestCaptor.getValue(); assertThat(actual.getRequest()).isSameAs(request); assertThat(actual.getURI()).isEqualTo(new URI("/hello")); @@ -119,7 +120,7 @@ void validateRequestWhenRequestUriAssertionIsThrownShouldReplaceUriInMessage() t @Test void resetRequestShouldDelegateToExpectationManager() { this.manager.reset(); - verify(this.delegate).reset(); + then(this.delegate).should().reset(); } @Test diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/TestRestTemplateContextCustomizerTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/TestRestTemplateContextCustomizerTests.java index f0206e7abe18..7f5217f0ea11 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/TestRestTemplateContextCustomizerTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/TestRestTemplateContextCustomizerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ import org.junit.jupiter.api.Test; -import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.boot.test.context.SpringBootTest; @@ -58,7 +57,7 @@ static class TestApplicationContext extends AbstractApplicationContext { private final ConfigurableListableBeanFactory beanFactory = new DefaultListableBeanFactory(); @Override - protected void refreshBeanFactory() throws BeansException, IllegalStateException { + protected void refreshBeanFactory() { } @Override @@ -67,7 +66,7 @@ protected void closeBeanFactory() { } @Override - public ConfigurableListableBeanFactory getBeanFactory() throws IllegalStateException { + public ConfigurableListableBeanFactory getBeanFactory() { return this.beanFactory; } diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/TestRestTemplateTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/TestRestTemplateTests.java index 341e9fef59ba..42151362145c 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/TestRestTemplateTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/TestRestTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -56,8 +56,8 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link TestRestTemplate}. @@ -125,7 +125,7 @@ void getRootUriRootUriNotSet() { } @Test - void authenticated() throws Exception { + void authenticated() { TestRestTemplate restTemplate = new TestRestTemplate("user", "password"); assertBasicAuthorizationCredentials(restTemplate, "user", "password"); } @@ -150,7 +150,7 @@ void restOperationsAreAvailable() { ReflectionUtils.doWithMethods(RestOperations.class, new MethodCallback() { @Override - public void doWith(Method method) throws IllegalArgumentException { + public void doWith(Method method) { Method equivalent = ReflectionUtils.findMethod(TestRestTemplate.class, method.getName(), method.getParameterTypes()); assertThat(equivalent).as("Method %s not found", method).isNotNull(); @@ -200,7 +200,7 @@ private Object mockArgument(Class type) throws Exception { } @Test - void withBasicAuthAddsBasicAuthWhenNotAlreadyPresent() throws Exception { + void withBasicAuthAddsBasicAuthWhenNotAlreadyPresent() { TestRestTemplate original = new TestRestTemplate(); TestRestTemplate basicAuth = original.withBasicAuth("user", "password"); assertThat(getConverterClasses(original)).containsExactlyElementsOf(getConverterClasses(basicAuth)); @@ -210,7 +210,7 @@ void withBasicAuthAddsBasicAuthWhenNotAlreadyPresent() throws Exception { } @Test - void withBasicAuthReplacesBasicAuthWhenAlreadyPresent() throws Exception { + void withBasicAuthReplacesBasicAuthWhenAlreadyPresent() { TestRestTemplate original = new TestRestTemplate("foo", "bar").withBasicAuth("replace", "replace"); TestRestTemplate basicAuth = original.withBasicAuth("user", "password"); assertThat(getConverterClasses(basicAuth)).containsExactlyElementsOf(getConverterClasses(original)); @@ -233,6 +233,36 @@ void withBasicAuthShouldUseNoOpErrorHandler() throws Exception { Class.forName("org.springframework.boot.test.web.client.TestRestTemplate$NoOpResponseErrorHandler")); } + @Test + void exchangeWithRelativeTemplatedUrlRequestEntity() throws Exception { + RequestEntity entity = RequestEntity.get("/a/b/c.{ext}", "txt").build(); + TestRestTemplate template = new TestRestTemplate(); + ClientHttpRequestFactory requestFactory = mock(ClientHttpRequestFactory.class); + MockClientHttpRequest request = new MockClientHttpRequest(); + request.setResponse(new MockClientHttpResponse(new byte[0], HttpStatus.OK)); + URI absoluteUri = URI.create("http://localhost:8080/a/b/c.txt"); + given(requestFactory.createRequest(eq(absoluteUri), eq(HttpMethod.GET))).willReturn(request); + template.getRestTemplate().setRequestFactory(requestFactory); + LocalHostUriTemplateHandler uriTemplateHandler = new LocalHostUriTemplateHandler(new MockEnvironment()); + template.setUriTemplateHandler(uriTemplateHandler); + template.exchange(entity, String.class); + then(requestFactory).should().createRequest(eq(absoluteUri), eq(HttpMethod.GET)); + } + + @Test + void exchangeWithAbsoluteTemplatedUrlRequestEntity() throws Exception { + RequestEntity entity = RequestEntity.get("https://api.example.com/a/b/c.{ext}", "txt").build(); + TestRestTemplate template = new TestRestTemplate(); + ClientHttpRequestFactory requestFactory = mock(ClientHttpRequestFactory.class); + MockClientHttpRequest request = new MockClientHttpRequest(); + request.setResponse(new MockClientHttpResponse(new byte[0], HttpStatus.OK)); + URI absoluteUri = URI.create("https://api.example.com/a/b/c.txt"); + given(requestFactory.createRequest(eq(absoluteUri), eq(HttpMethod.GET))).willReturn(request); + template.getRestTemplate().setRequestFactory(requestFactory); + template.exchange(entity, String.class); + then(requestFactory).should().createRequest(eq(absoluteUri), eq(HttpMethod.GET)); + } + @Test void deleteHandlesRelativeUris() throws IOException { verifyRelativeUriHandling(TestRestTemplate::delete); @@ -332,11 +362,11 @@ private void verifyRelativeUriHandling(TestRestTemplateCallback callback) throws LocalHostUriTemplateHandler uriTemplateHandler = new LocalHostUriTemplateHandler(new MockEnvironment()); template.setUriTemplateHandler(uriTemplateHandler); callback.doWithTestRestTemplate(template, URI.create("/a/b/c.txt?param=%7Bsomething%7D")); - verify(requestFactory).createRequest(eq(absoluteUri), any(HttpMethod.class)); + then(requestFactory).should().createRequest(eq(absoluteUri), any(HttpMethod.class)); } private void assertBasicAuthorizationCredentials(TestRestTemplate testRestTemplate, String username, - String password) throws Exception { + String password) { ClientHttpRequest request = ReflectionTestUtils.invokeMethod(testRestTemplate.getRestTemplate(), "createRequest", URI.create("http://localhost"), HttpMethod.POST); if (username == null) { diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/scan/SimpleFactoryBean.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/scan/SimpleFactoryBean.java index e224b1fce1a3..e93cafb80daa 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/scan/SimpleFactoryBean.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/scan/SimpleFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.test.web.client.scan; import org.springframework.beans.factory.FactoryBean; diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/htmlunit/LocalHostWebClientTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/htmlunit/LocalHostWebClientTests.java index aa102543d865..46c461341a84 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/htmlunit/LocalHostWebClientTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/htmlunit/LocalHostWebClientTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,9 +25,10 @@ import com.gargoylesoftware.htmlunit.WebRequest; import com.gargoylesoftware.htmlunit.WebResponse; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.mock.env.MockEnvironment; @@ -35,8 +36,8 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link LocalHostWebClient}. @@ -44,15 +45,12 @@ * @author Phillip Webb */ @SuppressWarnings("resource") +@ExtendWith(MockitoExtension.class) class LocalHostWebClientTests { @Captor private ArgumentCaptor requestCaptor; - LocalHostWebClientTests() { - MockitoAnnotations.initMocks(this); - } - @Test void createWhenEnvironmentIsNullWillThrowException() { assertThatIllegalArgumentException().isThrownBy(() -> new LocalHostWebClient(null)) @@ -66,7 +64,7 @@ void getPageWhenUrlIsRelativeAndNoPortWillUseLocalhost8080() throws Exception { WebConnection connection = mockConnection(); client.setWebConnection(connection); client.getPage("/test"); - verify(connection).getResponse(this.requestCaptor.capture()); + then(connection).should().getResponse(this.requestCaptor.capture()); assertThat(this.requestCaptor.getValue().getUrl()).isEqualTo(new URL("http://localhost:8080/test")); } @@ -78,7 +76,7 @@ void getPageWhenUrlIsRelativeAndHasPortWillUseLocalhostPort() throws Exception { WebConnection connection = mockConnection(); client.setWebConnection(connection); client.getPage("/test"); - verify(connection).getResponse(this.requestCaptor.capture()); + then(connection).should().getResponse(this.requestCaptor.capture()); assertThat(this.requestCaptor.getValue().getUrl()).isEqualTo(new URL("http://localhost:8181/test")); } diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/htmlunit/webdriver/LocalHostWebConnectionHtmlUnitDriverTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/htmlunit/webdriver/LocalHostWebConnectionHtmlUnitDriverTests.java index 8a543805cf77..5be09f5b6944 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/htmlunit/webdriver/LocalHostWebConnectionHtmlUnitDriverTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/htmlunit/webdriver/LocalHostWebConnectionHtmlUnitDriverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,9 +25,10 @@ import com.gargoylesoftware.htmlunit.WebRequest; import com.gargoylesoftware.htmlunit.WebWindow; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentMatcher; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; import org.openqa.selenium.Capabilities; import org.springframework.core.env.Environment; @@ -37,21 +38,21 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Tests for {@link LocalHostWebConnectionHtmlUnitDriver}. * * @author Phillip Webb */ +@ExtendWith(MockitoExtension.class) class LocalHostWebConnectionHtmlUnitDriverTests { - @Mock - private WebClient webClient; + private final WebClient webClient; - LocalHostWebConnectionHtmlUnitDriverTests() { - MockitoAnnotations.initMocks(this); + LocalHostWebConnectionHtmlUnitDriverTests(@Mock WebClient webClient) { + this.webClient = webClient; given(this.webClient.getOptions()).willReturn(new WebClientOptions()); given(this.webClient.getWebConsole()).willReturn(new WebConsole()); } @@ -90,7 +91,8 @@ void getWhenUrlIsRelativeAndNoPortWillUseLocalhost8080() throws Exception { MockEnvironment environment = new MockEnvironment(); LocalHostWebConnectionHtmlUnitDriver driver = new TestLocalHostWebConnectionHtmlUnitDriver(environment); driver.get("/test"); - verify(this.webClient).getPage(any(WebWindow.class), requestToUrl(new URL("http://localhost:8080/test"))); + then(this.webClient).should().getPage(any(WebWindow.class), + requestToUrl(new URL("http://localhost:8080/test"))); } @Test @@ -99,7 +101,8 @@ void getWhenUrlIsRelativeAndHasPortWillUseLocalhostPort() throws Exception { environment.setProperty("local.server.port", "8181"); LocalHostWebConnectionHtmlUnitDriver driver = new TestLocalHostWebConnectionHtmlUnitDriver(environment); driver.get("/test"); - verify(this.webClient).getPage(any(WebWindow.class), requestToUrl(new URL("http://localhost:8181/test"))); + then(this.webClient).should().getPage(any(WebWindow.class), + requestToUrl(new URL("http://localhost:8181/test"))); } private WebRequest requestToUrl(URL url) { diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/reactive/server/NoWebTestClientBeanChecker.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/reactive/server/NoWebTestClientBeanChecker.java index 82f74ee82150..2c8a28db628a 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/reactive/server/NoWebTestClientBeanChecker.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/reactive/server/NoWebTestClientBeanChecker.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.springframework.boot.test.web.reactive.server; -import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.BeanFactoryUtils; @@ -34,7 +33,7 @@ class NoWebTestClientBeanChecker implements ImportSelector, BeanFactoryAware { @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + public void setBeanFactory(BeanFactory beanFactory) { assertThat(BeanFactoryUtils.beanNamesForTypeIncludingAncestors((ListableBeanFactory) beanFactory, WebTestClient.class)).isEmpty(); } diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/reactive/server/WebTestClientContextCustomizerIntegrationTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/reactive/server/WebTestClientContextCustomizerIntegrationTests.java index e616464908d8..ee02eaff235a 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/reactive/server/WebTestClientContextCustomizerIntegrationTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/reactive/server/WebTestClientContextCustomizerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,8 +36,8 @@ import org.springframework.test.web.reactive.server.WebTestClient.Builder; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; /** * Integration test for {@link WebTestClientContextCustomizer}. @@ -56,7 +56,7 @@ class WebTestClientContextCustomizerIntegrationTests { @Test void test() { - verify(this.clientBuilderCustomizer).customize(any(Builder.class)); + then(this.clientBuilderCustomizer).should().customize(any(Builder.class)); this.webTestClient.get().uri("/").exchange().expectBody(String.class).isEqualTo("hello"); } diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/reactive/server/WebTestClientContextCustomizerWithCustomBasePathTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/reactive/server/WebTestClientContextCustomizerWithCustomBasePathTests.java new file mode 100644 index 000000000000..c1ef48bf3d21 --- /dev/null +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/reactive/server/WebTestClientContextCustomizerWithCustomBasePathTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.web.reactive.server; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.embedded.tomcat.TomcatReactiveWebServerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ContextPathCompositeHandler; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.reactive.server.WebTestClient; + +/** + * Tests for {@link WebTestClientContextCustomizer} with a custom base path for a reactive + * web application. + * + * @author Madhura Bhave + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = "spring.main.web-application-type=reactive") +@TestPropertySource(properties = "spring.webflux.base-path=/test") +class WebTestClientContextCustomizerWithCustomBasePathTests { + + @Autowired + private WebTestClient webTestClient; + + @Test + void test() { + this.webTestClient.get().uri("/hello").exchange().expectBody(String.class).isEqualTo("hello world"); + } + + @Configuration(proxyBeanMethods = false) + static class TestConfig { + + @Bean + TomcatReactiveWebServerFactory webServerFactory() { + return new TomcatReactiveWebServerFactory(0); + } + + @Bean + HttpHandler httpHandler() { + TestHandler httpHandler = new TestHandler(); + Map handlersMap = Collections.singletonMap("/test", httpHandler); + return new ContextPathCompositeHandler(handlersMap); + } + + } + + static class TestHandler implements HttpHandler { + + private static final DefaultDataBufferFactory factory = new DefaultDataBufferFactory(); + + @Override + public Mono handle(ServerHttpRequest request, ServerHttpResponse response) { + response.setStatusCode(HttpStatus.OK); + return response.writeWith(Mono.just(factory.wrap("hello world".getBytes()))); + } + + } + +} diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/reactive/server/WebTestClientContextCustomizerWithCustomContextPathTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/reactive/server/WebTestClientContextCustomizerWithCustomContextPathTests.java new file mode 100644 index 000000000000..9df89b87520c --- /dev/null +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/reactive/server/WebTestClientContextCustomizerWithCustomContextPathTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.web.reactive.server; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.DispatcherServlet; + +/** + * Tests for {@link WebTestClientContextCustomizer} with a custom context path for a + * servlet web application. + * + * @author Madhura Bhave + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@TestPropertySource(properties = "server.servlet.context-path=/test") +class WebTestClientContextCustomizerWithCustomContextPathTests { + + @Autowired + private WebTestClient webTestClient; + + @Test + void test() { + this.webTestClient.get().uri("/hello").exchange().expectBody(String.class).isEqualTo("hello world"); + } + + @Configuration(proxyBeanMethods = false) + @Import(TestController.class) + static class TestConfig { + + @Bean + TomcatServletWebServerFactory webServerFactory() { + TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(0); + factory.setContextPath("/test"); + return factory; + } + + @Bean + DispatcherServlet dispatcherServlet() { + return new DispatcherServlet(); + } + + } + + @RestController + static class TestController { + + @GetMapping("/hello") + String hello() { + return "hello world"; + } + + } + +} diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/reactive/server/WebTestClientContextCustomizerWithoutSupportedHttpClientTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/reactive/server/WebTestClientContextCustomizerWithoutSupportedHttpClientTests.java new file mode 100644 index 000000000000..4ce3b2dd7e4b --- /dev/null +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/reactive/server/WebTestClientContextCustomizerWithoutSupportedHttpClientTests.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.web.reactive.server; + +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.testsupport.classpath.ClassPathExclusions; +import org.springframework.test.context.ContextCustomizer; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link WebTestClientContextCustomizerFactory} when no supported HTTP client + * is on the classpath. + * + * @author Andy Wilkinson + */ +@ClassPathExclusions({ "reactor-netty*.jar", "jetty-client*.jar" }) +class WebTestClientContextCustomizerWithoutSupportedHttpClientTests { + + @Test + void createContextCustomizerWhenNoSupportedHttpClientIsAvailableShouldReturnNull() { + WebTestClientContextCustomizerFactory contextCustomizerFactory = new WebTestClientContextCustomizerFactory(); + ContextCustomizer contextCustomizer = contextCustomizerFactory.createContextCustomizer(TestClass.class, + Collections.emptyList()); + assertThat(contextCustomizer).isNull(); + } + + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) + private static class TestClass { + + } + +} diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/reactive/server/WebTestClientContextCustomizerWithoutWebfluxIntegrationTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/reactive/server/WebTestClientContextCustomizerWithoutWebfluxIntegrationTests.java new file mode 100644 index 000000000000..8acb1f7dde6c --- /dev/null +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/reactive/server/WebTestClientContextCustomizerWithoutWebfluxIntegrationTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.web.reactive.server; + +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.testsupport.classpath.ClassPathExclusions; +import org.springframework.test.context.ContextCustomizer; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link WebTestClientContextCustomizerFactory} when spring webflux is not on + * the classpath. + * + * @author Tobias Gesellchen + * @author Stephane Nicoll + */ +@ClassPathExclusions("spring-webflux*.jar") +class WebTestClientContextCustomizerWithoutWebfluxIntegrationTests { + + @Test + void customizerIsNotCreatedWithoutWebClient() { + WebTestClientContextCustomizerFactory contextCustomizerFactory = new WebTestClientContextCustomizerFactory(); + ContextCustomizer contextCustomizer = contextCustomizerFactory.createContextCustomizer(TestClass.class, + Collections.emptyList()); + assertThat(contextCustomizer).isNull(); + } + + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) + private static class TestClass { + + } + +} diff --git a/spring-boot-project/spring-boot-tools/org.springframework.boot.gradle.plugin/pom.xml b/spring-boot-project/spring-boot-tools/org.springframework.boot.gradle.plugin/pom.xml deleted file mode 100644 index 754672d7467e..000000000000 --- a/spring-boot-project/spring-boot-tools/org.springframework.boot.gradle.plugin/pom.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-tools - ${revision} - - org.springframework.boot.gradle.plugin - pom - Spring Boot Gradle Plugin Marker Artifact - Spring Boot Gradle Plugin Marker Artifact - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-gradle-plugin - ${project.version} - - - - - - org.codehaus.mojo - build-helper-maven-plugin - - - regex-property - - regex-property - - - false - - - - - - - diff --git a/spring-boot-project/spring-boot-tools/pom.xml b/spring-boot-project/spring-boot-tools/pom.xml deleted file mode 100644 index 1dd0d293d635..000000000000 --- a/spring-boot-project/spring-boot-tools/pom.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-parent - ${revision} - ../spring-boot-parent - - spring-boot-tools - pom - Spring Boot Tools - Spring Boot Tools - - ${basedir}/../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - org.springframework.boot.gradle.plugin - spring-boot-antlib - spring-boot-autoconfigure-processor - spring-boot-configuration-docs - spring-boot-configuration-metadata - spring-boot-configuration-processor - spring-boot-gradle-plugin - spring-boot-loader - spring-boot-loader-tools - spring-boot-maven-plugin - spring-boot-test-support - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-antlib/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-antlib/build.gradle new file mode 100644 index 000000000000..e00c34aeaf8e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-antlib/build.gradle @@ -0,0 +1,75 @@ +plugins { + id "java-library" + id "org.springframework.boot.conventions" + id "org.springframework.boot.deployed" +} + +description = "Spring Boot Antlib" + +ext { + antVersion = "1.10.7" +} + +configurations { + antUnit + antIvy +} + +dependencies { + antUnit "org.apache.ant:ant-antunit:1.3" + antIvy "org.apache.ivy:ivy:2.5.0" + + compileOnly(project(":spring-boot-project:spring-boot-tools:spring-boot-loader")) + compileOnly("org.apache.ant:ant:${antVersion}") + + implementation(project(":spring-boot-project:spring-boot-tools:spring-boot-loader-tools")) + implementation("org.springframework:spring-core") +} + +task copyIntegrationTestSources(type: Copy) { + from file("src/it") + into "${buildDir}/it" +} + +processResources { + eachFile { + filter { it.replace('${spring-boot.version}', project.version) } + } + inputs.property "version", project.version +} + +task integrationTest { + dependsOn copyIntegrationTestSources, jar + def resultsDir = file("${buildDir}/test-results/integrationTest") + inputs.dir(file("src/it")).withPathSensitivity(PathSensitivity.RELATIVE).withPropertyName("source") + inputs.files(sourceSets.main.runtimeClasspath).withNormalizer(ClasspathNormalizer).withPropertyName("classpath") + outputs.dirs resultsDir + doLast { + ant.with { + taskdef(resource: "org/apache/ant/antunit/antlib.xml", + classpath: configurations.antUnit.asPath) + taskdef(resource: "org/apache/ivy/ant/antlib.xml", + classpath: configurations.antIvy.asPath) + taskdef(resource: "org/springframework/boot/ant/antlib.xml", + classpath: sourceSets.main.runtimeClasspath.asPath, + uri: "antlib:org.springframework.boot.ant") + ant.property(name: "ivy.class.path", value: configurations.antIvy.asPath) + ant.property(name: "antunit.class.path", value: configurations.antUnit.asPath) + antunit { + propertyset { + ant.propertyref(name: "build.compiler") + ant.propertyref(name: "antunit.class.path") + ant.propertyref(name: "ivy.class.path") + } + plainlistener() + file("${buildDir}/test-results/integrationTest").mkdirs() + xmllistener(toDir: resultsDir) + fileset(dir: "${buildDir}/it", includes: "**/build.xml") + } + } + } +} + +check { + dependsOn integrationTest +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-antlib/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-antlib/pom.xml deleted file mode 100644 index d40a79a8ead8..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-antlib/pom.xml +++ /dev/null @@ -1,155 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-tools - ${revision} - - spring-boot-antlib - Spring Boot Antlib - Spring Boot Antlib - - ${basedir}/../../.. - 1.9.3 - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - - org.springframework.boot - spring-boot-loader-tools - compile - - - - org.springframework.boot - spring-boot-loader - provided - - - org.apache.ant - ant - ${ant.version} - provided - - - - - - org.apache.maven.plugins - maven-shade-plugin - - - - org.springframework.boot:spring-boot-loader-tools - org.springframework:spring-core - - - true - false - true - false - - - - shade-runtime-dependencies - package - - shade - - - - - - org.apache.maven.plugins - maven-antrun-plugin - - - antunit - integration-test - - - - - - - - - - - - - - - - - - - - - ${skipTests} - - - run - - - - - - org.apache.ant - ant - ${ant.version} - - - org.apache.ant - ant-launcher - ${ant.version} - - - org.apache.ant - ant-antunit - 1.3 - - - org.apache.ivy - ivy - 2.4.0 - - - org.eclipse.jdt.core.compiler - ecj - 4.6.1 - - - - - - - - java-8 - - [1.8,1.9) - - - org.eclipse.jdt.core.JDTCompilerAdapter - - - - java-9 - - [1.9,) - - - modern - - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-antlib/src/it/sample/build.xml b/spring-boot-project/spring-boot-tools/spring-boot-antlib/src/it/sample/build.xml index c6992bb33eca..1ea131312e56 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-antlib/src/it/sample/build.xml +++ b/spring-boot-project/spring-boot-tools/spring-boot-antlib/src/it/sample/build.xml @@ -6,11 +6,16 @@ + + + - + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-antlib/src/it/sample/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-antlib/src/it/sample/src/main/java/org/test/SampleApplication.java index 0e756da2a2ce..5bfe64988235 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-antlib/src/it/sample/src/main/java/org/test/SampleApplication.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-antlib/src/it/sample/src/main/java/org/test/SampleApplication.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.test; import org.joda.time.LocalDate; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-antlib/src/main/resources/org/springframework/boot/ant/antlib.xml b/spring-boot-project/spring-boot-tools/spring-boot-antlib/src/main/resources/org/springframework/boot/ant/antlib.xml index 787a2d68bab9..bf2f7307866d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-antlib/src/main/resources/org/springframework/boot/ant/antlib.xml +++ b/spring-boot-project/spring-boot-tools/spring-boot-antlib/src/main/resources/org/springframework/boot/ant/antlib.xml @@ -34,17 +34,6 @@ - - - - - - - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/build.gradle new file mode 100644 index 000000000000..c131957b7459 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/build.gradle @@ -0,0 +1,16 @@ +plugins { + id "java-library" + id "org.springframework.boot.conventions" + id "org.springframework.boot.deployed" + id "org.springframework.boot.annotation-processor" +} + +description = "Spring Boot AutoConfigure Annotation Processor" + +dependencies { + testImplementation(enforcedPlatform(project(":spring-boot-project:spring-boot-dependencies"))) + testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) + testImplementation("org.assertj:assertj-core") + testImplementation("org.springframework:spring-core") + testImplementation("org.junit.jupiter:junit-jupiter") +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/pom.xml deleted file mode 100644 index 6e9fd42e10c8..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/pom.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-tools - ${revision} - - spring-boot-autoconfigure-processor - Spring Boot Auto-Configure Annotation Processor - Spring Auto-Configure Annotation Processor - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - - org.springframework.boot - spring-boot-test-support - test - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - - none - - - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/main/java/org/springframework/boot/autoconfigureprocessor/AutoConfigureAnnotationProcessor.java b/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/main/java/org/springframework/boot/autoconfigureprocessor/AutoConfigureAnnotationProcessor.java index 5656396a1bc8..3b38f6badc6c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/main/java/org/springframework/boot/autoconfigureprocessor/AutoConfigureAnnotationProcessor.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/main/java/org/springframework/boot/autoconfigureprocessor/AutoConfigureAnnotationProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,9 @@ package org.springframework.boot.autoconfigureprocessor; import java.io.IOException; -import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -26,18 +28,18 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Properties; import java.util.Set; +import java.util.TreeMap; import java.util.stream.Stream; import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.Filer; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; import javax.lang.model.SourceVersion; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; -import javax.lang.model.element.ElementKind; import javax.lang.model.element.TypeElement; import javax.lang.model.type.DeclaredType; import javax.tools.FileObject; @@ -66,7 +68,7 @@ public class AutoConfigureAnnotationProcessor extends AbstractProcessor { private final Map valueExtractors; - private final Properties properties = new Properties(); + private final Map properties = new TreeMap<>(); public AutoConfigureAnnotationProcessor() { Map annotations = new LinkedHashMap<>(); @@ -124,10 +126,7 @@ private void process(RoundEnvironment roundEnv, String propertyKey, String annot TypeElement annotationType = this.processingEnv.getElementUtils().getTypeElement(annotationName); if (annotationType != null) { for (Element element : roundEnv.getElementsAnnotatedWith(annotationType)) { - Element enclosingElement = element.getEnclosingElement(); - if (enclosingElement != null && enclosingElement.getKind() == ElementKind.PACKAGE) { - processElement(element, propertyKey, annotationName); - } + processElement(element, propertyKey, annotationName); } } } @@ -177,10 +176,15 @@ private List getValues(String propertyKey, AnnotationMirror annotation) private void writeProperties() throws IOException { if (!this.properties.isEmpty()) { - FileObject file = this.processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", - PROPERTIES_PATH); - try (OutputStream outputStream = file.openOutputStream()) { - this.properties.store(outputStream, null); + Filer filer = this.processingEnv.getFiler(); + FileObject file = filer.createResource(StandardLocation.CLASS_OUTPUT, "", PROPERTIES_PATH); + try (Writer writer = new OutputStreamWriter(file.openOutputStream(), StandardCharsets.UTF_8)) { + for (Map.Entry entry : this.properties.entrySet()) { + writer.append(entry.getKey()); + writer.append("="); + writer.append(entry.getValue()); + writer.append(System.lineSeparator()); + } } } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/main/resources/META-INF/gradle/incremental.annotation.processors b/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/main/resources/META-INF/gradle/incremental.annotation.processors new file mode 100644 index 000000000000..242b07c64b5a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/main/resources/META-INF/gradle/incremental.annotation.processors @@ -0,0 +1 @@ +org.springframework.boot.autoconfigureprocessor.AutoConfigureAnnotationProcessor,aggregating \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/test/java/org/springframework/boot/autoconfigureprocessor/AutoConfigureAnnotationProcessorTests.java b/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/test/java/org/springframework/boot/autoconfigureprocessor/AutoConfigureAnnotationProcessorTests.java index ea6a7c1d9741..10f9c4492a41 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/test/java/org/springframework/boot/autoconfigureprocessor/AutoConfigureAnnotationProcessorTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/test/java/org/springframework/boot/autoconfigureprocessor/AutoConfigureAnnotationProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import org.junit.jupiter.api.io.TempDir; import org.springframework.boot.testsupport.compiler.TestCompiler; +import org.springframework.util.FileCopyUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -48,14 +49,14 @@ void createCompiler() throws IOException { @Test void annotatedClass() throws Exception { Properties properties = compile(TestClassConfiguration.class); - assertThat(properties).hasSize(5); + assertThat(properties).hasSize(7); assertThat(properties).containsEntry( "org.springframework.boot.autoconfigureprocessor.TestClassConfiguration.ConditionalOnClass", "java.io.InputStream,org.springframework.boot.autoconfigureprocessor." + "TestClassConfiguration$Nested,org.springframework.foo"); assertThat(properties).containsKey("org.springframework.boot.autoconfigureprocessor.TestClassConfiguration"); assertThat(properties) - .doesNotContainKey("org.springframework.boot.autoconfigureprocessor.TestClassConfiguration$Nested"); + .containsKey("org.springframework.boot.autoconfigureprocessor.TestClassConfiguration$Nested"); assertThat(properties).containsEntry( "org.springframework.boot.autoconfigureprocessor.TestClassConfiguration.ConditionalOnBean", "java.io.OutputStream"); @@ -96,11 +97,24 @@ void annotatedClassWithOrder() throws Exception { "123"); } + @Test // gh-19370 + void propertiesAreFullRepeatable() throws Exception { + String first = new String( + FileCopyUtils.copyToByteArray(process(TestOrderedClassConfiguration.class).getWrittenFile())); + String second = new String( + FileCopyUtils.copyToByteArray(process(TestOrderedClassConfiguration.class).getWrittenFile())); + assertThat(first).isEqualTo(second).doesNotContain("#"); + } + private Properties compile(Class... types) throws IOException { + return process(types).getWrittenProperties(); + } + + private TestAutoConfigureAnnotationProcessor process(Class... types) { TestAutoConfigureAnnotationProcessor processor = new TestAutoConfigureAnnotationProcessor( this.compiler.getOutputLocation()); this.compiler.getTask(types).call(processor); - return processor.getWrittenProperties(); + return processor; } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/test/java/org/springframework/boot/autoconfigureprocessor/TestAutoConfigureAnnotationProcessor.java b/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/test/java/org/springframework/boot/autoconfigureprocessor/TestAutoConfigureAnnotationProcessor.java index b87627701ed9..bfa249e1549a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/test/java/org/springframework/boot/autoconfigureprocessor/TestAutoConfigureAnnotationProcessor.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/test/java/org/springframework/boot/autoconfigureprocessor/TestAutoConfigureAnnotationProcessor.java @@ -60,7 +60,7 @@ private void put(Map annotations, String key, Class value) { } public Properties getWrittenProperties() throws IOException { - File file = new File(this.outputLocation, PROPERTIES_PATH); + File file = getWrittenFile(); if (!file.exists()) { return null; } @@ -71,4 +71,8 @@ public Properties getWrittenProperties() throws IOException { } } + public File getWrittenFile() { + return new File(this.outputLocation, PROPERTIES_PATH); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/build.gradle new file mode 100644 index 000000000000..6bcba07f56d9 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/build.gradle @@ -0,0 +1,29 @@ +plugins { + id "java-library" + id "org.springframework.boot.conventions" + id "org.springframework.boot.deployed" +} + +description = "Spring Boot Buildpack Platform" + +dependencies { + api("com.fasterxml.jackson.core:jackson-databind") + api("com.fasterxml.jackson.module:jackson-module-parameter-names") + api("net.java.dev.jna:jna-platform") + api("org.apache.commons:commons-compress:1.19") + api("org.apache.httpcomponents:httpclient") { + exclude(group: "commons-logging", module: "commons-logging") + } + api("org.springframework:spring-core") + api("org.tomlj:tomlj:1.0.0") + + testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) + testImplementation("com.jayway.jsonpath:json-path") + testImplementation("org.assertj:assertj-core") + testImplementation("org.testcontainers:testcontainers") + testImplementation("org.hamcrest:hamcrest") + testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.mockito:mockito-core") + testImplementation("org.mockito:mockito-junit-jupiter") + testImplementation("org.skyscreamer:jsonassert") +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/AbstractBuildLog.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/AbstractBuildLog.java new file mode 100644 index 000000000000..d3d818ff4873 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/AbstractBuildLog.java @@ -0,0 +1,129 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.util.List; +import java.util.function.Consumer; + +import org.springframework.boot.buildpack.platform.docker.LogUpdateEvent; +import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent; +import org.springframework.boot.buildpack.platform.docker.type.Image; +import org.springframework.boot.buildpack.platform.docker.type.ImageReference; +import org.springframework.boot.buildpack.platform.docker.type.VolumeName; + +/** + * Base class for {@link BuildLog} implementations. + * + * @author Phillip Webb + * @author Scott Frederick + * @author Andrey Shlykov + * @since 2.3.0 + */ +public abstract class AbstractBuildLog implements BuildLog { + + @Override + public void start(BuildRequest request) { + log("Building image '" + request.getName() + "'"); + log(); + } + + @Override + @Deprecated + public Consumer pullingBuilder(BuildRequest request, ImageReference imageReference) { + return pullingImage(imageReference, ImageType.BUILDER); + } + + @Override + @Deprecated + public void pulledBuilder(BuildRequest request, Image image) { + pulledImage(image, ImageType.BUILDER); + } + + @Override + @Deprecated + public Consumer pullingRunImage(BuildRequest request, ImageReference imageReference) { + return pullingImage(imageReference, ImageType.RUNNER); + } + + @Override + @Deprecated + public void pulledRunImage(BuildRequest request, Image image) { + pulledImage(image, ImageType.RUNNER); + } + + @Override + public Consumer pullingImage(ImageReference imageReference, ImageType imageType) { + return getProgressConsumer(String.format(" > Pulling %s '%s'", imageType.getDescription(), imageReference)); + } + + @Override + public void pulledImage(Image image, ImageType imageType) { + log(String.format(" > Pulled %s '%s'", imageType.getDescription(), getDigest(image))); + } + + @Override + public Consumer pushingImage(ImageReference imageReference) { + return getProgressConsumer(String.format(" > Pushing image '%s'", imageReference)); + } + + @Override + public void pushedImage(ImageReference imageReference) { + log(String.format(" > Pushed image '%s'", imageReference)); + } + + @Override + public void executingLifecycle(BuildRequest request, LifecycleVersion version, VolumeName buildCacheVolume) { + log(" > Executing lifecycle version " + version); + log(" > Using build cache volume '" + buildCacheVolume + "'"); + } + + @Override + public Consumer runningPhase(BuildRequest request, String name) { + log(); + log(" > Running " + name); + String prefix = String.format(" %-14s", "[" + name + "] "); + return (event) -> log(prefix + event); + } + + @Override + public void skippingPhase(String name, String reason) { + log(); + log(" > Skipping " + name + " " + reason); + log(); + } + + @Override + public void executedLifecycle(BuildRequest request) { + log(); + log("Successfully built image '" + request.getName() + "'"); + log(); + } + + private String getDigest(Image image) { + List digests = image.getDigests(); + return (digests.isEmpty() ? "" : digests.get(0)); + } + + protected void log() { + log(""); + } + + protected abstract void log(String message); + + protected abstract Consumer getProgressConsumer(String message); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ApiVersion.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ApiVersion.java new file mode 100644 index 000000000000..789264fe6e0c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ApiVersion.java @@ -0,0 +1,135 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.util.Assert; + +/** + * API Version number comprised of a major and minor value. + * + * @author Phillip Webb + * @author Scott Frederick + */ +final class ApiVersion { + + private static final Pattern PATTERN = Pattern.compile("^v?(\\d+)\\.(\\d*)$"); + + private final int major; + + private final int minor; + + private ApiVersion(int major, int minor) { + this.major = major; + this.minor = minor; + } + + /** + * Return the major version number. + * @return the major version + */ + int getMajor() { + return this.major; + } + + /** + * Return the minor version number. + * @return the minor version + */ + int getMinor() { + return this.minor; + } + + /** + * Assert that this API version supports the specified version. + * @param other the version to check against + * @see #supports(ApiVersion) + */ + void assertSupports(ApiVersion other) { + if (!supports(other)) { + throw new IllegalStateException( + "Detected platform API version '" + other + "' does not match supported version '" + this + "'"); + } + } + + /** + * Returns if this API version supports the given version. A {@code 0.x} matches only + * the same version number. A 1.x or higher release matches when the versions have the + * same major version and a minor that is equal or greater. + * @param other the version to check against + * @return if the specified API is supported + * @see #assertSupports(ApiVersion) + */ + boolean supports(ApiVersion other) { + if (equals(other)) { + return true; + } + if (this.major == 0 || this.major != other.major) { + return false; + } + return this.minor >= other.minor; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ApiVersion other = (ApiVersion) obj; + return (this.major == other.major) && (this.minor == other.minor); + } + + @Override + public int hashCode() { + return this.major * 31 + this.minor; + } + + @Override + public String toString() { + return this.major + "." + this.minor; + } + + /** + * Factory method to parse a string into an {@link ApiVersion} instance. + * @param value the value to parse. + * @return the corresponding {@link ApiVersion} + * @throws IllegalArgumentException if the value could not be parsed + */ + static ApiVersion parse(String value) { + Assert.hasText(value, "Value must not be empty"); + Matcher matcher = PATTERN.matcher(value); + Assert.isTrue(matcher.matches(), () -> "Malformed version number '" + value + "'"); + try { + int major = Integer.parseInt(matcher.group(1)); + int minor = Integer.parseInt(matcher.group(2)); + return new ApiVersion(major, minor); + } + catch (NumberFormatException ex) { + throw new IllegalArgumentException("Malformed version number '" + value + "'", ex); + } + } + + static ApiVersion of(int major, int minor) { + return new ApiVersion(major, minor); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ApiVersions.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ApiVersions.java new file mode 100644 index 000000000000..9257bfed7267 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ApiVersions.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.util.StringUtils; + +/** + * A set of API Version numbers comprised of major and minor values. + * + * @author Scott Frederick + */ +final class ApiVersions { + + /** + * The platform API versions supported by this release. + */ + static final ApiVersions SUPPORTED_PLATFORMS = new ApiVersions(ApiVersion.of(0, 3), ApiVersion.of(0, 4)); + + private final ApiVersion[] apiVersions; + + private ApiVersions(ApiVersion... versions) { + this.apiVersions = versions; + } + + /** + * Find the latest version among the specified versions that is supported by these API + * versions. + * @param others the versions to check against + * @return the version + */ + ApiVersion findLatestSupported(String... others) { + for (int versionsIndex = this.apiVersions.length - 1; versionsIndex >= 0; versionsIndex--) { + ApiVersion apiVersion = this.apiVersions[versionsIndex]; + for (int otherIndex = others.length - 1; otherIndex >= 0; otherIndex--) { + ApiVersion other = ApiVersion.parse(others[otherIndex]); + if (apiVersion.supports(other)) { + return apiVersion; + } + } + } + throw new IllegalStateException( + "Detected platform API versions '" + StringUtils.arrayToCommaDelimitedString(others) + + "' are not included in supported versions '" + this + "'"); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + ApiVersions other = (ApiVersions) obj; + return Arrays.equals(this.apiVersions, other.apiVersions); + } + + @Override + public int hashCode() { + return Arrays.hashCode(this.apiVersions); + } + + @Override + public String toString() { + return StringUtils.arrayToCommaDelimitedString(this.apiVersions); + } + + /** + * Factory method to parse strings into an {@link ApiVersions} instance. + * @param values the values to parse. + * @return the corresponding {@link ApiVersions} + * @throws IllegalArgumentException if any values could not be parsed + */ + static ApiVersions parse(String... values) { + List versions = Arrays.stream(values).map(ApiVersion::parse).collect(Collectors.toList()); + return new ApiVersions(versions.toArray(new ApiVersion[] {})); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildLog.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildLog.java new file mode 100644 index 000000000000..677a330db5bf --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildLog.java @@ -0,0 +1,162 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.io.PrintStream; +import java.util.function.Consumer; + +import org.springframework.boot.buildpack.platform.docker.LogUpdateEvent; +import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent; +import org.springframework.boot.buildpack.platform.docker.type.Image; +import org.springframework.boot.buildpack.platform.docker.type.ImageReference; +import org.springframework.boot.buildpack.platform.docker.type.VolumeName; + +/** + * Callback interface used to provide {@link Builder} output logging. + * + * @author Phillip Webb + * @author Scott Frederick + * @author Andrey Shlykov + * @since 2.3.0 + * @see #toSystemOut() + */ +public interface BuildLog { + + /** + * Log that a build is starting. + * @param request the build request + */ + void start(BuildRequest request); + + /** + * Log that the builder image is being pulled. + * @param request the build request + * @param imageReference the builder image reference + * @return a consumer for progress update events + * @deprecated since 2.4.0 for removal in 2.6.0 in favor of + * {@link #pullingImage(ImageReference, ImageType)} + */ + @Deprecated + Consumer pullingBuilder(BuildRequest request, ImageReference imageReference); + + /** + * Log that the builder image has been pulled. + * @param request the build request + * @param image the builder image that was pulled + * @deprecated since 2.4.0 for removal in 2.6.0 in favor of + * {@link #pulledImage(Image, ImageType)} + */ + @Deprecated + void pulledBuilder(BuildRequest request, Image image); + + /** + * Log that a run image is being pulled. + * @param request the build request + * @param imageReference the run image reference + * @return a consumer for progress update events + * @deprecated since 2.4.0 for removal in 2.6.0 in favor of + * {@link #pullingImage(ImageReference, ImageType)} + */ + @Deprecated + Consumer pullingRunImage(BuildRequest request, ImageReference imageReference); + + /** + * Log that a run image has been pulled. + * @param request the build request + * @param image the run image that was pulled + * @deprecated since 2.4.0 for removal in 2.6.0 in favor of + * {@link #pulledImage(Image, ImageType)} + */ + @Deprecated + void pulledRunImage(BuildRequest request, Image image); + + /** + * Log that an image is being pulled. + * @param imageReference the image reference + * @param imageType the image type + * @return a consumer for progress update events + */ + Consumer pullingImage(ImageReference imageReference, ImageType imageType); + + /** + * Log that an image has been pulled. + * @param image the image that was pulled + * @param imageType the image type that was pulled + */ + void pulledImage(Image image, ImageType imageType); + + /** + * Log that an image is being pushed. + * @param imageReference the image reference + * @return a consumer for progress update events + */ + Consumer pushingImage(ImageReference imageReference); + + /** + * Log that an image has been pushed. + * @param imageReference the image reference + */ + void pushedImage(ImageReference imageReference); + + /** + * Log that the lifecycle is executing. + * @param request the build request + * @param version the lifecycle version + * @param buildCacheVolume the name of the build cache volume in use + */ + void executingLifecycle(BuildRequest request, LifecycleVersion version, VolumeName buildCacheVolume); + + /** + * Log that a specific phase is running. + * @param request the build request + * @param name the name of the phase + * @return a consumer for log updates + */ + Consumer runningPhase(BuildRequest request, String name); + + /** + * Log that a specific phase is being skipped. + * @param name the name of the phase + * @param reason the reason the phase is skipped + */ + void skippingPhase(String name, String reason); + + /** + * Log that the lifecycle has executed. + * @param request the build request + */ + void executedLifecycle(BuildRequest request); + + /** + * Factory method that returns a {@link BuildLog} the outputs to {@link System#out}. + * @return a build log instance that logs to system out + */ + static BuildLog toSystemOut() { + return to(System.out); + } + + /** + * Factory method that returns a {@link BuildLog} the outputs to a given + * {@link PrintStream}. + * @param out the print stream used to output the log + * @return a build log instance that logs to the given print stream + */ + static BuildLog to(PrintStream out) { + return new PrintStreamBuildLog(out); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildOwner.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildOwner.java new file mode 100644 index 000000000000..a027d7237cd0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildOwner.java @@ -0,0 +1,102 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.util.Map; + +import org.springframework.boot.buildpack.platform.io.Owner; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * The {@link Owner} that should perform the build. + * + * @author Phillip Webb + * @author Andy Wilkinson + */ +class BuildOwner implements Owner { + + private static final String USER_PROPERTY_NAME = "CNB_USER_ID"; + + private static final String GROUP_PROPERTY_NAME = "CNB_GROUP_ID"; + + private final long uid; + + private final long gid; + + BuildOwner(Map env) { + this.uid = getValue(env, USER_PROPERTY_NAME); + this.gid = getValue(env, GROUP_PROPERTY_NAME); + } + + BuildOwner(long uid, long gid) { + this.uid = uid; + this.gid = gid; + } + + private long getValue(Map env, String name) { + String value = env.get(name); + Assert.state(StringUtils.hasText(value), + () -> "Missing '" + name + "' value from the builder environment '" + env + "'"); + try { + return Long.parseLong(value); + } + catch (NumberFormatException ex) { + throw new IllegalStateException( + "Malformed '" + name + "' value '" + value + "' in the builder environment '" + env + "'", ex); + } + } + + @Override + public long getUid() { + return this.uid; + } + + @Override + public long getGid() { + return this.gid; + } + + @Override + public String toString() { + return this.uid + "/" + this.gid; + } + + /** + * Factory method to create the {@link BuildOwner} by inspecting the image env for + * {@code CNB_USER_ID}/{@code CNB_GROUP_ID} variables. + * @param env the env to parse + * @return a {@link BuildOwner} instance extracted from the env + * @throws IllegalStateException if the env does not contain the correct CNB variables + */ + static BuildOwner fromEnv(Map env) { + Assert.notNull(env, "Env must not be null"); + return new BuildOwner(env); + } + + /** + * Factory method to create a new {@link BuildOwner} with specified user/group + * identifier. + * @param uid the user identifier + * @param gid the group identifier + * @return a new {@link BuildOwner} instance + */ + static BuildOwner of(long uid, long gid) { + return new BuildOwner(uid, gid); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java new file mode 100644 index 000000000000..93600e49e0ee --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java @@ -0,0 +1,393 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.io.File; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import org.springframework.boot.buildpack.platform.docker.type.Binding; +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.util.Assert; + +/** + * A build request to be handled by the {@link Builder}. + * + * @author Phillip Webb + * @author Scott Frederick + * @author Andrey Shlykov + * @since 2.3.0 + */ +public class BuildRequest { + + static final String DEFAULT_BUILDER_IMAGE_NAME = "paketobuildpacks/builder:base"; + + private static final ImageReference DEFAULT_BUILDER = ImageReference.of(DEFAULT_BUILDER_IMAGE_NAME); + + private final ImageReference name; + + private final Function applicationContent; + + private final ImageReference builder; + + private final ImageReference runImage; + + private final Creator creator; + + private final Map env; + + private final boolean cleanCache; + + private final boolean verboseLogging; + + private final PullPolicy pullPolicy; + + private final boolean publish; + + private final List buildpacks; + + private final List bindings; + + BuildRequest(ImageReference name, Function applicationContent) { + Assert.notNull(name, "Name must not be null"); + Assert.notNull(applicationContent, "ApplicationContent must not be null"); + this.name = name.inTaggedForm(); + this.applicationContent = applicationContent; + this.builder = DEFAULT_BUILDER; + this.runImage = null; + this.env = Collections.emptyMap(); + this.cleanCache = false; + this.verboseLogging = false; + this.pullPolicy = PullPolicy.ALWAYS; + this.publish = false; + this.creator = Creator.withVersion(""); + this.buildpacks = Collections.emptyList(); + this.bindings = Collections.emptyList(); + } + + BuildRequest(ImageReference name, Function applicationContent, ImageReference builder, + ImageReference runImage, Creator creator, Map env, boolean cleanCache, + boolean verboseLogging, PullPolicy pullPolicy, boolean publish, List buildpacks, + List bindings) { + this.name = name; + this.applicationContent = applicationContent; + this.builder = builder; + this.runImage = runImage; + this.creator = creator; + this.env = env; + this.cleanCache = cleanCache; + this.verboseLogging = verboseLogging; + this.pullPolicy = pullPolicy; + this.publish = publish; + this.buildpacks = buildpacks; + this.bindings = bindings; + } + + /** + * Return a new {@link BuildRequest} with an updated builder. + * @param builder the new builder to use + * @return an updated build request + */ + public BuildRequest withBuilder(ImageReference builder) { + Assert.notNull(builder, "Builder must not be null"); + return new BuildRequest(this.name, this.applicationContent, builder.inTaggedOrDigestForm(), this.runImage, + this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, + this.buildpacks, this.bindings); + } + + /** + * Return a new {@link BuildRequest} with an updated run image. + * @param runImageName the run image to use + * @return an updated build request + */ + public BuildRequest withRunImage(ImageReference runImageName) { + return new BuildRequest(this.name, this.applicationContent, this.builder, runImageName.inTaggedOrDigestForm(), + this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, + this.buildpacks, this.bindings); + } + + /** + * Return a new {@link BuildRequest} with an updated creator. + * @param creator the new {@code Creator} to use + * @return an updated build request + */ + public BuildRequest withCreator(Creator creator) { + Assert.notNull(creator, "Creator must not be null"); + return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, creator, this.env, + this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings); + } + + /** + * Return a new {@link BuildRequest} with an additional env variable. + * @param name the variable name + * @param value the variable value + * @return an updated build request + */ + public BuildRequest withEnv(String name, String value) { + Assert.hasText(name, "Name must not be empty"); + Assert.hasText(value, "Value must not be empty"); + Map env = new LinkedHashMap<>(this.env); + env.put(name, value); + return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, + Collections.unmodifiableMap(env), this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, + this.buildpacks, this.bindings); + } + + /** + * Return a new {@link BuildRequest} with additional env variables. + * @param env the additional variables + * @return an updated build request + */ + public BuildRequest withEnv(Map env) { + Assert.notNull(env, "Env must not be null"); + Map updatedEnv = new LinkedHashMap<>(this.env); + updatedEnv.putAll(env); + return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, + Collections.unmodifiableMap(updatedEnv), this.cleanCache, this.verboseLogging, this.pullPolicy, + this.publish, this.buildpacks, this.bindings); + } + + /** + * Return a new {@link BuildRequest} with an updated clean cache setting. + * @param cleanCache if the cache should be cleaned + * @return an updated build request + */ + public BuildRequest withCleanCache(boolean cleanCache) { + return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, + cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings); + } + + /** + * Return a new {@link BuildRequest} with an updated verbose logging setting. + * @param verboseLogging if verbose logging should be used + * @return an updated build request + */ + public BuildRequest withVerboseLogging(boolean verboseLogging) { + return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, + this.cleanCache, verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings); + } + + /** + * Return a new {@link BuildRequest} with the updated image pull policy. + * @param pullPolicy image pull policy {@link PullPolicy} + * @return an updated build request + */ + public BuildRequest withPullPolicy(PullPolicy pullPolicy) { + return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, + this.cleanCache, this.verboseLogging, pullPolicy, this.publish, this.buildpacks, this.bindings); + } + + /** + * Return a new {@link BuildRequest} with an updated publish setting. + * @param publish if the built image should be pushed to a registry + * @return an updated build request + */ + public BuildRequest withPublish(boolean publish) { + return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, + this.cleanCache, this.verboseLogging, this.pullPolicy, publish, this.buildpacks, this.bindings); + } + + /** + * Return a new {@link BuildRequest} with an updated buildpacks setting. + * @param buildpacks a collection of buildpacks to use when building the image + * @return an updated build request + * @since 2.5.0 + */ + public BuildRequest withBuildpacks(BuildpackReference... buildpacks) { + Assert.notEmpty(buildpacks, "Buildpacks must not be empty"); + return withBuildpacks(Arrays.asList(buildpacks)); + } + + /** + * Return a new {@link BuildRequest} with an updated buildpacks setting. + * @param buildpacks a collection of buildpacks to use when building the image + * @return an updated build request + * @since 2.5.0 + */ + public BuildRequest withBuildpacks(List buildpacks) { + Assert.notNull(buildpacks, "Buildpacks must not be null"); + return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, + this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, buildpacks, this.bindings); + } + + /** + * Return a new {@link BuildRequest} with updated bindings. + * @param bindings a collection of bindings to mount to the build container + * @return an updated build request + * @since 2.5.0 + */ + public BuildRequest withBindings(Binding... bindings) { + Assert.notEmpty(bindings, "Bindings must not be empty"); + return withBindings(Arrays.asList(bindings)); + } + + /** + * Return a new {@link BuildRequest} with updated bindings. + * @param bindings a collection of bindings to mount to the build container + * @return an updated build request + * @since 2.5.0 + */ + public BuildRequest withBindings(List bindings) { + Assert.notNull(bindings, "Bindings must not be null"); + return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, + this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, bindings); + } + + /** + * Return the name of the image that should be created. + * @return the name of the image + */ + public ImageReference getName() { + return this.name; + } + + /** + * Return a {@link TarArchive} containing the application content that the buildpack + * should package. This is typically the contents of the Jar. + * @param owner the owner of the tar entries + * @return the application content + * @see TarArchive#fromZip(File, Owner) + */ + public TarArchive getApplicationContent(Owner owner) { + return this.applicationContent.apply(owner); + } + + /** + * Return the builder that should be used. + * @return the builder to use + */ + public ImageReference getBuilder() { + return this.builder; + } + + /** + * Return the run image that should be used, if provided. + * @return the run image + */ + public ImageReference getRunImage() { + return this.runImage; + } + + /** + * Return the {@link Creator} the builder should use. + * @return the {@code Creator} + */ + public Creator getCreator() { + return this.creator; + } + + /** + * Return any env variable that should be passed to the builder. + * @return the builder env + */ + public Map getEnv() { + return this.env; + } + + /** + * Return if caches should be cleaned before packaging. + * @return if caches should be cleaned + */ + public boolean isCleanCache() { + return this.cleanCache; + } + + /** + * Return if verbose logging output should be used. + * @return if verbose logging should be used + */ + public boolean isVerboseLogging() { + return this.verboseLogging; + } + + /** + * Return if the built image should be pushed to a registry. + * @return if the built image should be pushed to a registry + */ + public boolean isPublish() { + return this.publish; + } + + /** + * Return the image {@link PullPolicy} that the builder should use. + * @return image pull policy + */ + public PullPolicy getPullPolicy() { + return this.pullPolicy; + } + + /** + * Return the collection of buildpacks to use when building the image, if provided. + * @return the buildpacks + */ + public List getBuildpacks() { + return this.buildpacks; + } + + /** + * Return the collection of bindings to mount to the build container. + * @return the bindings + * @since 2.5.0 + */ + public List getBindings() { + return this.bindings; + } + + /** + * Factory method to create a new {@link BuildRequest} from a JAR file. + * @param jarFile the source jar file + * @return a new build request instance + */ + public static BuildRequest forJarFile(File jarFile) { + assertJarFile(jarFile); + return forJarFile(ImageReference.forJarFile(jarFile).inTaggedForm(), jarFile); + } + + /** + * Factory method to create a new {@link BuildRequest} from a JAR file. + * @param name the name of the image that should be created + * @param jarFile the source jar file + * @return a new build request instance + */ + public static BuildRequest forJarFile(ImageReference name, File jarFile) { + assertJarFile(jarFile); + return new BuildRequest(name, (owner) -> TarArchive.fromZip(jarFile, owner)); + } + + /** + * Factory method to create a new {@link BuildRequest} with specific content. + * @param name the name of the image that should be created + * @param applicationContent function to provide the application content + * @return a new build request instance + */ + public static BuildRequest of(ImageReference name, Function applicationContent) { + return new BuildRequest(name, applicationContent); + } + + private static void assertJarFile(File jarFile) { + Assert.notNull(jarFile, "JarFile must not be null"); + Assert.isTrue(jarFile.exists(), "JarFile must exist"); + Assert.isTrue(jarFile.isFile(), "JarFile must be a file"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java new file mode 100644 index 000000000000..755d49e5e0b5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java @@ -0,0 +1,249 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.io.IOException; +import java.util.List; +import java.util.function.Consumer; + +import org.springframework.boot.buildpack.platform.build.BuilderMetadata.Stack; +import org.springframework.boot.buildpack.platform.docker.DockerApi; +import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent; +import org.springframework.boot.buildpack.platform.docker.TotalProgressPullListener; +import org.springframework.boot.buildpack.platform.docker.TotalProgressPushListener; +import org.springframework.boot.buildpack.platform.docker.UpdateListener; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration; +import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException; +import org.springframework.boot.buildpack.platform.docker.type.Image; +import org.springframework.boot.buildpack.platform.docker.type.ImageReference; +import org.springframework.boot.buildpack.platform.io.IOBiConsumer; +import org.springframework.boot.buildpack.platform.io.TarArchive; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Central API for running buildpack operations. + * + * @author Phillip Webb + * @author Scott Frederick + * @author Andrey Shlykov + * @since 2.3.0 + */ +public class Builder { + + private final BuildLog log; + + private final DockerApi docker; + + private final DockerConfiguration dockerConfiguration; + + /** + * Create a new builder instance. + */ + public Builder() { + this(BuildLog.toSystemOut()); + } + + /** + * Create a new builder instance. + * @param dockerConfiguration the docker configuration + * @since 2.4.0 + */ + public Builder(DockerConfiguration dockerConfiguration) { + this(BuildLog.toSystemOut(), dockerConfiguration); + } + + /** + * Create a new builder instance. + * @param log a logger used to record output + */ + public Builder(BuildLog log) { + this(log, new DockerApi(), null); + } + + /** + * Create a new builder instance. + * @param log a logger used to record output + * @param dockerConfiguration the docker configuration + * @since 2.4.0 + */ + public Builder(BuildLog log, DockerConfiguration dockerConfiguration) { + this(log, new DockerApi(dockerConfiguration), dockerConfiguration); + } + + Builder(BuildLog log, DockerApi docker, DockerConfiguration dockerConfiguration) { + Assert.notNull(log, "Log must not be null"); + this.log = log; + this.docker = docker; + this.dockerConfiguration = dockerConfiguration; + } + + public void build(BuildRequest request) throws DockerEngineException, IOException { + Assert.notNull(request, "Request must not be null"); + this.log.start(request); + String domain = request.getBuilder().getDomain(); + PullPolicy pullPolicy = request.getPullPolicy(); + ImageFetcher imageFetcher = new ImageFetcher(domain, getBuilderAuthHeader(), pullPolicy); + Image builderImage = imageFetcher.fetchImage(ImageType.BUILDER, request.getBuilder()); + BuilderMetadata builderMetadata = BuilderMetadata.fromImage(builderImage); + request = withRunImageIfNeeded(request, builderMetadata.getStack()); + Image runImage = imageFetcher.fetchImage(ImageType.RUNNER, request.getRunImage()); + assertStackIdsMatch(runImage, builderImage); + BuildOwner buildOwner = BuildOwner.fromEnv(builderImage.getConfig().getEnv()); + Buildpacks buildpacks = getBuildpacks(request, imageFetcher, builderMetadata); + EphemeralBuilder ephemeralBuilder = new EphemeralBuilder(buildOwner, builderImage, request.getName(), + builderMetadata, request.getCreator(), request.getEnv(), buildpacks); + this.docker.image().load(ephemeralBuilder.getArchive(), UpdateListener.none()); + try { + executeLifecycle(request, ephemeralBuilder); + if (request.isPublish()) { + pushImage(request.getName()); + } + } + finally { + this.docker.image().remove(ephemeralBuilder.getName(), true); + } + } + + private BuildRequest withRunImageIfNeeded(BuildRequest request, Stack builderStack) { + if (request.getRunImage() != null) { + return request; + } + return request.withRunImage(getRunImageReferenceForStack(builderStack)); + } + + private ImageReference getRunImageReferenceForStack(Stack stack) { + String name = stack.getRunImage().getImage(); + Assert.state(StringUtils.hasText(name), "Run image must be specified in the builder image stack"); + return ImageReference.of(name).inTaggedOrDigestForm(); + } + + private void assertStackIdsMatch(Image runImage, Image builderImage) { + StackId runImageStackId = StackId.fromImage(runImage); + StackId builderImageStackId = StackId.fromImage(builderImage); + Assert.state(runImageStackId.equals(builderImageStackId), () -> "Run image stack '" + runImageStackId + + "' does not match builder stack '" + builderImageStackId + "'"); + } + + private Buildpacks getBuildpacks(BuildRequest request, ImageFetcher imageFetcher, BuilderMetadata builderMetadata) { + BuildpackResolverContext resolverContext = new BuilderResolverContext(imageFetcher, builderMetadata); + return BuildpackResolvers.resolveAll(resolverContext, request.getBuildpacks()); + } + + private void executeLifecycle(BuildRequest request, EphemeralBuilder builder) throws IOException { + try (Lifecycle lifecycle = new Lifecycle(this.log, this.docker, request, builder)) { + lifecycle.execute(); + } + } + + private void pushImage(ImageReference reference) throws IOException { + Consumer progressConsumer = this.log.pushingImage(reference); + TotalProgressPushListener listener = new TotalProgressPushListener(progressConsumer); + this.docker.image().push(reference, listener, getPublishAuthHeader()); + this.log.pushedImage(reference); + } + + private String getBuilderAuthHeader() { + return (this.dockerConfiguration != null && this.dockerConfiguration.getBuilderRegistryAuthentication() != null) + ? this.dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader() : null; + } + + private String getPublishAuthHeader() { + return (this.dockerConfiguration != null && this.dockerConfiguration.getPublishRegistryAuthentication() != null) + ? this.dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader() : null; + } + + /** + * Internal utility class used to fetch images. + */ + private class ImageFetcher { + + private final String domain; + + private final String authHeader; + + private final PullPolicy pullPolicy; + + ImageFetcher(String domain, String authHeader, PullPolicy pullPolicy) { + this.domain = domain; + this.authHeader = authHeader; + this.pullPolicy = pullPolicy; + } + + Image fetchImage(ImageType type, ImageReference reference) throws IOException { + Assert.notNull(type, "Type must not be null"); + Assert.notNull(reference, "Reference must not be null"); + Assert.state(this.authHeader == null || reference.getDomain().equals(this.domain), + () -> String.format("%s '%s' must be pulled from the '%s' authenticated registry", + StringUtils.capitalize(type.getDescription()), reference, this.domain)); + if (this.pullPolicy == PullPolicy.ALWAYS) { + return pullImage(reference, type); + } + try { + return Builder.this.docker.image().inspect(reference); + } + catch (DockerEngineException ex) { + if (this.pullPolicy == PullPolicy.IF_NOT_PRESENT && ex.getStatusCode() == 404) { + return pullImage(reference, type); + } + throw ex; + } + } + + private Image pullImage(ImageReference reference, ImageType imageType) throws IOException { + TotalProgressPullListener listener = new TotalProgressPullListener( + Builder.this.log.pullingImage(reference, imageType)); + Image image = Builder.this.docker.image().pull(reference, listener, this.authHeader); + Builder.this.log.pulledImage(image, imageType); + return image; + } + + } + + /** + * {@link BuildpackResolverContext} implementation for the {@link Builder}. + */ + private class BuilderResolverContext implements BuildpackResolverContext { + + private final ImageFetcher imageFetcher; + + private final BuilderMetadata builderMetadata; + + BuilderResolverContext(ImageFetcher imageFetcher, BuilderMetadata builderMetadata) { + this.imageFetcher = imageFetcher; + this.builderMetadata = builderMetadata; + } + + @Override + public List getBuildpackMetadata() { + return this.builderMetadata.getBuildpacks(); + } + + @Override + public Image fetchImage(ImageReference reference, ImageType imageType) throws IOException { + return this.imageFetcher.fetchImage(imageType, reference); + } + + @Override + public void exportImageLayers(ImageReference reference, IOBiConsumer exports) + throws IOException { + Builder.this.docker.image().exportLayers(reference, exports); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuilderBuildpack.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuilderBuildpack.java new file mode 100644 index 000000000000..d67b0000da9f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuilderBuildpack.java @@ -0,0 +1,117 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.io.IOException; + +import org.springframework.boot.buildpack.platform.docker.type.Layer; +import org.springframework.boot.buildpack.platform.io.IOConsumer; +import org.springframework.util.Assert; + +/** + * A {@link Buildpack} that references a buildpack contained in the builder. + * + * The buildpack reference must contain a buildpack ID (for example, + * {@code "example/buildpack"}) or a buildpack ID and version (for example, + * {@code "example/buildpack@1.0.0"}). The reference can optionally contain a prefix + * {@code urn:cnb:builder:} to unambiguously identify it as a builder buildpack reference. + * If a version is not provided, the reference will match any version of a buildpack with + * the same ID as the reference. + * + * @author Scott Frederick + */ +class BuilderBuildpack implements Buildpack { + + private static final String PREFIX = "urn:cnb:builder:"; + + private final BuildpackCoordinates coordinates; + + BuilderBuildpack(BuildpackMetadata buildpackMetadata) { + this.coordinates = BuildpackCoordinates.fromBuildpackMetadata(buildpackMetadata); + } + + @Override + public BuildpackCoordinates getCoordinates() { + return this.coordinates; + } + + @Override + public void apply(IOConsumer layers) throws IOException { + } + + /** + * A {@link BuildpackResolver} compatible method to resolve builder buildpacks. + * @param context the resolver context + * @param reference the buildpack reference + * @return the resolved {@link Buildpack} or {@code null} + */ + static Buildpack resolve(BuildpackResolverContext context, BuildpackReference reference) { + boolean unambiguous = reference.hasPrefix(PREFIX); + BuilderReference builderReference = BuilderReference + .of(unambiguous ? reference.getSubReference(PREFIX) : reference.toString()); + BuildpackMetadata buildpackMetadata = findBuildpackMetadata(context, builderReference); + if (unambiguous) { + Assert.isTrue(buildpackMetadata != null, () -> "Buildpack '" + reference + "' not found in builder"); + } + return (buildpackMetadata != null) ? new BuilderBuildpack(buildpackMetadata) : null; + } + + private static BuildpackMetadata findBuildpackMetadata(BuildpackResolverContext context, + BuilderReference builderReference) { + for (BuildpackMetadata candidate : context.getBuildpackMetadata()) { + if (builderReference.matches(candidate)) { + return candidate; + } + } + return null; + } + + /** + * A reference to a buildpack builder. + */ + static class BuilderReference { + + private final String id; + + private final String version; + + BuilderReference(String id, String version) { + this.id = id; + this.version = version; + } + + @Override + public String toString() { + return (this.version != null) ? this.id + "@" + this.version : this.id; + } + + boolean matches(BuildpackMetadata candidate) { + return this.id.equals(candidate.getId()) + && (this.version == null || this.version.equals(candidate.getVersion())); + } + + static BuilderReference of(String value) { + if (value.contains("@")) { + String[] parts = value.split("@"); + return new BuilderReference(parts[0], parts[1]); + } + return new BuilderReference(value, null); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuilderException.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuilderException.java new file mode 100644 index 000000000000..a218dec6b195 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuilderException.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +/** + * Exception thrown to indicate a Builder error. + * + * @author Scott Frederick + * @since 2.3.0 + */ +public class BuilderException extends RuntimeException { + + private final String operation; + + private final int statusCode; + + BuilderException(String operation, int statusCode) { + super(buildMessage(operation, statusCode)); + this.operation = operation; + this.statusCode = statusCode; + } + + /** + * Return the Builder operation that failed. + * @return the operation description + */ + public String getOperation() { + return this.operation; + } + + /** + * Return the status code returned from a Builder operation. + * @return the statusCode the status code + */ + public int getStatusCode() { + return this.statusCode; + } + + private static String buildMessage(String operation, int statusCode) { + StringBuilder message = new StringBuilder("Builder"); + if (operation != null && !operation.isEmpty()) { + message.append(" lifecycle '").append(operation).append("'"); + } + message.append(" failed with status code ").append(statusCode); + return message.toString(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuilderMetadata.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuilderMetadata.java new file mode 100644 index 000000000000..d08c11bfa04c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuilderMetadata.java @@ -0,0 +1,317 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import org.springframework.boot.buildpack.platform.docker.type.Image; +import org.springframework.boot.buildpack.platform.docker.type.ImageConfig; +import org.springframework.boot.buildpack.platform.json.MappedObject; +import org.springframework.boot.buildpack.platform.json.SharedObjectMapper; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Builder metadata information. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @author Scott Frederick + */ +class BuilderMetadata extends MappedObject { + + private static final String LABEL_NAME = "io.buildpacks.builder.metadata"; + + private static final String[] EMPTY_MIRRORS = {}; + + private final Stack stack; + + private final Lifecycle lifecycle; + + private final CreatedBy createdBy; + + private final List buildpacks; + + BuilderMetadata(JsonNode node) { + super(node, MethodHandles.lookup()); + this.stack = valueAt("/stack", Stack.class); + this.lifecycle = valueAt("/lifecycle", Lifecycle.class); + this.createdBy = valueAt("/createdBy", CreatedBy.class); + this.buildpacks = extractBuildpacks(getNode().at("/buildpacks")); + } + + private List extractBuildpacks(JsonNode node) { + if (node.isEmpty()) { + return Collections.emptyList(); + } + List entries = new ArrayList<>(); + node.forEach((child) -> entries.add(BuildpackMetadata.fromJson(child))); + return entries; + } + + /** + * Return stack metadata. + * @return the stack metadata + */ + Stack getStack() { + return this.stack; + } + + /** + * Return lifecycle metadata. + * @return the lifecycle metadata + */ + Lifecycle getLifecycle() { + return this.lifecycle; + } + + /** + * Return information about who created the builder. + * @return the created by metadata + */ + CreatedBy getCreatedBy() { + return this.createdBy; + } + + /** + * Return the buildpacks that are bundled in the builder. + * @return the buildpacks + */ + List getBuildpacks() { + return this.buildpacks; + } + + /** + * Create an updated copy of this metadata. + * @param update consumer to apply updates + * @return an updated metadata instance + */ + BuilderMetadata copy(Consumer update) { + return new Update(this).run(update); + } + + /** + * Attach this metadata to the given update callback. + * @param update the update used to attach the metadata + */ + void attachTo(ImageConfig.Update update) { + try { + String json = SharedObjectMapper.get().writeValueAsString(getNode()); + update.withLabel(LABEL_NAME, json); + } + catch (JsonProcessingException ex) { + throw new IllegalStateException(ex); + } + } + + /** + * Factory method to extract {@link BuilderMetadata} from an image. + * @param image the source image + * @return the builder metadata + * @throws IOException on IO error + */ + static BuilderMetadata fromImage(Image image) throws IOException { + Assert.notNull(image, "Image must not be null"); + return fromImageConfig(image.getConfig()); + } + + /** + * Factory method to extract {@link BuilderMetadata} from image config. + * @param imageConfig the image config + * @return the builder metadata + * @throws IOException on IO error + */ + static BuilderMetadata fromImageConfig(ImageConfig imageConfig) throws IOException { + Assert.notNull(imageConfig, "ImageConfig must not be null"); + String json = imageConfig.getLabels().get(LABEL_NAME); + Assert.notNull(json, () -> "No '" + LABEL_NAME + "' label found in image config labels '" + + StringUtils.collectionToCommaDelimitedString(imageConfig.getLabels().keySet()) + "'"); + return fromJson(json); + } + + /** + * Factory method create {@link BuilderMetadata} from some JSON. + * @param json the source JSON + * @return the builder metadata + * @throws IOException on IO error + */ + static BuilderMetadata fromJson(String json) throws IOException { + return new BuilderMetadata(SharedObjectMapper.get().readTree(json)); + } + + /** + * Stack metadata. + */ + interface Stack { + + /** + * Return run image metadata. + * @return the run image metadata + */ + RunImage getRunImage(); + + /** + * Run image metadata. + */ + interface RunImage { + + /** + * Return the builder image reference. + * @return the image reference + */ + String getImage(); + + /** + * Return stack mirrors. + * @return the stack mirrors + */ + default String[] getMirrors() { + return EMPTY_MIRRORS; + } + + } + + } + + /** + * Lifecycle metadata. + */ + interface Lifecycle { + + /** + * Return the lifecycle version. + * @return the lifecycle version + */ + String getVersion(); + + /** + * Return the default API versions. + * @return the API versions + */ + Api getApi(); + + /** + * Return the supported API versions. + * @return the API versions + */ + Apis getApis(); + + /** + * Default API versions. + */ + interface Api { + + /** + * Return the default buildpack API version. + * @return the buildpack version + */ + String getBuildpack(); + + /** + * Return the default platform API version. + * @return the platform version + */ + String getPlatform(); + + } + + /** + * Supported API versions. + */ + interface Apis { + + /** + * Return the supported buildpack API versions. + * @return the buildpack versions + */ + default String[] getBuildpack() { + return valueAt(this, "/buildpack/supported", String[].class); + } + + /** + * Return the supported platform API versions. + * @return the platform versions + */ + default String[] getPlatform() { + return valueAt(this, "/platform/supported", String[].class); + } + + } + + } + + /** + * Created-by metadata. + */ + interface CreatedBy { + + /** + * Return the name of the creator. + * @return the creator name + */ + String getName(); + + /** + * Return the version of the creator. + * @return the creator version + */ + String getVersion(); + + } + + /** + * Update class used to change data when creating a copy. + */ + static final class Update { + + private final ObjectNode copy; + + private Update(BuilderMetadata source) { + this.copy = source.getNode().deepCopy(); + } + + private BuilderMetadata run(Consumer update) { + update.accept(this); + return new BuilderMetadata(this.copy); + } + + /** + * Update the builder meta-data with a specific created by section. + * @param name the name of the creator + * @param version the version of the creator + */ + void withCreatedBy(String name, String version) { + ObjectNode createdBy = (ObjectNode) this.copy.at("/createdBy"); + if (createdBy == null) { + createdBy = this.copy.putObject("createdBy"); + } + createdBy.put("name", name); + createdBy.put("version", version); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Buildpack.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Buildpack.java new file mode 100644 index 000000000000..0c4e86f484f2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Buildpack.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.io.IOException; + +import org.springframework.boot.buildpack.platform.docker.type.Layer; +import org.springframework.boot.buildpack.platform.io.IOConsumer; + +/** + * A Buildpack that should be invoked by the builder during image building. + * + * @author Scott Frederick + * @see BuildpackResolver + */ +interface Buildpack { + + /** + * Return the coordinates of the builder. + * @return the builder coordinates + */ + BuildpackCoordinates getCoordinates(); + + /** + * Apply the necessary buildpack layers. + * @param layers a consumer that should accept the layers + * @throws IOException on IO error + */ + void apply(IOConsumer layers) throws IOException; + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackCoordinates.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackCoordinates.java new file mode 100644 index 000000000000..954ddd902589 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackCoordinates.java @@ -0,0 +1,140 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; + +import org.tomlj.Toml; +import org.tomlj.TomlParseResult; + +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * A set of buildpack coordinates that uniquely identifies a buildpack. + * + * @author Scott Frederick + * @see Platform + * Interface Specification + */ +final class BuildpackCoordinates { + + private final String id; + + private final String version; + + private BuildpackCoordinates(String id, String version) { + Assert.hasText(id, "ID must not be empty"); + this.id = id; + this.version = version; + } + + String getId() { + return this.id; + } + + /** + * Return the buildpack ID with all "/" replaced by "_". + * @return the ID + */ + String getSanitizedId() { + return this.id.replace("/", "_"); + } + + String getVersion() { + return this.version; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + BuildpackCoordinates other = (BuildpackCoordinates) obj; + return this.id.equals(other.id) && ObjectUtils.nullSafeEquals(this.version, other.version); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + this.id.hashCode(); + result = prime * result + ObjectUtils.nullSafeHashCode(this.version); + return result; + } + + @Override + public String toString() { + return this.id + ((StringUtils.hasText(this.version)) ? "@" + this.version : ""); + } + + /** + * Create {@link BuildpackCoordinates} from a {@code buildpack.toml} + * file. + * @param inputStream an input stream containing {@code buildpack.toml} content + * @param path the path to the buildpack containing the {@code buildpack.toml} file + * @return a new {@link BuildpackCoordinates} instance + * @throws IOException on IO error + */ + static BuildpackCoordinates fromToml(InputStream inputStream, Path path) throws IOException { + return fromToml(Toml.parse(inputStream), path); + } + + private static BuildpackCoordinates fromToml(TomlParseResult toml, Path path) { + Assert.isTrue(!toml.isEmpty(), + () -> "Buildpack descriptor 'buildpack.toml' is required in buildpack '" + path + "'"); + Assert.hasText(toml.getString("buildpack.id"), + () -> "Buildpack descriptor must contain ID in buildpack '" + path + "'"); + Assert.hasText(toml.getString("buildpack.version"), + () -> "Buildpack descriptor must contain version in buildpack '" + path + "'"); + Assert.isTrue(toml.contains("stacks") || toml.contains("order"), + () -> "Buildpack descriptor must contain either 'stacks' or 'order' in buildpack '" + path + "'"); + Assert.isTrue(!(toml.contains("stacks") && toml.contains("order")), + () -> "Buildpack descriptor must not contain both 'stacks' and 'order' in buildpack '" + path + "'"); + return new BuildpackCoordinates(toml.getString("buildpack.id"), toml.getString("buildpack.version")); + } + + /** + * Create {@link BuildpackCoordinates} by extracting values from + * {@link BuildpackMetadata}. + * @param buildpackMetadata the buildpack metadata + * @return a new {@link BuildpackCoordinates} instance + */ + static BuildpackCoordinates fromBuildpackMetadata(BuildpackMetadata buildpackMetadata) { + Assert.notNull(buildpackMetadata, "BuildpackMetadata must not be null"); + return new BuildpackCoordinates(buildpackMetadata.getId(), buildpackMetadata.getVersion()); + } + + /** + * Create {@link BuildpackCoordinates} from an ID and version. + * @param id the buildpack ID + * @param version the buildpack version + * @return a new {@link BuildpackCoordinates} instance + */ + static BuildpackCoordinates of(String id, String version) { + return new BuildpackCoordinates(id, version); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackMetadata.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackMetadata.java new file mode 100644 index 000000000000..5b3d1e109860 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackMetadata.java @@ -0,0 +1,121 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; + +import com.fasterxml.jackson.databind.JsonNode; + +import org.springframework.boot.buildpack.platform.docker.type.Image; +import org.springframework.boot.buildpack.platform.docker.type.ImageConfig; +import org.springframework.boot.buildpack.platform.json.MappedObject; +import org.springframework.boot.buildpack.platform.json.SharedObjectMapper; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Buildpack metadata information. + * + * @author Scott Frederick + */ +final class BuildpackMetadata extends MappedObject { + + private static final String LABEL_NAME = "io.buildpacks.buildpackage.metadata"; + + private final String id; + + private final String version; + + private final String homepage; + + private BuildpackMetadata(JsonNode node) { + super(node, MethodHandles.lookup()); + this.id = valueAt("/id", String.class); + this.version = valueAt("/version", String.class); + this.homepage = valueAt("/homepage", String.class); + } + + /** + * Return the buildpack ID. + * @return the ID + */ + String getId() { + return this.id; + } + + /** + * Return the buildpack version. + * @return the version + */ + String getVersion() { + return this.version; + } + + /** + * Return the buildpack homepage address. + * @return the homepage + */ + String getHomepage() { + return this.homepage; + } + + /** + * Factory method to extract {@link BuildpackMetadata} from an image. + * @param image the source image + * @return the builder metadata + * @throws IOException on IO error + */ + static BuildpackMetadata fromImage(Image image) throws IOException { + Assert.notNull(image, "Image must not be null"); + return fromImageConfig(image.getConfig()); + } + + /** + * Factory method to extract {@link BuildpackMetadata} from image config. + * @param imageConfig the source image config + * @return the builder metadata + * @throws IOException on IO error + */ + static BuildpackMetadata fromImageConfig(ImageConfig imageConfig) throws IOException { + Assert.notNull(imageConfig, "ImageConfig must not be null"); + String json = imageConfig.getLabels().get(LABEL_NAME); + Assert.notNull(json, () -> "No '" + LABEL_NAME + "' label found in image config labels '" + + StringUtils.collectionToCommaDelimitedString(imageConfig.getLabels().keySet()) + "'"); + return fromJson(json); + } + + /** + * Factory method create {@link BuildpackMetadata} from JSON. + * @param json the source JSON + * @return the builder metadata + * @throws IOException on IO error + */ + static BuildpackMetadata fromJson(String json) throws IOException { + return fromJson(SharedObjectMapper.get().readTree(json)); + } + + /** + * Factory method create {@link BuildpackMetadata} from JSON. + * @param node the source JSON + * @return the builder metadata + */ + static BuildpackMetadata fromJson(JsonNode node) { + return new BuildpackMetadata(node); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackReference.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackReference.java new file mode 100644 index 000000000000..a9059fd71add --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackReference.java @@ -0,0 +1,100 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.springframework.util.Assert; + +/** + * An opaque reference to a {@link Buildpack}. + * + * @author Phillip Webb + * @author Scott Frederick + * @since 2.5.0 + * @see BuildpackResolver + */ +public final class BuildpackReference { + + private final String value; + + private BuildpackReference(String value) { + this.value = value; + } + + boolean hasPrefix(String prefix) { + return this.value.startsWith(prefix); + } + + String getSubReference(String prefix) { + return this.value.startsWith(prefix) ? this.value.substring(prefix.length()) : null; + } + + Path asPath() { + try { + URL url = new URL(this.value); + if (url.getProtocol().equals("file")) { + return Paths.get(url.getPath()); + } + return null; + } + catch (MalformedURLException ex) { + // not a URL, fall through to attempting to find a plain file path + } + try { + return Paths.get(this.value); + } + catch (Exception ex) { + return null; + } + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + return this.value.equals(((BuildpackReference) obj).value); + } + + @Override + public int hashCode() { + return this.value.hashCode(); + } + + @Override + public String toString() { + return this.value; + } + + /** + * Create a new {@link BuildpackReference} from the given value. + * @param value the value to use + * @return a new {@link BuildpackReference} + */ + public static BuildpackReference of(String value) { + Assert.hasText(value, "Value must not be empty"); + return new BuildpackReference(value); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackResolver.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackResolver.java new file mode 100644 index 000000000000..3711fdb5bee6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackResolver.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +/** + * Strategy inerface used to resolve a {@link BuildpackReference} to a {@link Buildpack}. + * + * @author Scott Frederick + * @author Phillip Webb + * @see BuildpackResolvers + */ +interface BuildpackResolver { + + /** + * Attempt to resolve the given {@link BuildpackReference}. + * @param context the resolver context + * @param reference the reference to resolve + * @return a resolved {@link Buildpack} instance or {@code null} + */ + Buildpack resolve(BuildpackResolverContext context, BuildpackReference reference); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackResolverContext.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackResolverContext.java new file mode 100644 index 000000000000..0dc760115710 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackResolverContext.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.io.IOException; +import java.util.List; + +import org.springframework.boot.buildpack.platform.docker.type.Image; +import org.springframework.boot.buildpack.platform.docker.type.ImageReference; +import org.springframework.boot.buildpack.platform.io.IOBiConsumer; +import org.springframework.boot.buildpack.platform.io.TarArchive; + +/** + * Context passed to a {@link BuildpackResolver}. + * + * @author Scott Frederick + * @author Phillip Webb + */ +interface BuildpackResolverContext { + + List getBuildpackMetadata(); + + /** + * Retrieve an image. + * @param reference the image reference + * @param type the type of image + * @return the retrieved image + * @throws IOException on IO error + */ + Image fetchImage(ImageReference reference, ImageType type) throws IOException; + + /** + * Export the layers of an image. + * @param reference the reference to export + * @param exports a consumer to receive the layers (contents can only be accessed + * during the callback) + * @throws IOException on IO error + */ + void exportImageLayers(ImageReference reference, IOBiConsumer exports) throws IOException; + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackResolvers.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackResolvers.java new file mode 100644 index 000000000000..1883df4264cd --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildpackResolvers.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * All {@link BuildpackResolver} instances that can be used to resolve + * {@link BuildpackReference BuildpackReferences}. + * + * @author Scott Frederick + * @author Phillip Webb + */ +final class BuildpackResolvers { + + private static final List resolvers = getResolvers(); + + private BuildpackResolvers() { + } + + private static List getResolvers() { + List resolvers = new ArrayList<>(); + resolvers.add(BuilderBuildpack::resolve); + resolvers.add(DirectoryBuildpack::resolve); + resolvers.add(TarGzipBuildpack::resolve); + resolvers.add(ImageBuildpack::resolve); + return Collections.unmodifiableList(resolvers); + } + + /** + * Resolve a collection of {@link BuildpackReference BuildpackReferences} to a + * {@link Buildpacks} instance. + * @param context the resolver context + * @param references the references to resolve + * @return a {@link Buildpacks} instance + */ + static Buildpacks resolveAll(BuildpackResolverContext context, Collection references) { + Assert.notNull(context, "Context must not be null"); + if (CollectionUtils.isEmpty(references)) { + return Buildpacks.EMPTY; + } + List buildpacks = new ArrayList<>(references.size()); + for (BuildpackReference reference : references) { + buildpacks.add(resolve(context, reference)); + } + return Buildpacks.of(buildpacks); + } + + private static Buildpack resolve(BuildpackResolverContext context, BuildpackReference reference) { + Assert.notNull(reference, "Reference must not be null"); + for (BuildpackResolver resolver : resolvers) { + Buildpack buildpack = resolver.resolve(context, reference); + if (buildpack != null) { + return buildpack; + } + } + throw new IllegalArgumentException("Invalid buildpack reference '" + reference + "'"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Buildpacks.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Buildpacks.java new file mode 100644 index 000000000000..6657dcd07479 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Buildpacks.java @@ -0,0 +1,86 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +import org.springframework.boot.buildpack.platform.docker.type.Layer; +import org.springframework.boot.buildpack.platform.io.Content; +import org.springframework.boot.buildpack.platform.io.IOConsumer; +import org.springframework.boot.buildpack.platform.io.Layout; +import org.springframework.boot.buildpack.platform.io.Owner; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +/** + * A collection of {@link Buildpack} instances that can be used to apply buildpack layers. + * + * @author Scott Frederick + * @author Phillip Webb + */ +final class Buildpacks { + + static final Buildpacks EMPTY = new Buildpacks(Collections.emptyList()); + + private final List buildpacks; + + private Buildpacks(List buildpacks) { + this.buildpacks = buildpacks; + } + + List getBuildpacks() { + return this.buildpacks; + } + + void apply(IOConsumer layers) throws IOException { + if (!this.buildpacks.isEmpty()) { + for (Buildpack buildpack : this.buildpacks) { + buildpack.apply(layers); + } + layers.accept(Layer.of(this::addOrderLayerContent)); + } + } + + void addOrderLayerContent(Layout layout) throws IOException { + layout.file("/cnb/order.toml", Owner.ROOT, Content.of(getOrderToml())); + } + + private String getOrderToml() { + StringBuilder builder = new StringBuilder(); + builder.append("[[order]]\n\n"); + for (Buildpack buildpack : this.buildpacks) { + appendToOrderToml(builder, buildpack.getCoordinates()); + } + return builder.toString(); + } + + private void appendToOrderToml(StringBuilder builder, BuildpackCoordinates coordinates) { + builder.append(" [[order.group]]\n"); + builder.append(" id = \"" + coordinates.getId() + "\"\n"); + if (StringUtils.hasText(coordinates.getVersion())) { + builder.append(" version = \"" + coordinates.getVersion() + "\"\n"); + } + builder.append("\n"); + } + + static Buildpacks of(List buildpacks) { + return CollectionUtils.isEmpty(buildpacks) ? EMPTY : new Buildpacks(buildpacks); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Creator.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Creator.java new file mode 100644 index 000000000000..d4d38148a38c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Creator.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import org.springframework.util.Assert; + +/** + * Identifying information about the tooling that created a builder. + * + * @author Scott Frederick + * @since 2.3.0 + */ +public class Creator { + + private final String version; + + Creator(String version) { + this.version = version; + } + + /** + * Return the name of the builder creator. + * @return the name + */ + public String getName() { + return "Spring Boot"; + } + + /** + * Return the version of the builder creator. + * @return the version + */ + public String getVersion() { + return this.version; + } + + /** + * Create a new {@code Creator} using the provided version. + * @param version the creator version + * @return a new creator instance + */ + public static Creator withVersion(String version) { + Assert.notNull(version, "Version must not be null"); + return new Creator(version); + } + + @Override + public String toString() { + return getName() + " version " + getVersion(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/DirectoryBuildpack.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/DirectoryBuildpack.java new file mode 100644 index 000000000000..bac39277e399 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/DirectoryBuildpack.java @@ -0,0 +1,158 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; + +import org.springframework.boot.buildpack.platform.docker.type.Layer; +import org.springframework.boot.buildpack.platform.io.Content; +import org.springframework.boot.buildpack.platform.io.FilePermissions; +import org.springframework.boot.buildpack.platform.io.IOConsumer; +import org.springframework.boot.buildpack.platform.io.Layout; +import org.springframework.boot.buildpack.platform.io.Owner; +import org.springframework.util.Assert; + +/** + * A {@link Buildpack} that references a buildpack in a directory on the local file + * system. + * + * The file system must contain a buildpack descriptor named {@code buildpack.toml} in the + * root of the directory. The contents of the directory tree will be provided as a single + * layer to be included in the builder image. + * + * @author Scott Frederick + */ +final class DirectoryBuildpack implements Buildpack { + + private final Path path; + + private final BuildpackCoordinates coordinates; + + private DirectoryBuildpack(Path path) { + this.path = path; + this.coordinates = findBuildpackCoordinates(path); + } + + private BuildpackCoordinates findBuildpackCoordinates(Path path) { + Path buildpackToml = path.resolve("buildpack.toml"); + Assert.isTrue(Files.exists(buildpackToml), + () -> "Buildpack descriptor 'buildpack.toml' is required in buildpack '" + path + "'"); + try { + try (InputStream inputStream = Files.newInputStream(buildpackToml)) { + return BuildpackCoordinates.fromToml(inputStream, path); + } + } + catch (IOException ex) { + throw new IllegalArgumentException("Error parsing descriptor for buildpack '" + path + "'", ex); + } + } + + @Override + public BuildpackCoordinates getCoordinates() { + return this.coordinates; + } + + @Override + public void apply(IOConsumer layers) throws IOException { + layers.accept(Layer.of(this::addLayerContent)); + } + + private void addLayerContent(Layout layout) throws IOException { + String id = this.coordinates.getSanitizedId(); + Path cnbPath = Paths.get("/cnb/buildpacks/", id, this.coordinates.getVersion()); + writeBasePathEntries(layout, cnbPath); + Files.walkFileTree(this.path, new LayoutFileVisitor(this.path, cnbPath, layout)); + } + + private void writeBasePathEntries(Layout layout, Path basePath) throws IOException { + int pathCount = basePath.getNameCount(); + for (int pathIndex = 1; pathIndex < pathCount + 1; pathIndex++) { + String name = "/" + basePath.subpath(0, pathIndex) + "/"; + layout.directory(name, Owner.ROOT); + } + } + + /** + * A {@link BuildpackResolver} compatible method to resolve directory buildpacks. + * @param context the resolver context + * @param reference the buildpack reference + * @return the resolved {@link Buildpack} or {@code null} + */ + static Buildpack resolve(BuildpackResolverContext context, BuildpackReference reference) { + Path path = reference.asPath(); + if (path != null && Files.exists(path) && Files.isDirectory(path)) { + return new DirectoryBuildpack(path); + } + return null; + } + + /** + * {@link SimpleFileVisitor} to used to create the {@link Layout}. + */ + private static class LayoutFileVisitor extends SimpleFileVisitor { + + private final Path basePath; + + private final Path layerPath; + + private final Layout layout; + + LayoutFileVisitor(Path basePath, Path layerPath, Layout layout) { + this.basePath = basePath; + this.layerPath = layerPath; + this.layout = layout; + } + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + if (!dir.equals(this.basePath)) { + this.layout.directory(relocate(dir), Owner.ROOT, getMode(dir)); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + this.layout.file(relocate(file), Owner.ROOT, getMode(file), Content.of(file.toFile())); + return FileVisitResult.CONTINUE; + } + + private int getMode(Path path) throws IOException { + try { + return FilePermissions.umaskForPath(path); + } + catch (IllegalStateException ex) { + throw new IllegalStateException( + "Buildpack content in a directory is not supported on this operating system"); + } + } + + private String relocate(Path path) { + Path node = path.subpath(this.basePath.getNameCount(), path.getNameCount()); + return Paths.get(this.layerPath.toString(), node.toString()).toString(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilder.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilder.java new file mode 100644 index 000000000000..1f221327da82 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilder.java @@ -0,0 +1,129 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.io.IOException; +import java.util.Map; + +import org.springframework.boot.buildpack.platform.docker.type.Image; +import org.springframework.boot.buildpack.platform.docker.type.ImageArchive; +import org.springframework.boot.buildpack.platform.docker.type.ImageReference; +import org.springframework.boot.buildpack.platform.docker.type.Layer; +import org.springframework.boot.buildpack.platform.io.Content; +import org.springframework.boot.buildpack.platform.io.Owner; + +/** + * An short lived builder that is created for each {@link Lifecycle} run. + * + * @author Phillip Webb + * @author Scott Frederick + */ +class EphemeralBuilder { + + static final String BUILDER_FOR_LABEL_NAME = "org.springframework.boot.builderFor"; + + private final BuildOwner buildOwner; + + private final BuilderMetadata builderMetadata; + + private final ImageArchive archive; + + private final Creator creator; + + /** + * Create a new {@link EphemeralBuilder} instance. + * @param buildOwner the build owner + * @param builderImage the base builder image + * @param targetImage the image being built + * @param builderMetadata the builder metadata + * @param creator the builder creator + * @param env the builder env + * @param buildpacks an optional set of buildpacks to apply + * @throws IOException on IO error + */ + EphemeralBuilder(BuildOwner buildOwner, Image builderImage, ImageReference targetImage, + BuilderMetadata builderMetadata, Creator creator, Map env, Buildpacks buildpacks) + throws IOException { + ImageReference name = ImageReference.random("pack.local/builder/").inTaggedForm(); + this.buildOwner = buildOwner; + this.creator = creator; + this.builderMetadata = builderMetadata.copy(this::updateMetadata); + this.archive = ImageArchive.from(builderImage, (update) -> { + update.withUpdatedConfig(this.builderMetadata::attachTo); + update.withUpdatedConfig((config) -> config.withLabel(BUILDER_FOR_LABEL_NAME, targetImage.toString())); + update.withTag(name); + if (env != null && !env.isEmpty()) { + update.withNewLayer(getEnvLayer(env)); + } + if (buildpacks != null) { + buildpacks.apply(update::withNewLayer); + } + }); + } + + private void updateMetadata(BuilderMetadata.Update update) { + update.withCreatedBy(this.creator.getName(), this.creator.getVersion()); + } + + private Layer getEnvLayer(Map env) throws IOException { + return Layer.of((layout) -> { + for (Map.Entry entry : env.entrySet()) { + String name = "/platform/env/" + entry.getKey(); + Content content = Content.of((entry.getValue() != null) ? entry.getValue() : ""); + layout.file(name, Owner.ROOT, content); + } + }); + } + + /** + * Return the name of this archive as tagged in Docker. + * @return the ephemeral builder name + */ + ImageReference getName() { + return this.archive.getTag(); + } + + /** + * Return the build owner that should be used for written content. + * @return the builder owner + */ + Owner getBuildOwner() { + return this.buildOwner; + } + + /** + * Return the builder meta-data that was used to create this ephemeral builder. + * @return the builder meta-data + */ + BuilderMetadata getBuilderMetadata() { + return this.builderMetadata; + } + + /** + * Return the contents of ephemeral builder for passing to Docker. + * @return the ephemeral builder archive + */ + ImageArchive getArchive() { + return this.archive; + } + + @Override + public String toString() { + return this.archive.getTag().toString(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageBuildpack.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageBuildpack.java new file mode 100644 index 000000000000..6b383bbfcfd1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageBuildpack.java @@ -0,0 +1,143 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; + +import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException; +import org.springframework.boot.buildpack.platform.docker.type.Image; +import org.springframework.boot.buildpack.platform.docker.type.ImageReference; +import org.springframework.boot.buildpack.platform.docker.type.Layer; +import org.springframework.boot.buildpack.platform.io.IOConsumer; +import org.springframework.boot.buildpack.platform.io.TarArchive; +import org.springframework.util.StreamUtils; + +/** + * A {@link Buildpack} that references a buildpack contained in an OCI image. + * + * The reference must be an OCI image reference. The reference can optionally contain a + * prefix {@code docker://} to unambiguously identify it as an image buildpack reference. + * + * @author Scott Frederick + * @author Phillip Webb + */ +final class ImageBuildpack implements Buildpack { + + private static final String PREFIX = "docker://"; + + private final BuildpackCoordinates coordinates; + + private final ExportedLayers exportedLayers; + + private ImageBuildpack(BuildpackResolverContext context, ImageReference imageReference) { + ImageReference reference = imageReference.inTaggedOrDigestForm(); + try { + Image image = context.fetchImage(reference, ImageType.BUILDPACK); + BuildpackMetadata buildpackMetadata = BuildpackMetadata.fromImage(image); + this.coordinates = BuildpackCoordinates.fromBuildpackMetadata(buildpackMetadata); + this.exportedLayers = new ExportedLayers(context, reference); + } + catch (IOException | DockerEngineException ex) { + throw new IllegalArgumentException("Error pulling buildpack image '" + reference + "'", ex); + } + } + + @Override + public BuildpackCoordinates getCoordinates() { + return this.coordinates; + } + + @Override + public void apply(IOConsumer layers) throws IOException { + this.exportedLayers.apply(layers); + } + + /** + * A {@link BuildpackResolver} compatible method to resolve image buildpacks. + * @param context the resolver context + * @param reference the buildpack reference + * @return the resolved {@link Buildpack} or {@code null} + */ + static Buildpack resolve(BuildpackResolverContext context, BuildpackReference reference) { + boolean unambiguous = reference.hasPrefix(PREFIX); + try { + ImageReference imageReference = ImageReference + .of((unambiguous) ? reference.getSubReference(PREFIX) : reference.toString()); + return new ImageBuildpack(context, imageReference); + } + catch (IllegalArgumentException ex) { + if (unambiguous) { + throw ex; + } + return null; + } + } + + private static class ExportedLayers { + + private final List layerFiles; + + ExportedLayers(BuildpackResolverContext context, ImageReference imageReference) throws IOException { + List layerFiles = new ArrayList<>(); + context.exportImageLayers(imageReference, (name, archive) -> layerFiles.add(copyToTemp(name, archive))); + this.layerFiles = Collections.unmodifiableList(layerFiles); + } + + private Path copyToTemp(String name, TarArchive archive) throws IOException { + String[] parts = name.split("/"); + Path path = Files.createTempFile("create-builder-scratch-", parts[0]); + try (OutputStream out = Files.newOutputStream(path)) { + archive.writeTo(out); + } + return path; + } + + void apply(IOConsumer layers) throws IOException { + for (Path path : this.layerFiles) { + layers.accept(Layer.fromTarArchive((out) -> copyLayerTar(path, out))); + } + } + + private void copyLayerTar(Path path, OutputStream out) throws IOException { + try (TarArchiveInputStream tarIn = new TarArchiveInputStream(Files.newInputStream(path)); + TarArchiveOutputStream tarOut = new TarArchiveOutputStream(out)) { + tarOut.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); + TarArchiveEntry entry = tarIn.getNextTarEntry(); + while (entry != null) { + tarOut.putArchiveEntry(entry); + StreamUtils.copy(tarIn, tarOut); + tarOut.closeArchiveEntry(); + entry = tarIn.getNextTarEntry(); + } + tarOut.finish(); + } + Files.delete(path); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageType.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageType.java new file mode 100644 index 000000000000..6a8cd4a1ba4a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageType.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +/** + * Image types. + * + * @author Andrey Shlykov + */ +enum ImageType { + + /** + * Builder image. + */ + BUILDER("builder image"), + + /** + * Run image. + */ + RUNNER("run image"), + + /** + * Buildpack image. + */ + BUILDPACK("buildpack image"); + + private final String description; + + ImageType(String description) { + this.description = description; + } + + String getDescription() { + return this.description; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java new file mode 100644 index 000000000000..c6adc770964b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java @@ -0,0 +1,268 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.io.Closeable; +import java.io.IOException; +import java.util.function.Consumer; + +import org.springframework.boot.buildpack.platform.docker.DockerApi; +import org.springframework.boot.buildpack.platform.docker.LogUpdateEvent; +import org.springframework.boot.buildpack.platform.docker.type.Binding; +import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig; +import org.springframework.boot.buildpack.platform.docker.type.ContainerContent; +import org.springframework.boot.buildpack.platform.docker.type.ContainerReference; +import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus; +import org.springframework.boot.buildpack.platform.docker.type.ImageReference; +import org.springframework.boot.buildpack.platform.docker.type.VolumeName; +import org.springframework.boot.buildpack.platform.io.TarArchive; +import org.springframework.util.Assert; + +/** + * A buildpack lifecycle used to run the build {@link Phase phases} needed to package an + * application. + * + * @author Phillip Webb + * @author Scott Frederick + */ +class Lifecycle implements Closeable { + + private static final LifecycleVersion LOGGING_MINIMUM_VERSION = LifecycleVersion.parse("0.0.5"); + + private static final String PLATFORM_API_VERSION_KEY = "CNB_PLATFORM_API"; + + private final BuildLog log; + + private final DockerApi docker; + + private final BuildRequest request; + + private final EphemeralBuilder builder; + + private final LifecycleVersion lifecycleVersion; + + private final ApiVersion platformVersion; + + private final VolumeName layersVolume; + + private final VolumeName applicationVolume; + + private final VolumeName buildCacheVolume; + + private final VolumeName launchCacheVolume; + + private boolean executed; + + private boolean applicationVolumePopulated; + + /** + * Create a new {@link Lifecycle} instance. + * @param log build output log + * @param docker the Docker API + * @param request the request to process + * @param builder the ephemeral builder used to run the phases + */ + Lifecycle(BuildLog log, DockerApi docker, BuildRequest request, EphemeralBuilder builder) { + this.log = log; + this.docker = docker; + this.request = request; + this.builder = builder; + this.lifecycleVersion = LifecycleVersion.parse(builder.getBuilderMetadata().getLifecycle().getVersion()); + this.platformVersion = getPlatformVersion(builder.getBuilderMetadata().getLifecycle()); + this.layersVolume = createRandomVolumeName("pack-layers-"); + this.applicationVolume = createRandomVolumeName("pack-app-"); + this.buildCacheVolume = createCacheVolumeName(request, ".build"); + this.launchCacheVolume = createCacheVolumeName(request, ".launch"); + } + + protected VolumeName createRandomVolumeName(String prefix) { + return VolumeName.random(prefix); + } + + private VolumeName createCacheVolumeName(BuildRequest request, String suffix) { + return VolumeName.basedOn(request.getName(), ImageReference::toLegacyString, "pack-cache-", suffix, 6); + } + + private ApiVersion getPlatformVersion(BuilderMetadata.Lifecycle lifecycle) { + if (lifecycle.getApis().getPlatform() != null) { + String[] supportedVersions = lifecycle.getApis().getPlatform(); + return ApiVersions.SUPPORTED_PLATFORMS.findLatestSupported(supportedVersions); + } + String version = lifecycle.getApi().getPlatform(); + return ApiVersions.SUPPORTED_PLATFORMS.findLatestSupported(version); + } + + /** + * Execute this lifecycle by running each phase in turn. + * @throws IOException on IO error + */ + void execute() throws IOException { + Assert.state(!this.executed, "Lifecycle has already been executed"); + this.executed = true; + this.log.executingLifecycle(this.request, this.lifecycleVersion, this.buildCacheVolume); + if (this.request.isCleanCache()) { + deleteVolume(this.buildCacheVolume); + } + run(createPhase()); + this.log.executedLifecycle(this.request); + } + + private Phase createPhase() { + Phase phase = new Phase("creator", isVerboseLogging()); + phase.withDaemonAccess(); + phase.withLogLevelArg(); + phase.withArgs("-app", Directory.APPLICATION); + phase.withArgs("-platform", Directory.PLATFORM); + phase.withArgs("-run-image", this.request.getRunImage()); + phase.withArgs("-layers", Directory.LAYERS); + phase.withArgs("-cache-dir", Directory.CACHE); + phase.withArgs("-launch-cache", Directory.LAUNCH_CACHE); + phase.withArgs("-daemon"); + if (this.request.isCleanCache()) { + phase.withArgs("-skip-restore"); + } + if (requiresProcessTypeDefault()) { + phase.withArgs("-process-type=web"); + } + phase.withArgs(this.request.getName()); + phase.withBinding(Binding.from(this.layersVolume, Directory.LAYERS)); + phase.withBinding(Binding.from(this.applicationVolume, Directory.APPLICATION)); + phase.withBinding(Binding.from(this.buildCacheVolume, Directory.CACHE)); + phase.withBinding(Binding.from(this.launchCacheVolume, Directory.LAUNCH_CACHE)); + if (this.request.getBindings() != null) { + this.request.getBindings().forEach(phase::withBinding); + } + phase.withEnv(PLATFORM_API_VERSION_KEY, this.platformVersion.toString()); + return phase; + } + + private boolean isVerboseLogging() { + return this.request.isVerboseLogging() && this.lifecycleVersion.isEqualOrGreaterThan(LOGGING_MINIMUM_VERSION); + } + + private boolean requiresProcessTypeDefault() { + return this.platformVersion.supports(ApiVersion.of(0, 4)); + } + + private void run(Phase phase) throws IOException { + Consumer logConsumer = this.log.runningPhase(this.request, phase.getName()); + ContainerConfig containerConfig = ContainerConfig.of(this.builder.getName(), phase::apply); + ContainerReference reference = createContainer(containerConfig); + try { + this.docker.container().start(reference); + this.docker.container().logs(reference, logConsumer::accept); + ContainerStatus status = this.docker.container().wait(reference); + if (status.getStatusCode() != 0) { + throw new BuilderException(phase.getName(), status.getStatusCode()); + } + } + finally { + this.docker.container().remove(reference, true); + } + } + + private ContainerReference createContainer(ContainerConfig config) throws IOException { + if (this.applicationVolumePopulated) { + return this.docker.container().create(config); + } + try { + TarArchive applicationContent = this.request.getApplicationContent(this.builder.getBuildOwner()); + return this.docker.container().create(config, + ContainerContent.of(applicationContent, Directory.APPLICATION)); + } + finally { + this.applicationVolumePopulated = true; + } + } + + @Override + public void close() throws IOException { + deleteVolume(this.layersVolume); + deleteVolume(this.applicationVolume); + } + + private void deleteVolume(VolumeName name) throws IOException { + this.docker.volume().delete(name, true); + } + + /** + * Common directories used by the various phases. + */ + private static class Directory { + + /** + * The directory used by buildpacks to write their layer contributions. A new + * layer directory is created for each lifecycle execution. + *

    + * Maps to the {@code } concept in the + * buildpack + * specification and the {@code -layers} argument from the reference lifecycle + * implementation. + */ + static final String LAYERS = "/layers"; + + /** + * The directory containing the original contributed application. A new + * application directory is created for each lifecycle execution. + *

    + * Maps to the {@code } concept in the + * buildpack + * specification and the {@code -app} argument from the reference lifecycle + * implementation. The reference lifecycle follows the Kubernetes/Docker + * convention of using {@code '/workspace'}. + *

    + * Note that application content is uploaded to the container with the first phase + * that runs and saved in a volume that is passed to subsequent phases. The + * directory is mutable and buildpacks may modify the content. + */ + static final String APPLICATION = "/workspace"; + + /** + * The directory used by buildpacks to obtain environment variables and platform + * specific concerns. The platform directory is read-only and is created/populated + * by the {@link EphemeralBuilder}. + *

    + * Maps to the {@code /env} and {@code /#} concepts in the + * buildpack + * specification and the {@code -platform} argument from the reference + * lifecycle implementation. + */ + static final String PLATFORM = "/platform"; + + /** + * The directory used by buildpacks for caching. The volume name is based on the + * image {@link BuildRequest#getName() name} being built, and is persistent across + * invocations even if the application content has changed. + *

    + * Maps to the {@code -path} argument from the reference lifecycle implementation + * cache and restore phases + */ + static final String CACHE = "/cache"; + + /** + * The directory used by buildpacks for launch related caching. The volume name is + * based on the image {@link BuildRequest#getName() name} being built, and is + * persistent across invocations even if the application content has changed. + *

    + * Maps to the {@code -launch-cache} argument from the reference lifecycle + * implementation export phase + */ + static final String LAUNCH_CACHE = "/launch-cache"; + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/LifecycleVersion.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/LifecycleVersion.java new file mode 100644 index 000000000000..375879263fa3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/LifecycleVersion.java @@ -0,0 +1,140 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.util.Comparator; + +import org.springframework.util.Assert; + +/** + * A lifecycle version number comprised of a major, minor and patch value. + * + * @author Phillip Webb + */ +class LifecycleVersion implements Comparable { + + private static final Comparator COMPARATOR = Comparator.comparingInt(LifecycleVersion::getMajor) + .thenComparingInt(LifecycleVersion::getMinor).thenComparing(LifecycleVersion::getPatch); + + private final int major; + + private final int minor; + + private final int patch; + + LifecycleVersion(int major, int minor, int patch) { + this.major = major; + this.minor = minor; + this.patch = patch; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + LifecycleVersion other = (LifecycleVersion) obj; + boolean result = true; + result = result && this.major == other.major; + result = result && this.minor == other.minor; + result = result && this.patch == other.patch; + return result; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + this.major; + result = prime * result + this.minor; + result = prime * result + this.patch; + return result; + } + + @Override + public String toString() { + return "v" + this.major + "." + this.minor + "." + this.patch; + } + + /** + * Return if this version is greater than or equal to the specified version. + * @param other the version to compare + * @return {@code true} if this version is greater than or equal to the specified + * version + */ + boolean isEqualOrGreaterThan(LifecycleVersion other) { + return compareTo(other) >= 0; + } + + @Override + public int compareTo(LifecycleVersion other) { + return COMPARATOR.compare(this, other); + } + + /** + * Return the major version number. + * @return the major version + */ + int getMajor() { + return this.major; + } + + /** + * Return the minor version number. + * @return the minor version + */ + int getMinor() { + return this.minor; + } + + /** + * Return the patch version number. + * @return the patch version + */ + int getPatch() { + return this.patch; + } + + /** + * Factory method to parse a string into a {@link LifecycleVersion} instance. + * @param value the value to parse. + * @return the corresponding {@link LifecycleVersion} + * @throws IllegalArgumentException if the value could not be parsed + */ + static LifecycleVersion parse(String value) { + Assert.hasText(value, "Value must not be empty"); + if (value.startsWith("v") || value.startsWith("V")) { + value = value.substring(1); + } + String[] components = value.split("\\."); + Assert.isTrue(components.length <= 3, "Malformed version number '" + value + "'"); + int[] versions = new int[3]; + for (int i = 0; i < components.length; i++) { + try { + versions[i] = Integer.parseInt(components[i]); + } + catch (NumberFormatException ex) { + throw new IllegalArgumentException("Malformed version number '" + value + "'", ex); + } + } + return new LifecycleVersion(versions[0], versions[1], versions[2]); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Phase.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Phase.java new file mode 100644 index 000000000000..234592afd4a4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Phase.java @@ -0,0 +1,132 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.boot.buildpack.platform.docker.type.Binding; +import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig; +import org.springframework.util.StringUtils; + +/** + * An individual build phase executed as part of a {@link Lifecycle} run. + * + * @author Phillip Webb + * @author Scott Frederick + */ +class Phase { + + private static final String DOMAIN_SOCKET_PATH = "/var/run/docker.sock"; + + private final String name; + + private final boolean verboseLogging; + + private boolean daemonAccess = false; + + private final List args = new ArrayList<>(); + + private final List bindings = new ArrayList<>(); + + private final Map env = new LinkedHashMap<>(); + + /** + * Create a new {@link Phase} instance. + * @param name the name of the phase + * @param verboseLogging if verbose logging is requested + */ + Phase(String name, boolean verboseLogging) { + this.name = name; + this.verboseLogging = verboseLogging; + } + + /** + * Update this phase with Docker daemon access. + */ + void withDaemonAccess() { + this.daemonAccess = true; + } + + /** + * Update this phase with a debug log level arguments if verbose logging has been + * requested. + */ + void withLogLevelArg() { + if (this.verboseLogging) { + this.args.add("-log-level"); + this.args.add("debug"); + } + } + + /** + * Update this phase with additional run arguments. + * @param args the arguments to add + */ + void withArgs(Object... args) { + Arrays.stream(args).map(Object::toString).forEach(this.args::add); + } + + /** + * Update this phase with an addition volume binding. + * @param binding the binding + */ + void withBinding(Binding binding) { + this.bindings.add(binding); + } + + /** + * Update this phase with an additional environment variable. + * @param name the variable name + * @param value the variable value + */ + void withEnv(String name, String value) { + this.env.put(name, value); + } + + /** + * Return the name of the phase. + * @return the phase name + */ + String getName() { + return this.name; + } + + @Override + public String toString() { + return this.name; + } + + /** + * Apply this phase settings to a {@link ContainerConfig} update. + * @param update the update to apply the phase to + */ + void apply(ContainerConfig.Update update) { + if (this.daemonAccess) { + update.withUser("root"); + update.withBinding(Binding.from(DOMAIN_SOCKET_PATH, DOMAIN_SOCKET_PATH)); + } + update.withCommand("/cnb/lifecycle/" + this.name, StringUtils.toStringArray(this.args)); + update.withLabel("author", "spring-boot"); + this.bindings.forEach(update::withBinding); + this.env.forEach(update::withEnv); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/PrintStreamBuildLog.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/PrintStreamBuildLog.java new file mode 100644 index 000000000000..7fdfe6d60474 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/PrintStreamBuildLog.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.io.PrintStream; +import java.util.function.Consumer; + +import org.springframework.boot.buildpack.platform.docker.TotalProgressBar; +import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent; + +/** + * {@link BuildLog} implementation that prints output to a {@link PrintStream}. + * + * @author Phillip Webb + * @see BuildLog#to(PrintStream) + */ +class PrintStreamBuildLog extends AbstractBuildLog { + + private final PrintStream out; + + PrintStreamBuildLog(PrintStream out) { + this.out = out; + } + + @Override + protected void log(String message) { + this.out.println(message); + } + + @Override + protected Consumer getProgressConsumer(String prefix) { + return new TotalProgressBar(prefix, '.', false, this.out); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/PullPolicy.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/PullPolicy.java new file mode 100644 index 000000000000..4cbbac8694a8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/PullPolicy.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +/** + * Image pull policy. + * + * @author Andrey Shlykov + * @since 2.4.0 + */ +public enum PullPolicy { + + /** + * Always pull the image from the registry. + */ + ALWAYS, + + /** + * Never pull the image from the registry. + */ + NEVER, + + /** + * Pull the image from the registry only if it does not exist locally. + */ + IF_NOT_PRESENT + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/StackId.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/StackId.java new file mode 100644 index 000000000000..b3e344ee53a4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/StackId.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import org.springframework.boot.buildpack.platform.docker.type.Image; +import org.springframework.boot.buildpack.platform.docker.type.ImageConfig; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * A Stack ID. + * + * @author Phillip Webb + * @author Andy Wilkinson + */ +class StackId { + + private static final String LABEL_NAME = "io.buildpacks.stack.id"; + + private final String value; + + StackId(String value) { + this.value = value; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + return this.value.equals(((StackId) obj).value); + } + + @Override + public int hashCode() { + return this.value.hashCode(); + } + + @Override + public String toString() { + return this.value; + } + + /** + * Factory method to create a {@link StackId} from an {@link Image}. + * @param image the source image + * @return the extracted stack ID + */ + static StackId fromImage(Image image) { + Assert.notNull(image, "Image must not be null"); + return fromImageConfig(image.getConfig()); + } + + /** + * Factory method to create a {@link StackId} from an {@link ImageConfig}. + * @param imageConfig the source image config + * @return the extracted stack ID + */ + private static StackId fromImageConfig(ImageConfig imageConfig) { + String value = imageConfig.getLabels().get(LABEL_NAME); + Assert.state(StringUtils.hasText(value), () -> "Missing '" + LABEL_NAME + "' stack label"); + return new StackId(value); + } + + /** + * Factory method to create a {@link StackId} with a given value. + * @param value the stack ID value + * @return a new stack ID instance + */ + static StackId of(String value) { + Assert.hasText(value, "Value must not be empty"); + return new StackId(value); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/TarGzipBuildpack.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/TarGzipBuildpack.java new file mode 100644 index 000000000000..735c76619f6a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/TarGzipBuildpack.java @@ -0,0 +1,129 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.apache.commons.compress.archivers.ArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; + +import org.springframework.boot.buildpack.platform.docker.type.Layer; +import org.springframework.boot.buildpack.platform.io.IOConsumer; +import org.springframework.util.StreamUtils; + +/** + * A {@link Buildpack} that references a buildpack contained in a local gzipped tar + * archive file. + * + * The archive must contain a buildpack descriptor named {@code buildpack.toml} at the + * root of the archive. The contents of the archive will be provided as a single layer to + * be included in the builder image. + * + * @author Scott Frederick + */ +final class TarGzipBuildpack implements Buildpack { + + private final Path path; + + private final BuildpackCoordinates coordinates; + + private TarGzipBuildpack(Path path) { + this.path = path; + this.coordinates = findBuildpackCoordinates(path); + } + + private BuildpackCoordinates findBuildpackCoordinates(Path path) { + try { + try (TarArchiveInputStream tar = new TarArchiveInputStream( + new GzipCompressorInputStream(Files.newInputStream(path)))) { + ArchiveEntry entry = tar.getNextEntry(); + while (entry != null) { + if ("buildpack.toml".equals(entry.getName())) { + return BuildpackCoordinates.fromToml(tar, path); + } + entry = tar.getNextEntry(); + } + throw new IllegalArgumentException( + "Buildpack descriptor 'buildpack.toml' is required in buildpack '" + path + "'"); + } + } + catch (IOException ex) { + throw new RuntimeException("Error parsing descriptor for buildpack '" + path + "'", ex); + } + } + + @Override + public BuildpackCoordinates getCoordinates() { + return this.coordinates; + } + + @Override + public void apply(IOConsumer layers) throws IOException { + layers.accept(Layer.fromTarArchive(this::copyAndRebaseEntries)); + } + + private void copyAndRebaseEntries(OutputStream outputStream) throws IOException { + String id = this.coordinates.getSanitizedId(); + Path basePath = Paths.get("/cnb/buildpacks/", id, this.coordinates.getVersion()); + try (TarArchiveInputStream tar = new TarArchiveInputStream( + new GzipCompressorInputStream(Files.newInputStream(this.path))); + TarArchiveOutputStream output = new TarArchiveOutputStream(outputStream)) { + writeBasePathEntries(output, basePath); + TarArchiveEntry entry = tar.getNextTarEntry(); + while (entry != null) { + entry.setName(basePath + "/" + entry.getName()); + output.putArchiveEntry(entry); + StreamUtils.copy(tar, output); + output.closeArchiveEntry(); + entry = tar.getNextTarEntry(); + } + output.finish(); + } + } + + private void writeBasePathEntries(TarArchiveOutputStream output, Path basePath) throws IOException { + int pathCount = basePath.getNameCount(); + for (int pathIndex = 1; pathIndex < pathCount + 1; pathIndex++) { + String name = "/" + basePath.subpath(0, pathIndex) + "/"; + TarArchiveEntry entry = new TarArchiveEntry(name); + output.putArchiveEntry(entry); + output.closeArchiveEntry(); + } + } + + /** + * A {@link BuildpackResolver} compatible method to resolve tar-gzip buildpacks. + * @param context the resolver context + * @param reference the buildpack reference + * @return the resolved {@link Buildpack} or {@code null} + */ + static Buildpack resolve(BuildpackResolverContext context, BuildpackReference reference) { + Path path = reference.asPath(); + if (path != null && Files.exists(path) && Files.isRegularFile(path)) { + return new TarGzipBuildpack(path); + } + return null; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/package-info.java new file mode 100644 index 000000000000..77300b6d6750 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Central API for performing a buildpack build. + */ +package org.springframework.boot.buildpack.platform.build; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java new file mode 100644 index 000000000000..c07523d5440f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java @@ -0,0 +1,485 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.http.client.utils.URIBuilder; + +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration; +import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport; +import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport.Response; +import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig; +import org.springframework.boot.buildpack.platform.docker.type.ContainerContent; +import org.springframework.boot.buildpack.platform.docker.type.ContainerReference; +import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus; +import org.springframework.boot.buildpack.platform.docker.type.Image; +import org.springframework.boot.buildpack.platform.docker.type.ImageArchive; +import org.springframework.boot.buildpack.platform.docker.type.ImageReference; +import org.springframework.boot.buildpack.platform.docker.type.VolumeName; +import org.springframework.boot.buildpack.platform.io.IOBiConsumer; +import org.springframework.boot.buildpack.platform.io.TarArchive; +import org.springframework.boot.buildpack.platform.json.JsonStream; +import org.springframework.boot.buildpack.platform.json.SharedObjectMapper; +import org.springframework.util.Assert; +import org.springframework.util.StreamUtils; +import org.springframework.util.StringUtils; + +/** + * Provides access to the limited set of Docker APIs needed by pack. + * + * @author Phillip Webb + * @author Scott Frederick + * @since 2.3.0 + */ +public class DockerApi { + + private static final List FORCE_PARAMS = Collections.unmodifiableList(Arrays.asList("force", "1")); + + static final String API_VERSION = "v1.24"; + + private final HttpTransport http; + + private final JsonStream jsonStream; + + private final ImageApi image; + + private final ContainerApi container; + + private final VolumeApi volume; + + /** + * Create a new {@link DockerApi} instance. + */ + public DockerApi() { + this(new DockerConfiguration()); + } + + /** + * Create a new {@link DockerApi} instance. + * @param dockerConfiguration the docker configuration + * @since 2.4.0 + */ + public DockerApi(DockerConfiguration dockerConfiguration) { + this(HttpTransport.create((dockerConfiguration != null) ? dockerConfiguration.getHost() : null)); + } + + /** + * Create a new {@link DockerApi} instance backed by a specific {@link HttpTransport} + * implementation. + * @param http the http implementation + */ + DockerApi(HttpTransport http) { + this.http = http; + this.jsonStream = new JsonStream(SharedObjectMapper.get()); + this.image = new ImageApi(); + this.container = new ContainerApi(); + this.volume = new VolumeApi(); + } + + private HttpTransport http() { + return this.http; + } + + private JsonStream jsonStream() { + return this.jsonStream; + } + + private URI buildUrl(String path, Collection params) { + return buildUrl(path, StringUtils.toStringArray(params)); + } + + private URI buildUrl(String path, String... params) { + try { + URIBuilder builder = new URIBuilder("/" + API_VERSION + path); + int param = 0; + while (param < params.length) { + builder.addParameter(params[param++], params[param++]); + } + return builder.build(); + } + catch (URISyntaxException ex) { + throw new IllegalStateException(ex); + } + } + + /** + * Return the Docker API for image operations. + * @return the image API + */ + public ImageApi image() { + return this.image; + } + + /** + * Return the Docker API for container operations. + * @return the container API + */ + public ContainerApi container() { + return this.container; + } + + public VolumeApi volume() { + return this.volume; + } + + /** + * Docker API for image operations. + */ + public class ImageApi { + + ImageApi() { + } + + /** + * Pull an image from a registry. + * @param reference the image reference to pull + * @param listener a pull listener to receive update events + * @return the {@link ImageApi pulled image} instance + * @throws IOException on IO error + */ + public Image pull(ImageReference reference, UpdateListener listener) throws IOException { + return pull(reference, listener, null); + } + + /** + * Pull an image from a registry. + * @param reference the image reference to pull + * @param listener a pull listener to receive update events + * @param registryAuth registry authentication credentials + * @return the {@link ImageApi pulled image} instance + * @throws IOException on IO error + */ + public Image pull(ImageReference reference, UpdateListener listener, String registryAuth) + throws IOException { + Assert.notNull(reference, "Reference must not be null"); + Assert.notNull(listener, "Listener must not be null"); + URI createUri = buildUrl("/images/create", "fromImage", reference.toString()); + DigestCaptureUpdateListener digestCapture = new DigestCaptureUpdateListener(); + listener.onStart(); + try { + try (Response response = http().post(createUri, registryAuth)) { + jsonStream().get(response.getContent(), PullImageUpdateEvent.class, (event) -> { + digestCapture.onUpdate(event); + listener.onUpdate(event); + }); + } + return inspect(reference.withDigest(digestCapture.getCapturedDigest())); + } + finally { + listener.onFinish(); + } + } + + /** + * Push an image to a registry. + * @param reference the image reference to push + * @param listener a push listener to receive update events + * @param registryAuth registry authentication credentials + * @throws IOException on IO error + */ + public void push(ImageReference reference, UpdateListener listener, String registryAuth) + throws IOException { + Assert.notNull(reference, "Reference must not be null"); + Assert.notNull(listener, "Listener must not be null"); + URI pushUri = buildUrl("/images/" + reference + "/push"); + ErrorCaptureUpdateListener errorListener = new ErrorCaptureUpdateListener(); + listener.onStart(); + try { + try (Response response = http().post(pushUri, registryAuth)) { + jsonStream().get(response.getContent(), PushImageUpdateEvent.class, (event) -> { + errorListener.onUpdate(event); + listener.onUpdate(event); + }); + } + } + finally { + listener.onFinish(); + } + } + + /** + * Load an {@link ImageArchive} into Docker. + * @param archive the archive to load + * @param listener a pull listener to receive update events + * @throws IOException on IO error + */ + public void load(ImageArchive archive, UpdateListener listener) throws IOException { + Assert.notNull(archive, "Archive must not be null"); + Assert.notNull(listener, "Listener must not be null"); + URI loadUri = buildUrl("/images/load"); + StreamCaptureUpdateListener streamListener = new StreamCaptureUpdateListener(); + listener.onStart(); + try { + try (Response response = http().post(loadUri, "application/x-tar", archive::writeTo)) { + jsonStream().get(response.getContent(), LoadImageUpdateEvent.class, (event) -> { + streamListener.onUpdate(event); + listener.onUpdate(event); + }); + } + Assert.state(StringUtils.hasText(streamListener.getCapturedStream()), + "Invalid response received when loading image " + + ((archive.getTag() != null) ? "\"" + archive.getTag() + "\"" : "")); + } + finally { + listener.onFinish(); + } + } + + /** + * Export the layers of an image. + * @param reference the reference to export + * @param exports a consumer to receive the layers (contents can only be accessed + * during the callback) + * @throws IOException on IO error + */ + public void exportLayers(ImageReference reference, IOBiConsumer exports) + throws IOException { + Assert.notNull(reference, "Reference must not be null"); + Assert.notNull(exports, "Exports must not be null"); + URI saveUri = buildUrl("/images/" + reference + "/get"); + Response response = http().get(saveUri); + try (TarArchiveInputStream tar = new TarArchiveInputStream(response.getContent())) { + TarArchiveEntry entry = tar.getNextTarEntry(); + while (entry != null) { + if (entry.getName().endsWith("/layer.tar")) { + TarArchive archive = (out) -> StreamUtils.copy(tar, out); + exports.accept(entry.getName(), archive); + } + entry = tar.getNextTarEntry(); + } + } + } + + /** + * Remove a specific image. + * @param reference the reference the remove + * @param force if removal should be forced + * @throws IOException on IO error + */ + public void remove(ImageReference reference, boolean force) throws IOException { + Assert.notNull(reference, "Reference must not be null"); + Collection params = force ? FORCE_PARAMS : Collections.emptySet(); + URI uri = buildUrl("/images/" + reference, params); + http().delete(uri); + } + + /** + * Inspect an image. + * @param reference the image reference + * @return the image from the local repository + * @throws IOException on IO error + */ + public Image inspect(ImageReference reference) throws IOException { + Assert.notNull(reference, "Reference must not be null"); + URI imageUri = buildUrl("/images/" + reference + "/json"); + try (Response response = http().get(imageUri)) { + return Image.of(response.getContent()); + } + } + + } + + /** + * Docker API for container operations. + */ + public class ContainerApi { + + ContainerApi() { + } + + /** + * Create a new container a {@link ContainerConfig}. + * @param config the container config + * @param contents additional contents to include + * @return a {@link ContainerReference} for the newly created container + * @throws IOException on IO error + */ + public ContainerReference create(ContainerConfig config, ContainerContent... contents) throws IOException { + Assert.notNull(config, "Config must not be null"); + Assert.noNullElements(contents, "Contents must not contain null elements"); + ContainerReference containerReference = createContainer(config); + for (ContainerContent content : contents) { + uploadContainerContent(containerReference, content); + } + return containerReference; + } + + private ContainerReference createContainer(ContainerConfig config) throws IOException { + URI createUri = buildUrl("/containers/create"); + try (Response response = http().post(createUri, "application/json", config::writeTo)) { + return ContainerReference + .of(SharedObjectMapper.get().readTree(response.getContent()).at("/Id").asText()); + } + } + + private void uploadContainerContent(ContainerReference reference, ContainerContent content) throws IOException { + URI uri = buildUrl("/containers/" + reference + "/archive", "path", content.getDestinationPath()); + http().put(uri, "application/x-tar", content.getArchive()::writeTo).close(); + } + + /** + * Start a specific container. + * @param reference the container reference to start + * @throws IOException on IO error + */ + public void start(ContainerReference reference) throws IOException { + Assert.notNull(reference, "Reference must not be null"); + URI uri = buildUrl("/containers/" + reference + "/start"); + http().post(uri); + } + + /** + * Return and follow logs for a specific container. + * @param reference the container reference + * @param listener a listener to receive log update events + * @throws IOException on IO error + */ + public void logs(ContainerReference reference, UpdateListener listener) throws IOException { + Assert.notNull(reference, "Reference must not be null"); + Assert.notNull(listener, "Listener must not be null"); + String[] params = { "stdout", "1", "stderr", "1", "follow", "1" }; + URI uri = buildUrl("/containers/" + reference + "/logs", params); + listener.onStart(); + try { + try (Response response = http().get(uri)) { + LogUpdateEvent.readAll(response.getContent(), listener::onUpdate); + } + } + finally { + listener.onFinish(); + } + } + + /** + * Wait for a container to stop and retrieve the status. + * @param reference the container reference + * @return a {@link ContainerStatus} indicating the exit status of the container + * @throws IOException on IO error + */ + public ContainerStatus wait(ContainerReference reference) throws IOException { + Assert.notNull(reference, "Reference must not be null"); + URI uri = buildUrl("/containers/" + reference + "/wait"); + Response response = http().post(uri); + return ContainerStatus.of(response.getContent()); + } + + /** + * Remove a specific container. + * @param reference the container to remove + * @param force if removal should be forced + * @throws IOException on IO error + */ + public void remove(ContainerReference reference, boolean force) throws IOException { + Assert.notNull(reference, "Reference must not be null"); + Collection params = force ? FORCE_PARAMS : Collections.emptySet(); + URI uri = buildUrl("/containers/" + reference, params); + http().delete(uri); + } + + } + + /** + * Docker API for volume operations. + */ + public class VolumeApi { + + VolumeApi() { + } + + /** + * Delete a volume. + * @param name the name of the volume to delete + * @param force if the deletion should be forced + * @throws IOException on IO error + */ + public void delete(VolumeName name, boolean force) throws IOException { + Assert.notNull(name, "Name must not be null"); + Collection params = force ? FORCE_PARAMS : Collections.emptySet(); + URI uri = buildUrl("/volumes/" + name, params); + http().delete(uri); + } + + } + + /** + * {@link UpdateListener} used to capture the image digest. + */ + private static class DigestCaptureUpdateListener implements UpdateListener { + + private static final String PREFIX = "Digest:"; + + private String digest; + + @Override + public void onUpdate(ProgressUpdateEvent event) { + String status = event.getStatus(); + if (status != null && status.startsWith(PREFIX)) { + String digest = status.substring(PREFIX.length()).trim(); + Assert.state(this.digest == null || this.digest.equals(digest), "Different digests IDs provided"); + this.digest = digest; + } + } + + String getCapturedDigest() { + Assert.hasText(this.digest, "No digest found"); + return this.digest; + } + + } + + /** + * {@link UpdateListener} used to ensure an image load response stream. + */ + private static class StreamCaptureUpdateListener implements UpdateListener { + + private String stream; + + @Override + public void onUpdate(LoadImageUpdateEvent event) { + this.stream = event.getStream(); + } + + String getCapturedStream() { + return this.stream; + } + + } + + /** + * {@link UpdateListener} used to capture the details of an error in a response + * stream. + */ + private static class ErrorCaptureUpdateListener implements UpdateListener { + + @Override + public void onUpdate(PushImageUpdateEvent event) { + Assert.state(event.getErrorDetail() == null, + () -> "Error response received when pushing image: " + event.getErrorDetail().getMessage()); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ImageProgressUpdateEvent.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ImageProgressUpdateEvent.java new file mode 100644 index 000000000000..ba2878ab3e69 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ImageProgressUpdateEvent.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker; + +/** + * A {@link ProgressUpdateEvent} fired for image events. + * + * @author Phillip Webb + * @author Scott Frederick + * @since 2.4.0 + */ +public class ImageProgressUpdateEvent extends ProgressUpdateEvent { + + private final String id; + + protected ImageProgressUpdateEvent(String id, String status, ProgressDetail progressDetail, String progress) { + super(status, progressDetail, progress); + this.id = id; + } + + /** + * Returns the ID of the image layer being updated if available. + * @return the ID of the updated layer or {@code null} + */ + public String getId() { + return this.id; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/LoadImageUpdateEvent.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/LoadImageUpdateEvent.java new file mode 100644 index 000000000000..2fb0f5ad58c9 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/LoadImageUpdateEvent.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker; + +import com.fasterxml.jackson.annotation.JsonCreator; + +/** + * A {@link ProgressUpdateEvent} fired as an image is loaded. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public class LoadImageUpdateEvent extends ProgressUpdateEvent { + + private final String stream; + + @JsonCreator + public LoadImageUpdateEvent(String stream, String status, ProgressDetail progressDetail, String progress) { + super(status, progressDetail, progress); + this.stream = stream; + } + + /** + * Return the stream response or {@code null} if no response is available. + * @return the stream response. + */ + public String getStream() { + return this.stream; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/LogUpdateEvent.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/LogUpdateEvent.java new file mode 100644 index 000000000000..740b159a774f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/LogUpdateEvent.java @@ -0,0 +1,153 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.function.Consumer; +import java.util.regex.Pattern; + +import org.springframework.util.Assert; +import org.springframework.util.StreamUtils; + +/** + * An update event used to provide log updates. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public class LogUpdateEvent extends UpdateEvent { + + private static final Pattern ANSI_PATTERN = Pattern.compile("\u001B\\[[;\\d]*m"); + + private static final Pattern TRAILING_NEW_LINE_PATTERN = Pattern.compile("\\n$"); + + private final StreamType streamType; + + private final byte[] payload; + + private final String string; + + LogUpdateEvent(StreamType streamType, byte[] payload) { + this.streamType = streamType; + this.payload = payload; + String string = new String(payload, StandardCharsets.UTF_8); + string = ANSI_PATTERN.matcher(string).replaceAll(""); + string = TRAILING_NEW_LINE_PATTERN.matcher(string).replaceAll(""); + this.string = string; + } + + public void print() { + switch (this.streamType) { + case STD_OUT: + System.out.println(this); + return; + case STD_ERR: + System.err.println(this); + return; + } + } + + public StreamType getStreamType() { + return this.streamType; + } + + public byte[] getPayload() { + return this.payload; + } + + @Override + public String toString() { + return this.string; + } + + static void readAll(InputStream inputStream, Consumer consumer) throws IOException { + try { + LogUpdateEvent event; + while ((event = LogUpdateEvent.read(inputStream)) != null) { + consumer.accept(event); + } + } + catch (IllegalStateException ex) { + byte[] message = ex.getMessage().getBytes(StandardCharsets.UTF_8); + consumer.accept(new LogUpdateEvent(StreamType.STD_ERR, message)); + StreamUtils.drain(inputStream); + } + finally { + inputStream.close(); + } + } + + private static LogUpdateEvent read(InputStream inputStream) throws IOException { + byte[] header = read(inputStream, 8); + if (header == null) { + return null; + } + StreamType streamType = StreamType.forId(header[0]); + long size = 0; + for (int i = 0; i < 4; i++) { + size = (size << 8) + (header[i + 4] & 0xff); + } + byte[] payload = read(inputStream, size); + return new LogUpdateEvent(streamType, payload); + } + + private static byte[] read(InputStream inputStream, long size) throws IOException { + byte[] data = new byte[(int) size]; + int offset = 0; + do { + int amountRead = inputStream.read(data, offset, data.length - offset); + if (amountRead == -1) { + return null; + } + offset += amountRead; + } + while (offset < data.length); + return data; + } + + /** + * Stream types supported by the event. + */ + public enum StreamType { + + /** + * Input from {@code stdin}. + */ + STD_IN, + + /** + * Output to {@code stdout}. + */ + STD_OUT, + + /** + * Output to {@code stderr}. + */ + STD_ERR; + + static StreamType forId(byte id) { + int upperBound = values().length; + Assert.state(id > 0 && id < upperBound, + () -> "Stream type is out of bounds. Must be >= 0 and < " + upperBound + ", but was " + id); + return values()[id]; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ProgressUpdateEvent.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ProgressUpdateEvent.java new file mode 100644 index 000000000000..271b6c19a2bd --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ProgressUpdateEvent.java @@ -0,0 +1,102 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker; + +import com.fasterxml.jackson.annotation.JsonCreator; + +/** + * An {@link UpdateEvent} that includes progress information. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public abstract class ProgressUpdateEvent extends UpdateEvent { + + private final String status; + + private final ProgressDetail progressDetail; + + private final String progress; + + protected ProgressUpdateEvent(String status, ProgressDetail progressDetail, String progress) { + this.status = status; + this.progressDetail = (ProgressDetail.isEmpty(progressDetail)) ? null : progressDetail; + this.progress = progress; + } + + /** + * Return the status for the update. For example, "Extracting" or "Downloading". + * @return the status of the update. + */ + public String getStatus() { + return this.status; + } + + /** + * Return progress details if available. + * @return progress details or {@code null} + */ + public ProgressDetail getProgressDetail() { + return this.progressDetail; + } + + /** + * Return a text based progress bar if progress information is available. + * @return the progress bar or {@code null} + */ + public String getProgress() { + return this.progress; + } + + /** + * Provide details about the progress of a task. + */ + public static class ProgressDetail { + + private final Integer current; + + private final Integer total; + + @JsonCreator + public ProgressDetail(Integer current, Integer total) { + this.current = current; + this.total = total; + } + + /** + * Return the current progress value. + * @return the current progress + */ + public int getCurrent() { + return this.current; + } + + /** + * Return the total progress possible value. + * @return the total progress possible + */ + public int getTotal() { + return this.total; + } + + public static boolean isEmpty(ProgressDetail progressDetail) { + return progressDetail == null || progressDetail.current == null || progressDetail.total == null; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/PullImageUpdateEvent.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/PullImageUpdateEvent.java new file mode 100644 index 000000000000..73152e3a0874 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/PullImageUpdateEvent.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker; + +import com.fasterxml.jackson.annotation.JsonCreator; + +/** + * A {@link ProgressUpdateEvent} fired as an image is pulled. + * + * @author Phillip Webb + * @author Scott Frederick + * @since 2.3.0 + */ +public class PullImageUpdateEvent extends ImageProgressUpdateEvent { + + @JsonCreator + public PullImageUpdateEvent(String id, String status, ProgressDetail progressDetail, String progress) { + super(id, status, progressDetail, progress); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/PushImageUpdateEvent.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/PushImageUpdateEvent.java new file mode 100644 index 000000000000..2ebfa6638bc9 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/PushImageUpdateEvent.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * A {@link ProgressUpdateEvent} fired as an image is pushed to a registry. + * + * @author Scott Frederick + * @since 2.4.0 + */ +public class PushImageUpdateEvent extends ImageProgressUpdateEvent { + + private final ErrorDetail errorDetail; + + @JsonCreator + public PushImageUpdateEvent(String id, String status, ProgressDetail progressDetail, String progress, + ErrorDetail errorDetail) { + super(id, status, progressDetail, progress); + this.errorDetail = errorDetail; + } + + /** + * Returns the details of any error encountered during processing. + * @return the error + */ + public ErrorDetail getErrorDetail() { + return this.errorDetail; + } + + /** + * Details of an error embedded in a response stream. + */ + public static class ErrorDetail { + + private final String message; + + @JsonCreator + public ErrorDetail(@JsonProperty("message") String message) { + this.message = message; + } + + /** + * Returns the message field from the error detail. + * @return the message + */ + public String getMessage() { + return this.message; + } + + @Override + public String toString() { + return this.message; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/TotalProgressBar.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/TotalProgressBar.java new file mode 100644 index 000000000000..b478efd33d4e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/TotalProgressBar.java @@ -0,0 +1,88 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker; + +import java.io.PrintStream; +import java.util.function.Consumer; + +/** + * Utility to render a simple progress bar based on consumed {@link TotalProgressEvent} + * objects. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public class TotalProgressBar implements Consumer { + + private final char progressChar; + + private final boolean bookend; + + private final PrintStream out; + + private int printed; + + /** + * Create a new {@link TotalProgressBar} instance. + * @param prefix the prefix to output + */ + public TotalProgressBar(String prefix) { + this(prefix, System.out); + } + + /** + * Create a new {@link TotalProgressBar} instance. + * @param prefix the prefix to output + * @param out the output print stream to use + */ + public TotalProgressBar(String prefix, PrintStream out) { + this(prefix, '#', true, out); + } + + /** + * Create a new {@link TotalProgressBar} instance. + * @param prefix the prefix to output + * @param progressChar the progress char to print + * @param bookend if bookends should be printed + * @param out the output print stream to use + */ + public TotalProgressBar(String prefix, char progressChar, boolean bookend, PrintStream out) { + this.progressChar = progressChar; + this.bookend = bookend; + if (prefix != null && !prefix.isEmpty()) { + out.print(prefix); + out.print(" "); + } + if (bookend) { + out.print("[ "); + } + this.out = out; + } + + @Override + public void accept(TotalProgressEvent event) { + int percent = event.getPercent() / 2; + while (this.printed < percent) { + this.out.print(this.progressChar); + this.printed++; + } + if (event.getPercent() == 100) { + this.out.println(this.bookend ? " ]" : ""); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/TotalProgressEvent.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/TotalProgressEvent.java new file mode 100644 index 000000000000..e14b5d661db2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/TotalProgressEvent.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker; + +import org.springframework.util.Assert; + +/** + * Event published by the {@link TotalProgressPullListener} showing the total progress of + * an operation. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public class TotalProgressEvent { + + private final int percent; + + /** + * Create a new {@link TotalProgressEvent} with a specific percent value. + * @param percent the progress as a percentage + */ + public TotalProgressEvent(int percent) { + Assert.isTrue(percent >= 0 && percent <= 100, "Percent must be in the range 0 to 100"); + this.percent = percent; + } + + /** + * Return the total progress. + * @return the total progress + */ + public int getPercent() { + return this.percent; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/TotalProgressListener.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/TotalProgressListener.java new file mode 100644 index 000000000000..fa397c611f57 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/TotalProgressListener.java @@ -0,0 +1,133 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; + +import org.springframework.boot.buildpack.platform.docker.ProgressUpdateEvent.ProgressDetail; + +/** + * {@link UpdateListener} that calculates the total progress of the entire image operation + * and publishes {@link TotalProgressEvent}. + * + * @param the type of {@link ImageProgressUpdateEvent} + * @author Phillip Webb + * @author Scott Frederick + * @since 2.4.0 + */ +public abstract class TotalProgressListener implements UpdateListener { + + private final Map layers = new ConcurrentHashMap<>(); + + private final Consumer consumer; + + private final String[] trackedStatusKeys; + + private boolean progressStarted; + + /** + * Create a new {@link TotalProgressListener} that sends {@link TotalProgressEvent + * events} to the given consumer. + * @param consumer the consumer that receives {@link TotalProgressEvent progress + * events} + * @param trackedStatusKeys a list of status event keys to track the progress of + */ + protected TotalProgressListener(Consumer consumer, String[] trackedStatusKeys) { + this.consumer = consumer; + this.trackedStatusKeys = trackedStatusKeys; + } + + @Override + public void onStart() { + } + + @Override + public void onUpdate(E event) { + if (event.getId() != null) { + this.layers.computeIfAbsent(event.getId(), (value) -> new Layer(this.trackedStatusKeys)).update(event); + } + this.progressStarted = this.progressStarted || event.getProgress() != null; + if (this.progressStarted) { + publish(0); + } + } + + @Override + public void onFinish() { + this.layers.values().forEach(Layer::finish); + publish(100); + } + + private void publish(int fallback) { + int count = 0; + int total = 0; + for (Layer layer : this.layers.values()) { + count++; + total += layer.getProgress(); + } + TotalProgressEvent event = new TotalProgressEvent( + (count != 0) ? withinPercentageBounds(total / count) : fallback); + this.consumer.accept(event); + } + + private static int withinPercentageBounds(int value) { + if (value < 0) { + return 0; + } + return Math.min(value, 100); + } + + /** + * Progress for an individual layer. + */ + private static class Layer { + + private final Map progressByStatus = new HashMap<>(); + + Layer(String[] trackedStatusKeys) { + Arrays.stream(trackedStatusKeys).forEach((status) -> this.progressByStatus.put(status, 0)); + } + + void update(ImageProgressUpdateEvent event) { + String status = event.getStatus(); + if (event.getProgressDetail() != null && this.progressByStatus.containsKey(status)) { + int current = this.progressByStatus.get(status); + this.progressByStatus.put(status, updateProgress(current, event.getProgressDetail())); + } + } + + private int updateProgress(int current, ProgressDetail detail) { + int result = withinPercentageBounds((int) ((100.0 / detail.getTotal()) * detail.getCurrent())); + return Math.max(result, current); + } + + void finish() { + this.progressByStatus.keySet().forEach((key) -> this.progressByStatus.put(key, 100)); + } + + int getProgress() { + return withinPercentageBounds((this.progressByStatus.values().stream().mapToInt(Integer::valueOf).sum()) + / this.progressByStatus.size()); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/TotalProgressPullListener.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/TotalProgressPullListener.java new file mode 100644 index 000000000000..dec52093ebd2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/TotalProgressPullListener.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker; + +import java.util.function.Consumer; + +/** + * {@link UpdateListener} that calculates the total progress of the entire pull operation + * and publishes {@link TotalProgressEvent}. + * + * @author Phillip Webb + * @author Scott Frederick + * @since 2.3.0 + */ +public class TotalProgressPullListener extends TotalProgressListener { + + private static final String[] TRACKED_STATUS_KEYS = { "Downloading", "Extracting" }; + + /** + * Create a new {@link TotalProgressPullListener} that prints a progress bar to + * {@link System#out}. + * @param prefix the prefix to output + */ + public TotalProgressPullListener(String prefix) { + this(new TotalProgressBar(prefix)); + } + + /** + * Create a new {@link TotalProgressPullListener} that sends {@link TotalProgressEvent + * events} to the given consumer. + * @param consumer the consumer that receives {@link TotalProgressEvent progress + * events} + */ + public TotalProgressPullListener(Consumer consumer) { + super(consumer, TRACKED_STATUS_KEYS); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/TotalProgressPushListener.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/TotalProgressPushListener.java new file mode 100644 index 000000000000..ff5516d77642 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/TotalProgressPushListener.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker; + +import java.util.function.Consumer; + +/** + * {@link UpdateListener} that calculates the total progress of the entire push operation + * and publishes {@link TotalProgressEvent}. + * + * @author Scott Frederick + * @since 2.4.0 + */ +public class TotalProgressPushListener extends TotalProgressListener { + + private static final String[] TRACKED_STATUS_KEYS = { "Pushing" }; + + /** + * Create a new {@link TotalProgressPushListener} that prints a progress bar to + * {@link System#out}. + * @param prefix the prefix to output + */ + public TotalProgressPushListener(String prefix) { + this(new TotalProgressBar(prefix)); + } + + /** + * Create a new {@link TotalProgressPushListener} that sends {@link TotalProgressEvent + * events} to the given consumer. + * @param consumer the consumer that receives {@link TotalProgressEvent progress + * events} + */ + public TotalProgressPushListener(Consumer consumer) { + super(consumer, TRACKED_STATUS_KEYS); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/UpdateEvent.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/UpdateEvent.java new file mode 100644 index 000000000000..2e62b52e491c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/UpdateEvent.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker; + +/** + * Base class for update events published by Docker. + * + * @author Phillip Webb + * @since 2.3.0 + * @see UpdateListener + */ +public abstract class UpdateEvent { + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/UpdateListener.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/UpdateListener.java new file mode 100644 index 000000000000..0fdf3980fa59 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/UpdateListener.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker; + +/** + * Listener for update events published from the {@link DockerApi}. + * + * @param the update event type + * @author Phillip Webb + * @since 2.3.0 + */ +@FunctionalInterface +public interface UpdateListener { + + /** + * A no-op update listener. + * @see #none() + */ + UpdateListener NONE = (event) -> { + }; + + /** + * Called when the operation starts. + */ + default void onStart() { + } + + /** + * Called when an update event is available. + * @param event the update event + */ + void onUpdate(E event); + + /** + * Called when the operation finishes (with or without error). + */ + default void onFinish() { + } + + /** + * A no-op update listener that does nothing. + * @param the event type + * @return a no-op update listener + */ + @SuppressWarnings("unchecked") + static UpdateListener none() { + return (UpdateListener) NONE; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfiguration.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfiguration.java new file mode 100644 index 000000000000..68135780922d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfiguration.java @@ -0,0 +1,93 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.configuration; + +import org.springframework.util.Assert; + +/** + * Docker configuration options. + * + * @author Wei Jiang + * @author Scott Frederick + * @since 2.4.0 + */ +public final class DockerConfiguration { + + private final DockerHost host; + + private final DockerRegistryAuthentication builderAuthentication; + + private final DockerRegistryAuthentication publishAuthentication; + + public DockerConfiguration() { + this(null, null, null); + } + + private DockerConfiguration(DockerHost host, DockerRegistryAuthentication builderAuthentication, + DockerRegistryAuthentication publishAuthentication) { + this.host = host; + this.builderAuthentication = builderAuthentication; + this.publishAuthentication = publishAuthentication; + } + + public DockerHost getHost() { + return this.host; + } + + public DockerRegistryAuthentication getBuilderRegistryAuthentication() { + return this.builderAuthentication; + } + + public DockerRegistryAuthentication getPublishRegistryAuthentication() { + return this.publishAuthentication; + } + + public DockerConfiguration withHost(String address, boolean secure, String certificatePath) { + Assert.notNull(address, "Address must not be null"); + return new DockerConfiguration(new DockerHost(address, secure, certificatePath), this.builderAuthentication, + this.publishAuthentication); + } + + public DockerConfiguration withBuilderRegistryTokenAuthentication(String token) { + Assert.notNull(token, "Token must not be null"); + return new DockerConfiguration(this.host, new DockerRegistryTokenAuthentication(token), + this.publishAuthentication); + } + + public DockerConfiguration withBuilderRegistryUserAuthentication(String username, String password, String url, + String email) { + Assert.notNull(username, "Username must not be null"); + Assert.notNull(password, "Password must not be null"); + return new DockerConfiguration(this.host, new DockerRegistryUserAuthentication(username, password, url, email), + this.publishAuthentication); + } + + public DockerConfiguration withPublishRegistryTokenAuthentication(String token) { + Assert.notNull(token, "Token must not be null"); + return new DockerConfiguration(this.host, this.builderAuthentication, + new DockerRegistryTokenAuthentication(token)); + } + + public DockerConfiguration withPublishRegistryUserAuthentication(String username, String password, String url, + String email) { + Assert.notNull(username, "Username must not be null"); + Assert.notNull(password, "Password must not be null"); + return new DockerConfiguration(this.host, this.builderAuthentication, + new DockerRegistryUserAuthentication(username, password, url, email)); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerHost.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerHost.java new file mode 100644 index 000000000000..024b587f29fe --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerHost.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.configuration; + +/** + * Docker host connection options. + * + * @author Scott Frederick + * @since 2.4.0 + */ +public class DockerHost { + + private final String address; + + private final boolean secure; + + private final String certificatePath; + + public DockerHost(String address, boolean secure, String certificatePath) { + this.address = address; + this.secure = secure; + this.certificatePath = certificatePath; + } + + public String getAddress() { + return this.address; + } + + public boolean isSecure() { + return this.secure; + } + + public String getCertificatePath() { + return this.certificatePath; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryAuthentication.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryAuthentication.java new file mode 100644 index 000000000000..3df4b4fadcbd --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryAuthentication.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.configuration; + +/** + * Docker registry authentication configuration. + * + * @author Scott Frederick + * @since 2.4.0 + */ +public interface DockerRegistryAuthentication { + + /** + * Returns the auth header that should be used for docker authentication. + * @return the auth header + */ + String getAuthHeader(); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryTokenAuthentication.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryTokenAuthentication.java new file mode 100644 index 000000000000..d0923c22cd19 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryTokenAuthentication.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.configuration; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Docker registry authentication configuration using a token. + * + * @author Scott Frederick + */ +class DockerRegistryTokenAuthentication extends JsonEncodedDockerRegistryAuthentication { + + @JsonProperty("identitytoken") + private final String token; + + DockerRegistryTokenAuthentication(String token) { + this.token = token; + createAuthHeader(); + } + + String getToken() { + return this.token; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryUserAuthentication.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryUserAuthentication.java new file mode 100644 index 000000000000..c5a068e73015 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryUserAuthentication.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.configuration; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Docker registry authentication configuration using user credentials. + * + * @author Scott Frederick + */ +class DockerRegistryUserAuthentication extends JsonEncodedDockerRegistryAuthentication { + + @JsonProperty + private final String username; + + @JsonProperty + private final String password; + + @JsonProperty("serveraddress") + private final String url; + + @JsonProperty + private final String email; + + DockerRegistryUserAuthentication(String username, String password, String url, String email) { + this.username = username; + this.password = password; + this.url = url; + this.email = email; + createAuthHeader(); + } + + String getUsername() { + return this.username; + } + + String getPassword() { + return this.password; + } + + String getUrl() { + return this.url; + } + + String getEmail() { + return this.email; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/JsonEncodedDockerRegistryAuthentication.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/JsonEncodedDockerRegistryAuthentication.java new file mode 100644 index 000000000000..9710ff90d92f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/JsonEncodedDockerRegistryAuthentication.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.configuration; + +import com.fasterxml.jackson.core.JsonProcessingException; + +import org.springframework.boot.buildpack.platform.json.SharedObjectMapper; +import org.springframework.util.Base64Utils; + +/** + * {@link DockerRegistryAuthentication} that uses a Base64 encoded auth header value based + * on the JSON created from the instance. + * + * @author Scott Frederick + */ +class JsonEncodedDockerRegistryAuthentication implements DockerRegistryAuthentication { + + private String authHeader; + + @Override + public String getAuthHeader() { + return this.authHeader; + } + + protected void createAuthHeader() { + try { + this.authHeader = Base64Utils.encodeToUrlSafeString(SharedObjectMapper.get().writeValueAsBytes(this)); + } + catch (JsonProcessingException ex) { + throw new IllegalStateException("Error creating Docker registry authentication header", ex); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/package-info.java new file mode 100644 index 000000000000..d5c3fe8a3c62 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Docker configuration options. + */ +package org.springframework.boot.buildpack.platform.docker.configuration; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/package-info.java new file mode 100644 index 000000000000..c6b88a0c72aa --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * A limited Docker API providing the operations needed by pack. + */ +package org.springframework.boot.buildpack.platform.docker; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/CertificateParser.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/CertificateParser.java new file mode 100644 index 000000000000..307d722fa350 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/CertificateParser.java @@ -0,0 +1,105 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.ssl; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.util.Base64Utils; + +/** + * Parser for X.509 certificates in PEM format. + * + * @author Scott Frederick + * @author Phillip Webb + */ +final class CertificateParser { + + private static final String HEADER = "-+BEGIN\\s+.*CERTIFICATE[^-]*-+(?:\\s|\\r|\\n)+"; + + private static final String BASE64_TEXT = "([a-z0-9+/=\\r\\n]+)"; + + private static final String FOOTER = "-+END\\s+.*CERTIFICATE[^-]*-+"; + + private static final Pattern PATTERN = Pattern.compile(HEADER + BASE64_TEXT + FOOTER, Pattern.CASE_INSENSITIVE); + + private CertificateParser() { + } + + /** + * Load certificates from the specified file paths. + * @param paths one or more paths to certificate files + * @return certificates parsed from specified file paths + */ + static X509Certificate[] parse(Path... paths) { + CertificateFactory factory = getCertificateFactory(); + List certificates = new ArrayList<>(); + for (Path path : paths) { + readCertificates(path, factory, certificates::add); + } + return certificates.toArray(new X509Certificate[0]); + } + + private static CertificateFactory getCertificateFactory() { + try { + return CertificateFactory.getInstance("X.509"); + } + catch (CertificateException ex) { + throw new IllegalStateException("Unable to get X.509 certificate factory", ex); + } + } + + private static void readCertificates(Path path, CertificateFactory factory, Consumer consumer) { + try { + String text = readText(path); + Matcher matcher = PATTERN.matcher(text); + while (matcher.find()) { + String encodedText = matcher.group(1); + byte[] decodedBytes = decodeBase64(encodedText); + ByteArrayInputStream inputStream = new ByteArrayInputStream(decodedBytes); + while (inputStream.available() > 0) { + consumer.accept((X509Certificate) factory.generateCertificate(inputStream)); + } + } + } + catch (CertificateException | IOException ex) { + throw new IllegalStateException("Error reading certificate from '" + path + "' : " + ex.getMessage(), ex); + } + } + + private static String readText(Path path) throws IOException { + byte[] bytes = Files.readAllBytes(path); + return new String(bytes, StandardCharsets.UTF_8); + } + + private static byte[] decodeBase64(String content) { + byte[] bytes = content.replaceAll("\r", "").replaceAll("\n", "").getBytes(); + return Base64Utils.decode(bytes); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/KeyStoreFactory.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/KeyStoreFactory.java new file mode 100644 index 000000000000..f54ac5cbfe99 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/KeyStoreFactory.java @@ -0,0 +1,94 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.ssl; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/** + * Utility methods for creating Java trust material from key and certificate files. + * + * @author Scott Frederick + */ +final class KeyStoreFactory { + + private static final char[] NO_PASSWORD = {}; + + private KeyStoreFactory() { + } + + /** + * Create a new {@link KeyStore} populated with the certificate stored at the + * specified file path and an optional private key. + * @param certPath the path to the certificate authority file + * @param keyPath the path to the private file + * @param alias the alias to use for KeyStore entries + * @return the {@code KeyStore} + */ + static KeyStore create(Path certPath, Path keyPath, String alias) { + try { + KeyStore keyStore = getKeyStore(); + X509Certificate[] certificates = CertificateParser.parse(certPath); + PrivateKey privateKey = getPrivateKey(keyPath); + try { + addCertificates(keyStore, certificates, privateKey, alias); + } + catch (KeyStoreException ex) { + throw new IllegalStateException("Error adding certificates to KeyStore: " + ex.getMessage(), ex); + } + return keyStore; + } + catch (GeneralSecurityException | IOException ex) { + throw new IllegalStateException("Error creating KeyStore: " + ex.getMessage(), ex); + } + } + + private static KeyStore getKeyStore() + throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException { + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null); + return keyStore; + } + + private static PrivateKey getPrivateKey(Path path) { + if (path != null && Files.exists(path)) { + return PrivateKeyParser.parse(path); + } + return null; + } + + private static void addCertificates(KeyStore keyStore, X509Certificate[] certificates, PrivateKey privateKey, + String alias) throws KeyStoreException { + if (privateKey != null) { + keyStore.setKeyEntry(alias, privateKey, NO_PASSWORD, certificates); + } + else { + for (int index = 0; index < certificates.length; index++) { + keyStore.setCertificateEntry(alias + "-" + index, certificates[index]); + } + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/PrivateKeyParser.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/PrivateKeyParser.java new file mode 100644 index 000000000000..63e870b28df2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/PrivateKeyParser.java @@ -0,0 +1,145 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.ssl; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.util.Base64Utils; + +/** + * Parser for PKCS private key files in PEM format. + * + * @author Scott Frederick + * @author Phillip Webb + */ +final class PrivateKeyParser { + + private static final String PKCS1_HEADER = "-+BEGIN\\s+RSA\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+"; + + private static final String PKCS1_FOOTER = "-+END\\s+RSA\\s+PRIVATE\\s+KEY[^-]*-+"; + + private static final String PKCS8_FOOTER = "-+END\\s+PRIVATE\\s+KEY[^-]*-+"; + + private static final String PKCS8_HEADER = "-+BEGIN\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+"; + + private static final String BASE64_TEXT = "([a-z0-9+/=\\r\\n]+)"; + + private static final Pattern PKCS1_PATTERN = Pattern.compile(PKCS1_HEADER + BASE64_TEXT + PKCS1_FOOTER, + Pattern.CASE_INSENSITIVE); + + private static final Pattern PKCS8_KEY_PATTERN = Pattern.compile(PKCS8_HEADER + BASE64_TEXT + PKCS8_FOOTER, + Pattern.CASE_INSENSITIVE); + + private PrivateKeyParser() { + } + + /** + * Load a private key from the specified file paths. + * @param path the path to the private key file + * @return private key from specified file path + */ + static PrivateKey parse(Path path) { + try { + String text = readText(path); + Matcher matcher = PKCS1_PATTERN.matcher(text); + if (matcher.find()) { + return parsePkcs1(decodeBase64(matcher.group(1))); + } + matcher = PKCS8_KEY_PATTERN.matcher(text); + if (matcher.find()) { + return parsePkcs8(decodeBase64(matcher.group(1))); + } + throw new IllegalStateException("Unrecognized private key format in " + path); + } + catch (GeneralSecurityException | IOException ex) { + throw new IllegalStateException("Error loading private key file " + path, ex); + } + } + + private static PrivateKey parsePkcs1(byte[] privateKeyBytes) throws GeneralSecurityException { + byte[] pkcs8Bytes = convertPkcs1ToPkcs8(privateKeyBytes); + return parsePkcs8(pkcs8Bytes); + } + + private static byte[] convertPkcs1ToPkcs8(byte[] pkcs1) { + try { + ByteArrayOutputStream result = new ByteArrayOutputStream(); + int pkcs1Length = pkcs1.length; + int totalLength = pkcs1Length + 22; + // Sequence + total length + result.write(bytes(0x30, 0x82)); + result.write((totalLength >> 8) & 0xff); + result.write(totalLength & 0xff); + // Integer (0) + result.write(bytes(0x02, 0x01, 0x00)); + // Sequence: 1.2.840.113549.1.1.1, NULL + result.write( + bytes(0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00)); + // Octet string + length + result.write(bytes(0x04, 0x82)); + result.write((pkcs1Length >> 8) & 0xff); + result.write(pkcs1Length & 0xff); + // PKCS1 + result.write(pkcs1); + return result.toByteArray(); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + private static byte[] bytes(int... elements) { + byte[] result = new byte[elements.length]; + for (int i = 0; i < elements.length; i++) { + result[i] = (byte) elements[i]; + } + return result; + } + + private static PrivateKey parsePkcs8(byte[] privateKeyBytes) throws GeneralSecurityException { + try { + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + return keyFactory.generatePrivate(keySpec); + } + catch (InvalidKeySpecException ex) { + throw new IllegalArgumentException("Unexpected key format", ex); + } + } + + private static String readText(Path path) throws IOException { + byte[] bytes = Files.readAllBytes(path); + return new String(bytes, StandardCharsets.UTF_8); + } + + private static byte[] decodeBase64(String content) { + byte[] contentBytes = content.replaceAll("\r", "").replaceAll("\n", "").getBytes(); + return Base64Utils.decode(contentBytes); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/SslContextFactory.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/SslContextFactory.java new file mode 100644 index 000000000000..1c8349887eb7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/SslContextFactory.java @@ -0,0 +1,97 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.ssl; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; + +import org.springframework.util.Assert; + +/** + * Builds an {@link SSLContext} for use with an HTTP connection. + * + * @author Scott Frederick + * @author Phillip Webb + * @since 2.3.0 + */ +public class SslContextFactory { + + private static final char[] NO_PASSWORD = {}; + + private static final String KEY_STORE_ALIAS = "spring-boot-docker"; + + public SslContextFactory() { + } + + /** + * Create an {@link SSLContext} from files in the specified directory. The directory + * must contain files with the names 'key.pem', 'cert.pem', and 'ca.pem'. + * @param directory the path to a directory containing certificate and key files + * @return the {@code SSLContext} + */ + public SSLContext forDirectory(String directory) { + try { + Path keyPath = Paths.get(directory, "key.pem"); + Path certPath = Paths.get(directory, "cert.pem"); + Path caPath = Paths.get(directory, "ca.pem"); + Path caKeyPath = Paths.get(directory, "ca-key.pem"); + verifyCertificateFiles(keyPath, certPath, caPath); + KeyManagerFactory keyManagerFactory = getKeyManagerFactory(keyPath, certPath); + TrustManagerFactory trustManagerFactory = getTrustManagerFactory(caPath, caKeyPath); + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null); + return sslContext; + } + catch (RuntimeException ex) { + throw ex; + } + catch (Exception ex) { + throw new RuntimeException(ex.getMessage(), ex); + } + } + + private KeyManagerFactory getKeyManagerFactory(Path keyPath, Path certPath) throws Exception { + KeyStore store = KeyStoreFactory.create(certPath, keyPath, KEY_STORE_ALIAS); + KeyManagerFactory factory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + factory.init(store, NO_PASSWORD); + return factory; + } + + private TrustManagerFactory getTrustManagerFactory(Path caPath, Path caKeyPath) + throws NoSuchAlgorithmException, KeyStoreException { + KeyStore store = KeyStoreFactory.create(caPath, caKeyPath, KEY_STORE_ALIAS); + TrustManagerFactory factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + factory.init(store); + return factory; + } + + private static void verifyCertificateFiles(Path... paths) { + for (Path path : paths) { + Assert.state(Files.exists(path) && Files.isRegularFile(path), + "Certificate path must contain the files 'ca.pem', 'cert.pem', and 'key.pem' files"); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/package-info.java new file mode 100644 index 000000000000..eb6019b92341 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Utilities and classes for managing SSL context and keys. + */ +package org.springframework.boot.buildpack.platform.docker.ssl; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/DockerConnectionException.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/DockerConnectionException.java new file mode 100644 index 000000000000..ec22850faa17 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/DockerConnectionException.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.transport; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Exception thrown when connection to the Docker daemon fails. + * + * @author Scott Frederick + * @since 2.3.0 + */ +public class DockerConnectionException extends RuntimeException { + + private static final String JNA_EXCEPTION_CLASS_NAME = "com.sun.jna.LastErrorException"; + + public DockerConnectionException(String host, Exception cause) { + super(buildMessage(host, cause), cause); + } + + private static String buildMessage(String host, Exception cause) { + Assert.notNull(host, "Host must not be null"); + Assert.notNull(cause, "Cause must not be null"); + StringBuilder message = new StringBuilder("Connection to the Docker daemon at '" + host + "' failed"); + String causeMessage = getCauseMessage(cause); + if (StringUtils.hasText(causeMessage)) { + message.append(" with error \"").append(causeMessage).append("\""); + } + message.append("; ensure the Docker daemon is running and accessible"); + return message.toString(); + } + + private static String getCauseMessage(Exception cause) { + if (cause.getCause() != null && cause.getCause().getClass().getName().equals(JNA_EXCEPTION_CLASS_NAME)) { + return cause.getCause().getMessage(); + } + return cause.getMessage(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/DockerEngineException.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/DockerEngineException.java new file mode 100644 index 000000000000..e521f4a8afa4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/DockerEngineException.java @@ -0,0 +1,102 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.transport; + +import java.net.URI; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Exception thrown when a call to the Docker API fails. + * + * @author Phillip Webb + * @author Scott Frederick + * @since 2.3.0 + */ +public class DockerEngineException extends RuntimeException { + + private final int statusCode; + + private final String reasonPhrase; + + private final Errors errors; + + private final Message responseMessage; + + public DockerEngineException(String host, URI uri, int statusCode, String reasonPhrase, Errors errors, + Message responseMessage) { + super(buildMessage(host, uri, statusCode, reasonPhrase, errors, responseMessage)); + this.statusCode = statusCode; + this.reasonPhrase = reasonPhrase; + this.errors = errors; + this.responseMessage = responseMessage; + } + + /** + * Return the status code returned by the Docker API. + * @return the statusCode the status code + */ + public int getStatusCode() { + return this.statusCode; + } + + /** + * Return the reason phrase returned by the Docker API. + * @return the reasonPhrase + */ + public String getReasonPhrase() { + return this.reasonPhrase; + } + + /** + * Return the errors from the body of the Docker API response, or {@code null} if the + * errors JSON could not be read. + * @return the errors or {@code null} + */ + public Errors getErrors() { + return this.errors; + } + + /** + * Return the message from the body of the Docker API response, or {@code null} if the + * message JSON could not be read. + * @return the message or {@code null} + */ + public Message getResponseMessage() { + return this.responseMessage; + } + + private static String buildMessage(String host, URI uri, int statusCode, String reasonPhrase, Errors errors, + Message responseMessage) { + Assert.notNull(host, "Host must not be null"); + Assert.notNull(uri, "URI must not be null"); + StringBuilder message = new StringBuilder( + "Docker API call to '" + host + uri + "' failed with status code " + statusCode); + if (StringUtils.hasLength(reasonPhrase)) { + message.append(" \"").append(reasonPhrase).append("\""); + } + if (responseMessage != null && StringUtils.hasLength(responseMessage.getMessage())) { + message.append(" and message \"").append(responseMessage.getMessage()).append("\""); + } + if (errors != null && !errors.isEmpty()) { + message.append(" ").append(errors); + } + return message.toString(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/Errors.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/Errors.java new file mode 100644 index 000000000000..4aec87719cb9 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/Errors.java @@ -0,0 +1,106 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.transport; + +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Stream; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Errors returned from the Docker API. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public class Errors implements Iterable { + + private final List errors; + + @JsonCreator + Errors(@JsonProperty("errors") List errors) { + this.errors = (errors != null) ? errors : Collections.emptyList(); + } + + @Override + public Iterator iterator() { + return this.errors.iterator(); + } + + /** + * Returns a sequential {@code Stream} of the errors. + * @return a stream of the errors + */ + public Stream stream() { + return this.errors.stream(); + } + + /** + * Return if there are any contained errors. + * @return if the errors are empty + */ + public boolean isEmpty() { + return this.errors.isEmpty(); + } + + @Override + public String toString() { + return this.errors.toString(); + } + + /** + * An individual Docker error. + */ + public static class Error { + + private final String code; + + private final String message; + + @JsonCreator + Error(String code, String message) { + this.code = code; + this.message = message; + } + + /** + * Return the error code. + * @return the error code + */ + public String getCode() { + return this.code; + } + + /** + * Return the error message. + * @return the error message + */ + public String getMessage() { + return this.message; + } + + @Override + public String toString() { + return this.code + ": " + this.message; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java new file mode 100644 index 000000000000..f2d079420281 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java @@ -0,0 +1,264 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.transport; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; +import org.apache.http.StatusLine; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.entity.AbstractHttpEntity; +import org.apache.http.impl.client.CloseableHttpClient; + +import org.springframework.boot.buildpack.platform.io.Content; +import org.springframework.boot.buildpack.platform.io.IOConsumer; +import org.springframework.boot.buildpack.platform.json.SharedObjectMapper; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Abstract base class for {@link HttpTransport} implementations backed by a + * {@link HttpClient}. + * + * @author Phillip Webb + * @author Mike Smithson + * @author Scott Frederick + */ +abstract class HttpClientTransport implements HttpTransport { + + static final String REGISTRY_AUTH_HEADER = "X-Registry-Auth"; + + private final CloseableHttpClient client; + + private final HttpHost host; + + protected HttpClientTransport(CloseableHttpClient client, HttpHost host) { + Assert.notNull(client, "Client must not be null"); + Assert.notNull(host, "Host must not be null"); + this.client = client; + this.host = host; + } + + /** + * Perform a HTTP GET operation. + * @param uri the destination URI + * @return the operation response + */ + @Override + public Response get(URI uri) { + return execute(new HttpGet(uri)); + } + + /** + * Perform a HTTP POST operation. + * @param uri the destination URI + * @return the operation response + */ + @Override + public Response post(URI uri) { + return execute(new HttpPost(uri)); + } + + /** + * Perform a HTTP POST operation. + * @param uri the destination URI + * @param registryAuth registry authentication credentials + * @return the operation response + */ + @Override + public Response post(URI uri, String registryAuth) { + return execute(new HttpPost(uri), registryAuth); + } + + /** + * Perform a HTTP POST operation. + * @param uri the destination URI + * @param contentType the content type to write + * @param writer a content writer + * @return the operation response + */ + @Override + public Response post(URI uri, String contentType, IOConsumer writer) { + return execute(new HttpPost(uri), contentType, writer); + } + + /** + * Perform a HTTP PUT operation. + * @param uri the destination URI + * @param contentType the content type to write + * @param writer a content writer + * @return the operation response + */ + @Override + public Response put(URI uri, String contentType, IOConsumer writer) { + return execute(new HttpPut(uri), contentType, writer); + } + + /** + * Perform a HTTP DELETE operation. + * @param uri the destination URI + * @return the operation response + */ + @Override + public Response delete(URI uri) { + return execute(new HttpDelete(uri)); + } + + private Response execute(HttpEntityEnclosingRequestBase request, String contentType, + IOConsumer writer) { + request.setEntity(new WritableHttpEntity(contentType, writer)); + return execute(request); + } + + private Response execute(HttpEntityEnclosingRequestBase request, String registryAuth) { + if (StringUtils.hasText(registryAuth)) { + request.setHeader(REGISTRY_AUTH_HEADER, registryAuth); + } + return execute(request); + } + + private Response execute(HttpUriRequest request) { + try { + CloseableHttpResponse response = this.client.execute(this.host, request); + StatusLine statusLine = response.getStatusLine(); + int statusCode = statusLine.getStatusCode(); + HttpEntity entity = response.getEntity(); + if (statusCode >= 400 && statusCode <= 500) { + Errors errors = (statusCode != 500) ? getErrorsFromResponse(entity) : null; + Message message = getMessageFromResponse(entity); + throw new DockerEngineException(this.host.toHostString(), request.getURI(), statusCode, + statusLine.getReasonPhrase(), errors, message); + } + return new HttpClientResponse(response); + } + catch (IOException ex) { + throw new DockerConnectionException(this.host.toHostString(), ex); + } + } + + private Errors getErrorsFromResponse(HttpEntity entity) { + try { + return SharedObjectMapper.get().readValue(entity.getContent(), Errors.class); + } + catch (IOException ex) { + return null; + } + } + + private Message getMessageFromResponse(HttpEntity entity) { + try { + return (entity.getContent() != null) + ? SharedObjectMapper.get().readValue(entity.getContent(), Message.class) : null; + } + catch (IOException ex) { + return null; + } + } + + HttpHost getHost() { + return this.host; + } + + /** + * {@link HttpEntity} to send {@link Content} content. + */ + private static class WritableHttpEntity extends AbstractHttpEntity { + + private final IOConsumer writer; + + WritableHttpEntity(String contentType, IOConsumer writer) { + setContentType(contentType); + this.writer = writer; + } + + @Override + public boolean isRepeatable() { + return false; + } + + @Override + public long getContentLength() { + if (this.contentType != null && this.contentType.getValue().equals("application/json")) { + return calculateStringContentLength(); + } + return -1; + } + + @Override + public InputStream getContent() throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + @Override + public void writeTo(OutputStream outputStream) throws IOException { + this.writer.accept(outputStream); + } + + @Override + public boolean isStreaming() { + return true; + } + + private int calculateStringContentLength() { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + this.writer.accept(bytes); + return bytes.toByteArray().length; + } + catch (IOException ex) { + return -1; + } + } + + } + + /** + * An HTTP operation response. + */ + private static class HttpClientResponse implements Response { + + private final CloseableHttpResponse response; + + HttpClientResponse(CloseableHttpResponse response) { + this.response = response; + } + + @Override + public InputStream getContent() throws IOException { + return this.response.getEntity().getContent(); + } + + @Override + public void close() throws IOException { + this.response.close(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpTransport.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpTransport.java new file mode 100644 index 000000000000..3814b93ed47e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpTransport.java @@ -0,0 +1,147 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.transport; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; + +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerHost; +import org.springframework.boot.buildpack.platform.io.IOConsumer; +import org.springframework.boot.buildpack.platform.system.Environment; + +/** + * HTTP transport used for docker access. + * + * @author Phillip Webb + * @author Scott Frederick + * @since 2.3.0 + */ +public interface HttpTransport { + + /** + * Perform a HTTP GET operation. + * @param uri the destination URI (excluding any host/port) + * @return the operation response + * @throws IOException on IO error + */ + Response get(URI uri) throws IOException; + + /** + * Perform a HTTP POST operation. + * @param uri the destination URI (excluding any host/port) + * @return the operation response + * @throws IOException on IO error + */ + Response post(URI uri) throws IOException; + + /** + * Perform a HTTP POST operation. + * @param uri the destination URI (excluding any host/port) + * @param registryAuth registry authentication credentials + * @return the operation response + * @throws IOException on IO error + */ + Response post(URI uri, String registryAuth) throws IOException; + + /** + * Perform a HTTP POST operation. + * @param uri the destination URI (excluding any host/port) + * @param contentType the content type to write + * @param writer a content writer + * @return the operation response + * @throws IOException on IO error + */ + Response post(URI uri, String contentType, IOConsumer writer) throws IOException; + + /** + * Perform a HTTP PUT operation. + * @param uri the destination URI (excluding any host/port) + * @param contentType the content type to write + * @param writer a content writer + * @return the operation response + * @throws IOException on IO error + */ + Response put(URI uri, String contentType, IOConsumer writer) throws IOException; + + /** + * Perform a HTTP DELETE operation. + * @param uri the destination URI (excluding any host/port) + * @return the operation response + * @throws IOException on IO error + */ + Response delete(URI uri) throws IOException; + + /** + * Create the most suitable {@link HttpTransport} based on the + * {@link Environment#SYSTEM system environment}. + * @return a {@link HttpTransport} instance + */ + static HttpTransport create() { + return create(Environment.SYSTEM); + } + + /** + * Create the most suitable {@link HttpTransport} based on the + * {@link Environment#SYSTEM system environment}. + * @param dockerHost the Docker engine host configuration + * @return a {@link HttpTransport} instance + */ + static HttpTransport create(DockerHost dockerHost) { + return create(Environment.SYSTEM, dockerHost); + } + + /** + * Create the most suitable {@link HttpTransport} based on the given + * {@link Environment}. + * @param environment the source environment + * @return a {@link HttpTransport} instance + */ + static HttpTransport create(Environment environment) { + return create(environment, null); + } + + /** + * Create the most suitable {@link HttpTransport} based on the given + * {@link Environment} and {@link DockerConfiguration}. + * @param environment the source environment + * @param dockerHost the Docker engine host configuration + * @return a {@link HttpTransport} instance + */ + static HttpTransport create(Environment environment, DockerHost dockerHost) { + HttpTransport remote = RemoteHttpClientTransport.createIfPossible(environment, dockerHost); + return (remote != null) ? remote : LocalHttpClientTransport.create(environment); + } + + /** + * An HTTP operation response. + */ + interface Response extends Closeable { + + /** + * Return the content of the response. + * @return the response content + * @throws IOException on IO error + */ + InputStream getContent() throws IOException; + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/LocalHttpClientTransport.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/LocalHttpClientTransport.java new file mode 100644 index 000000000000..9861f4d45a6c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/LocalHttpClientTransport.java @@ -0,0 +1,160 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.transport; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.UnknownHostException; + +import com.sun.jna.Platform; +import org.apache.http.HttpHost; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.DnsResolver; +import org.apache.http.conn.HttpClientConnectionManager; +import org.apache.http.conn.SchemePortResolver; +import org.apache.http.conn.UnsupportedSchemeException; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.BasicHttpClientConnectionManager; +import org.apache.http.protocol.HttpContext; +import org.apache.http.util.Args; + +import org.springframework.boot.buildpack.platform.socket.DomainSocket; +import org.springframework.boot.buildpack.platform.socket.NamedPipeSocket; +import org.springframework.boot.buildpack.platform.system.Environment; + +/** + * {@link HttpClientTransport} that talks to local Docker. + * + * @author Phillip Webb + * @author Scott Frederick + */ +final class LocalHttpClientTransport extends HttpClientTransport { + + private static final String UNIX_SOCKET_PREFIX = "unix://"; + + private static final String DOCKER_HOST = "DOCKER_HOST"; + + private static final HttpHost LOCAL_DOCKER_HOST = HttpHost.create("docker://localhost"); + + private LocalHttpClientTransport(CloseableHttpClient client) { + super(client, LOCAL_DOCKER_HOST); + } + + static LocalHttpClientTransport create(Environment environment) { + HttpClientBuilder builder = HttpClients.custom(); + builder.setConnectionManager(new LocalConnectionManager(socketFilePath(environment))); + builder.setSchemePortResolver(new LocalSchemePortResolver()); + return new LocalHttpClientTransport(builder.build()); + } + + private static String socketFilePath(Environment environment) { + String host = environment.get(DOCKER_HOST); + if (host != null && host.startsWith(UNIX_SOCKET_PREFIX)) { + return host.substring(UNIX_SOCKET_PREFIX.length()); + } + return host; + } + + /** + * {@link HttpClientConnectionManager} for local Docker. + */ + private static class LocalConnectionManager extends BasicHttpClientConnectionManager { + + LocalConnectionManager(String host) { + super(getRegistry(host), null, null, new LocalDnsResolver()); + } + + private static Registry getRegistry(String host) { + RegistryBuilder builder = RegistryBuilder.create(); + builder.register("docker", new LocalConnectionSocketFactory(host)); + return builder.build(); + } + + } + + /** + * {@link DnsResolver} that ensures only the loopback address is used. + */ + private static class LocalDnsResolver implements DnsResolver { + + private static final InetAddress[] LOOPBACK = new InetAddress[] { InetAddress.getLoopbackAddress() }; + + @Override + public InetAddress[] resolve(String host) throws UnknownHostException { + return LOOPBACK; + } + + } + + /** + * {@link ConnectionSocketFactory} that connects to the local Docker domain socket or + * named pipe. + */ + private static class LocalConnectionSocketFactory implements ConnectionSocketFactory { + + private static final String DOMAIN_SOCKET_PATH = "/var/run/docker.sock"; + + private static final String WINDOWS_NAMED_PIPE_PATH = "//./pipe/docker_engine"; + + private final String host; + + LocalConnectionSocketFactory(String host) { + this.host = host; + } + + @Override + public Socket createSocket(HttpContext context) throws IOException { + if (Platform.isWindows()) { + return NamedPipeSocket.get((this.host != null) ? this.host : WINDOWS_NAMED_PIPE_PATH); + } + return DomainSocket.get((this.host != null) ? this.host : DOMAIN_SOCKET_PATH); + } + + @Override + public Socket connectSocket(int connectTimeout, Socket sock, HttpHost host, InetSocketAddress remoteAddress, + InetSocketAddress localAddress, HttpContext context) throws IOException { + return sock; + } + + } + + /** + * {@link SchemePortResolver} for local Docker. + */ + private static class LocalSchemePortResolver implements SchemePortResolver { + + private static final int DEFAULT_DOCKER_PORT = 2376; + + @Override + public int resolve(HttpHost host) throws UnsupportedSchemeException { + Args.notNull(host, "HTTP host"); + String name = host.getSchemeName(); + if ("docker".equals(name)) { + return DEFAULT_DOCKER_PORT; + } + throw new UnsupportedSchemeException(name + " protocol is not supported"); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/Message.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/Message.java new file mode 100644 index 000000000000..e1f51a162880 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/Message.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.transport; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * A message returned from the Docker API. + * + * @author Scott Frederick + * @since 2.3.1 + */ +public class Message { + + private final String message; + + @JsonCreator + Message(@JsonProperty("message") String message) { + this.message = message; + } + + /** + * Return the message contained in the response. + * @return the message + */ + public String getMessage() { + return this.message; + } + + @Override + public String toString() { + return this.message; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/RemoteHttpClientTransport.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/RemoteHttpClientTransport.java new file mode 100644 index 000000000000..1655cc3f3418 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/RemoteHttpClientTransport.java @@ -0,0 +1,124 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.transport; + +import java.nio.file.Files; +import java.nio.file.Paths; + +import javax.net.ssl.SSLContext; + +import org.apache.http.HttpHost; +import org.apache.http.conn.socket.LayeredConnectionSocketFactory; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; + +import org.springframework.boot.buildpack.platform.docker.configuration.DockerHost; +import org.springframework.boot.buildpack.platform.docker.ssl.SslContextFactory; +import org.springframework.boot.buildpack.platform.system.Environment; +import org.springframework.util.Assert; + +/** + * {@link HttpClientTransport} that talks to a remote Docker. + * + * @author Scott Frederick + * @author Phillip Webb + */ +final class RemoteHttpClientTransport extends HttpClientTransport { + + private static final String UNIX_SOCKET_PREFIX = "unix://"; + + private static final String DOCKER_HOST = "DOCKER_HOST"; + + private static final String DOCKER_TLS_VERIFY = "DOCKER_TLS_VERIFY"; + + private static final String DOCKER_CERT_PATH = "DOCKER_CERT_PATH"; + + private RemoteHttpClientTransport(CloseableHttpClient client, HttpHost host) { + super(client, host); + } + + static RemoteHttpClientTransport createIfPossible(Environment environment, DockerHost dockerHost) { + return createIfPossible(environment, dockerHost, new SslContextFactory()); + } + + static RemoteHttpClientTransport createIfPossible(Environment environment, DockerHost dockerHost, + SslContextFactory sslContextFactory) { + DockerHost host = getHost(environment, dockerHost); + if (host == null || host.getAddress() == null || isLocalFileReference(host.getAddress())) { + return null; + } + return create(host, sslContextFactory, HttpHost.create(host.getAddress())); + } + + private static boolean isLocalFileReference(String host) { + String filePath = host.startsWith(UNIX_SOCKET_PREFIX) ? host.substring(UNIX_SOCKET_PREFIX.length()) : host; + try { + return Files.exists(Paths.get(filePath)); + } + catch (Exception ex) { + return false; + } + } + + private static RemoteHttpClientTransport create(DockerHost host, SslContextFactory sslContextFactory, + HttpHost tcpHost) { + HttpClientBuilder builder = HttpClients.custom(); + if (host.isSecure()) { + builder.setSSLSocketFactory(getSecureConnectionSocketFactory(host, sslContextFactory)); + } + String scheme = host.isSecure() ? "https" : "http"; + HttpHost httpHost = new HttpHost(tcpHost.getHostName(), tcpHost.getPort(), scheme); + return new RemoteHttpClientTransport(builder.build(), httpHost); + } + + private static LayeredConnectionSocketFactory getSecureConnectionSocketFactory(DockerHost host, + SslContextFactory sslContextFactory) { + String directory = host.getCertificatePath(); + Assert.hasText(directory, + () -> "Docker host TLS verification requires trust material location to be specified with certificate path"); + SSLContext sslContext = sslContextFactory.forDirectory(directory); + return new SSLConnectionSocketFactory(sslContext); + } + + private static DockerHost getHost(Environment environment, DockerHost dockerHost) { + if (environment.get(DOCKER_HOST) != null) { + return new EnvironmentDockerHost(environment); + } + return dockerHost; + } + + private static class EnvironmentDockerHost extends DockerHost { + + EnvironmentDockerHost(Environment environment) { + super(environment.get(DOCKER_HOST), isTrue(environment.get(DOCKER_TLS_VERIFY)), + environment.get(DOCKER_CERT_PATH)); + } + + private static boolean isTrue(String value) { + try { + return (value != null) && (Integer.parseInt(value) == 1); + } + catch (NumberFormatException ex) { + return false; + } + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/package-info.java new file mode 100644 index 000000000000..60217d2eafc1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Docker transport classes providing HTTP operations on a local or remote engine. + */ +package org.springframework.boot.buildpack.platform.docker.transport; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Binding.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Binding.java new file mode 100644 index 000000000000..1c5e9647ba36 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Binding.java @@ -0,0 +1,93 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import java.util.Objects; + +import org.springframework.util.Assert; + +/** + * Volume bindings to apply when creating a container. + * + * @author Scott Frederick + * @since 2.5.0 + */ +public final class Binding { + + private final String value; + + private Binding(String value) { + this.value = value; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Binding)) { + return false; + } + Binding binding = (Binding) obj; + return Objects.equals(this.value, binding.value); + } + + @Override + public int hashCode() { + return Objects.hash(this.value); + } + + @Override + public String toString() { + return this.value; + } + + /** + * Create a {@link Binding} with the specified value containing a host source, + * container destination, and options. + * @param value the volume binding value + * @return a new {@link Binding} instance + */ + public static Binding of(String value) { + Assert.notNull(value, "Value must not be null"); + return new Binding(value); + } + + /** + * Create a {@link Binding} from the specified source and destination. + * @param sourceVolume the volume binding host source + * @param destination the volume binding container destination + * @return a new {@link Binding} instance + */ + public static Binding from(VolumeName sourceVolume, String destination) { + Assert.notNull(sourceVolume, "SourceVolume must not be null"); + return from(sourceVolume.toString(), destination); + } + + /** + * Create a {@link Binding} from the specified source and destination. + * @param source the volume binding host source + * @param destination the volume binding container destination + * @return a new {@link Binding} instance + */ + public static Binding from(String source, String destination) { + Assert.notNull(source, "Source must not be null"); + Assert.notNull(destination, "Destination must not be null"); + return new Binding(source + ":" + destination); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ContainerConfig.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ContainerConfig.java new file mode 100644 index 000000000000..da945f6848ba --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ContainerConfig.java @@ -0,0 +1,187 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import org.springframework.boot.buildpack.platform.json.SharedObjectMapper; +import org.springframework.util.Assert; +import org.springframework.util.StreamUtils; +import org.springframework.util.StringUtils; + +/** + * Configuration used when creating a new container. + * + * @author Phillip Webb + * @author Scott Frederick + * @since 2.3.0 + */ +public class ContainerConfig { + + private final String json; + + ContainerConfig(String user, ImageReference image, String command, List args, Map labels, + List bindings, Map env) throws IOException { + Assert.notNull(image, "Image must not be null"); + Assert.hasText(command, "Command must not be empty"); + ObjectMapper objectMapper = SharedObjectMapper.get(); + ObjectNode node = objectMapper.createObjectNode(); + if (StringUtils.hasText(user)) { + node.put("User", user); + } + node.put("Image", image.toString()); + ArrayNode commandNode = node.putArray("Cmd"); + commandNode.add(command); + args.forEach(commandNode::add); + ArrayNode envNode = node.putArray("Env"); + env.forEach((name, value) -> envNode.add(name + "=" + value)); + ObjectNode labelsNode = node.putObject("Labels"); + labels.forEach(labelsNode::put); + ObjectNode hostConfigNode = node.putObject("HostConfig"); + ArrayNode bindsNode = hostConfigNode.putArray("Binds"); + bindings.forEach((binding) -> bindsNode.add(binding.toString())); + this.json = objectMapper.writeValueAsString(node); + } + + /** + * Write this container configuration to the specified {@link OutputStream}. + * @param outputStream the output stream + * @throws IOException on IO error + */ + public void writeTo(OutputStream outputStream) throws IOException { + StreamUtils.copy(this.json, StandardCharsets.UTF_8, outputStream); + } + + @Override + public String toString() { + return this.json; + } + + /** + * Factory method to create a {@link ContainerConfig} with specific settings. + * @param imageReference the source image for the container config + * @param update an update callback used to customize the config + * @return a new {@link ContainerConfig} instance + */ + public static ContainerConfig of(ImageReference imageReference, Consumer update) { + Assert.notNull(imageReference, "ImageReference must not be null"); + Assert.notNull(update, "Update must not be null"); + return new Update(imageReference).run(update); + } + + /** + * Update class used to change data when creating a container config. + */ + public static class Update { + + private final ImageReference image; + + private String user; + + private String command; + + private final List args = new ArrayList<>(); + + private final Map labels = new LinkedHashMap<>(); + + private final List bindings = new ArrayList<>(); + + private final Map env = new LinkedHashMap<>(); + + Update(ImageReference image) { + this.image = image; + } + + private ContainerConfig run(Consumer update) { + update.accept(this); + try { + return new ContainerConfig(this.user, this.image, this.command, this.args, this.labels, this.bindings, + this.env); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + /** + * Update the container config with a specific user. + * @param user the user to set + */ + public void withUser(String user) { + this.user = user; + } + + /** + * Update the container config with a specific command. + * @param command the command to set + * @param args additional arguments to add + * @see #withArgs(String...) + */ + public void withCommand(String command, String... args) { + this.command = command; + withArgs(args); + } + + /** + * Update the container config with additional args. + * @param args the arguments to add + */ + public void withArgs(String... args) { + this.args.addAll(Arrays.asList(args)); + } + + /** + * Update the container config with an additional label. + * @param name the label name + * @param value the label value + */ + public void withLabel(String name, String value) { + this.labels.put(name, value); + } + + /** + * Update the container config with an additional binding. + * @param binding the binding + */ + public void withBinding(Binding binding) { + this.bindings.add(binding); + } + + /** + * Update the container config with an additional environment variable. + * @param name the variable name + * @param value the variable value + */ + public void withEnv(String name, String value) { + this.env.put(name, value); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ContainerContent.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ContainerContent.java new file mode 100644 index 000000000000..c0f5dd217d49 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ContainerContent.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import org.springframework.boot.buildpack.platform.io.TarArchive; +import org.springframework.util.Assert; + +/** + * Additional content that can be written to a created container. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public interface ContainerContent { + + /** + * Return the actual content to be added. + * @return the content + */ + TarArchive getArchive(); + + /** + * Return the destination path where the content should be added. + * @return the destination path + */ + String getDestinationPath(); + + /** + * Factory method to create a new {@link ContainerContent} instance written to the + * root of the container. + * @param archive the archive to add + * @return a new {@link ContainerContent} instance + */ + static ContainerContent of(TarArchive archive) { + return of(archive, "/"); + } + + /** + * Factory method to create a new {@link ContainerContent} instance. + * @param archive the archive to add + * @param destinationPath the destination path within the container + * @return a new {@link ContainerContent} instance + */ + static ContainerContent of(TarArchive archive, String destinationPath) { + Assert.notNull(archive, "Archive must not be null"); + Assert.hasText(destinationPath, "DestinationPath must not be empty"); + return new ContainerContent() { + + @Override + public TarArchive getArchive() { + return archive; + } + + @Override + public String getDestinationPath() { + return destinationPath; + } + + }; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ContainerReference.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ContainerReference.java new file mode 100644 index 000000000000..f5a5c6ac5a39 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ContainerReference.java @@ -0,0 +1,67 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import org.springframework.util.Assert; + +/** + * A reference to a Docker container. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public final class ContainerReference { + + private final String value; + + private ContainerReference(String value) { + Assert.hasText(value, "Value must not be empty"); + this.value = value; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ContainerReference other = (ContainerReference) obj; + return this.value.equals(other.value); + } + + @Override + public int hashCode() { + return this.value.hashCode(); + } + + @Override + public String toString() { + return this.value; + } + + /** + * Factory method to create a {@link ContainerReference} with a specific value. + * @param value the container reference value + * @return a new container reference instance + */ + public static ContainerReference of(String value) { + return new ContainerReference(value); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ContainerStatus.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ContainerStatus.java new file mode 100644 index 000000000000..b80f31474064 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ContainerStatus.java @@ -0,0 +1,88 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.invoke.MethodHandles; + +import com.fasterxml.jackson.databind.JsonNode; + +import org.springframework.boot.buildpack.platform.json.MappedObject; + +/** + * Status details returned from {@code Docker container wait}. + * + * @author Scott Frederick + * @since 2.3.0 + */ +public class ContainerStatus extends MappedObject { + + private final int statusCode; + + private final String waitingErrorMessage; + + ContainerStatus(int statusCode, String waitingErrorMessage) { + super(null, null); + this.statusCode = statusCode; + this.waitingErrorMessage = waitingErrorMessage; + } + + ContainerStatus(JsonNode node) { + super(node, MethodHandles.lookup()); + this.statusCode = valueAt("/StatusCode", Integer.class); + this.waitingErrorMessage = valueAt("/Error/Message", String.class); + } + + /** + * Return the container exit status code. + * @return the exit status code + */ + public int getStatusCode() { + return this.statusCode; + } + + /** + * Return a message indicating an error waiting for a container to stop. + * @return the waiting error message + */ + public String getWaitingErrorMessage() { + return this.waitingErrorMessage; + } + + /** + * Create a new {@link ContainerStatus} instance from the specified JSON content + * stream. + * @param content the JSON content stream + * @return a new {@link ContainerStatus} instance + * @throws IOException on IO error + */ + public static ContainerStatus of(InputStream content) throws IOException { + return of(content, ContainerStatus::new); + } + + /** + * Create a new {@link ContainerStatus} instance with the specified values. + * @param statusCode the status code + * @param errorMessage the error message + * @return a new {@link ContainerStatus} instance + */ + public static ContainerStatus of(int statusCode, String errorMessage) { + return new ContainerStatus(statusCode, errorMessage); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Image.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Image.java new file mode 100644 index 000000000000..4864fe3d53a2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Image.java @@ -0,0 +1,114 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.JsonNode; + +import org.springframework.boot.buildpack.platform.json.MappedObject; + +/** + * Image details as returned from {@code Docker inspect}. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public class Image extends MappedObject { + + private final List digests; + + private final ImageConfig config; + + private final List layers; + + private final String os; + + Image(JsonNode node) { + super(node, MethodHandles.lookup()); + this.digests = getDigests(getNode().at("/RepoDigests")); + this.config = new ImageConfig(getNode().at("/Config")); + this.layers = extractLayers(valueAt("/RootFS/Layers", String[].class)); + this.os = valueAt("/Os", String.class); + } + + private List getDigests(JsonNode node) { + if (node.isEmpty()) { + return Collections.emptyList(); + } + List digests = new ArrayList<>(); + node.forEach((child) -> digests.add(child.asText())); + return Collections.unmodifiableList(digests); + } + + private List extractLayers(String[] layers) { + if (layers == null) { + return Collections.emptyList(); + } + return Collections.unmodifiableList(Arrays.stream(layers).map(LayerId::of).collect(Collectors.toList())); + } + + /** + * Return the digests of the image. + * @return the image digests + */ + public List getDigests() { + return this.digests; + } + + /** + * Return image config information. + * @return the image config + */ + public ImageConfig getConfig() { + return this.config; + } + + /** + * Return the layer IDs contained in the image. + * @return the layer IDs. + */ + public List getLayers() { + return this.layers; + } + + /** + * Return the OS of the image. + * @return the image OS + */ + public String getOs() { + return (this.os != null) ? this.os : "linux"; + } + + /** + * Create a new {@link Image} instance from the specified JSON content. + * @param content the JSON content + * @return a new {@link Image} instance + * @throws IOException on IO error + */ + public static Image of(InputStream content) throws IOException { + return of(content, Image::new); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchive.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchive.java new file mode 100644 index 000000000000..01e222560484 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchive.java @@ -0,0 +1,298 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import java.io.IOException; +import java.io.OutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import org.springframework.boot.buildpack.platform.io.Content; +import org.springframework.boot.buildpack.platform.io.IOConsumer; +import org.springframework.boot.buildpack.platform.io.InspectedContent; +import org.springframework.boot.buildpack.platform.io.Layout; +import org.springframework.boot.buildpack.platform.io.Owner; +import org.springframework.boot.buildpack.platform.io.TarArchive; +import org.springframework.boot.buildpack.platform.json.SharedObjectMapper; +import org.springframework.util.Assert; + +/** + * An image archive that can be loaded into Docker. + * + * @author Phillip Webb + * @author Scott Frederick + * @since 2.3.0 + * @see #from(Image, IOConsumer) + * @see Docker Image + * Specification + */ +public class ImageArchive implements TarArchive { + + private static final Instant WINDOWS_EPOCH_PLUS_SECOND = OffsetDateTime.of(1980, 1, 1, 0, 0, 1, 0, ZoneOffset.UTC) + .toInstant(); + + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ISO_ZONED_DATE_TIME + .withZone(ZoneOffset.UTC); + + private static final IOConsumer NO_UPDATES = (update) -> { + }; + + private final ObjectMapper objectMapper; + + private final ImageConfig imageConfig; + + private final Instant createDate; + + private final ImageReference tag; + + private final String os; + + private final List existingLayers; + + private final List newLayers; + + ImageArchive(ObjectMapper objectMapper, ImageConfig imageConfig, Instant createDate, ImageReference tag, String os, + List existingLayers, List newLayers) { + this.objectMapper = objectMapper; + this.imageConfig = imageConfig; + this.createDate = createDate; + this.tag = tag; + this.os = os; + this.existingLayers = existingLayers; + this.newLayers = newLayers; + } + + /** + * Return the image config for the archive. + * @return the image config + */ + public ImageConfig getImageConfig() { + return this.imageConfig; + } + + /** + * Return the create data of the archive. + * @return the create date + */ + public Instant getCreateDate() { + return this.createDate; + } + + /** + * Return the tag of the archive. + * @return the tag + */ + public ImageReference getTag() { + return this.tag; + } + + @Override + public void writeTo(OutputStream outputStream) throws IOException { + TarArchive.of(this::write).writeTo(outputStream); + } + + private void write(Layout writer) throws IOException { + List writtenLayers = writeLayers(writer); + String config = writeConfig(writer, writtenLayers); + writeManifest(writer, config, writtenLayers); + } + + private List writeLayers(Layout writer) throws IOException { + List writtenLayers = new ArrayList<>(); + for (Layer layer : this.newLayers) { + writtenLayers.add(writeLayer(writer, layer)); + } + return Collections.unmodifiableList(writtenLayers); + } + + private LayerId writeLayer(Layout writer, Layer layer) throws IOException { + LayerId id = layer.getId(); + writer.file("/" + id.getHash() + ".tar", Owner.ROOT, layer); + return id; + } + + private String writeConfig(Layout writer, List writtenLayers) throws IOException { + try { + ObjectNode config = createConfig(writtenLayers); + String json = this.objectMapper.writeValueAsString(config).replace("\r\n", "\n"); + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + InspectedContent content = InspectedContent.of(Content.of(json), digest::update); + String name = "/" + LayerId.ofSha256Digest(digest.digest()).getHash() + ".json"; + writer.file(name, Owner.ROOT, content); + return name; + } + catch (NoSuchAlgorithmException ex) { + throw new IllegalStateException(ex); + } + } + + private ObjectNode createConfig(List writtenLayers) { + ObjectNode config = this.objectMapper.createObjectNode(); + config.set("config", this.imageConfig.getNodeCopy()); + config.set("created", config.textNode(getCreatedDate())); + config.set("history", createHistory(writtenLayers)); + config.set("os", config.textNode(this.os)); + config.set("rootfs", createRootFs(writtenLayers)); + return config; + } + + private String getCreatedDate() { + return DATE_FORMATTER.format(this.createDate); + } + + private JsonNode createHistory(List writtenLayers) { + ArrayNode history = this.objectMapper.createArrayNode(); + int size = this.existingLayers.size() + writtenLayers.size(); + for (int i = 0; i < size; i++) { + history.addObject(); + } + return history; + } + + private JsonNode createRootFs(List writtenLayers) { + ObjectNode rootFs = this.objectMapper.createObjectNode(); + ArrayNode diffIds = rootFs.putArray("diff_ids"); + this.existingLayers.stream().map(Object::toString).forEach(diffIds::add); + writtenLayers.stream().map(Object::toString).forEach(diffIds::add); + return rootFs; + } + + private void writeManifest(Layout writer, String config, List writtenLayers) throws IOException { + ArrayNode manifest = createManifest(config, writtenLayers); + String manifestJson = this.objectMapper.writeValueAsString(manifest); + writer.file("/manifest.json", Owner.ROOT, Content.of(manifestJson)); + } + + private ArrayNode createManifest(String config, List writtenLayers) { + ArrayNode manifest = this.objectMapper.createArrayNode(); + ObjectNode entry = manifest.addObject(); + entry.set("Config", entry.textNode(config)); + entry.set("Layers", getManifestLayers(writtenLayers)); + if (this.tag != null) { + entry.set("RepoTags", entry.arrayNode().add(this.tag.toString())); + } + return manifest; + } + + private ArrayNode getManifestLayers(List writtenLayers) { + ArrayNode layers = this.objectMapper.createArrayNode(); + for (int i = 0; i < this.existingLayers.size(); i++) { + layers.add(""); + } + writtenLayers.stream().map((id) -> id.getHash() + ".tar").forEach(layers::add); + return layers; + } + + /** + * Create a new {@link ImageArchive} based on an existing {@link Image}. + * @param image the image that this archive is based on + * @return the new image archive. + * @throws IOException on IO error + */ + public static ImageArchive from(Image image) throws IOException { + return from(image, NO_UPDATES); + } + + /** + * Create a new {@link ImageArchive} based on an existing {@link Image}. + * @param image the image that this archive is based on + * @param update consumer to apply updates + * @return the new image archive. + * @throws IOException on IO error + */ + public static ImageArchive from(Image image, IOConsumer update) throws IOException { + return new Update(image).applyTo(update); + } + + /** + * Update class used to change data when creating an image archive. + */ + public static final class Update { + + private final Image image; + + private ImageConfig config; + + private Instant createDate; + + private ImageReference tag; + + private final List newLayers = new ArrayList<>(); + + private Update(Image image) { + this.image = image; + this.config = image.getConfig(); + } + + private ImageArchive applyTo(IOConsumer update) throws IOException { + update.accept(this); + Instant createDate = (this.createDate != null) ? this.createDate : WINDOWS_EPOCH_PLUS_SECOND; + return new ImageArchive(SharedObjectMapper.get(), this.config, createDate, this.tag, this.image.getOs(), + this.image.getLayers(), Collections.unmodifiableList(this.newLayers)); + } + + /** + * Apply updates to the {@link ImageConfig}. + * @param update consumer to apply updates + */ + public void withUpdatedConfig(Consumer update) { + this.config = this.config.copy(update); + } + + /** + * Add a new layer to the image archive. + * @param layer the layer to add + */ + public void withNewLayer(Layer layer) { + Assert.notNull(layer, "Layer must not be null"); + this.newLayers.add(layer); + } + + /** + * Set the create date for the image archive. + * @param createDate the create date + */ + public void withCreateDate(Instant createDate) { + Assert.notNull(createDate, "CreateDate must not be null"); + this.createDate = createDate; + } + + /** + * Set the tag for the image archive. + * @param tag the tag + */ + public void withTag(ImageReference tag) { + Assert.notNull(tag, "Tag must not be null"); + this.tag = tag.inTaggedForm(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageConfig.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageConfig.java new file mode 100644 index 000000000000..6b5f510ed888 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageConfig.java @@ -0,0 +1,136 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import java.lang.invoke.MethodHandles; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Consumer; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import org.springframework.boot.buildpack.platform.json.MappedObject; + +/** + * Image configuration information. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @since 2.3.0 + */ +public class ImageConfig extends MappedObject { + + private final Map labels; + + private final Map configEnv; + + ImageConfig(JsonNode node) { + super(node, MethodHandles.lookup()); + this.labels = extractLabels(); + this.configEnv = parseConfigEnv(); + } + + @SuppressWarnings("unchecked") + private Map extractLabels() { + Map labels = valueAt("/Labels", Map.class); + if (labels == null) { + return Collections.emptyMap(); + } + return labels; + } + + private Map parseConfigEnv() { + String[] entries = valueAt("/Env", String[].class); + if (entries == null) { + return Collections.emptyMap(); + } + Map env = new LinkedHashMap<>(); + for (String entry : entries) { + int i = entry.indexOf('='); + String name = (i != -1) ? entry.substring(0, i) : entry; + String value = (i != -1) ? entry.substring(i + 1) : null; + env.put(name, value); + } + return Collections.unmodifiableMap(env); + } + + JsonNode getNodeCopy() { + return super.getNode().deepCopy(); + } + + /** + * Return the image labels. If the image has no labels, an empty {@code Map} is + * returned. + * @return the image labels, never {@code null} + */ + public Map getLabels() { + return this.labels; + } + + /** + * Return the image environment variables. If the image has no environment variables, + * an empty {@code Map} is returned. + * @return the env, never {@code null} + */ + public Map getEnv() { + return this.configEnv; + } + + /** + * Create an updated copy of this image config. + * @param update consumer to apply updates + * @return an updated image config + */ + public ImageConfig copy(Consumer update) { + return new Update(this).run(update); + + } + + /** + * Update class used to change data when creating a copy. + */ + public static final class Update { + + private final ObjectNode copy; + + private Update(ImageConfig source) { + this.copy = source.getNode().deepCopy(); + } + + private ImageConfig run(Consumer update) { + update.accept(this); + return new ImageConfig(this.copy); + } + + /** + * Update the image config with an additional label. + * @param label the label name + * @param value the label value + */ + public void withLabel(String label, String value) { + JsonNode labels = this.copy.at("/Labels"); + if (labels.isMissingNode()) { + labels = this.copy.putObject("Labels"); + } + ((ObjectNode) labels).put(label, value); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageName.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageName.java new file mode 100644 index 000000000000..073871c39692 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageName.java @@ -0,0 +1,148 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import org.springframework.util.Assert; + +/** + * A Docker image name of the form {@literal "docker.io/library/ubuntu"}. + * + * @author Phillip Webb + * @author Scott Frederick + * @since 2.3.0 + * @see ImageReference + * @see #of(String) + */ +public class ImageName { + + private static final String DEFAULT_DOMAIN = "docker.io"; + + private static final String OFFICIAL_REPOSITORY_NAME = "library"; + + private static final String LEGACY_DOMAIN = "index.docker.io"; + + private final String domain; + + private final String name; + + private final String string; + + ImageName(String domain, String path) { + Assert.hasText(path, "Path must not be empty"); + this.domain = getDomainOrDefault(domain); + this.name = getNameWithDefaultPath(this.domain, path); + this.string = this.domain + "/" + this.name; + } + + /** + * Return the domain for this image name. + * @return the domain + */ + public String getDomain() { + return this.domain; + } + + /** + * Return the name of this image. + * @return the image name + */ + public String getName() { + return this.name; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ImageName other = (ImageName) obj; + boolean result = true; + result = result && this.domain.equals(other.domain); + result = result && this.name.equals(other.name); + return result; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + this.domain.hashCode(); + result = prime * result + this.name.hashCode(); + return result; + } + + @Override + public String toString() { + return this.string; + } + + public String toLegacyString() { + if (DEFAULT_DOMAIN.equals(this.domain)) { + return LEGACY_DOMAIN + "/" + this.name; + } + return this.string; + } + + private String getDomainOrDefault(String domain) { + if (domain == null || LEGACY_DOMAIN.equals(domain)) { + return DEFAULT_DOMAIN; + } + return domain; + } + + private String getNameWithDefaultPath(String domain, String name) { + if (DEFAULT_DOMAIN.equals(domain) && !name.contains("/")) { + return OFFICIAL_REPOSITORY_NAME + "/" + name; + } + return name; + } + + /** + * Create a new {@link ImageName} from the given value. The following value forms can + * be used: + *

      + *
    • {@code name} (maps to {@code docker.io/library/name})
    • + *
    • {@code domain/name}
    • + *
    • {@code domain:port/name}
    • + *
    + * @param value the value to parse + * @return an {@link ImageName} instance + */ + public static ImageName of(String value) { + Assert.hasText(value, "Value must not be empty"); + String domain = parseDomain(value); + String path = (domain != null) ? value.substring(domain.length() + 1) : value; + Assert.isTrue(Regex.PATH.matcher(path).matches(), + () -> "Unable to parse name \"" + value + "\". " + + "Image name must be in the form '[domainHost:port/][path/]name', " + + "with 'path' and 'name' containing only [a-z0-9][.][_][-]"); + return new ImageName(domain, path); + } + + static String parseDomain(String value) { + int firstSlash = value.indexOf('/'); + String candidate = (firstSlash != -1) ? value.substring(0, firstSlash) : null; + if (candidate != null && Regex.DOMAIN.matcher(candidate).matches()) { + return candidate; + } + return null; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageReference.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageReference.java new file mode 100644 index 000000000000..f3b29ab19e00 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageReference.java @@ -0,0 +1,289 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import java.io.File; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * A reference to a Docker image of the form {@code "imagename[:tag|@digest]"}. + * + * @author Phillip Webb + * @author Scott Frederick + * @since 2.3.0 + * @see ImageName + */ +public final class ImageReference { + + private static final Pattern JAR_VERSION_PATTERN = Pattern.compile("^(.*)(\\-\\d+)$"); + + private static final String LATEST = "latest"; + + private final ImageName name; + + private final String tag; + + private final String digest; + + private final String string; + + private ImageReference(ImageName name, String tag, String digest) { + Assert.notNull(name, "Name must not be null"); + this.name = name; + this.tag = tag; + this.digest = digest; + this.string = buildString(name.toString(), tag, digest); + } + + /** + * Return the domain for this image name. + * @return the domain + * @see ImageName#getDomain() + */ + public String getDomain() { + return this.name.getDomain(); + } + + /** + * Return the name of this image. + * @return the image name + * @see ImageName#getName() + */ + public String getName() { + return this.name.getName(); + } + + /** + * Return the tag from the reference or {@code null}. + * @return the referenced tag + */ + public String getTag() { + return this.tag; + } + + /** + * Return the digest from the reference or {@code null}. + * @return the referenced digest + */ + public String getDigest() { + return this.digest; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ImageReference other = (ImageReference) obj; + boolean result = true; + result = result && this.name.equals(other.name); + result = result && ObjectUtils.nullSafeEquals(this.tag, other.tag); + result = result && ObjectUtils.nullSafeEquals(this.digest, other.digest); + return result; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + this.name.hashCode(); + result = prime * result + ObjectUtils.nullSafeHashCode(this.tag); + result = prime * result + ObjectUtils.nullSafeHashCode(this.digest); + return result; + } + + @Override + public String toString() { + return this.string; + } + + public String toLegacyString() { + return buildString(this.name.toLegacyString(), this.tag, this.digest); + } + + private String buildString(String name, String tag, String digest) { + StringBuilder string = new StringBuilder(name); + if (tag != null) { + string.append(":").append(tag); + } + if (digest != null) { + string.append("@").append(digest); + } + return string.toString(); + } + + /** + * Create a new {@link ImageReference} with an updated digest. + * @param digest the new digest + * @return an updated image reference + */ + public ImageReference withDigest(String digest) { + return new ImageReference(this.name, null, digest); + } + + /** + * Return an {@link ImageReference} in the form {@code "imagename:tag"}. If the tag + * has not been defined then {@code latest} is used. + * @return the image reference in tagged form + * @throws IllegalStateException if the image reference contains a digest + */ + public ImageReference inTaggedForm() { + Assert.state(this.digest == null, () -> "Image reference '" + this + "' cannot contain a digest"); + return new ImageReference(this.name, (this.tag != null) ? this.tag : LATEST, null); + } + + /** + * Return an {@link ImageReference} containing either a tag or a digest. If neither + * the digest or the tag has been defined then tag {@code latest} is used. + * @return the image reference in tagged or digest form + */ + public ImageReference inTaggedOrDigestForm() { + if (this.digest != null) { + return this; + } + return inTaggedForm(); + } + + /** + * Create a new {@link ImageReference} instance deduced from a source JAR file that + * follows common Java naming conventions. + * @param jarFile the source jar file + * @return an {@link ImageName} for the jar file. + */ + public static ImageReference forJarFile(File jarFile) { + String filename = jarFile.getName(); + Assert.isTrue(filename.toLowerCase().endsWith(".jar"), () -> "File '" + jarFile + "' is not a JAR"); + filename = filename.substring(0, filename.length() - 4); + int firstDot = filename.indexOf('.'); + if (firstDot == -1) { + return of(filename); + } + String name = filename.substring(0, firstDot); + String version = filename.substring(firstDot + 1); + Matcher matcher = JAR_VERSION_PATTERN.matcher(name); + if (matcher.matches()) { + name = matcher.group(1); + version = matcher.group(2).substring(1) + "." + version; + } + return of(ImageName.of(name), version); + } + + /** + * Generate an image name with a random suffix. + * @param prefix the name prefix + * @return a random image reference + */ + public static ImageReference random(String prefix) { + return ImageReference.random(prefix, 10); + } + + /** + * Generate an image name with a random suffix. + * @param prefix the name prefix + * @param randomLength the number of chars in the random part of the name + * @return a random image reference + */ + public static ImageReference random(String prefix, int randomLength) { + return of(RandomString.generate(prefix, randomLength)); + } + + /** + * Create a new {@link ImageReference} from the given value. The following value forms + * can be used: + *
      + *
    • {@code name} (maps to {@code docker.io/library/name})
    • + *
    • {@code domain/name}
    • + *
    • {@code domain:port/name}
    • + *
    • {@code domain:port/name:tag}
    • + *
    • {@code domain:port/name@digest}
    • + *
    + * @param value the value to parse + * @return an {@link ImageName} instance + */ + public static ImageReference of(String value) { + Assert.hasText(value, "Value must not be null"); + String domain = ImageName.parseDomain(value); + String path = (domain != null) ? value.substring(domain.length() + 1) : value; + String digest = null; + int digestSplit = path.indexOf("@"); + if (digestSplit != -1) { + String remainder = path.substring(digestSplit + 1); + Matcher matcher = Regex.DIGEST.matcher(remainder); + if (matcher.find()) { + digest = remainder.substring(0, matcher.end()); + remainder = remainder.substring(matcher.end()); + path = path.substring(0, digestSplit) + remainder; + } + } + String tag = null; + int tagSplit = path.lastIndexOf(":"); + if (tagSplit != -1) { + String remainder = path.substring(tagSplit + 1); + Matcher matcher = Regex.TAG.matcher(remainder); + if (matcher.find()) { + tag = remainder.substring(0, matcher.end()); + remainder = remainder.substring(matcher.end()); + path = path.substring(0, tagSplit) + remainder; + } + } + Assert.isTrue(Regex.PATH.matcher(path).matches(), + () -> "Unable to parse image reference \"" + value + "\". " + + "Image reference must be in the form '[domainHost:port/][path/]name[:tag][@digest]', " + + "with 'path' and 'name' containing only [a-z0-9][.][_][-]"); + ImageName name = new ImageName(domain, path); + return new ImageReference(name, tag, digest); + } + + /** + * Create a new {@link ImageReference} from the given {@link ImageName}. + * @param name the image name + * @return a new image reference + */ + public static ImageReference of(ImageName name) { + return new ImageReference(name, null, null); + } + + /** + * Create a new {@link ImageReference} from the given {@link ImageName} and tag. + * @param name the image name + * @param tag the referenced tag + * @return a new image reference + */ + public static ImageReference of(ImageName name, String tag) { + return new ImageReference(name, tag, null); + } + + /** + * Create a new {@link ImageReference} from the given {@link ImageName}, tag and + * digest. + * @param name the image name + * @param tag the referenced tag + * @param digest the referenced digest + * @return a new image reference + */ + public static ImageReference of(ImageName name, String tag, String digest) { + return new ImageReference(name, tag, digest); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Layer.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Layer.java new file mode 100644 index 000000000000..6d481eb12d7f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Layer.java @@ -0,0 +1,94 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import java.io.IOException; +import java.io.OutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import org.springframework.boot.buildpack.platform.io.Content; +import org.springframework.boot.buildpack.platform.io.IOConsumer; +import org.springframework.boot.buildpack.platform.io.InspectedContent; +import org.springframework.boot.buildpack.platform.io.Layout; +import org.springframework.boot.buildpack.platform.io.TarArchive; +import org.springframework.util.Assert; + +/** + * A layer that can be written to an {@link ImageArchive}. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public class Layer implements Content { + + private final Content content; + + private final LayerId id; + + Layer(TarArchive tarArchive) throws NoSuchAlgorithmException, IOException { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + this.content = InspectedContent.of(tarArchive::writeTo, digest::update); + this.id = LayerId.ofSha256Digest(digest.digest()); + } + + /** + * Return the ID of the layer. + * @return the layer ID + */ + public LayerId getId() { + return this.id; + } + + @Override + public int size() { + return this.content.size(); + } + + @Override + public void writeTo(OutputStream outputStream) throws IOException { + this.content.writeTo(outputStream); + } + + /** + * Factory method to create a new {@link Layer} with a specific {@link Layout}. + * @param layout the layer layout + * @return a new layer instance + * @throws IOException on IO error + */ + public static Layer of(IOConsumer layout) throws IOException { + Assert.notNull(layout, "Layout must not be null"); + return fromTarArchive(TarArchive.of(layout)); + } + + /** + * Factory method to create a new {@link Layer} from a {@link TarArchive}. + * @param tarArchive the contents of the layer + * @return a new layer instance + * @throws IOException on error + */ + public static Layer fromTarArchive(TarArchive tarArchive) throws IOException { + Assert.notNull(tarArchive, "TarArchive must not be null"); + try { + return new Layer(tarArchive); + } + catch (NoSuchAlgorithmException ex) { + throw new IllegalStateException(ex); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/LayerId.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/LayerId.java new file mode 100644 index 000000000000..d30cc6a331aa --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/LayerId.java @@ -0,0 +1,105 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import java.math.BigInteger; + +import org.springframework.util.Assert; + +/** + * A layer ID as used inside a Docker image of the form {@code algorithm: hash}. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public final class LayerId { + + private final String value; + + private final String algorithm; + + private final String hash; + + private LayerId(String value, String algorithm, String hash) { + this.value = value; + this.algorithm = algorithm; + this.hash = hash; + } + + /** + * Return the algorithm of layer. + * @return the algorithm + */ + public String getAlgorithm() { + return this.algorithm; + } + + /** + * Return the hash of the layer. + * @return the layer hash + */ + public String getHash() { + return this.hash; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + return this.value.equals(((LayerId) obj).value); + } + + @Override + public int hashCode() { + return this.value.hashCode(); + } + + @Override + public String toString() { + return this.value; + } + + /** + * Create a new {@link LayerId} with the specified value. + * @param value the layer ID value of the form {@code algorithm: hash} + * @return a new layer ID instance + */ + public static LayerId of(String value) { + Assert.hasText(value, "Value must not be empty"); + int i = value.indexOf(':'); + Assert.isTrue(i >= 0, () -> "Invalid layer ID '" + value + "'"); + return new LayerId(value, value.substring(0, i), value.substring(i + 1)); + } + + /** + * Create a new {@link LayerId} from a SHA-256 digest. + * @param digest the digest + * @return a new layer ID instance + */ + public static LayerId ofSha256Digest(byte[] digest) { + Assert.notNull(digest, "Digest must not be null"); + Assert.isTrue(digest.length == 32, "Digest must be exactly 32 bytes"); + String algorithm = "sha256"; + String hash = String.format("%064x", new BigInteger(1, digest)); + return new LayerId(algorithm + ":" + hash, algorithm, hash); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/RandomString.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/RandomString.java new file mode 100644 index 000000000000..d9696d3db7f3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/RandomString.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import java.util.Random; +import java.util.stream.IntStream; + +import org.springframework.util.Assert; + +/** + * Utility class used to generate random strings. + * + * @author Phillip Webb + */ +final class RandomString { + + private static final Random random = new Random(); + + private RandomString() { + } + + static String generate(String prefix, int randomLength) { + Assert.notNull(prefix, "Prefix must not be null"); + return prefix + generateRandom(randomLength); + } + + static CharSequence generateRandom(int length) { + IntStream chars = random.ints('a', 'z' + 1).limit(length); + return chars.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Regex.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Regex.java new file mode 100644 index 000000000000..c4ebb66520a3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Regex.java @@ -0,0 +1,121 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import java.util.regex.Pattern; + +/** + * Regular Expressions for image names and references based on those found in the Docker + * codebase. + * + * @author Scott Frederick + * @author Phillip Webb + * @see Docker + * grammar reference + * @see Docker grammar + * implementation + * @see How + * are Docker image names parsed? + */ +final class Regex implements CharSequence { + + static final Pattern DOMAIN; + static { + Regex component = Regex.oneOf("[a-zA-Z0-9]", "[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]"); + Regex dotComponent = Regex.group("[.]", component); + Regex colonPort = Regex.of("[:][0-9]+"); + Regex dottedDomain = Regex.group(component, dotComponent.oneOrMoreTimes()); + Regex dottedDomainAndPort = Regex.group(component, dotComponent.oneOrMoreTimes(), colonPort); + Regex nameAndPort = Regex.group(component, colonPort); + DOMAIN = Regex.oneOf(dottedDomain, nameAndPort, dottedDomainAndPort, "localhost").compile(); + } + + private static final Regex PATH_COMPONENT; + static { + Regex segment = Regex.of("[a-z0-9]+"); + Regex separator = Regex.group("[._]|__|[-]*"); + Regex separatedSegment = Regex.group(separator, segment).oneOrMoreTimes(); + PATH_COMPONENT = Regex.of(segment, Regex.group(separatedSegment).zeroOrOnce()); + } + + static final Pattern PATH; + static { + Regex component = PATH_COMPONENT; + Regex slashComponent = Regex.group("[/]", component); + Regex slashComponents = Regex.group(slashComponent.oneOrMoreTimes()); + PATH = Regex.of(component, slashComponents.zeroOrOnce()).compile(); + } + + static final Pattern TAG = Regex.of("^[\\w][\\w.-]{0,127}").compile(); + + static final Pattern DIGEST = Regex.of("^[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[A-Fa-f0-9]]{32,}") + .compile(); + + private final String value; + + private Regex(CharSequence value) { + this.value = value.toString(); + } + + private Regex oneOrMoreTimes() { + return new Regex(this.value + "+"); + } + + private Regex zeroOrOnce() { + return new Regex(this.value + "?"); + } + + Pattern compile() { + return Pattern.compile("^" + this.value + "$"); + } + + @Override + public int length() { + return this.value.length(); + } + + @Override + public char charAt(int index) { + return this.value.charAt(index); + } + + @Override + public CharSequence subSequence(int start, int end) { + return this.value.subSequence(start, end); + } + + @Override + public String toString() { + return this.value; + } + + private static Regex of(CharSequence... expressions) { + return new Regex(String.join("", expressions)); + } + + private static Regex oneOf(CharSequence... expressions) { + return new Regex("(?:" + String.join("|", expressions) + ")"); + } + + private static Regex group(CharSequence... expressions) { + return new Regex("(?:" + String.join("", expressions) + ")"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/VolumeName.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/VolumeName.java new file mode 100644 index 000000000000..8a01d88a1832 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/VolumeName.java @@ -0,0 +1,144 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.function.Function; + +import org.springframework.util.Assert; + +/** + * A Docker volume name. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public final class VolumeName { + + private final String value; + + private VolumeName(String value) { + this.value = value; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + return this.value.equals(((VolumeName) obj).value); + } + + @Override + public int hashCode() { + return this.value.hashCode(); + } + + @Override + public String toString() { + return this.value; + } + + /** + * Factory method to create a new {@link VolumeName} with a random name. + * @param prefix the prefix to use with the random name + * @return a randomly named volume + */ + public static VolumeName random(String prefix) { + return random(prefix, 10); + } + + /** + * Factory method to create a new {@link VolumeName} with a random name. + * @param prefix the prefix to use with the random name + * @param randomLength the number of chars in the random part of the name + * @return a randomly volume reference + */ + public static VolumeName random(String prefix, int randomLength) { + return of(RandomString.generate(prefix, randomLength)); + } + + /** + * Factory method to create a new {@link VolumeName} based on an object. The resulting + * name will be based off a SHA-256 digest of the given object's {@code toString()} + * method. + * @param the source object type + * @param source the source object + * @param prefix the prefix to use with the volume name + * @param suffix the suffix to use with the volume name + * @param digestLength the number of chars in the digest part of the name + * @return a name based off the image reference + */ + public static VolumeName basedOn(S source, String prefix, String suffix, int digestLength) { + return basedOn(source, Object::toString, prefix, suffix, digestLength); + } + + /** + * Factory method to create a new {@link VolumeName} based on an object. The resulting + * name will be based off a SHA-256 digest of the given object's name. + * @param the source object type + * @param source the source object + * @param nameExtractor a method to extract the name of the object + * @param prefix the prefix to use with the volume name + * @param suffix the suffix to use with the volume name + * @param digestLength the number of chars in the digest part of the name + * @return a name based off the image reference + */ + public static VolumeName basedOn(S source, Function nameExtractor, String prefix, String suffix, + int digestLength) { + Assert.notNull(source, "Source must not be null"); + Assert.notNull(nameExtractor, "NameExtractor must not be null"); + Assert.notNull(prefix, "Prefix must not be null"); + Assert.notNull(suffix, "Suffix must not be null"); + return of(prefix + getDigest(nameExtractor.apply(source), digestLength) + suffix); + } + + private static String getDigest(String name, int length) { + try { + MessageDigest digest = MessageDigest.getInstance("sha-256"); + return asHexString(digest.digest(name.getBytes(StandardCharsets.UTF_8)), length); + } + catch (NoSuchAlgorithmException ex) { + throw new IllegalStateException(ex); + } + } + + private static String asHexString(byte[] digest, int digestLength) { + Assert.isTrue(digestLength <= digest.length, + () -> "DigestLength must be less than or equal to " + digest.length); + byte[] shortDigest = new byte[6]; + System.arraycopy(digest, 0, shortDigest, 0, shortDigest.length); + return String.format("%0" + digestLength + "x", new BigInteger(1, shortDigest)); + } + + /** + * Factory method to create a {@link VolumeName} with a specific value. + * @param value the volume reference value + * @return a new {@link VolumeName} instance + */ + public static VolumeName of(String value) { + Assert.notNull(value, "Value must not be null"); + return new VolumeName(value); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/package-info.java new file mode 100644 index 000000000000..f67ce1a7c324 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Docker types. + */ +package org.springframework.boot.buildpack.platform.docker.type; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/Content.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/Content.java new file mode 100644 index 000000000000..911df2ad4927 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/Content.java @@ -0,0 +1,106 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.io; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +import org.springframework.util.Assert; +import org.springframework.util.FileCopyUtils; + +/** + * Content with a known size that can be written to an {@link OutputStream}. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public interface Content { + + /** + * The size of the content in bytes. + * @return the content size + */ + int size(); + + /** + * Write the content to the given output stream. + * @param outputStream the output stream to write to + * @throws IOException on IO error + */ + void writeTo(OutputStream outputStream) throws IOException; + + /** + * Create a new {@link Content} from the given UTF-8 string. + * @param string the string to write + * @return a new {@link Content} instance + */ + static Content of(String string) { + Assert.notNull(string, "String must not be null"); + return of(string.getBytes(StandardCharsets.UTF_8)); + } + + /** + * Create a new {@link Content} from the given input stream. + * @param bytes the bytes to write + * @return a new {@link Content} instance + */ + static Content of(byte[] bytes) { + Assert.notNull(bytes, "Bytes must not be null"); + return of(bytes.length, () -> new ByteArrayInputStream(bytes)); + } + + /** + * Create a new {@link Content} from the given file. + * @param file the file to write + * @return a new {@link Content} instance + */ + static Content of(File file) { + Assert.notNull(file, "File must not be null"); + return of((int) file.length(), () -> new FileInputStream(file)); + } + + /** + * Create a new {@link Content} from the given input stream. The stream will be closed + * after it has been written. + * @param size the size of the supplied input stream + * @param supplier the input stream supplier + * @return a new {@link Content} instance + */ + static Content of(int size, IOSupplier supplier) { + Assert.isTrue(size >= 0, "Size must not be negative"); + Assert.notNull(supplier, "Supplier must not be null"); + return new Content() { + + @Override + public int size() { + return size; + } + + @Override + public void writeTo(OutputStream outputStream) throws IOException { + FileCopyUtils.copy(supplier.get(), outputStream); + } + + }; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/DefaultOwner.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/DefaultOwner.java new file mode 100644 index 000000000000..1fc71958fa5c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/DefaultOwner.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.io; + +/** + * Default {@link Owner} implementation. + * + * @author Phillip Webb + * @see Owner#of(long, long) + */ +class DefaultOwner implements Owner { + + private final long uid; + + private final long gid; + + DefaultOwner(long uid, long gid) { + this.uid = uid; + this.gid = gid; + } + + @Override + public long getUid() { + return this.uid; + } + + @Override + public long getGid() { + return this.gid; + } + + @Override + public String toString() { + return this.uid + "/" + this.gid; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/FilePermissions.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/FilePermissions.java new file mode 100644 index 000000000000..7f63c012d82e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/FilePermissions.java @@ -0,0 +1,87 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.io; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFileAttributeView; +import java.nio.file.attribute.PosixFilePermission; +import java.util.Collection; + +import org.springframework.util.Assert; + +/** + * Utilities for dealing with file permissions and attributes. + * + * @author Scott Frederick + * @since 2.5.0 + */ +public final class FilePermissions { + + private FilePermissions() { + } + + /** + * Return the integer representation of the file permissions for a path, where the + * integer value conforms to the + * umask octal notation. + * @param path the file path + * @return the integer representation + * @throws IOException if path permissions cannot be read + */ + public static int umaskForPath(Path path) throws IOException { + Assert.notNull(path, "Path must not be null"); + PosixFileAttributeView attributeView = Files.getFileAttributeView(path, PosixFileAttributeView.class); + Assert.state(attributeView != null, "Unsupported file type for retrieving Posix attributes"); + return posixPermissionsToUmask(attributeView.readAttributes().permissions()); + } + + /** + * Return the integer representation of a set of Posix file permissions, where the + * integer value conforms to the + * umask octal notation. + * @param permissions the set of {@code PosixFilePermission}s + * @return the integer representation + */ + public static int posixPermissionsToUmask(Collection permissions) { + Assert.notNull(permissions, "Permissions must not be null"); + int owner = permissionToUmask(permissions, PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.OWNER_WRITE, + PosixFilePermission.OWNER_READ); + int group = permissionToUmask(permissions, PosixFilePermission.GROUP_EXECUTE, PosixFilePermission.GROUP_WRITE, + PosixFilePermission.GROUP_READ); + int other = permissionToUmask(permissions, PosixFilePermission.OTHERS_EXECUTE, PosixFilePermission.OTHERS_WRITE, + PosixFilePermission.OTHERS_READ); + return Integer.parseInt("" + owner + group + other, 8); + } + + private static int permissionToUmask(Collection permissions, PosixFilePermission execute, + PosixFilePermission write, PosixFilePermission read) { + int value = 0; + if (permissions.contains(execute)) { + value += 1; + } + if (permissions.contains(write)) { + value += 2; + } + if (permissions.contains(read)) { + value += 4; + } + return value; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/IOBiConsumer.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/IOBiConsumer.java new file mode 100644 index 000000000000..1cda2c7d31d9 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/IOBiConsumer.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.io; + +import java.io.IOException; + +/** + * BiConsumer that can safely throw {@link IOException IO exceptions}. + * + * @param the first consumed type + * @param the second consumed type + * @author Phillip Webb + * @since 2.3.0 + */ +@FunctionalInterface +public interface IOBiConsumer { + + /** + * Performs this operation on the given argument. + * @param t the first instance to consume + * @param u the second instance to consumer + * @throws IOException on IO error + */ + void accept(T t, U u) throws IOException; + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/IOConsumer.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/IOConsumer.java new file mode 100644 index 000000000000..699b56b3396d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/IOConsumer.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.io; + +import java.io.IOException; + +/** + * Consumer that can safely throw {@link IOException IO exceptions}. + * + * @param the consumed type + * @author Phillip Webb + * @since 2.3.0 + */ +@FunctionalInterface +public interface IOConsumer { + + /** + * Performs this operation on the given argument. + * @param t the instance to consume + * @throws IOException on IO error + */ + void accept(T t) throws IOException; + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/IOSupplier.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/IOSupplier.java new file mode 100644 index 000000000000..df8d237e9596 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/IOSupplier.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.io; + +import java.io.IOException; + +/** + * Supplier that can safely throw {@link IOException IO exceptions}. + * + * @param the supplied type + * @author Phillip Webb + * @since 2.3.0 + */ +@FunctionalInterface +public interface IOSupplier { + + /** + * Gets the supplied value. + * @return the supplied value + * @throws IOException on IO error + */ + T get() throws IOException; + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/InspectedContent.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/InspectedContent.java new file mode 100644 index 000000000000..66aab11e7bfa --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/InspectedContent.java @@ -0,0 +1,187 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.io; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.springframework.util.Assert; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StreamUtils; + +/** + * {@link Content} that is reads and inspects a source of data only once but allows it to + * be consumed multiple times. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public class InspectedContent implements Content { + + static final int MEMORY_LIMIT = 4 * 1024 + 3; + + private final int size; + + private final Object content; + + InspectedContent(int size, Object content) { + this.size = size; + this.content = content; + } + + @Override + public int size() { + return this.size; + } + + @Override + public void writeTo(OutputStream outputStream) throws IOException { + if (this.content instanceof byte[]) { + FileCopyUtils.copy((byte[]) this.content, outputStream); + } + else if (this.content instanceof File) { + FileCopyUtils.copy(new FileInputStream((File) this.content), outputStream); + } + else { + throw new IllegalStateException("Unknown content type"); + } + } + + /** + * Factory method to create an {@link InspectedContent} instance from a source input + * stream. + * @param inputStream the content input stream + * @param inspectors any inspectors to apply + * @return a new inspected content instance + * @throws IOException on IO error + */ + public static InspectedContent of(InputStream inputStream, Inspector... inspectors) throws IOException { + Assert.notNull(inputStream, "InputStream must not be null"); + return of((outputStream) -> FileCopyUtils.copy(inputStream, outputStream), inspectors); + } + + /** + * Factory method to create an {@link InspectedContent} instance from source content. + * @param content the content + * @param inspectors any inspectors to apply + * @return a new inspected content instance + * @throws IOException on IO error + */ + public static InspectedContent of(Content content, Inspector... inspectors) throws IOException { + Assert.notNull(content, "Content must not be null"); + return of(content::writeTo, inspectors); + } + + /** + * Factory method to create an {@link InspectedContent} instance from a source write + * method. + * @param writer a consumer representing the write method + * @param inspectors any inspectors to apply + * @return a new inspected content instance + * @throws IOException on IO error + */ + public static InspectedContent of(IOConsumer writer, Inspector... inspectors) throws IOException { + Assert.notNull(writer, "Writer must not be null"); + InspectingOutputStream outputStream = new InspectingOutputStream(inspectors); + try { + writer.accept(outputStream); + } + finally { + outputStream.close(); + } + return new InspectedContent(outputStream.getSize(), outputStream.getContent()); + } + + /** + * Interface that can be used to inspect content as it is initially read. + */ + public interface Inspector { + + /** + * Update inspected information based on the provided bytes. + * @param input the array of bytes. + * @param offset the offset to start from in the array of bytes. + * @param len the number of bytes to use, starting at {@code offset}. + * @throws IOException on IO error + */ + void update(byte[] input, int offset, int len) throws IOException; + + } + + /** + * Internal {@link OutputStream} used to capture the content either as bytes, or to a + * File if the content is too large. + */ + private static final class InspectingOutputStream extends OutputStream { + + private final Inspector[] inspectors; + + private int size; + + private OutputStream delegate; + + private File tempFile; + + private final byte[] singleByteBuffer = new byte[0]; + + private InspectingOutputStream(Inspector[] inspectors) { + this.inspectors = inspectors; + this.delegate = new ByteArrayOutputStream(); + } + + @Override + public void write(int b) throws IOException { + this.singleByteBuffer[0] = (byte) (b & 0xFF); + write(this.singleByteBuffer); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + int size = len - off; + if (this.tempFile == null && (this.size + size) > MEMORY_LIMIT) { + convertToTempFile(); + } + this.delegate.write(b, off, len); + for (Inspector inspector : this.inspectors) { + inspector.update(b, off, len); + } + this.size += size; + } + + private void convertToTempFile() throws IOException { + this.tempFile = File.createTempFile("buildpack", ".tmp"); + byte[] bytes = ((ByteArrayOutputStream) this.delegate).toByteArray(); + this.delegate = new FileOutputStream(this.tempFile); + StreamUtils.copy(bytes, this.delegate); + } + + private Object getContent() { + return (this.tempFile != null) ? this.tempFile : ((ByteArrayOutputStream) this.delegate).toByteArray(); + } + + private int getSize() { + return this.size; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/Layout.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/Layout.java new file mode 100644 index 000000000000..1d4041efb2e7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/Layout.java @@ -0,0 +1,70 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.io; + +import java.io.IOException; + +/** + * Interface that can be used to write a file/directory layout. + * + * @author Phillip Webb + * @author Scott Frederick + * @since 2.3.0 + */ +public interface Layout { + + /** + * Add a directory to the content. + * @param name the full name of the directory to add + * @param owner the owner of the directory + * @throws IOException on IO error + */ + default void directory(String name, Owner owner) throws IOException { + directory(name, owner, 0755); + } + + /** + * Add a directory to the content. + * @param name the full name of the directory to add + * @param owner the owner of the directory + * @param mode the permissions for the file + * @throws IOException on IO error + */ + void directory(String name, Owner owner, int mode) throws IOException; + + /** + * Write a file to the content. + * @param name the full name of the file to add + * @param owner the owner of the file + * @param content the content to add + * @throws IOException on IO error + */ + default void file(String name, Owner owner, Content content) throws IOException { + file(name, owner, 0644, content); + } + + /** + * Write a file to the content. + * @param name the full name of the file to add + * @param owner the owner of the file + * @param mode the permissions for the file + * @param content the content to add + * @throws IOException on IO error + */ + void file(String name, Owner owner, int mode, Content content) throws IOException; + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/Owner.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/Owner.java new file mode 100644 index 000000000000..88c00bed4ee7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/Owner.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.io; + +/** + * A user and group ID that can be used to indicate file ownership. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public interface Owner { + + /** + * Owner for root ownership. + */ + Owner ROOT = Owner.of(0, 0); + + /** + * Return the user identifier (UID) of the owner. + * @return the user identifier + */ + long getUid(); + + /** + * Return the group identifier (GID) of the owner. + * @return the group identifier + */ + long getGid(); + + /** + * Factory method to create a new {@link Owner} with specified user/group identifier. + * @param uid the user identifier + * @param gid the group identifier + * @return a new {@link Owner} instance + */ + static Owner of(long uid, long gid) { + return new DefaultOwner(uid, gid); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/TarArchive.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/TarArchive.java new file mode 100644 index 000000000000..70a8e7ca979c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/TarArchive.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.io; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + +/** + * A TAR archive that can be written to an output stream. + * + * @author Phillip Webb + * @since 2.3.0 + */ +@FunctionalInterface +public interface TarArchive { + + /** + * {@link Instant} that can be used to normalize TAR files so all entries have the + * same modification time. + */ + Instant NORMALIZED_TIME = OffsetDateTime.of(1980, 1, 1, 0, 0, 1, 0, ZoneOffset.UTC).toInstant(); + + /** + * Write the TAR archive to the given output stream. + * @param outputStream the output stream to write to + * @throws IOException on IO error + */ + void writeTo(OutputStream outputStream) throws IOException; + + /** + * Factory method to create a new {@link TarArchive} instance with a specific layout. + * @param layout the TAR layout + * @return a new {@link TarArchive} instance + */ + static TarArchive of(IOConsumer layout) { + return (outputStream) -> { + TarLayoutWriter writer = new TarLayoutWriter(outputStream); + layout.accept(writer); + writer.finish(); + }; + } + + /** + * Factory method to adapt a ZIP file to {@link TarArchive}. + * @param zip the source zip file + * @param owner the owner of the entries in the TAR + * @return a new {@link TarArchive} instance + */ + static TarArchive fromZip(File zip, Owner owner) { + return new ZipFileTarArchive(zip, owner); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/TarLayoutWriter.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/TarLayoutWriter.java new file mode 100644 index 000000000000..1f8db8280414 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/TarLayoutWriter.java @@ -0,0 +1,86 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.io; + +import java.io.Closeable; +import java.io.IOException; +import java.io.OutputStream; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.apache.commons.compress.archivers.tar.TarConstants; + +import org.springframework.util.StreamUtils; + +/** + * {@link Layout} for writing TAR archive content directly to an {@link OutputStream}. + * + * @author Phillip Webb + * @author Scott Frederick + */ +class TarLayoutWriter implements Layout, Closeable { + + static final long NORMALIZED_MOD_TIME = TarArchive.NORMALIZED_TIME.toEpochMilli(); + + private final TarArchiveOutputStream outputStream; + + TarLayoutWriter(OutputStream outputStream) { + this.outputStream = new TarArchiveOutputStream(outputStream); + this.outputStream.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); + } + + @Override + public void directory(String name, Owner owner, int mode) throws IOException { + this.outputStream.putArchiveEntry(createDirectoryEntry(name, owner, mode)); + this.outputStream.closeArchiveEntry(); + } + + @Override + public void file(String name, Owner owner, int mode, Content content) throws IOException { + this.outputStream.putArchiveEntry(createFileEntry(name, owner, mode, content.size())); + content.writeTo(StreamUtils.nonClosing(this.outputStream)); + this.outputStream.closeArchiveEntry(); + } + + private TarArchiveEntry createDirectoryEntry(String name, Owner owner, int mode) { + return createEntry(name, owner, TarConstants.LF_DIR, mode, 0); + } + + private TarArchiveEntry createFileEntry(String name, Owner owner, int mode, int size) { + return createEntry(name, owner, TarConstants.LF_NORMAL, mode, size); + } + + private TarArchiveEntry createEntry(String name, Owner owner, byte linkFlag, int mode, int size) { + TarArchiveEntry entry = new TarArchiveEntry(name, linkFlag, true); + entry.setUserId(owner.getUid()); + entry.setGroupId(owner.getGid()); + entry.setMode(mode); + entry.setModTime(NORMALIZED_MOD_TIME); + entry.setSize(size); + return entry; + } + + void finish() throws IOException { + this.outputStream.finish(); + } + + @Override + public void close() throws IOException { + this.outputStream.close(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/ZipFileTarArchive.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/ZipFileTarArchive.java new file mode 100644 index 000000000000..0bab02e1f8cc --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/ZipFileTarArchive.java @@ -0,0 +1,109 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.io; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Enumeration; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.apache.commons.compress.archivers.tar.TarConstants; +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipFile; + +import org.springframework.util.Assert; +import org.springframework.util.StreamUtils; + +/** + * Adapter class to convert a ZIP file to a {@link TarArchive}. + * + * @author Phillip Webb + * @author Scott Frederick + * @since 2.3.0 + */ +public class ZipFileTarArchive implements TarArchive { + + static final long NORMALIZED_MOD_TIME = TarArchive.NORMALIZED_TIME.toEpochMilli(); + + private final File zip; + + private final Owner owner; + + /** + * Creates an archive from the contents of the given {@code zip}. Each entry in the + * archive will be owned by the given {@code owner}. + * @param zip the zip to use as a source + * @param owner the owner of the tar entries + */ + public ZipFileTarArchive(File zip, Owner owner) { + Assert.notNull(zip, "Zip must not be null"); + Assert.notNull(owner, "Owner must not be null"); + assertArchiveHasEntries(zip); + this.zip = zip; + this.owner = owner; + } + + @Override + public void writeTo(OutputStream outputStream) throws IOException { + TarArchiveOutputStream tar = new TarArchiveOutputStream(outputStream); + tar.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); + try (ZipFile zipFile = new ZipFile(this.zip)) { + Enumeration entries = zipFile.getEntries(); + while (entries.hasMoreElements()) { + ZipArchiveEntry zipEntry = entries.nextElement(); + copy(zipEntry, zipFile.getInputStream(zipEntry), tar); + } + } + tar.finish(); + } + + private void assertArchiveHasEntries(File jarFile) { + try (ZipFile zipFile = new ZipFile(jarFile)) { + Assert.state(zipFile.getEntries().hasMoreElements(), () -> "File '" + jarFile + + "' is not compatible with buildpacks; ensure jar file is valid and launch script is not enabled"); + } + catch (IOException ex) { + throw new IllegalStateException("File '" + jarFile + "' is not readable", ex); + } + } + + private void copy(ZipArchiveEntry zipEntry, InputStream zip, TarArchiveOutputStream tar) throws IOException { + TarArchiveEntry tarEntry = convert(zipEntry); + tar.putArchiveEntry(tarEntry); + if (tarEntry.isFile()) { + StreamUtils.copyRange(zip, tar, 0, tarEntry.getSize()); + } + tar.closeArchiveEntry(); + } + + private TarArchiveEntry convert(ZipArchiveEntry zipEntry) { + byte linkFlag = (zipEntry.isDirectory()) ? TarConstants.LF_DIR : TarConstants.LF_NORMAL; + TarArchiveEntry tarEntry = new TarArchiveEntry(zipEntry.getName(), linkFlag, true); + tarEntry.setUserId(this.owner.getUid()); + tarEntry.setGroupId(this.owner.getGid()); + tarEntry.setModTime(NORMALIZED_MOD_TIME); + tarEntry.setMode(zipEntry.getUnixMode()); + if (!zipEntry.isDirectory()) { + tarEntry.setSize(zipEntry.getSize()); + } + return tarEntry; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/package-info.java new file mode 100644 index 000000000000..6e1750c6d43e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * IO classes and utilities. + */ +package org.springframework.boot.buildpack.platform.io; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/json/JsonStream.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/json/JsonStream.java new file mode 100644 index 000000000000..87dd8ecbcb9a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/json/JsonStream.java @@ -0,0 +1,91 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.json; + +import java.io.IOException; +import java.io.InputStream; +import java.util.function.Consumer; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * Utility class that allows JSON to be parsed and processed as it's received. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public class JsonStream { + + private final ObjectMapper objectMapper; + + /** + * Create a new {@link JsonStream} backed by the given object mapper. + * @param objectMapper the object mapper to use + */ + public JsonStream(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + /** + * Stream {@link ObjectNode object nodes} from the content as they become available. + * @param content the source content + * @param consumer the {@link ObjectNode} consumer + * @throws IOException on IO error + */ + public void get(InputStream content, Consumer consumer) throws IOException { + get(content, ObjectNode.class, consumer); + } + + /** + * Stream objects from the content as they become available. + * @param the object type + * @param content the source content + * @param type the object type + * @param consumer the {@link ObjectNode} consumer + * @throws IOException on IO error + */ + public void get(InputStream content, Class type, Consumer consumer) throws IOException { + JsonFactory jsonFactory = this.objectMapper.getFactory(); + JsonParser parser = jsonFactory.createParser(content); + while (!parser.isClosed()) { + JsonToken token = parser.nextToken(); + if (token != null && token != JsonToken.END_OBJECT) { + T node = read(parser, type); + if (node != null) { + consumer.accept(node); + } + } + } + } + + @SuppressWarnings("unchecked") + private T read(JsonParser parser, Class type) throws IOException { + if (ObjectNode.class.isAssignableFrom(type)) { + ObjectNode node = this.objectMapper.readTree(parser); + if (node == null || node.isMissingNode() || node.isEmpty()) { + return null; + } + return (T) node; + } + return this.objectMapper.readValue(parser, type); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/json/MappedObject.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/json/MappedObject.java new file mode 100644 index 000000000000..1e51ef5e4b94 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/json/MappedObject.java @@ -0,0 +1,228 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.json; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.function.Function; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.springframework.util.Assert; + +/** + * Base class for mapped JSON objects. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public class MappedObject { + + private final JsonNode node; + + private final Lookup lookup; + + /** + * Create a new {@link MappedObject} instance. + * @param node the source node + * @param lookup method handle lookup + */ + protected MappedObject(JsonNode node, Lookup lookup) { + this.node = node; + this.lookup = lookup; + } + + /** + * Return the source node of the mapped object. + * @return the source node + */ + protected final JsonNode getNode() { + return this.node; + } + + /** + * Get the value at the given JSON path expression as a specific type. + * @param the data type + * @param expression the JSON path expression + * @param type the desired type. May be a simple JSON type or an interface + * @return the value + */ + protected T valueAt(String expression, Class type) { + return valueAt(this, this.node, this.lookup, expression, type); + } + + @SuppressWarnings("unchecked") + protected static T getRoot(Object proxy) { + MappedInvocationHandler handler = (MappedInvocationHandler) Proxy.getInvocationHandler(proxy); + return (T) handler.root; + } + + protected static T valueAt(Object proxy, String expression, Class type) { + MappedInvocationHandler handler = (MappedInvocationHandler) Proxy.getInvocationHandler(proxy); + return valueAt(handler.root, handler.node, handler.lookup, expression, type); + } + + @SuppressWarnings("unchecked") + private static T valueAt(MappedObject root, JsonNode node, Lookup lookup, String expression, Class type) { + JsonNode result = node.at(expression); + if (result.isMissingNode() && expression.startsWith("/") && expression.length() > 1 + && Character.isLowerCase(expression.charAt(1))) { + StringBuilder alternative = new StringBuilder(expression); + alternative.setCharAt(1, Character.toUpperCase(alternative.charAt(1))); + result = node.at(alternative.toString()); + } + if (type.isInterface() && !type.getName().startsWith("java")) { + return (T) Proxy.newProxyInstance(MappedObject.class.getClassLoader(), new Class[] { type }, + new MappedInvocationHandler(root, result, lookup)); + } + if (result.isMissingNode()) { + return null; + } + try { + return SharedObjectMapper.get().treeToValue(result, type); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + /** + * Factory method to create a new {@link MappedObject} instance. + * @param the mapped object type + * @param content the JSON content for the object + * @param factory a factory to create the mapped object from a {@link JsonNode} + * @return the mapped object + * @throws IOException on IO error + */ + protected static T of(String content, Function factory) throws IOException { + return of(content, ObjectMapper::readTree, factory); + } + + /** + * Factory method to create a new {@link MappedObject} instance. + * @param the mapped object type + * @param content the JSON content for the object + * @param factory a factory to create the mapped object from a {@link JsonNode} + * @return the mapped object + * @throws IOException on IO error + */ + protected static T of(InputStream content, Function factory) + throws IOException { + return of(content, ObjectMapper::readTree, factory); + } + + /** + * Factory method to create a new {@link MappedObject} instance. + * @param the mapped object type + * @param the content type + * @param content the JSON content for the object + * @param reader the content reader + * @param factory a factory to create the mapped object from a {@link JsonNode} + * @return the mapped object + * @throws IOException on IO error + */ + protected static T of(C content, ContentReader reader, Function factory) + throws IOException { + ObjectMapper objectMapper = SharedObjectMapper.get(); + JsonNode node = reader.read(objectMapper, content); + return factory.apply(node); + } + + /** + * Strategy used to read JSON content. + * + * @param the content type + */ + @FunctionalInterface + protected interface ContentReader { + + /** + * Read JSON content as a {@link JsonNode}. + * @param objectMapper the source object mapper + * @param content the content to read + * @return a {@link JsonNode} + * @throws IOException on IO error + */ + JsonNode read(ObjectMapper objectMapper, C content) throws IOException; + + } + + /** + * {@link InvocationHandler} used to support + * {@link MappedObject#valueAt(String, Class) valueAt} with {@code interface} types. + */ + private static class MappedInvocationHandler implements InvocationHandler { + + private static final String GET = "get"; + + private static final String IS = "is"; + + private final MappedObject root; + + private final JsonNode node; + + private final Lookup lookup; + + MappedInvocationHandler(MappedObject root, JsonNode node, Lookup lookup) { + this.root = root; + this.node = node; + this.lookup = lookup; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + Class declaringClass = method.getDeclaringClass(); + if (method.isDefault()) { + Lookup lookup = this.lookup.in(declaringClass); + MethodHandle methodHandle = lookup.unreflectSpecial(method, declaringClass).bindTo(proxy); + return methodHandle.invokeWithArguments(); + } + if (declaringClass == Object.class) { + method.invoke(proxy, args); + } + Assert.state(args == null || args.length == 0, () -> "Unsupported method " + method); + String name = getName(method.getName()); + Class type = method.getReturnType(); + return valueForProperty(name, type); + } + + private String getName(String name) { + StringBuilder result = new StringBuilder(name); + if (name.startsWith(GET)) { + result = new StringBuilder(name.substring(GET.length())); + } + if (name.startsWith(IS)) { + result = new StringBuilder(name.substring(IS.length())); + } + Assert.state(result.length() >= 0, "Missing name"); + result.setCharAt(0, Character.toLowerCase(result.charAt(0))); + return result.toString(); + } + + private Object valueForProperty(String name, Class type) { + return valueAt(this.root, this.node, this.lookup, "/" + name, type); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/json/SharedObjectMapper.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/json/SharedObjectMapper.java new file mode 100644 index 000000000000..6b71e53d45f8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/json/SharedObjectMapper.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.json; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; + +/** + * Provides access to a shared pre-configured {@link ObjectMapper}. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public final class SharedObjectMapper { + + private static final ObjectMapper INSTANCE; + + static { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(new ParameterNamesModule()); + objectMapper.enable(SerializationFeature.INDENT_OUTPUT); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + objectMapper.setPropertyNamingStrategy(PropertyNamingStrategies.LOWER_CAMEL_CASE); + INSTANCE = objectMapper; + } + + private SharedObjectMapper() { + } + + public static ObjectMapper get() { + return INSTANCE; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/json/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/json/package-info.java new file mode 100644 index 000000000000..c7b1f8b5602d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/json/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Utilities and classes for JSON processing. + */ +package org.springframework.boot.buildpack.platform.json; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/AbstractSocket.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/AbstractSocket.java new file mode 100644 index 000000000000..50cc2e73034c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/AbstractSocket.java @@ -0,0 +1,87 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.socket; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketAddress; + +/** + * Abstract base class for custom socket implementation. + * + * @author Phillip Webb + */ +class AbstractSocket extends Socket { + + @Override + public void connect(SocketAddress endpoint) throws IOException { + } + + @Override + public void connect(SocketAddress endpoint, int timeout) throws IOException { + } + + @Override + public boolean isConnected() { + return true; + } + + @Override + public boolean isBound() { + return true; + } + + @Override + public void shutdownInput() throws IOException { + throw new UnsupportedSocketOperationException(); + } + + @Override + public void shutdownOutput() throws IOException { + throw new UnsupportedSocketOperationException(); + } + + @Override + public InetAddress getInetAddress() { + return null; + } + + @Override + public InetAddress getLocalAddress() { + return null; + } + + @Override + public SocketAddress getLocalSocketAddress() { + return null; + } + + @Override + public SocketAddress getRemoteSocketAddress() { + return null; + } + + private static class UnsupportedSocketOperationException extends UnsupportedOperationException { + + UnsupportedSocketOperationException() { + super("Unsupported socket operation"); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/BsdDomainSocket.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/BsdDomainSocket.java new file mode 100644 index 000000000000..4fdfeea64200 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/BsdDomainSocket.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.socket; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; + +import com.sun.jna.LastErrorException; +import com.sun.jna.Native; +import com.sun.jna.Platform; +import com.sun.jna.Structure; + +import org.springframework.util.Assert; + +/** + * {@link DomainSocket} implementation for BSD based platforms. + * + * @author Phillip Webb + */ +class BsdDomainSocket extends DomainSocket { + + private static final int MAX_PATH_LENGTH = 104; + + static { + Native.register(Platform.C_LIBRARY_NAME); + } + + BsdDomainSocket(String path) throws IOException { + super(path); + } + + @Override + protected void connect(String path, int handle) { + SockaddrUn address = new SockaddrUn(AF_LOCAL, path.getBytes(StandardCharsets.UTF_8)); + connect(handle, address, address.size()); + } + + private native int connect(int fd, SockaddrUn address, int addressLen) throws LastErrorException; + + /** + * Native {@code sockaddr_un} structure as defined in {@code sys/un.h}. + */ + public static class SockaddrUn extends Structure implements Structure.ByReference { + + public byte sunLen; + + public byte sunFamily; + + public byte[] sunPath = new byte[MAX_PATH_LENGTH]; + + private SockaddrUn(byte sunFamily, byte[] path) { + Assert.isTrue(path.length < MAX_PATH_LENGTH, () -> "Path cannot exceed " + MAX_PATH_LENGTH + " bytes"); + System.arraycopy(path, 0, this.sunPath, 0, path.length); + this.sunPath[path.length] = 0; + this.sunLen = (byte) (fieldOffset("sunPath") + path.length); + this.sunFamily = sunFamily; + allocateMemory(); + } + + @Override + protected List getFieldOrder() { + return Arrays.asList(new String[] { "sunLen", "sunFamily", "sunPath" }); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/DomainSocket.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/DomainSocket.java new file mode 100644 index 000000000000..96f1c4f8c17c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/DomainSocket.java @@ -0,0 +1,195 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.socket; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.ByteBuffer; + +import com.sun.jna.LastErrorException; +import com.sun.jna.Native; +import com.sun.jna.Platform; + +import org.springframework.boot.buildpack.platform.socket.FileDescriptor.Handle; + +/** + * A {@link Socket} implementation for Linux of BSD domain sockets. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public abstract class DomainSocket extends AbstractSocket { + + private static final int SHUT_RD = 0; + + private static final int SHUT_WR = 1; + + protected static final int PF_LOCAL = 1; + + protected static final byte AF_LOCAL = 1; + + protected static final int SOCK_STREAM = 1; + + private final FileDescriptor fileDescriptor; + + private final InputStream inputStream; + + private final OutputStream outputStream; + + static { + Native.register(Platform.C_LIBRARY_NAME); + } + + DomainSocket(String path) throws IOException { + try { + this.fileDescriptor = open(path); + this.inputStream = new DomainSocketInputStream(); + this.outputStream = new DomainSocketOutputStream(); + } + catch (LastErrorException ex) { + throw new IOException(ex); + } + } + + private FileDescriptor open(String path) { + int handle = socket(PF_LOCAL, SOCK_STREAM, 0); + connect(path, handle); + return new FileDescriptor(handle, this::close); + } + + private int read(ByteBuffer buffer) throws IOException { + try (Handle handle = this.fileDescriptor.acquire()) { + if (handle.isClosed()) { + return -1; + } + try { + return read(handle.intValue(), buffer, buffer.remaining()); + } + catch (LastErrorException ex) { + throw new IOException(ex); + } + } + } + + public void write(ByteBuffer buffer) throws IOException { + try (Handle handle = this.fileDescriptor.acquire()) { + if (!handle.isClosed()) { + try { + write(handle.intValue(), buffer, buffer.remaining()); + } + catch (LastErrorException ex) { + throw new IOException(ex); + } + } + } + } + + @Override + public InputStream getInputStream() { + return this.inputStream; + } + + @Override + public OutputStream getOutputStream() { + return this.outputStream; + } + + @Override + public void close() throws IOException { + super.close(); + try { + this.fileDescriptor.close(); + } + catch (LastErrorException ex) { + throw new IOException(ex); + } + } + + protected abstract void connect(String path, int handle); + + private native int socket(int domain, int type, int protocol) throws LastErrorException; + + private native int read(int fd, ByteBuffer buffer, int count) throws LastErrorException; + + private native int write(int fd, ByteBuffer buffer, int count) throws LastErrorException; + + private native int close(int fd) throws LastErrorException; + + /** + * Return a new {@link DomainSocket} for the given path. + * @param path the path to the domain socket + * @return a {@link DomainSocket} instance + * @throws IOException if the socket cannot be opened + */ + public static DomainSocket get(String path) throws IOException { + if (Platform.isMac() || isBsdPlatform()) { + return new BsdDomainSocket(path); + } + return new LinuxDomainSocket(path); + } + + private static boolean isBsdPlatform() { + return Platform.isFreeBSD() || Platform.iskFreeBSD() || Platform.isNetBSD() || Platform.isOpenBSD(); + } + + /** + * {@link InputStream} returned from the {@link DomainSocket}. + */ + private class DomainSocketInputStream extends InputStream { + + @Override + public int read() throws IOException { + ByteBuffer buffer = ByteBuffer.allocate(1); + int amountRead = DomainSocket.this.read(buffer); + return (amountRead != 1) ? -1 : buffer.get() & 0xFF; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (len == 0) { + return 0; + } + int amountRead = DomainSocket.this.read(ByteBuffer.wrap(b, off, len)); + return (amountRead > 0) ? amountRead : -1; + } + + } + + /** + * {@link OutputStream} returned from the {@link DomainSocket}. + */ + private class DomainSocketOutputStream extends OutputStream { + + @Override + public void write(int b) throws IOException { + ByteBuffer buffer = ByteBuffer.allocate(1); + buffer.put(0, (byte) (b & 0xFF)); + DomainSocket.this.write(buffer); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + if (len != 0) { + DomainSocket.this.write(ByteBuffer.wrap(b, off, len)); + } + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/FileDescriptor.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/FileDescriptor.java new file mode 100644 index 000000000000..c2ab74e0a8b6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/FileDescriptor.java @@ -0,0 +1,117 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.socket; + +import java.io.Closeable; +import java.io.IOException; +import java.util.function.IntConsumer; + +/** + * Provides access to the underlying file system representation of an open file. + * + * @author Phillip Webb + * @see #acquire() + */ +class FileDescriptor { + + private final Handle openHandle; + + private final Handle closedHandler; + + private final IntConsumer closer; + + private Status status = Status.OPEN; + + private int referenceCount; + + FileDescriptor(int handle, IntConsumer closer) { + this.openHandle = new Handle(handle); + this.closedHandler = new Handle(-1); + this.closer = closer; + } + + /** + * Acquire an instance of the actual {@link Handle}. The caller must + * {@link Handle#close() close} the resulting handle when done. + * @return the handle + */ + synchronized Handle acquire() { + this.referenceCount++; + return (this.status != Status.OPEN) ? this.closedHandler : this.openHandle; + } + + private synchronized void release() { + this.referenceCount--; + if (this.referenceCount == 0 && this.status == Status.CLOSE_PENDING) { + this.closer.accept(this.openHandle.value); + this.status = Status.CLOSED; + } + } + + /** + * Close the underlying file when all handles have been released. + */ + synchronized void close() { + if (this.status == Status.OPEN) { + if (this.referenceCount == 0) { + this.closer.accept(this.openHandle.value); + this.status = Status.CLOSED; + } + else { + this.status = Status.CLOSE_PENDING; + } + } + } + + /** + * The status of the file descriptor. + */ + private enum Status { + + OPEN, CLOSE_PENDING, CLOSED + + } + + /** + * Provides access to the actual file descriptor handle. + */ + final class Handle implements Closeable { + + private final int value; + + private Handle(int value) { + this.value = value; + } + + boolean isClosed() { + return this.value == -1; + } + + int intValue() { + return this.value; + } + + @Override + public void close() throws IOException { + if (!isClosed()) { + release(); + } + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/LinuxDomainSocket.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/LinuxDomainSocket.java new file mode 100644 index 000000000000..24950d6c9fc1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/LinuxDomainSocket.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.socket; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; + +import com.sun.jna.LastErrorException; +import com.sun.jna.Native; +import com.sun.jna.Platform; +import com.sun.jna.Structure; + +import org.springframework.util.Assert; + +/** + * {@link DomainSocket} implementation for Linux based platforms. + * + * @author Phillip Webb + */ +class LinuxDomainSocket extends DomainSocket { + + static { + Native.register(Platform.C_LIBRARY_NAME); + } + + LinuxDomainSocket(String path) throws IOException { + super(path); + } + + private static final int MAX_PATH_LENGTH = 108; + + @Override + protected void connect(String path, int handle) { + SockaddrUn address = new SockaddrUn(AF_LOCAL, path.getBytes(StandardCharsets.UTF_8)); + connect(handle, address, address.size()); + } + + private native int connect(int fd, SockaddrUn address, int addressLen) throws LastErrorException; + + /** + * Native {@code sockaddr_un} structure as defined in {@code sys/un.h}. + */ + public static class SockaddrUn extends Structure implements Structure.ByReference { + + public short sunFamily; + + public byte[] sunPath = new byte[MAX_PATH_LENGTH]; + + private SockaddrUn(byte sunFamily, byte[] path) { + Assert.isTrue(path.length < MAX_PATH_LENGTH, () -> "Path cannot exceed " + MAX_PATH_LENGTH + " bytes"); + System.arraycopy(path, 0, this.sunPath, 0, path.length); + this.sunPath[path.length] = 0; + this.sunFamily = sunFamily; + allocateMemory(); + } + + @Override + protected List getFieldOrder() { + return Arrays.asList(new String[] { "sunFamily", "sunPath" }); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/NamedPipeSocket.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/NamedPipeSocket.java new file mode 100644 index 000000000000..edaa77392e8e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/NamedPipeSocket.java @@ -0,0 +1,212 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.socket; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousByteChannel; +import java.nio.channels.AsynchronousCloseException; +import java.nio.channels.AsynchronousFileChannel; +import java.nio.channels.Channels; +import java.nio.channels.CompletionHandler; +import java.nio.file.FileSystemException; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import com.sun.jna.Platform; +import com.sun.jna.platform.win32.Kernel32; + +/** + * A {@link Socket} implementation for named pipes. + * + * @author Phillip Webb + * @author Scott Frederick + * @since 2.3.0 + */ +public class NamedPipeSocket extends Socket { + + private static final int WAIT_INTERVAL = 100; + + private static final long TIMEOUT = TimeUnit.MILLISECONDS.toNanos(1000); + + private final AsynchronousFileByteChannel channel; + + NamedPipeSocket(String path) throws IOException { + this.channel = open(path); + } + + private AsynchronousFileByteChannel open(String path) throws IOException { + Consumer awaiter = Platform.isWindows() ? new WindowsAwaiter() : new SleepAwaiter(); + long startTime = System.nanoTime(); + while (true) { + try { + return new AsynchronousFileByteChannel(AsynchronousFileChannel.open(Paths.get(path), + StandardOpenOption.READ, StandardOpenOption.WRITE)); + } + catch (FileSystemException ex) { + if (System.nanoTime() - startTime >= TIMEOUT) { + throw ex; + } + awaiter.accept(path); + } + } + } + + @Override + public InputStream getInputStream() { + return Channels.newInputStream(this.channel); + } + + @Override + public OutputStream getOutputStream() { + return Channels.newOutputStream(this.channel); + } + + @Override + public void close() throws IOException { + if (this.channel != null) { + this.channel.close(); + } + } + + /** + * Return a new {@link NamedPipeSocket} for the given path. + * @param path the path to the domain socket + * @return a {@link NamedPipeSocket} instance + * @throws IOException if the socket cannot be opened + */ + public static NamedPipeSocket get(String path) throws IOException { + return new NamedPipeSocket(path); + } + + /** + * Adapt an {@code AsynchronousByteChannel} to an {@code AsynchronousFileChannel}. + */ + private static class AsynchronousFileByteChannel implements AsynchronousByteChannel { + + private final AsynchronousFileChannel fileChannel; + + AsynchronousFileByteChannel(AsynchronousFileChannel fileChannel) { + this.fileChannel = fileChannel; + } + + @Override + public void read(ByteBuffer dst, A attachment, CompletionHandler handler) { + this.fileChannel.read(dst, 0, attachment, new CompletionHandler() { + + @Override + public void completed(Integer read, A attachment) { + handler.completed((read > 0) ? read : -1, attachment); + } + + @Override + public void failed(Throwable exc, A attachment) { + if (exc instanceof AsynchronousCloseException) { + handler.completed(-1, attachment); + return; + } + handler.failed(exc, attachment); + } + }); + + } + + @Override + public Future read(ByteBuffer dst) { + CompletableFutureHandler future = new CompletableFutureHandler(); + this.fileChannel.read(dst, 0, null, future); + return future; + } + + @Override + public void write(ByteBuffer src, A attachment, CompletionHandler handler) { + this.fileChannel.write(src, 0, attachment, handler); + } + + @Override + public Future write(ByteBuffer src) { + return this.fileChannel.write(src, 0); + } + + @Override + public void close() throws IOException { + this.fileChannel.close(); + } + + @Override + public boolean isOpen() { + return this.fileChannel.isOpen(); + } + + private static class CompletableFutureHandler extends CompletableFuture + implements CompletionHandler { + + @Override + public void completed(Integer read, Object attachment) { + complete((read > 0) ? read : -1); + } + + @Override + public void failed(Throwable exc, Object attachment) { + if (exc instanceof AsynchronousCloseException) { + complete(-1); + return; + } + completeExceptionally(exc); + } + + } + + } + + /** + * Waits for the name pipe file using a simple sleep. + */ + private static class SleepAwaiter implements Consumer { + + @Override + public void accept(String path) { + try { + Thread.sleep(WAIT_INTERVAL); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + + } + + /** + * Waits for the name pipe file using Windows specific logic. + */ + private static class WindowsAwaiter implements Consumer { + + @Override + public void accept(String path) { + Kernel32.INSTANCE.WaitNamedPipe(path, WAIT_INTERVAL); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/package-info.java new file mode 100644 index 000000000000..b4cc36f3a9ea --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Low-level {@link java.net.Socket} implementations required for local Docker access. + */ +package org.springframework.boot.buildpack.platform.socket; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/system/Environment.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/system/Environment.java new file mode 100644 index 000000000000..e687cb63217e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/system/Environment.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.system; + +/** + * Provides access to environment variable values. + * + * @author Scott Frederick + * @author Phillip Webb + * @since 2.3.0 + */ +@FunctionalInterface +public interface Environment { + + /** + * Standard {@link Environment} implementation backed by + * {@link System#getenv(String)}. + */ + Environment SYSTEM = System::getenv; + + /** + * Gets the value of the specified environment variable. + * @param name the name of the environment variable + * @return the string value of the variable, or {@code null} if the variable is not + * defined in the environment + */ + String get(String name); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/system/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/system/package-info.java new file mode 100644 index 000000000000..06fb86716d1f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/system/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * System abstractions. + */ +package org.springframework.boot.buildpack.platform.system; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ApiVersionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ApiVersionTests.java new file mode 100644 index 000000000000..e679b5bedd60 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ApiVersionTests.java @@ -0,0 +1,118 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link ApiVersion}. + * + * @author Phillip Webb + * @author Scott Frederick + */ +class ApiVersionTests { + + @Test + void parseWhenVersionIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> ApiVersion.parse(null)) + .withMessage("Value must not be empty"); + } + + @Test + void parseWhenVersionIsEmptyThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> ApiVersion.parse("")) + .withMessage("Value must not be empty"); + } + + @Test + void parseWhenVersionDoesNotMatchPatternThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> ApiVersion.parse("bad")) + .withMessage("Malformed version number 'bad'"); + } + + @Test + void parseReturnsVersion() { + ApiVersion version = ApiVersion.parse("1.2"); + assertThat(version.getMajor()).isEqualTo(1); + assertThat(version.getMinor()).isEqualTo(2); + } + + @Test + void assertSupportsWhenSupports() { + ApiVersion.parse("1.2").assertSupports(ApiVersion.parse("1.0")); + } + + @Test + void assertSupportsWhenDoesNotSupportThrowsException() { + assertThatIllegalStateException() + .isThrownBy(() -> ApiVersion.parse("1.2").assertSupports(ApiVersion.parse("1.3"))) + .withMessage("Detected platform API version '1.3' does not match supported version '1.2'"); + } + + @Test + void supportWhenSame() { + assertThat(supports("0.0", "0.0")).isTrue(); + assertThat(supports("0.1", "0.1")).isTrue(); + assertThat(supports("1.0", "1.0")).isTrue(); + assertThat(supports("1.1", "1.1")).isTrue(); + } + + @Test + void supportsWhenDifferentMajor() { + assertThat(supports("0.0", "1.0")).isFalse(); + assertThat(supports("1.0", "0.0")).isFalse(); + assertThat(supports("1.0", "2.0")).isFalse(); + assertThat(supports("2.0", "1.0")).isFalse(); + assertThat(supports("1.1", "2.1")).isFalse(); + assertThat(supports("2.1", "1.1")).isFalse(); + } + + @Test + void supportsWhenDifferentMinor() { + assertThat(supports("1.2", "1.1")).isTrue(); + assertThat(supports("1.2", "1.3")).isFalse(); + } + + @Test + void supportWhenMajorZeroAndDifferentMinor() { + assertThat(supports("0.2", "0.1")).isFalse(); + assertThat(supports("0.2", "0.3")).isFalse(); + } + + @Test + void toStringReturnsString() { + assertThat(ApiVersion.parse("1.2").toString()).isEqualTo("1.2"); + } + + @Test + void equalsAndHashCode() { + ApiVersion v12a = ApiVersion.parse("1.2"); + ApiVersion v12b = ApiVersion.parse("1.2"); + ApiVersion v13 = ApiVersion.parse("1.3"); + assertThat(v12a.hashCode()).isEqualTo(v12b.hashCode()); + assertThat(v12a).isEqualTo(v12a).isEqualTo(v12b).isNotEqualTo(v13); + } + + private boolean supports(String v1, String v2) { + return ApiVersion.parse(v1).supports(ApiVersion.parse(v2)); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ApiVersionsTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ApiVersionsTests.java new file mode 100644 index 000000000000..421f2a191062 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ApiVersionsTests.java @@ -0,0 +1,82 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link ApiVersions}. + * + * @author Scott Frederick + */ +class ApiVersionsTests { + + @Test + void findsLatestWhenOneMatchesMajor() { + ApiVersion version = ApiVersions.parse("1.1", "2.2").findLatestSupported("1.0"); + assertThat(version).isEqualTo(ApiVersion.parse("1.1")); + } + + @Test + void findsLatestWhenOneMatchesWithReleaseVersions() { + ApiVersion version = ApiVersions.parse("1.1", "1.2").findLatestSupported("1.1"); + assertThat(version).isEqualTo(ApiVersion.parse("1.2")); + } + + @Test + void findsLatestWhenOneMatchesWithPreReleaseVersions() { + ApiVersion version = ApiVersions.parse("0.2", "0.3").findLatestSupported("0.2"); + assertThat(version).isEqualTo(ApiVersion.parse("0.2")); + } + + @Test + void findsLatestWhenMultipleMatchesWithReleaseVersions() { + ApiVersion version = ApiVersions.parse("1.1", "1.2").findLatestSupported("1.1", "1.2"); + assertThat(version).isEqualTo(ApiVersion.parse("1.2")); + } + + @Test + void findsLatestWhenMultipleMatchesWithPreReleaseVersions() { + ApiVersion version = ApiVersions.parse("0.2", "0.3").findLatestSupported("0.2", "0.3"); + assertThat(version).isEqualTo(ApiVersion.parse("0.3")); + } + + @Test + void findLatestWhenNoneSupportedThrowsException() { + assertThatIllegalStateException() + .isThrownBy(() -> ApiVersions.parse("1.1", "1.2").findLatestSupported("1.3", "1.4")).withMessage( + "Detected platform API versions '1.3,1.4' are not included in supported versions '1.1,1.2'"); + } + + @Test + void toStringReturnsString() { + assertThat(ApiVersions.parse("1.1", "2.2", "3.3").toString()).isEqualTo("1.1,2.2,3.3"); + } + + @Test + void equalsAndHashCode() { + ApiVersions v12a = ApiVersions.parse("1.2", "2.3"); + ApiVersions v12b = ApiVersions.parse("1.2", "2.3"); + ApiVersions v13 = ApiVersions.parse("1.3", "2.4"); + assertThat(v12a.hashCode()).isEqualTo(v12b.hashCode()); + assertThat(v12a).isEqualTo(v12a).isEqualTo(v12b).isNotEqualTo(v13); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildLogTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildLogTests.java new file mode 100644 index 000000000000..bf4ecd254618 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildLogTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link BuildLog}. + * + * @author Phillip Webb + */ +class BuildLogTests { + + @Test + void toSystemOutPrintsToSystemOut() { + BuildLog log = BuildLog.toSystemOut(); + assertThat(log).isInstanceOf(PrintStreamBuildLog.class); + assertThat(log).extracting("out").isSameAs(System.out); + } + + @Test + void toPrintsToOutput() { + BuildLog log = BuildLog.to(System.err); + assertThat(log).isInstanceOf(PrintStreamBuildLog.class); + assertThat(log).extracting("out").isSameAs(System.err); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildOwnerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildOwnerTests.java new file mode 100644 index 000000000000..44697e9c4ff4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildOwnerTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link BuildOwner}. + * + * @author Phillip Webb + * @author Andy Wilkinson + */ +class BuildOwnerTests { + + @Test + void fromEnvReturnsOwner() { + Map env = new LinkedHashMap<>(); + env.put("CNB_USER_ID", "123"); + env.put("CNB_GROUP_ID", "456"); + BuildOwner owner = BuildOwner.fromEnv(env); + assertThat(owner.getUid()).isEqualTo(123); + assertThat(owner.getGid()).isEqualTo(456); + assertThat(owner.toString()).isEqualTo("123/456"); + } + + @Test + void fromEnvWhenEnvIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> BuildOwner.fromEnv(null)) + .withMessage("Env must not be null"); + } + + @Test + void fromEnvWhenUserPropertyIsMissingThrowsException() { + Map env = new LinkedHashMap<>(); + env.put("CNB_GROUP_ID", "456"); + assertThatIllegalStateException().isThrownBy(() -> BuildOwner.fromEnv(env)) + .withMessage("Missing 'CNB_USER_ID' value from the builder environment '" + env + "'"); + } + + @Test + void fromEnvWhenGroupPropertyIsMissingThrowsException() { + Map env = new LinkedHashMap<>(); + env.put("CNB_USER_ID", "123"); + assertThatIllegalStateException().isThrownBy(() -> BuildOwner.fromEnv(env)) + .withMessage("Missing 'CNB_GROUP_ID' value from the builder environment '" + env + "'"); + } + + @Test + void fromEnvWhenUserPropertyIsMalformedThrowsException() { + Map env = new LinkedHashMap<>(); + env.put("CNB_USER_ID", "nope"); + env.put("CNB_GROUP_ID", "456"); + assertThatIllegalStateException().isThrownBy(() -> BuildOwner.fromEnv(env)) + .withMessage("Malformed 'CNB_USER_ID' value 'nope' in the builder environment '" + env + "'"); + } + + @Test + void fromEnvWhenGroupPropertyIsMalformedThrowsException() { + Map env = new LinkedHashMap<>(); + env.put("CNB_USER_ID", "123"); + env.put("CNB_GROUP_ID", "nope"); + assertThatIllegalStateException().isThrownBy(() -> BuildOwner.fromEnv(env)) + .withMessage("Malformed 'CNB_GROUP_ID' value 'nope' in the builder environment '" + env + "'"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java new file mode 100644 index 000000000000..2a4a47ef412d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java @@ -0,0 +1,236 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.buildpack.platform.docker.type.Binding; +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 static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.entry; + +/** + * Tests for {@link BuildRequest}. + * + * @author Phillip Webb + * @author Scott Frederick + */ +class BuildRequestTests { + + @TempDir + File tempDir; + + @Test + void forJarFileReturnsRequest() throws IOException { + File jarFile = new File(this.tempDir, "my-app-0.0.1.jar"); + writeTestJarFile(jarFile); + BuildRequest request = BuildRequest.forJarFile(jarFile); + assertThat(request.getName().toString()).isEqualTo("docker.io/library/my-app:0.0.1"); + assertThat(request.getBuilder().toString()).isEqualTo("docker.io/" + BuildRequest.DEFAULT_BUILDER_IMAGE_NAME); + assertThat(request.getApplicationContent(Owner.ROOT)).satisfies(this::hasExpectedJarContent); + assertThat(request.getEnv()).isEmpty(); + } + + @Test + void forJarFileWithNameReturnsRequest() throws IOException { + File jarFile = new File(this.tempDir, "my-app-0.0.1.jar"); + writeTestJarFile(jarFile); + BuildRequest request = BuildRequest.forJarFile(ImageReference.of("test-app"), jarFile); + assertThat(request.getName().toString()).isEqualTo("docker.io/library/test-app:latest"); + assertThat(request.getBuilder().toString()).isEqualTo("docker.io/" + BuildRequest.DEFAULT_BUILDER_IMAGE_NAME); + assertThat(request.getApplicationContent(Owner.ROOT)).satisfies(this::hasExpectedJarContent); + assertThat(request.getEnv()).isEmpty(); + } + + @Test + void forJarFileWhenJarFileIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> BuildRequest.forJarFile(null)) + .withMessage("JarFile must not be null"); + } + + @Test + void forJarFileWhenJarFileIsMissingThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> BuildRequest.forJarFile(new File(this.tempDir, "missing.jar"))) + .withMessage("JarFile must exist"); + } + + @Test + void forJarFileWhenJarFileIsDirectoryThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> BuildRequest.forJarFile(this.tempDir)) + .withMessage("JarFile must be a file"); + } + + @Test + void withBuilderUpdatesBuilder() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")) + .withBuilder(ImageReference.of("spring/builder")); + assertThat(request.getBuilder().toString()).isEqualTo("docker.io/spring/builder:latest"); + } + + @Test + void withBuilderWhenHasDigestUpdatesBuilder() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")).withBuilder(ImageReference + .of("spring/builder@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d")); + assertThat(request.getBuilder().toString()).isEqualTo( + "docker.io/spring/builder@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + } + + @Test + void withRunImageUpdatesRunImage() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")) + .withRunImage(ImageReference.of("example.com/custom/run-image:latest")); + assertThat(request.getRunImage().toString()).isEqualTo("example.com/custom/run-image:latest"); + } + + @Test + void withRunImageWhenHasDigestUpdatesRunImage() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")).withRunImage(ImageReference + .of("example.com/custom/run-image@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d")); + assertThat(request.getRunImage().toString()).isEqualTo( + "example.com/custom/run-image@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + } + + @Test + void withCreatorUpdatesCreator() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); + BuildRequest withCreator = request.withCreator(Creator.withVersion("1.0.0")); + assertThat(request.getCreator().getName()).isEqualTo("Spring Boot"); + assertThat(request.getCreator().getVersion()).isEqualTo(""); + assertThat(withCreator.getCreator().getName()).isEqualTo("Spring Boot"); + assertThat(withCreator.getCreator().getVersion()).isEqualTo("1.0.0"); + } + + @Test + void withEnvAddsEnvEntry() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); + BuildRequest withEnv = request.withEnv("spring", "boot"); + assertThat(request.getEnv()).isEmpty(); + assertThat(withEnv.getEnv()).containsExactly(entry("spring", "boot")); + } + + @Test + void withEnvMapAddsEnvEntries() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); + Map env = new LinkedHashMap<>(); + env.put("spring", "boot"); + env.put("test", "test"); + BuildRequest withEnv = request.withEnv(env); + assertThat(request.getEnv()).isEmpty(); + assertThat(withEnv.getEnv()).containsExactly(entry("spring", "boot"), entry("test", "test")); + } + + @Test + void withEnvWhenKeyIsNullThrowsException() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); + assertThatIllegalArgumentException().isThrownBy(() -> request.withEnv(null, "test")) + .withMessage("Name must not be empty"); + } + + @Test + void withEnvWhenValueIsNullThrowsException() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); + assertThatIllegalArgumentException().isThrownBy(() -> request.withEnv("test", null)) + .withMessage("Value must not be empty"); + } + + @Test + void withBuildpacksAddsBuildpacks() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); + BuildpackReference buildpackReference1 = BuildpackReference.of("example/buildpack1"); + BuildpackReference buildpackReference2 = BuildpackReference.of("example/buildpack2"); + BuildRequest withBuildpacks = request.withBuildpacks(buildpackReference1, buildpackReference2); + assertThat(request.getBuildpacks()).isEmpty(); + assertThat(withBuildpacks.getBuildpacks()).containsExactly(buildpackReference1, buildpackReference2); + } + + @Test + void withBuildpacksWhenBuildpacksIsNullThrowsException() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); + assertThatIllegalArgumentException().isThrownBy(() -> request.withBuildpacks((List) null)) + .withMessage("Buildpacks must not be null"); + } + + @Test + void withBindingsAddsBindings() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); + BuildRequest withBindings = request.withBindings(Binding.of("/host/path:/container/path:ro"), + Binding.of("volume-name:/container/path:rw")); + assertThat(request.getBindings()).isEmpty(); + assertThat(withBindings.getBindings()).containsExactly(Binding.of("/host/path:/container/path:ro"), + Binding.of("volume-name:/container/path:rw")); + } + + @Test + void withBindingsWhenBindingsIsNullThrowsException() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); + assertThatIllegalArgumentException().isThrownBy(() -> request.withBindings((List) null)) + .withMessage("Bindings must not be null"); + } + + private void hasExpectedJarContent(TarArchive archive) { + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + archive.writeTo(outputStream); + try (TarArchiveInputStream tar = new TarArchiveInputStream( + new ByteArrayInputStream(outputStream.toByteArray()))) { + assertThat(tar.getNextEntry().getName()).isEqualTo("spring/"); + assertThat(tar.getNextEntry().getName()).isEqualTo("spring/boot"); + assertThat(tar.getNextEntry()).isNull(); + } + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + private File writeTestJarFile(String name) throws IOException { + File file = new File(this.tempDir, name); + writeTestJarFile(file); + return file; + } + + private void writeTestJarFile(File file) throws IOException { + try (ZipArchiveOutputStream zip = new ZipArchiveOutputStream(file)) { + ZipArchiveEntry dirEntry = new ZipArchiveEntry("spring/"); + zip.putArchiveEntry(dirEntry); + zip.closeArchiveEntry(); + ZipArchiveEntry fileEntry = new ZipArchiveEntry("spring/boot"); + zip.putArchiveEntry(fileEntry); + zip.write("test".getBytes(StandardCharsets.UTF_8)); + zip.closeArchiveEntry(); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderBuildpackTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderBuildpackTests.java new file mode 100644 index 000000000000..0907c1a2c25b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderBuildpackTests.java @@ -0,0 +1,115 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.docker.type.Layer; +import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link BuilderBuildpack}. + * + * @author Scott Frederick + */ +class BuilderBuildpackTests extends AbstractJsonTests { + + private BuildpackResolverContext resolverContext; + + @BeforeEach + void setUp() throws Exception { + BuilderMetadata metadata = BuilderMetadata.fromJson(getContentAsString("builder-metadata.json")); + this.resolverContext = mock(BuildpackResolverContext.class); + given(this.resolverContext.getBuildpackMetadata()).willReturn(metadata.getBuildpacks()); + } + + @Test + void resolveWhenFullyQualifiedBuildpackWithVersionResolves() throws Exception { + BuildpackReference reference = BuildpackReference.of("urn:cnb:builder:paketo-buildpacks/spring-boot@3.5.0"); + Buildpack buildpack = BuilderBuildpack.resolve(this.resolverContext, reference); + assertThat(buildpack.getCoordinates()) + .isEqualTo(BuildpackCoordinates.of("paketo-buildpacks/spring-boot", "3.5.0")); + assertThatNoLayersAreAdded(buildpack); + } + + @Test + void resolveWhenFullyQualifiedBuildpackWithoutVersionResolves() throws Exception { + BuildpackReference reference = BuildpackReference.of("urn:cnb:builder:paketo-buildpacks/spring-boot"); + Buildpack buildpack = BuilderBuildpack.resolve(this.resolverContext, reference); + assertThat(buildpack.getCoordinates()) + .isEqualTo(BuildpackCoordinates.of("paketo-buildpacks/spring-boot", "3.5.0")); + assertThatNoLayersAreAdded(buildpack); + } + + @Test + void resolveWhenUnqualifiedBuildpackWithVersionResolves() throws Exception { + BuildpackReference reference = BuildpackReference.of("paketo-buildpacks/spring-boot@3.5.0"); + Buildpack buildpack = BuilderBuildpack.resolve(this.resolverContext, reference); + assertThat(buildpack.getCoordinates()) + .isEqualTo(BuildpackCoordinates.of("paketo-buildpacks/spring-boot", "3.5.0")); + assertThatNoLayersAreAdded(buildpack); + } + + @Test + void resolveWhenUnqualifiedBuildpackWithoutVersionResolves() throws Exception { + BuildpackReference reference = BuildpackReference.of("paketo-buildpacks/spring-boot"); + Buildpack buildpack = BuilderBuildpack.resolve(this.resolverContext, reference); + assertThat(buildpack.getCoordinates()) + .isEqualTo(BuildpackCoordinates.of("paketo-buildpacks/spring-boot", "3.5.0")); + assertThatNoLayersAreAdded(buildpack); + } + + @Test + void resolveWhenFullyQualifiedBuildpackWithVersionNotInBuilderThrowsException() { + BuildpackReference reference = BuildpackReference.of("urn:cnb:builder:example/buildpack1@1.2.3"); + assertThatIllegalArgumentException().isThrownBy(() -> BuilderBuildpack.resolve(this.resolverContext, reference)) + .withMessageContaining("'urn:cnb:builder:example/buildpack1@1.2.3'") + .withMessageContaining("not found in builder"); + } + + @Test + void resolveWhenFullyQualifiedBuildpackWithoutVersionNotInBuilderThrowsException() { + BuildpackReference reference = BuildpackReference.of("urn:cnb:builder:example/buildpack1"); + assertThatIllegalArgumentException().isThrownBy(() -> BuilderBuildpack.resolve(this.resolverContext, reference)) + .withMessageContaining("'urn:cnb:builder:example/buildpack1'") + .withMessageContaining("not found in builder"); + } + + @Test + void resolveWhenUnqualifiedBuildpackNotInBuilderReturnsNull() { + BuildpackReference reference = BuildpackReference.of("example/buildpack1@1.2.3"); + Buildpack buildpack = BuilderBuildpack.resolve(this.resolverContext, reference); + assertThat(buildpack).isNull(); + } + + private void assertThatNoLayersAreAdded(Buildpack buildpack) throws IOException { + List layers = new ArrayList<>(); + buildpack.apply(layers::add); + assertThat(layers).isEmpty(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderExceptionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderExceptionTests.java new file mode 100644 index 000000000000..2118b93e1d4f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderExceptionTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link BuilderException}. + * + * @author Scott Frederick + */ +class BuilderExceptionTests { + + @Test + void create() { + BuilderException exception = new BuilderException("detector", 1); + assertThat(exception.getOperation()).isEqualTo("detector"); + assertThat(exception.getStatusCode()).isEqualTo(1); + assertThat(exception.getMessage()).isEqualTo("Builder lifecycle 'detector' failed with status code 1"); + } + + @Test + void createWhenOperationIsNull() { + BuilderException exception = new BuilderException(null, 1); + assertThat(exception.getOperation()).isNull(); + assertThat(exception.getStatusCode()).isEqualTo(1); + assertThat(exception.getMessage()).isEqualTo("Builder failed with status code 1"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderMetadataTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderMetadataTests.java new file mode 100644 index 000000000000..ccb0ac23cd40 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderMetadataTests.java @@ -0,0 +1,132 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.io.IOException; +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.docker.type.Image; +import org.springframework.boot.buildpack.platform.docker.type.ImageConfig; +import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link BuilderMetadata}. + * + * @author Phillip Webb + * @author Scott Frederick + * @author Andy Wilkinson + */ +class BuilderMetadataTests extends AbstractJsonTests { + + @Test + void fromImageLoadsMetadata() throws IOException { + Image image = Image.of(getContent("image.json")); + BuilderMetadata metadata = BuilderMetadata.fromImage(image); + assertThat(metadata.getStack().getRunImage().getImage()).isEqualTo("cloudfoundry/run:base-cnb"); + assertThat(metadata.getStack().getRunImage().getMirrors()).isEmpty(); + assertThat(metadata.getLifecycle().getVersion()).isEqualTo("0.7.2"); + assertThat(metadata.getLifecycle().getApi().getBuildpack()).isEqualTo("0.2"); + assertThat(metadata.getLifecycle().getApi().getPlatform()).isEqualTo("0.3"); + assertThat(metadata.getCreatedBy().getName()).isEqualTo("Pack CLI"); + assertThat(metadata.getCreatedBy().getVersion()) + .isEqualTo("v0.9.0 (git sha: d42c384a39f367588f2653f2a99702db910e5ad7)"); + assertThat(metadata.getBuildpacks()).extracting(BuildpackMetadata::getId, BuildpackMetadata::getVersion) + .contains(tuple("paketo-buildpacks/java", "4.10.0")) + .contains(tuple("paketo-buildpacks/spring-boot", "3.5.0")) + .contains(tuple("paketo-buildpacks/executable-jar", "3.1.3")) + .contains(tuple("paketo-buildpacks/graalvm", "4.1.0")) + .contains(tuple("paketo-buildpacks/java-native-image", "4.7.0")) + .contains(tuple("paketo-buildpacks/spring-boot-native-image", "2.0.1")) + .contains(tuple("paketo-buildpacks/bellsoft-liberica", "6.2.0")); + } + + @Test + void fromImageWhenImageIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> BuilderMetadata.fromImage(null)) + .withMessage("Image must not be null"); + } + + @Test + void fromImageWhenImageConfigIsNullThrowsException() { + Image image = mock(Image.class); + assertThatIllegalArgumentException().isThrownBy(() -> BuilderMetadata.fromImage(image)) + .withMessage("ImageConfig must not be null"); + } + + @Test + void fromImageConfigWhenLabelIsMissingThrowsException() { + Image image = mock(Image.class); + ImageConfig imageConfig = mock(ImageConfig.class); + given(image.getConfig()).willReturn(imageConfig); + given(imageConfig.getLabels()).willReturn(Collections.singletonMap("alpha", "a")); + assertThatIllegalArgumentException().isThrownBy(() -> BuilderMetadata.fromImage(image)) + .withMessage("No 'io.buildpacks.builder.metadata' label found in image config labels 'alpha'"); + } + + @Test + void fromJsonLoadsMetadataWithoutSupportedApis() throws IOException { + BuilderMetadata metadata = BuilderMetadata.fromJson(getContentAsString("builder-metadata.json")); + assertThat(metadata.getStack().getRunImage().getImage()).isEqualTo("cloudfoundry/run:base-cnb"); + assertThat(metadata.getStack().getRunImage().getMirrors()).isEmpty(); + assertThat(metadata.getLifecycle().getVersion()).isEqualTo("0.7.2"); + assertThat(metadata.getLifecycle().getApi().getBuildpack()).isEqualTo("0.2"); + assertThat(metadata.getLifecycle().getApi().getPlatform()).isEqualTo("0.4"); + assertThat(metadata.getLifecycle().getApis().getBuildpack()).isNull(); + assertThat(metadata.getLifecycle().getApis().getPlatform()).isNull(); + } + + @Test + void fromJsonLoadsMetadataWithSupportedApis() throws IOException { + BuilderMetadata metadata = BuilderMetadata.fromJson(getContentAsString("builder-metadata-supported-apis.json")); + assertThat(metadata.getLifecycle().getVersion()).isEqualTo("0.7.2"); + assertThat(metadata.getLifecycle().getApi().getBuildpack()).isEqualTo("0.2"); + assertThat(metadata.getLifecycle().getApi().getPlatform()).isEqualTo("0.4"); + assertThat(metadata.getLifecycle().getApis().getBuildpack()).containsExactly("0.1", "0.2", "0.3"); + assertThat(metadata.getLifecycle().getApis().getPlatform()).containsExactly("0.3", "0.4"); + } + + @Test + void copyWithUpdatedCreatedByReturnsNewMetadata() throws IOException { + Image image = Image.of(getContent("image.json")); + BuilderMetadata metadata = BuilderMetadata.fromImage(image); + BuilderMetadata copy = metadata.copy((update) -> update.withCreatedBy("test123", "test456")); + assertThat(copy).isNotSameAs(metadata); + assertThat(copy.getCreatedBy().getName()).isEqualTo("test123"); + assertThat(copy.getCreatedBy().getVersion()).isEqualTo("test456"); + } + + @Test + void attachToUpdatesMetadata() throws IOException { + Image image = Image.of(getContent("image.json")); + ImageConfig imageConfig = image.getConfig(); + BuilderMetadata metadata = BuilderMetadata.fromImage(image); + ImageConfig imageConfigCopy = imageConfig.copy(metadata::attachTo); + String label = imageConfigCopy.getLabels().get("io.buildpacks.builder.metadata"); + BuilderMetadata metadataCopy = BuilderMetadata.fromJson(label); + assertThat(metadataCopy.getStack().getRunImage().getImage()) + .isEqualTo(metadata.getStack().getRunImage().getImage()); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java new file mode 100644 index 000000000000..fe1b5f08046d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java @@ -0,0 +1,423 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.net.URI; + +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.stubbing.Answer; + +import org.springframework.boot.buildpack.platform.docker.DockerApi; +import org.springframework.boot.buildpack.platform.docker.DockerApi.ContainerApi; +import org.springframework.boot.buildpack.platform.docker.DockerApi.ImageApi; +import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi; +import org.springframework.boot.buildpack.platform.docker.TotalProgressPullListener; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration; +import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException; +import org.springframework.boot.buildpack.platform.docker.type.ContainerReference; +import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus; +import org.springframework.boot.buildpack.platform.docker.type.Image; +import org.springframework.boot.buildpack.platform.docker.type.ImageArchive; +import org.springframework.boot.buildpack.platform.docker.type.ImageReference; +import org.springframework.boot.buildpack.platform.io.TarArchive; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; + +/** + * Tests for {@link Builder}. + * + * @author Phillip Webb + * @author Scott Frederick + */ +class BuilderTests { + + @Test + void createWhenLogIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new Builder((BuildLog) null)) + .withMessage("Log must not be null"); + } + + @Test + void createWithDockerConfiguration() { + Builder builder = new Builder(BuildLog.toSystemOut()); + assertThat(builder).isNotNull(); + } + + @Test + void buildWhenRequestIsNullThrowsException() { + Builder builder = new Builder(); + assertThatIllegalArgumentException().isThrownBy(() -> builder.build(null)) + .withMessage("Request must not be null"); + } + + @Test + void buildInvokesBuilder() throws Exception { + TestPrintStream out = new TestPrintStream(); + DockerApi docker = mockDockerApi(); + Image builderImage = loadImage("image.json"); + Image runImage = loadImage("run-image.json"); + given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(), isNull())) + .willAnswer(withPulledImage(builderImage)); + given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull())) + .willAnswer(withPulledImage(runImage)); + Builder builder = new Builder(BuildLog.to(out), docker, null); + BuildRequest request = getTestRequest(); + builder.build(request); + assertThat(out.toString()).contains("Running creator"); + assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); + ArgumentCaptor archive = ArgumentCaptor.forClass(ImageArchive.class); + then(docker.image()).should().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(), + isNull()); + then(docker.image()).should().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), + isNull()); + then(docker.image()).should().load(archive.capture(), any()); + then(docker.image()).should().remove(archive.getValue().getTag(), true); + then(docker.image()).shouldHaveNoMoreInteractions(); + } + + @Test + void buildInvokesBuilderAndPublishesImage() throws Exception { + TestPrintStream out = new TestPrintStream(); + DockerApi docker = mockDockerApi(); + Image builderImage = loadImage("image.json"); + Image runImage = loadImage("run-image.json"); + DockerConfiguration dockerConfiguration = new DockerConfiguration() + .withBuilderRegistryTokenAuthentication("builder token") + .withPublishRegistryTokenAuthentication("publish token"); + given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(), + eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()))) + .willAnswer(withPulledImage(builderImage)); + given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), + eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()))) + .willAnswer(withPulledImage(runImage)); + Builder builder = new Builder(BuildLog.to(out), docker, dockerConfiguration); + BuildRequest request = getTestRequest().withPublish(true); + builder.build(request); + assertThat(out.toString()).contains("Running creator"); + assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); + ArgumentCaptor archive = ArgumentCaptor.forClass(ImageArchive.class); + then(docker.image()).should().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(), + eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())); + then(docker.image()).should().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), + eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())); + then(docker.image()).should().push(eq(request.getName()), any(), + eq(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader())); + then(docker.image()).should().load(archive.capture(), any()); + then(docker.image()).should().remove(archive.getValue().getTag(), true); + then(docker.image()).shouldHaveNoMoreInteractions(); + } + + @Test + void buildInvokesBuilderWithDefaultImageTags() throws Exception { + TestPrintStream out = new TestPrintStream(); + DockerApi docker = mockDockerApi(); + Image builderImage = loadImage("image-with-no-run-image-tag.json"); + Image runImage = loadImage("run-image.json"); + given(docker.image().pull(eq(ImageReference.of("gcr.io/paketo-buildpacks/builder:latest")), any(), isNull())) + .willAnswer(withPulledImage(builderImage)); + given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:latest")), any(), isNull())) + .willAnswer(withPulledImage(runImage)); + Builder builder = new Builder(BuildLog.to(out), docker, null); + BuildRequest request = getTestRequest().withBuilder(ImageReference.of("gcr.io/paketo-buildpacks/builder")); + builder.build(request); + assertThat(out.toString()).contains("Running creator"); + assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); + ArgumentCaptor archive = ArgumentCaptor.forClass(ImageArchive.class); + then(docker.image()).should().load(archive.capture(), any()); + then(docker.image()).should().remove(archive.getValue().getTag(), true); + } + + @Test + void buildInvokesBuilderWithRunImageInDigestForm() throws Exception { + TestPrintStream out = new TestPrintStream(); + DockerApi docker = mockDockerApi(); + Image builderImage = loadImage("image-with-run-image-digest.json"); + Image runImage = loadImage("run-image.json"); + given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(), isNull())) + .willAnswer(withPulledImage(builderImage)); + given(docker.image().pull(eq(ImageReference.of( + "docker.io/cloudfoundry/run@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d")), + any(), isNull())).willAnswer(withPulledImage(runImage)); + Builder builder = new Builder(BuildLog.to(out), docker, null); + BuildRequest request = getTestRequest(); + builder.build(request); + assertThat(out.toString()).contains("Running creator"); + assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); + ArgumentCaptor archive = ArgumentCaptor.forClass(ImageArchive.class); + then(docker.image()).should().load(archive.capture(), any()); + then(docker.image()).should().remove(archive.getValue().getTag(), true); + } + + @Test + void buildInvokesBuilderWithRunImageFromRequest() throws Exception { + TestPrintStream out = new TestPrintStream(); + DockerApi docker = mockDockerApi(); + Image builderImage = loadImage("image.json"); + Image runImage = loadImage("run-image.json"); + given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(), isNull())) + .willAnswer(withPulledImage(builderImage)); + given(docker.image().pull(eq(ImageReference.of("example.com/custom/run:latest")), any(), isNull())) + .willAnswer(withPulledImage(runImage)); + Builder builder = new Builder(BuildLog.to(out), docker, null); + BuildRequest request = getTestRequest().withRunImage(ImageReference.of("example.com/custom/run:latest")); + builder.build(request); + assertThat(out.toString()).contains("Running creator"); + assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); + ArgumentCaptor archive = ArgumentCaptor.forClass(ImageArchive.class); + then(docker.image()).should().load(archive.capture(), any()); + then(docker.image()).should().remove(archive.getValue().getTag(), true); + } + + @Test + void buildInvokesBuilderWithNeverPullPolicy() throws Exception { + TestPrintStream out = new TestPrintStream(); + DockerApi docker = mockDockerApi(); + Image builderImage = loadImage("image.json"); + Image runImage = loadImage("run-image.json"); + given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(), isNull())) + .willAnswer(withPulledImage(builderImage)); + given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull())) + .willAnswer(withPulledImage(runImage)); + given(docker.image().inspect(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)))) + .willReturn(builderImage); + given(docker.image().inspect(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")))) + .willReturn(runImage); + Builder builder = new Builder(BuildLog.to(out), docker, null); + BuildRequest request = getTestRequest().withPullPolicy(PullPolicy.NEVER); + builder.build(request); + assertThat(out.toString()).contains("Running creator"); + assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); + ArgumentCaptor archive = ArgumentCaptor.forClass(ImageArchive.class); + then(docker.image()).should().load(archive.capture(), any()); + then(docker.image()).should().remove(archive.getValue().getTag(), true); + then(docker.image()).should(never()).pull(any(), any()); + then(docker.image()).should(times(2)).inspect(any()); + } + + @Test + void buildInvokesBuilderWithAlwaysPullPolicy() throws Exception { + TestPrintStream out = new TestPrintStream(); + DockerApi docker = mockDockerApi(); + Image builderImage = loadImage("image.json"); + Image runImage = loadImage("run-image.json"); + given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(), isNull())) + .willAnswer(withPulledImage(builderImage)); + given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull())) + .willAnswer(withPulledImage(runImage)); + given(docker.image().inspect(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)))) + .willReturn(builderImage); + given(docker.image().inspect(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")))) + .willReturn(runImage); + Builder builder = new Builder(BuildLog.to(out), docker, null); + BuildRequest request = getTestRequest().withPullPolicy(PullPolicy.ALWAYS); + builder.build(request); + assertThat(out.toString()).contains("Running creator"); + assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); + ArgumentCaptor archive = ArgumentCaptor.forClass(ImageArchive.class); + then(docker.image()).should().load(archive.capture(), any()); + then(docker.image()).should().remove(archive.getValue().getTag(), true); + then(docker.image()).should(times(2)).pull(any(), any(), isNull()); + then(docker.image()).should(never()).inspect(any()); + } + + @Test + void buildInvokesBuilderWithIfNotPresentPullPolicy() throws Exception { + TestPrintStream out = new TestPrintStream(); + DockerApi docker = mockDockerApi(); + Image builderImage = loadImage("image.json"); + Image runImage = loadImage("run-image.json"); + given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(), isNull())) + .willAnswer(withPulledImage(builderImage)); + given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull())) + .willAnswer(withPulledImage(runImage)); + given(docker.image().inspect(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)))).willThrow( + new DockerEngineException("docker://localhost/", new URI("example"), 404, "NOT FOUND", null, null)) + .willReturn(builderImage); + given(docker.image().inspect(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")))).willThrow( + new DockerEngineException("docker://localhost/", new URI("example"), 404, "NOT FOUND", null, null)) + .willReturn(runImage); + Builder builder = new Builder(BuildLog.to(out), docker, null); + BuildRequest request = getTestRequest().withPullPolicy(PullPolicy.IF_NOT_PRESENT); + builder.build(request); + assertThat(out.toString()).contains("Running creator"); + assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); + ArgumentCaptor archive = ArgumentCaptor.forClass(ImageArchive.class); + then(docker.image()).should().load(archive.capture(), any()); + then(docker.image()).should().remove(archive.getValue().getTag(), true); + then(docker.image()).should(times(2)).inspect(any()); + then(docker.image()).should(times(2)).pull(any(), any(), isNull()); + } + + @Test + void buildWhenStackIdDoesNotMatchThrowsException() throws Exception { + TestPrintStream out = new TestPrintStream(); + DockerApi docker = mockDockerApi(); + Image builderImage = loadImage("image.json"); + Image runImage = loadImage("run-image-with-bad-stack.json"); + given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(), isNull())) + .willAnswer(withPulledImage(builderImage)); + given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull())) + .willAnswer(withPulledImage(runImage)); + Builder builder = new Builder(BuildLog.to(out), docker, null); + BuildRequest request = getTestRequest(); + assertThatIllegalStateException().isThrownBy(() -> builder.build(request)).withMessage( + "Run image stack 'org.cloudfoundry.stacks.cfwindowsfs3' does not match builder stack 'io.buildpacks.stacks.bionic'"); + } + + @Test + void buildWhenBuilderReturnsErrorThrowsException() throws Exception { + TestPrintStream out = new TestPrintStream(); + DockerApi docker = mockDockerApiLifecycleError(); + Image builderImage = loadImage("image.json"); + Image runImage = loadImage("run-image.json"); + given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(), isNull())) + .willAnswer(withPulledImage(builderImage)); + given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull())) + .willAnswer(withPulledImage(runImage)); + Builder builder = new Builder(BuildLog.to(out), docker, null); + BuildRequest request = getTestRequest(); + assertThatExceptionOfType(BuilderException.class).isThrownBy(() -> builder.build(request)) + .withMessage("Builder lifecycle 'creator' failed with status code 9"); + } + + @Test + void buildWhenDetectedRunImageInDifferentAuthenticatedRegistryThrowsException() throws Exception { + TestPrintStream out = new TestPrintStream(); + DockerApi docker = mockDockerApi(); + Image builderImage = loadImage("image-with-run-image-different-registry.json"); + DockerConfiguration dockerConfiguration = new DockerConfiguration() + .withBuilderRegistryTokenAuthentication("builder token"); + given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(), + eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()))) + .willAnswer(withPulledImage(builderImage)); + Builder builder = new Builder(BuildLog.to(out), docker, dockerConfiguration); + BuildRequest request = getTestRequest(); + assertThatIllegalStateException().isThrownBy(() -> builder.build(request)).withMessage( + "Run image 'example.com/custom/run:latest' must be pulled from the 'docker.io' authenticated registry"); + } + + @Test + void buildWhenRequestedRunImageInDifferentAuthenticatedRegistryThrowsException() throws Exception { + TestPrintStream out = new TestPrintStream(); + DockerApi docker = mockDockerApi(); + Image builderImage = loadImage("image.json"); + DockerConfiguration dockerConfiguration = new DockerConfiguration() + .withBuilderRegistryTokenAuthentication("builder token"); + given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(), + eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()))) + .willAnswer(withPulledImage(builderImage)); + Builder builder = new Builder(BuildLog.to(out), docker, dockerConfiguration); + BuildRequest request = getTestRequest().withRunImage(ImageReference.of("example.com/custom/run:latest")); + assertThatIllegalStateException().isThrownBy(() -> builder.build(request)).withMessage( + "Run image 'example.com/custom/run:latest' must be pulled from the 'docker.io' authenticated registry"); + } + + @Test + void buildWhenRequestedBuildpackNotInBuilderThrowsException() throws Exception { + TestPrintStream out = new TestPrintStream(); + DockerApi docker = mockDockerApiLifecycleError(); + Image builderImage = loadImage("image.json"); + Image runImage = loadImage("run-image.json"); + given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(), isNull())) + .willAnswer(withPulledImage(builderImage)); + given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull())) + .willAnswer(withPulledImage(runImage)); + Builder builder = new Builder(BuildLog.to(out), docker, null); + BuildpackReference reference = BuildpackReference.of("urn:cnb:builder:example/buildpack@1.2.3"); + BuildRequest request = getTestRequest().withBuildpacks(reference); + assertThatIllegalArgumentException().isThrownBy(() -> builder.build(request)) + .withMessageContaining("'urn:cnb:builder:example/buildpack@1.2.3'") + .withMessageContaining("not found in builder"); + } + + private DockerApi mockDockerApi() throws IOException { + ContainerApi containerApi = mock(ContainerApi.class); + ContainerReference reference = ContainerReference.of("container-ref"); + given(containerApi.create(any(), any())).willReturn(reference); + given(containerApi.wait(eq(reference))).willReturn(ContainerStatus.of(0, null)); + ImageApi imageApi = mock(ImageApi.class); + VolumeApi volumeApi = mock(VolumeApi.class); + DockerApi docker = mock(DockerApi.class); + given(docker.image()).willReturn(imageApi); + given(docker.container()).willReturn(containerApi); + given(docker.volume()).willReturn(volumeApi); + return docker; + } + + private DockerApi mockDockerApiLifecycleError() throws IOException { + ContainerApi containerApi = mock(ContainerApi.class); + ContainerReference reference = ContainerReference.of("container-ref"); + given(containerApi.create(any(), any())).willReturn(reference); + given(containerApi.wait(eq(reference))).willReturn(ContainerStatus.of(9, null)); + ImageApi imageApi = mock(ImageApi.class); + VolumeApi volumeApi = mock(VolumeApi.class); + DockerApi docker = mock(DockerApi.class); + given(docker.image()).willReturn(imageApi); + given(docker.container()).willReturn(containerApi); + given(docker.volume()).willReturn(volumeApi); + return docker; + } + + private BuildRequest getTestRequest() { + TarArchive content = mock(TarArchive.class); + ImageReference name = ImageReference.of("my-application"); + return BuildRequest.of(name, (owner) -> content); + } + + private Image loadImage(String name) throws IOException { + return Image.of(getClass().getResourceAsStream(name)); + } + + private Answer withPulledImage(Image image) { + return (invocation) -> { + TotalProgressPullListener listener = invocation.getArgument(1, TotalProgressPullListener.class); + listener.onStart(); + listener.onFinish(); + return image; + }; + + } + + static class TestPrintStream extends PrintStream { + + TestPrintStream() { + super(new ByteArrayOutputStream()); + } + + @Override + public String toString() { + return this.out.toString(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackCoordinatesTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackCoordinatesTests.java new file mode 100644 index 000000000000..ef7711aef36f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackCoordinatesTests.java @@ -0,0 +1,172 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link BuildpackCoordinates}. + * + * @author Scott Frederick + * @author Phillip Webb + */ +class BuildpackCoordinatesTests extends AbstractJsonTests { + + private final Path archive = Paths.get("/buildpack/path"); + + @Test + void fromToml() throws IOException { + BuildpackCoordinates coordinates = BuildpackCoordinates + .fromToml(createTomlStream("example/buildpack1", "0.0.1", true, false), this.archive); + assertThat(coordinates.getId()).isEqualTo("example/buildpack1"); + assertThat(coordinates.getVersion()).isEqualTo("0.0.1"); + } + + @Test + void fromTomlWhenMissingDescriptorThrowsException() throws Exception { + ByteArrayInputStream coordinates = new ByteArrayInputStream("".getBytes()); + assertThatIllegalArgumentException().isThrownBy(() -> BuildpackCoordinates.fromToml(coordinates, this.archive)) + .withMessageContaining("Buildpack descriptor 'buildpack.toml' is required") + .withMessageContaining(this.archive.toString()); + } + + @Test + void fromTomlWhenMissingIDThrowsException() throws Exception { + InputStream coordinates = createTomlStream(null, null, true, false); + assertThatIllegalArgumentException().isThrownBy(() -> BuildpackCoordinates.fromToml(coordinates, this.archive)) + .withMessageContaining("Buildpack descriptor must contain ID") + .withMessageContaining(this.archive.toString()); + } + + @Test + void fromTomlWhenMissingVersionThrowsException() throws Exception { + InputStream coordinates = createTomlStream("example/buildpack1", null, true, false); + assertThatIllegalArgumentException().isThrownBy(() -> BuildpackCoordinates.fromToml(coordinates, this.archive)) + .withMessageContaining("Buildpack descriptor must contain version") + .withMessageContaining(this.archive.toString()); + } + + @Test + void fromTomlWhenMissingStacksAndOrderThrowsException() throws Exception { + InputStream coordinates = createTomlStream("example/buildpack1", "0.0.1", false, false); + assertThatIllegalArgumentException().isThrownBy(() -> BuildpackCoordinates.fromToml(coordinates, this.archive)) + .withMessageContaining("Buildpack descriptor must contain either 'stacks' or 'order'") + .withMessageContaining(this.archive.toString()); + } + + @Test + void fromTomlWhenContainsBothStacksAndOrderThrowsException() throws Exception { + InputStream coordinates = createTomlStream("example/buildpack1", "0.0.1", true, true); + assertThatIllegalArgumentException().isThrownBy(() -> BuildpackCoordinates.fromToml(coordinates, this.archive)) + .withMessageContaining("Buildpack descriptor must not contain both 'stacks' and 'order'") + .withMessageContaining(this.archive.toString()); + } + + @Test + void fromBuildpackMetadataWhenMetadataIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> BuildpackCoordinates.fromBuildpackMetadata(null)) + .withMessage("BuildpackMetadata must not be null"); + } + + @Test + void fromBuildpackMetadataReturnsCoordinates() throws Exception { + BuildpackMetadata metadata = BuildpackMetadata.fromJson(getContentAsString("buildpack-metadata.json")); + BuildpackCoordinates coordinates = BuildpackCoordinates.fromBuildpackMetadata(metadata); + assertThat(coordinates.getId()).isEqualTo("example/hello-universe"); + assertThat(coordinates.getVersion()).isEqualTo("0.0.1"); + } + + @Test + void ofWhenIdIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> BuildpackCoordinates.of(null, null)) + .withMessage("ID must not be empty"); + } + + @Test + void ofReturnsCoordinates() { + BuildpackCoordinates coordinates = BuildpackCoordinates.of("id", "1"); + assertThat(coordinates).hasToString("id@1"); + } + + @Test + void getIdReturnsId() { + BuildpackCoordinates coordinates = BuildpackCoordinates.of("id", "1"); + assertThat(coordinates.getId()).isEqualTo("id"); + } + + @Test + void getVersionReturnsVersion() { + BuildpackCoordinates coordinates = BuildpackCoordinates.of("id", "1"); + assertThat(coordinates.getVersion()).isEqualTo("1"); + } + + @Test + void getVersionWhenVersionIsNullReturnsNull() { + BuildpackCoordinates coordinates = BuildpackCoordinates.of("id", null); + assertThat(coordinates.getVersion()).isNull(); + } + + @Test + void toStringReturnsNiceString() { + BuildpackCoordinates coordinates = BuildpackCoordinates.of("id", "1"); + assertThat(coordinates).hasToString("id@1"); + } + + @Test + void equalsAndHashCode() { + BuildpackCoordinates c1a = BuildpackCoordinates.of("id", "1"); + BuildpackCoordinates c1b = BuildpackCoordinates.of("id", "1"); + BuildpackCoordinates c2 = BuildpackCoordinates.of("id", "2"); + assertThat(c1a).isEqualTo(c1a).isEqualTo(c1b).isNotEqualTo(c2); + assertThat(c1a.hashCode()).isEqualTo(c1b.hashCode()); + } + + private InputStream createTomlStream(String id, String version, boolean includeStacks, boolean includeOrder) { + StringBuilder builder = new StringBuilder(); + builder.append("[buildpack]\n"); + if (id != null) { + builder.append("id = \"").append(id).append("\"\n"); + } + if (version != null) { + builder.append("version = \"").append(version).append("\"\n"); + } + builder.append("name = \"Example buildpack\"\n"); + builder.append("homepage = \"https://github.com/example/example-buildpack\"\n"); + if (includeStacks) { + builder.append("[[stacks]]\n"); + builder.append("id = \"io.buildpacks.stacks.bionic\"\n"); + } + if (includeOrder) { + builder.append("[[order]]\n"); + builder.append("group = [ { id = \"example/buildpack2\", version=\"0.0.2\" } ]\n"); + } + return new ByteArrayInputStream(builder.toString().getBytes(StandardCharsets.UTF_8)); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackMetadataTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackMetadataTests.java new file mode 100644 index 000000000000..413fd2100e87 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackMetadataTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.io.IOException; +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.docker.type.Image; +import org.springframework.boot.buildpack.platform.docker.type.ImageConfig; +import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link BuildpackMetadata}. + * + * @author Scott Frederick + */ +class BuildpackMetadataTests extends AbstractJsonTests { + + @Test + void fromImageLoadsMetadata() throws IOException { + Image image = Image.of(getContent("buildpack-image.json")); + BuildpackMetadata metadata = BuildpackMetadata.fromImage(image); + assertThat(metadata.getId()).isEqualTo("example/hello-universe"); + assertThat(metadata.getVersion()).isEqualTo("0.0.1"); + } + + @Test + void fromImageWhenImageIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> BuildpackMetadata.fromImage(null)) + .withMessage("Image must not be null"); + } + + @Test + void fromImageWhenImageConfigIsNullThrowsException() { + Image image = mock(Image.class); + assertThatIllegalArgumentException().isThrownBy(() -> BuildpackMetadata.fromImage(image)) + .withMessage("ImageConfig must not be null"); + } + + @Test + void fromImageConfigWhenLabelIsMissingThrowsException() { + Image image = mock(Image.class); + ImageConfig imageConfig = mock(ImageConfig.class); + given(image.getConfig()).willReturn(imageConfig); + given(imageConfig.getLabels()).willReturn(Collections.singletonMap("alpha", "a")); + assertThatIllegalArgumentException().isThrownBy(() -> BuildpackMetadata.fromImage(image)) + .withMessage("No 'io.buildpacks.buildpackage.metadata' label found in image config labels 'alpha'"); + } + + @Test + void fromJsonLoadsMetadata() throws IOException { + BuildpackMetadata metadata = BuildpackMetadata.fromJson(getContentAsString("buildpack-metadata.json")); + assertThat(metadata.getId()).isEqualTo("example/hello-universe"); + assertThat(metadata.getVersion()).isEqualTo("0.0.1"); + assertThat(metadata.getHomepage()).isEqualTo("https://github.com/example/tree/main/buildpacks/hello-universe"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackReferenceTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackReferenceTests.java new file mode 100644 index 000000000000..0b5a9ba932a4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackReferenceTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.nio.file.Paths; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link BuildpackReference}. + * + * @author Phillip Webb + */ +class BuildpackReferenceTests { + + @Test + void ofWhenValueIsEmptyThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> BuildpackReference.of("")) + .withMessage("Value must not be empty"); + } + + @Test + void ofCreatesInstance() { + BuildpackReference reference = BuildpackReference.of("test"); + assertThat(reference).isNotNull(); + } + + @Test + void toStringReturnsValue() { + BuildpackReference reference = BuildpackReference.of("test"); + assertThat(reference).hasToString("test"); + } + + @Test + void equalsAndHashCode() { + BuildpackReference a = BuildpackReference.of("test1"); + BuildpackReference b = BuildpackReference.of("test1"); + BuildpackReference c = BuildpackReference.of("test2"); + assertThat(a).isEqualTo(a).isEqualTo(b).isNotEqualTo(c); + assertThat(a.hashCode()).isEqualTo(b.hashCode()); + } + + @Test + void hasPrefixWhenPrefixMatchReturnsTrue() { + BuildpackReference reference = BuildpackReference.of("test"); + assertThat(reference.hasPrefix("te")).isTrue(); + } + + @Test + void hasPrefixWhenPrifixMismatchReturnsFalse() { + BuildpackReference reference = BuildpackReference.of("test"); + assertThat(reference.hasPrefix("st")).isFalse(); + } + + @Test + void getSubReferenceWhenPrefixMatchReturnsSubReference() { + BuildpackReference reference = BuildpackReference.of("test"); + assertThat(reference.getSubReference("te")).isEqualTo("st"); + } + + @Test + void getSubReferenceWhenPrefixMismatchReturnsNull() { + BuildpackReference reference = BuildpackReference.of("test"); + assertThat(reference.getSubReference("st")).isNull(); + } + + @Test + void asPathWhenFileUrlReturnsPath() { + BuildpackReference reference = BuildpackReference.of("file:///test.dat"); + assertThat(reference.asPath()).isEqualTo(Paths.get("/test.dat")); + } + + @Test + void asPathWhenPathReturnsPath() { + BuildpackReference reference = BuildpackReference.of("/test.dat"); + assertThat(reference.asPath()).isEqualTo(Paths.get("/test.dat")); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackResolversTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackResolversTests.java new file mode 100644 index 000000000000..4356f56387bd --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpackResolversTests.java @@ -0,0 +1,103 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.buildpack.platform.docker.type.Image; +import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link BuildpackResolvers}. + * + * @author Scott Frederick + */ +class BuildpackResolversTests extends AbstractJsonTests { + + private BuildpackResolverContext resolverContext; + + @BeforeEach + void setup() throws Exception { + BuilderMetadata metadata = BuilderMetadata.fromJson(getContentAsString("builder-metadata.json")); + this.resolverContext = mock(BuildpackResolverContext.class); + given(this.resolverContext.getBuildpackMetadata()).willReturn(metadata.getBuildpacks()); + } + + @Test + void resolveAllWithBuilderBuildpackReferenceReturnsExpectedBuildpack() { + BuildpackReference reference = BuildpackReference.of("urn:cnb:builder:paketo-buildpacks/spring-boot@3.5.0"); + Buildpacks buildpacks = BuildpackResolvers.resolveAll(this.resolverContext, Collections.singleton(reference)); + assertThat(buildpacks.getBuildpacks()).hasSize(1); + assertThat(buildpacks.getBuildpacks().get(0)).isInstanceOf(BuilderBuildpack.class); + } + + @Test + void resolveAllWithDirectoryBuildpackReferenceReturnsExpectedBuildpack(@TempDir Path temp) throws IOException { + FileCopyUtils.copy(getClass().getResourceAsStream("buildpack.toml"), + Files.newOutputStream(temp.resolve("buildpack.toml"))); + BuildpackReference reference = BuildpackReference.of(temp.toAbsolutePath().toString()); + Buildpacks buildpacks = BuildpackResolvers.resolveAll(this.resolverContext, Collections.singleton(reference)); + assertThat(buildpacks.getBuildpacks()).hasSize(1); + assertThat(buildpacks.getBuildpacks().get(0)).isInstanceOf(DirectoryBuildpack.class); + } + + @Test + void resolveAllWithTarGzipBuildpackReferenceReturnsExpectedBuildpack(@TempDir File temp) throws Exception { + TestTarGzip testTarGzip = new TestTarGzip(temp); + Path archive = testTarGzip.createArchive(); + BuildpackReference reference = BuildpackReference.of(archive.toString()); + Buildpacks buildpacks = BuildpackResolvers.resolveAll(this.resolverContext, Collections.singleton(reference)); + assertThat(buildpacks.getBuildpacks()).hasSize(1); + assertThat(buildpacks.getBuildpacks().get(0)).isInstanceOf(TarGzipBuildpack.class); + } + + @Test + void resolveAllWithImageBuildpackReferenceReturnsExpectedBuildpack() throws IOException { + Image image = Image.of(getContent("buildpack-image.json")); + BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class); + given(resolverContext.fetchImage(any(), any())).willReturn(image); + BuildpackReference reference = BuildpackReference.of("docker://example/buildpack1:latest"); + Buildpacks buildpacks = BuildpackResolvers.resolveAll(resolverContext, Collections.singleton(reference)); + assertThat(buildpacks.getBuildpacks()).hasSize(1); + assertThat(buildpacks.getBuildpacks().get(0)).isInstanceOf(ImageBuildpack.class); + } + + @Test + void resolveAllWithInvalidLocatorThrowsException() { + BuildpackReference reference = BuildpackReference.of("unknown-buildpack@0.0.1"); + assertThatIllegalArgumentException() + .isThrownBy(() -> BuildpackResolvers.resolveAll(this.resolverContext, Collections.singleton(reference))) + .withMessageContaining("Invalid buildpack reference") + .withMessageContaining("'unknown-buildpack@0.0.1'"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpacksTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpacksTests.java new file mode 100644 index 000000000000..0d13924f8337 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildpacksTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.docker.type.Layer; +import org.springframework.util.StreamUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Buildpacks}. + * + * @author Scott Frederick + * @author Phillip Webb + */ +class BuildpacksTests { + + @Test + void ofWhenBuildpacksIsNullReturnsEmpty() { + Buildpacks buildpacks = Buildpacks.of(null); + assertThat(buildpacks).isSameAs(Buildpacks.EMPTY); + assertThat(buildpacks.getBuildpacks()).isEmpty(); + } + + @Test + void ofReturnsBuildpacks() { + List buildpackList = new ArrayList<>(); + buildpackList.add(new TestBuildpack("example/buildpack1", "0.0.1")); + buildpackList.add(new TestBuildpack("example/buildpack2", "0.0.2")); + Buildpacks buildpacks = Buildpacks.of(buildpackList); + assertThat(buildpacks.getBuildpacks()).isEqualTo(buildpackList); + } + + @Test + void applyWritesLayersAndOrderLayer() throws Exception { + List buildpackList = new ArrayList<>(); + buildpackList.add(new TestBuildpack("example/buildpack1", "0.0.1")); + buildpackList.add(new TestBuildpack("example/buildpack2", "0.0.2")); + buildpackList.add(new TestBuildpack("example/buildpack3", null)); + Buildpacks buildpacks = Buildpacks.of(buildpackList); + List layers = new ArrayList<>(); + buildpacks.apply(layers::add); + assertThat(layers).hasSize(4); + assertThatLayerContentIsCorrect(layers.get(0), "example_buildpack1/0.0.1"); + assertThatLayerContentIsCorrect(layers.get(1), "example_buildpack2/0.0.2"); + assertThatLayerContentIsCorrect(layers.get(2), "example_buildpack3/null"); + assertThatOrderLayerContentIsCorrect(layers.get(3)); + } + + private void assertThatLayerContentIsCorrect(Layer layer, String path) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + layer.writeTo(out); + try (TarArchiveInputStream tar = new TarArchiveInputStream(new ByteArrayInputStream(out.toByteArray()))) { + assertThat(tar.getNextEntry().getName()).isEqualTo("/cnb/buildpacks/" + path + "/buildpack.toml"); + assertThat(tar.getNextEntry()).isNull(); + } + } + + private void assertThatOrderLayerContentIsCorrect(Layer layer) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + layer.writeTo(out); + try (TarArchiveInputStream tar = new TarArchiveInputStream(new ByteArrayInputStream(out.toByteArray()))) { + assertThat(tar.getNextEntry().getName()).isEqualTo("/cnb/order.toml"); + byte[] content = StreamUtils.copyToByteArray(tar); + String toml = new String(content, StandardCharsets.UTF_8); + assertThat(toml).isEqualTo(getExpectedToml()); + } + } + + private String getExpectedToml() { + StringBuilder toml = new StringBuilder(); + toml.append("[[order]]\n"); + toml.append("\n"); + toml.append(" [[order.group]]\n"); + toml.append(" id = \"example/buildpack1\"\n"); + toml.append(" version = \"0.0.1\"\n"); + toml.append("\n"); + toml.append(" [[order.group]]\n"); + toml.append(" id = \"example/buildpack2\"\n"); + toml.append(" version = \"0.0.2\"\n"); + toml.append("\n"); + toml.append(" [[order.group]]\n"); + toml.append(" id = \"example/buildpack3\"\n"); + toml.append("\n"); + return toml.toString(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/DirectoryBuildpackTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/DirectoryBuildpackTests.java new file mode 100644 index 000000000000..65a0fed9ff99 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/DirectoryBuildpackTests.java @@ -0,0 +1,178 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link DirectoryBuildpack}. + * + * @author Scott Frederick + */ +@DisabledOnOs(OS.WINDOWS) +class DirectoryBuildpackTests { + + @TempDir + File temp; + + private File buildpackDir; + + private BuildpackResolverContext resolverContext; + + @BeforeEach + void setUp() { + this.buildpackDir = new File(this.temp, "buildpack"); + this.buildpackDir.mkdirs(); + this.resolverContext = mock(BuildpackResolverContext.class); + } + + @Test + void resolveWhenPath() throws Exception { + writeBuildpackDescriptor(); + writeScripts(); + BuildpackReference reference = BuildpackReference.of(this.buildpackDir.toString()); + Buildpack buildpack = DirectoryBuildpack.resolve(this.resolverContext, reference); + assertThat(buildpack).isNotNull(); + assertThat(buildpack.getCoordinates()).hasToString("example/buildpack1@0.0.1"); + assertHasExpectedLayers(buildpack); + } + + @Test + void resolveWhenFileUrl() throws Exception { + writeBuildpackDescriptor(); + writeScripts(); + BuildpackReference reference = BuildpackReference.of("file://" + this.buildpackDir.toString()); + Buildpack buildpack = DirectoryBuildpack.resolve(this.resolverContext, reference); + assertThat(buildpack).isNotNull(); + assertThat(buildpack.getCoordinates()).hasToString("example/buildpack1@0.0.1"); + assertHasExpectedLayers(buildpack); + } + + @Test + void resolveWhenDirectoryWithoutBuildpackTomlThrowsException() throws Exception { + Files.createDirectories(this.buildpackDir.toPath()); + BuildpackReference reference = BuildpackReference.of(this.buildpackDir.toString()); + assertThatIllegalArgumentException() + .isThrownBy(() -> DirectoryBuildpack.resolve(this.resolverContext, reference)) + .withMessageContaining("Buildpack descriptor 'buildpack.toml' is required") + .withMessageContaining(this.buildpackDir.getAbsolutePath()); + } + + @Test + void resolveWhenFileReturnsNull() throws Exception { + Path file = Files.createFile(Paths.get(this.buildpackDir.toString(), "test")); + BuildpackReference reference = BuildpackReference.of(file.toString()); + Buildpack buildpack = DirectoryBuildpack.resolve(this.resolverContext, reference); + assertThat(buildpack).isNull(); + } + + @Test + void resolveWhenDirectoryDoesNotExistReturnsNull() { + BuildpackReference reference = BuildpackReference.of("/test/a/missing/buildpack"); + Buildpack buildpack = DirectoryBuildpack.resolve(this.resolverContext, reference); + assertThat(buildpack).isNull(); + } + + @Test + void locateDirectoryAsUrlThatDoesNotExistThrowsException() { + BuildpackReference reference = BuildpackReference.of("file:///test/a/missing/buildpack"); + Buildpack buildpack = DirectoryBuildpack.resolve(this.resolverContext, reference); + assertThat(buildpack).isNull(); + } + + private void assertHasExpectedLayers(Buildpack buildpack) throws IOException { + List layers = new ArrayList<>(); + buildpack.apply((layer) -> { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + layer.writeTo(out); + layers.add(out); + }); + assertThat(layers).hasSize(1); + byte[] content = layers.get(0).toByteArray(); + try (TarArchiveInputStream tar = new TarArchiveInputStream(new ByteArrayInputStream(content))) { + List entries = new ArrayList<>(); + TarArchiveEntry entry = tar.getNextTarEntry(); + while (entry != null) { + entries.add(entry); + entry = tar.getNextTarEntry(); + } + assertThat(entries).extracting("name", "mode").containsExactlyInAnyOrder(tuple("/cnb/", 0755), + tuple("/cnb/buildpacks/", 0755), tuple("/cnb/buildpacks/example_buildpack1/", 0755), + tuple("/cnb/buildpacks/example_buildpack1/0.0.1/", 0755), + tuple("/cnb/buildpacks/example_buildpack1/0.0.1/buildpack.toml", 0644), + tuple("/cnb/buildpacks/example_buildpack1/0.0.1/bin/", 0755), + tuple("/cnb/buildpacks/example_buildpack1/0.0.1/bin/detect", 0744), + tuple("/cnb/buildpacks/example_buildpack1/0.0.1/bin/build", 0744)); + } + } + + private void writeBuildpackDescriptor() throws IOException { + Path descriptor = Files.createFile(Paths.get(this.buildpackDir.getAbsolutePath(), "buildpack.toml"), + PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-r--r--"))); + try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(descriptor))) { + writer.println("[buildpack]"); + writer.println("id = \"example/buildpack1\""); + writer.println("version = \"0.0.1\""); + writer.println("name = \"Example buildpack\""); + writer.println("homepage = \"https://github.com/example/example-buildpack\""); + writer.println("[[stacks]]"); + writer.println("id = \"io.buildpacks.stacks.bionic\""); + } + } + + private void writeScripts() throws IOException { + Path binDirectory = Files.createDirectory(Paths.get(this.buildpackDir.getAbsolutePath(), "bin"), + PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwxr-xr-x"))); + binDirectory.toFile().mkdirs(); + Path detect = Files.createFile(Paths.get(binDirectory.toString(), "detect"), + PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwxr--r--"))); + try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(detect))) { + writer.println("#!/usr/bin/env bash"); + writer.println("echo \"---> detect\""); + } + Path build = Files.createFile(Paths.get(binDirectory.toString(), "build"), + PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwxr--r--"))); + try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(build))) { + writer.println("#!/usr/bin/env bash"); + writer.println("echo \"---> build\""); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilderTests.java new file mode 100644 index 000000000000..9ba9ab9e821e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/EphemeralBuilderTests.java @@ -0,0 +1,205 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.compress.archivers.ArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.utils.IOUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.buildpack.platform.docker.type.Image; +import org.springframework.boot.buildpack.platform.docker.type.ImageArchive; +import org.springframework.boot.buildpack.platform.docker.type.ImageConfig; +import org.springframework.boot.buildpack.platform.docker.type.ImageReference; +import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +/** + * Tests for {@link EphemeralBuilder}. + * + * @author Phillip Webb + * @author Scott Frederick + */ +class EphemeralBuilderTests extends AbstractJsonTests { + + @TempDir + File temp; + + private final BuildOwner owner = BuildOwner.of(123, 456); + + private Image image; + + private ImageReference targetImage; + + private BuilderMetadata metadata; + + private Map env; + + private Buildpacks buildpacks; + + private final Creator creator = Creator.withVersion("dev"); + + @BeforeEach + void setup() throws Exception { + this.image = Image.of(getContent("image.json")); + this.targetImage = ImageReference.of("my-image:latest"); + this.metadata = BuilderMetadata.fromImage(this.image); + this.env = new HashMap<>(); + this.env.put("spring", "boot"); + this.env.put("empty", null); + } + + @Test + void getNameHasRandomName() throws Exception { + EphemeralBuilder b1 = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata, + this.creator, this.env, this.buildpacks); + EphemeralBuilder b2 = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata, + this.creator, this.env, this.buildpacks); + assertThat(b1.getName().toString()).startsWith("pack.local/builder/").endsWith(":latest"); + assertThat(b1.getName().toString()).isNotEqualTo(b2.getName().toString()); + } + + @Test + void getArchiveHasCreatedByConfig() throws Exception { + EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata, + this.creator, this.env, this.buildpacks); + ImageConfig config = builder.getArchive().getImageConfig(); + BuilderMetadata ephemeralMetadata = BuilderMetadata.fromImageConfig(config); + assertThat(ephemeralMetadata.getCreatedBy().getName()).isEqualTo("Spring Boot"); + assertThat(ephemeralMetadata.getCreatedBy().getVersion()).isEqualTo("dev"); + } + + @Test + void getArchiveHasTag() throws Exception { + EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata, + this.creator, this.env, this.buildpacks); + ImageReference tag = builder.getArchive().getTag(); + assertThat(tag.toString()).startsWith("pack.local/builder/").endsWith(":latest"); + } + + @Test + void getArchiveHasFixedCreateDate() throws Exception { + EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata, + this.creator, this.env, this.buildpacks); + Instant createInstant = builder.getArchive().getCreateDate(); + OffsetDateTime createDateTime = OffsetDateTime.ofInstant(createInstant, ZoneId.of("UTC")); + assertThat(createDateTime.getYear()).isEqualTo(1980); + assertThat(createDateTime.getMonthValue()).isEqualTo(1); + assertThat(createDateTime.getDayOfMonth()).isEqualTo(1); + assertThat(createDateTime.getHour()).isEqualTo(0); + assertThat(createDateTime.getMinute()).isEqualTo(0); + assertThat(createDateTime.getSecond()).isEqualTo(1); + } + + @Test + void getArchiveContainsEnvLayer() throws Exception { + EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata, + this.creator, this.env, this.buildpacks); + File directory = unpack(getLayer(builder.getArchive(), 0), "env"); + assertThat(new File(directory, "platform/env/spring")).usingCharset(StandardCharsets.UTF_8).hasContent("boot"); + assertThat(new File(directory, "platform/env/empty")).usingCharset(StandardCharsets.UTF_8).hasContent(""); + } + + @Test + void getArchiveHasBuilderForLabel() throws Exception { + EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata, + this.creator, this.env, this.buildpacks); + ImageConfig config = builder.getArchive().getImageConfig(); + assertThat(config.getLabels()) + .contains(entry(EphemeralBuilder.BUILDER_FOR_LABEL_NAME, this.targetImage.toString())); + } + + @Test + void getArchiveContainsBuildpackLayers() throws Exception { + List buildpackList = new ArrayList<>(); + buildpackList.add(new TestBuildpack("example/buildpack1", "0.0.1")); + buildpackList.add(new TestBuildpack("example/buildpack2", "0.0.2")); + buildpackList.add(new TestBuildpack("example/buildpack3", "0.0.3")); + this.buildpacks = Buildpacks.of(buildpackList); + EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.targetImage, this.metadata, + this.creator, null, this.buildpacks); + assertBuildpackLayerContent(builder, 0, "/cnb/buildpacks/example_buildpack1/0.0.1/buildpack.toml"); + assertBuildpackLayerContent(builder, 1, "/cnb/buildpacks/example_buildpack2/0.0.2/buildpack.toml"); + assertBuildpackLayerContent(builder, 2, "/cnb/buildpacks/example_buildpack3/0.0.3/buildpack.toml"); + File orderDirectory = unpack(getLayer(builder.getArchive(), 3), "order"); + assertThat(new File(orderDirectory, "cnb/order.toml")).usingCharset(StandardCharsets.UTF_8) + .hasContent(content("order.toml")); + } + + private void assertBuildpackLayerContent(EphemeralBuilder builder, int index, String s) throws Exception { + File buildpackDirectory = unpack(getLayer(builder.getArchive(), index), "buildpack"); + assertThat(new File(buildpackDirectory, s)).usingCharset(StandardCharsets.UTF_8).hasContent("[test]"); + } + + private TarArchiveInputStream getLayer(ImageArchive archive, int index) throws Exception { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + archive.writeTo(outputStream); + TarArchiveInputStream tar = new TarArchiveInputStream(new ByteArrayInputStream(outputStream.toByteArray())); + for (int i = 0; i <= index; i++) { + tar.getNextEntry(); + } + return new TarArchiveInputStream(tar); + } + + private File unpack(TarArchiveInputStream archive, String name) throws Exception { + File directory = new File(this.temp, name); + directory.mkdirs(); + ArchiveEntry entry = archive.getNextEntry(); + while (entry != null) { + File file = new File(directory, entry.getName()); + if (entry.isDirectory()) { + file.mkdirs(); + } + else { + file.getParentFile().mkdirs(); + try (OutputStream out = new FileOutputStream(file)) { + IOUtils.copy(archive, out); + } + } + entry = archive.getNextEntry(); + } + return directory; + } + + private String content(String fileName) throws IOException { + InputStream in = getClass().getResourceAsStream(fileName); + return FileCopyUtils.copyToString(new InputStreamReader(in, StandardCharsets.UTF_8)); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ImageBuildpackTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ImageBuildpackTests.java new file mode 100644 index 000000000000..a9195daa78f1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ImageBuildpackTests.java @@ -0,0 +1,211 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.invocation.InvocationOnMock; + +import org.springframework.boot.buildpack.platform.docker.type.Image; +import org.springframework.boot.buildpack.platform.docker.type.ImageReference; +import org.springframework.boot.buildpack.platform.io.IOBiConsumer; +import org.springframework.boot.buildpack.platform.io.TarArchive; +import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.fail; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ImageBuildpack}. + * + * @author Scott Frederick + * @author Phillip Webb + */ +class ImageBuildpackTests extends AbstractJsonTests { + + private String longFilePath; + + @BeforeEach + void setUp() { + StringBuilder path = new StringBuilder(); + new Random().ints('a', 'z' + 1).limit(100).forEach((i) -> path.append((char) i)); + this.longFilePath = path.toString(); + } + + @Test + void resolveWhenFullyQualifiedReferenceReturnsBuilder() throws Exception { + Image image = Image.of(getContent("buildpack-image.json")); + ImageReference imageReference = ImageReference.of("example/buildpack1:1.0.0"); + BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class); + given(resolverContext.fetchImage(eq(imageReference), eq(ImageType.BUILDPACK))).willReturn(image); + willAnswer(this::withMockLayers).given(resolverContext).exportImageLayers(eq(imageReference), any()); + BuildpackReference reference = BuildpackReference.of("docker://example/buildpack1:1.0.0"); + Buildpack buildpack = ImageBuildpack.resolve(resolverContext, reference); + assertThat(buildpack.getCoordinates()).hasToString("example/hello-universe@0.0.1"); + assertHasExpectedLayers(buildpack); + } + + @Test + void resolveWhenUnqualifiedReferenceReturnsBuilder() throws Exception { + Image image = Image.of(getContent("buildpack-image.json")); + ImageReference imageReference = ImageReference.of("example/buildpack1:1.0.0"); + BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class); + given(resolverContext.fetchImage(eq(imageReference), eq(ImageType.BUILDPACK))).willReturn(image); + willAnswer(this::withMockLayers).given(resolverContext).exportImageLayers(eq(imageReference), any()); + BuildpackReference reference = BuildpackReference.of("example/buildpack1:1.0.0"); + Buildpack buildpack = ImageBuildpack.resolve(resolverContext, reference); + assertThat(buildpack.getCoordinates()).hasToString("example/hello-universe@0.0.1"); + assertHasExpectedLayers(buildpack); + } + + @Test + void resolveReferenceWithoutTagUsesLatestTag() throws Exception { + Image image = Image.of(getContent("buildpack-image.json")); + ImageReference imageReference = ImageReference.of("example/buildpack1:latest"); + BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class); + given(resolverContext.fetchImage(eq(imageReference), eq(ImageType.BUILDPACK))).willReturn(image); + willAnswer(this::withMockLayers).given(resolverContext).exportImageLayers(eq(imageReference), any()); + BuildpackReference reference = BuildpackReference.of("example/buildpack1"); + Buildpack buildpack = ImageBuildpack.resolve(resolverContext, reference); + assertThat(buildpack.getCoordinates()).hasToString("example/hello-universe@0.0.1"); + assertHasExpectedLayers(buildpack); + } + + @Test + void resolveReferenceWithDigestUsesDigest() throws Exception { + Image image = Image.of(getContent("buildpack-image.json")); + String digest = "sha256:4acb6bfd6c4f0cabaf7f3690e444afe51f1c7de54d51da7e63fac709c56f1c30"; + ImageReference imageReference = ImageReference.of("example/buildpack1@" + digest); + BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class); + given(resolverContext.fetchImage(eq(imageReference), eq(ImageType.BUILDPACK))).willReturn(image); + willAnswer(this::withMockLayers).given(resolverContext).exportImageLayers(eq(imageReference), any()); + BuildpackReference reference = BuildpackReference.of("example/buildpack1@" + digest); + Buildpack buildpack = ImageBuildpack.resolve(resolverContext, reference); + assertThat(buildpack.getCoordinates()).hasToString("example/hello-universe@0.0.1"); + assertHasExpectedLayers(buildpack); + } + + @Test + void resolveWhenWhenImageNotPulledThrowsException() throws Exception { + BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class); + given(resolverContext.fetchImage(any(), any())).willThrow(IOException.class); + BuildpackReference reference = BuildpackReference.of("docker://example/buildpack1"); + assertThatIllegalArgumentException().isThrownBy(() -> ImageBuildpack.resolve(resolverContext, reference)) + .withMessageContaining("Error pulling buildpack image") + .withMessageContaining("example/buildpack1:latest"); + } + + @Test + void resolveWhenMissingMetadataLabelThrowsException() throws Exception { + Image image = Image.of(getContent("image.json")); + BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class); + given(resolverContext.fetchImage(any(), any())).willReturn(image); + BuildpackReference reference = BuildpackReference.of("docker://example/buildpack1:latest"); + assertThatIllegalArgumentException().isThrownBy(() -> ImageBuildpack.resolve(resolverContext, reference)) + .withMessageContaining("No 'io.buildpacks.buildpackage.metadata' label found"); + } + + @Test + void resolveWhenFullyQualifiedReferenceWithInvalidImageReferenceThrowsException() { + BuildpackReference reference = BuildpackReference.of("docker://buildpack@0.0.1"); + BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class); + assertThatIllegalArgumentException().isThrownBy(() -> ImageBuildpack.resolve(resolverContext, reference)) + .withMessageContaining("Unable to parse image reference \"buildpack@0.0.1\""); + } + + @Test + void resolveWhenUnqualifiedReferenceWithInvalidImageReferenceReturnsNull() { + BuildpackReference reference = BuildpackReference.of("buildpack@0.0.1"); + BuildpackResolverContext resolverContext = mock(BuildpackResolverContext.class); + Buildpack buildpack = ImageBuildpack.resolve(resolverContext, reference); + assertThat(buildpack).isNull(); + } + + private Object withMockLayers(InvocationOnMock invocation) { + try { + IOBiConsumer consumer = invocation.getArgument(1); + TarArchive archive = (out) -> { + try (TarArchiveOutputStream tarOut = new TarArchiveOutputStream(out)) { + tarOut.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); + writeTarEntry(tarOut, "/cnb/"); + writeTarEntry(tarOut, "/cnb/buildpacks/"); + writeTarEntry(tarOut, "/cnb/buildpacks/example_buildpack/"); + writeTarEntry(tarOut, "/cnb/buildpacks/example_buildpack/0.0.1/"); + writeTarEntry(tarOut, "/cnb/buildpacks/example_buildpack/0.0.1/buildpack.toml"); + writeTarEntry(tarOut, "/cnb/buildpacks/example_buildpack/0.0.1/" + this.longFilePath); + tarOut.finish(); + } + }; + consumer.accept("test", archive); + } + catch (IOException ex) { + fail("Error writing mock layers", ex); + } + return null; + } + + private void writeTarEntry(TarArchiveOutputStream tarOut, String name) throws IOException { + TarArchiveEntry entry = new TarArchiveEntry(name); + tarOut.putArchiveEntry(entry); + tarOut.closeArchiveEntry(); + } + + private void assertHasExpectedLayers(Buildpack buildpack) throws IOException { + List layers = new ArrayList<>(); + buildpack.apply((layer) -> { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + layer.writeTo(out); + layers.add(out); + }); + assertThat(layers).hasSize(1); + byte[] content = layers.get(0).toByteArray(); + List entries = new ArrayList<>(); + try (TarArchiveInputStream tar = new TarArchiveInputStream(new ByteArrayInputStream(content))) { + TarArchiveEntry entry = tar.getNextTarEntry(); + while (entry != null) { + entries.add(entry); + entry = tar.getNextTarEntry(); + } + } + assertThat(entries).extracting("name", "mode").containsExactlyInAnyOrder( + tuple("cnb/", TarArchiveEntry.DEFAULT_DIR_MODE), + tuple("cnb/buildpacks/", TarArchiveEntry.DEFAULT_DIR_MODE), + tuple("cnb/buildpacks/example_buildpack/", TarArchiveEntry.DEFAULT_DIR_MODE), + tuple("cnb/buildpacks/example_buildpack/0.0.1/", TarArchiveEntry.DEFAULT_DIR_MODE), + tuple("cnb/buildpacks/example_buildpack/0.0.1/buildpack.toml", TarArchiveEntry.DEFAULT_FILE_MODE), + tuple("cnb/buildpacks/example_buildpack/0.0.1/" + this.longFilePath, + TarArchiveEntry.DEFAULT_FILE_MODE)); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java new file mode 100644 index 000000000000..e239e0226b1a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java @@ -0,0 +1,299 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.Map; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.stubbing.Answer; + +import org.springframework.boot.buildpack.platform.docker.DockerApi; +import org.springframework.boot.buildpack.platform.docker.DockerApi.ContainerApi; +import org.springframework.boot.buildpack.platform.docker.DockerApi.ImageApi; +import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi; +import org.springframework.boot.buildpack.platform.docker.type.Binding; +import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig; +import org.springframework.boot.buildpack.platform.docker.type.ContainerContent; +import org.springframework.boot.buildpack.platform.docker.type.ContainerReference; +import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus; +import org.springframework.boot.buildpack.platform.docker.type.ImageReference; +import org.springframework.boot.buildpack.platform.docker.type.VolumeName; +import org.springframework.boot.buildpack.platform.io.IOConsumer; +import org.springframework.boot.buildpack.platform.io.TarArchive; +import org.springframework.boot.buildpack.platform.json.SharedObjectMapper; +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.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link Lifecycle}. + * + * @author Phillip Webb + * @author Scott Frederick + */ +class LifecycleTests { + + private TestPrintStream out; + + private DockerApi docker; + + private final Map configs = new LinkedHashMap<>(); + + private final Map content = new LinkedHashMap<>(); + + @BeforeEach + void setup() { + this.out = new TestPrintStream(); + this.docker = mockDockerApi(); + } + + @Test + void executeExecutesPhases() throws Exception { + given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); + createLifecycle().execute(); + assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator.json")); + assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); + } + + @Test + void executeWithBindingsExecutesPhases() throws Exception { + given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); + BuildRequest request = getTestRequest().withBindings(Binding.of("/host/src/path:/container/dest/path:ro"), + Binding.of("volume-name:/container/volume/path:rw")); + createLifecycle(request).execute(); + assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-bindings.json")); + assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); + } + + @Test + void executeExecutesPhasesWithPlatformApi03() throws Exception { + given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); + createLifecycle("builder-metadata-platform-api-0.3.json").execute(); + assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-platform-api-0.3.json")); + assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); + } + + @Test + void executeOnlyUploadsContentOnce() throws Exception { + given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); + createLifecycle().execute(); + assertThat(this.content).hasSize(1); + } + + @Test + void executeWhenAlreadyRunThrowsException() throws Exception { + given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); + Lifecycle lifecycle = createLifecycle(); + lifecycle.execute(); + assertThatIllegalStateException().isThrownBy(lifecycle::execute) + .withMessage("Lifecycle has already been executed"); + } + + @Test + void executeWhenBuilderReturnsErrorThrowsException() throws Exception { + given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(9, null)); + assertThatExceptionOfType(BuilderException.class).isThrownBy(() -> createLifecycle().execute()) + .withMessage("Builder lifecycle 'creator' failed with status code 9"); + } + + @Test + void executeWhenCleanCacheClearsCache() throws Exception { + given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); + BuildRequest request = getTestRequest().withCleanCache(true); + createLifecycle(request).execute(); + assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-clean-cache.json")); + VolumeName name = VolumeName.of("pack-cache-b35197ac41ea.build"); + then(this.docker.volume()).should().delete(name, true); + } + + @Test + void executeWhenPlatformApiNotSupportedThrowsException() throws Exception { + given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); + assertThatIllegalStateException() + .isThrownBy(() -> createLifecycle("builder-metadata-unsupported-api.json").execute()) + .withMessage("Detected platform API versions '0.2' are not included in supported versions '0.3,0.4'"); + } + + @Test + void executeWhenMultiplePlatformApisNotSupportedThrowsException() throws Exception { + given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); + assertThatIllegalStateException() + .isThrownBy(() -> createLifecycle("builder-metadata-unsupported-apis.json").execute()).withMessage( + "Detected platform API versions '0.5,0.6' are not included in supported versions '0.3,0.4'"); + } + + @Test + void executeWhenMultiplePlatformApisSupportedExecutesPhase() throws Exception { + given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); + createLifecycle("builder-metadata-supported-apis.json").execute(); + assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator.json")); + } + + @Test + void closeClearsVolumes() throws Exception { + createLifecycle().close(); + then(this.docker.volume()).should().delete(VolumeName.of("pack-layers-aaaaaaaaaa"), true); + then(this.docker.volume()).should().delete(VolumeName.of("pack-app-aaaaaaaaaa"), true); + } + + private DockerApi mockDockerApi() { + DockerApi docker = mock(DockerApi.class); + ImageApi imageApi = mock(ImageApi.class); + ContainerApi containerApi = mock(ContainerApi.class); + VolumeApi volumeApi = mock(VolumeApi.class); + given(docker.image()).willReturn(imageApi); + given(docker.container()).willReturn(containerApi); + given(docker.volume()).willReturn(volumeApi); + return docker; + } + + private BuildRequest getTestRequest() { + TarArchive content = mock(TarArchive.class); + ImageReference name = ImageReference.of("my-application"); + return BuildRequest.of(name, (owner) -> content).withRunImage(ImageReference.of("cloudfoundry/run")); + } + + private Lifecycle createLifecycle() throws IOException { + return createLifecycle(getTestRequest()); + } + + private Lifecycle createLifecycle(BuildRequest request) throws IOException { + EphemeralBuilder builder = mockEphemeralBuilder(); + return createLifecycle(request, builder); + } + + private Lifecycle createLifecycle(String builderMetadata) throws IOException { + EphemeralBuilder builder = mockEphemeralBuilder(builderMetadata); + return createLifecycle(getTestRequest(), builder); + } + + private Lifecycle createLifecycle(BuildRequest request, EphemeralBuilder ephemeralBuilder) { + return new TestLifecycle(BuildLog.to(this.out), this.docker, request, ephemeralBuilder); + } + + private EphemeralBuilder mockEphemeralBuilder() throws IOException { + return mockEphemeralBuilder("builder-metadata.json"); + } + + private EphemeralBuilder mockEphemeralBuilder(String builderMetadata) throws IOException { + EphemeralBuilder builder = mock(EphemeralBuilder.class); + byte[] metadataContent = FileCopyUtils.copyToByteArray(getClass().getResourceAsStream(builderMetadata)); + BuilderMetadata metadata = BuilderMetadata.fromJson(new String(metadataContent, StandardCharsets.UTF_8)); + given(builder.getName()).willReturn(ImageReference.of("pack.local/ephemeral-builder")); + given(builder.getBuilderMetadata()).willReturn(metadata); + return builder; + } + + private Answer answerWithGeneratedContainerId() { + return (invocation) -> { + ContainerConfig config = invocation.getArgument(0, ContainerConfig.class); + ArrayNode command = getCommand(config); + String name = command.get(0).asText().substring(1).replaceAll("/", "-"); + this.configs.put(name, config); + if (invocation.getArguments().length > 1) { + this.content.put(name, invocation.getArgument(1, ContainerContent.class)); + } + return ContainerReference.of(name); + }; + } + + private ArrayNode getCommand(ContainerConfig config) throws JsonProcessingException { + JsonNode node = SharedObjectMapper.get().readTree(config.toString()); + return (ArrayNode) node.at("/Cmd"); + } + + private void assertPhaseWasRun(String name, IOConsumer configConsumer) throws IOException { + ContainerReference containerReference = ContainerReference.of("cnb-lifecycle-" + name); + then(this.docker.container()).should().start(containerReference); + then(this.docker.container()).should().logs(eq(containerReference), any()); + then(this.docker.container()).should().remove(containerReference, true); + configConsumer.accept(this.configs.get(containerReference.toString())); + } + + private IOConsumer withExpectedConfig(String name) { + return (config) -> { + InputStream in = getClass().getResourceAsStream(name); + String json = FileCopyUtils.copyToString(new InputStreamReader(in, StandardCharsets.UTF_8)); + assertThat(config.toString()).isEqualToIgnoringWhitespace(json); + }; + } + + static class TestLifecycle extends Lifecycle { + + TestLifecycle(BuildLog log, DockerApi docker, BuildRequest request, EphemeralBuilder builder) { + super(log, docker, request, builder); + } + + @Override + protected VolumeName createRandomVolumeName(String prefix) { + return VolumeName.of(prefix + "aaaaaaaaaa"); + } + + } + + static class TestPrintStream extends PrintStream { + + TestPrintStream() { + super(new ByteArrayOutputStream()); + } + + @Override + public String toString() { + return this.out.toString(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleVersionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleVersionTests.java new file mode 100644 index 000000000000..2f645c5b8bd0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleVersionTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link LifecycleVersion}. + * + * @author Phillip Webb + */ +class LifecycleVersionTests { + + @Test + void parseWhenValueIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> LifecycleVersion.parse(null)) + .withMessage("Value must not be empty"); + } + + @Test + void parseWhenTooLongThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> LifecycleVersion.parse("v1.2.3.4")) + .withMessage("Malformed version number '1.2.3.4'"); + } + + @Test + void parseWhenNonNumericThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> LifecycleVersion.parse("v1.2.3a")) + .withMessage("Malformed version number '1.2.3a'"); + } + + @Test + void compareTo() { + LifecycleVersion v4 = LifecycleVersion.parse("0.0.4"); + assertThat(LifecycleVersion.parse("0.0.3").compareTo(v4)).isNegative(); + assertThat(LifecycleVersion.parse("0.0.4").compareTo(v4)).isZero(); + assertThat(LifecycleVersion.parse("0.0.5").compareTo(v4)).isPositive(); + } + + @Test + void isEqualOrGreaterThan() { + LifecycleVersion v4 = LifecycleVersion.parse("0.0.4"); + assertThat(LifecycleVersion.parse("0.0.3").isEqualOrGreaterThan(v4)).isFalse(); + assertThat(LifecycleVersion.parse("0.0.4").isEqualOrGreaterThan(v4)).isTrue(); + assertThat(LifecycleVersion.parse("0.0.5").isEqualOrGreaterThan(v4)).isTrue(); + } + + @Test + void parseReturnsVersion() { + assertThat(LifecycleVersion.parse("1.2.3").toString()).isEqualTo("v1.2.3"); + assertThat(LifecycleVersion.parse("1.2").toString()).isEqualTo("v1.2.0"); + assertThat(LifecycleVersion.parse("1").toString()).isEqualTo("v1.0.0"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PhaseTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PhaseTests.java new file mode 100644 index 000000000000..6f1b0c5af9ee --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PhaseTests.java @@ -0,0 +1,134 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.docker.type.Binding; +import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig.Update; +import org.springframework.boot.buildpack.platform.docker.type.VolumeName; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link Phase}. + * + * @author Phillip Webb + * @author Scott Frederick + */ +class PhaseTests { + + private static final String[] NO_ARGS = {}; + + @Test + void getNameReturnsName() { + Phase phase = new Phase("test", false); + assertThat(phase.getName()).isEqualTo("test"); + } + + @Test + void toStringReturnsName() { + Phase phase = new Phase("test", false); + assertThat(phase).hasToString("test"); + } + + @Test + void applyUpdatesConfiguration() { + Phase phase = new Phase("test", false); + Update update = mock(Update.class); + phase.apply(update); + then(update).should().withCommand("/cnb/lifecycle/test", NO_ARGS); + then(update).should().withLabel("author", "spring-boot"); + then(update).shouldHaveNoMoreInteractions(); + } + + @Test + void applyWhenWithDaemonAccessUpdatesConfigurationWithRootUserAndDomainSocketBinding() { + Phase phase = new Phase("test", false); + phase.withDaemonAccess(); + Update update = mock(Update.class); + phase.apply(update); + then(update).should().withUser("root"); + then(update).should().withBinding(Binding.from("/var/run/docker.sock", "/var/run/docker.sock")); + then(update).should().withCommand("/cnb/lifecycle/test", NO_ARGS); + then(update).should().withLabel("author", "spring-boot"); + then(update).shouldHaveNoMoreInteractions(); + } + + @Test + void applyWhenWithLogLevelArgAndVerboseLoggingUpdatesConfigurationWithLogLevel() { + Phase phase = new Phase("test", true); + phase.withLogLevelArg(); + Update update = mock(Update.class); + phase.apply(update); + then(update).should().withCommand("/cnb/lifecycle/test", "-log-level", "debug"); + then(update).should().withLabel("author", "spring-boot"); + then(update).shouldHaveNoMoreInteractions(); + } + + @Test + void applyWhenWithLogLevelArgAndNonVerboseLoggingDoesNotUpdateLogLevel() { + Phase phase = new Phase("test", false); + phase.withLogLevelArg(); + Update update = mock(Update.class); + phase.apply(update); + then(update).should().withCommand("/cnb/lifecycle/test"); + then(update).should().withLabel("author", "spring-boot"); + then(update).shouldHaveNoMoreInteractions(); + } + + @Test + void applyWhenWithArgsUpdatesConfigurationWithArguments() { + Phase phase = new Phase("test", false); + phase.withArgs("a", "b", "c"); + Update update = mock(Update.class); + phase.apply(update); + then(update).should().withCommand("/cnb/lifecycle/test", "a", "b", "c"); + then(update).should().withLabel("author", "spring-boot"); + then(update).shouldHaveNoMoreInteractions(); + } + + @Test + void applyWhenWithBindsUpdatesConfigurationWithBinds() { + Phase phase = new Phase("test", false); + VolumeName volumeName = VolumeName.of("test"); + phase.withBinding(Binding.from(volumeName, "/test")); + Update update = mock(Update.class); + phase.apply(update); + then(update).should().withCommand("/cnb/lifecycle/test"); + then(update).should().withLabel("author", "spring-boot"); + then(update).should().withBinding(Binding.from(volumeName, "/test")); + then(update).shouldHaveNoMoreInteractions(); + } + + @Test + void applyWhenWithEnvUpdatesConfigurationWithEnv() { + Phase phase = new Phase("test", false); + phase.withEnv("name1", "value1"); + phase.withEnv("name2", "value2"); + Update update = mock(Update.class); + phase.apply(update); + then(update).should().withCommand("/cnb/lifecycle/test"); + then(update).should().withLabel("author", "spring-boot"); + then(update).should().withEnv("name1", "value1"); + then(update).should().withEnv("name2", "value2"); + then(update).shouldHaveNoMoreInteractions(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PrintStreamBuildLogTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PrintStreamBuildLogTests.java new file mode 100644 index 000000000000..ec0ec4f3f75b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PrintStreamBuildLogTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.io.ByteArrayOutputStream; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.docker.LogUpdateEvent; +import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent; +import org.springframework.boot.buildpack.platform.docker.type.Image; +import org.springframework.boot.buildpack.platform.docker.type.ImageReference; +import org.springframework.boot.buildpack.platform.docker.type.VolumeName; +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link PrintStreamBuildLog}. + * + * @author Phillip Webb + */ +class PrintStreamBuildLogTests { + + @Test + void printsExpectedOutput() throws Exception { + TestPrintStream out = new TestPrintStream(); + PrintStreamBuildLog log = new PrintStreamBuildLog(out); + BuildRequest request = mock(BuildRequest.class); + ImageReference name = ImageReference.of("my-app:latest"); + ImageReference builderImageReference = ImageReference.of("cnb/builder"); + Image builderImage = mock(Image.class); + given(builderImage.getDigests()).willReturn(Collections.singletonList("00000001")); + ImageReference runImageReference = ImageReference.of("cnb/runner"); + Image runImage = mock(Image.class); + given(runImage.getDigests()).willReturn(Collections.singletonList("00000002")); + given(request.getName()).willReturn(name); + log.start(request); + Consumer pullBuildImageConsumer = log.pullingImage(builderImageReference, + ImageType.BUILDER); + pullBuildImageConsumer.accept(new TotalProgressEvent(100)); + log.pulledImage(builderImage, ImageType.BUILDER); + Consumer pullRunImageConsumer = log.pullingImage(runImageReference, ImageType.RUNNER); + pullRunImageConsumer.accept(new TotalProgressEvent(100)); + log.pulledImage(runImage, ImageType.RUNNER); + log.executingLifecycle(request, LifecycleVersion.parse("0.5"), VolumeName.of("pack-abc.cache")); + Consumer phase1Consumer = log.runningPhase(request, "alphabet"); + phase1Consumer.accept(mockLogEvent("one")); + phase1Consumer.accept(mockLogEvent("two")); + phase1Consumer.accept(mockLogEvent("three")); + Consumer phase2Consumer = log.runningPhase(request, "basket"); + phase2Consumer.accept(mockLogEvent("spring")); + phase2Consumer.accept(mockLogEvent("boot")); + log.executedLifecycle(request); + String expected = FileCopyUtils.copyToString(new InputStreamReader( + getClass().getResourceAsStream("print-stream-build-log.txt"), StandardCharsets.UTF_8)); + assertThat(out.toString()).isEqualToIgnoringNewLines(expected); + } + + private LogUpdateEvent mockLogEvent(String string) { + LogUpdateEvent event = mock(LogUpdateEvent.class); + given(event.toString()).willReturn(string); + return event; + } + + static class TestPrintStream extends PrintStream { + + TestPrintStream() { + super(new ByteArrayOutputStream()); + } + + @Override + public String toString() { + return this.out.toString(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/StackIdTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/StackIdTests.java new file mode 100644 index 000000000000..3204772eabe9 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/StackIdTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.docker.type.Image; +import org.springframework.boot.buildpack.platform.docker.type.ImageConfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link StackId}. + * + * @author Phillip Webb + */ +class StackIdTests { + + @Test + void fromImageWhenImageIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> StackId.fromImage(null)) + .withMessage("Image must not be null"); + } + + @Test + void fromImageWhenLabelIsMissingThrowsException() { + Image image = mock(Image.class); + ImageConfig imageConfig = mock(ImageConfig.class); + given(image.getConfig()).willReturn(imageConfig); + assertThatIllegalStateException().isThrownBy(() -> StackId.fromImage(image)) + .withMessage("Missing 'io.buildpacks.stack.id' stack label"); + } + + @Test + void fromImageCreatesStackId() { + Image image = mock(Image.class); + ImageConfig imageConfig = mock(ImageConfig.class); + given(image.getConfig()).willReturn(imageConfig); + given(imageConfig.getLabels()).willReturn(Collections.singletonMap("io.buildpacks.stack.id", "test")); + StackId stackId = StackId.fromImage(image); + assertThat(stackId.toString()).isEqualTo("test"); + } + + @Test + void ofCreatesStackId() { + StackId stackId = StackId.of("test"); + assertThat(stackId.toString()).isEqualTo("test"); + } + + @Test + void equalsAndHashCode() { + StackId s1 = StackId.of("a"); + StackId s2 = StackId.of("a"); + StackId s3 = StackId.of("b"); + assertThat(s1.hashCode()).isEqualTo(s2.hashCode()); + assertThat(s1).isEqualTo(s1).isEqualTo(s2).isNotEqualTo(s3); + } + + @Test + void toStringReturnsValue() { + StackId stackId = StackId.of("test"); + assertThat(stackId.toString()).isEqualTo("test"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/TarGzipBuildpackTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/TarGzipBuildpackTests.java new file mode 100644 index 000000000000..9bc68cb8bbf2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/TarGzipBuildpackTests.java @@ -0,0 +1,94 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.io.File; +import java.nio.file.Path; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link TarGzipBuildpack}. + * + * @author Scott Frederick + */ +class TarGzipBuildpackTests { + + private File buildpackDir; + + private TestTarGzip testTarGzip; + + private BuildpackResolverContext resolverContext; + + @BeforeEach + void setUp(@TempDir File temp) { + this.buildpackDir = new File(temp, "buildpack"); + this.buildpackDir.mkdirs(); + this.testTarGzip = new TestTarGzip(this.buildpackDir); + this.resolverContext = mock(BuildpackResolverContext.class); + } + + @Test + void resolveWhenFilePathReturnsBuildpack() throws Exception { + Path compressedArchive = this.testTarGzip.createArchive(); + BuildpackReference reference = BuildpackReference.of(compressedArchive.toString()); + Buildpack buildpack = TarGzipBuildpack.resolve(this.resolverContext, reference); + assertThat(buildpack).isNotNull(); + assertThat(buildpack.getCoordinates()).hasToString("example/buildpack1@0.0.1"); + this.testTarGzip.assertHasExpectedLayers(buildpack); + } + + @Test + void resolveWhenFileUrlReturnsBuildpack() throws Exception { + Path compressedArchive = this.testTarGzip.createArchive(); + BuildpackReference reference = BuildpackReference.of("file://" + compressedArchive.toString()); + Buildpack buildpack = TarGzipBuildpack.resolve(this.resolverContext, reference); + assertThat(buildpack).isNotNull(); + assertThat(buildpack.getCoordinates()).hasToString("example/buildpack1@0.0.1"); + this.testTarGzip.assertHasExpectedLayers(buildpack); + } + + @Test + void resolveWhenArchiveWithoutDescriptorThrowsException() throws Exception { + Path compressedArchive = this.testTarGzip.createEmptyArchive(); + BuildpackReference reference = BuildpackReference.of(compressedArchive.toString()); + assertThatIllegalArgumentException().isThrownBy(() -> TarGzipBuildpack.resolve(this.resolverContext, reference)) + .withMessageContaining("Buildpack descriptor 'buildpack.toml' is required") + .withMessageContaining(compressedArchive.toString()); + } + + @Test + void resolveWhenArchiveWithDirectoryReturnsNull() { + BuildpackReference reference = BuildpackReference.of(this.buildpackDir.getAbsolutePath()); + Buildpack buildpack = TarGzipBuildpack.resolve(this.resolverContext, reference); + assertThat(buildpack).isNull(); + } + + @Test + void resolveWhenArchiveThatDoesNotExistReturnsNull() { + BuildpackReference reference = BuildpackReference.of("/test/i/am/missing/buildpack.tar"); + Buildpack buildpack = TarGzipBuildpack.resolve(this.resolverContext, reference); + assertThat(buildpack).isNull(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/TestBuildpack.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/TestBuildpack.java new file mode 100644 index 000000000000..8775f4c03654 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/TestBuildpack.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.io.IOException; + +import org.springframework.boot.buildpack.platform.docker.type.Layer; +import org.springframework.boot.buildpack.platform.io.Content; +import org.springframework.boot.buildpack.platform.io.IOConsumer; +import org.springframework.boot.buildpack.platform.io.Layout; +import org.springframework.boot.buildpack.platform.io.Owner; + +/** + * A test {@link Buildpack}. + * + * @author Scott Frederick + * @author Phillip Webb + */ +class TestBuildpack implements Buildpack { + + private final BuildpackCoordinates coordinates; + + TestBuildpack(String id, String version) { + this.coordinates = BuildpackCoordinates.of(id, version); + } + + @Override + public BuildpackCoordinates getCoordinates() { + return this.coordinates; + } + + @Override + public void apply(IOConsumer layers) throws IOException { + layers.accept(Layer.of(this::getContent)); + } + + private void getContent(Layout layout) throws IOException { + String id = this.coordinates.getSanitizedId(); + String dir = "/cnb/buildpacks/" + id + "/" + this.coordinates.getVersion(); + layout.file(dir + "/buildpack.toml", Owner.ROOT, Content.of("[test]")); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/TestTarGzip.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/TestTarGzip.java new file mode 100644 index 000000000000..4c50d71c4d1a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/TestTarGzip.java @@ -0,0 +1,134 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream; +import org.apache.commons.compress.utils.IOUtils; + +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Utility to create test tgz files. + * + * @author Scott Frederick + */ +class TestTarGzip { + + private final File buildpackDir; + + TestTarGzip(File buildpackDir) { + this.buildpackDir = buildpackDir; + } + + Path createArchive() throws Exception { + return createArchive(true); + } + + Path createEmptyArchive() throws Exception { + return createArchive(false); + } + + private Path createArchive(boolean addContent) throws Exception { + Path path = Paths.get(this.buildpackDir.getAbsolutePath(), "buildpack.tar"); + Path archive = Files.createFile(path); + if (addContent) { + writeBuildpackContentToArchive(archive); + } + return compressBuildpackArchive(archive); + } + + private Path compressBuildpackArchive(Path archive) throws Exception { + Path tgzPath = Paths.get(this.buildpackDir.getAbsolutePath(), "buildpack.tgz"); + FileCopyUtils.copy(Files.newInputStream(archive), + new GzipCompressorOutputStream(Files.newOutputStream(tgzPath))); + return tgzPath; + } + + private void writeBuildpackContentToArchive(Path archive) throws Exception { + StringBuilder buildpackToml = new StringBuilder(); + buildpackToml.append("[buildpack]\n"); + buildpackToml.append("id = \"example/buildpack1\"\n"); + buildpackToml.append("version = \"0.0.1\"\n"); + buildpackToml.append("name = \"Example buildpack\"\n"); + buildpackToml.append("homepage = \"https://github.com/example/example-buildpack\"\n"); + buildpackToml.append("[[stacks]]\n"); + buildpackToml.append("id = \"io.buildpacks.stacks.bionic\"\n"); + String detectScript = "#!/usr/bin/env bash\n" + "echo \"---> detect\"\n"; + String buildScript = "#!/usr/bin/env bash\n" + "echo \"---> build\"\n"; + try (TarArchiveOutputStream tar = new TarArchiveOutputStream(Files.newOutputStream(archive))) { + writeEntry(tar, "buildpack.toml", buildpackToml.toString()); + writeEntry(tar, "bin/"); + writeEntry(tar, "bin/detect", detectScript); + writeEntry(tar, "bin/build", buildScript); + tar.finish(); + } + } + + private void writeEntry(TarArchiveOutputStream tar, String entryName) throws IOException { + TarArchiveEntry entry = new TarArchiveEntry(entryName); + tar.putArchiveEntry(entry); + tar.closeArchiveEntry(); + } + + private void writeEntry(TarArchiveOutputStream tar, String entryName, String content) throws IOException { + TarArchiveEntry entry = new TarArchiveEntry(entryName); + entry.setSize(content.length()); + tar.putArchiveEntry(entry); + IOUtils.copy(new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)), tar); + tar.closeArchiveEntry(); + } + + void assertHasExpectedLayers(Buildpack buildpack) throws IOException { + List layers = new ArrayList<>(); + buildpack.apply((layer) -> { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + layer.writeTo(out); + layers.add(out); + }); + assertThat(layers).hasSize(1); + byte[] content = layers.get(0).toByteArray(); + try (TarArchiveInputStream tar = new TarArchiveInputStream(new ByteArrayInputStream(content))) { + assertThat(tar.getNextEntry().getName()).isEqualTo("cnb/"); + assertThat(tar.getNextEntry().getName()).isEqualTo("cnb/buildpacks/"); + assertThat(tar.getNextEntry().getName()).isEqualTo("cnb/buildpacks/example_buildpack1/"); + assertThat(tar.getNextEntry().getName()).isEqualTo("cnb/buildpacks/example_buildpack1/0.0.1/"); + assertThat(tar.getNextEntry().getName()) + .isEqualTo("cnb/buildpacks/example_buildpack1/0.0.1/buildpack.toml"); + assertThat(tar.getNextEntry().getName()).isEqualTo("cnb/buildpacks/example_buildpack1/0.0.1/bin/"); + assertThat(tar.getNextEntry().getName()).isEqualTo("cnb/buildpacks/example_buildpack1/0.0.1/bin/detect"); + assertThat(tar.getNextEntry().getName()).isEqualTo("cnb/buildpacks/example_buildpack1/0.0.1/bin/build"); + assertThat(tar.getNextEntry()).isNull(); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiIntegrationTests.java new file mode 100644 index 000000000000..b5242e2be6ee --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiIntegrationTests.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.docker.type.ImageReference; +import org.springframework.boot.testsupport.testcontainers.DisabledIfDockerUnavailable; + +/** + * Integration tests for {@link DockerApi}. + * + * @author Phillip Webb + */ +@DisabledIfDockerUnavailable +class DockerApiIntegrationTests { + + private final DockerApi docker = new DockerApi(); + + @Test + void pullImage() throws IOException { + this.docker.image().pull(ImageReference.of("gcr.io/paketo-buildpacks/builder:base"), + new TotalProgressPullListener(new TotalProgressBar("Pulling: "))); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java new file mode 100644 index 000000000000..519bab6e525c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java @@ -0,0 +1,532 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.boot.buildpack.platform.docker.DockerApi.ContainerApi; +import org.springframework.boot.buildpack.platform.docker.DockerApi.ImageApi; +import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi; +import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport; +import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport.Response; +import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig; +import org.springframework.boot.buildpack.platform.docker.type.ContainerContent; +import org.springframework.boot.buildpack.platform.docker.type.ContainerReference; +import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus; +import org.springframework.boot.buildpack.platform.docker.type.Image; +import org.springframework.boot.buildpack.platform.docker.type.ImageArchive; +import org.springframework.boot.buildpack.platform.docker.type.ImageReference; +import org.springframework.boot.buildpack.platform.docker.type.VolumeName; +import org.springframework.boot.buildpack.platform.io.Content; +import org.springframework.boot.buildpack.platform.io.IOConsumer; +import org.springframework.boot.buildpack.platform.io.Owner; +import org.springframework.boot.buildpack.platform.io.TarArchive; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; + +/** + * Tests for {@link DockerApi}. + * + * @author Phillip Webb + * @author Scott Frederick + */ +@ExtendWith(MockitoExtension.class) +class DockerApiTests { + + private static final String API_URL = "/" + DockerApi.API_VERSION; + + private static final String IMAGES_URL = API_URL + "/images"; + + private static final String CONTAINERS_URL = API_URL + "/containers"; + + private static final String VOLUMES_URL = API_URL + "/volumes"; + + @Mock + private HttpTransport http; + + private DockerApi dockerApi; + + @BeforeEach + void setup() { + this.dockerApi = new DockerApi(this.http); + } + + private HttpTransport http() { + return this.http; + } + + private Response emptyResponse() { + return responseOf(null); + } + + private Response responseOf(String name) { + return new Response() { + + @Override + public void close() { + } + + @Override + public InputStream getContent() { + if (name == null) { + return null; + } + return getClass().getResourceAsStream(name); + } + + }; + } + + @Test + void createDockerApi() { + DockerApi api = new DockerApi(); + assertThat(api).isNotNull(); + } + + @Nested + class ImageDockerApiTests { + + private ImageApi api; + + @Mock + private UpdateListener pullListener; + + @Mock + private UpdateListener pushListener; + + @Mock + private UpdateListener loadListener; + + @Captor + private ArgumentCaptor> writer; + + @BeforeEach + void setup() { + this.api = DockerApiTests.this.dockerApi.image(); + } + + @Test + void pullWhenReferenceIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.api.pull(null, this.pullListener)) + .withMessage("Reference must not be null"); + } + + @Test + void pullWhenListenerIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.api.pull(ImageReference.of("ubuntu"), null)) + .withMessage("Listener must not be null"); + } + + @Test + void pullPullsImageAndProducesEvents() throws Exception { + ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base"); + URI createUri = new URI(IMAGES_URL + "/create?fromImage=gcr.io%2Fpaketo-buildpacks%2Fbuilder%3Abase"); + String imageHash = "4acb6bfd6c4f0cabaf7f3690e444afe51f1c7de54d51da7e63fac709c56f1c30"; + URI imageUri = new URI(IMAGES_URL + "/gcr.io/paketo-buildpacks/builder@sha256:" + imageHash + "/json"); + given(http().post(eq(createUri), isNull())).willReturn(responseOf("pull-stream.json")); + given(http().get(imageUri)).willReturn(responseOf("type/image.json")); + Image image = this.api.pull(reference, this.pullListener); + assertThat(image.getLayers()).hasSize(46); + InOrder ordered = inOrder(this.pullListener); + ordered.verify(this.pullListener).onStart(); + ordered.verify(this.pullListener, times(595)).onUpdate(any()); + ordered.verify(this.pullListener).onFinish(); + } + + @Test + void pullWithRegistryAuthPullsImageAndProducesEvents() throws Exception { + ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base"); + URI createUri = new URI(IMAGES_URL + "/create?fromImage=gcr.io%2Fpaketo-buildpacks%2Fbuilder%3Abase"); + String imageHash = "4acb6bfd6c4f0cabaf7f3690e444afe51f1c7de54d51da7e63fac709c56f1c30"; + URI imageUri = new URI(IMAGES_URL + "/gcr.io/paketo-buildpacks/builder@sha256:" + imageHash + "/json"); + given(http().post(eq(createUri), eq("auth token"))).willReturn(responseOf("pull-stream.json")); + given(http().get(imageUri)).willReturn(responseOf("type/image.json")); + Image image = this.api.pull(reference, this.pullListener, "auth token"); + assertThat(image.getLayers()).hasSize(46); + InOrder ordered = inOrder(this.pullListener); + ordered.verify(this.pullListener).onStart(); + ordered.verify(this.pullListener, times(595)).onUpdate(any()); + ordered.verify(this.pullListener).onFinish(); + } + + @Test + void pushWhenReferenceIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.api.push(null, this.pushListener, null)) + .withMessage("Reference must not be null"); + } + + @Test + void pushWhenListenerIsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.api.push(ImageReference.of("ubuntu"), null, null)) + .withMessage("Listener must not be null"); + } + + @Test + void pushPushesImageAndProducesEvents() throws Exception { + ImageReference reference = ImageReference.of("localhost:5000/ubuntu"); + URI pushUri = new URI(IMAGES_URL + "/localhost:5000/ubuntu/push"); + given(http().post(pushUri, "auth token")).willReturn(responseOf("push-stream.json")); + this.api.push(reference, this.pushListener, "auth token"); + InOrder ordered = inOrder(this.pushListener); + ordered.verify(this.pushListener).onStart(); + ordered.verify(this.pushListener, times(44)).onUpdate(any()); + ordered.verify(this.pushListener).onFinish(); + } + + @Test + void pushWithErrorInStreamThrowsException() throws Exception { + ImageReference reference = ImageReference.of("localhost:5000/ubuntu"); + URI pushUri = new URI(IMAGES_URL + "/localhost:5000/ubuntu/push"); + given(http().post(pushUri, "auth token")).willReturn(responseOf("push-stream-with-error.json")); + assertThatIllegalStateException() + .isThrownBy(() -> this.api.push(reference, this.pushListener, "auth token")) + .withMessageContaining("test message"); + } + + @Test + void loadWhenArchiveIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.api.load(null, UpdateListener.none())) + .withMessage("Archive must not be null"); + } + + @Test + void loadWhenListenerIsNullThrowsException() { + ImageArchive archive = mock(ImageArchive.class); + assertThatIllegalArgumentException().isThrownBy(() -> this.api.load(archive, null)) + .withMessage("Listener must not be null"); + } + + @Test // gh-23130 + void loadWithEmptyResponseThrowsException() throws Exception { + Image image = Image.of(getClass().getResourceAsStream("type/image.json")); + ImageArchive archive = ImageArchive.from(image); + URI loadUri = new URI(IMAGES_URL + "/load"); + given(http().post(eq(loadUri), eq("application/x-tar"), any())).willReturn(emptyResponse()); + assertThatIllegalStateException().isThrownBy(() -> this.api.load(archive, this.loadListener)) + .withMessageContaining("Invalid response received"); + } + + @Test + void loadLoadsImage() throws Exception { + Image image = Image.of(getClass().getResourceAsStream("type/image.json")); + ImageArchive archive = ImageArchive.from(image); + URI loadUri = new URI(IMAGES_URL + "/load"); + given(http().post(eq(loadUri), eq("application/x-tar"), any())).willReturn(responseOf("load-stream.json")); + this.api.load(archive, this.loadListener); + InOrder ordered = inOrder(this.loadListener); + ordered.verify(this.loadListener).onStart(); + ordered.verify(this.loadListener).onUpdate(any()); + ordered.verify(this.loadListener).onFinish(); + then(http()).should().post(any(), any(), this.writer.capture()); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + this.writer.getValue().accept(out); + assertThat(out.toByteArray()).hasSizeGreaterThan(21000); + } + + @Test + void removeWhenReferenceIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.api.remove(null, true)) + .withMessage("Reference must not be null"); + } + + @Test + void removeRemovesContainer() throws Exception { + ImageReference reference = ImageReference + .of("ubuntu@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + URI removeUri = new URI(IMAGES_URL + + "/docker.io/library/ubuntu@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + given(http().delete(removeUri)).willReturn(emptyResponse()); + this.api.remove(reference, false); + then(http()).should().delete(removeUri); + } + + @Test + void removeWhenForceIsTrueRemovesContainer() throws Exception { + ImageReference reference = ImageReference + .of("ubuntu@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + URI removeUri = new URI(IMAGES_URL + + "/docker.io/library/ubuntu@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d?force=1"); + given(http().delete(removeUri)).willReturn(emptyResponse()); + this.api.remove(reference, true); + then(http()).should().delete(removeUri); + } + + @Test + void inspectWhenReferenceIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.api.inspect(null)) + .withMessage("Reference must not be null"); + } + + @Test + void inspectInspectImage() throws Exception { + ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base"); + URI imageUri = new URI(IMAGES_URL + "/gcr.io/paketo-buildpacks/builder:base/json"); + given(http().get(imageUri)).willReturn(responseOf("type/image.json")); + Image image = this.api.inspect(reference); + assertThat(image.getLayers()).hasSize(46); + } + + @Test + void exportLayersWhenReferenceIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.api.exportLayers(null, (name, archive) -> { + })).withMessage("Reference must not be null"); + } + + @Test + void exportLayersWhenExportsIsNullThrowsException() { + ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base"); + assertThatIllegalArgumentException().isThrownBy(() -> this.api.exportLayers(reference, null)) + .withMessage("Exports must not be null"); + } + + @Test + void exportLayersExportsLayerTars() throws Exception { + ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base"); + URI exportUri = new URI(IMAGES_URL + "/gcr.io/paketo-buildpacks/builder:base/get"); + given(DockerApiTests.this.http.get(exportUri)).willReturn(responseOf("export.tar")); + MultiValueMap contents = new LinkedMultiValueMap<>(); + this.api.exportLayers(reference, (name, archive) -> { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + archive.writeTo(out); + try (TarArchiveInputStream in = new TarArchiveInputStream( + new ByteArrayInputStream(out.toByteArray()))) { + TarArchiveEntry entry = in.getNextTarEntry(); + while (entry != null) { + contents.add(name, entry.getName()); + entry = in.getNextTarEntry(); + } + } + }); + assertThat(contents).hasSize(3).containsKeys( + "1bf6c63a1e9ed1dd7cb961273bf60b8e0f440361faf273baf866f408e4910601/layer.tar", + "8fdfb915302159a842cbfae6faec5311b00c071ebf14e12da7116ae7532e9319/layer.tar", + "93cd584bb189bfca4f51744bd19d836fd36da70710395af5a1523ee88f208c6a/layer.tar"); + assertThat(contents.get("1bf6c63a1e9ed1dd7cb961273bf60b8e0f440361faf273baf866f408e4910601/layer.tar")) + .containsExactly("etc/", "etc/apt/", "etc/apt/sources.list"); + } + + } + + @Nested + class ContainerDockerApiTests { + + private ContainerApi api; + + @Captor + private ArgumentCaptor> writer; + + @Mock + private UpdateListener logListener; + + @BeforeEach + void setup() { + this.api = DockerApiTests.this.dockerApi.container(); + } + + @Test + void createWhenConfigIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.api.create(null)) + .withMessage("Config must not be null"); + } + + @Test + void createCreatesContainer() throws Exception { + ImageReference imageReference = ImageReference.of("ubuntu:bionic"); + ContainerConfig config = ContainerConfig.of(imageReference, (update) -> update.withCommand("/bin/bash")); + URI createUri = new URI(CONTAINERS_URL + "/create"); + given(http().post(eq(createUri), eq("application/json"), any())) + .willReturn(responseOf("create-container-response.json")); + ContainerReference containerReference = this.api.create(config); + assertThat(containerReference.toString()).isEqualTo("e90e34656806"); + then(http()).should().post(any(), any(), this.writer.capture()); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + this.writer.getValue().accept(out); + assertThat(out.toByteArray().length).isEqualTo(config.toString().length()); + } + + @Test + void createWhenHasContentContainerWithContent() throws Exception { + ImageReference imageReference = ImageReference.of("ubuntu:bionic"); + ContainerConfig config = ContainerConfig.of(imageReference, (update) -> update.withCommand("/bin/bash")); + TarArchive archive = TarArchive.of((layout) -> { + layout.directory("/test", Owner.ROOT); + layout.file("/test/file", Owner.ROOT, Content.of("test")); + }); + ContainerContent content = ContainerContent.of(archive); + URI createUri = new URI(CONTAINERS_URL + "/create"); + given(http().post(eq(createUri), eq("application/json"), any())) + .willReturn(responseOf("create-container-response.json")); + URI uploadUri = new URI(CONTAINERS_URL + "/e90e34656806/archive?path=%2F"); + given(http().put(eq(uploadUri), eq("application/x-tar"), any())).willReturn(emptyResponse()); + ContainerReference containerReference = this.api.create(config, content); + assertThat(containerReference.toString()).isEqualTo("e90e34656806"); + then(http()).should().post(any(), any(), this.writer.capture()); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + this.writer.getValue().accept(out); + assertThat(out.toByteArray().length).isEqualTo(config.toString().length()); + then(http()).should().put(any(), any(), this.writer.capture()); + this.writer.getValue().accept(out); + assertThat(out.toByteArray()).hasSizeGreaterThan(2000); + } + + @Test + void startWhenReferenceIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.api.start(null)) + .withMessage("Reference must not be null"); + } + + @Test + void startStartsContainer() throws Exception { + ContainerReference reference = ContainerReference.of("e90e34656806"); + URI startContainerUri = new URI(CONTAINERS_URL + "/e90e34656806/start"); + given(http().post(startContainerUri)).willReturn(emptyResponse()); + this.api.start(reference); + then(http()).should().post(startContainerUri); + } + + @Test + void logsWhenReferenceIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.api.logs(null, UpdateListener.none())) + .withMessage("Reference must not be null"); + } + + @Test + void logsWhenListenerIsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.api.logs(ContainerReference.of("e90e34656806"), null)) + .withMessage("Listener must not be null"); + } + + @Test + void logsProducesEvents() throws Exception { + ContainerReference reference = ContainerReference.of("e90e34656806"); + URI logsUri = new URI(CONTAINERS_URL + "/e90e34656806/logs?stdout=1&stderr=1&follow=1"); + given(http().get(logsUri)).willReturn(responseOf("log-update-event.stream")); + this.api.logs(reference, this.logListener); + InOrder ordered = inOrder(this.logListener); + ordered.verify(this.logListener).onStart(); + ordered.verify(this.logListener, times(7)).onUpdate(any()); + ordered.verify(this.logListener).onFinish(); + } + + @Test + void waitWhenReferenceIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.api.wait(null)) + .withMessage("Reference must not be null"); + } + + @Test + void waitReturnsStatus() throws Exception { + ContainerReference reference = ContainerReference.of("e90e34656806"); + URI waitUri = new URI(CONTAINERS_URL + "/e90e34656806/wait"); + given(http().post(waitUri)).willReturn(responseOf("container-wait-response.json")); + ContainerStatus status = this.api.wait(reference); + assertThat(status.getStatusCode()).isEqualTo(1); + } + + @Test + void removeWhenReferenceIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.api.remove(null, true)) + .withMessage("Reference must not be null"); + } + + @Test + void removeRemovesContainer() throws Exception { + ContainerReference reference = ContainerReference.of("e90e34656806"); + URI removeUri = new URI(CONTAINERS_URL + "/e90e34656806"); + given(http().delete(removeUri)).willReturn(emptyResponse()); + this.api.remove(reference, false); + then(http()).should().delete(removeUri); + } + + @Test + void removeWhenForceIsTrueRemovesContainer() throws Exception { + ContainerReference reference = ContainerReference.of("e90e34656806"); + URI removeUri = new URI(CONTAINERS_URL + "/e90e34656806?force=1"); + given(http().delete(removeUri)).willReturn(emptyResponse()); + this.api.remove(reference, true); + then(http()).should().delete(removeUri); + } + + } + + @Nested + class VolumeDockerApiTests { + + private VolumeApi api; + + @BeforeEach + void setup() { + this.api = DockerApiTests.this.dockerApi.volume(); + } + + @Test + void deleteWhenNameIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.api.delete(null, false)) + .withMessage("Name must not be null"); + } + + @Test + void deleteDeletesContainer() throws Exception { + VolumeName name = VolumeName.of("test"); + URI removeUri = new URI(VOLUMES_URL + "/test"); + given(http().delete(removeUri)).willReturn(emptyResponse()); + this.api.delete(name, false); + then(http()).should().delete(removeUri); + } + + @Test + void deleteWhenForceIsTrueDeletesContainer() throws Exception { + VolumeName name = VolumeName.of("test"); + URI removeUri = new URI(VOLUMES_URL + "/test?force=1"); + given(http().delete(removeUri)).willReturn(emptyResponse()); + this.api.delete(name, true); + then(http()).should().delete(removeUri); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/LoadImageUpdateEventTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/LoadImageUpdateEventTests.java new file mode 100644 index 000000000000..40fa5a1e4168 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/LoadImageUpdateEventTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.docker.ProgressUpdateEvent.ProgressDetail; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link LoadImageUpdateEvent}. + * + * @author Phillip Webb + * @author Scott Frederick + */ +class LoadImageUpdateEventTests extends ProgressUpdateEventTests { + + @Test + void getStreamReturnsStream() { + LoadImageUpdateEvent event = createEvent(); + assertThat(event.getStream()).isEqualTo("stream"); + } + + @Override + protected LoadImageUpdateEvent createEvent(String status, ProgressDetail progressDetail, String progress) { + return new LoadImageUpdateEvent("stream", status, progressDetail, progress); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/LogUpdateEventTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/LogUpdateEventTests.java new file mode 100644 index 000000000000..e8a6fc429793 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/LogUpdateEventTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link LogUpdateEvent}. + * + * @author Phillip Webb + */ +class LogUpdateEventTests { + + @Test + void readAllWhenSimpleStreamReturnsEvents() throws Exception { + List events = readAll("log-update-event.stream"); + assertThat(events).hasSize(7); + assertThat(events.get(0).toString()) + .isEqualTo("Analyzing image '307c032c4ceaa6330b6c02af945a1fe56a8c3c27c28268574b217c1d38b093cf'"); + assertThat(events.get(1).toString()) + .isEqualTo("Writing metadata for uncached layer 'org.cloudfoundry.openjdk:openjdk-jre'"); + assertThat(events.get(2).toString()) + .isEqualTo("Using cached launch layer 'org.cloudfoundry.jvmapplication:executable-jar'"); + } + + @Test + void readAllWhenAnsiStreamReturnsEvents() throws Exception { + List events = readAll("log-update-event-ansi.stream"); + assertThat(events).hasSize(20); + assertThat(events.get(0).toString()).isEqualTo(""); + assertThat(events.get(1).toString()).isEqualTo("Cloud Foundry OpenJDK Buildpack v1.0.64"); + assertThat(events.get(2).toString()).isEqualTo(" OpenJDK JRE 11.0.5: Reusing cached layer"); + } + + @Test + void readSucceedsWhenStreamTypeIsInvalid() throws IOException { + List events = readAll("log-update-event-invalid-stream-type.stream"); + assertThat(events).hasSize(1); + assertThat(events.get(0).toString()).isEqualTo("Stream type is out of bounds. Must be >= 0 and < 3, but was 3"); + } + + private List readAll(String name) throws IOException { + List events = new ArrayList<>(); + try (InputStream inputStream = getClass().getResourceAsStream(name)) { + LogUpdateEvent.readAll(inputStream, events::add); + } + return events; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ProgressUpdateEventTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ProgressUpdateEventTests.java new file mode 100644 index 000000000000..421f9e94039d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ProgressUpdateEventTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.docker.ProgressUpdateEvent.ProgressDetail; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ProgressUpdateEvent}. + * + * @param The event type + * @author Phillip Webb + * @author Scott Frederick + */ +abstract class ProgressUpdateEventTests { + + @Test + void getStatusReturnsStatus() { + ProgressUpdateEvent event = createEvent(); + assertThat(event.getStatus()).isEqualTo("status"); + } + + @Test + void getProgressDetailsReturnsProgressDetails() { + ProgressUpdateEvent event = createEvent(); + assertThat(event.getProgressDetail().getCurrent()).isEqualTo(1); + assertThat(event.getProgressDetail().getTotal()).isEqualTo(2); + } + + @Test + void getProgressReturnsProgress() { + ProgressUpdateEvent event = createEvent(); + assertThat(event.getProgress()).isEqualTo("progress"); + } + + @Test + void progressDetailIsEmptyWhenCurrentIsNullReturnsTrue() { + ProgressDetail detail = new ProgressDetail(null, 2); + assertThat(ProgressDetail.isEmpty(detail)).isTrue(); + } + + @Test + void progressDetailIsEmptyWhenTotalIsNullReturnsTrue() { + ProgressDetail detail = new ProgressDetail(1, null); + assertThat(ProgressDetail.isEmpty(detail)).isTrue(); + } + + @Test + void progressDetailIsEmptyWhenTotalAndCurrentAreNotNullReturnsFalse() { + ProgressDetail detail = new ProgressDetail(1, 2); + assertThat(ProgressDetail.isEmpty(detail)).isFalse(); + } + + protected E createEvent() { + return createEvent("status", new ProgressDetail(1, 2), "progress"); + } + + protected abstract E createEvent(String status, ProgressDetail progressDetail, String progress); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/PullImageUpdateEventTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/PullImageUpdateEventTests.java new file mode 100644 index 000000000000..7cbe33a7deaa --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/PullImageUpdateEventTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.docker.ProgressUpdateEvent.ProgressDetail; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link PullImageUpdateEvent}. + * + * @author Phillip Webb + * @author Scott Frederick + */ +class PullImageUpdateEventTests extends ProgressUpdateEventTests { + + @Test + void getIdReturnsId() { + PullImageUpdateEvent event = createEvent(); + assertThat(event.getId()).isEqualTo("id"); + } + + @Override + protected PullImageUpdateEvent createEvent(String status, ProgressDetail progressDetail, String progress) { + return new PullImageUpdateEvent("id", status, progressDetail, progress); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/PullUpdateEventTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/PullUpdateEventTests.java new file mode 100644 index 000000000000..79bb5fd97bd6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/PullUpdateEventTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link PullImageUpdateEvent}. + * + * @author Phillip Webb + */ +class PullUpdateEventTests extends AbstractJsonTests { + + @Test + void readValueWhenFullDeserializesJson() throws Exception { + PullImageUpdateEvent event = getObjectMapper().readValue(getContent("pull-update-full.json"), + PullImageUpdateEvent.class); + assertThat(event.getId()).isEqualTo("4f4fb700ef54"); + assertThat(event.getStatus()).isEqualTo("Extracting"); + assertThat(event.getProgressDetail().getCurrent()).isEqualTo(16); + assertThat(event.getProgressDetail().getTotal()).isEqualTo(32); + assertThat(event.getProgress()).isEqualTo("[==================================================>] 32B/32B"); + } + + @Test + void readValueWhenMinimalDeserializesJson() throws Exception { + PullImageUpdateEvent event = getObjectMapper().readValue(getContent("pull-update-minimal.json"), + PullImageUpdateEvent.class); + assertThat(event.getId()).isNull(); + assertThat(event.getStatus()).isEqualTo("Status: Downloaded newer image for paketo-buildpacks/cnb:base"); + assertThat(event.getProgressDetail()).isNull(); + assertThat(event.getProgress()).isNull(); + } + + @Test + void readValueWhenEmptyDetailsDeserializesJson() throws Exception { + PullImageUpdateEvent event = getObjectMapper().readValue(getContent("pull-with-empty-details.json"), + PullImageUpdateEvent.class); + assertThat(event.getId()).isEqualTo("d837a2a1365e"); + assertThat(event.getStatus()).isEqualTo("Pulling fs layer"); + assertThat(event.getProgressDetail()).isNull(); + assertThat(event.getProgress()).isNull(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/PushImageUpdateEventTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/PushImageUpdateEventTests.java new file mode 100644 index 000000000000..913212055aae --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/PushImageUpdateEventTests.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.docker.ProgressUpdateEvent.ProgressDetail; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link PushImageUpdateEvent}. + * + * @author Scott Frederick + */ +class PushImageUpdateEventTests extends ProgressUpdateEventTests { + + @Test + void getIdReturnsId() { + PushImageUpdateEvent event = createEvent(); + assertThat(event.getId()).isEqualTo("id"); + } + + @Test + void getErrorReturnsErrorDetail() { + PushImageUpdateEvent event = new PushImageUpdateEvent(null, null, null, null, + new PushImageUpdateEvent.ErrorDetail("test message")); + assertThat(event.getErrorDetail().getMessage()).isEqualTo("test message"); + } + + @Override + protected PushImageUpdateEvent createEvent(String status, ProgressDetail progressDetail, String progress) { + return new PushImageUpdateEvent("id", status, progressDetail, progress, null); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/TotalProgressBarTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/TotalProgressBarTests.java new file mode 100644 index 000000000000..345e0d4544cc --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/TotalProgressBarTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link TotalProgressBar}. + * + * @author Phillip Webb + */ +class TotalProgressBarTests { + + @Test + void withPrefixAndBookends() { + TestPrintStream out = new TestPrintStream(); + TotalProgressBar bar = new TotalProgressBar("prefix:", '#', true, out); + assertThat(out).hasToString("prefix: [ "); + bar.accept(new TotalProgressEvent(10)); + assertThat(out.toString()).isEqualTo("prefix: [ #####"); + bar.accept(new TotalProgressEvent(50)); + assertThat(out.toString()).isEqualTo("prefix: [ #########################"); + bar.accept(new TotalProgressEvent(100)); + assertThat(out.toString()) + .isEqualTo(String.format("prefix: [ ################################################## ]%n")); + } + + @Test + void withoutPrefix() { + TestPrintStream out = new TestPrintStream(); + TotalProgressBar bar = new TotalProgressBar(null, '#', true, out); + assertThat(out).hasToString("[ "); + bar.accept(new TotalProgressEvent(10)); + assertThat(out.toString()).isEqualTo("[ #####"); + bar.accept(new TotalProgressEvent(50)); + assertThat(out.toString()).isEqualTo("[ #########################"); + bar.accept(new TotalProgressEvent(100)); + assertThat(out.toString()).isEqualTo(String.format("[ ################################################## ]%n")); + } + + @Test + void withoutBookends() { + TestPrintStream out = new TestPrintStream(); + TotalProgressBar bar = new TotalProgressBar("", '.', false, out); + assertThat(out).hasToString(""); + bar.accept(new TotalProgressEvent(10)); + assertThat(out.toString()).isEqualTo("....."); + bar.accept(new TotalProgressEvent(50)); + assertThat(out.toString()).isEqualTo("........................."); + bar.accept(new TotalProgressEvent(100)); + assertThat(out.toString()).isEqualTo(String.format("..................................................%n")); + } + + static class TestPrintStream extends PrintStream { + + TestPrintStream() { + super(new ByteArrayOutputStream()); + } + + @Override + public String toString() { + return this.out.toString(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/TotalProgressEventTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/TotalProgressEventTests.java new file mode 100644 index 000000000000..e018c3411607 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/TotalProgressEventTests.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link TotalProgressEvent}. + * + * @author Phillip Webb + */ +class TotalProgressEventTests { + + @Test + void create() { + assertThat(new TotalProgressEvent(0).getPercent()).isEqualTo(0); + assertThat(new TotalProgressEvent(10).getPercent()).isEqualTo(10); + assertThat(new TotalProgressEvent(100).getPercent()).isEqualTo(100); + } + + @Test + void createWhenPercentLessThanZeroThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new TotalProgressEvent(-1)) + .withMessage("Percent must be in the range 0 to 100"); + } + + @Test + void createWhenEventMoreThanOneHundredThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new TotalProgressEvent(101)) + .withMessage("Percent must be in the range 0 to 100"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/TotalProgressListenerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/TotalProgressListenerTests.java new file mode 100644 index 000000000000..1dd2db0e3dcf --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/TotalProgressListenerTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import com.fasterxml.jackson.annotation.JsonCreator; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; +import org.springframework.boot.buildpack.platform.json.JsonStream; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link TotalProgressPullListener}. + * + * @author Phillip Webb + * @author Scott Frederick + */ +class TotalProgressListenerTests extends AbstractJsonTests { + + @Test + void totalProgress() throws Exception { + List progress = new ArrayList<>(); + TestTotalProgressListener listener = new TestTotalProgressListener((event) -> progress.add(event.getPercent())); + run(listener); + int last = 0; + for (Integer update : progress) { + assertThat(update).isGreaterThanOrEqualTo(last); + last = update; + } + assertThat(last).isEqualTo(100); + } + + @Test + @Disabled("For visual inspection") + void totalProgressUpdatesSmoothly() throws Exception { + TestTotalProgressListener listener = new TestTotalProgressListener(new TotalProgressBar("Pulling layers:")); + run(listener); + } + + private void run(TestTotalProgressListener listener) throws IOException { + JsonStream jsonStream = new JsonStream(getObjectMapper()); + listener.onStart(); + jsonStream.get(getContent("pull-stream.json"), TestImageUpdateEvent.class, listener::onUpdate); + listener.onFinish(); + } + + private static class TestTotalProgressListener extends TotalProgressListener { + + TestTotalProgressListener(Consumer consumer) { + super(consumer, new String[] { "Pulling", "Downloading", "Extracting" }); + } + + @Override + public void onUpdate(TestImageUpdateEvent event) { + super.onUpdate(event); + try { + Thread.sleep(10); + } + catch (InterruptedException ex) { + } + } + + } + + private static class TestImageUpdateEvent extends ImageProgressUpdateEvent { + + @JsonCreator + TestImageUpdateEvent(String id, String status, ProgressDetail progressDetail, String progress) { + super(id, status, progressDetail, progress); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationTests.java new file mode 100644 index 000000000000..e5ee7f51b705 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.configuration; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DockerConfiguration}. + * + * @author Wei Jiang + * @author Scott Frederick + */ +class DockerConfigurationTests { + + @Test + void createDockerConfigurationWithDefaults() { + DockerConfiguration configuration = new DockerConfiguration(); + assertThat(configuration.getBuilderRegistryAuthentication()).isNull(); + } + + @Test + void createDockerConfigurationWithUserAuth() { + DockerConfiguration configuration = new DockerConfiguration().withBuilderRegistryUserAuthentication("user", + "secret", "https://docker.example.com", "docker@example.com"); + DockerRegistryAuthentication auth = configuration.getBuilderRegistryAuthentication(); + assertThat(auth).isNotNull(); + assertThat(auth).isInstanceOf(DockerRegistryUserAuthentication.class); + DockerRegistryUserAuthentication userAuth = (DockerRegistryUserAuthentication) auth; + assertThat(userAuth.getUrl()).isEqualTo("https://docker.example.com"); + assertThat(userAuth.getUsername()).isEqualTo("user"); + assertThat(userAuth.getPassword()).isEqualTo("secret"); + assertThat(userAuth.getEmail()).isEqualTo("docker@example.com"); + } + + @Test + void createDockerConfigurationWithTokenAuth() { + DockerConfiguration configuration = new DockerConfiguration().withBuilderRegistryTokenAuthentication("token"); + DockerRegistryAuthentication auth = configuration.getBuilderRegistryAuthentication(); + assertThat(auth).isNotNull(); + assertThat(auth).isInstanceOf(DockerRegistryTokenAuthentication.class); + DockerRegistryTokenAuthentication tokenAuth = (DockerRegistryTokenAuthentication) auth; + assertThat(tokenAuth.getToken()).isEqualTo("token"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryTokenAuthenticationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryTokenAuthenticationTests.java new file mode 100644 index 000000000000..272dbeb0b58d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryTokenAuthenticationTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.configuration; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; +import org.springframework.util.Base64Utils; +import org.springframework.util.StreamUtils; + +/** + * Tests for {@link DockerRegistryTokenAuthentication}. + * + * @author Scott Frederick + */ +class DockerRegistryTokenAuthenticationTests extends AbstractJsonTests { + + @Test + void createAuthHeaderReturnsEncodedHeader() throws IOException, JSONException { + DockerRegistryTokenAuthentication auth = new DockerRegistryTokenAuthentication("tokenvalue"); + String header = auth.getAuthHeader(); + String expectedJson = StreamUtils.copyToString(getContent("auth-token.json"), StandardCharsets.UTF_8); + JSONAssert.assertEquals(expectedJson, new String(Base64Utils.decodeFromUrlSafeString(header)), false); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryUserAuthenticationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryUserAuthenticationTests.java new file mode 100644 index 000000000000..d47208768770 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryUserAuthenticationTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.configuration; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; +import org.springframework.util.Base64Utils; +import org.springframework.util.StreamUtils; + +/** + * Tests for {@link DockerRegistryUserAuthentication}. + * + * @author Scott Frederick + */ +class DockerRegistryUserAuthenticationTests extends AbstractJsonTests { + + @Test + void createMinimalAuthHeaderReturnsEncodedHeader() throws IOException, JSONException { + DockerRegistryUserAuthentication auth = new DockerRegistryUserAuthentication("user", "secret", + "https://docker.example.com", "docker@example.com"); + JSONAssert.assertEquals(jsonContent("auth-user-full.json"), decoded(auth.getAuthHeader()), false); + } + + @Test + void createFullAuthHeaderReturnsEncodedHeader() throws IOException, JSONException { + DockerRegistryUserAuthentication auth = new DockerRegistryUserAuthentication("user", "secret", null, null); + JSONAssert.assertEquals(jsonContent("auth-user-minimal.json"), decoded(auth.getAuthHeader()), false); + } + + private String jsonContent(String s) throws IOException { + return StreamUtils.copyToString(getContent(s), StandardCharsets.UTF_8); + } + + private String decoded(String header) { + return new String(Base64Utils.decodeFromUrlSafeString(header)); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ssl/CertificateParserTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ssl/CertificateParserTests.java new file mode 100644 index 000000000000..479817196c0a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ssl/CertificateParserTests.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.ssl; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.cert.X509Certificate; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link CertificateParser}. + * + * @author Scott Frederick + */ +class CertificateParserTests { + + private PemFileWriter fileWriter; + + @BeforeEach + void setUp() throws IOException { + this.fileWriter = new PemFileWriter(); + } + + @AfterEach + void tearDown() throws IOException { + this.fileWriter.cleanup(); + } + + @Test + void parseCertificates() throws IOException { + Path caPath = this.fileWriter.writeFile("ca.pem", PemFileWriter.CA_CERTIFICATE); + Path certPath = this.fileWriter.writeFile("cert.pem", PemFileWriter.CERTIFICATE); + X509Certificate[] certificates = CertificateParser.parse(caPath, certPath); + assertThat(certificates).isNotNull(); + assertThat(certificates.length).isEqualTo(2); + assertThat(certificates[0].getType()).isEqualTo("X.509"); + assertThat(certificates[1].getType()).isEqualTo("X.509"); + } + + @Test + void parseCertificateChain() throws IOException { + Path path = this.fileWriter.writeFile("ca.pem", PemFileWriter.CA_CERTIFICATE, PemFileWriter.CERTIFICATE); + X509Certificate[] certificates = CertificateParser.parse(path); + assertThat(certificates).isNotNull(); + assertThat(certificates.length).isEqualTo(2); + assertThat(certificates[0].getType()).isEqualTo("X.509"); + assertThat(certificates[1].getType()).isEqualTo("X.509"); + } + + @Test + void parseWithInvalidPathWillThrowException() throws URISyntaxException { + Path path = Paths.get(new URI("file:///bad/path/cert.pem")); + assertThatIllegalStateException().isThrownBy(() -> CertificateParser.parse(path)) + .withMessageContaining(path.toString()); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ssl/KeyStoreFactoryTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ssl/KeyStoreFactoryTests.java new file mode 100644 index 000000000000..083f5d9b0431 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ssl/KeyStoreFactoryTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.ssl; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link KeyStoreFactory}. + * + * @author Scott Frederick + */ +class KeyStoreFactoryTests { + + private PemFileWriter fileWriter; + + @BeforeEach + void setUp() throws IOException { + this.fileWriter = new PemFileWriter(); + } + + @AfterEach + void tearDown() throws IOException { + this.fileWriter.cleanup(); + } + + @Test + void createKeyStoreWithCertChain() + throws IOException, KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException { + Path certPath = this.fileWriter.writeFile("cert.pem", PemFileWriter.CA_CERTIFICATE, PemFileWriter.CERTIFICATE); + KeyStore keyStore = KeyStoreFactory.create(certPath, null, "test-alias"); + assertThat(keyStore.containsAlias("test-alias-0")).isTrue(); + assertThat(keyStore.getCertificate("test-alias-0")).isNotNull(); + assertThat(keyStore.getKey("test-alias-0", new char[] {})).isNull(); + assertThat(keyStore.containsAlias("test-alias-1")).isTrue(); + assertThat(keyStore.getCertificate("test-alias-1")).isNotNull(); + assertThat(keyStore.getKey("test-alias-1", new char[] {})).isNull(); + Files.delete(certPath); + } + + @Test + void createKeyStoreWithCertChainAndPrivateKey() + throws IOException, KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException { + Path certPath = this.fileWriter.writeFile("cert.pem", PemFileWriter.CA_CERTIFICATE, PemFileWriter.CERTIFICATE); + Path keyPath = this.fileWriter.writeFile("key.pem", PemFileWriter.PRIVATE_KEY); + KeyStore keyStore = KeyStoreFactory.create(certPath, keyPath, "test-alias"); + assertThat(keyStore.containsAlias("test-alias")).isTrue(); + assertThat(keyStore.getCertificate("test-alias")).isNotNull(); + assertThat(keyStore.getKey("test-alias", new char[] {})).isNotNull(); + Files.delete(certPath); + Files.delete(keyPath); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ssl/PemFileWriter.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ssl/PemFileWriter.java new file mode 100644 index 000000000000..39f3c19ccc91 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ssl/PemFileWriter.java @@ -0,0 +1,122 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.ssl; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; + +import org.springframework.util.FileSystemUtils; + +/** + * Utility to write certificate and key PEM files for testing. + * + * @author Scott Frederick + */ +public class PemFileWriter { + + private static final String EXAMPLE_SECRET_QUALIFIER = "example"; + + public static final String CA_CERTIFICATE = "-----BEGIN TRUSTED CERTIFICATE-----\n" + + "MIIClzCCAgACCQCPbjkRoMVEQDANBgkqhkiG9w0BAQUFADCBjzELMAkGA1UEBhMC\n" + + "VVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28x\n" + + "DTALBgNVBAoMBFRlc3QxDTALBgNVBAsMBFRlc3QxFDASBgNVBAMMC2V4YW1wbGUu\n" + + "Y29tMR8wHQYJKoZIhvcNAQkBFhB0ZXN0QGV4YW1wbGUuY29tMB4XDTIwMDMyNzIx\n" + + "NTgwNFoXDTIxMDMyNzIxNTgwNFowgY8xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApD\n" + + "YWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMQ0wCwYDVQQKDARUZXN0\n" + + "MQ0wCwYDVQQLDARUZXN0MRQwEgYDVQQDDAtleGFtcGxlLmNvbTEfMB0GCSqGSIb3\n" + + "DQEJARYQdGVzdEBleGFtcGxlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC\n" + + "gYEA1YzixWEoyzrd20C2R1gjyPCoPfFLlG6UYTyT0tueNy6yjv6qbJ8lcZg7616O\n" + + "3I9LuOHhZh9U+fCDCgPfiDdyJfDEW/P+dsOMFyMUXPrJPze2yPpOnvV8iJ5DM93u\n" + + "fEVhCCyzLdYu0P2P3hU2W+T3/Im9DA7FOPA2vF1SrIJ2qtUCAwEAATANBgkqhkiG\n" + + "9w0BAQUFAAOBgQBdShkwUv78vkn1jAdtfbB+7mpV9tufVdo29j7pmotTCz3ny5fc\n" + + "zLEfeu6JPugAR71JYbc2CqGrMneSk1zT91EH6ohIz8OR5VNvzB7N7q65Ci7OFMPl\n" + + "ly6k3rHpMCBtHoyNFhNVfPLxGJ9VlWFKLgIAbCmL4OIQm1l6Fr1MSM38Zw==\n" + "-----END TRUSTED CERTIFICATE-----\n"; + + public static final String CA_PRIVATE_KEY = EXAMPLE_SECRET_QUALIFIER + "-----BEGIN PRIVATE KEY-----\n" + + "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBANWM4sVhKMs63dtA\n" + + "tkdYI8jwqD3xS5RulGE8k9Lbnjcuso7+qmyfJXGYO+tejtyPS7jh4WYfVPnwgwoD\n" + + "34g3ciXwxFvz/nbDjBcjFFz6yT83tsj6Tp71fIieQzPd7nxFYQgssy3WLtD9j94V\n" + + "Nlvk9/yJvQwOxTjwNrxdUqyCdqrVAgMBAAECgYEAyJTlZ8nj3Eg1nLxCue6C5jmN\n" + + "fWkIuanH+zFAE/0utdxJ4WA4yYAOVo1MMr8FZwu9bzHTWe2yDnWnT5/ltPeHYX2X\n" + + "9Pg5cY0tjq07utaMwLKWgJ0Xoh2UpVM799t/rSvMWmLaZ2c8nipX+gQfYJFpX8Vg\n" + + "mR3QPxwdmNyFo13qif0CQQD4z2SqCfARuxscTCJDZ6wReikMQxaJvq74lPEtT26L\n" + + "rBr/bN+mG7+rMEHxs5wtU47aNjUKuVVC0Qfhsf95ahvHAkEA27inSlxrwGvhvFsD\n" + + "FWdgDsfYpPZdL4YgpVSEvcoypRGg2suJw2omcKcY56XpkmWUqZc06QirumtnEC0P\n" + + "HfnsgwJBAMVhEURrOc13FxytsQiz96atuF6H4htH79o3ndQKDXI0B/7VSd6maLjP\n" + + "QaESkTTL8qldE1r8h4zH8m6zHC4fZQUCQFWJ+8bdWC2fUlBr9jVc+26Fqvf92aVo\n" + + "yEjVMKBamYDd7gt/9fAX4UM2KmH0m4wc89VaQoT+lSyMJ6GKiToYVFUCQEXcyoeO\n" + + "zWqtSgEX/eXQXzmMKxYnjv1O//ba3Q7UiHd/XO5j4QXAJpcB6h0h00uC5KY2d0Zy\n" + "JQ1kB1C2l6l9tyc=\n" + + "-----END PRIVATE KEY-----"; + + public static final String CERTIFICATE = "-----BEGIN CERTIFICATE-----\n" + + "MIICjzCCAfgCAQEwDQYJKoZIhvcNAQEFBQAwgY8xCzAJBgNVBAYTAlVTMRMwEQYD\n" + + "VQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMQ0wCwYDVQQK\n" + + "DARUZXN0MQ0wCwYDVQQLDARUZXN0MRQwEgYDVQQDDAtleGFtcGxlLmNvbTEfMB0G\n" + + "CSqGSIb3DQEJARYQdGVzdEBleGFtcGxlLmNvbTAeFw0yMDAzMjcyMjAxNDZaFw0y\n" + + "MTAzMjcyMjAxNDZaMIGPMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5p\n" + + "YTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwEVGVzdDENMAsGA1UE\n" + + "CwwEVGVzdDEUMBIGA1UEAwwLZXhhbXBsZS5jb20xHzAdBgkqhkiG9w0BCQEWEHRl\n" + + "c3RAZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAM7kd2cj\n" + + "F49wm1+OQ7Q5GE96cXueWNPr/Nwei71tf6G4BmE0B+suXHEvnLpHTj9pdX/ZzBIK\n" + + "8jIZ/x8RnSduK/Ky+zm1QMYUWZtWCAgCW8WzgB69Cn/hQG8KSX3S9bqODuQAvP54\n" + + "GQJD7+4kVuNBGjFb4DaD4nvMmPtALSZf8ZCZAgMBAAEwDQYJKoZIhvcNAQEFBQAD\n" + + "gYEAOn6X8+0VVlDjF+TvTgI0KIasA6nDm+KXe7LVtfvqWqQZH4qyd2uiwcDM3Aux\n" + + "a/OsPdOw0j+NqFDBd3mSMhSVgfvXdK6j9WaxY1VGXyaidLARgvn63wfzgr857sQW\n" + + "c8eSxbwEQxwlMvVxW6Os4VhCfUQr8VrBrvPa2zs+6IlK+Ug=\n" + "-----END CERTIFICATE-----\n"; + + public static final String PRIVATE_KEY = EXAMPLE_SECRET_QUALIFIER + "-----BEGIN RSA PRIVATE KEY-----\n" + + "MIICXAIBAAKBgQDO5HdnIxePcJtfjkO0ORhPenF7nljT6/zcHou9bX+huAZhNAfr\n" + + "LlxxL5y6R04/aXV/2cwSCvIyGf8fEZ0nbivysvs5tUDGFFmbVggIAlvFs4AevQp/\n" + + "4UBvCkl90vW6jg7kALz+eBkCQ+/uJFbjQRoxW+A2g+J7zJj7QC0mX/GQmQIDAQAB\n" + + "AoGAIWPsBWA7gDHrUYuzT5XbX5BiWlIfAezXPWtMoEDY1W/Oz8dG8+TilH3brJCv\n" + + "hzps9TpgXhUYK4/Yhdog4+k6/EEY80RvcObOnflazTCVS041B0Ipm27uZjIq2+1F\n" + + "ZfbWP+B3crpzh8wvIYA+6BCcZV9zi8Od32NEs39CtrOrFPUCQQDxnt9+JlWjtteR\n" + + "VttRSKjtzKIF08BzNuZlRP9HNWveLhphIvdwBfjASwqgtuslqziEnGG8kniWzyYB\n" + + "a/ZZVoT3AkEA2zSBMpvGPDkGbOMqbnR8UL3uijkOj+blQe1gsyu3dUa9T42O1u9h\n" + + "Iz5SdCYlSFHbDNRFrwuW2QnhippqIQqC7wJAbVeyWEpM0yu5XiJqWdyB5iuG3xA2\n" + + "tW0Q0p9ozvbT+9XtRiwmweFR8uOCybw9qexURV7ntAis3cKctmP/Neq7fQJBAKGa\n" + + "59UjutYTRIVqRJICFtR/8ii9P9sfYs1j7/KnvC0d5duMhU44VOjivW8b4Eic8F1Y\n" + + "8bbHWILSIhFJHg0V7skCQDa8/YkRWF/3pwIZNWQr4ce4OzvYsFMkRvGRdX8B2a0p\n" + + "wSKcVTdEdO2DhBlYddN0zG0rjq4vDMtdmldEl4BdldQ=\n" + "-----END RSA PRIVATE KEY-----\n"; + + private final Path tempDir; + + public PemFileWriter() throws IOException { + this.tempDir = Files.createTempDirectory("buildpack-platform-docker-ssl-tests"); + } + + Path writeFile(String name, String... contents) throws IOException { + Path path = Paths.get(this.tempDir.toString(), name); + for (String content : contents) { + Files.write(path, content.replaceAll(EXAMPLE_SECRET_QUALIFIER, "").getBytes(), StandardOpenOption.CREATE, + StandardOpenOption.APPEND); + } + return path; + } + + public Path getTempDir() { + return this.tempDir; + } + + void cleanup() throws IOException { + FileSystemUtils.deleteRecursively(this.tempDir); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ssl/PrivateKeyParserTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ssl/PrivateKeyParserTests.java new file mode 100644 index 000000000000..10896e86f77a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ssl/PrivateKeyParserTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.ssl; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.PrivateKey; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link PrivateKeyParser}. + * + * @author Scott Frederick + */ +class PrivateKeyParserTests { + + private PemFileWriter fileWriter; + + @BeforeEach + void setUp() throws IOException { + this.fileWriter = new PemFileWriter(); + } + + @AfterEach + void tearDown() throws IOException { + this.fileWriter.cleanup(); + } + + @Test + void parsePkcs8KeyFile() throws IOException { + Path path = this.fileWriter.writeFile("key.pem", PemFileWriter.CA_PRIVATE_KEY); + PrivateKey privateKey = PrivateKeyParser.parse(path); + assertThat(privateKey).isNotNull(); + assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); + Files.delete(path); + } + + @Test + void parsePkcs1KeyFile() throws IOException { + Path path = this.fileWriter.writeFile("key.pem", PemFileWriter.PRIVATE_KEY); + PrivateKey privateKey = PrivateKeyParser.parse(path); + assertThat(privateKey).isNotNull(); + // keys in PKCS#1 format are converted to PKCS#8 for parsing + assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); + Files.delete(path); + } + + @Test + void parseWithNonKeyFileWillThrowException() throws IOException { + Path path = this.fileWriter.writeFile("text.pem", "plain text"); + assertThatIllegalStateException().isThrownBy(() -> PrivateKeyParser.parse(path)) + .withMessageContaining(path.toString()); + Files.delete(path); + } + + @Test + void parseWithInvalidPathWillThrowException() throws URISyntaxException { + Path path = Paths.get(new URI("file:///bad/path/key.pem")); + assertThatIllegalStateException().isThrownBy(() -> PrivateKeyParser.parse(path)) + .withMessageContaining(path.toString()); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ssl/SslContextFactoryTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ssl/SslContextFactoryTests.java new file mode 100644 index 000000000000..0f370d5a8e53 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ssl/SslContextFactoryTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.ssl; + +import java.io.IOException; + +import javax.net.ssl.SSLContext; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SslContextFactory}. + * + * @author Scott Frederick + */ +class SslContextFactoryTests { + + private PemFileWriter fileWriter; + + @BeforeEach + void setUp() throws IOException { + this.fileWriter = new PemFileWriter(); + } + + @AfterEach + void tearDown() throws IOException { + this.fileWriter.cleanup(); + } + + @Test + void createKeyStoreWithCertChain() throws IOException { + this.fileWriter.writeFile("cert.pem", PemFileWriter.CERTIFICATE); + this.fileWriter.writeFile("key.pem", PemFileWriter.PRIVATE_KEY); + this.fileWriter.writeFile("ca.pem", PemFileWriter.CA_CERTIFICATE); + SSLContext sslContext = new SslContextFactory().forDirectory(this.fileWriter.getTempDir().toString()); + assertThat(sslContext).isNotNull(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/DockerConnectionExceptionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/DockerConnectionExceptionTests.java new file mode 100644 index 000000000000..0501c5364c14 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/DockerConnectionExceptionTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.transport; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link DockerEngineException}. + * + * @author Scott Frederick + */ +class DockerConnectionExceptionTests { + + private static final String HOST = "docker://localhost/"; + + @Test + void createWhenHostIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new DockerConnectionException(null, null)) + .withMessage("Host must not be null"); + } + + @Test + void createWhenCauseIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new DockerConnectionException(HOST, null)) + .withMessage("Cause must not be null"); + } + + @Test + void createWithIOException() { + DockerConnectionException exception = new DockerConnectionException(HOST, new IOException("error")); + assertThat(exception.getMessage()) + .contains("Connection to the Docker daemon at 'docker://localhost/' failed with error \"error\""); + } + + @Test + void createWithLastErrorException() { + DockerConnectionException exception = new DockerConnectionException(HOST, + new IOException(new com.sun.jna.LastErrorException("root cause"))); + assertThat(exception.getMessage()) + .contains("Connection to the Docker daemon at 'docker://localhost/' failed with error \"root cause\""); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/DockerEngineExceptionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/DockerEngineExceptionTests.java new file mode 100644 index 000000000000..d3a76a38ce85 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/DockerEngineExceptionTests.java @@ -0,0 +1,126 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.transport; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link DockerEngineException}. + * + * @author Phillip Webb + * @author Scott Frederick + */ +class DockerEngineExceptionTests { + + private static final String HOST = "docker://localhost/"; + + private static final URI URI; + static { + try { + URI = new URI("example"); + } + catch (URISyntaxException ex) { + throw new IllegalStateException(ex); + } + } + + private static final Errors NO_ERRORS = new Errors(Collections.emptyList()); + + private static final Errors ERRORS = new Errors(Collections.singletonList(new Errors.Error("code", "message"))); + + private static final Message NO_MESSAGE = new Message(null); + + private static final Message MESSAGE = new Message("response message"); + + @Test + void createWhenHostIsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new DockerEngineException(null, null, 404, null, NO_ERRORS, NO_MESSAGE)) + .withMessage("Host must not be null"); + } + + @Test + void createWhenUriIsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new DockerEngineException(HOST, null, 404, null, NO_ERRORS, NO_MESSAGE)) + .withMessage("URI must not be null"); + } + + @Test + void create() { + DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", ERRORS, MESSAGE); + assertThat(exception.getMessage()).isEqualTo( + "Docker API call to 'docker://localhost/example' failed with status code 404 \"missing\" and message \"response message\" [code: message]"); + assertThat(exception.getStatusCode()).isEqualTo(404); + assertThat(exception.getReasonPhrase()).isEqualTo("missing"); + assertThat(exception.getErrors()).isSameAs(ERRORS); + assertThat(exception.getResponseMessage()).isSameAs(MESSAGE); + } + + @Test + void createWhenReasonPhraseIsNull() { + DockerEngineException exception = new DockerEngineException(HOST, URI, 404, null, ERRORS, MESSAGE); + assertThat(exception.getMessage()).isEqualTo( + "Docker API call to 'docker://localhost/example' failed with status code 404 and message \"response message\" [code: message]"); + assertThat(exception.getStatusCode()).isEqualTo(404); + assertThat(exception.getReasonPhrase()).isNull(); + assertThat(exception.getErrors()).isSameAs(ERRORS); + assertThat(exception.getResponseMessage()).isSameAs(MESSAGE); + } + + @Test + void createWhenErrorsIsNull() { + DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", null, MESSAGE); + assertThat(exception.getMessage()).isEqualTo( + "Docker API call to 'docker://localhost/example' failed with status code 404 \"missing\" and message \"response message\""); + assertThat(exception.getErrors()).isNull(); + } + + @Test + void createWhenErrorsIsEmpty() { + DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", NO_ERRORS, MESSAGE); + assertThat(exception.getMessage()).isEqualTo( + "Docker API call to 'docker://localhost/example' failed with status code 404 \"missing\" and message \"response message\""); + assertThat(exception.getStatusCode()).isEqualTo(404); + assertThat(exception.getReasonPhrase()).isEqualTo("missing"); + assertThat(exception.getErrors()).isSameAs(NO_ERRORS); + } + + @Test + void createWhenMessageIsNull() { + DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", ERRORS, null); + assertThat(exception.getMessage()).isEqualTo( + "Docker API call to 'docker://localhost/example' failed with status code 404 \"missing\" [code: message]"); + assertThat(exception.getResponseMessage()).isNull(); + } + + @Test + void createWhenMessageIsEmpty() { + DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", ERRORS, NO_MESSAGE); + assertThat(exception.getMessage()).isEqualTo( + "Docker API call to 'docker://localhost/example' failed with status code 404 \"missing\" [code: message]"); + assertThat(exception.getResponseMessage()).isSameAs(NO_MESSAGE); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/ErrorsTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/ErrorsTests.java new file mode 100644 index 000000000000..8d47ba063362 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/ErrorsTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.transport; + +import java.util.Iterator; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.docker.transport.Errors.Error; +import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Errors}. + * + * @author Phillip Webb + */ +class ErrorsTests extends AbstractJsonTests { + + @Test + void readValueDeserializesJson() throws Exception { + Errors errors = getObjectMapper().readValue(getContent("errors.json"), Errors.class); + Iterator iterator = errors.iterator(); + Error error1 = iterator.next(); + Error error2 = iterator.next(); + assertThat(iterator.hasNext()).isFalse(); + assertThat(error1.getCode()).isEqualTo("TEST1"); + assertThat(error1.getMessage()).isEqualTo("Test One"); + assertThat(error2.getCode()).isEqualTo("TEST2"); + assertThat(error2.getMessage()).isEqualTo("Test Two"); + } + + @Test + void toStringHasErrorDetails() throws Exception { + Errors errors = getObjectMapper().readValue(getContent("errors.json"), Errors.class); + assertThat(errors.toString()).isEqualTo("[TEST1: Test One, TEST2: Test Two]"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransportTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransportTests.java new file mode 100644 index 000000000000..397aa5194cc2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransportTests.java @@ -0,0 +1,339 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.transport; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.charset.StandardCharsets; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpEntityEnclosingRequest; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.impl.client.CloseableHttpClient; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport.Response; +import org.springframework.util.StreamUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; + +/** + * Tests for {@link HttpClientTransport}. + * + * @author Phillip Webb + * @author Mike Smithson + * @author Scott Frederick + */ +@ExtendWith(MockitoExtension.class) +class HttpClientTransportTests { + + private static final String APPLICATION_JSON = "application/json"; + + private static final String APPLICATION_X_TAR = "application/x-tar"; + + @Mock + private CloseableHttpClient client; + + @Mock + private CloseableHttpResponse response; + + @Mock + private StatusLine statusLine; + + @Mock + private HttpEntity entity; + + @Mock + private InputStream content; + + @Captor + private ArgumentCaptor hostCaptor; + + @Captor + private ArgumentCaptor requestCaptor; + + private HttpClientTransport http; + + private URI uri; + + @BeforeEach + void setup() throws Exception { + this.http = new TestHttpClientTransport(this.client); + this.uri = new URI("example"); + } + + @Test + void getShouldExecuteHttpGet() throws Exception { + givenClientWillReturnResponse(); + given(this.entity.getContent()).willReturn(this.content); + given(this.statusLine.getStatusCode()).willReturn(200); + Response response = this.http.get(this.uri); + then(this.client).should().execute(this.hostCaptor.capture(), this.requestCaptor.capture()); + HttpUriRequest request = this.requestCaptor.getValue(); + assertThat(request).isInstanceOf(HttpGet.class); + assertThat(request.getURI()).isEqualTo(this.uri); + assertThat(request.getFirstHeader(HttpHeaders.CONTENT_TYPE)).isNull(); + assertThat(response.getContent()).isSameAs(this.content); + } + + @Test + void postShouldExecuteHttpPost() throws Exception { + givenClientWillReturnResponse(); + given(this.entity.getContent()).willReturn(this.content); + given(this.statusLine.getStatusCode()).willReturn(200); + Response response = this.http.post(this.uri); + then(this.client).should().execute(this.hostCaptor.capture(), this.requestCaptor.capture()); + HttpUriRequest request = this.requestCaptor.getValue(); + assertThat(request).isInstanceOf(HttpPost.class); + assertThat(request.getURI()).isEqualTo(this.uri); + assertThat(request.getFirstHeader(HttpHeaders.CONTENT_TYPE)).isNull(); + assertThat(request.getFirstHeader(HttpClientTransport.REGISTRY_AUTH_HEADER)).isNull(); + assertThat(response.getContent()).isSameAs(this.content); + } + + @Test + void postWithRegistryAuthShouldExecuteHttpPostWithHeader() throws Exception { + givenClientWillReturnResponse(); + given(this.entity.getContent()).willReturn(this.content); + given(this.statusLine.getStatusCode()).willReturn(200); + Response response = this.http.post(this.uri, "auth token"); + then(this.client).should().execute(this.hostCaptor.capture(), this.requestCaptor.capture()); + HttpUriRequest request = this.requestCaptor.getValue(); + assertThat(request).isInstanceOf(HttpPost.class); + assertThat(request.getURI()).isEqualTo(this.uri); + assertThat(request.getFirstHeader(HttpHeaders.CONTENT_TYPE)).isNull(); + assertThat(request.getFirstHeader(HttpClientTransport.REGISTRY_AUTH_HEADER).getValue()).isEqualTo("auth token"); + assertThat(response.getContent()).isSameAs(this.content); + } + + @Test + void postWithEmptyRegistryAuthShouldExecuteHttpPostWithoutHeader() throws Exception { + givenClientWillReturnResponse(); + given(this.entity.getContent()).willReturn(this.content); + given(this.statusLine.getStatusCode()).willReturn(200); + Response response = this.http.post(this.uri, ""); + then(this.client).should().execute(this.hostCaptor.capture(), this.requestCaptor.capture()); + HttpUriRequest request = this.requestCaptor.getValue(); + assertThat(request).isInstanceOf(HttpPost.class); + assertThat(request.getURI()).isEqualTo(this.uri); + assertThat(request.getFirstHeader(HttpHeaders.CONTENT_TYPE)).isNull(); + assertThat(request.getFirstHeader(HttpClientTransport.REGISTRY_AUTH_HEADER)).isNull(); + assertThat(response.getContent()).isSameAs(this.content); + } + + @Test + void postWithJsonContentShouldExecuteHttpPost() throws Exception { + String content = "test"; + givenClientWillReturnResponse(); + given(this.entity.getContent()).willReturn(this.content); + given(this.statusLine.getStatusCode()).willReturn(200); + Response response = this.http.post(this.uri, APPLICATION_JSON, + (out) -> StreamUtils.copy(content, StandardCharsets.UTF_8, out)); + then(this.client).should().execute(this.hostCaptor.capture(), this.requestCaptor.capture()); + HttpUriRequest request = this.requestCaptor.getValue(); + HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity(); + assertThat(request).isInstanceOf(HttpPost.class); + assertThat(request.getURI()).isEqualTo(this.uri); + assertThat(entity.isRepeatable()).isFalse(); + assertThat(entity.getContentLength()).isEqualTo(content.length()); + assertThat(entity.getContentType().getValue()).isEqualTo(APPLICATION_JSON); + assertThat(entity.isStreaming()).isTrue(); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(entity::getContent); + assertThat(writeToString(entity)).isEqualTo(content); + assertThat(response.getContent()).isSameAs(this.content); + } + + @Test + void postWithArchiveContentShouldExecuteHttpPost() throws Exception { + String content = "test"; + givenClientWillReturnResponse(); + given(this.entity.getContent()).willReturn(this.content); + given(this.statusLine.getStatusCode()).willReturn(200); + Response response = this.http.post(this.uri, APPLICATION_X_TAR, + (out) -> StreamUtils.copy(content, StandardCharsets.UTF_8, out)); + then(this.client).should().execute(this.hostCaptor.capture(), this.requestCaptor.capture()); + HttpUriRequest request = this.requestCaptor.getValue(); + HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity(); + assertThat(request).isInstanceOf(HttpPost.class); + assertThat(request.getURI()).isEqualTo(this.uri); + assertThat(entity.isRepeatable()).isFalse(); + assertThat(entity.getContentLength()).isEqualTo(-1); + assertThat(entity.getContentType().getValue()).isEqualTo(APPLICATION_X_TAR); + assertThat(entity.isStreaming()).isTrue(); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(entity::getContent); + assertThat(writeToString(entity)).isEqualTo(content); + assertThat(response.getContent()).isSameAs(this.content); + } + + @Test + void putWithJsonContentShouldExecuteHttpPut() throws Exception { + String content = "test"; + givenClientWillReturnResponse(); + given(this.entity.getContent()).willReturn(this.content); + given(this.statusLine.getStatusCode()).willReturn(200); + Response response = this.http.put(this.uri, APPLICATION_JSON, + (out) -> StreamUtils.copy(content, StandardCharsets.UTF_8, out)); + then(this.client).should().execute(this.hostCaptor.capture(), this.requestCaptor.capture()); + HttpUriRequest request = this.requestCaptor.getValue(); + HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity(); + assertThat(request).isInstanceOf(HttpPut.class); + assertThat(request.getURI()).isEqualTo(this.uri); + assertThat(entity.isRepeatable()).isFalse(); + assertThat(entity.getContentLength()).isEqualTo(content.length()); + assertThat(entity.getContentType().getValue()).isEqualTo(APPLICATION_JSON); + assertThat(entity.isStreaming()).isTrue(); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(entity::getContent); + assertThat(writeToString(entity)).isEqualTo(content); + assertThat(response.getContent()).isSameAs(this.content); + } + + @Test + void putWithArchiveContentShouldExecuteHttpPut() throws Exception { + String content = "test"; + givenClientWillReturnResponse(); + given(this.entity.getContent()).willReturn(this.content); + given(this.statusLine.getStatusCode()).willReturn(200); + Response response = this.http.put(this.uri, APPLICATION_X_TAR, + (out) -> StreamUtils.copy(content, StandardCharsets.UTF_8, out)); + then(this.client).should().execute(this.hostCaptor.capture(), this.requestCaptor.capture()); + HttpUriRequest request = this.requestCaptor.getValue(); + HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity(); + assertThat(request).isInstanceOf(HttpPut.class); + assertThat(request.getURI()).isEqualTo(this.uri); + assertThat(entity.isRepeatable()).isFalse(); + assertThat(entity.getContentLength()).isEqualTo(-1); + assertThat(entity.getContentType().getValue()).isEqualTo(APPLICATION_X_TAR); + assertThat(entity.isStreaming()).isTrue(); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(entity::getContent); + assertThat(writeToString(entity)).isEqualTo(content); + assertThat(response.getContent()).isSameAs(this.content); + } + + @Test + void deleteShouldExecuteHttpDelete() throws IOException { + givenClientWillReturnResponse(); + given(this.entity.getContent()).willReturn(this.content); + given(this.statusLine.getStatusCode()).willReturn(200); + Response response = this.http.delete(this.uri); + then(this.client).should().execute(this.hostCaptor.capture(), this.requestCaptor.capture()); + HttpUriRequest request = this.requestCaptor.getValue(); + assertThat(request).isInstanceOf(HttpDelete.class); + assertThat(request.getURI()).isEqualTo(this.uri); + assertThat(request.getFirstHeader(HttpHeaders.CONTENT_TYPE)).isNull(); + assertThat(response.getContent()).isSameAs(this.content); + } + + @Test + void executeWhenResponseIsIn400RangeShouldThrowDockerException() throws IOException { + givenClientWillReturnResponse(); + given(this.entity.getContent()).willReturn(getClass().getResourceAsStream("errors.json")); + given(this.statusLine.getStatusCode()).willReturn(404); + assertThatExceptionOfType(DockerEngineException.class).isThrownBy(() -> this.http.get(this.uri)) + .satisfies((ex) -> { + assertThat(ex.getErrors()).hasSize(2); + assertThat(ex.getResponseMessage()).isNull(); + }); + } + + @Test + void executeWhenResponseIsIn500RangeWithNoContentShouldThrowDockerException() throws IOException { + givenClientWillReturnResponse(); + given(this.statusLine.getStatusCode()).willReturn(500); + assertThatExceptionOfType(DockerEngineException.class).isThrownBy(() -> this.http.get(this.uri)) + .satisfies((ex) -> { + assertThat(ex.getErrors()).isNull(); + assertThat(ex.getResponseMessage()).isNull(); + }); + } + + @Test + void executeWhenResponseIsIn500RangeWithMessageShouldThrowDockerException() throws IOException { + givenClientWillReturnResponse(); + given(this.entity.getContent()).willReturn(getClass().getResourceAsStream("message.json")); + given(this.statusLine.getStatusCode()).willReturn(500); + assertThatExceptionOfType(DockerEngineException.class).isThrownBy(() -> this.http.get(this.uri)) + .satisfies((ex) -> { + assertThat(ex.getErrors()).isNull(); + assertThat(ex.getResponseMessage().getMessage()).contains("test message"); + }); + } + + @Test + void executeWhenResponseIsIn500RangeWithOtherContentShouldThrowDockerException() throws IOException { + givenClientWillReturnResponse(); + given(this.entity.getContent()).willReturn(this.content); + given(this.statusLine.getStatusCode()).willReturn(500); + assertThatExceptionOfType(DockerEngineException.class).isThrownBy(() -> this.http.get(this.uri)) + .satisfies((ex) -> { + assertThat(ex.getErrors()).isNull(); + assertThat(ex.getResponseMessage()).isNull(); + }); + } + + @Test + void executeWhenClientThrowsIOExceptionRethrowsAsDockerException() throws IOException { + given(this.client.execute(any(HttpHost.class), any(HttpRequest.class))) + .willThrow(new IOException("test IO exception")); + assertThatExceptionOfType(DockerConnectionException.class).isThrownBy(() -> this.http.get(this.uri)) + .satisfies((ex) -> assertThat(ex.getMessage()).contains("test IO exception")); + } + + private String writeToString(HttpEntity entity) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + entity.writeTo(out); + return new String(out.toByteArray(), StandardCharsets.UTF_8); + } + + private void givenClientWillReturnResponse() throws IOException { + given(this.client.execute(any(HttpHost.class), any(HttpRequest.class))).willReturn(this.response); + given(this.response.getEntity()).willReturn(this.entity); + given(this.response.getStatusLine()).willReturn(this.statusLine); + } + + /** + * Test {@link HttpClientTransport} implementation. + */ + static class TestHttpClientTransport extends HttpClientTransport { + + protected TestHttpClientTransport(CloseableHttpClient client) { + super(client, HttpHost.create("docker://localhost")); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpTransportTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpTransportTests.java new file mode 100644 index 000000000000..38a65a819c91 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpTransportTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.transport; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link HttpTransport}. + * + * @author Phillip Webb + * @author Scott Frederick + */ +class HttpTransportTests { + + @Test + void createWhenDockerHostVariableIsAddressReturnsRemote() { + Map environment = Collections.singletonMap("DOCKER_HOST", "tcp://192.168.1.0"); + HttpTransport transport = HttpTransport.create(environment::get); + assertThat(transport).isInstanceOf(RemoteHttpClientTransport.class); + } + + @Test + void createWhenDockerHostVariableIsFileReturnsLocal(@TempDir Path tempDir) throws IOException { + String dummySocketFilePath = Files.createTempFile(tempDir, "http-transport", null).toAbsolutePath().toString(); + Map environment = Collections.singletonMap("DOCKER_HOST", dummySocketFilePath); + HttpTransport transport = HttpTransport.create(environment::get); + assertThat(transport).isInstanceOf(LocalHttpClientTransport.class); + } + + @Test + void createWhenDockerHostVariableIsUnixSchemePrefixedFileReturnsLocal(@TempDir Path tempDir) throws IOException { + String dummySocketFilePath = "unix://" + + Files.createTempFile(tempDir, "http-transport", null).toAbsolutePath().toString(); + Map environment = Collections.singletonMap("DOCKER_HOST", dummySocketFilePath); + HttpTransport transport = HttpTransport.create(environment::get); + assertThat(transport).isInstanceOf(LocalHttpClientTransport.class); + } + + @Test + void createWhenDoesNotHaveDockerHostVariableReturnsLocal() { + HttpTransport transport = HttpTransport.create((name) -> null); + assertThat(transport).isInstanceOf(LocalHttpClientTransport.class); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/MessageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/MessageTests.java new file mode 100644 index 000000000000..c504ac426b2e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/MessageTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.transport; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Message}. + * + * @author Scott Frederick + */ +class MessageTests extends AbstractJsonTests { + + @Test + void readValueDeserializesJson() throws Exception { + Message message = getObjectMapper().readValue(getContent("message.json"), Message.class); + assertThat(message.getMessage()).isEqualTo("test message"); + } + + @Test + void toStringHasErrorDetails() throws Exception { + Message errors = getObjectMapper().readValue(getContent("message.json"), Message.class); + assertThat(errors.toString()).isEqualTo("test message"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/RemoteHttpClientTransportTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/RemoteHttpClientTransportTests.java new file mode 100644 index 000000000000..acf3528fa38c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/RemoteHttpClientTransportTests.java @@ -0,0 +1,153 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.transport; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Consumer; + +import javax.net.ssl.SSLContext; + +import org.apache.http.HttpHost; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerHost; +import org.springframework.boot.buildpack.platform.docker.ssl.SslContextFactory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link RemoteHttpClientTransport} + * + * @author Scott Frederick + * @author Phillip Webb + */ +class RemoteHttpClientTransportTests { + + private final Map environment = new LinkedHashMap<>(); + + private final DockerConfiguration dockerConfiguration = new DockerConfiguration(); + + @Test + void createIfPossibleWhenDockerHostIsNotSetReturnsNull() { + RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get, + new DockerHost(null, false, null)); + assertThat(transport).isNull(); + } + + @Test + void createIfPossibleWithoutDockerConfigurationReturnsNull() { + RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get, null); + assertThat(transport).isNull(); + } + + @Test + void createIfPossibleWhenDockerHostInEnvironmentIsFileReturnsNull(@TempDir Path tempDir) throws IOException { + String dummySocketFilePath = Files.createTempFile(tempDir, "remote-transport", null).toAbsolutePath() + .toString(); + this.environment.put("DOCKER_HOST", dummySocketFilePath); + RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get, null); + assertThat(transport).isNull(); + } + + @Test + void createIfPossibleWhenDockerHostInConfigurationIsFileReturnsNull(@TempDir Path tempDir) throws IOException { + String dummySocketFilePath = Files.createTempFile(tempDir, "remote-transport", null).toAbsolutePath() + .toString(); + RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get, + new DockerHost(dummySocketFilePath, false, null)); + assertThat(transport).isNull(); + } + + @Test + void createIfPossibleWhenDockerHostInEnvironmentIsAddressReturnsTransport() { + this.environment.put("DOCKER_HOST", "tcp://192.168.1.2:2376"); + RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get, null); + assertThat(transport).isNotNull(); + } + + @Test + void createIfPossibleWhenDockerHostInConfigurationIsAddressReturnsTransport() { + RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get, + new DockerHost("tcp://192.168.1.2:2376", false, null)); + assertThat(transport).isNotNull(); + } + + @Test + void createIfPossibleWhenTlsVerifyInEnvironmentWithMissingCertPathThrowsException() { + this.environment.put("DOCKER_HOST", "tcp://192.168.1.2:2376"); + this.environment.put("DOCKER_TLS_VERIFY", "1"); + assertThatIllegalArgumentException() + .isThrownBy(() -> RemoteHttpClientTransport.createIfPossible(this.environment::get, null)) + .withMessageContaining("Docker host TLS verification requires trust material"); + } + + @Test + void createIfPossibleWhenTlsVerifyInConfigurationWithMissingCertPathThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> RemoteHttpClientTransport.createIfPossible(this.environment::get, + new DockerHost("tcp://192.168.1.2:2376", true, null))) + .withMessageContaining("Docker host TLS verification requires trust material"); + } + + @Test + void createIfPossibleWhenNoTlsVerifyUsesHttp() { + this.environment.put("DOCKER_HOST", "tcp://192.168.1.2:2376"); + RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get, null); + assertThat(transport.getHost()).satisfies(hostOf("http", "192.168.1.2", 2376)); + } + + @Test + void createIfPossibleWhenTlsVerifyInEnvironmentUsesHttps() throws Exception { + this.environment.put("DOCKER_HOST", "tcp://192.168.1.2:2376"); + this.environment.put("DOCKER_TLS_VERIFY", "1"); + this.environment.put("DOCKER_CERT_PATH", "/test-cert-path"); + SslContextFactory sslContextFactory = mock(SslContextFactory.class); + given(sslContextFactory.forDirectory("/test-cert-path")).willReturn(SSLContext.getDefault()); + RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get, + this.dockerConfiguration.getHost(), sslContextFactory); + assertThat(transport.getHost()).satisfies(hostOf("https", "192.168.1.2", 2376)); + } + + @Test + void createIfPossibleWhenTlsVerifyInConfigurationUsesHttps() throws Exception { + SslContextFactory sslContextFactory = mock(SslContextFactory.class); + given(sslContextFactory.forDirectory("/test-cert-path")).willReturn(SSLContext.getDefault()); + RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get, + this.dockerConfiguration.withHost("tcp://192.168.1.2:2376", true, "/test-cert-path").getHost(), + sslContextFactory); + assertThat(transport.getHost()).satisfies(hostOf("https", "192.168.1.2", 2376)); + } + + private Consumer hostOf(String scheme, String hostName, int port) { + return (host) -> { + assertThat(host).isNotNull(); + assertThat(host.getSchemeName()).isEqualTo(scheme); + assertThat(host.getHostName()).isEqualTo(hostName); + assertThat(host.getPort()).isEqualTo(port); + }; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/BindingTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/BindingTests.java new file mode 100644 index 000000000000..c8eb1783d85f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/BindingTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link Binding}. + * + * @author Scott Frederick + */ +class BindingTests { + + @Test + void ofReturnsValue() { + Binding binding = Binding.of("host-src:container-dest:ro"); + assertThat(binding).hasToString("host-src:container-dest:ro"); + } + + @Test + void ofWithNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> Binding.of(null)) + .withMessageContaining("Value must not be null"); + } + + @Test + void fromReturnsValue() { + Binding binding = Binding.from("host-src", "container-dest"); + assertThat(binding).hasToString("host-src:container-dest"); + } + + @Test + void fromWithNullSourceThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> Binding.from((String) null, "container-dest")) + .withMessageContaining("Source must not be null"); + } + + @Test + void fromWithNullDestinationThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> Binding.from("host-src", null)) + .withMessageContaining("Destination must not be null"); + } + + @Test + void fromVolumeNameSourceReturnsValue() { + Binding binding = Binding.from(VolumeName.of("host-src"), "container-dest"); + assertThat(binding).hasToString("host-src:container-dest"); + } + + @Test + void fromVolumeNameSourceWithNullSourceThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> Binding.from((VolumeName) null, "container-dest")) + .withMessageContaining("SourceVolume must not be null"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ContainerConfigTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ContainerConfigTests.java new file mode 100644 index 000000000000..cc94d2d7aed0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ContainerConfigTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; +import org.springframework.util.StreamUtils; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link ContainerConfig}. + * + * @author Phillip Webb + * @author Scott Frederick + */ +class ContainerConfigTests extends AbstractJsonTests { + + @Test + void ofWhenImageReferenceIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> ContainerConfig.of(null, (update) -> { + })).withMessage("ImageReference must not be null"); + } + + @Test + void ofWhenUpdateIsNullThrowsException() { + ImageReference imageReference = ImageReference.of("ubuntu:bionic"); + assertThatIllegalArgumentException().isThrownBy(() -> ContainerConfig.of(imageReference, null)) + .withMessage("Update must not be null"); + } + + @Test + void writeToWritesJson() throws Exception { + ImageReference imageReference = ImageReference.of("ubuntu:bionic"); + ContainerConfig containerConfig = ContainerConfig.of(imageReference, (update) -> { + update.withUser("root"); + update.withCommand("ls", "-l"); + update.withArgs("-h"); + update.withLabel("spring", "boot"); + update.withBinding(Binding.from("bind-source", "bind-dest")); + update.withEnv("name1", "value1"); + update.withEnv("name2", "value2"); + }); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + containerConfig.writeTo(outputStream); + String actualJson = new String(outputStream.toByteArray(), StandardCharsets.UTF_8); + String expectedJson = StreamUtils.copyToString(getContent("container-config.json"), StandardCharsets.UTF_8); + JSONAssert.assertEquals(expectedJson, actualJson, false); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ContainerContentTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ContainerContentTests.java new file mode 100644 index 000000000000..c5390ac1de3d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ContainerContentTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.io.TarArchive; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ContainerContent}. + * + * @author Phillip Webb + */ +class ContainerContentTests { + + @Test + void ofWhenArchiveIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> ContainerContent.of(null)) + .withMessage("Archive must not be null"); + } + + @Test + void ofWhenDestinationPathIsNullThrowsException() { + TarArchive archive = mock(TarArchive.class); + assertThatIllegalArgumentException().isThrownBy(() -> ContainerContent.of(archive, null)) + .withMessage("DestinationPath must not be empty"); + } + + @Test + void ofWhenDestinationPathIsEmptyThrowsException() { + TarArchive archive = mock(TarArchive.class); + assertThatIllegalArgumentException().isThrownBy(() -> ContainerContent.of(archive, "")) + .withMessage("DestinationPath must not be empty"); + } + + @Test + void ofCreatesContainerContent() { + TarArchive archive = mock(TarArchive.class); + ContainerContent content = ContainerContent.of(archive); + assertThat(content.getArchive()).isSameAs(archive); + assertThat(content.getDestinationPath()).isEqualTo("/"); + } + + @Test + void ofWithDestinationPathCreatesContainerContent() { + TarArchive archive = mock(TarArchive.class); + ContainerContent content = ContainerContent.of(archive, "/test"); + assertThat(content.getArchive()).isSameAs(archive); + assertThat(content.getDestinationPath()).isEqualTo("/test"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ContainerReferenceTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ContainerReferenceTests.java new file mode 100644 index 000000000000..43f8cc33cf9f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ContainerReferenceTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link ContainerReference}. + * + * @author Phillip Webb + */ +class ContainerReferenceTests { + + @Test + void ofCreatesInstance() { + ContainerReference reference = ContainerReference + .of("92691aec176333f7ae890de9aaeeafef11166efcaa3908edf83eb44a5c943781"); + assertThat(reference.toString()).isEqualTo("92691aec176333f7ae890de9aaeeafef11166efcaa3908edf83eb44a5c943781"); + } + + @Test + void ofWhenNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> ContainerReference.of(null)) + .withMessage("Value must not be empty"); + } + + @Test + void ofWhenEmptyThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> ContainerReference.of("")) + .withMessage("Value must not be empty"); + } + + @Test + void hashCodeAndEquals() { + ContainerReference r1 = ContainerReference + .of("92691aec176333f7ae890de9aaeeafef11166efcaa3908edf83eb44a5c943781"); + ContainerReference r2 = ContainerReference + .of("92691aec176333f7ae890de9aaeeafef11166efcaa3908edf83eb44a5c943781"); + ContainerReference r3 = ContainerReference + .of("02691aec176333f7ae890de9aaeeafef11166efcaa3908edf83eb44a5c943781"); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + assertThat(r1).isEqualTo(r1).isEqualTo(r2).isNotEqualTo(r3); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ContainerStatusTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ContainerStatusTests.java new file mode 100644 index 000000000000..7755d5866fbd --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ContainerStatusTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ContainerStatus}. + * + * @author Scott Frederick + */ +class ContainerStatusTests { + + @Test + void ofCreatesFromJson() throws IOException { + ContainerStatus status = ContainerStatus.of(getClass().getResourceAsStream("container-status-error.json")); + assertThat(status.getStatusCode()).isEqualTo(1); + assertThat(status.getWaitingErrorMessage()).isEqualTo("error detail"); + } + + @Test + void ofCreatesFromValues() { + ContainerStatus status = ContainerStatus.of(1, "error detail"); + assertThat(status.getStatusCode()).isEqualTo(1); + assertThat(status.getWaitingErrorMessage()).isEqualTo("error detail"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveTests.java new file mode 100644 index 000000000000..8457a459791f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import org.springframework.boot.buildpack.platform.io.Owner; +import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; +import org.springframework.util.StreamUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ImageArchive}. + * + * @author Phillip Webb + * @author Scott Frederick + */ +class ImageArchiveTests extends AbstractJsonTests { + + @Test + void fromImageWritesToValidArchiveTar() throws Exception { + Image image = Image.of(getContent("image.json")); + ImageArchive archive = ImageArchive.from(image, (update) -> { + update.withNewLayer(Layer.of((layout) -> layout.directory("/spring", Owner.ROOT))); + update.withTag(ImageReference.of("pack.local/builder/6b7874626575656b6162")); + }); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + archive.writeTo(outputStream); + try (TarArchiveInputStream tar = new TarArchiveInputStream( + new ByteArrayInputStream(outputStream.toByteArray()))) { + TarArchiveEntry layerEntry = tar.getNextTarEntry(); + byte[] layerContent = read(tar, layerEntry.getSize()); + TarArchiveEntry configEntry = tar.getNextTarEntry(); + byte[] configContent = read(tar, configEntry.getSize()); + TarArchiveEntry manifestEntry = tar.getNextTarEntry(); + byte[] manifestContent = read(tar, manifestEntry.getSize()); + assertThat(tar.getNextTarEntry()).isNull(); + assertExpectedLayer(layerEntry, layerContent); + assertExpectedConfig(configEntry, configContent); + assertExpectedManifest(manifestEntry, manifestContent); + } + } + + private void assertExpectedLayer(TarArchiveEntry entry, byte[] content) throws Exception { + assertThat(entry.getName()).isEqualTo("/bb09e17fd1bd2ee47155f1349645fcd9fff31e1247c7ed99cad469f1c16a4216.tar"); + try (TarArchiveInputStream tar = new TarArchiveInputStream(new ByteArrayInputStream(content))) { + TarArchiveEntry contentEntry = tar.getNextTarEntry(); + assertThat(contentEntry.getName()).isEqualTo("/spring/"); + } + } + + private void assertExpectedConfig(TarArchiveEntry entry, byte[] content) throws Exception { + assertThat(entry.getName()).isEqualTo("/682f8d24b9d9c313d1190a0e955dcb5e65ec9beea40420999839c6f0cbb38382.json"); + String actualJson = new String(content, StandardCharsets.UTF_8); + String expectedJson = StreamUtils.copyToString(getContent("image-archive-config.json"), StandardCharsets.UTF_8); + JSONAssert.assertEquals(expectedJson, actualJson, false); + } + + private void assertExpectedManifest(TarArchiveEntry entry, byte[] content) throws Exception { + assertThat(entry.getName()).isEqualTo("/manifest.json"); + String actualJson = new String(content, StandardCharsets.UTF_8); + String expectedJson = StreamUtils.copyToString(getContent("image-archive-manifest.json"), + StandardCharsets.UTF_8); + JSONAssert.assertEquals(expectedJson, actualJson, false); + } + + private byte[] read(TarArchiveInputStream tar, long size) throws IOException { + byte[] content = new byte[(int) size]; + tar.read(content); + return content; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageConfigTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageConfigTests.java new file mode 100644 index 000000000000..38eb2e67874e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageConfigTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import java.io.IOException; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +/** + * Tests for {@link ImageConfig}. + * + * @author Phillip Webb + * @author Andy Wilkinson + */ +class ImageConfigTests extends AbstractJsonTests { + + @Test + void getEnvContainsParsedValues() throws Exception { + ImageConfig imageConfig = getImageConfig(); + Map env = imageConfig.getEnv(); + assertThat(env).contains(entry("PATH", "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"), + entry("CNB_USER_ID", "2000"), entry("CNB_GROUP_ID", "2000"), + entry("CNB_STACK_ID", "org.cloudfoundry.stacks.cflinuxfs3")); + } + + @Test + void whenConfigHasNoEnvThenImageConfigEnvIsEmpty() throws Exception { + ImageConfig imageConfig = getMinimalImageConfig(); + Map env = imageConfig.getEnv(); + assertThat(env).isEmpty(); + } + + @Test + void whenConfigHasNoLabelsThenImageConfigLabelsIsEmpty() throws Exception { + ImageConfig imageConfig = getMinimalImageConfig(); + Map env = imageConfig.getLabels(); + assertThat(env).isEmpty(); + } + + @Test + void getLabelsReturnsLabels() throws Exception { + ImageConfig imageConfig = getImageConfig(); + Map labels = imageConfig.getLabels(); + assertThat(labels).hasSize(4).contains(entry("io.buildpacks.stack.id", "org.cloudfoundry.stacks.cflinuxfs3")); + } + + @Test + void updateWithLabelUpdatesLabels() throws Exception { + ImageConfig imageConfig = getImageConfig(); + ImageConfig updatedImageConfig = imageConfig + .copy((update) -> update.withLabel("io.buildpacks.stack.id", "test")); + assertThat(imageConfig.getLabels()).hasSize(4) + .contains(entry("io.buildpacks.stack.id", "org.cloudfoundry.stacks.cflinuxfs3")); + assertThat(updatedImageConfig.getLabels()).hasSize(4).contains(entry("io.buildpacks.stack.id", "test")); + } + + private ImageConfig getImageConfig() throws IOException { + return new ImageConfig(getObjectMapper().readTree(getContent("image-config.json"))); + } + + private ImageConfig getMinimalImageConfig() throws IOException { + return new ImageConfig(getObjectMapper().readTree(getContent("minimal-image-config.json"))); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageNameTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageNameTests.java new file mode 100644 index 000000000000..c1e667e0932c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageNameTests.java @@ -0,0 +1,161 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link ImageName}. + * + * @author Phillip Webb + * @author Scott Frederick + */ +class ImageNameTests { + + @Test + void ofWhenNameOnlyCreatesImageName() { + ImageName imageName = ImageName.of("ubuntu"); + assertThat(imageName.toString()).isEqualTo("docker.io/library/ubuntu"); + assertThat(imageName.getDomain()).isEqualTo("docker.io"); + assertThat(imageName.getName()).isEqualTo("library/ubuntu"); + } + + @Test + void ofWhenSlashedNameCreatesImageName() { + ImageName imageName = ImageName.of("canonical/ubuntu"); + assertThat(imageName.toString()).isEqualTo("docker.io/canonical/ubuntu"); + assertThat(imageName.getDomain()).isEqualTo("docker.io"); + assertThat(imageName.getName()).isEqualTo("canonical/ubuntu"); + } + + @Test + void ofWhenLocalhostNameCreatesImageName() { + ImageName imageName = ImageName.of("localhost/canonical/ubuntu"); + assertThat(imageName.toString()).isEqualTo("localhost/canonical/ubuntu"); + assertThat(imageName.getDomain()).isEqualTo("localhost"); + assertThat(imageName.getName()).isEqualTo("canonical/ubuntu"); + } + + @Test + void ofWhenDomainAndNameCreatesImageName() { + ImageName imageName = ImageName.of("repo.spring.io/canonical/ubuntu"); + assertThat(imageName.toString()).isEqualTo("repo.spring.io/canonical/ubuntu"); + assertThat(imageName.getDomain()).isEqualTo("repo.spring.io"); + assertThat(imageName.getName()).isEqualTo("canonical/ubuntu"); + } + + @Test + void ofWhenDomainNameAndPortCreatesImageName() { + ImageName imageName = ImageName.of("repo.spring.io:8080/canonical/ubuntu"); + assertThat(imageName.toString()).isEqualTo("repo.spring.io:8080/canonical/ubuntu"); + assertThat(imageName.getDomain()).isEqualTo("repo.spring.io:8080"); + assertThat(imageName.getName()).isEqualTo("canonical/ubuntu"); + } + + @Test + void ofWhenSimpleNameAndPortCreatesImageName() { + ImageName imageName = ImageName.of("repo:8080/ubuntu"); + assertThat(imageName.toString()).isEqualTo("repo:8080/ubuntu"); + assertThat(imageName.getDomain()).isEqualTo("repo:8080"); + assertThat(imageName.getName()).isEqualTo("ubuntu"); + } + + @Test + void ofWhenSimplePathAndPortCreatesImageName() { + ImageName imageName = ImageName.of("repo:8080/canonical/ubuntu"); + assertThat(imageName.toString()).isEqualTo("repo:8080/canonical/ubuntu"); + assertThat(imageName.getDomain()).isEqualTo("repo:8080"); + assertThat(imageName.getName()).isEqualTo("canonical/ubuntu"); + } + + @Test + void ofWhenNameWithLongPathCreatesImageName() { + ImageName imageName = ImageName.of("path1/path2/path3/ubuntu"); + assertThat(imageName.toString()).isEqualTo("docker.io/path1/path2/path3/ubuntu"); + assertThat(imageName.getDomain()).isEqualTo("docker.io"); + assertThat(imageName.getName()).isEqualTo("path1/path2/path3/ubuntu"); + } + + @Test + void ofWhenLocalhostDomainCreatesImageName() { + ImageName imageName = ImageName.of("localhost/ubuntu"); + assertThat(imageName.getDomain()).isEqualTo("localhost"); + assertThat(imageName.getName()).isEqualTo("ubuntu"); + } + + @Test + void ofWhenLocalhostDomainAndPathCreatesImageName() { + ImageName imageName = ImageName.of("localhost/library/ubuntu"); + assertThat(imageName.getDomain()).isEqualTo("localhost"); + assertThat(imageName.getName()).isEqualTo("library/ubuntu"); + } + + @Test + void ofWhenLegacyDomainUsesNewDomain() { + ImageName imageName = ImageName.of("index.docker.io/ubuntu"); + assertThat(imageName.toString()).isEqualTo("docker.io/library/ubuntu"); + assertThat(imageName.getDomain()).isEqualTo("docker.io"); + assertThat(imageName.getName()).isEqualTo("library/ubuntu"); + } + + @Test + void ofWhenNameIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> ImageName.of(null)) + .withMessage("Value must not be empty"); + } + + @Test + void ofWhenNameIsEmptyThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> ImageName.of("")).withMessage("Value must not be empty"); + } + + @Test + void ofWhenContainsUppercaseThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> ImageName.of("Test")) + .withMessageContaining("Unable to parse name").withMessageContaining("Test"); + } + + @Test + void ofWhenNameIncludesTagThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> ImageName.of("ubuntu:latest")) + .withMessageContaining("Unable to parse name").withMessageContaining(":latest"); + } + + @Test + void ofWhenNameIncludeDigestThrowsException() { + assertThatIllegalArgumentException().isThrownBy( + () -> ImageName.of("ubuntu@sha256:47bfdb88c3ae13e488167607973b7688f69d9e8c142c2045af343ec199649c09")) + .withMessageContaining("Unable to parse name").withMessageContaining("@sha256:47b"); + } + + @Test + void hashCodeAndEquals() { + ImageName n1 = ImageName.of("ubuntu"); + ImageName n2 = ImageName.of("library/ubuntu"); + ImageName n3 = ImageName.of("docker.io/ubuntu"); + ImageName n4 = ImageName.of("docker.io/library/ubuntu"); + ImageName n5 = ImageName.of("index.docker.io/library/ubuntu"); + ImageName n6 = ImageName.of("alpine"); + assertThat(n1.hashCode()).isEqualTo(n2.hashCode()).isEqualTo(n3.hashCode()).isEqualTo(n4.hashCode()) + .isEqualTo(n5.hashCode()); + assertThat(n1).isEqualTo(n1).isEqualTo(n2).isEqualTo(n3).isEqualTo(n4).isNotEqualTo(n6); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageReferenceTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageReferenceTests.java new file mode 100644 index 000000000000..e19c0e14fb31 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageReferenceTests.java @@ -0,0 +1,274 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import java.io.File; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link ImageReference}. + * + * @author Phillip Webb + * @author Scott Frederick + */ +class ImageReferenceTests { + + @Test + void ofSimpleName() { + ImageReference reference = ImageReference.of("ubuntu"); + assertThat(reference.getDomain()).isEqualTo("docker.io"); + assertThat(reference.getName()).isEqualTo("library/ubuntu"); + assertThat(reference.getTag()).isNull(); + assertThat(reference.getDigest()).isNull(); + assertThat(reference.toString()).isEqualTo("docker.io/library/ubuntu"); + } + + @Test + void ofLibrarySlashName() { + ImageReference reference = ImageReference.of("library/ubuntu"); + assertThat(reference.getDomain()).isEqualTo("docker.io"); + assertThat(reference.getName()).isEqualTo("library/ubuntu"); + assertThat(reference.getTag()).isNull(); + assertThat(reference.getDigest()).isNull(); + assertThat(reference.toString()).isEqualTo("docker.io/library/ubuntu"); + } + + @Test + void ofSlashName() { + ImageReference reference = ImageReference.of("adoptopenjdk/openjdk11"); + assertThat(reference.getDomain()).isEqualTo("docker.io"); + assertThat(reference.getName()).isEqualTo("adoptopenjdk/openjdk11"); + assertThat(reference.getTag()).isNull(); + assertThat(reference.getDigest()).isNull(); + assertThat(reference.toString()).isEqualTo("docker.io/adoptopenjdk/openjdk11"); + } + + @Test + void ofCustomDomain() { + ImageReference reference = ImageReference.of("repo.example.com/java/jdk"); + assertThat(reference.getDomain()).isEqualTo("repo.example.com"); + assertThat(reference.getName()).isEqualTo("java/jdk"); + assertThat(reference.getTag()).isNull(); + assertThat(reference.getDigest()).isNull(); + assertThat(reference.toString()).isEqualTo("repo.example.com/java/jdk"); + } + + @Test + void ofCustomDomainAndPort() { + ImageReference reference = ImageReference.of("repo.example.com:8080/java/jdk"); + assertThat(reference.getDomain()).isEqualTo("repo.example.com:8080"); + assertThat(reference.getName()).isEqualTo("java/jdk"); + assertThat(reference.getTag()).isNull(); + assertThat(reference.getDigest()).isNull(); + assertThat(reference.toString()).isEqualTo("repo.example.com:8080/java/jdk"); + } + + @Test + void ofLegacyDomain() { + ImageReference reference = ImageReference.of("index.docker.io/ubuntu"); + assertThat(reference.getDomain()).isEqualTo("docker.io"); + assertThat(reference.getName()).isEqualTo("library/ubuntu"); + assertThat(reference.getTag()).isNull(); + assertThat(reference.getDigest()).isNull(); + assertThat(reference.toString()).isEqualTo("docker.io/library/ubuntu"); + } + + @Test + void ofNameAndTag() { + ImageReference reference = ImageReference.of("ubuntu:bionic"); + assertThat(reference.getDomain()).isEqualTo("docker.io"); + assertThat(reference.getName()).isEqualTo("library/ubuntu"); + assertThat(reference.getTag()).isEqualTo("bionic"); + assertThat(reference.getDigest()).isNull(); + assertThat(reference.toString()).isEqualTo("docker.io/library/ubuntu:bionic"); + } + + @Test + void ofDomainPortAndTag() { + ImageReference reference = ImageReference.of("repo.example.com:8080/library/ubuntu:v1"); + assertThat(reference.getDomain()).isEqualTo("repo.example.com:8080"); + assertThat(reference.getName()).isEqualTo("library/ubuntu"); + assertThat(reference.getTag()).isEqualTo("v1"); + assertThat(reference.getDigest()).isNull(); + assertThat(reference.toString()).isEqualTo("repo.example.com:8080/library/ubuntu:v1"); + } + + @Test + void ofNameAndDigest() { + ImageReference reference = ImageReference + .of("ubuntu@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + assertThat(reference.getDomain()).isEqualTo("docker.io"); + assertThat(reference.getName()).isEqualTo("library/ubuntu"); + assertThat(reference.getTag()).isNull(); + assertThat(reference.getDigest()) + .isEqualTo("sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + assertThat(reference.toString()).isEqualTo( + "docker.io/library/ubuntu@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + } + + @Test + void ofNameAndTagAndDigest() { + ImageReference reference = ImageReference + .of("ubuntu:bionic@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + assertThat(reference.getDomain()).isEqualTo("docker.io"); + assertThat(reference.getName()).isEqualTo("library/ubuntu"); + assertThat(reference.getTag()).isEqualTo("bionic"); + assertThat(reference.getDigest()) + .isEqualTo("sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + assertThat(reference.toString()).isEqualTo( + "docker.io/library/ubuntu:bionic@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + } + + @Test + void ofCustomDomainAndPortWithTag() { + ImageReference reference = ImageReference.of( + "example.com:8080/canonical/ubuntu:bionic@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + assertThat(reference.getDomain()).isEqualTo("example.com:8080"); + assertThat(reference.getName()).isEqualTo("canonical/ubuntu"); + assertThat(reference.getTag()).isEqualTo("bionic"); + assertThat(reference.getDigest()) + .isEqualTo("sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + assertThat(reference.toString()).isEqualTo( + "example.com:8080/canonical/ubuntu:bionic@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + } + + @Test + void ofImageName() { + ImageReference reference = ImageReference.of(ImageName.of("ubuntu")); + assertThat(reference.toString()).isEqualTo("docker.io/library/ubuntu"); + } + + @Test + void ofImageNameAndTag() { + ImageReference reference = ImageReference.of(ImageName.of("ubuntu"), "bionic"); + assertThat(reference.toString()).isEqualTo("docker.io/library/ubuntu:bionic"); + } + + @Test + void ofImageNameTagAndDigest() { + ImageReference reference = ImageReference.of(ImageName.of("ubuntu"), "bionic", + "sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + assertThat(reference.toString()).isEqualTo( + "docker.io/library/ubuntu:bionic@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + } + + @Test + void ofWhenHasIllegalCharacter() { + assertThatIllegalArgumentException() + .isThrownBy(() -> ImageReference + .of("registry.example.com/example/example-app:1.6.0-dev.2.uncommitted+wip.foo.c75795d")) + .withMessageContaining("Unable to parse image reference"); + } + + @Test + void forJarFile() { + assertForJarFile("spring-boot.2.0.0.BUILD-SNAPSHOT.jar", "library/spring-boot", "2.0.0.BUILD-SNAPSHOT"); + assertForJarFile("spring-boot.2.0.0.M1.jar", "library/spring-boot", "2.0.0.M1"); + assertForJarFile("spring-boot.2.0.0.RC1.jar", "library/spring-boot", "2.0.0.RC1"); + assertForJarFile("spring-boot.2.0.0.RELEASE.jar", "library/spring-boot", "2.0.0.RELEASE"); + assertForJarFile("sample-0.0.1-SNAPSHOT.jar", "library/sample", "0.0.1-SNAPSHOT"); + assertForJarFile("sample-0.0.1.jar", "library/sample", "0.0.1"); + } + + private void assertForJarFile(String jarFile, String expectedName, String expectedTag) { + ImageReference reference = ImageReference.forJarFile(new File(jarFile)); + assertThat(reference.getName()).isEqualTo(expectedName); + assertThat(reference.getTag()).isEqualTo(expectedTag); + } + + @Test + void randomGeneratesRandomName() { + String prefix = "pack.local/builder/"; + ImageReference random = ImageReference.random(prefix); + assertThat(random.toString()).startsWith(prefix).hasSize(prefix.length() + 10); + ImageReference another = ImageReference.random(prefix); + int attempts = 0; + while (another.equals(random)) { + assertThat(attempts).as("Duplicate results").isLessThan(10); + another = ImageReference.random(prefix); + attempts++; + } + } + + @Test + void randomWithLengthGeneratesRandomName() { + String prefix = "pack.local/builder/"; + ImageReference random = ImageReference.random(prefix, 20); + assertThat(random.toString()).startsWith(prefix).hasSize(prefix.length() + 20); + } + + @Test + void randomWherePrefixIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> ImageReference.random(null)) + .withMessage("Prefix must not be null"); + } + + @Test + void inTaggedFormWhenHasDigestThrowsException() { + ImageReference reference = ImageReference + .of("ubuntu@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + assertThatIllegalStateException().isThrownBy(() -> reference.inTaggedForm()).withMessage( + "Image reference 'docker.io/library/ubuntu@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d' cannot contain a digest"); + } + + @Test + void inTaggedFormWhenHasNoTagUsesLatest() { + ImageReference reference = ImageReference.of("ubuntu"); + assertThat(reference.inTaggedForm().toString()).isEqualTo("docker.io/library/ubuntu:latest"); + } + + @Test + void inTaggedFormWhenHasTagUsesTag() { + ImageReference reference = ImageReference.of("ubuntu:bionic"); + assertThat(reference.inTaggedForm().toString()).isEqualTo("docker.io/library/ubuntu:bionic"); + } + + @Test + void inTaggedOrDigestFormWhenHasDigestUsesDigest() { + ImageReference reference = ImageReference + .of("ubuntu@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + assertThat(reference.inTaggedOrDigestForm().toString()).isEqualTo( + "docker.io/library/ubuntu@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); + } + + @Test + void inTaggedOrDigestFormWhenHasTagUsesTag() { + ImageReference reference = ImageReference.of("ubuntu:bionic"); + assertThat(reference.inTaggedOrDigestForm().toString()).isEqualTo("docker.io/library/ubuntu:bionic"); + } + + @Test + void inTaggedOrDigestFormWhenHasNoTagOrDigestUsesLatest() { + ImageReference reference = ImageReference.of("ubuntu"); + assertThat(reference.inTaggedOrDigestForm().toString()).isEqualTo("docker.io/library/ubuntu:latest"); + } + + @Test + void equalsAndHashCode() { + ImageReference r1 = ImageReference.of("ubuntu:bionic"); + ImageReference r2 = ImageReference.of("docker.io/library/ubuntu:bionic"); + ImageReference r3 = ImageReference.of("docker.io/library/ubuntu:latest"); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + assertThat(r1).isEqualTo(r1).isEqualTo(r2).isNotEqualTo(r3); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageTests.java new file mode 100644 index 000000000000..c4535b23a4e9 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +/** + * Tests for {@link Image}. + * + * @author Phillip Webb + */ +class ImageTests extends AbstractJsonTests { + + @Test + void getConfigEnvContainsParsedValues() throws Exception { + Image image = getImage(); + Map env = image.getConfig().getEnv(); + assertThat(env).contains(entry("PATH", "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"), + entry("CNB_USER_ID", "2000"), entry("CNB_GROUP_ID", "2000"), + entry("CNB_STACK_ID", "org.cloudfoundry.stacks.cflinuxfs3")); + } + + @Test + void getConfigLabelsReturnsLabels() throws Exception { + Image image = getImage(); + Map labels = image.getConfig().getLabels(); + assertThat(labels).contains(entry("io.buildpacks.stack.id", "org.cloudfoundry.stacks.cflinuxfs3")); + } + + @Test + void getLayersReturnsImageLayers() throws Exception { + Image image = getImage(); + List layers = image.getLayers(); + assertThat(layers).hasSize(46); + assertThat(layers.get(0).toString()) + .isEqualTo("sha256:733a8e5ce32984099ef675fce04730f6e2a6dcfdf5bd292fea01a8f936265342"); + assertThat(layers.get(45).toString()) + .isEqualTo("sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"); + } + + @Test + void getOsReturnsOs() throws Exception { + Image image = getImage(); + assertThat(image.getOs()).isEqualTo("linux"); + } + + private Image getImage() throws IOException { + return Image.of(getContent("image.json")); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/LayerIdTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/LayerIdTests.java new file mode 100644 index 000000000000..34d9bd52e279 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/LayerIdTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Test for {@link LayerId}. + * + * @author Phillip Webb + */ +class LayerIdTests { + + @Test + void ofReturnsLayerId() { + LayerId id = LayerId.of("sha256:9a183e56c86d376b408bdf922746d0a657f62b0e18c7c8f82a496b87710c576f"); + assertThat(id.getAlgorithm()).isEqualTo("sha256"); + assertThat(id.getHash()).isEqualTo("9a183e56c86d376b408bdf922746d0a657f62b0e18c7c8f82a496b87710c576f"); + assertThat(id.toString()).isEqualTo("sha256:9a183e56c86d376b408bdf922746d0a657f62b0e18c7c8f82a496b87710c576f"); + } + + @Test + void hashCodeAndEquals() { + LayerId id1 = LayerId.of("sha256:9a183e56c86d376b408bdf922746d0a657f62b0e18c7c8f82a496b87710c576f"); + LayerId id2 = LayerId.of("sha256:9a183e56c86d376b408bdf922746d0a657f62b0e18c7c8f82a496b87710c576f"); + LayerId id3 = LayerId.of("sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + assertThat(id1.hashCode()).isEqualTo(id2.hashCode()); + assertThat(id1).isEqualTo(id1).isEqualTo(id2).isNotEqualTo(id3); + } + + @Test + void ofWhenValueIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> LayerId.of((String) null)) + .withMessage("Value must not be empty"); + } + + @Test + void ofWhenValueIsEmptyThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> LayerId.of(" ")).withMessage("Value must not be empty"); + } + + @Test + void ofSha256Digest() throws Exception { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + digest.update("test".getBytes(StandardCharsets.UTF_8)); + LayerId id = LayerId.ofSha256Digest(digest.digest()); + assertThat(id.toString()).isEqualTo("sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"); + } + + @Test + void ofSha256DigestWithZeroPadding() { + byte[] digest = new byte[32]; + Arrays.fill(digest, (byte) 127); + digest[0] = 1; + LayerId id = LayerId.ofSha256Digest(digest); + assertThat(id.toString()).isEqualTo("sha256:017f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"); + } + + @Test + void ofSha256DigestWhenNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> LayerId.ofSha256Digest((byte[]) null)) + .withMessage("Digest must not be null"); + } + + @Test + void ofSha256DigestWhenWrongLengthThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> LayerId.ofSha256Digest(new byte[31])) + .withMessage("Digest must be exactly 32 bytes"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/LayerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/LayerTests.java new file mode 100644 index 000000000000..6f237af47667 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/LayerTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.io.Content; +import org.springframework.boot.buildpack.platform.io.IOConsumer; +import org.springframework.boot.buildpack.platform.io.Layout; +import org.springframework.boot.buildpack.platform.io.Owner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link Layer}. + * + * @author Phillip Webb + */ +class LayerTests { + + @Test + void ofWhenLayoutIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> Layer.of((IOConsumer) null)) + .withMessage("Layout must not be null"); + } + + @Test + void fromTarArchiveWhenTarArchiveIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> Layer.fromTarArchive(null)) + .withMessage("TarArchive must not be null"); + } + + @Test + void ofCreatesLayer() throws Exception { + Layer layer = Layer.of((layout) -> { + layout.directory("/directory", Owner.ROOT); + layout.file("/directory/file", Owner.ROOT, Content.of("test")); + }); + assertThat(layer.getId().toString()) + .isEqualTo("sha256:d03a34f73804698c875eb56ff694fc2fceccc69b645e4adceb004ed13588613b"); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + layer.writeTo(outputStream); + try (TarArchiveInputStream tarStream = new TarArchiveInputStream( + new ByteArrayInputStream(outputStream.toByteArray()))) { + assertThat(tarStream.getNextTarEntry().getName()).isEqualTo("/directory/"); + assertThat(tarStream.getNextTarEntry().getName()).isEqualTo("/directory/file"); + assertThat(tarStream.getNextTarEntry()).isNull(); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/RandomStringTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/RandomStringTests.java new file mode 100644 index 000000000000..8305239a9fda --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/RandomStringTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link RandomString}. + * + * @author Phillip Webb + */ +class RandomStringTests { + + @Test + void generateWhenPrefixIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> RandomString.generate(null, 10)) + .withMessage("Prefix must not be null"); + } + + @Test + void generateGeneratesRandomString() { + String s1 = RandomString.generate("abc-", 10); + String s2 = RandomString.generate("abc-", 10); + String s3 = RandomString.generate("abc-", 20); + assertThat(s1).hasSize(14).startsWith("abc-").isNotEqualTo(s2); + assertThat(s3).hasSize(24).startsWith("abc-"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/VolumeNameTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/VolumeNameTests.java new file mode 100644 index 000000000000..365caa9e680b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/VolumeNameTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.type; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link VolumeName}. + * + * @author Phillip Webb + */ +class VolumeNameTests { + + @Test + void randomWhenPrefixIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> VolumeName.random(null)) + .withMessage("Prefix must not be null"); + } + + @Test + void randomGeneratesRandomString() { + VolumeName v1 = VolumeName.random("abc-"); + VolumeName v2 = VolumeName.random("abc-"); + assertThat(v1.toString()).startsWith("abc-").hasSize(14); + assertThat(v2.toString()).startsWith("abc-").hasSize(14); + assertThat(v1).isNotEqualTo(v2); + assertThat(v1.toString()).isNotEqualTo(v2.toString()); + } + + @Test + void randomStringWithLengthGeneratesRandomString() { + VolumeName v1 = VolumeName.random("abc-", 20); + VolumeName v2 = VolumeName.random("abc-", 20); + assertThat(v1.toString()).startsWith("abc-").hasSize(24); + assertThat(v2.toString()).startsWith("abc-").hasSize(24); + assertThat(v1).isNotEqualTo(v2); + assertThat(v1.toString()).isNotEqualTo(v2.toString()); + } + + @Test + void basedOnWhenSourceIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> VolumeName.basedOn(null, "prefix", "suffix", 6)) + .withMessage("Source must not be null"); + } + + @Test + void basedOnWhenNameExtractorIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> VolumeName.basedOn("test", null, "prefix", "suffix", 6)) + .withMessage("NameExtractor must not be null"); + } + + @Test + void basedOnWhenPrefixIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> VolumeName.basedOn("test", null, "suffix", 6)) + .withMessage("Prefix must not be null"); + } + + @Test + void basedOnWhenSuffixIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> VolumeName.basedOn("test", "prefix", null, 6)) + .withMessage("Suffix must not be null"); + } + + @Test + void basedOnGeneratesHashBasedName() { + VolumeName name = VolumeName.basedOn("index.docker.io/library/myapp:latest", "pack-cache-", ".build", 6); + assertThat(name.toString()).isEqualTo("pack-cache-40a311b545d7.build"); + } + + @Test + void basedOnWhenSizeIsTooBigThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> VolumeName.basedOn("name", "prefix", "suffix", 33)) + .withMessage("DigestLength must be less than or equal to 32"); + } + + @Test + void ofWhenValueIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> VolumeName.of(null)) + .withMessage("Value must not be null"); + } + + @Test + void ofGeneratesValue() { + VolumeName name = VolumeName.of("test"); + assertThat(name.toString()).isEqualTo("test"); + } + + @Test + void equalsAndHashCode() { + VolumeName n1 = VolumeName.of("test1"); + VolumeName n2 = VolumeName.of("test1"); + VolumeName n3 = VolumeName.of("test2"); + assertThat(n1.hashCode()).isEqualTo(n2.hashCode()); + assertThat(n1).isEqualTo(n1).isEqualTo(n2).isNotEqualTo(n3); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/ContentTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/ContentTests.java new file mode 100644 index 000000000000..5dd9e148738b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/ContentTests.java @@ -0,0 +1,82 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.io; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link Content}. + * + * @author Phillip Webb + */ +class ContentTests { + + @Test + void ofWhenStreamIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> Content.of(1, (IOSupplier) null)) + .withMessage("Supplier must not be null"); + } + + @Test + void ofWhenStreamReturnsWritable() throws Exception { + byte[] bytes = { 1, 2, 3, 4 }; + ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes); + Content writable = Content.of(4, () -> inputStream); + assertThat(writeToAndGetBytes(writable)).isEqualTo(bytes); + } + + @Test + void ofWhenStringIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> Content.of((String) null)) + .withMessage("String must not be null"); + } + + @Test + void ofWhenStringReturnsWritable() throws Exception { + Content writable = Content.of("spring"); + assertThat(writeToAndGetBytes(writable)).isEqualTo("spring".getBytes(StandardCharsets.UTF_8)); + } + + @Test + void ofWhenBytesIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> Content.of((byte[]) null)) + .withMessage("Bytes must not be null"); + } + + @Test + void ofWhenBytesReturnsWritable() throws Exception { + byte[] bytes = { 1, 2, 3, 4 }; + Content writable = Content.of(bytes); + assertThat(writeToAndGetBytes(writable)).isEqualTo(bytes); + } + + private byte[] writeToAndGetBytes(Content writable) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + writable.writeTo(outputStream); + return outputStream.toByteArray(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/DefaultOwnerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/DefaultOwnerTests.java new file mode 100644 index 000000000000..1398ec76ae01 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/DefaultOwnerTests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.io; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DefaultOwner}. + * + * @author Phillip Webb + */ +class DefaultOwnerTests { + + @Test + void getUidReturnsUid() { + DefaultOwner owner = new DefaultOwner(123, 456); + assertThat(owner.getUid()).isEqualTo(123); + } + + @Test + void getGidReturnsGid() { + DefaultOwner owner = new DefaultOwner(123, 456); + assertThat(owner.getGid()).isEqualTo(456); + } + + @Test + void toStringReturnsString() { + DefaultOwner owner = new DefaultOwner(123, 456); + assertThat(owner).hasToString("123/456"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/FilePermissionsTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/FilePermissionsTests.java new file mode 100644 index 000000000000..a3bcbe21b2da --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/FilePermissionsTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.io; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.Collections; +import java.util.Set; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIOException; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link FilePermissions}. + * + * @author Scott Frederick + */ +class FilePermissionsTests { + + @TempDir + Path tempDir; + + @Test + @DisabledOnOs(OS.WINDOWS) + void umaskForPath() throws IOException { + FileAttribute> fileAttribute = PosixFilePermissions + .asFileAttribute(PosixFilePermissions.fromString("rw-r-----")); + Path tempFile = Files.createTempFile(this.tempDir, "umask", null, fileAttribute); + assertThat(FilePermissions.umaskForPath(tempFile)).isEqualTo(0640); + } + + @Test + @DisabledOnOs(OS.WINDOWS) + void umaskForPathWithNonExistentFile() throws IOException { + assertThatIOException() + .isThrownBy(() -> FilePermissions.umaskForPath(Paths.get(this.tempDir.toString(), "does-not-exist"))); + } + + @Test + @EnabledOnOs(OS.WINDOWS) + void umaskForPathOnWindowsFails() throws IOException { + Path tempFile = Files.createTempFile("umask", null); + assertThatIllegalStateException().isThrownBy(() -> FilePermissions.umaskForPath(tempFile)) + .withMessageContaining("Unsupported file type for retrieving Posix attributes"); + } + + @Test + void umaskForPathWithNullPath() throws IOException { + assertThatIllegalArgumentException().isThrownBy(() -> FilePermissions.umaskForPath(null)); + } + + @Test + void posixPermissionsToUmask() { + Set permissions = PosixFilePermissions.fromString("rwxrw-r--"); + assertThat(FilePermissions.posixPermissionsToUmask(permissions)).isEqualTo(0764); + } + + @Test + void posixPermissionsToUmaskWithEmptyPermissions() { + Set permissions = Collections.emptySet(); + assertThat(FilePermissions.posixPermissionsToUmask(permissions)).isEqualTo(0); + } + + @Test + void posixPermissionsToUmaskWithNullPermissions() { + assertThatIllegalArgumentException().isThrownBy(() -> FilePermissions.posixPermissionsToUmask(null)); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/InspectedContentTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/InspectedContentTests.java new file mode 100644 index 000000000000..999729184765 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/InspectedContentTests.java @@ -0,0 +1,99 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.io; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link InspectedContent}. + * + * @author Phillip Webb + */ +class InspectedContentTests { + + @Test + void ofWhenInputStreamThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> InspectedContent.of((InputStream) null)) + .withMessage("InputStream must not be null"); + } + + @Test + void ofWhenContentIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> InspectedContent.of((Content) null)) + .withMessage("Content must not be null"); + } + + @Test + void ofWhenConsumerIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> InspectedContent.of((IOConsumer) null)) + .withMessage("Writer must not be null"); + } + + @Test + void ofFromContent() throws Exception { + InspectedContent content = InspectedContent.of(Content.of("test")); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + content.writeTo(outputStream); + assertThat(outputStream.toByteArray()).containsExactly("test".getBytes(StandardCharsets.UTF_8)); + } + + @Test + void ofSmallContent() throws Exception { + InputStream inputStream = new ByteArrayInputStream(new byte[] { 0, 1, 2 }); + InspectedContent content = InspectedContent.of(inputStream); + assertThat(content.size()).isEqualTo(3); + assertThat(readBytes(content)).containsExactly(0, 1, 2); + } + + @Test + void ofLargeContent() throws Exception { + byte[] bytes = new byte[InspectedContent.MEMORY_LIMIT + 3]; + System.arraycopy(new byte[] { 0, 1, 2 }, 0, bytes, 0, 3); + InputStream inputStream = new ByteArrayInputStream(bytes); + InspectedContent content = InspectedContent.of(inputStream); + assertThat(content.size()).isEqualTo(bytes.length); + assertThat(readBytes(content)).isEqualTo(bytes); + } + + @Test + void ofWithInspector() throws Exception { + InputStream inputStream = new ByteArrayInputStream("test".getBytes(StandardCharsets.UTF_8)); + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + InspectedContent.of(inputStream, digest::update); + assertThat(digest.digest()).inHexadecimal().contains(0x9f, 0x86, 0xd0, 0x81, 0x88, 0x4c, 0x7d, 0x65, 0x9a, 0x2f, + 0xea, 0xa0, 0xc5, 0x5a, 0xd0, 0x15, 0xa3, 0xbf, 0x4f, 0x1b, 0x2b, 0x0b, 0x82, 0x2c, 0xd1, 0x5d, 0x6c, + 0x15, 0xb0, 0xf0, 0x0a, 0x08); + } + + private byte[] readBytes(InspectedContent content) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + content.writeTo(outputStream); + return outputStream.toByteArray(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/OwnerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/OwnerTests.java new file mode 100644 index 000000000000..a281a0020630 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/OwnerTests.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.io; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Owner}. + * + * @author Phillip Webb + */ +class OwnerTests { + + @Test + void ofReturnsNewOwner() { + Owner owner = Owner.of(123, 456); + assertThat(owner.getUid()).isEqualTo(123); + assertThat(owner.getGid()).isEqualTo(456); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/TarArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/TarArchiveTests.java new file mode 100644 index 000000000000..891c1a1b8eda --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/TarArchiveTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.io; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link TarArchive}. + * + * @author Phillip Webb + */ +class TarArchiveTests { + + @TempDir + File tempDir; + + @Test + void ofWritesTarContent() throws Exception { + Owner owner = Owner.of(123, 456); + TarArchive tarArchive = TarArchive.of((content) -> { + content.directory("/workspace", owner); + content.directory("/layers", owner); + content.directory("/cnb", Owner.ROOT); + content.directory("/cnb/buildpacks", Owner.ROOT); + content.directory("/platform", Owner.ROOT); + content.directory("/platform/env", Owner.ROOT); + }); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + tarArchive.writeTo(outputStream); + try (TarArchiveInputStream tarStream = new TarArchiveInputStream( + new ByteArrayInputStream(outputStream.toByteArray()))) { + List entries = new ArrayList<>(); + TarArchiveEntry entry = tarStream.getNextTarEntry(); + while (entry != null) { + entries.add(entry); + entry = tarStream.getNextTarEntry(); + } + assertThat(entries).hasSize(6); + assertThat(entries.get(0).getName()).isEqualTo("/workspace/"); + assertThat(entries.get(0).getLongUserId()).isEqualTo(123); + assertThat(entries.get(0).getLongGroupId()).isEqualTo(456); + assertThat(entries.get(2).getName()).isEqualTo("/cnb/"); + assertThat(entries.get(2).getLongUserId()).isEqualTo(0); + assertThat(entries.get(2).getLongGroupId()).isEqualTo(0); + } + } + + @Test + void fromZipFileReturnsZipFileAdapter() throws Exception { + Owner owner = Owner.of(123, 456); + File file = new File(this.tempDir, "test.zip"); + writeTestZip(file); + TarArchive tarArchive = TarArchive.fromZip(file, owner); + assertThat(tarArchive).isInstanceOf(ZipFileTarArchive.class); + } + + private void writeTestZip(File file) throws IOException { + try (ZipArchiveOutputStream zip = new ZipArchiveOutputStream(file)) { + ZipArchiveEntry dirEntry = new ZipArchiveEntry("spring/"); + zip.putArchiveEntry(dirEntry); + zip.closeArchiveEntry(); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/TarLayoutWriterTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/TarLayoutWriterTests.java new file mode 100644 index 000000000000..05f5fae8ed54 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/TarLayoutWriterTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.io; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Date; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link TarLayoutWriter}. + * + * @author Phillip Webb + * @author Scott Frederick + */ +class TarLayoutWriterTests { + + @Test + void writesTarArchive() throws Exception { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (TarLayoutWriter writer = new TarLayoutWriter(outputStream)) { + writer.directory("/foo", Owner.ROOT); + writer.file("/foo/bar.txt", Owner.of(1, 1), 0777, Content.of("test")); + } + try (TarArchiveInputStream tarInputStream = new TarArchiveInputStream( + new ByteArrayInputStream(outputStream.toByteArray()))) { + TarArchiveEntry directoryEntry = tarInputStream.getNextTarEntry(); + TarArchiveEntry fileEntry = tarInputStream.getNextTarEntry(); + byte[] fileContent = new byte[(int) fileEntry.getSize()]; + tarInputStream.read(fileContent); + assertThat(tarInputStream.getNextEntry()).isNull(); + assertThat(directoryEntry.getName()).isEqualTo("/foo/"); + assertThat(directoryEntry.getMode()).isEqualTo(0755); + assertThat(directoryEntry.getLongUserId()).isEqualTo(0); + assertThat(directoryEntry.getLongGroupId()).isEqualTo(0); + assertThat(directoryEntry.getModTime()).isEqualTo(new Date(TarLayoutWriter.NORMALIZED_MOD_TIME)); + assertThat(fileEntry.getName()).isEqualTo("/foo/bar.txt"); + assertThat(fileEntry.getMode()).isEqualTo(0777); + assertThat(fileEntry.getLongUserId()).isEqualTo(1); + assertThat(fileEntry.getLongGroupId()).isEqualTo(1); + assertThat(fileEntry.getModTime()).isEqualTo(new Date(TarLayoutWriter.NORMALIZED_MOD_TIME)); + assertThat(fileContent).isEqualTo("test".getBytes(StandardCharsets.UTF_8)); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/ZipFileTarArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/ZipFileTarArchiveTests.java new file mode 100644 index 000000000000..23ae04ae540b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/ZipFileTarArchiveTests.java @@ -0,0 +1,97 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.io; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link ZipFileTarArchive}. + * + * @author Phillip Webb + * @author Scott Frederick + */ +class ZipFileTarArchiveTests { + + @TempDir + File tempDir; + + @Test + void createWhenZipIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new ZipFileTarArchive(null, Owner.ROOT)) + .withMessage("Zip must not be null"); + } + + @Test + void createWhenOwnerIsNullThrowsException() throws Exception { + File file = new File(this.tempDir, "test.zip"); + writeTestZip(file); + assertThatIllegalArgumentException().isThrownBy(() -> new ZipFileTarArchive(file, null)) + .withMessage("Owner must not be null"); + } + + @Test + void writeToAdaptsContent() throws Exception { + Owner owner = Owner.of(123, 456); + File file = new File(this.tempDir, "test.zip"); + writeTestZip(file); + TarArchive tarArchive = TarArchive.fromZip(file, owner); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + tarArchive.writeTo(outputStream); + try (TarArchiveInputStream tarStream = new TarArchiveInputStream( + new ByteArrayInputStream(outputStream.toByteArray()))) { + TarArchiveEntry dirEntry = tarStream.getNextTarEntry(); + assertThat(dirEntry.getName()).isEqualTo("spring/"); + assertThat(dirEntry.getLongUserId()).isEqualTo(123); + assertThat(dirEntry.getLongGroupId()).isEqualTo(456); + TarArchiveEntry fileEntry = tarStream.getNextTarEntry(); + assertThat(fileEntry.getName()).isEqualTo("spring/boot"); + assertThat(fileEntry.getLongUserId()).isEqualTo(123); + assertThat(fileEntry.getLongGroupId()).isEqualTo(456); + assertThat(fileEntry.getSize()).isEqualTo(4); + assertThat(fileEntry.getMode()).isEqualTo(0755); + assertThat(tarStream).hasContent("test"); + } + } + + private void writeTestZip(File file) throws IOException { + try (ZipArchiveOutputStream zip = new ZipArchiveOutputStream(file)) { + ZipArchiveEntry dirEntry = new ZipArchiveEntry("spring/"); + zip.putArchiveEntry(dirEntry); + zip.closeArchiveEntry(); + ZipArchiveEntry fileEntry = new ZipArchiveEntry("spring/boot"); + fileEntry.setUnixMode(0755); + zip.putArchiveEntry(fileEntry); + zip.write("test".getBytes(StandardCharsets.UTF_8)); + zip.closeArchiveEntry(); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/json/AbstractJsonTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/json/AbstractJsonTests.java new file mode 100644 index 000000000000..7065adeba8ba --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/json/AbstractJsonTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.json; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Abstract base class for JSON based tests. + * + * @author Phillip Webb + * @author Scott Frederick + */ +public abstract class AbstractJsonTests { + + protected final ObjectMapper getObjectMapper() { + return SharedObjectMapper.get(); + } + + protected final InputStream getContent(String name) { + InputStream result = getClass().getResourceAsStream(name); + assertThat(result).as("JSON source " + name).isNotNull(); + return result; + } + + protected final String getContentAsString(String name) { + return new BufferedReader(new InputStreamReader(getContent(name), StandardCharsets.UTF_8)).lines() + .collect(Collectors.joining("\n")); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/json/JsonStreamTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/json/JsonStreamTests.java new file mode 100644 index 000000000000..285cc599e3bc --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/json/JsonStreamTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.json; + +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JsonStream}. + * + * @author Phillip Webb + * @author Scott Frederick + */ +class JsonStreamTests extends AbstractJsonTests { + + private JsonStream jsonStream; + + JsonStreamTests() { + this.jsonStream = new JsonStream(getObjectMapper()); + } + + @Test + void getWhenReadingObjectNodeReturnsNodes() throws Exception { + List result = new ArrayList<>(); + this.jsonStream.get(getContent("stream.json"), result::add); + assertThat(result).hasSize(595); + assertThat(result.get(594).toString()) + .contains("Status: Downloaded newer image for paketo-buildpacks/cnb:base"); + } + + @Test + void getWhenReadTypesReturnsTypes() throws Exception { + List result = new ArrayList<>(); + this.jsonStream.get(getContent("stream.json"), TestEvent.class, result::add); + assertThat(result).hasSize(595); + assertThat(result.get(1).getId()).isEqualTo("5667fdb72017"); + assertThat(result.get(594).getStatus()) + .isEqualTo("Status: Downloaded newer image for paketo-buildpacks/cnb:base"); + } + + /** + * Event for type deserialization tests. + */ + static class TestEvent { + + private final String id; + + private final String status; + + @JsonCreator + TestEvent(String id, String status) { + this.id = id; + this.status = status; + } + + String getId() { + return this.id; + } + + String getStatus() { + return this.status; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/json/MappedObjectTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/json/MappedObjectTests.java new file mode 100644 index 000000000000..3e52079ec500 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/json/MappedObjectTests.java @@ -0,0 +1,122 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.json; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.invoke.MethodHandles; + +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.json.MappedObjectTests.TestMappedObject.Person; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MappedObject}. + * + * @author Phillip Webb + */ +class MappedObjectTests extends AbstractJsonTests { + + private final TestMappedObject mapped; + + MappedObjectTests() throws IOException { + this.mapped = TestMappedObject.of(getContent("test-mapped-object.json")); + } + + @Test + void ofReadsJson() { + assertThat(this.mapped.getNode()).isNotNull(); + } + + @Test + void valueAtWhenStringReturnsValue() { + assertThat(this.mapped.valueAt("/string", String.class)).isEqualTo("stringvalue"); + } + + @Test + void valueAtWhenStringArrayReturnsValue() { + assertThat(this.mapped.valueAt("/stringarray", String[].class)).containsExactly("a", "b"); + } + + @Test + void valueAtWhenMissingReturnsNull() { + assertThat(this.mapped.valueAt("/missing", String.class)).isNull(); + } + + @Test + void valueAtWhenInterfaceReturnsProxy() { + Person person = this.mapped.valueAt("/person", Person.class); + assertThat(person.getName().getFirst()).isEqualTo("spring"); + assertThat(person.getName().getLast()).isEqualTo("boot"); + } + + @Test + void valueAtWhenInterfaceAndMissingReturnsProxy() { + Person person = this.mapped.valueAt("/missing", Person.class); + assertThat(person.getName().getFirst()).isNull(); + assertThat(person.getName().getLast()).isNull(); + } + + @Test + void valueAtWhenActualPropertyStartsWithUppercaseReturnsValue() { + assertThat(this.mapped.valueAt("/startsWithUppercase", String.class)).isEqualTo("value"); + } + + @Test + void valueAtWhenDefaultMethodReturnsValue() { + Person person = this.mapped.valueAt("/person", Person.class); + assertThat(person.getName().getFullName()).isEqualTo("dr spring boot"); + } + + /** + * {@link MappedObject} for testing. + */ + static class TestMappedObject extends MappedObject { + + TestMappedObject(JsonNode node) { + super(node, MethodHandles.lookup()); + } + + static TestMappedObject of(InputStream content) throws IOException { + return of(content, TestMappedObject::new); + } + + interface Person { + + Name getName(); + + interface Name { + + String getFirst(); + + String getLast(); + + default String getFullName() { + String title = valueAt(this, "/title", String.class); + return title + " " + getFirst() + " " + getLast(); + } + + } + + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/json/SharedObjectMapperTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/json/SharedObjectMapperTests.java new file mode 100644 index 000000000000..16a7b2844de4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/json/SharedObjectMapperTests.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.json; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SharedObjectMapper}. + * + * @author Phillip Webb + */ +class SharedObjectMapperTests { + + @Test + void getReturnsConfiguredObjectMapper() { + ObjectMapper mapper = SharedObjectMapper.get(); + assertThat(mapper).isNotNull(); + assertThat(mapper.getRegisteredModuleIds()).contains(new ParameterNamesModule().getTypeId()); + assertThat(SerializationFeature.INDENT_OUTPUT + .enabledIn(mapper.getSerializationConfig().getSerializationFeatures())).isTrue(); + assertThat(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES + .enabledIn(mapper.getDeserializationConfig().getDeserializationFeatures())).isFalse(); + assertThat(mapper.getSerializationConfig().getPropertyNamingStrategy()) + .isEqualTo(PropertyNamingStrategies.LOWER_CAMEL_CASE); + assertThat(mapper.getDeserializationConfig().getPropertyNamingStrategy()) + .isEqualTo(PropertyNamingStrategies.LOWER_CAMEL_CASE); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/socket/FileDescriptorTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/socket/FileDescriptorTests.java new file mode 100644 index 000000000000..1c25f597caa4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/socket/FileDescriptorTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.socket; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.socket.FileDescriptor.Handle; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for {@link FileDescriptor}. + * + * @author Phillip Webb + */ +class FileDescriptorTests { + + private int sourceHandle = 123; + + private int closedHandle = 0; + + @Test + void acquireReturnsHandle() throws Exception { + FileDescriptor descriptor = new FileDescriptor(this.sourceHandle, this::close); + try (Handle handle = descriptor.acquire()) { + assertThat(handle.intValue()).isEqualTo(this.sourceHandle); + assertThat(handle.isClosed()).isFalse(); + } + } + + @Test + void acquireWhenClosedReturnsClosedHandle() throws Exception { + FileDescriptor descriptor = new FileDescriptor(this.sourceHandle, this::close); + descriptor.close(); + try (Handle handle = descriptor.acquire()) { + assertThat(handle.intValue()).isEqualTo(-1); + assertThat(handle.isClosed()).isTrue(); + } + } + + @Test + void acquireWhenPendingCloseReturnsClosedHandle() throws Exception { + FileDescriptor descriptor = new FileDescriptor(this.sourceHandle, this::close); + try (Handle handle1 = descriptor.acquire()) { + descriptor.close(); + try (Handle handle2 = descriptor.acquire()) { + assertThat(handle2.intValue()).isEqualTo(-1); + assertThat(handle2.isClosed()).isTrue(); + } + } + } + + @Test + void finalizeTriggersClose() { + FileDescriptor descriptor = new FileDescriptor(this.sourceHandle, this::close); + descriptor.close(); + assertThat(this.closedHandle).isEqualTo(this.sourceHandle); + } + + @Test + void closeWhenHandleAcquiredClosesOnRelease() throws Exception { + FileDescriptor descriptor = new FileDescriptor(this.sourceHandle, this::close); + try (Handle handle = descriptor.acquire()) { + descriptor.close(); + assertThat(this.closedHandle).isEqualTo(0); + } + assertThat(this.closedHandle).isEqualTo(this.sourceHandle); + } + + @Test + void closeWhenHandleNotAcquiredClosesImmediately() { + FileDescriptor descriptor = new FileDescriptor(this.sourceHandle, this::close); + descriptor.close(); + assertThat(this.closedHandle).isEqualTo(this.sourceHandle); + } + + private void close(int handle) { + this.closedHandle = handle; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/builder-metadata-platform-api-0.3.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/builder-metadata-platform-api-0.3.json new file mode 100644 index 000000000000..b6c755e911c1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/builder-metadata-platform-api-0.3.json @@ -0,0 +1,142 @@ +{ + "description": "Ubuntu bionic base image with buildpacks for Java, NodeJS and Golang", + "buildpacks": [ + { + "id": "org.cloudfoundry.googlestackdriver", + "version": "v1.1.11" + }, + { + "id": "org.cloudfoundry.springboot", + "version": "v1.2.13" + }, + { + "id": "org.cloudfoundry.debug", + "version": "v1.2.11" + }, + { + "id": "org.cloudfoundry.tomcat", + "version": "v1.3.18" + }, + { + "id": "org.cloudfoundry.go", + "version": "v0.0.4" + }, + { + "id": "org.cloudfoundry.openjdk", + "version": "v1.2.14" + }, + { + "id": "org.cloudfoundry.buildsystem", + "version": "v1.2.15" + }, + { + "id": "org.cloudfoundry.jvmapplication", + "version": "v1.1.12" + }, + { + "id": "org.cloudfoundry.springautoreconfiguration", + "version": "v1.1.11" + }, + { + "id": "org.cloudfoundry.archiveexpanding", + "version": "v1.0.102" + }, + { + "id": "org.cloudfoundry.jmx", + "version": "v1.1.12" + }, + { + "id": "org.cloudfoundry.nodejs", + "version": "v2.0.8" + }, + { + "id": "org.cloudfoundry.jdbc", + "version": "v1.1.14" + }, + { + "id": "org.cloudfoundry.procfile", + "version": "v1.1.12" + }, + { + "id": "org.cloudfoundry.dotnet-core", + "version": "v0.0.6" + }, + { + "id": "org.cloudfoundry.azureapplicationinsights", + "version": "v1.1.12" + }, + { + "id": "org.cloudfoundry.distzip", + "version": "v1.1.12" + }, + { + "id": "org.cloudfoundry.dep", + "version": "0.0.101" + }, + { + "id": "org.cloudfoundry.go-compiler", + "version": "0.0.105" + }, + { + "id": "org.cloudfoundry.go-mod", + "version": "0.0.89" + }, + { + "id": "org.cloudfoundry.node-engine", + "version": "0.0.163" + }, + { + "id": "org.cloudfoundry.npm", + "version": "0.1.3" + }, + { + "id": "org.cloudfoundry.yarn-install", + "version": "0.1.10" + }, + { + "id": "org.cloudfoundry.dotnet-core-aspnet", + "version": "0.0.118" + }, + { + "id": "org.cloudfoundry.dotnet-core-build", + "version": "0.0.68" + }, + { + "id": "org.cloudfoundry.dotnet-core-conf", + "version": "0.0.115" + }, + { + "id": "org.cloudfoundry.dotnet-core-runtime", + "version": "0.0.127" + }, + { + "id": "org.cloudfoundry.dotnet-core-sdk", + "version": "0.0.122" + }, + { + "id": "org.cloudfoundry.icu", + "version": "0.0.43" + }, + { + "id": "org.cloudfoundry.node-engine", + "version": "0.0.158" + } + ], + "stack": { + "runImage": { + "image": "cloudfoundry/run:base-cnb", + "mirrors": null + } + }, + "lifecycle": { + "version": "0.7.2", + "api": { + "buildpack": "0.2", + "platform": "0.3" + } + }, + "createdBy": { + "name": "Pack CLI", + "version": "v0.9.0 (git sha: d42c384a39f367588f2653f2a99702db910e5ad7)" + } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/builder-metadata-supported-apis.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/builder-metadata-supported-apis.json new file mode 100644 index 000000000000..4a695f9766f2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/builder-metadata-supported-apis.json @@ -0,0 +1,43 @@ +{ + "description": "Ubuntu bionic base image with buildpacks for Java, NodeJS and Golang", + "buildpacks": [ + { + "id": "org.cloudfoundry.springboot", + "version": "v1.2.13" + } + ], + "stack": { + "runImage": { + "image": "cloudfoundry/run:base-cnb", + "mirrors": null + } + }, + "lifecycle": { + "version": "0.7.2", + "api": { + "buildpack": "0.2", + "platform": "0.4" + }, + "apis": { + "buildpack": { + "deprecated": [], + "supported": [ + "0.1", + "0.2", + "0.3" + ] + }, + "platform": { + "deprecated": [], + "supported": [ + "0.3", + "0.4" + ] + } + } + }, + "createdBy": { + "name": "Pack CLI", + "version": "v0.9.0 (git sha: d42c384a39f367588f2653f2a99702db910e5ad7)" + } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/builder-metadata-unsupported-api.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/builder-metadata-unsupported-api.json new file mode 100644 index 000000000000..f7a7ae9b3947 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/builder-metadata-unsupported-api.json @@ -0,0 +1,26 @@ +{ + "description": "Ubuntu bionic base image with buildpacks for Java, NodeJS and Golang", + "buildpacks": [ + { + "id": "org.cloudfoundry.springboot", + "version": "v1.2.13" + } + ], + "stack": { + "runImage": { + "image": "cloudfoundry/run:base-cnb", + "mirrors": null + } + }, + "lifecycle": { + "version": "0.7.2", + "api": { + "buildpack": "0.2", + "platform": "0.2" + } + }, + "createdBy": { + "name": "Pack CLI", + "version": "v0.9.0 (git sha: d42c384a39f367588f2653f2a99702db910e5ad7)" + } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/builder-metadata-unsupported-apis.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/builder-metadata-unsupported-apis.json new file mode 100644 index 000000000000..74f94029cee7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/builder-metadata-unsupported-apis.json @@ -0,0 +1,43 @@ +{ + "description": "Ubuntu bionic base image with buildpacks for Java, NodeJS and Golang", + "buildpacks": [ + { + "id": "org.cloudfoundry.springboot", + "version": "v1.2.13" + } + ], + "stack": { + "runImage": { + "image": "cloudfoundry/run:base-cnb", + "mirrors": null + } + }, + "lifecycle": { + "version": "0.7.2", + "api": { + "buildpack": "0.2", + "platform": "0.3" + }, + "apis": { + "buildpack": { + "deprecated": [], + "supported": [ + "0.1", + "0.2", + "0.3" + ] + }, + "platform": { + "deprecated": [], + "supported": [ + "0.5", + "0.6" + ] + } + } + }, + "createdBy": { + "name": "Pack CLI", + "version": "v0.9.0 (git sha: d42c384a39f367588f2653f2a99702db910e5ad7)" + } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/builder-metadata.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/builder-metadata.json new file mode 100644 index 000000000000..b2470b87a31e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/builder-metadata.json @@ -0,0 +1,192 @@ +{ + "description": "Ubuntu bionic base image with buildpacks for Java, NodeJS and Golang", + "buildpacks": [ + { + "id": "paketo-buildpacks/dotnet-core", + "version": "0.0.9", + "homepage": "https://github.com/paketo-buildpacks/dotnet-core" + }, + { + "id": "paketo-buildpacks/dotnet-core-runtime", + "version": "0.0.201", + "homepage": "https://github.com/paketo-buildpacks/dotnet-core-runtime" + }, + { + "id": "paketo-buildpacks/dotnet-core-sdk", + "version": "0.0.196", + "homepage": "https://github.com/paketo-buildpacks/dotnet-core-sdk" + }, + { + "id": "paketo-buildpacks/dotnet-execute", + "version": "0.0.180", + "homepage": "https://github.com/paketo-buildpacks/dotnet-execute" + }, + { + "id": "paketo-buildpacks/dotnet-publish", + "version": "0.0.121", + "homepage": "https://github.com/paketo-buildpacks/dotnet-publish" + }, + { + "id": "paketo-buildpacks/dotnet-core-aspnet", + "version": "0.0.196", + "homepage": "https://github.com/paketo-buildpacks/dotnet-core-aspnet" + }, + { + "id": "paketo-buildpacks/java-native-image", + "version": "4.7.0", + "homepage": "https://github.com/paketo-buildpacks/java-native-image" + }, + { + "id": "paketo-buildpacks/spring-boot", + "version": "3.5.0", + "homepage": "https://github.com/paketo-buildpacks/spring-boot" + }, + { + "id": "paketo-buildpacks/executable-jar", + "version": "3.1.3", + "homepage": "https://github.com/paketo-buildpacks/executable-jar" + }, + { + "id": "paketo-buildpacks/graalvm", + "version": "4.1.0", + "homepage": "https://github.com/paketo-buildpacks/graalvm" + }, + { + "id": "paketo-buildpacks/gradle", + "version": "3.5.0", + "homepage": "https://github.com/paketo-buildpacks/gradle" + }, + { + "id": "paketo-buildpacks/leiningen", + "version": "1.2.1", + "homepage": "https://github.com/paketo-buildpacks/leiningen" + }, + { + "id": "paketo-buildpacks/procfile", + "version": "3.0.0", + "homepage": "https://github.com/paketo-buildpacks/procfile" + }, + { + "id": "paketo-buildpacks/sbt", + "version": "3.6.0", + "homepage": "https://github.com/paketo-buildpacks/sbt" + }, + { + "id": "paketo-buildpacks/spring-boot-native-image", + "version": "2.0.1", + "homepage": "https://github.com/paketo-buildpacks/spring-boot-native-image" + }, + { + "id": "paketo-buildpacks/environment-variables", + "version": "2.1.2", + "homepage": "https://github.com/paketo-buildpacks/environment-variables" + }, + { + "id": "paketo-buildpacks/image-labels", + "version": "2.0.7", + "homepage": "https://github.com/paketo-buildpacks/image-labels" + }, + { + "id": "paketo-buildpacks/maven", + "version": "3.2.1", + "homepage": "https://github.com/paketo-buildpacks/maven" + }, + { + "id": "paketo-buildpacks/java", + "version": "4.10.0", + "homepage": "https://github.com/paketo-buildpacks/java" + }, + { + "id": "paketo-buildpacks/ca-certificates", + "version": "1.0.1", + "homepage": "https://github.com/paketo-buildpacks/ca-certificates" + }, + { + "id": "paketo-buildpacks/environment-variables", + "version": "2.1.2", + "homepage": "https://github.com/paketo-buildpacks/environment-variables" + }, + { + "id": "paketo-buildpacks/executable-jar", + "version": "3.1.3", + "homepage": "https://github.com/paketo-buildpacks/executable-jar" + }, + { + "id": "paketo-buildpacks/procfile", + "version": "3.0.0", + "homepage": "https://github.com/paketo-buildpacks/procfile" + }, + { + "id": "paketo-buildpacks/apache-tomcat", + "version": "3.2.0", + "homepage": "https://github.com/paketo-buildpacks/apache-tomcat" + }, + { + "id": "paketo-buildpacks/gradle", + "version": "3.5.0", + "homepage": "https://github.com/paketo-buildpacks/gradle" + }, + { + "id": "paketo-buildpacks/maven", + "version": "3.2.1", + "homepage": "https://github.com/paketo-buildpacks/maven" + }, + { + "id": "paketo-buildpacks/sbt", + "version": "3.6.0", + "homepage": "https://github.com/paketo-buildpacks/sbt" + }, + { + "id": "paketo-buildpacks/bellsoft-liberica", + "version": "6.2.0", + "homepage": "https://github.com/paketo-buildpacks/bellsoft-liberica" + }, + { + "id": "paketo-buildpacks/image-labels", + "version": "2.0.7", + "homepage": "https://github.com/paketo-buildpacks/image-labels" + }, + { + "id": "paketo-buildpacks/debug", + "version": "2.1.4", + "homepage": "https://github.com/paketo-buildpacks/debug" + }, + { + "id": "paketo-buildpacks/dist-zip", + "version": "2.2.2", + "homepage": "https://github.com/paketo-buildpacks/dist-zip" + }, + { + "id": "paketo-buildpacks/spring-boot", + "version": "3.5.0", + "homepage": "https://github.com/paketo-buildpacks/spring-boot" + }, + { + "id": "paketo-buildpacks/jmx", + "version": "2.1.4", + "homepage": "https://github.com/paketo-buildpacks/jmx" + }, + { + "id": "paketo-buildpacks/leiningen", + "version": "1.2.1", + "homepage": "https://github.com/paketo-buildpacks/leiningen" + } + ], + "stack": { + "runImage": { + "image": "cloudfoundry/run:base-cnb", + "mirrors": null + } + }, + "lifecycle": { + "version": "0.7.2", + "api": { + "buildpack": "0.2", + "platform": "0.4" + } + }, + "createdBy": { + "name": "Pack CLI", + "version": "v0.9.0 (git sha: d42c384a39f367588f2653f2a99702db910e5ad7)" + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/buildpack-image.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/buildpack-image.json new file mode 100644 index 000000000000..41a3777526d1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/buildpack-image.json @@ -0,0 +1,78 @@ +{ + "Id": "sha256:a266647e285b52403b556adc963f1809556aa999f2f694e8dc54098c570ee55a", + "RepoTags": [ + "example/hello-universe:latest" + ], + "RepoDigests": [], + "Parent": "", + "Comment": "", + "Created": "1980-01-01T00:00:01Z", + "Container": "", + "ContainerConfig": { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": null, + "Image": "", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": null + }, + "DockerVersion": "", + "Author": "", + "Config": { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": null, + "Image": "", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": { + "io.buildpacks.buildpackage.metadata": "{\"id\":\"example/hello-universe\",\"version\":\"0.0.1\",\"homepage\":\"https://github.com/buildpacks/example/tree/main/buildpacks/hello-universe\",\"stacks\":[{\"id\":\"io.buildpacks.example.stacks.alpine\"},{\"id\":\"io.buildpacks.stacks.bionic\"}]}", + "io.buildpacks.buildpack.layers": "{\"example/hello-moon\":{\"0.0.3\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.alpine\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:4bfdc8714aee68da6662c43bc28d3b41202c88e915641c356523dabe729814c2\",\"homepage\":\"https://github.com/example/tree/main/buildpacks/hello-moon\"}},\"example/hello-universe\":{\"0.0.1\":{\"api\":\"0.2\",\"order\":[{\"group\":[{\"id\":\"example/hello-world\",\"version\":\"0.0.2\"},{\"id\":\"example/hello-moon\",\"version\":\"0.0.2\"}]}],\"layerDiffID\":\"sha256:739b4e8f3caae7237584a1bfe029ebdb05403752b1a60a4f9be991b1d51dbb69\",\"homepage\":\"https://github.com/example/tree/main/buildpacks/hello-universe\"}},\"example/hello-world\":{\"0.0.2\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.alpine\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:f752fe099c846e501bdc991d1a22f98c055ddc62f01cfc0495fff2c69f8eb940\",\"homepage\":\"https://github.com/example/tree/main/buildpacks/hello-world\"}}}" + } + }, + "Architecture": "amd64", + "Os": "linux", + "Size": 4654, + "VirtualSize": 4654, + "GraphDriver": { + "Data": { + "LowerDir": "/var/lib/docker/overlay2/cbf39b4508463beeb1d0a553c3e2baa84b8cd8dbc95681aaecc243e3ca77bcf4/diff:/var/lib/docker/overlay2/15e3d01b65c962b50a3da1b6663b8196284fb3c7e7f8497f2c1a0a736d0ec237/diff", + "MergedDir": "/var/lib/docker/overlay2/1425ea68b0daff01bcc32e55e09eeeada2318d7dd1dc4e184711359da8425bb7/merged", + "UpperDir": "/var/lib/docker/overlay2/1425ea68b0daff01bcc32e55e09eeeada2318d7dd1dc4e184711359da8425bb7/diff", + "WorkDir": "/var/lib/docker/overlay2/1425ea68b0daff01bcc32e55e09eeeada2318d7dd1dc4e184711359da8425bb7/work" + }, + "Name": "overlay2" + }, + "RootFS": { + "Type": "layers", + "Layers": [ + "sha256:4bfdc8714aee68da6662c43bc28d3b41202c88e915641c356523dabe729814c2", + "sha256:f752fe099c846e501bdc991d1a22f98c055ddc62f01cfc0495fff2c69f8eb940", + "sha256:739b4e8f3caae7237584a1bfe029ebdb05403752b1a60a4f9be991b1d51dbb69" + ] + }, + "Metadata": { + "LastTagTime": "2021-01-27T22:56:06.4599859Z" + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/buildpack-metadata.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/buildpack-metadata.json new file mode 100644 index 000000000000..bdb2b1265840 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/buildpack-metadata.json @@ -0,0 +1,13 @@ +{ + "id": "example/hello-universe", + "version": "0.0.1", + "homepage": "https://github.com/example/tree/main/buildpacks/hello-universe", + "stacks": [ + { + "id": "io.buildpacks.stacks.alpine" + }, + { + "id": "io.buildpacks.stacks.bionic" + } + ] +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/buildpack.toml b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/buildpack.toml new file mode 100644 index 000000000000..2a15b01943c3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/buildpack.toml @@ -0,0 +1,8 @@ +[buildpack] +id = "test"; +version = "1.0.0" +name = "Example buildpack" +homepage = "https://github.com/example/example-buildpack" + +[[stacks]] +id = "io.buildpacks.stacks.bionic" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/image-with-no-run-image-tag.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/image-with-no-run-image-tag.json new file mode 100644 index 000000000000..20114b3df3db --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/image-with-no-run-image-tag.json @@ -0,0 +1,132 @@ +{ + "Id": "sha256:44cc64492fb6a6d78d3e6d087f380ae6e479aa1b2c79823b32cdacfcc2f3d715", + "RepoTags": [ + "paketo-buildpacks/cnb:base", + "paketo-buildpacks/builder:base-platform-api-0.2" + ], + "RepoDigests": [ + "paketo-buidpacks/cnb@sha256:5b03a853e636b78c44e475bbc514e2b7b140cc41cca8ab907e9753431ae8c0b0" + ], + "Parent": "", + "Comment": "", + "Created": "1980-01-01T00:00:01Z", + "Container": "", + "ContainerConfig": { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": null, + "Image": "", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": null + }, + "DockerVersion": "", + "Author": "", + "Config": { + "Hostname": "", + "Domainname": "", + "User": "1000:1000", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "CNB_USER_ID=1000", + "CNB_GROUP_ID=1000", + "CNB_STACK_ID=io.buildpacks.stacks.bionic" + ], + "Cmd": [ + "/bin/bash" + ], + "ArgsEscaped": true, + "Image": "sha256:2d153261a5e359c632a17377cfb5d1986c27b96c8b6e95334bf80f1029dbd4bb", + "Volumes": null, + "WorkingDir": "/layers", + "Entrypoint": null, + "OnBuild": null, + "Labels": { + "io.buildpacks.builder.metadata": "{\"description\":\"Ubuntu bionic base image with buildpacks for Java, NodeJS and Golang\",\"buildpacks\":[{\"id\":\"org.cloudfoundry.googlestackdriver\",\"version\":\"v1.1.11\"},{\"id\":\"org.cloudfoundry.springboot\",\"version\":\"v1.2.13\"},{\"id\":\"org.cloudfoundry.debug\",\"version\":\"v1.2.11\"},{\"id\":\"org.cloudfoundry.tomcat\",\"version\":\"v1.3.18\"},{\"id\":\"org.cloudfoundry.go\",\"version\":\"v0.0.4\"},{\"id\":\"org.cloudfoundry.openjdk\",\"version\":\"v1.2.14\"},{\"id\":\"org.cloudfoundry.buildsystem\",\"version\":\"v1.2.15\"},{\"id\":\"org.cloudfoundry.jvmapplication\",\"version\":\"v1.1.12\"},{\"id\":\"org.cloudfoundry.springautoreconfiguration\",\"version\":\"v1.1.11\"},{\"id\":\"org.cloudfoundry.archiveexpanding\",\"version\":\"v1.0.102\"},{\"id\":\"org.cloudfoundry.jmx\",\"version\":\"v1.1.12\"},{\"id\":\"org.cloudfoundry.nodejs\",\"version\":\"v2.0.8\"},{\"id\":\"org.cloudfoundry.jdbc\",\"version\":\"v1.1.14\"},{\"id\":\"org.cloudfoundry.procfile\",\"version\":\"v1.1.12\"},{\"id\":\"org.cloudfoundry.dotnet-core\",\"version\":\"v0.0.6\"},{\"id\":\"org.cloudfoundry.azureapplicationinsights\",\"version\":\"v1.1.12\"},{\"id\":\"org.cloudfoundry.distzip\",\"version\":\"v1.1.12\"},{\"id\":\"org.cloudfoundry.dep\",\"version\":\"0.0.101\"},{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.105\"},{\"id\":\"org.cloudfoundry.go-mod\",\"version\":\"0.0.89\"},{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.163\"},{\"id\":\"org.cloudfoundry.npm\",\"version\":\"0.1.3\"},{\"id\":\"org.cloudfoundry.yarn-install\",\"version\":\"0.1.10\"},{\"id\":\"org.cloudfoundry.dotnet-core-aspnet\",\"version\":\"0.0.118\"},{\"id\":\"org.cloudfoundry.dotnet-core-build\",\"version\":\"0.0.68\"},{\"id\":\"org.cloudfoundry.dotnet-core-conf\",\"version\":\"0.0.115\"},{\"id\":\"org.cloudfoundry.dotnet-core-runtime\",\"version\":\"0.0.127\"},{\"id\":\"org.cloudfoundry.dotnet-core-sdk\",\"version\":\"0.0.122\"},{\"id\":\"org.cloudfoundry.icu\",\"version\":\"0.0.43\"},{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.158\"}],\"stack\":{\"runImage\":{\"image\":\"cloudfoundry/run\",\"mirrors\":null}},\"lifecycle\":{\"version\":\"0.7.2\",\"api\":{\"buildpack\":\"0.2\",\"platform\":\"0.3\"}},\"createdBy\":{\"name\":\"Pack CLI\",\"version\":\"v0.9.0 (git sha: d42c384a39f367588f2653f2a99702db910e5ad7)\"}}", + "io.buildpacks.buildpack.layers": "{\"org.cloudfoundry.archiveexpanding\":{\"v1.0.102\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:73b1a8ac1f7fca3d545766ce7fd3c56b40a63724ab78e464d71a29da0c6ac31c\"}},\"org.cloudfoundry.azureapplicationinsights\":{\"v1.1.12\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:a0a2f7c467efbb8b1ac222f09013b88b68f3c117ec6b6e9dc95564be50f271ab\"}},\"org.cloudfoundry.buildsystem\":{\"v1.2.15\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:3f50d3a0e1a969a9606b59e5295842d731e425108cb349ce6c69a5b30ea1bab9\"}},\"org.cloudfoundry.debug\":{\"v1.2.11\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:04559213a01cfac69a8d6a6facb58b8681666525c74f605207c40a61a0f4c9b7\"}},\"org.cloudfoundry.dep\":{\"0.0.101\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.tiny\"}],\"layerDiffID\":\"sha256:6aae3a2d671d369eec34dc9146ef267d06c87461f271fbfbe9136775ecf5dfb8\"}},\"org.cloudfoundry.distzip\":{\"v1.1.12\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:a0715e661e13d7d3ded5bdc068edd01e5b3aa0e2805152f4c8a1428b4e0673df\"}},\"org.cloudfoundry.dotnet-core\":{\"v0.0.6\":{\"api\":\"0.2\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.158\",\"optional\":true},{\"id\":\"org.cloudfoundry.icu\",\"version\":\"0.0.43\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-runtime\",\"version\":\"0.0.127\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-aspnet\",\"version\":\"0.0.118\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-sdk\",\"version\":\"0.0.122\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-build\",\"version\":\"0.0.68\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-conf\",\"version\":\"0.0.115\"}]}],\"layerDiffID\":\"sha256:aa0effdf787ecfe74d60d6771006717fd1a9ce1ce0a8161624baa61b68120357\"}},\"org.cloudfoundry.dotnet-core-aspnet\":{\"0.0.118\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:a06615b5adc1a3afb7abd524e82f6900a28910927fcf0d4e9b85fd1fcbeb53ad\"}},\"org.cloudfoundry.dotnet-core-build\":{\"0.0.68\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:26d6f1e76275d17860005f7ab9b74fdd2283fcf84e0446bd88d49a6b4e9609f9\"}},\"org.cloudfoundry.dotnet-core-conf\":{\"0.0.115\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:55f7c052cf70c8ca01b8e241c0c5c8a9675599d4904c69bfb961a472e246238d\"}},\"org.cloudfoundry.dotnet-core-runtime\":{\"0.0.127\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:d9958b816a9ad179fca8c18d17c07e9814b152d461c685e1443bec6f990ab990\"}},\"org.cloudfoundry.dotnet-core-sdk\":{\"0.0.122\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:52142799a4b687fe6e5cf397c41064499ea6cc554b94904d46c1acade998e11f\"}},\"org.cloudfoundry.go\":{\"v0.0.4\":{\"api\":\"0.2\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.105\"},{\"id\":\"org.cloudfoundry.go-mod\",\"version\":\"0.0.89\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.105\"},{\"id\":\"org.cloudfoundry.dep\",\"version\":\"0.0.101\"}]}],\"layerDiffID\":\"sha256:352a299d6af4773322ed3643d8f98b01aad6f15d838d1852e52a0a3ca56c6efb\"}},\"org.cloudfoundry.go-compiler\":{\"0.0.105\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.tiny\"}],\"layerDiffID\":\"sha256:cb21f14e306d94e437c5418d275bcc6efcea6bc9b3d26a400bdf54fa62242c24\"}},\"org.cloudfoundry.go-mod\":{\"0.0.89\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.tiny\"}],\"layerDiffID\":\"sha256:c9da8171f5ca048109ffba5e940e3a7d2db567eda281f92b0eb483173df06add\"}},\"org.cloudfoundry.googlestackdriver\":{\"v1.1.11\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:ff29efc56c31eeccc79a33c6e4abd7b1ab3547d95e1cf83974af65a493576c41\"}},\"org.cloudfoundry.icu\":{\"0.0.43\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:48063dcdd043f9c88604d10fe9542569be8f8111d46806c96b08d77763ffa347\"}},\"org.cloudfoundry.jdbc\":{\"v1.1.14\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:a9c9bbbd69c212b7ab3c1a7f03011ccc4d99a6fce1bf1c785325c7bcad789e62\"}},\"org.cloudfoundry.jmx\":{\"v1.1.12\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:da62dec6eb4ed884952a1b867fd89e3bfe3c510e5c849cc0ac7050ff867a2469\"}},\"org.cloudfoundry.jvmapplication\":{\"v1.1.12\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:c10732392b97c121a78a5f20201c2a5e834a2b8677196cdd49260a489a54fd22\"}},\"org.cloudfoundry.node-engine\":{\"0.0.158\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:70cf83155575fdb607f23ace41e31b1d5cb1c24dbbbf56f71c383b583724d339\"},\"0.0.163\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:11486cb955594f9d43909b60f94209bb6854f502a5a093207b657afbaa38a777\"}},\"org.cloudfoundry.nodejs\":{\"v2.0.8\":{\"api\":\"0.2\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.163\"},{\"id\":\"org.cloudfoundry.yarn-install\",\"version\":\"0.1.10\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.163\"},{\"id\":\"org.cloudfoundry.npm\",\"version\":\"0.1.3\"}]}],\"layerDiffID\":\"sha256:76fe727e4aafc7f56f01282296ab736521c38b9d19c1ae5ebb193f9cd55fa109\"}},\"org.cloudfoundry.npm\":{\"0.1.3\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:243bbd007cb0ee99b704bfe0cf62e1301baa4095ab4c39b01293787a0e4234f1\"}},\"org.cloudfoundry.openjdk\":{\"v1.2.14\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:486b2abf434bb90cf04bab74f2f8bd2eb488ff90632b56eac4bddcbbf02e8151\"}},\"org.cloudfoundry.procfile\":{\"v1.1.12\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:b7b78159dfdaa0dd484c58652e02fa6b755abfd0adb88f106d16178144e46f33\"}},\"org.cloudfoundry.springautoreconfiguration\":{\"v1.1.11\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:c185540c10fea822c6db1b987fcfe22b55a4662648124b98475db4c9dcddb2ab\"}},\"org.cloudfoundry.springboot\":{\"v1.2.13\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:b87e68574cc7dccbe974fa760702ef650711036bf144fd9da1f3a2d8f6ac335f\"}},\"org.cloudfoundry.tomcat\":{\"v1.3.18\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:467c0082c57b80b48487a9b8429887c0744ddc5b066b3f7678866bde89b78ab2\"}},\"org.cloudfoundry.yarn-install\":{\"0.1.10\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:6aefa0ba7ce01584b4a531b18e36470298cee3b30ecae0e0c64b532a5cebd6e7\"}}}", + "io.buildpacks.buildpack.order": "[{\"group\":[{\"id\":\"org.cloudfoundry.openjdk\"},{\"id\":\"org.cloudfoundry.buildsystem\",\"optional\":true},{\"id\":\"org.cloudfoundry.jvmapplication\"},{\"id\":\"org.cloudfoundry.tomcat\",\"optional\":true},{\"id\":\"org.cloudfoundry.springboot\",\"optional\":true},{\"id\":\"org.cloudfoundry.distzip\",\"optional\":true},{\"id\":\"org.cloudfoundry.procfile\",\"optional\":true},{\"id\":\"org.cloudfoundry.azureapplicationinsights\",\"optional\":true},{\"id\":\"org.cloudfoundry.debug\",\"optional\":true},{\"id\":\"org.cloudfoundry.googlestackdriver\",\"optional\":true},{\"id\":\"org.cloudfoundry.jdbc\",\"optional\":true},{\"id\":\"org.cloudfoundry.jmx\",\"optional\":true},{\"id\":\"org.cloudfoundry.springautoreconfiguration\",\"optional\":true}]},{\"group\":[{\"id\":\"org.cloudfoundry.nodejs\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.go\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.dotnet-core\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.procfile\"}]}]", + "io.buildpacks.stack.id": "io.buildpacks.stacks.bionic", + "io.buildpacks.stack.mixins": "[\"build:git\",\"build:build-essential\"]" + } + }, + "Architecture": "amd64", + "Os": "linux", + "Size": 688884758, + "VirtualSize": 688884758, + "GraphDriver": { + "Data": { + "LowerDir": "/var/lib/docker/overlay2/6a79181b2840da2706624f46ce5abd4448973b4f951925d5a276b273256063b2/diff:/var/lib/docker/overlay2/429419a203100f60ab16ec6c879fce975c8138422b9053f80accd6124c730fc2/diff:/var/lib/docker/overlay2/6e45ed6daf4f4f3b90fd1ec5fa958775000875661d3e8be3f1af218d192b058d/diff:/var/lib/docker/overlay2/22928ad308cdd55b3fe849d92b6e38c6bc303ba7c9beb8c0e79aa958e16b1864/diff:/var/lib/docker/overlay2/2ca9ec213226a1604f57c8e141d6f1168134a5cb2ccd8f91ee9be5a39036e6bf/diff:/var/lib/docker/overlay2/96ae944fe00ec20cf5b4441b112ebcc9395faaf08108c9ee38c62e1da33af1c8/diff:/var/lib/docker/overlay2/13ee52e300e476e27350c9ac6274dedf26af85c3079b42a41f9dfc92eff57a80/diff:/var/lib/docker/overlay2/223edb4cc62a2ba2b8bda866905a55c4798c6c32e31d22d60e6ed4f3169ce85e/diff:/var/lib/docker/overlay2/a41235cd7277299cb74ead47def3771885948719e24075ea3bf37580f3af7ae2/diff:/var/lib/docker/overlay2/ed0438e8e2c27b9d62ad21a0761237c350a2ffc9e52f47c019e4f627091c832e/diff:/var/lib/docker/overlay2/0c27c8229b31eafc57ab739b44962dcc07b72f3d8950888873ecb3cfd385032f/diff:/var/lib/docker/overlay2/0957cbcca052cd58bcf9a3d945b0e6876b0df79c1c534da1872c3415a019427d/diff:/var/lib/docker/overlay2/b621414d53d71349c07df8ed45e3e04b2e97bfbaf4bf0d86463f46e0f810eeb4/diff:/var/lib/docker/overlay2/ad521bc47f0bb44262358cf47c3d81a544d098494cf24a5b510620d34eb9c353/diff:/var/lib/docker/overlay2/081501d5bfbd927e69c10eb320513c7c0d5f00bea8cf9e55faa90579fd33adf4/diff:/var/lib/docker/overlay2/fb1ba66bee5568f5700c72865d020d4171a62bfdd099c3cc05b9a253d36a35a4/diff:/var/lib/docker/overlay2/06bcc6b3adeca727d554f1a745ee33242dfe1b3c6392023ac947666057303288/diff:/var/lib/docker/overlay2/1c5397d63d893202dffde29013ee826fb695bda26c718ee03ddde376be4da0a3/diff:/var/lib/docker/overlay2/76075fb7fd3c6b3fb116fb3b464e220918e56d94461c61af9a1aff288ebdba60/diff:/var/lib/docker/overlay2/43d1026bb7b618393912ecc9ddf57b604336184d5f8dc70bcf6332b5f08a3e8d/diff:/var/lib/docker/overlay2/ee27d1fba3deaca0556f7bab171cb3368f169011dd132cf335b5308728f6db8f/diff:/var/lib/docker/overlay2/464d3ec8d86ff31dcb5063ea25521368ea8e9c7964f65e15ff5e0e1ecdbe991e/diff:/var/lib/docker/overlay2/a4a80c33c8b78f68bdc9dbd5903cc2ba1d48e78b9a97d43acb018823ece8e6cb/diff:/var/lib/docker/overlay2/6494f2f1693cff8b16d51fa95620eb0bb691a76fb39b5175d953649577791297/diff:/var/lib/docker/overlay2/9d49e146f82eb5fc4fd81613538e9c5f5f95091fbbc8c49729c6c9140ae356de/diff:/var/lib/docker/overlay2/2934818c52bcd017abe000e71342d67fbc9ccb7dbc165ce05e3250e2110229a5/diff:/var/lib/docker/overlay2/651ca06b2bf75e2122855264287fc937f30d2b49229d628909895be7128b4eb6/diff:/var/lib/docker/overlay2/c93bab59be44fa1b66689dc059d26742d00d2e787d06c3236e1f116199c9807e/diff:/var/lib/docker/overlay2/d0a8e2a0c7e0df172f7a8ebe75e2dce371bb6cc65531b06799bc677c5b5e3627/diff:/var/lib/docker/overlay2/7d14bac240e0d7936351e3fac80b7fbe2a209f4de8992091c4f75e41f9627852/diff:/var/lib/docker/overlay2/d6b192ea137a4ae95e309d263ee8c890e35da02aacd9bdcf5adbd4c28a0c0a3f/diff:/var/lib/docker/overlay2/335bfb632ab7723e25fb5dc7b67389e6ec38178ef10bfbf83337501403e61574/diff:/var/lib/docker/overlay2/0293c7e3472da58f51cbdf15fb293ff71e32c1f80f83f00fb09f8941deef5e43/diff:/var/lib/docker/overlay2/55faa8b47bcb0dd29c3836580f451a0461dd499065af9c830beff6e8329ab484/diff:/var/lib/docker/overlay2/afcb6e109c1ba7d71b8a8b7e573d4ce04f22da3fe0ee523359db5cfb95e65bb6/diff:/var/lib/docker/overlay2/b42eefd9bf6629ae9d16e7aba6ba3939d37816aba7a0999f6d639012a3119be1/diff:/var/lib/docker/overlay2/a9832c8f81ee889a622ce4d95d9f4bab2f91d30e18f69bfd7cfc385c781068d4/diff:/var/lib/docker/overlay2/224041c135f13881a98b9e833584bedab81d5650061457f522a1ebd1daa2c77a/diff:/var/lib/docker/overlay2/73dfd4e2075fccb239b3d5e9b33b32b8e410bdc3cd5a620b41346f44cc5c51f7/diff:/var/lib/docker/overlay2/b3924ed7c91730f6714d33c455db888604b59ab093033b3f59ac16ecdd777987/diff:/var/lib/docker/overlay2/e36a32cd0ab20b216a8db1a8a166b17464399e4d587d22504088a7a6ef0a68a4/diff:/var/lib/docker/overlay2/3334e94fe191333b65f571912c0fcfbbf31aeb090a2fb9b4cfdbc32a37c0fe5f/diff", + "MergedDir": "/var/lib/docker/overlay2/f5d133c5929da8cc8266cbbc3e36f924f4a9c835f943fb436445a26b7e1bcc56/merged", + "UpperDir": "/var/lib/docker/overlay2/f5d133c5929da8cc8266cbbc3e36f924f4a9c835f943fb436445a26b7e1bcc56/diff", + "WorkDir": "/var/lib/docker/overlay2/f5d133c5929da8cc8266cbbc3e36f924f4a9c835f943fb436445a26b7e1bcc56/work" + }, + "Name": "overlay2" + }, + "RootFS": { + "Type": "layers", + "Layers": [ + "sha256:c8be1b8f4d60d99c281fc2db75e0f56df42a83ad2f0b091621ce19357e19d853", + "sha256:977183d4e9995d9cd5ffdfc0f29e911ec9de777bcb0f507895daa1068477f76f", + "sha256:6597da2e2e52f4d438ad49a14ca79324f130a9ea08745505aa174a8db51cb79d", + "sha256:16542a8fc3be1bfaff6ed1daa7922e7c3b47b6c3a8d98b7fca58b9517bb99b75", + "sha256:2df36adfe1af661aebb75a0db796b074bb8f861fbc8f98f6f642570692b3b133", + "sha256:f499c7d34e01d860492ef1cc34b7d7e1319b3c3c81ee7d23258b21605b5902ca", + "sha256:c4bf1d4e5d4adb566b173a0769d247f67c5dd8ff90dfdcebd8c7060f1c06caa9", + "sha256:15259abd479904cbe0d8d421e5b05b2e5745e2bf82e62cdd7fb6d3eafbe4168a", + "sha256:6aa3691a73805f608e5fce69fb6bc89aec8362f58a6b4be2682515e9cfa3cc1a", + "sha256:2d6ad1b66f5660dd860c1fe2d90d26398fcfab4dc1c87c3d5e7c0fc24f8d6fb2", + "sha256:ff29efc56c31eeccc79a33c6e4abd7b1ab3547d95e1cf83974af65a493576c41", + "sha256:b87e68574cc7dccbe974fa760702ef650711036bf144fd9da1f3a2d8f6ac335f", + "sha256:04559213a01cfac69a8d6a6facb58b8681666525c74f605207c40a61a0f4c9b7", + "sha256:467c0082c57b80b48487a9b8429887c0744ddc5b066b3f7678866bde89b78ab2", + "sha256:352a299d6af4773322ed3643d8f98b01aad6f15d838d1852e52a0a3ca56c6efb", + "sha256:486b2abf434bb90cf04bab74f2f8bd2eb488ff90632b56eac4bddcbbf02e8151", + "sha256:3f50d3a0e1a969a9606b59e5295842d731e425108cb349ce6c69a5b30ea1bab9", + "sha256:c10732392b97c121a78a5f20201c2a5e834a2b8677196cdd49260a489a54fd22", + "sha256:c185540c10fea822c6db1b987fcfe22b55a4662648124b98475db4c9dcddb2ab", + "sha256:73b1a8ac1f7fca3d545766ce7fd3c56b40a63724ab78e464d71a29da0c6ac31c", + "sha256:da62dec6eb4ed884952a1b867fd89e3bfe3c510e5c849cc0ac7050ff867a2469", + "sha256:76fe727e4aafc7f56f01282296ab736521c38b9d19c1ae5ebb193f9cd55fa109", + "sha256:a9c9bbbd69c212b7ab3c1a7f03011ccc4d99a6fce1bf1c785325c7bcad789e62", + "sha256:b7b78159dfdaa0dd484c58652e02fa6b755abfd0adb88f106d16178144e46f33", + "sha256:aa0effdf787ecfe74d60d6771006717fd1a9ce1ce0a8161624baa61b68120357", + "sha256:a0a2f7c467efbb8b1ac222f09013b88b68f3c117ec6b6e9dc95564be50f271ab", + "sha256:a0715e661e13d7d3ded5bdc068edd01e5b3aa0e2805152f4c8a1428b4e0673df", + "sha256:6aae3a2d671d369eec34dc9146ef267d06c87461f271fbfbe9136775ecf5dfb8", + "sha256:cb21f14e306d94e437c5418d275bcc6efcea6bc9b3d26a400bdf54fa62242c24", + "sha256:c9da8171f5ca048109ffba5e940e3a7d2db567eda281f92b0eb483173df06add", + "sha256:11486cb955594f9d43909b60f94209bb6854f502a5a093207b657afbaa38a777", + "sha256:243bbd007cb0ee99b704bfe0cf62e1301baa4095ab4c39b01293787a0e4234f1", + "sha256:6aefa0ba7ce01584b4a531b18e36470298cee3b30ecae0e0c64b532a5cebd6e7", + "sha256:a06615b5adc1a3afb7abd524e82f6900a28910927fcf0d4e9b85fd1fcbeb53ad", + "sha256:26d6f1e76275d17860005f7ab9b74fdd2283fcf84e0446bd88d49a6b4e9609f9", + "sha256:55f7c052cf70c8ca01b8e241c0c5c8a9675599d4904c69bfb961a472e246238d", + "sha256:d9958b816a9ad179fca8c18d17c07e9814b152d461c685e1443bec6f990ab990", + "sha256:52142799a4b687fe6e5cf397c41064499ea6cc554b94904d46c1acade998e11f", + "sha256:48063dcdd043f9c88604d10fe9542569be8f8111d46806c96b08d77763ffa347", + "sha256:70cf83155575fdb607f23ace41e31b1d5cb1c24dbbbf56f71c383b583724d339", + "sha256:6cf0f8f815d5371cf5c04e7ebf76c62467948d693b8343184d1446036980d261", + "sha256:7cbffcbb09fc5e9d00372e80990016609c09cc3113429ddc951c4a19b1a5ec72", + "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + ] + }, + "Metadata": { + "LastTagTime": "0001-01-01T00:00:00Z" + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/image-with-run-image-different-registry.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/image-with-run-image-different-registry.json new file mode 100644 index 000000000000..71d6951ec3db --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/image-with-run-image-different-registry.json @@ -0,0 +1,132 @@ +{ + "Id": "sha256:44cc64492fb6a6d78d3e6d087f380ae6e479aa1b2c79823b32cdacfcc2f3d715", + "RepoTags": [ + "paketo-buildpacks/cnb:base", + "paketo-buildpacks/builder:base-platform-api-0.2" + ], + "RepoDigests": [ + "paketo-buidpacks/cnb@sha256:5b03a853e636b78c44e475bbc514e2b7b140cc41cca8ab907e9753431ae8c0b0" + ], + "Parent": "", + "Comment": "", + "Created": "1980-01-01T00:00:01Z", + "Container": "", + "ContainerConfig": { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": null, + "Image": "", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": null + }, + "DockerVersion": "", + "Author": "", + "Config": { + "Hostname": "", + "Domainname": "", + "User": "1000:1000", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "CNB_USER_ID=1000", + "CNB_GROUP_ID=1000", + "CNB_STACK_ID=io.buildpacks.stacks.bionic" + ], + "Cmd": [ + "/bin/bash" + ], + "ArgsEscaped": true, + "Image": "sha256:2d153261a5e359c632a17377cfb5d1986c27b96c8b6e95334bf80f1029dbd4bb", + "Volumes": null, + "WorkingDir": "/layers", + "Entrypoint": null, + "OnBuild": null, + "Labels": { + "io.buildpacks.builder.metadata": "{\"description\":\"Ubuntu bionic base image with buildpacks for Java, NodeJS and Golang\",\"buildpacks\":[{\"id\":\"org.cloudfoundry.googlestackdriver\",\"version\":\"v1.1.11\"},{\"id\":\"org.cloudfoundry.springboot\",\"version\":\"v1.2.13\"},{\"id\":\"org.cloudfoundry.debug\",\"version\":\"v1.2.11\"},{\"id\":\"org.cloudfoundry.tomcat\",\"version\":\"v1.3.18\"},{\"id\":\"org.cloudfoundry.go\",\"version\":\"v0.0.4\"},{\"id\":\"org.cloudfoundry.openjdk\",\"version\":\"v1.2.14\"},{\"id\":\"org.cloudfoundry.buildsystem\",\"version\":\"v1.2.15\"},{\"id\":\"org.cloudfoundry.jvmapplication\",\"version\":\"v1.1.12\"},{\"id\":\"org.cloudfoundry.springautoreconfiguration\",\"version\":\"v1.1.11\"},{\"id\":\"org.cloudfoundry.archiveexpanding\",\"version\":\"v1.0.102\"},{\"id\":\"org.cloudfoundry.jmx\",\"version\":\"v1.1.12\"},{\"id\":\"org.cloudfoundry.nodejs\",\"version\":\"v2.0.8\"},{\"id\":\"org.cloudfoundry.jdbc\",\"version\":\"v1.1.14\"},{\"id\":\"org.cloudfoundry.procfile\",\"version\":\"v1.1.12\"},{\"id\":\"org.cloudfoundry.dotnet-core\",\"version\":\"v0.0.6\"},{\"id\":\"org.cloudfoundry.azureapplicationinsights\",\"version\":\"v1.1.12\"},{\"id\":\"org.cloudfoundry.distzip\",\"version\":\"v1.1.12\"},{\"id\":\"org.cloudfoundry.dep\",\"version\":\"0.0.101\"},{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.105\"},{\"id\":\"org.cloudfoundry.go-mod\",\"version\":\"0.0.89\"},{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.163\"},{\"id\":\"org.cloudfoundry.npm\",\"version\":\"0.1.3\"},{\"id\":\"org.cloudfoundry.yarn-install\",\"version\":\"0.1.10\"},{\"id\":\"org.cloudfoundry.dotnet-core-aspnet\",\"version\":\"0.0.118\"},{\"id\":\"org.cloudfoundry.dotnet-core-build\",\"version\":\"0.0.68\"},{\"id\":\"org.cloudfoundry.dotnet-core-conf\",\"version\":\"0.0.115\"},{\"id\":\"org.cloudfoundry.dotnet-core-runtime\",\"version\":\"0.0.127\"},{\"id\":\"org.cloudfoundry.dotnet-core-sdk\",\"version\":\"0.0.122\"},{\"id\":\"org.cloudfoundry.icu\",\"version\":\"0.0.43\"},{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.158\"}],\"stack\":{\"runImage\":{\"image\":\"example.com/custom/run:latest\",\"mirrors\":null}},\"lifecycle\":{\"version\":\"0.7.2\",\"api\":{\"buildpack\":\"0.2\",\"platform\":\"0.3\"}},\"createdBy\":{\"name\":\"Pack CLI\",\"version\":\"v0.9.0 (git sha: d42c384a39f367588f2653f2a99702db910e5ad7)\"}}", + "io.buildpacks.buildpack.layers": "{\"org.cloudfoundry.archiveexpanding\":{\"v1.0.102\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:73b1a8ac1f7fca3d545766ce7fd3c56b40a63724ab78e464d71a29da0c6ac31c\"}},\"org.cloudfoundry.azureapplicationinsights\":{\"v1.1.12\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:a0a2f7c467efbb8b1ac222f09013b88b68f3c117ec6b6e9dc95564be50f271ab\"}},\"org.cloudfoundry.buildsystem\":{\"v1.2.15\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:3f50d3a0e1a969a9606b59e5295842d731e425108cb349ce6c69a5b30ea1bab9\"}},\"org.cloudfoundry.debug\":{\"v1.2.11\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:04559213a01cfac69a8d6a6facb58b8681666525c74f605207c40a61a0f4c9b7\"}},\"org.cloudfoundry.dep\":{\"0.0.101\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.tiny\"}],\"layerDiffID\":\"sha256:6aae3a2d671d369eec34dc9146ef267d06c87461f271fbfbe9136775ecf5dfb8\"}},\"org.cloudfoundry.distzip\":{\"v1.1.12\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:a0715e661e13d7d3ded5bdc068edd01e5b3aa0e2805152f4c8a1428b4e0673df\"}},\"org.cloudfoundry.dotnet-core\":{\"v0.0.6\":{\"api\":\"0.2\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.158\",\"optional\":true},{\"id\":\"org.cloudfoundry.icu\",\"version\":\"0.0.43\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-runtime\",\"version\":\"0.0.127\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-aspnet\",\"version\":\"0.0.118\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-sdk\",\"version\":\"0.0.122\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-build\",\"version\":\"0.0.68\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-conf\",\"version\":\"0.0.115\"}]}],\"layerDiffID\":\"sha256:aa0effdf787ecfe74d60d6771006717fd1a9ce1ce0a8161624baa61b68120357\"}},\"org.cloudfoundry.dotnet-core-aspnet\":{\"0.0.118\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:a06615b5adc1a3afb7abd524e82f6900a28910927fcf0d4e9b85fd1fcbeb53ad\"}},\"org.cloudfoundry.dotnet-core-build\":{\"0.0.68\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:26d6f1e76275d17860005f7ab9b74fdd2283fcf84e0446bd88d49a6b4e9609f9\"}},\"org.cloudfoundry.dotnet-core-conf\":{\"0.0.115\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:55f7c052cf70c8ca01b8e241c0c5c8a9675599d4904c69bfb961a472e246238d\"}},\"org.cloudfoundry.dotnet-core-runtime\":{\"0.0.127\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:d9958b816a9ad179fca8c18d17c07e9814b152d461c685e1443bec6f990ab990\"}},\"org.cloudfoundry.dotnet-core-sdk\":{\"0.0.122\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:52142799a4b687fe6e5cf397c41064499ea6cc554b94904d46c1acade998e11f\"}},\"org.cloudfoundry.go\":{\"v0.0.4\":{\"api\":\"0.2\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.105\"},{\"id\":\"org.cloudfoundry.go-mod\",\"version\":\"0.0.89\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.105\"},{\"id\":\"org.cloudfoundry.dep\",\"version\":\"0.0.101\"}]}],\"layerDiffID\":\"sha256:352a299d6af4773322ed3643d8f98b01aad6f15d838d1852e52a0a3ca56c6efb\"}},\"org.cloudfoundry.go-compiler\":{\"0.0.105\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.tiny\"}],\"layerDiffID\":\"sha256:cb21f14e306d94e437c5418d275bcc6efcea6bc9b3d26a400bdf54fa62242c24\"}},\"org.cloudfoundry.go-mod\":{\"0.0.89\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.tiny\"}],\"layerDiffID\":\"sha256:c9da8171f5ca048109ffba5e940e3a7d2db567eda281f92b0eb483173df06add\"}},\"org.cloudfoundry.googlestackdriver\":{\"v1.1.11\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:ff29efc56c31eeccc79a33c6e4abd7b1ab3547d95e1cf83974af65a493576c41\"}},\"org.cloudfoundry.icu\":{\"0.0.43\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:48063dcdd043f9c88604d10fe9542569be8f8111d46806c96b08d77763ffa347\"}},\"org.cloudfoundry.jdbc\":{\"v1.1.14\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:a9c9bbbd69c212b7ab3c1a7f03011ccc4d99a6fce1bf1c785325c7bcad789e62\"}},\"org.cloudfoundry.jmx\":{\"v1.1.12\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:da62dec6eb4ed884952a1b867fd89e3bfe3c510e5c849cc0ac7050ff867a2469\"}},\"org.cloudfoundry.jvmapplication\":{\"v1.1.12\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:c10732392b97c121a78a5f20201c2a5e834a2b8677196cdd49260a489a54fd22\"}},\"org.cloudfoundry.node-engine\":{\"0.0.158\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:70cf83155575fdb607f23ace41e31b1d5cb1c24dbbbf56f71c383b583724d339\"},\"0.0.163\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:11486cb955594f9d43909b60f94209bb6854f502a5a093207b657afbaa38a777\"}},\"org.cloudfoundry.nodejs\":{\"v2.0.8\":{\"api\":\"0.2\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.163\"},{\"id\":\"org.cloudfoundry.yarn-install\",\"version\":\"0.1.10\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.163\"},{\"id\":\"org.cloudfoundry.npm\",\"version\":\"0.1.3\"}]}],\"layerDiffID\":\"sha256:76fe727e4aafc7f56f01282296ab736521c38b9d19c1ae5ebb193f9cd55fa109\"}},\"org.cloudfoundry.npm\":{\"0.1.3\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:243bbd007cb0ee99b704bfe0cf62e1301baa4095ab4c39b01293787a0e4234f1\"}},\"org.cloudfoundry.openjdk\":{\"v1.2.14\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:486b2abf434bb90cf04bab74f2f8bd2eb488ff90632b56eac4bddcbbf02e8151\"}},\"org.cloudfoundry.procfile\":{\"v1.1.12\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:b7b78159dfdaa0dd484c58652e02fa6b755abfd0adb88f106d16178144e46f33\"}},\"org.cloudfoundry.springautoreconfiguration\":{\"v1.1.11\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:c185540c10fea822c6db1b987fcfe22b55a4662648124b98475db4c9dcddb2ab\"}},\"org.cloudfoundry.springboot\":{\"v1.2.13\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:b87e68574cc7dccbe974fa760702ef650711036bf144fd9da1f3a2d8f6ac335f\"}},\"org.cloudfoundry.tomcat\":{\"v1.3.18\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:467c0082c57b80b48487a9b8429887c0744ddc5b066b3f7678866bde89b78ab2\"}},\"org.cloudfoundry.yarn-install\":{\"0.1.10\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:6aefa0ba7ce01584b4a531b18e36470298cee3b30ecae0e0c64b532a5cebd6e7\"}}}", + "io.buildpacks.buildpack.order": "[{\"group\":[{\"id\":\"org.cloudfoundry.openjdk\"},{\"id\":\"org.cloudfoundry.buildsystem\",\"optional\":true},{\"id\":\"org.cloudfoundry.jvmapplication\"},{\"id\":\"org.cloudfoundry.tomcat\",\"optional\":true},{\"id\":\"org.cloudfoundry.springboot\",\"optional\":true},{\"id\":\"org.cloudfoundry.distzip\",\"optional\":true},{\"id\":\"org.cloudfoundry.procfile\",\"optional\":true},{\"id\":\"org.cloudfoundry.azureapplicationinsights\",\"optional\":true},{\"id\":\"org.cloudfoundry.debug\",\"optional\":true},{\"id\":\"org.cloudfoundry.googlestackdriver\",\"optional\":true},{\"id\":\"org.cloudfoundry.jdbc\",\"optional\":true},{\"id\":\"org.cloudfoundry.jmx\",\"optional\":true},{\"id\":\"org.cloudfoundry.springautoreconfiguration\",\"optional\":true}]},{\"group\":[{\"id\":\"org.cloudfoundry.nodejs\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.go\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.dotnet-core\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.procfile\"}]}]", + "io.buildpacks.stack.id": "io.buildpacks.stacks.bionic", + "io.buildpacks.stack.mixins": "[\"build:git\",\"build:build-essential\"]" + } + }, + "Architecture": "amd64", + "Os": "linux", + "Size": 688884758, + "VirtualSize": 688884758, + "GraphDriver": { + "Data": { + "LowerDir": "/var/lib/docker/overlay2/6a79181b2840da2706624f46ce5abd4448973b4f951925d5a276b273256063b2/diff:/var/lib/docker/overlay2/429419a203100f60ab16ec6c879fce975c8138422b9053f80accd6124c730fc2/diff:/var/lib/docker/overlay2/6e45ed6daf4f4f3b90fd1ec5fa958775000875661d3e8be3f1af218d192b058d/diff:/var/lib/docker/overlay2/22928ad308cdd55b3fe849d92b6e38c6bc303ba7c9beb8c0e79aa958e16b1864/diff:/var/lib/docker/overlay2/2ca9ec213226a1604f57c8e141d6f1168134a5cb2ccd8f91ee9be5a39036e6bf/diff:/var/lib/docker/overlay2/96ae944fe00ec20cf5b4441b112ebcc9395faaf08108c9ee38c62e1da33af1c8/diff:/var/lib/docker/overlay2/13ee52e300e476e27350c9ac6274dedf26af85c3079b42a41f9dfc92eff57a80/diff:/var/lib/docker/overlay2/223edb4cc62a2ba2b8bda866905a55c4798c6c32e31d22d60e6ed4f3169ce85e/diff:/var/lib/docker/overlay2/a41235cd7277299cb74ead47def3771885948719e24075ea3bf37580f3af7ae2/diff:/var/lib/docker/overlay2/ed0438e8e2c27b9d62ad21a0761237c350a2ffc9e52f47c019e4f627091c832e/diff:/var/lib/docker/overlay2/0c27c8229b31eafc57ab739b44962dcc07b72f3d8950888873ecb3cfd385032f/diff:/var/lib/docker/overlay2/0957cbcca052cd58bcf9a3d945b0e6876b0df79c1c534da1872c3415a019427d/diff:/var/lib/docker/overlay2/b621414d53d71349c07df8ed45e3e04b2e97bfbaf4bf0d86463f46e0f810eeb4/diff:/var/lib/docker/overlay2/ad521bc47f0bb44262358cf47c3d81a544d098494cf24a5b510620d34eb9c353/diff:/var/lib/docker/overlay2/081501d5bfbd927e69c10eb320513c7c0d5f00bea8cf9e55faa90579fd33adf4/diff:/var/lib/docker/overlay2/fb1ba66bee5568f5700c72865d020d4171a62bfdd099c3cc05b9a253d36a35a4/diff:/var/lib/docker/overlay2/06bcc6b3adeca727d554f1a745ee33242dfe1b3c6392023ac947666057303288/diff:/var/lib/docker/overlay2/1c5397d63d893202dffde29013ee826fb695bda26c718ee03ddde376be4da0a3/diff:/var/lib/docker/overlay2/76075fb7fd3c6b3fb116fb3b464e220918e56d94461c61af9a1aff288ebdba60/diff:/var/lib/docker/overlay2/43d1026bb7b618393912ecc9ddf57b604336184d5f8dc70bcf6332b5f08a3e8d/diff:/var/lib/docker/overlay2/ee27d1fba3deaca0556f7bab171cb3368f169011dd132cf335b5308728f6db8f/diff:/var/lib/docker/overlay2/464d3ec8d86ff31dcb5063ea25521368ea8e9c7964f65e15ff5e0e1ecdbe991e/diff:/var/lib/docker/overlay2/a4a80c33c8b78f68bdc9dbd5903cc2ba1d48e78b9a97d43acb018823ece8e6cb/diff:/var/lib/docker/overlay2/6494f2f1693cff8b16d51fa95620eb0bb691a76fb39b5175d953649577791297/diff:/var/lib/docker/overlay2/9d49e146f82eb5fc4fd81613538e9c5f5f95091fbbc8c49729c6c9140ae356de/diff:/var/lib/docker/overlay2/2934818c52bcd017abe000e71342d67fbc9ccb7dbc165ce05e3250e2110229a5/diff:/var/lib/docker/overlay2/651ca06b2bf75e2122855264287fc937f30d2b49229d628909895be7128b4eb6/diff:/var/lib/docker/overlay2/c93bab59be44fa1b66689dc059d26742d00d2e787d06c3236e1f116199c9807e/diff:/var/lib/docker/overlay2/d0a8e2a0c7e0df172f7a8ebe75e2dce371bb6cc65531b06799bc677c5b5e3627/diff:/var/lib/docker/overlay2/7d14bac240e0d7936351e3fac80b7fbe2a209f4de8992091c4f75e41f9627852/diff:/var/lib/docker/overlay2/d6b192ea137a4ae95e309d263ee8c890e35da02aacd9bdcf5adbd4c28a0c0a3f/diff:/var/lib/docker/overlay2/335bfb632ab7723e25fb5dc7b67389e6ec38178ef10bfbf83337501403e61574/diff:/var/lib/docker/overlay2/0293c7e3472da58f51cbdf15fb293ff71e32c1f80f83f00fb09f8941deef5e43/diff:/var/lib/docker/overlay2/55faa8b47bcb0dd29c3836580f451a0461dd499065af9c830beff6e8329ab484/diff:/var/lib/docker/overlay2/afcb6e109c1ba7d71b8a8b7e573d4ce04f22da3fe0ee523359db5cfb95e65bb6/diff:/var/lib/docker/overlay2/b42eefd9bf6629ae9d16e7aba6ba3939d37816aba7a0999f6d639012a3119be1/diff:/var/lib/docker/overlay2/a9832c8f81ee889a622ce4d95d9f4bab2f91d30e18f69bfd7cfc385c781068d4/diff:/var/lib/docker/overlay2/224041c135f13881a98b9e833584bedab81d5650061457f522a1ebd1daa2c77a/diff:/var/lib/docker/overlay2/73dfd4e2075fccb239b3d5e9b33b32b8e410bdc3cd5a620b41346f44cc5c51f7/diff:/var/lib/docker/overlay2/b3924ed7c91730f6714d33c455db888604b59ab093033b3f59ac16ecdd777987/diff:/var/lib/docker/overlay2/e36a32cd0ab20b216a8db1a8a166b17464399e4d587d22504088a7a6ef0a68a4/diff:/var/lib/docker/overlay2/3334e94fe191333b65f571912c0fcfbbf31aeb090a2fb9b4cfdbc32a37c0fe5f/diff", + "MergedDir": "/var/lib/docker/overlay2/f5d133c5929da8cc8266cbbc3e36f924f4a9c835f943fb436445a26b7e1bcc56/merged", + "UpperDir": "/var/lib/docker/overlay2/f5d133c5929da8cc8266cbbc3e36f924f4a9c835f943fb436445a26b7e1bcc56/diff", + "WorkDir": "/var/lib/docker/overlay2/f5d133c5929da8cc8266cbbc3e36f924f4a9c835f943fb436445a26b7e1bcc56/work" + }, + "Name": "overlay2" + }, + "RootFS": { + "Type": "layers", + "Layers": [ + "sha256:c8be1b8f4d60d99c281fc2db75e0f56df42a83ad2f0b091621ce19357e19d853", + "sha256:977183d4e9995d9cd5ffdfc0f29e911ec9de777bcb0f507895daa1068477f76f", + "sha256:6597da2e2e52f4d438ad49a14ca79324f130a9ea08745505aa174a8db51cb79d", + "sha256:16542a8fc3be1bfaff6ed1daa7922e7c3b47b6c3a8d98b7fca58b9517bb99b75", + "sha256:2df36adfe1af661aebb75a0db796b074bb8f861fbc8f98f6f642570692b3b133", + "sha256:f499c7d34e01d860492ef1cc34b7d7e1319b3c3c81ee7d23258b21605b5902ca", + "sha256:c4bf1d4e5d4adb566b173a0769d247f67c5dd8ff90dfdcebd8c7060f1c06caa9", + "sha256:15259abd479904cbe0d8d421e5b05b2e5745e2bf82e62cdd7fb6d3eafbe4168a", + "sha256:6aa3691a73805f608e5fce69fb6bc89aec8362f58a6b4be2682515e9cfa3cc1a", + "sha256:2d6ad1b66f5660dd860c1fe2d90d26398fcfab4dc1c87c3d5e7c0fc24f8d6fb2", + "sha256:ff29efc56c31eeccc79a33c6e4abd7b1ab3547d95e1cf83974af65a493576c41", + "sha256:b87e68574cc7dccbe974fa760702ef650711036bf144fd9da1f3a2d8f6ac335f", + "sha256:04559213a01cfac69a8d6a6facb58b8681666525c74f605207c40a61a0f4c9b7", + "sha256:467c0082c57b80b48487a9b8429887c0744ddc5b066b3f7678866bde89b78ab2", + "sha256:352a299d6af4773322ed3643d8f98b01aad6f15d838d1852e52a0a3ca56c6efb", + "sha256:486b2abf434bb90cf04bab74f2f8bd2eb488ff90632b56eac4bddcbbf02e8151", + "sha256:3f50d3a0e1a969a9606b59e5295842d731e425108cb349ce6c69a5b30ea1bab9", + "sha256:c10732392b97c121a78a5f20201c2a5e834a2b8677196cdd49260a489a54fd22", + "sha256:c185540c10fea822c6db1b987fcfe22b55a4662648124b98475db4c9dcddb2ab", + "sha256:73b1a8ac1f7fca3d545766ce7fd3c56b40a63724ab78e464d71a29da0c6ac31c", + "sha256:da62dec6eb4ed884952a1b867fd89e3bfe3c510e5c849cc0ac7050ff867a2469", + "sha256:76fe727e4aafc7f56f01282296ab736521c38b9d19c1ae5ebb193f9cd55fa109", + "sha256:a9c9bbbd69c212b7ab3c1a7f03011ccc4d99a6fce1bf1c785325c7bcad789e62", + "sha256:b7b78159dfdaa0dd484c58652e02fa6b755abfd0adb88f106d16178144e46f33", + "sha256:aa0effdf787ecfe74d60d6771006717fd1a9ce1ce0a8161624baa61b68120357", + "sha256:a0a2f7c467efbb8b1ac222f09013b88b68f3c117ec6b6e9dc95564be50f271ab", + "sha256:a0715e661e13d7d3ded5bdc068edd01e5b3aa0e2805152f4c8a1428b4e0673df", + "sha256:6aae3a2d671d369eec34dc9146ef267d06c87461f271fbfbe9136775ecf5dfb8", + "sha256:cb21f14e306d94e437c5418d275bcc6efcea6bc9b3d26a400bdf54fa62242c24", + "sha256:c9da8171f5ca048109ffba5e940e3a7d2db567eda281f92b0eb483173df06add", + "sha256:11486cb955594f9d43909b60f94209bb6854f502a5a093207b657afbaa38a777", + "sha256:243bbd007cb0ee99b704bfe0cf62e1301baa4095ab4c39b01293787a0e4234f1", + "sha256:6aefa0ba7ce01584b4a531b18e36470298cee3b30ecae0e0c64b532a5cebd6e7", + "sha256:a06615b5adc1a3afb7abd524e82f6900a28910927fcf0d4e9b85fd1fcbeb53ad", + "sha256:26d6f1e76275d17860005f7ab9b74fdd2283fcf84e0446bd88d49a6b4e9609f9", + "sha256:55f7c052cf70c8ca01b8e241c0c5c8a9675599d4904c69bfb961a472e246238d", + "sha256:d9958b816a9ad179fca8c18d17c07e9814b152d461c685e1443bec6f990ab990", + "sha256:52142799a4b687fe6e5cf397c41064499ea6cc554b94904d46c1acade998e11f", + "sha256:48063dcdd043f9c88604d10fe9542569be8f8111d46806c96b08d77763ffa347", + "sha256:70cf83155575fdb607f23ace41e31b1d5cb1c24dbbbf56f71c383b583724d339", + "sha256:6cf0f8f815d5371cf5c04e7ebf76c62467948d693b8343184d1446036980d261", + "sha256:7cbffcbb09fc5e9d00372e80990016609c09cc3113429ddc951c4a19b1a5ec72", + "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + ] + }, + "Metadata": { + "LastTagTime": "0001-01-01T00:00:00Z" + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/image-with-run-image-digest.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/image-with-run-image-digest.json new file mode 100644 index 000000000000..d31e02e3d9b4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/image-with-run-image-digest.json @@ -0,0 +1,132 @@ +{ + "Id": "sha256:44cc64492fb6a6d78d3e6d087f380ae6e479aa1b2c79823b32cdacfcc2f3d715", + "RepoTags": [ + "paketo-buildpacks/cnb:base", + "paketo-buildpacks/builder:base-platform-api-0.2" + ], + "RepoDigests": [ + "paketo-buidpacks/cnb@sha256:5b03a853e636b78c44e475bbc514e2b7b140cc41cca8ab907e9753431ae8c0b0" + ], + "Parent": "", + "Comment": "", + "Created": "1980-01-01T00:00:01Z", + "Container": "", + "ContainerConfig": { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": null, + "Image": "", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": null + }, + "DockerVersion": "", + "Author": "", + "Config": { + "Hostname": "", + "Domainname": "", + "User": "1000:1000", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "CNB_USER_ID=1000", + "CNB_GROUP_ID=1000", + "CNB_STACK_ID=io.buildpacks.stacks.bionic" + ], + "Cmd": [ + "/bin/bash" + ], + "ArgsEscaped": true, + "Image": "sha256:2d153261a5e359c632a17377cfb5d1986c27b96c8b6e95334bf80f1029dbd4bb", + "Volumes": null, + "WorkingDir": "/layers", + "Entrypoint": null, + "OnBuild": null, + "Labels": { + "io.buildpacks.builder.metadata": "{\"description\":\"Ubuntu bionic base image with buildpacks for Java, NodeJS and Golang\",\"buildpacks\":[{\"id\":\"org.cloudfoundry.googlestackdriver\",\"version\":\"v1.1.11\"},{\"id\":\"org.cloudfoundry.springboot\",\"version\":\"v1.2.13\"},{\"id\":\"org.cloudfoundry.debug\",\"version\":\"v1.2.11\"},{\"id\":\"org.cloudfoundry.tomcat\",\"version\":\"v1.3.18\"},{\"id\":\"org.cloudfoundry.go\",\"version\":\"v0.0.4\"},{\"id\":\"org.cloudfoundry.openjdk\",\"version\":\"v1.2.14\"},{\"id\":\"org.cloudfoundry.buildsystem\",\"version\":\"v1.2.15\"},{\"id\":\"org.cloudfoundry.jvmapplication\",\"version\":\"v1.1.12\"},{\"id\":\"org.cloudfoundry.springautoreconfiguration\",\"version\":\"v1.1.11\"},{\"id\":\"org.cloudfoundry.archiveexpanding\",\"version\":\"v1.0.102\"},{\"id\":\"org.cloudfoundry.jmx\",\"version\":\"v1.1.12\"},{\"id\":\"org.cloudfoundry.nodejs\",\"version\":\"v2.0.8\"},{\"id\":\"org.cloudfoundry.jdbc\",\"version\":\"v1.1.14\"},{\"id\":\"org.cloudfoundry.procfile\",\"version\":\"v1.1.12\"},{\"id\":\"org.cloudfoundry.dotnet-core\",\"version\":\"v0.0.6\"},{\"id\":\"org.cloudfoundry.azureapplicationinsights\",\"version\":\"v1.1.12\"},{\"id\":\"org.cloudfoundry.distzip\",\"version\":\"v1.1.12\"},{\"id\":\"org.cloudfoundry.dep\",\"version\":\"0.0.101\"},{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.105\"},{\"id\":\"org.cloudfoundry.go-mod\",\"version\":\"0.0.89\"},{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.163\"},{\"id\":\"org.cloudfoundry.npm\",\"version\":\"0.1.3\"},{\"id\":\"org.cloudfoundry.yarn-install\",\"version\":\"0.1.10\"},{\"id\":\"org.cloudfoundry.dotnet-core-aspnet\",\"version\":\"0.0.118\"},{\"id\":\"org.cloudfoundry.dotnet-core-build\",\"version\":\"0.0.68\"},{\"id\":\"org.cloudfoundry.dotnet-core-conf\",\"version\":\"0.0.115\"},{\"id\":\"org.cloudfoundry.dotnet-core-runtime\",\"version\":\"0.0.127\"},{\"id\":\"org.cloudfoundry.dotnet-core-sdk\",\"version\":\"0.0.122\"},{\"id\":\"org.cloudfoundry.icu\",\"version\":\"0.0.43\"},{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.158\"}],\"stack\":{\"runImage\":{\"image\":\"cloudfoundry/run@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d\",\"mirrors\":null}},\"lifecycle\":{\"version\":\"0.7.2\",\"api\":{\"buildpack\":\"0.2\",\"platform\":\"0.3\"}},\"createdBy\":{\"name\":\"Pack CLI\",\"version\":\"v0.9.0 (git sha: d42c384a39f367588f2653f2a99702db910e5ad7)\"}}", + "io.buildpacks.buildpack.layers": "{\"org.cloudfoundry.archiveexpanding\":{\"v1.0.102\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:73b1a8ac1f7fca3d545766ce7fd3c56b40a63724ab78e464d71a29da0c6ac31c\"}},\"org.cloudfoundry.azureapplicationinsights\":{\"v1.1.12\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:a0a2f7c467efbb8b1ac222f09013b88b68f3c117ec6b6e9dc95564be50f271ab\"}},\"org.cloudfoundry.buildsystem\":{\"v1.2.15\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:3f50d3a0e1a969a9606b59e5295842d731e425108cb349ce6c69a5b30ea1bab9\"}},\"org.cloudfoundry.debug\":{\"v1.2.11\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:04559213a01cfac69a8d6a6facb58b8681666525c74f605207c40a61a0f4c9b7\"}},\"org.cloudfoundry.dep\":{\"0.0.101\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.tiny\"}],\"layerDiffID\":\"sha256:6aae3a2d671d369eec34dc9146ef267d06c87461f271fbfbe9136775ecf5dfb8\"}},\"org.cloudfoundry.distzip\":{\"v1.1.12\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:a0715e661e13d7d3ded5bdc068edd01e5b3aa0e2805152f4c8a1428b4e0673df\"}},\"org.cloudfoundry.dotnet-core\":{\"v0.0.6\":{\"api\":\"0.2\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.158\",\"optional\":true},{\"id\":\"org.cloudfoundry.icu\",\"version\":\"0.0.43\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-runtime\",\"version\":\"0.0.127\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-aspnet\",\"version\":\"0.0.118\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-sdk\",\"version\":\"0.0.122\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-build\",\"version\":\"0.0.68\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-conf\",\"version\":\"0.0.115\"}]}],\"layerDiffID\":\"sha256:aa0effdf787ecfe74d60d6771006717fd1a9ce1ce0a8161624baa61b68120357\"}},\"org.cloudfoundry.dotnet-core-aspnet\":{\"0.0.118\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:a06615b5adc1a3afb7abd524e82f6900a28910927fcf0d4e9b85fd1fcbeb53ad\"}},\"org.cloudfoundry.dotnet-core-build\":{\"0.0.68\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:26d6f1e76275d17860005f7ab9b74fdd2283fcf84e0446bd88d49a6b4e9609f9\"}},\"org.cloudfoundry.dotnet-core-conf\":{\"0.0.115\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:55f7c052cf70c8ca01b8e241c0c5c8a9675599d4904c69bfb961a472e246238d\"}},\"org.cloudfoundry.dotnet-core-runtime\":{\"0.0.127\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:d9958b816a9ad179fca8c18d17c07e9814b152d461c685e1443bec6f990ab990\"}},\"org.cloudfoundry.dotnet-core-sdk\":{\"0.0.122\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:52142799a4b687fe6e5cf397c41064499ea6cc554b94904d46c1acade998e11f\"}},\"org.cloudfoundry.go\":{\"v0.0.4\":{\"api\":\"0.2\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.105\"},{\"id\":\"org.cloudfoundry.go-mod\",\"version\":\"0.0.89\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.105\"},{\"id\":\"org.cloudfoundry.dep\",\"version\":\"0.0.101\"}]}],\"layerDiffID\":\"sha256:352a299d6af4773322ed3643d8f98b01aad6f15d838d1852e52a0a3ca56c6efb\"}},\"org.cloudfoundry.go-compiler\":{\"0.0.105\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.tiny\"}],\"layerDiffID\":\"sha256:cb21f14e306d94e437c5418d275bcc6efcea6bc9b3d26a400bdf54fa62242c24\"}},\"org.cloudfoundry.go-mod\":{\"0.0.89\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.tiny\"}],\"layerDiffID\":\"sha256:c9da8171f5ca048109ffba5e940e3a7d2db567eda281f92b0eb483173df06add\"}},\"org.cloudfoundry.googlestackdriver\":{\"v1.1.11\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:ff29efc56c31eeccc79a33c6e4abd7b1ab3547d95e1cf83974af65a493576c41\"}},\"org.cloudfoundry.icu\":{\"0.0.43\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:48063dcdd043f9c88604d10fe9542569be8f8111d46806c96b08d77763ffa347\"}},\"org.cloudfoundry.jdbc\":{\"v1.1.14\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:a9c9bbbd69c212b7ab3c1a7f03011ccc4d99a6fce1bf1c785325c7bcad789e62\"}},\"org.cloudfoundry.jmx\":{\"v1.1.12\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:da62dec6eb4ed884952a1b867fd89e3bfe3c510e5c849cc0ac7050ff867a2469\"}},\"org.cloudfoundry.jvmapplication\":{\"v1.1.12\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:c10732392b97c121a78a5f20201c2a5e834a2b8677196cdd49260a489a54fd22\"}},\"org.cloudfoundry.node-engine\":{\"0.0.158\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:70cf83155575fdb607f23ace41e31b1d5cb1c24dbbbf56f71c383b583724d339\"},\"0.0.163\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:11486cb955594f9d43909b60f94209bb6854f502a5a093207b657afbaa38a777\"}},\"org.cloudfoundry.nodejs\":{\"v2.0.8\":{\"api\":\"0.2\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.163\"},{\"id\":\"org.cloudfoundry.yarn-install\",\"version\":\"0.1.10\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.163\"},{\"id\":\"org.cloudfoundry.npm\",\"version\":\"0.1.3\"}]}],\"layerDiffID\":\"sha256:76fe727e4aafc7f56f01282296ab736521c38b9d19c1ae5ebb193f9cd55fa109\"}},\"org.cloudfoundry.npm\":{\"0.1.3\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:243bbd007cb0ee99b704bfe0cf62e1301baa4095ab4c39b01293787a0e4234f1\"}},\"org.cloudfoundry.openjdk\":{\"v1.2.14\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:486b2abf434bb90cf04bab74f2f8bd2eb488ff90632b56eac4bddcbbf02e8151\"}},\"org.cloudfoundry.procfile\":{\"v1.1.12\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:b7b78159dfdaa0dd484c58652e02fa6b755abfd0adb88f106d16178144e46f33\"}},\"org.cloudfoundry.springautoreconfiguration\":{\"v1.1.11\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:c185540c10fea822c6db1b987fcfe22b55a4662648124b98475db4c9dcddb2ab\"}},\"org.cloudfoundry.springboot\":{\"v1.2.13\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:b87e68574cc7dccbe974fa760702ef650711036bf144fd9da1f3a2d8f6ac335f\"}},\"org.cloudfoundry.tomcat\":{\"v1.3.18\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:467c0082c57b80b48487a9b8429887c0744ddc5b066b3f7678866bde89b78ab2\"}},\"org.cloudfoundry.yarn-install\":{\"0.1.10\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:6aefa0ba7ce01584b4a531b18e36470298cee3b30ecae0e0c64b532a5cebd6e7\"}}}", + "io.buildpacks.buildpack.order": "[{\"group\":[{\"id\":\"org.cloudfoundry.openjdk\"},{\"id\":\"org.cloudfoundry.buildsystem\",\"optional\":true},{\"id\":\"org.cloudfoundry.jvmapplication\"},{\"id\":\"org.cloudfoundry.tomcat\",\"optional\":true},{\"id\":\"org.cloudfoundry.springboot\",\"optional\":true},{\"id\":\"org.cloudfoundry.distzip\",\"optional\":true},{\"id\":\"org.cloudfoundry.procfile\",\"optional\":true},{\"id\":\"org.cloudfoundry.azureapplicationinsights\",\"optional\":true},{\"id\":\"org.cloudfoundry.debug\",\"optional\":true},{\"id\":\"org.cloudfoundry.googlestackdriver\",\"optional\":true},{\"id\":\"org.cloudfoundry.jdbc\",\"optional\":true},{\"id\":\"org.cloudfoundry.jmx\",\"optional\":true},{\"id\":\"org.cloudfoundry.springautoreconfiguration\",\"optional\":true}]},{\"group\":[{\"id\":\"org.cloudfoundry.nodejs\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.go\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.dotnet-core\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.procfile\"}]}]", + "io.buildpacks.stack.id": "io.buildpacks.stacks.bionic", + "io.buildpacks.stack.mixins": "[\"build:git\",\"build:build-essential\"]" + } + }, + "Architecture": "amd64", + "Os": "linux", + "Size": 688884758, + "VirtualSize": 688884758, + "GraphDriver": { + "Data": { + "LowerDir": "/var/lib/docker/overlay2/6a79181b2840da2706624f46ce5abd4448973b4f951925d5a276b273256063b2/diff:/var/lib/docker/overlay2/429419a203100f60ab16ec6c879fce975c8138422b9053f80accd6124c730fc2/diff:/var/lib/docker/overlay2/6e45ed6daf4f4f3b90fd1ec5fa958775000875661d3e8be3f1af218d192b058d/diff:/var/lib/docker/overlay2/22928ad308cdd55b3fe849d92b6e38c6bc303ba7c9beb8c0e79aa958e16b1864/diff:/var/lib/docker/overlay2/2ca9ec213226a1604f57c8e141d6f1168134a5cb2ccd8f91ee9be5a39036e6bf/diff:/var/lib/docker/overlay2/96ae944fe00ec20cf5b4441b112ebcc9395faaf08108c9ee38c62e1da33af1c8/diff:/var/lib/docker/overlay2/13ee52e300e476e27350c9ac6274dedf26af85c3079b42a41f9dfc92eff57a80/diff:/var/lib/docker/overlay2/223edb4cc62a2ba2b8bda866905a55c4798c6c32e31d22d60e6ed4f3169ce85e/diff:/var/lib/docker/overlay2/a41235cd7277299cb74ead47def3771885948719e24075ea3bf37580f3af7ae2/diff:/var/lib/docker/overlay2/ed0438e8e2c27b9d62ad21a0761237c350a2ffc9e52f47c019e4f627091c832e/diff:/var/lib/docker/overlay2/0c27c8229b31eafc57ab739b44962dcc07b72f3d8950888873ecb3cfd385032f/diff:/var/lib/docker/overlay2/0957cbcca052cd58bcf9a3d945b0e6876b0df79c1c534da1872c3415a019427d/diff:/var/lib/docker/overlay2/b621414d53d71349c07df8ed45e3e04b2e97bfbaf4bf0d86463f46e0f810eeb4/diff:/var/lib/docker/overlay2/ad521bc47f0bb44262358cf47c3d81a544d098494cf24a5b510620d34eb9c353/diff:/var/lib/docker/overlay2/081501d5bfbd927e69c10eb320513c7c0d5f00bea8cf9e55faa90579fd33adf4/diff:/var/lib/docker/overlay2/fb1ba66bee5568f5700c72865d020d4171a62bfdd099c3cc05b9a253d36a35a4/diff:/var/lib/docker/overlay2/06bcc6b3adeca727d554f1a745ee33242dfe1b3c6392023ac947666057303288/diff:/var/lib/docker/overlay2/1c5397d63d893202dffde29013ee826fb695bda26c718ee03ddde376be4da0a3/diff:/var/lib/docker/overlay2/76075fb7fd3c6b3fb116fb3b464e220918e56d94461c61af9a1aff288ebdba60/diff:/var/lib/docker/overlay2/43d1026bb7b618393912ecc9ddf57b604336184d5f8dc70bcf6332b5f08a3e8d/diff:/var/lib/docker/overlay2/ee27d1fba3deaca0556f7bab171cb3368f169011dd132cf335b5308728f6db8f/diff:/var/lib/docker/overlay2/464d3ec8d86ff31dcb5063ea25521368ea8e9c7964f65e15ff5e0e1ecdbe991e/diff:/var/lib/docker/overlay2/a4a80c33c8b78f68bdc9dbd5903cc2ba1d48e78b9a97d43acb018823ece8e6cb/diff:/var/lib/docker/overlay2/6494f2f1693cff8b16d51fa95620eb0bb691a76fb39b5175d953649577791297/diff:/var/lib/docker/overlay2/9d49e146f82eb5fc4fd81613538e9c5f5f95091fbbc8c49729c6c9140ae356de/diff:/var/lib/docker/overlay2/2934818c52bcd017abe000e71342d67fbc9ccb7dbc165ce05e3250e2110229a5/diff:/var/lib/docker/overlay2/651ca06b2bf75e2122855264287fc937f30d2b49229d628909895be7128b4eb6/diff:/var/lib/docker/overlay2/c93bab59be44fa1b66689dc059d26742d00d2e787d06c3236e1f116199c9807e/diff:/var/lib/docker/overlay2/d0a8e2a0c7e0df172f7a8ebe75e2dce371bb6cc65531b06799bc677c5b5e3627/diff:/var/lib/docker/overlay2/7d14bac240e0d7936351e3fac80b7fbe2a209f4de8992091c4f75e41f9627852/diff:/var/lib/docker/overlay2/d6b192ea137a4ae95e309d263ee8c890e35da02aacd9bdcf5adbd4c28a0c0a3f/diff:/var/lib/docker/overlay2/335bfb632ab7723e25fb5dc7b67389e6ec38178ef10bfbf83337501403e61574/diff:/var/lib/docker/overlay2/0293c7e3472da58f51cbdf15fb293ff71e32c1f80f83f00fb09f8941deef5e43/diff:/var/lib/docker/overlay2/55faa8b47bcb0dd29c3836580f451a0461dd499065af9c830beff6e8329ab484/diff:/var/lib/docker/overlay2/afcb6e109c1ba7d71b8a8b7e573d4ce04f22da3fe0ee523359db5cfb95e65bb6/diff:/var/lib/docker/overlay2/b42eefd9bf6629ae9d16e7aba6ba3939d37816aba7a0999f6d639012a3119be1/diff:/var/lib/docker/overlay2/a9832c8f81ee889a622ce4d95d9f4bab2f91d30e18f69bfd7cfc385c781068d4/diff:/var/lib/docker/overlay2/224041c135f13881a98b9e833584bedab81d5650061457f522a1ebd1daa2c77a/diff:/var/lib/docker/overlay2/73dfd4e2075fccb239b3d5e9b33b32b8e410bdc3cd5a620b41346f44cc5c51f7/diff:/var/lib/docker/overlay2/b3924ed7c91730f6714d33c455db888604b59ab093033b3f59ac16ecdd777987/diff:/var/lib/docker/overlay2/e36a32cd0ab20b216a8db1a8a166b17464399e4d587d22504088a7a6ef0a68a4/diff:/var/lib/docker/overlay2/3334e94fe191333b65f571912c0fcfbbf31aeb090a2fb9b4cfdbc32a37c0fe5f/diff", + "MergedDir": "/var/lib/docker/overlay2/f5d133c5929da8cc8266cbbc3e36f924f4a9c835f943fb436445a26b7e1bcc56/merged", + "UpperDir": "/var/lib/docker/overlay2/f5d133c5929da8cc8266cbbc3e36f924f4a9c835f943fb436445a26b7e1bcc56/diff", + "WorkDir": "/var/lib/docker/overlay2/f5d133c5929da8cc8266cbbc3e36f924f4a9c835f943fb436445a26b7e1bcc56/work" + }, + "Name": "overlay2" + }, + "RootFS": { + "Type": "layers", + "Layers": [ + "sha256:c8be1b8f4d60d99c281fc2db75e0f56df42a83ad2f0b091621ce19357e19d853", + "sha256:977183d4e9995d9cd5ffdfc0f29e911ec9de777bcb0f507895daa1068477f76f", + "sha256:6597da2e2e52f4d438ad49a14ca79324f130a9ea08745505aa174a8db51cb79d", + "sha256:16542a8fc3be1bfaff6ed1daa7922e7c3b47b6c3a8d98b7fca58b9517bb99b75", + "sha256:2df36adfe1af661aebb75a0db796b074bb8f861fbc8f98f6f642570692b3b133", + "sha256:f499c7d34e01d860492ef1cc34b7d7e1319b3c3c81ee7d23258b21605b5902ca", + "sha256:c4bf1d4e5d4adb566b173a0769d247f67c5dd8ff90dfdcebd8c7060f1c06caa9", + "sha256:15259abd479904cbe0d8d421e5b05b2e5745e2bf82e62cdd7fb6d3eafbe4168a", + "sha256:6aa3691a73805f608e5fce69fb6bc89aec8362f58a6b4be2682515e9cfa3cc1a", + "sha256:2d6ad1b66f5660dd860c1fe2d90d26398fcfab4dc1c87c3d5e7c0fc24f8d6fb2", + "sha256:ff29efc56c31eeccc79a33c6e4abd7b1ab3547d95e1cf83974af65a493576c41", + "sha256:b87e68574cc7dccbe974fa760702ef650711036bf144fd9da1f3a2d8f6ac335f", + "sha256:04559213a01cfac69a8d6a6facb58b8681666525c74f605207c40a61a0f4c9b7", + "sha256:467c0082c57b80b48487a9b8429887c0744ddc5b066b3f7678866bde89b78ab2", + "sha256:352a299d6af4773322ed3643d8f98b01aad6f15d838d1852e52a0a3ca56c6efb", + "sha256:486b2abf434bb90cf04bab74f2f8bd2eb488ff90632b56eac4bddcbbf02e8151", + "sha256:3f50d3a0e1a969a9606b59e5295842d731e425108cb349ce6c69a5b30ea1bab9", + "sha256:c10732392b97c121a78a5f20201c2a5e834a2b8677196cdd49260a489a54fd22", + "sha256:c185540c10fea822c6db1b987fcfe22b55a4662648124b98475db4c9dcddb2ab", + "sha256:73b1a8ac1f7fca3d545766ce7fd3c56b40a63724ab78e464d71a29da0c6ac31c", + "sha256:da62dec6eb4ed884952a1b867fd89e3bfe3c510e5c849cc0ac7050ff867a2469", + "sha256:76fe727e4aafc7f56f01282296ab736521c38b9d19c1ae5ebb193f9cd55fa109", + "sha256:a9c9bbbd69c212b7ab3c1a7f03011ccc4d99a6fce1bf1c785325c7bcad789e62", + "sha256:b7b78159dfdaa0dd484c58652e02fa6b755abfd0adb88f106d16178144e46f33", + "sha256:aa0effdf787ecfe74d60d6771006717fd1a9ce1ce0a8161624baa61b68120357", + "sha256:a0a2f7c467efbb8b1ac222f09013b88b68f3c117ec6b6e9dc95564be50f271ab", + "sha256:a0715e661e13d7d3ded5bdc068edd01e5b3aa0e2805152f4c8a1428b4e0673df", + "sha256:6aae3a2d671d369eec34dc9146ef267d06c87461f271fbfbe9136775ecf5dfb8", + "sha256:cb21f14e306d94e437c5418d275bcc6efcea6bc9b3d26a400bdf54fa62242c24", + "sha256:c9da8171f5ca048109ffba5e940e3a7d2db567eda281f92b0eb483173df06add", + "sha256:11486cb955594f9d43909b60f94209bb6854f502a5a093207b657afbaa38a777", + "sha256:243bbd007cb0ee99b704bfe0cf62e1301baa4095ab4c39b01293787a0e4234f1", + "sha256:6aefa0ba7ce01584b4a531b18e36470298cee3b30ecae0e0c64b532a5cebd6e7", + "sha256:a06615b5adc1a3afb7abd524e82f6900a28910927fcf0d4e9b85fd1fcbeb53ad", + "sha256:26d6f1e76275d17860005f7ab9b74fdd2283fcf84e0446bd88d49a6b4e9609f9", + "sha256:55f7c052cf70c8ca01b8e241c0c5c8a9675599d4904c69bfb961a472e246238d", + "sha256:d9958b816a9ad179fca8c18d17c07e9814b152d461c685e1443bec6f990ab990", + "sha256:52142799a4b687fe6e5cf397c41064499ea6cc554b94904d46c1acade998e11f", + "sha256:48063dcdd043f9c88604d10fe9542569be8f8111d46806c96b08d77763ffa347", + "sha256:70cf83155575fdb607f23ace41e31b1d5cb1c24dbbbf56f71c383b583724d339", + "sha256:6cf0f8f815d5371cf5c04e7ebf76c62467948d693b8343184d1446036980d261", + "sha256:7cbffcbb09fc5e9d00372e80990016609c09cc3113429ddc951c4a19b1a5ec72", + "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + ] + }, + "Metadata": { + "LastTagTime": "0001-01-01T00:00:00Z" + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/image.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/image.json new file mode 100644 index 000000000000..ade232f0a48d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/image.json @@ -0,0 +1,132 @@ +{ + "Id": "sha256:44cc64492fb6a6d78d3e6d087f380ae6e479aa1b2c79823b32cdacfcc2f3d715", + "RepoTags": [ + "paketo-buildpacks/cnb:base", + "paketo-buildpacks/builder:base-platform-api-0.2" + ], + "RepoDigests": [ + "paketo-buidpacks/cnb@sha256:5b03a853e636b78c44e475bbc514e2b7b140cc41cca8ab907e9753431ae8c0b0" + ], + "Parent": "", + "Comment": "", + "Created": "1980-01-01T00:00:01Z", + "Container": "", + "ContainerConfig": { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": null, + "Image": "", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": null + }, + "DockerVersion": "", + "Author": "", + "Config": { + "Hostname": "", + "Domainname": "", + "User": "1000:1000", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "CNB_USER_ID=1000", + "CNB_GROUP_ID=1000", + "CNB_STACK_ID=io.buildpacks.stacks.bionic" + ], + "Cmd": [ + "/bin/bash" + ], + "ArgsEscaped": true, + "Image": "sha256:2d153261a5e359c632a17377cfb5d1986c27b96c8b6e95334bf80f1029dbd4bb", + "Volumes": null, + "WorkingDir": "/layers", + "Entrypoint": null, + "OnBuild": null, + "Labels": { + "io.buildpacks.builder.metadata": "{\"description\":\"Ubuntu bionic base image with buildpacks for Java, NodeJS and Golang\",\"buildpacks\":[{\"id\":\"paketo-buildpacks/dotnet-core\",\"version\":\"0.0.9\",\"homepage\":\"https://github.com/paketo-buildpacks/dotnet-core\"},{\"id\":\"paketo-buildpacks/dotnet-core-runtime\",\"version\":\"0.0.201\",\"homepage\":\"https://github.com/paketo-buildpacks/dotnet-core-runtime\"},{\"id\":\"paketo-buildpacks/dotnet-core-sdk\",\"version\":\"0.0.196\",\"homepage\":\"https://github.com/paketo-buildpacks/dotnet-core-sdk\"},{\"id\":\"paketo-buildpacks/dotnet-execute\",\"version\":\"0.0.180\",\"homepage\":\"https://github.com/paketo-buildpacks/dotnet-execute\"},{\"id\":\"paketo-buildpacks/dotnet-publish\",\"version\":\"0.0.121\",\"homepage\":\"https://github.com/paketo-buildpacks/dotnet-publish\"},{\"id\":\"paketo-buildpacks/dotnet-core-aspnet\",\"version\":\"0.0.196\",\"homepage\":\"https://github.com/paketo-buildpacks/dotnet-core-aspnet\"},{\"id\":\"paketo-buildpacks/java-native-image\",\"version\":\"4.7.0\",\"homepage\":\"https://github.com/paketo-buildpacks/java-native-image\"},{\"id\":\"paketo-buildpacks/spring-boot\",\"version\":\"3.5.0\",\"homepage\":\"https://github.com/paketo-buildpacks/spring-boot\"},{\"id\":\"paketo-buildpacks/executable-jar\",\"version\":\"3.1.3\",\"homepage\":\"https://github.com/paketo-buildpacks/executable-jar\"},{\"id\":\"paketo-buildpacks/graalvm\",\"version\":\"4.1.0\",\"homepage\":\"https://github.com/paketo-buildpacks/graalvm\"},{\"id\":\"paketo-buildpacks/gradle\",\"version\":\"3.5.0\",\"homepage\":\"https://github.com/paketo-buildpacks/gradle\"},{\"id\":\"paketo-buildpacks/leiningen\",\"version\":\"1.2.1\",\"homepage\":\"https://github.com/paketo-buildpacks/leiningen\"},{\"id\":\"paketo-buildpacks/sbt\",\"version\":\"3.6.0\",\"homepage\":\"https://github.com/paketo-buildpacks/sbt\"},{\"id\":\"paketo-buildpacks/spring-boot-native-image\",\"version\":\"2.0.1\",\"homepage\":\"https://github.com/paketo-buildpacks/spring-boot-native-image\"},{\"id\":\"paketo-buildpacks/environment-variables\",\"version\":\"2.1.2\",\"homepage\":\"https://github.com/paketo-buildpacks/environment-variables\"},{\"id\":\"paketo-buildpacks/image-labels\",\"version\":\"2.0.7\",\"homepage\":\"https://github.com/paketo-buildpacks/image-labels\"},{\"id\":\"paketo-buildpacks/maven\",\"version\":\"3.2.1\",\"homepage\":\"https://github.com/paketo-buildpacks/maven\"},{\"id\":\"paketo-buildpacks/java\",\"version\":\"4.10.0\",\"homepage\":\"https://github.com/paketo-buildpacks/java\"},{\"id\":\"paketo-buildpacks/ca-certificates\",\"version\":\"1.0.1\",\"homepage\":\"https://github.com/paketo-buildpacks/ca-certificates\"},{\"id\":\"paketo-buildpacks/environment-variables\",\"version\":\"2.1.2\",\"homepage\":\"https://github.com/paketo-buildpacks/environment-variables\"},{\"id\":\"paketo-buildpacks/executable-jar\",\"version\":\"3.1.3\",\"homepage\":\"https://github.com/paketo-buildpacks/executable-jar\"},{\"id\":\"paketo-buildpacks/procfile\",\"version\":\"3.0.0\",\"homepage\":\"https://github.com/paketo-buildpacks/procfile\"},{\"id\":\"paketo-buildpacks/apache-tomcat\",\"version\":\"3.2.0\",\"homepage\":\"https://github.com/paketo-buildpacks/apache-tomcat\"},{\"id\":\"paketo-buildpacks/gradle\",\"version\":\"3.5.0\",\"homepage\":\"https://github.com/paketo-buildpacks/gradle\"},{\"id\":\"paketo-buildpacks/maven\",\"version\":\"3.2.1\",\"homepage\":\"https://github.com/paketo-buildpacks/maven\"},{\"id\":\"paketo-buildpacks/sbt\",\"version\":\"3.6.0\",\"homepage\":\"https://github.com/paketo-buildpacks/sbt\"},{\"id\":\"paketo-buildpacks/bellsoft-liberica\",\"version\":\"6.2.0\",\"homepage\":\"https://github.com/paketo-buildpacks/bellsoft-liberica\"},{\"id\":\"paketo-buildpacks/google-stackdriver\",\"version\":\"2.16.0\",\"homepage\":\"https://github.com/paketo-buildpacks/google-stackdriver\"},{\"id\":\"paketo-buildpacks/image-labels\",\"version\":\"2.0.7\",\"homepage\":\"https://github.com/paketo-buildpacks/image-labels\"},{\"id\":\"paketo-buildpacks/dist-zip\",\"version\":\"2.2.2\",\"homepage\":\"https://github.com/paketo-buildpacks/dist-zip\"},{\"id\":\"paketo-buildpacks/spring-boot\",\"version\":\"3.5.0\",\"homepage\":\"https://github.com/paketo-buildpacks/spring-boot\"},{\"id\":\"paketo-buildpacks/jmx\",\"version\":\"2.1.4\",\"homepage\":\"https://github.com/paketo-buildpacks/jmx\"},{\"id\":\"paketo-buildpacks/leiningen\",\"version\":\"1.2.1\",\"homepage\":\"https://github.com/paketo-buildpacks/leiningen\"}],\"stack\":{\"runImage\":{\"image\":\"cloudfoundry/run:base-cnb\",\"mirrors\":null}},\"lifecycle\":{\"version\":\"0.7.2\",\"api\":{\"buildpack\":\"0.2\",\"platform\":\"0.3\"}},\"createdBy\":{\"name\":\"Pack CLI\",\"version\":\"v0.9.0 (git sha: d42c384a39f367588f2653f2a99702db910e5ad7)\"}}", + "io.buildpacks.buildpack.layers": "{\"org.cloudfoundry.archiveexpanding\":{\"v1.0.102\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:73b1a8ac1f7fca3d545766ce7fd3c56b40a63724ab78e464d71a29da0c6ac31c\"}},\"org.cloudfoundry.azureapplicationinsights\":{\"v1.1.12\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:a0a2f7c467efbb8b1ac222f09013b88b68f3c117ec6b6e9dc95564be50f271ab\"}},\"org.cloudfoundry.buildsystem\":{\"v1.2.15\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:3f50d3a0e1a969a9606b59e5295842d731e425108cb349ce6c69a5b30ea1bab9\"}},\"org.cloudfoundry.debug\":{\"v1.2.11\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:04559213a01cfac69a8d6a6facb58b8681666525c74f605207c40a61a0f4c9b7\"}},\"org.cloudfoundry.dep\":{\"0.0.101\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.tiny\"}],\"layerDiffID\":\"sha256:6aae3a2d671d369eec34dc9146ef267d06c87461f271fbfbe9136775ecf5dfb8\"}},\"org.cloudfoundry.distzip\":{\"v1.1.12\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:a0715e661e13d7d3ded5bdc068edd01e5b3aa0e2805152f4c8a1428b4e0673df\"}},\"org.cloudfoundry.dotnet-core\":{\"v0.0.6\":{\"api\":\"0.2\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.158\",\"optional\":true},{\"id\":\"org.cloudfoundry.icu\",\"version\":\"0.0.43\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-runtime\",\"version\":\"0.0.127\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-aspnet\",\"version\":\"0.0.118\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-sdk\",\"version\":\"0.0.122\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-build\",\"version\":\"0.0.68\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-conf\",\"version\":\"0.0.115\"}]}],\"layerDiffID\":\"sha256:aa0effdf787ecfe74d60d6771006717fd1a9ce1ce0a8161624baa61b68120357\"}},\"org.cloudfoundry.dotnet-core-aspnet\":{\"0.0.118\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:a06615b5adc1a3afb7abd524e82f6900a28910927fcf0d4e9b85fd1fcbeb53ad\"}},\"org.cloudfoundry.dotnet-core-build\":{\"0.0.68\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:26d6f1e76275d17860005f7ab9b74fdd2283fcf84e0446bd88d49a6b4e9609f9\"}},\"org.cloudfoundry.dotnet-core-conf\":{\"0.0.115\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:55f7c052cf70c8ca01b8e241c0c5c8a9675599d4904c69bfb961a472e246238d\"}},\"org.cloudfoundry.dotnet-core-runtime\":{\"0.0.127\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:d9958b816a9ad179fca8c18d17c07e9814b152d461c685e1443bec6f990ab990\"}},\"org.cloudfoundry.dotnet-core-sdk\":{\"0.0.122\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:52142799a4b687fe6e5cf397c41064499ea6cc554b94904d46c1acade998e11f\"}},\"org.cloudfoundry.go\":{\"v0.0.4\":{\"api\":\"0.2\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.105\"},{\"id\":\"org.cloudfoundry.go-mod\",\"version\":\"0.0.89\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.105\"},{\"id\":\"org.cloudfoundry.dep\",\"version\":\"0.0.101\"}]}],\"layerDiffID\":\"sha256:352a299d6af4773322ed3643d8f98b01aad6f15d838d1852e52a0a3ca56c6efb\"}},\"org.cloudfoundry.go-compiler\":{\"0.0.105\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.tiny\"}],\"layerDiffID\":\"sha256:cb21f14e306d94e437c5418d275bcc6efcea6bc9b3d26a400bdf54fa62242c24\"}},\"org.cloudfoundry.go-mod\":{\"0.0.89\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.tiny\"}],\"layerDiffID\":\"sha256:c9da8171f5ca048109ffba5e940e3a7d2db567eda281f92b0eb483173df06add\"}},\"org.cloudfoundry.googlestackdriver\":{\"v1.1.11\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:ff29efc56c31eeccc79a33c6e4abd7b1ab3547d95e1cf83974af65a493576c41\"}},\"org.cloudfoundry.icu\":{\"0.0.43\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:48063dcdd043f9c88604d10fe9542569be8f8111d46806c96b08d77763ffa347\"}},\"org.cloudfoundry.jdbc\":{\"v1.1.14\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:a9c9bbbd69c212b7ab3c1a7f03011ccc4d99a6fce1bf1c785325c7bcad789e62\"}},\"org.cloudfoundry.jmx\":{\"v1.1.12\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:da62dec6eb4ed884952a1b867fd89e3bfe3c510e5c849cc0ac7050ff867a2469\"}},\"org.cloudfoundry.jvmapplication\":{\"v1.1.12\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:c10732392b97c121a78a5f20201c2a5e834a2b8677196cdd49260a489a54fd22\"}},\"org.cloudfoundry.node-engine\":{\"0.0.158\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:70cf83155575fdb607f23ace41e31b1d5cb1c24dbbbf56f71c383b583724d339\"},\"0.0.163\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:11486cb955594f9d43909b60f94209bb6854f502a5a093207b657afbaa38a777\"}},\"org.cloudfoundry.nodejs\":{\"v2.0.8\":{\"api\":\"0.2\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.163\"},{\"id\":\"org.cloudfoundry.yarn-install\",\"version\":\"0.1.10\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.163\"},{\"id\":\"org.cloudfoundry.npm\",\"version\":\"0.1.3\"}]}],\"layerDiffID\":\"sha256:76fe727e4aafc7f56f01282296ab736521c38b9d19c1ae5ebb193f9cd55fa109\"}},\"org.cloudfoundry.npm\":{\"0.1.3\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:243bbd007cb0ee99b704bfe0cf62e1301baa4095ab4c39b01293787a0e4234f1\"}},\"org.cloudfoundry.openjdk\":{\"v1.2.14\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:486b2abf434bb90cf04bab74f2f8bd2eb488ff90632b56eac4bddcbbf02e8151\"}},\"org.cloudfoundry.procfile\":{\"v1.1.12\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:b7b78159dfdaa0dd484c58652e02fa6b755abfd0adb88f106d16178144e46f33\"}},\"org.cloudfoundry.springautoreconfiguration\":{\"v1.1.11\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:c185540c10fea822c6db1b987fcfe22b55a4662648124b98475db4c9dcddb2ab\"}},\"org.cloudfoundry.springboot\":{\"v1.2.13\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:b87e68574cc7dccbe974fa760702ef650711036bf144fd9da1f3a2d8f6ac335f\"}},\"org.cloudfoundry.tomcat\":{\"v1.3.18\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:467c0082c57b80b48487a9b8429887c0744ddc5b066b3f7678866bde89b78ab2\"}},\"org.cloudfoundry.yarn-install\":{\"0.1.10\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:6aefa0ba7ce01584b4a531b18e36470298cee3b30ecae0e0c64b532a5cebd6e7\"}}}", + "io.buildpacks.buildpack.order": "[{\"group\":[{\"id\":\"org.cloudfoundry.openjdk\"},{\"id\":\"org.cloudfoundry.buildsystem\",\"optional\":true},{\"id\":\"org.cloudfoundry.jvmapplication\"},{\"id\":\"org.cloudfoundry.tomcat\",\"optional\":true},{\"id\":\"org.cloudfoundry.springboot\",\"optional\":true},{\"id\":\"org.cloudfoundry.distzip\",\"optional\":true},{\"id\":\"org.cloudfoundry.procfile\",\"optional\":true},{\"id\":\"org.cloudfoundry.azureapplicationinsights\",\"optional\":true},{\"id\":\"org.cloudfoundry.debug\",\"optional\":true},{\"id\":\"org.cloudfoundry.googlestackdriver\",\"optional\":true},{\"id\":\"org.cloudfoundry.jdbc\",\"optional\":true},{\"id\":\"org.cloudfoundry.jmx\",\"optional\":true},{\"id\":\"org.cloudfoundry.springautoreconfiguration\",\"optional\":true}]},{\"group\":[{\"id\":\"org.cloudfoundry.nodejs\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.go\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.dotnet-core\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.procfile\"}]}]", + "io.buildpacks.stack.id": "io.buildpacks.stacks.bionic", + "io.buildpacks.stack.mixins": "[\"build:git\",\"build:build-essential\"]" + } + }, + "Architecture": "amd64", + "Os": "linux", + "Size": 688884758, + "VirtualSize": 688884758, + "GraphDriver": { + "Data": { + "LowerDir": "/var/lib/docker/overlay2/6a79181b2840da2706624f46ce5abd4448973b4f951925d5a276b273256063b2/diff:/var/lib/docker/overlay2/429419a203100f60ab16ec6c879fce975c8138422b9053f80accd6124c730fc2/diff:/var/lib/docker/overlay2/6e45ed6daf4f4f3b90fd1ec5fa958775000875661d3e8be3f1af218d192b058d/diff:/var/lib/docker/overlay2/22928ad308cdd55b3fe849d92b6e38c6bc303ba7c9beb8c0e79aa958e16b1864/diff:/var/lib/docker/overlay2/2ca9ec213226a1604f57c8e141d6f1168134a5cb2ccd8f91ee9be5a39036e6bf/diff:/var/lib/docker/overlay2/96ae944fe00ec20cf5b4441b112ebcc9395faaf08108c9ee38c62e1da33af1c8/diff:/var/lib/docker/overlay2/13ee52e300e476e27350c9ac6274dedf26af85c3079b42a41f9dfc92eff57a80/diff:/var/lib/docker/overlay2/223edb4cc62a2ba2b8bda866905a55c4798c6c32e31d22d60e6ed4f3169ce85e/diff:/var/lib/docker/overlay2/a41235cd7277299cb74ead47def3771885948719e24075ea3bf37580f3af7ae2/diff:/var/lib/docker/overlay2/ed0438e8e2c27b9d62ad21a0761237c350a2ffc9e52f47c019e4f627091c832e/diff:/var/lib/docker/overlay2/0c27c8229b31eafc57ab739b44962dcc07b72f3d8950888873ecb3cfd385032f/diff:/var/lib/docker/overlay2/0957cbcca052cd58bcf9a3d945b0e6876b0df79c1c534da1872c3415a019427d/diff:/var/lib/docker/overlay2/b621414d53d71349c07df8ed45e3e04b2e97bfbaf4bf0d86463f46e0f810eeb4/diff:/var/lib/docker/overlay2/ad521bc47f0bb44262358cf47c3d81a544d098494cf24a5b510620d34eb9c353/diff:/var/lib/docker/overlay2/081501d5bfbd927e69c10eb320513c7c0d5f00bea8cf9e55faa90579fd33adf4/diff:/var/lib/docker/overlay2/fb1ba66bee5568f5700c72865d020d4171a62bfdd099c3cc05b9a253d36a35a4/diff:/var/lib/docker/overlay2/06bcc6b3adeca727d554f1a745ee33242dfe1b3c6392023ac947666057303288/diff:/var/lib/docker/overlay2/1c5397d63d893202dffde29013ee826fb695bda26c718ee03ddde376be4da0a3/diff:/var/lib/docker/overlay2/76075fb7fd3c6b3fb116fb3b464e220918e56d94461c61af9a1aff288ebdba60/diff:/var/lib/docker/overlay2/43d1026bb7b618393912ecc9ddf57b604336184d5f8dc70bcf6332b5f08a3e8d/diff:/var/lib/docker/overlay2/ee27d1fba3deaca0556f7bab171cb3368f169011dd132cf335b5308728f6db8f/diff:/var/lib/docker/overlay2/464d3ec8d86ff31dcb5063ea25521368ea8e9c7964f65e15ff5e0e1ecdbe991e/diff:/var/lib/docker/overlay2/a4a80c33c8b78f68bdc9dbd5903cc2ba1d48e78b9a97d43acb018823ece8e6cb/diff:/var/lib/docker/overlay2/6494f2f1693cff8b16d51fa95620eb0bb691a76fb39b5175d953649577791297/diff:/var/lib/docker/overlay2/9d49e146f82eb5fc4fd81613538e9c5f5f95091fbbc8c49729c6c9140ae356de/diff:/var/lib/docker/overlay2/2934818c52bcd017abe000e71342d67fbc9ccb7dbc165ce05e3250e2110229a5/diff:/var/lib/docker/overlay2/651ca06b2bf75e2122855264287fc937f30d2b49229d628909895be7128b4eb6/diff:/var/lib/docker/overlay2/c93bab59be44fa1b66689dc059d26742d00d2e787d06c3236e1f116199c9807e/diff:/var/lib/docker/overlay2/d0a8e2a0c7e0df172f7a8ebe75e2dce371bb6cc65531b06799bc677c5b5e3627/diff:/var/lib/docker/overlay2/7d14bac240e0d7936351e3fac80b7fbe2a209f4de8992091c4f75e41f9627852/diff:/var/lib/docker/overlay2/d6b192ea137a4ae95e309d263ee8c890e35da02aacd9bdcf5adbd4c28a0c0a3f/diff:/var/lib/docker/overlay2/335bfb632ab7723e25fb5dc7b67389e6ec38178ef10bfbf83337501403e61574/diff:/var/lib/docker/overlay2/0293c7e3472da58f51cbdf15fb293ff71e32c1f80f83f00fb09f8941deef5e43/diff:/var/lib/docker/overlay2/55faa8b47bcb0dd29c3836580f451a0461dd499065af9c830beff6e8329ab484/diff:/var/lib/docker/overlay2/afcb6e109c1ba7d71b8a8b7e573d4ce04f22da3fe0ee523359db5cfb95e65bb6/diff:/var/lib/docker/overlay2/b42eefd9bf6629ae9d16e7aba6ba3939d37816aba7a0999f6d639012a3119be1/diff:/var/lib/docker/overlay2/a9832c8f81ee889a622ce4d95d9f4bab2f91d30e18f69bfd7cfc385c781068d4/diff:/var/lib/docker/overlay2/224041c135f13881a98b9e833584bedab81d5650061457f522a1ebd1daa2c77a/diff:/var/lib/docker/overlay2/73dfd4e2075fccb239b3d5e9b33b32b8e410bdc3cd5a620b41346f44cc5c51f7/diff:/var/lib/docker/overlay2/b3924ed7c91730f6714d33c455db888604b59ab093033b3f59ac16ecdd777987/diff:/var/lib/docker/overlay2/e36a32cd0ab20b216a8db1a8a166b17464399e4d587d22504088a7a6ef0a68a4/diff:/var/lib/docker/overlay2/3334e94fe191333b65f571912c0fcfbbf31aeb090a2fb9b4cfdbc32a37c0fe5f/diff", + "MergedDir": "/var/lib/docker/overlay2/f5d133c5929da8cc8266cbbc3e36f924f4a9c835f943fb436445a26b7e1bcc56/merged", + "UpperDir": "/var/lib/docker/overlay2/f5d133c5929da8cc8266cbbc3e36f924f4a9c835f943fb436445a26b7e1bcc56/diff", + "WorkDir": "/var/lib/docker/overlay2/f5d133c5929da8cc8266cbbc3e36f924f4a9c835f943fb436445a26b7e1bcc56/work" + }, + "Name": "overlay2" + }, + "RootFS": { + "Type": "layers", + "Layers": [ + "sha256:c8be1b8f4d60d99c281fc2db75e0f56df42a83ad2f0b091621ce19357e19d853", + "sha256:977183d4e9995d9cd5ffdfc0f29e911ec9de777bcb0f507895daa1068477f76f", + "sha256:6597da2e2e52f4d438ad49a14ca79324f130a9ea08745505aa174a8db51cb79d", + "sha256:16542a8fc3be1bfaff6ed1daa7922e7c3b47b6c3a8d98b7fca58b9517bb99b75", + "sha256:2df36adfe1af661aebb75a0db796b074bb8f861fbc8f98f6f642570692b3b133", + "sha256:f499c7d34e01d860492ef1cc34b7d7e1319b3c3c81ee7d23258b21605b5902ca", + "sha256:c4bf1d4e5d4adb566b173a0769d247f67c5dd8ff90dfdcebd8c7060f1c06caa9", + "sha256:15259abd479904cbe0d8d421e5b05b2e5745e2bf82e62cdd7fb6d3eafbe4168a", + "sha256:6aa3691a73805f608e5fce69fb6bc89aec8362f58a6b4be2682515e9cfa3cc1a", + "sha256:2d6ad1b66f5660dd860c1fe2d90d26398fcfab4dc1c87c3d5e7c0fc24f8d6fb2", + "sha256:ff29efc56c31eeccc79a33c6e4abd7b1ab3547d95e1cf83974af65a493576c41", + "sha256:b87e68574cc7dccbe974fa760702ef650711036bf144fd9da1f3a2d8f6ac335f", + "sha256:04559213a01cfac69a8d6a6facb58b8681666525c74f605207c40a61a0f4c9b7", + "sha256:467c0082c57b80b48487a9b8429887c0744ddc5b066b3f7678866bde89b78ab2", + "sha256:352a299d6af4773322ed3643d8f98b01aad6f15d838d1852e52a0a3ca56c6efb", + "sha256:486b2abf434bb90cf04bab74f2f8bd2eb488ff90632b56eac4bddcbbf02e8151", + "sha256:3f50d3a0e1a969a9606b59e5295842d731e425108cb349ce6c69a5b30ea1bab9", + "sha256:c10732392b97c121a78a5f20201c2a5e834a2b8677196cdd49260a489a54fd22", + "sha256:c185540c10fea822c6db1b987fcfe22b55a4662648124b98475db4c9dcddb2ab", + "sha256:73b1a8ac1f7fca3d545766ce7fd3c56b40a63724ab78e464d71a29da0c6ac31c", + "sha256:da62dec6eb4ed884952a1b867fd89e3bfe3c510e5c849cc0ac7050ff867a2469", + "sha256:76fe727e4aafc7f56f01282296ab736521c38b9d19c1ae5ebb193f9cd55fa109", + "sha256:a9c9bbbd69c212b7ab3c1a7f03011ccc4d99a6fce1bf1c785325c7bcad789e62", + "sha256:b7b78159dfdaa0dd484c58652e02fa6b755abfd0adb88f106d16178144e46f33", + "sha256:aa0effdf787ecfe74d60d6771006717fd1a9ce1ce0a8161624baa61b68120357", + "sha256:a0a2f7c467efbb8b1ac222f09013b88b68f3c117ec6b6e9dc95564be50f271ab", + "sha256:a0715e661e13d7d3ded5bdc068edd01e5b3aa0e2805152f4c8a1428b4e0673df", + "sha256:6aae3a2d671d369eec34dc9146ef267d06c87461f271fbfbe9136775ecf5dfb8", + "sha256:cb21f14e306d94e437c5418d275bcc6efcea6bc9b3d26a400bdf54fa62242c24", + "sha256:c9da8171f5ca048109ffba5e940e3a7d2db567eda281f92b0eb483173df06add", + "sha256:11486cb955594f9d43909b60f94209bb6854f502a5a093207b657afbaa38a777", + "sha256:243bbd007cb0ee99b704bfe0cf62e1301baa4095ab4c39b01293787a0e4234f1", + "sha256:6aefa0ba7ce01584b4a531b18e36470298cee3b30ecae0e0c64b532a5cebd6e7", + "sha256:a06615b5adc1a3afb7abd524e82f6900a28910927fcf0d4e9b85fd1fcbeb53ad", + "sha256:26d6f1e76275d17860005f7ab9b74fdd2283fcf84e0446bd88d49a6b4e9609f9", + "sha256:55f7c052cf70c8ca01b8e241c0c5c8a9675599d4904c69bfb961a472e246238d", + "sha256:d9958b816a9ad179fca8c18d17c07e9814b152d461c685e1443bec6f990ab990", + "sha256:52142799a4b687fe6e5cf397c41064499ea6cc554b94904d46c1acade998e11f", + "sha256:48063dcdd043f9c88604d10fe9542569be8f8111d46806c96b08d77763ffa347", + "sha256:70cf83155575fdb607f23ace41e31b1d5cb1c24dbbbf56f71c383b583724d339", + "sha256:6cf0f8f815d5371cf5c04e7ebf76c62467948d693b8343184d1446036980d261", + "sha256:7cbffcbb09fc5e9d00372e80990016609c09cc3113429ddc951c4a19b1a5ec72", + "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + ] + }, + "Metadata": { + "LastTagTime": "0001-01-01T00:00:00Z" + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-bindings.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-bindings.json new file mode 100644 index 000000000000..b3a33778d3bb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-bindings.json @@ -0,0 +1,12 @@ +{ + "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-dir", "/cache", "-launch-cache", "/launch-cache", "-daemon", "-process-type=web", "docker.io/library/my-application:latest" ], + "Env" : [ "CNB_PLATFORM_API=0.4" ], + "Labels" : { + "author" : "spring-boot" + }, + "HostConfig" : { + "Binds" : [ "/var/run/docker.sock:/var/run/docker.sock", "pack-layers-aaaaaaaaaa:/layers", "pack-app-aaaaaaaaaa:/workspace", "pack-cache-b35197ac41ea.build:/cache", "pack-cache-b35197ac41ea.launch:/launch-cache", "/host/src/path:/container/dest/path:ro", "volume-name:/container/volume/path:rw" ] + } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-clean-cache.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-clean-cache.json new file mode 100644 index 000000000000..481f406c7687 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-clean-cache.json @@ -0,0 +1,12 @@ +{ + "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-dir", "/cache", "-launch-cache", "/launch-cache", "-daemon", "-skip-restore", "-process-type=web", "docker.io/library/my-application:latest" ], + "Env" : [ "CNB_PLATFORM_API=0.4" ], + "Labels" : { + "author" : "spring-boot" + }, + "HostConfig" : { + "Binds" : [ "/var/run/docker.sock:/var/run/docker.sock", "pack-layers-aaaaaaaaaa:/layers", "pack-app-aaaaaaaaaa:/workspace", "pack-cache-b35197ac41ea.build:/cache", "pack-cache-b35197ac41ea.launch:/launch-cache" ] + } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-platform-api-0.3.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-platform-api-0.3.json new file mode 100644 index 000000000000..d04ec56db362 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-platform-api-0.3.json @@ -0,0 +1,12 @@ +{ + "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-dir", "/cache", "-launch-cache", "/launch-cache", "-daemon", "docker.io/library/my-application:latest" ], + "Env" : [ "CNB_PLATFORM_API=0.3" ], + "Labels" : { + "author" : "spring-boot" + }, + "HostConfig" : { + "Binds" : [ "/var/run/docker.sock:/var/run/docker.sock", "pack-layers-aaaaaaaaaa:/layers", "pack-app-aaaaaaaaaa:/workspace", "pack-cache-b35197ac41ea.build:/cache", "pack-cache-b35197ac41ea.launch:/launch-cache" ] + } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator.json new file mode 100644 index 000000000000..916fa52f8fa6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator.json @@ -0,0 +1,12 @@ +{ + "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-dir", "/cache", "-launch-cache", "/launch-cache", "-daemon", "-process-type=web", "docker.io/library/my-application:latest" ], + "Env" : [ "CNB_PLATFORM_API=0.4" ], + "Labels" : { + "author" : "spring-boot" + }, + "HostConfig" : { + "Binds" : [ "/var/run/docker.sock:/var/run/docker.sock", "pack-layers-aaaaaaaaaa:/layers", "pack-app-aaaaaaaaaa:/workspace", "pack-cache-b35197ac41ea.build:/cache", "pack-cache-b35197ac41ea.launch:/launch-cache" ] + } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/order.toml b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/order.toml new file mode 100644 index 000000000000..f31703545024 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/order.toml @@ -0,0 +1,14 @@ +[[order]] + + [[order.group]] + id = "example/buildpack1" + version = "0.0.1" + + [[order.group]] + id = "example/buildpack2" + version = "0.0.2" + + [[order.group]] + id = "example/buildpack3" + version = "0.0.3" + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/print-stream-build-log.txt b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/print-stream-build-log.txt new file mode 100644 index 000000000000..83cfdffd0a56 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/print-stream-build-log.txt @@ -0,0 +1,19 @@ +Building image 'docker.io/library/my-app:latest' + + > Pulling builder image 'docker.io/cnb/builder' .................................................. + > Pulled builder image '00000001' + > Pulling run image 'docker.io/cnb/runner' .................................................. + > Pulled run image '00000002' + > Executing lifecycle version v0.5.0 + > Using build cache volume 'pack-abc.cache' + + > Running alphabet + [alphabet] one + [alphabet] two + [alphabet] three + + > Running basket + [basket] spring + [basket] boot + +Successfully built image 'docker.io/library/my-app:latest' diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/run-image-with-bad-stack.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/run-image-with-bad-stack.json new file mode 100644 index 000000000000..70c92f54ac9d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/run-image-with-bad-stack.json @@ -0,0 +1,142 @@ +{ + "Id": "sha256:9b450bffdb05bcf660d464d0bfdf344ee6ca38e9b8de4f408c8080b0c9319349", + "RepoTags": [ + "paketo-buildpacks/cnb:latest" + ], + "RepoDigests": [ + "paketo-buildpacks/run@sha256:715806bb793b66e3fc1a5a8f5584c6a1b6db05425e573887673bddcf426f1b90" + ], + "Parent": "", + "Comment": "", + "Created": "2019-10-30T19:34:56.296666503Z", + "Container": "84597380a7968131ab47dd1b8183a96dcfe9e1e4acff1efe5824dcd762184a67", + "ContainerConfig": { + "Hostname": "84597380a796", + "Domainname": "", + "User": "vcap", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "CNB_USER_ID=2000", + "CNB_GROUP_ID=2000", + "CNB_STACK_ID=org.cloudfoundry.stacks.cflinuxfs3" + ], + "Cmd": [ + "/bin/sh", + "-c", + "#(nop) ", + "LABEL io.buildpacks.stack.id=org.cloudfoundry.stacks.cflinuxfs3" + ], + "Image": "sha256:523c8ade6e06f814469b2cf04c8045a74becee17088955f2657958476d3fba1f", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": { + "io.buildpacks.stack.id": "org.cloudfoundry.stacks.cflinuxfs3" + } + }, + "DockerVersion": "18.09.6", + "Author": "", + "Config": { + "Hostname": "", + "Domainname": "", + "User": "vcap", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "CNB_USER_ID=2000", + "CNB_GROUP_ID=2000", + "CNB_STACK_ID=org.cloudfoundry.stacks.cflinuxfs3" + ], + "Cmd": null, + "Image": "sha256:523c8ade6e06f814469b2cf04c8045a74becee17088955f2657958476d3fba1f", + "Volumes": null, + "WorkingDir": "/layers", + "Entrypoint": null, + "OnBuild": null, + "Labels": { + "io.buildpacks.builder.metadata": "{\"description\":\"cflinuxfs3 base image with buildpacks for Java, .NET, NodeJS, Python, Golang, PHP, HTTPD and NGINX\",\"buildpacks\":[{\"id\":\"org.cloudfoundry.googlestackdriver\",\"version\":\"v1.0.40\",\"latest\":true},{\"id\":\"org.cloudfoundry.buildsystem\",\"version\":\"v1.0.114\",\"latest\":true},{\"id\":\"org.cloudfoundry.jdbc\",\"version\":\"v1.0.94\",\"latest\":true},{\"id\":\"org.cloudfoundry.springautoreconfiguration\",\"version\":\"v1.0.100\",\"latest\":true},{\"id\":\"org.cloudfoundry.archiveexpanding\",\"version\":\"v1.0.87\",\"latest\":true},{\"id\":\"org.cloudfoundry.jvmapplication\",\"version\":\"v1.0.72\",\"latest\":true},{\"id\":\"org.cloudfoundry.debug\",\"version\":\"v1.0.92\",\"latest\":true},{\"id\":\"org.cloudfoundry.go\",\"version\":\"v0.0.1\",\"latest\":true},{\"id\":\"org.cloudfoundry.openjdk\",\"version\":\"v1.0.53\",\"latest\":true},{\"id\":\"org.cloudfoundry.procfile\",\"version\":\"v1.0.37\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core\",\"version\":\"v0.0.2\",\"latest\":true},{\"id\":\"org.cloudfoundry.azureapplicationinsights\",\"version\":\"v1.0.94\",\"latest\":true},{\"id\":\"org.cloudfoundry.php\",\"version\":\"v0.0.0-RC1\",\"latest\":true},{\"id\":\"org.cloudfoundry.tomcat\",\"version\":\"v1.1.9\",\"latest\":true},{\"id\":\"org.cloudfoundry.nodejs\",\"version\":\"v0.0.3\",\"latest\":true},{\"id\":\"org.cloudfoundry.jmx\",\"version\":\"v1.0.94\",\"latest\":true},{\"id\":\"org.cloudfoundry.springboot\",\"version\":\"v1.0.97\",\"latest\":true},{\"id\":\"org.cloudfoundry.distzip\",\"version\":\"v1.0.89\",\"latest\":true},{\"id\":\"org.cloudfoundry.python\",\"version\":\"v0.0.1\",\"latest\":true},{\"id\":\"org.cloudfoundry.dep\",\"version\":\"0.0.51\",\"latest\":true},{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.48\",\"latest\":true},{\"id\":\"org.cloudfoundry.go-mod\",\"version\":\"0.0.44\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-aspnet\",\"version\":\"0.0.53\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-build\",\"version\":\"0.0.18\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-conf\",\"version\":\"0.0.57\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-runtime\",\"version\":\"0.0.66\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-sdk\",\"version\":\"0.0.55\",\"latest\":true},{\"id\":\"org.cloudfoundry.httpd\",\"version\":\"0.0.21\",\"latest\":true},{\"id\":\"org.cloudfoundry.nginx\",\"version\":\"0.0.25\",\"latest\":true},{\"id\":\"org.cloudfoundry.php-composer\",\"version\":\"0.0.16\",\"latest\":true},{\"id\":\"org.cloudfoundry.php-dist\",\"version\":\"0.0.30\",\"latest\":true},{\"id\":\"org.cloudfoundry.php-web\",\"version\":\"0.0.24\",\"latest\":true},{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.85\",\"latest\":true},{\"id\":\"org.cloudfoundry.npm\",\"version\":\"0.0.53\",\"latest\":true},{\"id\":\"org.cloudfoundry.yarn\",\"version\":\"0.0.58\",\"latest\":true},{\"id\":\"org.cloudfoundry.conda\",\"version\":\"0.0.37\",\"latest\":true},{\"id\":\"org.cloudfoundry.pip\",\"version\":\"0.0.53\",\"latest\":true},{\"id\":\"org.cloudfoundry.pipenv\",\"version\":\"0.0.38\",\"latest\":true},{\"id\":\"org.cloudfoundry.python-runtime\",\"version\":\"0.0.57\",\"latest\":true}],\"groups\":[{\"buildpacks\":[{\"id\":\"org.cloudfoundry.archiveexpanding\",\"version\":\"v1.0.87\",\"optional\":true},{\"id\":\"org.cloudfoundry.openjdk\",\"version\":\"v1.0.53\"},{\"id\":\"org.cloudfoundry.buildsystem\",\"version\":\"v1.0.114\",\"optional\":true},{\"id\":\"org.cloudfoundry.jvmapplication\",\"version\":\"v1.0.72\"},{\"id\":\"org.cloudfoundry.tomcat\",\"version\":\"v1.1.9\",\"optional\":true},{\"id\":\"org.cloudfoundry.springboot\",\"version\":\"v1.0.97\",\"optional\":true},{\"id\":\"org.cloudfoundry.distzip\",\"version\":\"v1.0.89\",\"optional\":true},{\"id\":\"org.cloudfoundry.procfile\",\"version\":\"v1.0.37\",\"optional\":true},{\"id\":\"org.cloudfoundry.azureapplicationinsights\",\"version\":\"v1.0.94\",\"optional\":true},{\"id\":\"org.cloudfoundry.debug\",\"version\":\"v1.0.92\",\"optional\":true},{\"id\":\"org.cloudfoundry.googlestackdriver\",\"version\":\"v1.0.40\",\"optional\":true},{\"id\":\"org.cloudfoundry.jdbc\",\"version\":\"v1.0.94\",\"optional\":true},{\"id\":\"org.cloudfoundry.jmx\",\"version\":\"v1.0.94\",\"optional\":true},{\"id\":\"org.cloudfoundry.springautoreconfiguration\",\"version\":\"v1.0.100\",\"optional\":true}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.nodejs\",\"version\":\"v0.0.3\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.python\",\"version\":\"v0.0.1\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.go\",\"version\":\"v0.0.1\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.dotnet-core\",\"version\":\"v0.0.2\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.php\",\"version\":\"v0.0.0-RC1\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.httpd\",\"version\":\"0.0.21\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.nginx\",\"version\":\"0.0.25\"}]}],\"stack\":{\"runImage\":{\"image\":\"cloudfoundry/run:full-cnb\",\"mirrors\":null}},\"lifecycle\":{\"version\":\"0.5.0\",\"api\":{\"buildpack\":\"0.2\",\"platform\":\"0.1\"}},\"createdBy\":{\"name\":\"Pack CLI\",\"version\":\"v0.5.0 (git sha: c9cfac75b49609524e1ea33f809c12071406547c)\"}}", + "io.buildpacks.buildpack.layers": "{\"org.cloudfoundry.archiveexpanding\":{\"v1.0.87\":{\"layerDiffID\":\"sha256:391d950d763a33d8ae0373f218aa59907599f51e42cd864129591887e1291034\"}},\"org.cloudfoundry.azureapplicationinsights\":{\"v1.0.94\":{\"layerDiffID\":\"sha256:3544ba1fa82d1e89619ed04c2485fab3445b1603959d224792d1183dd658033d\"}},\"org.cloudfoundry.buildsystem\":{\"v1.0.114\":{\"layerDiffID\":\"sha256:0c6ddab305e5452850f3c09fe15310dff8dc7221702d736dc7705882c1df9658\"}},\"org.cloudfoundry.conda\":{\"0.0.37\":{\"layerDiffID\":\"sha256:0943c634f5c24311ebdeca6fef5682a4a374c89a831700d188bff7f987470004\"}},\"org.cloudfoundry.debug\":{\"v1.0.92\":{\"layerDiffID\":\"sha256:ef935546e2c99da3e8962f2eb3cd6813e9e9a8b19bc8d15b56d1cac37f0342d5\"}},\"org.cloudfoundry.dep\":{\"0.0.51\":{\"layerDiffID\":\"sha256:996aee90e29ed78d80a5a0c0e50d60a732a18fddae06f87b68bef183beddd2c4\"}},\"org.cloudfoundry.distzip\":{\"v1.0.89\":{\"layerDiffID\":\"sha256:e5df92d3db931488225ca9f7290de0334225d4bd7c48fc2dcd380d0921bb6680\"}},\"org.cloudfoundry.dotnet-core\":{\"v0.0.2\":{\"layerDiffID\":\"sha256:96c7f369c29bbf11b971e4dbb6473e8991b666f9de046a414317634eb0a25d2a\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.dotnet-core-runtime\",\"version\":\"0.0.66\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-aspnet\",\"version\":\"0.0.53\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-sdk\",\"version\":\"0.0.55\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-build\",\"version\":\"0.0.18\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-conf\",\"version\":\"0.0.57\"}]}]}},\"org.cloudfoundry.dotnet-core-aspnet\":{\"0.0.53\":{\"layerDiffID\":\"sha256:ab9aaff2160873663388faea6d987cd8f2b5935137b81c64fde145bf2a330d54\"}},\"org.cloudfoundry.dotnet-core-build\":{\"0.0.18\":{\"layerDiffID\":\"sha256:e1f3ab860045b96235cbc1b89a3e73add955a303eb42905b570b6012b73b9184\"}},\"org.cloudfoundry.dotnet-core-conf\":{\"0.0.57\":{\"layerDiffID\":\"sha256:0b260d90d097379d4351132b45110d013b98f4a335795baeb95788fcebcb7f3c\"}},\"org.cloudfoundry.dotnet-core-runtime\":{\"0.0.66\":{\"layerDiffID\":\"sha256:f0f5ecd72b4e0a38d3ad73b5756d8f209955932e9615715502a61dffe56f401a\"}},\"org.cloudfoundry.dotnet-core-sdk\":{\"0.0.55\":{\"layerDiffID\":\"sha256:b4cd790490e41c808e8d65f9ac8f2e58c79bc1a9919a713c4519e77b26dc2053\"}},\"org.cloudfoundry.go\":{\"v0.0.1\":{\"layerDiffID\":\"sha256:6d644992d62bd09a2bbf490b7fe3aa1e35e6d0d2479583c2decec7092f193310\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.48\"},{\"id\":\"org.cloudfoundry.go-mod\",\"version\":\"0.0.44\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.48\"},{\"id\":\"org.cloudfoundry.dep\",\"version\":\"0.0.51\"}]}]}},\"org.cloudfoundry.go-compiler\":{\"0.0.48\":{\"layerDiffID\":\"sha256:30f6a316d4da01d694d8c17aa84b37f468cccc7184248e255486eb3095ebb87c\"}},\"org.cloudfoundry.go-mod\":{\"0.0.44\":{\"layerDiffID\":\"sha256:c694476a7241ba4e4a0663606d4d6eec7ed8624252c010fbef2713968e8f9436\"}},\"org.cloudfoundry.googlestackdriver\":{\"v1.0.40\":{\"layerDiffID\":\"sha256:8debe4b6b4290dbbfecea9edea61c22fb455e69e3cbc7d63b17f8e1ab8ea669b\"}},\"org.cloudfoundry.httpd\":{\"0.0.21\":{\"layerDiffID\":\"sha256:16b88c0e7f950c32c7496117d1efad90a8557a2badcb267d99a19676b1f0b76a\"}},\"org.cloudfoundry.jdbc\":{\"v1.0.94\":{\"layerDiffID\":\"sha256:a9527973bb5d7ccdf88b5be8eb81e024094be1709df659af3127865463c1c188\"}},\"org.cloudfoundry.jmx\":{\"v1.0.94\":{\"layerDiffID\":\"sha256:52845fb94361dad36cc4136e49b92c79ca59c16c579e2f51df0c58ba355c4367\"}},\"org.cloudfoundry.jvmapplication\":{\"v1.0.72\":{\"layerDiffID\":\"sha256:5b3ec0a6ed9e3de93bb082151f56b1cde5d7e31f2809039a1b5b55a5052fe873\"}},\"org.cloudfoundry.nginx\":{\"0.0.25\":{\"layerDiffID\":\"sha256:49d36ba00b17fb605f374ca7877ae129678de925d10fd1955f07c2b6f74dd1c9\"}},\"org.cloudfoundry.node-engine\":{\"0.0.85\":{\"layerDiffID\":\"sha256:3d12e651068a0ff19afdd568b5d14ee5292f849542b31d6c9b099a09344e1f4d\"}},\"org.cloudfoundry.nodejs\":{\"v0.0.3\":{\"layerDiffID\":\"sha256:27ad0fc48c381eb77f69b4e80edccb4d8a2399f5cebd5a8c5a3e1c32313343a6\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.85\"},{\"id\":\"org.cloudfoundry.yarn\",\"version\":\"0.0.58\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.85\"},{\"id\":\"org.cloudfoundry.npm\",\"version\":\"0.0.53\"}]}]}},\"org.cloudfoundry.npm\":{\"0.0.53\":{\"layerDiffID\":\"sha256:f01e41975a9335f5983021b081bc700e46b85efb262670223c4db61eea0a3ebd\"}},\"org.cloudfoundry.openjdk\":{\"v1.0.53\":{\"layerDiffID\":\"sha256:59d817c36a25078c8ec1f6de0d8336aec598037f89708ed13dbf661557a25084\"}},\"org.cloudfoundry.php\":{\"v0.0.0-RC1\":{\"layerDiffID\":\"sha256:12d99406b52b526af152628cd72ba6eacf5d18484dc79cfdacd4b38a21620a2b\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.php-dist\",\"version\":\"0.0.30\"},{\"id\":\"org.cloudfoundry.php-composer\",\"version\":\"0.0.16\",\"optional\":true},{\"id\":\"org.cloudfoundry.httpd\",\"version\":\"0.0.21\",\"optional\":true},{\"id\":\"org.cloudfoundry.php-web\",\"version\":\"0.0.24\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.php-dist\",\"version\":\"0.0.30\"},{\"id\":\"org.cloudfoundry.php-composer\",\"version\":\"0.0.16\",\"optional\":true},{\"id\":\"org.cloudfoundry.nginx\",\"version\":\"0.0.25\",\"optional\":true},{\"id\":\"org.cloudfoundry.php-web\",\"version\":\"0.0.24\"}]}]}},\"org.cloudfoundry.php-composer\":{\"0.0.16\":{\"layerDiffID\":\"sha256:b31d189a88ca43fee6077c25bcb623582d569193ed6ac11b4e5623558911e3de\"}},\"org.cloudfoundry.php-dist\":{\"0.0.30\":{\"layerDiffID\":\"sha256:3ecfd2822cf64c609c9c8489e2accfbc0b1de0f2a3637ff1b5d30768fb34b40c\"}},\"org.cloudfoundry.php-web\":{\"0.0.24\":{\"layerDiffID\":\"sha256:a7f09c3e09b29c5503962a068f29e8726cb91d1dbce2fab688aee0a98189b2be\"}},\"org.cloudfoundry.pip\":{\"0.0.53\":{\"layerDiffID\":\"sha256:9a183e56c86d376b408bdf922746d0a657f62b0e18c7c8f82a496b87710c576f\"}},\"org.cloudfoundry.pipenv\":{\"0.0.38\":{\"layerDiffID\":\"sha256:d919f3c2f534ddbb0b6057f82bca36051ce80a2a9cd3016c320ae276884311f5\"}},\"org.cloudfoundry.procfile\":{\"v1.0.37\":{\"layerDiffID\":\"sha256:6636ce01d12372e56a89ec77ea8d9ed510f8c701df1220750add4613764c05a4\"}},\"org.cloudfoundry.python\":{\"v0.0.1\":{\"layerDiffID\":\"sha256:290ac64fbae3288821551371c8dda38fcf5dfa063a54cb270dcc395a090f5173\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.python-runtime\",\"version\":\"0.0.57\"},{\"id\":\"org.cloudfoundry.pipenv\",\"version\":\"0.0.38\",\"optional\":true},{\"id\":\"org.cloudfoundry.pip\",\"version\":\"0.0.53\",\"optional\":true}]},{\"group\":[{\"id\":\"org.cloudfoundry.conda\",\"version\":\"0.0.37\"}]}]}},\"org.cloudfoundry.python-runtime\":{\"0.0.57\":{\"layerDiffID\":\"sha256:108a3eb288f8094aab6ffd822c593902e48e85c8a37b7da2bd21b15f785d92c5\"}},\"org.cloudfoundry.springautoreconfiguration\":{\"v1.0.100\":{\"layerDiffID\":\"sha256:480cd420e43c6895240c87c88969b87417549c02393cde1b6f71a3a3d5a2a620\"}},\"org.cloudfoundry.springboot\":{\"v1.0.97\":{\"layerDiffID\":\"sha256:7bf3a57229276fb913155b077d00a18ec6cba92c7f062728ca1c3bc3503c0b55\"}},\"org.cloudfoundry.tomcat\":{\"v1.1.9\":{\"layerDiffID\":\"sha256:3b3cda9eceb0fca56c274e3be93daf53f59501e6b3628fabbaea8ea416eb757a\"}},\"org.cloudfoundry.yarn\":{\"0.0.58\":{\"layerDiffID\":\"sha256:2b1b655bb8752f631e786c4c55670315d8569acccfe26402942977c216f2803a\"}}}", + "io.buildpacks.buildpack.order": "[{\"group\":[{\"id\":\"org.cloudfoundry.archiveexpanding\",\"optional\":true},{\"id\":\"org.cloudfoundry.openjdk\"},{\"id\":\"org.cloudfoundry.buildsystem\",\"optional\":true},{\"id\":\"org.cloudfoundry.jvmapplication\"},{\"id\":\"org.cloudfoundry.tomcat\",\"optional\":true},{\"id\":\"org.cloudfoundry.springboot\",\"optional\":true},{\"id\":\"org.cloudfoundry.distzip\",\"optional\":true},{\"id\":\"org.cloudfoundry.procfile\",\"optional\":true},{\"id\":\"org.cloudfoundry.azureapplicationinsights\",\"optional\":true},{\"id\":\"org.cloudfoundry.debug\",\"optional\":true},{\"id\":\"org.cloudfoundry.googlestackdriver\",\"optional\":true},{\"id\":\"org.cloudfoundry.jdbc\",\"optional\":true},{\"id\":\"org.cloudfoundry.jmx\",\"optional\":true},{\"id\":\"org.cloudfoundry.springautoreconfiguration\",\"optional\":true}]},{\"group\":[{\"id\":\"org.cloudfoundry.nodejs\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.python\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.go\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.dotnet-core\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.php\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.httpd\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.nginx\"}]}]", + "io.buildpacks.stack.id": "org.cloudfoundry.stacks.cfwindowsfs3" + } + }, + "Architecture": "amd64", + "Os": "linux", + "Size": 1559461360, + "VirtualSize": 1559461360, + "GraphDriver": { + "Data": { + "LowerDir": "/var/lib/docker/overlay2/58e30cd9f3a4da4e0d30f20c3b50de7655e261fb3d32f04818f1bd960c1e8b6c/diff:/var/lib/docker/overlay2/ad95d738069aa405ff17a9ebb1fdc32f8490b0dd885c3ba3a28e2c3b25d64641/diff:/var/lib/docker/overlay2/74d2896cfe9efc6945ff18870a7213583b987ecf4306e189ff6b793f77af5dcd/diff:/var/lib/docker/overlay2/1052615e5c240724e10928048f735cc9e7a7676a9af5f173b895df57c6921a40/diff:/var/lib/docker/overlay2/b5a62216c4282e7568e84427073f096551977c8c6f80d3a04ebb04c25730edde/diff:/var/lib/docker/overlay2/016a36bf7d7d7258eca08da62c01e47bf8e531531f914dde7cae33e191ab2218/diff:/var/lib/docker/overlay2/a585012bf1cf9da0472b2bbe86c4919355593e1a02cf399a9b012928eb816bcd/diff:/var/lib/docker/overlay2/b4aa8b70bd59d7b7dc6d6fb2e655c2334dc8360c764232f83d036d1f241e3298/diff:/var/lib/docker/overlay2/5f4cab16092522163e2dba6587b48d53ee3b09c8778b0736999bc120dd3753b1/diff:/var/lib/docker/overlay2/90e60622603d230f238976f4d9f65797fc9f070df62b1d2ccad0cefe4e205b43/diff:/var/lib/docker/overlay2/c43877934a580e47cc477ed46e71246468d7b6d7151abc5f1a97bb1e8c8104cf/diff:/var/lib/docker/overlay2/8734b165cabb3ff234a08d488f622135aeae9b7347cf41273445ff7d07aa4565/diff:/var/lib/docker/overlay2/2743cd9d4b7da84925b1b530732dad97108fe77e75865de580255579ba2cdb92/diff:/var/lib/docker/overlay2/68308d057b24bbcde7a4880f5db0e653743debdcc0ff3e736d1776296c4168a1/diff:/var/lib/docker/overlay2/7a4411dc4ac1ed7a1da9aabf088985b8b131e0db047e513f9890eb9c001c1895/diff:/var/lib/docker/overlay2/7f7c262fea8dea5ec86507188848ea391354a76468b09ec93523920e18a400ea/diff:/var/lib/docker/overlay2/8b3bfa567fb956204ad866e49489dacd2fdf5fbfa4f9b05ed3668e1106a5383b/diff:/var/lib/docker/overlay2/31bbc4f1616a35b7ce157266e44513963502e30d836a8fd7b7ee18436a8c46cf/diff:/var/lib/docker/overlay2/149b8e9f1142cdf6dcdfe17ea286ec17197f1a329cf23d5c82958a2032facf54/diff:/var/lib/docker/overlay2/92fb1e680083eb8314c5310bf10ced63ec2b0a98afbf84cc5175a98b3d44507a/diff:/var/lib/docker/overlay2/175a35b6f7af6eb91ca500dbd3d7e798f6d174cf8549881ffe5eed8e92a70b9f/diff:/var/lib/docker/overlay2/48ca54bbd27f7df19acf2b6cc719d05dd3b63f8133038a55d216a4498d4dc913/diff:/var/lib/docker/overlay2/ffe3cc3b93c9030f9dcb0e64c258d1e554f1f0cf27a0f8d4e98bb7ece5ffe882/diff:/var/lib/docker/overlay2/1fb2d962bb27e95c40a9a2c1aa910ca847d186d04e3d7dcdf93967101cc30dde/diff:/var/lib/docker/overlay2/10b34138f9e9e8d70c684d0a564452b1309363441b9d7e048f75e0e1179411dc/diff:/var/lib/docker/overlay2/1d888c7e9c62c22ccda6478f03f3df4b43d43fa3b32a2c2fdc9345fdc7193cd9/diff:/var/lib/docker/overlay2/649fc275c002d7336b277365636e1c8e5651bb3ed1557806d26dd6dfa1d9119a/diff:/var/lib/docker/overlay2/4484c2c0ee4a20aa17017c8cd54c842c876fea32afb297e88614d759ec5410dc/diff:/var/lib/docker/overlay2/bd5f374e0ea6749c90535d778f2689c076b7290ad9d3f050af0a40c9626fdea4/diff:/var/lib/docker/overlay2/c6ba97531b15be65bccaf7ebc866d8bc0b88ce838b224aceb196a55824b289a5/diff:/var/lib/docker/overlay2/6c65fab249fe652cd20a6391b2e0786379b6d2c7d4fde02914dfb4fac84035bd/diff:/var/lib/docker/overlay2/f391b54493024e0183331b8ec7835107bc1b84b8a6e77d852f5357724eb940ff/diff:/var/lib/docker/overlay2/8044f9e3ceb529c80531fa2fe52ad550286f788e69843f235e7d756b90c213b8/diff:/var/lib/docker/overlay2/7d3b5539c46c9f0e7c4f6f733f435d1bf6428a8ca81ba71f4da1031cef58aa6c/diff:/var/lib/docker/overlay2/b8080b36b0ddec4e4d738571ddf9d89815f6a95a555d282cfebb73519b4835a0/diff:/var/lib/docker/overlay2/8a737007d5862aa43119254122eb7050c8bd110a3b653c8d6afca23e76fc4042/diff:/var/lib/docker/overlay2/3bb8f3670831e2031be2173381caf02874ad72e664716a990a330bcc3454f4a2/diff:/var/lib/docker/overlay2/cbd675efde19ccac72d3566404e5df8b152a9063c1668d8154711c7db398f852/diff:/var/lib/docker/overlay2/84fb9095136cb645f7f15aeeeba1db6fae3999cb48a559daf8dd46bf3befbeba/diff:/var/lib/docker/overlay2/cbc51912822c4a3fb8624e0cf678e5dedeb76dc2fa0e5bc56f3cbfbfefb26d68/diff:/var/lib/docker/overlay2/d08d5bdcf39aaf46bdf1e0f4576bb64931af646213ff350065b4d306e00f7e28/diff:/var/lib/docker/overlay2/cf180c218fe181bdf836065c5e85103816ea9e8dbb8ab54fb311209c33455eb2/diff:/var/lib/docker/overlay2/b0aef801fd38973eaf116001e05e7c3f8e2eb58ccc7ed37a4bd8d4fcc2ad172b/diff:/var/lib/docker/overlay2/f73c585ae34bd962e1fee2c3e2d95d47b9daf68b23cf469fb13bc3282cf77238/diff:/var/lib/docker/overlay2/c071c8471b26e55a90b6573a21c581dec43b6c7683a3fe87cb33a0734c83342a/diff", + "MergedDir": "/var/lib/docker/overlay2/41ced64ea40f3382f7a475030a5bc89b9c86e2a03d43031c5eba3c5c72616c2b/merged", + "UpperDir": "/var/lib/docker/overlay2/41ced64ea40f3382f7a475030a5bc89b9c86e2a03d43031c5eba3c5c72616c2b/diff", + "WorkDir": "/var/lib/docker/overlay2/41ced64ea40f3382f7a475030a5bc89b9c86e2a03d43031c5eba3c5c72616c2b/work" + }, + "Name": "overlay2" + }, + "RootFS": { + "Type": "layers", + "Layers": [ + "sha256:733a8e5ce32984099ef675fce04730f6e2a6dcfdf5bd292fea01a8f936265342", + "sha256:7755b972f0b4f49de73ef5114fb3ba9c69d80f217e80da99f56f0d0a5dcb3d70", + "sha256:8f0b2d09ab4b38530a1630403967d11a601e56e02e79d3f56370d34fd071fe38", + "sha256:8debe4b6b4290dbbfecea9edea61c22fb455e69e3cbc7d63b17f8e1ab8ea669b", + "sha256:0c6ddab305e5452850f3c09fe15310dff8dc7221702d736dc7705882c1df9658", + "sha256:a9527973bb5d7ccdf88b5be8eb81e024094be1709df659af3127865463c1c188", + "sha256:480cd420e43c6895240c87c88969b87417549c02393cde1b6f71a3a3d5a2a620", + "sha256:391d950d763a33d8ae0373f218aa59907599f51e42cd864129591887e1291034", + "sha256:5b3ec0a6ed9e3de93bb082151f56b1cde5d7e31f2809039a1b5b55a5052fe873", + "sha256:ef935546e2c99da3e8962f2eb3cd6813e9e9a8b19bc8d15b56d1cac37f0342d5", + "sha256:6d644992d62bd09a2bbf490b7fe3aa1e35e6d0d2479583c2decec7092f193310", + "sha256:59d817c36a25078c8ec1f6de0d8336aec598037f89708ed13dbf661557a25084", + "sha256:6636ce01d12372e56a89ec77ea8d9ed510f8c701df1220750add4613764c05a4", + "sha256:96c7f369c29bbf11b971e4dbb6473e8991b666f9de046a414317634eb0a25d2a", + "sha256:3544ba1fa82d1e89619ed04c2485fab3445b1603959d224792d1183dd658033d", + "sha256:12d99406b52b526af152628cd72ba6eacf5d18484dc79cfdacd4b38a21620a2b", + "sha256:3b3cda9eceb0fca56c274e3be93daf53f59501e6b3628fabbaea8ea416eb757a", + "sha256:27ad0fc48c381eb77f69b4e80edccb4d8a2399f5cebd5a8c5a3e1c32313343a6", + "sha256:52845fb94361dad36cc4136e49b92c79ca59c16c579e2f51df0c58ba355c4367", + "sha256:7bf3a57229276fb913155b077d00a18ec6cba92c7f062728ca1c3bc3503c0b55", + "sha256:e5df92d3db931488225ca9f7290de0334225d4bd7c48fc2dcd380d0921bb6680", + "sha256:290ac64fbae3288821551371c8dda38fcf5dfa063a54cb270dcc395a090f5173", + "sha256:996aee90e29ed78d80a5a0c0e50d60a732a18fddae06f87b68bef183beddd2c4", + "sha256:30f6a316d4da01d694d8c17aa84b37f468cccc7184248e255486eb3095ebb87c", + "sha256:c694476a7241ba4e4a0663606d4d6eec7ed8624252c010fbef2713968e8f9436", + "sha256:ab9aaff2160873663388faea6d987cd8f2b5935137b81c64fde145bf2a330d54", + "sha256:e1f3ab860045b96235cbc1b89a3e73add955a303eb42905b570b6012b73b9184", + "sha256:0b260d90d097379d4351132b45110d013b98f4a335795baeb95788fcebcb7f3c", + "sha256:f0f5ecd72b4e0a38d3ad73b5756d8f209955932e9615715502a61dffe56f401a", + "sha256:b4cd790490e41c808e8d65f9ac8f2e58c79bc1a9919a713c4519e77b26dc2053", + "sha256:16b88c0e7f950c32c7496117d1efad90a8557a2badcb267d99a19676b1f0b76a", + "sha256:49d36ba00b17fb605f374ca7877ae129678de925d10fd1955f07c2b6f74dd1c9", + "sha256:b31d189a88ca43fee6077c25bcb623582d569193ed6ac11b4e5623558911e3de", + "sha256:3ecfd2822cf64c609c9c8489e2accfbc0b1de0f2a3637ff1b5d30768fb34b40c", + "sha256:a7f09c3e09b29c5503962a068f29e8726cb91d1dbce2fab688aee0a98189b2be", + "sha256:3d12e651068a0ff19afdd568b5d14ee5292f849542b31d6c9b099a09344e1f4d", + "sha256:f01e41975a9335f5983021b081bc700e46b85efb262670223c4db61eea0a3ebd", + "sha256:2b1b655bb8752f631e786c4c55670315d8569acccfe26402942977c216f2803a", + "sha256:0943c634f5c24311ebdeca6fef5682a4a374c89a831700d188bff7f987470004", + "sha256:9a183e56c86d376b408bdf922746d0a657f62b0e18c7c8f82a496b87710c576f", + "sha256:d919f3c2f534ddbb0b6057f82bca36051ce80a2a9cd3016c320ae276884311f5", + "sha256:108a3eb288f8094aab6ffd822c593902e48e85c8a37b7da2bd21b15f785d92c5", + "sha256:f8b5dcfa1d082af23bb2b2c08526131921329d48d1614d9f2f163a997176087a", + "sha256:ee13e75c33e0af49fbf6c3aaa5bbd102fc468c2d554c4f94763d35a33964dfe4", + "sha256:2571abab1776d4c2e427fba10d61531afff2ab0789f89ef46ce925b6a5d98e0f", + "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + ] + }, + "Metadata": { + "LastTagTime": "0001-01-01T00:00:00Z" + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/run-image.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/run-image.json new file mode 100644 index 000000000000..596cd4d8ead8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/run-image.json @@ -0,0 +1,97 @@ +{ + "Id": "sha256:1332879bc8e38793a45ebe5a750f2a1c35df07ec2aa9c18f694644a9de77359b", + "RepoTags": [ + "cloudfoundry/run:base-cnb" + ], + "RepoDigests": [ + "cloudfoundry/run@sha256:fb5ecb90a42b2067a859aab23fc1f5e9d9c2589d07ba285608879e7baa415aad" + ], + "Parent": "", + "Comment": "", + "Created": "2020-03-20T20:18:18.117972538Z", + "Container": "91d1af87c3bb6163cd9c7cb21e6891cd25f5fa3c7417779047776e288c0bc234", + "ContainerConfig": { + "Hostname": "91d1af87c3bb", + "Domainname": "", + "User": "1000:1000", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Cmd": [ + "/bin/sh", + "-c", + "#(nop) ", + "LABEL io.buildpacks.stack.id=io.buildpacks.stacks.bionic" + ], + "ArgsEscaped": true, + "Image": "sha256:fbe314bcb23f15a2a09603b6620acd67c332fd08fbf2a7bc3db8fb2f5078d994", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": { + "io.buildpacks.stack.id": "io.buildpacks.stacks.bionic" + } + }, + "DockerVersion": "18.09.6", + "Author": "", + "Config": { + "Hostname": "", + "Domainname": "", + "User": "1000:1000", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Cmd": [ + "/bin/bash" + ], + "ArgsEscaped": true, + "Image": "sha256:fbe314bcb23f15a2a09603b6620acd67c332fd08fbf2a7bc3db8fb2f5078d994", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": { + "io.buildpacks.stack.id": "io.buildpacks.stacks.bionic" + } + }, + "Architecture": "amd64", + "Os": "linux", + "Size": 71248531, + "VirtualSize": 71248531, + "GraphDriver": { + "Data": { + "LowerDir": "/var/lib/docker/overlay2/17f0a4530fbc3e2982f9dc8feb8c8ddc124473bdd50130dae20856ac597d82dd/diff:/var/lib/docker/overlay2/73dfd4e2075fccb239b3d5e9b33b32b8e410bdc3cd5a620b41346f44cc5c51f7/diff:/var/lib/docker/overlay2/b3924ed7c91730f6714d33c455db888604b59ab093033b3f59ac16ecdd777987/diff:/var/lib/docker/overlay2/e36a32cd0ab20b216a8db1a8a166b17464399e4d587d22504088a7a6ef0a68a4/diff:/var/lib/docker/overlay2/3334e94fe191333b65f571912c0fcfbbf31aeb090a2fb9b4cfdbc32a37c0fe5f/diff", + "MergedDir": "/var/lib/docker/overlay2/8d3f9e3c00bc5072f8051ec7884500ca394f2331d8bcc9452f68d04531f50f82/merged", + "UpperDir": "/var/lib/docker/overlay2/8d3f9e3c00bc5072f8051ec7884500ca394f2331d8bcc9452f68d04531f50f82/diff", + "WorkDir": "/var/lib/docker/overlay2/8d3f9e3c00bc5072f8051ec7884500ca394f2331d8bcc9452f68d04531f50f82/work" + }, + "Name": "overlay2" + }, + "RootFS": { + "Type": "layers", + "Layers": [ + "sha256:c8be1b8f4d60d99c281fc2db75e0f56df42a83ad2f0b091621ce19357e19d853", + "sha256:977183d4e9995d9cd5ffdfc0f29e911ec9de777bcb0f507895daa1068477f76f", + "sha256:6597da2e2e52f4d438ad49a14ca79324f130a9ea08745505aa174a8db51cb79d", + "sha256:16542a8fc3be1bfaff6ed1daa7922e7c3b47b6c3a8d98b7fca58b9517bb99b75", + "sha256:c1daeb79beb276c7441d9a1d7281433e9a7edb9f652b8996ecc62b51e88a47b2", + "sha256:eb195d29dc1aa6e4239f00e7868deebc5ac12bebe76104e0b774c1ef29ca78e3" + ] + }, + "Metadata": { + "LastTagTime": "0001-01-01T00:00:00Z" + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/auth-token.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/auth-token.json new file mode 100644 index 000000000000..32fe9c70bc18 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/auth-token.json @@ -0,0 +1,3 @@ +{ + "identitytoken": "tokenvalue" +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/auth-user-full.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/auth-user-full.json new file mode 100644 index 000000000000..a3e615deb6dd --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/auth-user-full.json @@ -0,0 +1,6 @@ +{ + "username": "user", + "password": "secret", + "email": "docker@example.com", + "serveraddress": "https://docker.example.com" +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/auth-user-minimal.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/auth-user-minimal.json new file mode 100644 index 000000000000..7f637981f2ad --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/auth-user-minimal.json @@ -0,0 +1,4 @@ +{ + "username": "user", + "password": "secret" +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/container-wait-response.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/container-wait-response.json new file mode 100644 index 000000000000..2cacf5d6df82 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/container-wait-response.json @@ -0,0 +1,3 @@ +{ + "StatusCode": 1 +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/create-container-response.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/create-container-response.json new file mode 100644 index 000000000000..2726ac000f9c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/create-container-response.json @@ -0,0 +1,4 @@ + { + "Id": "e90e34656806", + "Warnings": [] +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/export.tar b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/export.tar new file mode 100644 index 000000000000..9f2ed1e92261 Binary files /dev/null and b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/export.tar differ diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/load-stream.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/load-stream.json new file mode 100644 index 000000000000..6dc66acf296d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/load-stream.json @@ -0,0 +1 @@ +{"stream":"Loaded image: pack.local/builder/auqfjjbaod:latest\n"} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/log-update-event-ansi.stream b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/log-update-event-ansi.stream new file mode 100644 index 000000000000..baec6ab8e931 Binary files /dev/null and b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/log-update-event-ansi.stream differ diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/log-update-event-invalid-stream-type.stream b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/log-update-event-invalid-stream-type.stream new file mode 100644 index 000000000000..f9ddbfa14e6a Binary files /dev/null and b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/log-update-event-invalid-stream-type.stream differ diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/log-update-event.stream b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/log-update-event.stream new file mode 100644 index 000000000000..329398402157 Binary files /dev/null and b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/log-update-event.stream differ diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/pull-stream.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/pull-stream.json new file mode 100644 index 000000000000..f198286bd3b0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/pull-stream.json @@ -0,0 +1,598 @@ +{ + "status": "Pulling from paketo-buildpacks/cnb", + "id": "base" +} +{"status":"Pulling fs layer","progressDetail":{},"id":"5667fdb72017"} +{"status":"Pulling fs layer","progressDetail":{},"id":"d83811f270d5"} +{"status":"Pulling fs layer","progressDetail":{},"id":"ee671aafb583"} +{"status":"Pulling fs layer","progressDetail":{},"id":"7fc152dfb3a6"} +{"status":"Pulling fs layer","progressDetail":{},"id":"4ab897fa6fbf"} +{"status":"Pulling fs layer","progressDetail":{},"id":"d837a2a1365e"} +{"status":"Pulling fs layer","progressDetail":{},"id":"988ae18fe41a"} +{"status":"Pulling fs layer","progressDetail":{},"id":"eeb8ef83b565"} +{"status":"Pulling fs layer","progressDetail":{},"id":"357fefdf9bc9"} +{"status":"Pulling fs layer","progressDetail":{},"id":"45b746196f82"} +{"status":"Pulling fs layer","progressDetail":{},"id":"fbf4ce20f8c2"} +{"status":"Pulling fs layer","progressDetail":{},"id":"90aca3c647fe"} +{"status":"Pulling fs layer","progressDetail":{},"id":"1dd62f37c84c"} +{"status":"Pulling fs layer","progressDetail":{},"id":"3192b2fa42db"} +{"status":"Pulling fs layer","progressDetail":{},"id":"ae190b8f66a7"} +{"status":"Pulling fs layer","progressDetail":{},"id":"97bb6e138460"} +{"status":"Waiting","progressDetail":{},"id":"eeb8ef83b565"} +{"status":"Pulling fs layer","progressDetail":{},"id":"2edb982d5170"} +{"status":"Waiting","progressDetail":{},"id":"357fefdf9bc9"} +{"status":"Pulling fs layer","progressDetail":{},"id":"7ddc8e6d6da9"} +{"status":"Pulling fs layer","progressDetail":{},"id":"0df6fd234b59"} +{"status":"Waiting","progressDetail":{},"id":"45b746196f82"} +{"status":"Pulling fs layer","progressDetail":{},"id":"8fc1ba8efe21"} +{"status":"Pulling fs layer","progressDetail":{},"id":"1f6f45e783b5"} +{"status":"Pulling fs layer","progressDetail":{},"id":"43ea61082f68"} +{"status":"Waiting","progressDetail":{},"id":"7fc152dfb3a6"} +{"status":"Pulling fs layer","progressDetail":{},"id":"b8cf53bbc6ba"} +{"status":"Waiting","progressDetail":{},"id":"fbf4ce20f8c2"} +{"status":"Pulling fs layer","progressDetail":{},"id":"25efb07e4521"} +{"status":"Waiting","progressDetail":{},"id":"90aca3c647fe"} +{"status":"Pulling fs layer","progressDetail":{},"id":"1c3245356213"} +{"status":"Waiting","progressDetail":{},"id":"1dd62f37c84c"} +{"status":"Pulling fs layer","progressDetail":{},"id":"61ebb123c1eb"} +{"status":"Pulling fs layer","progressDetail":{},"id":"0964b769d2c9"} +{"status":"Waiting","progressDetail":{},"id":"3192b2fa42db"} +{"status":"Pulling fs layer","progressDetail":{},"id":"87f7843f43cd"} +{"status":"Pulling fs layer","progressDetail":{},"id":"a89dbf94d794"} +{"status":"Waiting","progressDetail":{},"id":"ae190b8f66a7"} +{"status":"Pulling fs layer","progressDetail":{},"id":"f0d43ddca77f"} +{"status":"Pulling fs layer","progressDetail":{},"id":"7c674f0cb40c"} +{"status":"Waiting","progressDetail":{},"id":"97bb6e138460"} +{"status":"Pulling fs layer","progressDetail":{},"id":"b48a885b52bc"} +{"status":"Pulling fs layer","progressDetail":{},"id":"272cdf839cbb"} +{"status":"Pulling fs layer","progressDetail":{},"id":"50d054c97f4f"} +{"status":"Pulling fs layer","progressDetail":{},"id":"4c6bbd90b64d"} +{"status":"Waiting","progressDetail":{},"id":"2edb982d5170"} +{"status":"Pulling fs layer","progressDetail":{},"id":"4f4fb700ef54"} +{"status":"Waiting","progressDetail":{},"id":"7ddc8e6d6da9"} +{"status":"Waiting","progressDetail":{},"id":"b8cf53bbc6ba"} +{"status":"Waiting","progressDetail":{},"id":"25efb07e4521"} +{"status":"Waiting","progressDetail":{},"id":"0df6fd234b59"} +{"status":"Waiting","progressDetail":{},"id":"1c3245356213"} +{"status":"Waiting","progressDetail":{},"id":"61ebb123c1eb"} +{"status":"Waiting","progressDetail":{},"id":"8fc1ba8efe21"} +{"status":"Waiting","progressDetail":{},"id":"0964b769d2c9"} +{"status":"Waiting","progressDetail":{},"id":"87f7843f43cd"} +{"status":"Waiting","progressDetail":{},"id":"1f6f45e783b5"} +{"status":"Waiting","progressDetail":{},"id":"a89dbf94d794"} +{"status":"Waiting","progressDetail":{},"id":"43ea61082f68"} +{"status":"Waiting","progressDetail":{},"id":"f0d43ddca77f"} +{"status":"Waiting","progressDetail":{},"id":"7c674f0cb40c"} +{"status":"Waiting","progressDetail":{},"id":"b48a885b52bc"} +{"status":"Waiting","progressDetail":{},"id":"272cdf839cbb"} +{"status":"Waiting","progressDetail":{},"id":"50d054c97f4f"} +{"status":"Waiting","progressDetail":{},"id":"4c6bbd90b64d"} +{"status":"Waiting","progressDetail":{},"id":"4f4fb700ef54"} +{"status":"Waiting","progressDetail":{},"id":"4ab897fa6fbf"} +{"status":"Waiting","progressDetail":{},"id":"d837a2a1365e"} +{"status":"Waiting","progressDetail":{},"id":"988ae18fe41a"} +{"status":"Downloading","progressDetail":{"current":487,"total":850},"progress":"[============================\u003e ] 487B/850B","id":"ee671aafb583"} +{"status":"Downloading","progressDetail":{"current":485,"total":35355},"progress":"[\u003e ] 485B/35.35kB","id":"d83811f270d5"} +{"status":"Downloading","progressDetail":{"current":35355,"total":35355},"progress":"[==================================================\u003e] 35.35kB/35.35kB","id":"d83811f270d5"} +{"status":"Verifying Checksum","progressDetail":{},"id":"d83811f270d5"} +{"status":"Download complete","progressDetail":{},"id":"d83811f270d5"} +{"status":"Downloading","progressDetail":{"current":277600,"total":26683298},"progress":"[\u003e ] 277.6kB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":850,"total":850},"progress":"[==================================================\u003e] 850B/850B","id":"ee671aafb583"} +{"status":"Verifying Checksum","progressDetail":{},"id":"ee671aafb583"} +{"status":"Download complete","progressDetail":{},"id":"ee671aafb583"} +{"status":"Downloading","progressDetail":{"current":2218692,"total":26683298},"progress":"[====\u003e ] 2.219MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":4160196,"total":26683298},"progress":"[=======\u003e ] 4.16MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":6109892,"total":26683298},"progress":"[===========\u003e ] 6.11MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":7772868,"total":26683298},"progress":"[==============\u003e ] 7.773MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":9444036,"total":26683298},"progress":"[=================\u003e ] 9.444MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":163,"total":163},"progress":"[==================================================\u003e] 163B/163B","id":"7fc152dfb3a6"} +{"status":"Verifying Checksum","progressDetail":{},"id":"7fc152dfb3a6"} +{"status":"Download complete","progressDetail":{},"id":"7fc152dfb3a6"} +{"status":"Downloading","progressDetail":{"current":10832580,"total":26683298},"progress":"[====================\u003e ] 10.83MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":531179,"total":88111129},"progress":"[\u003e ] 531.2kB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":11668164,"total":26683298},"progress":"[=====================\u003e ] 11.67MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":1604331,"total":88111129},"progress":"[\u003e ] 1.604MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":12495556,"total":26683298},"progress":"[=======================\u003e ] 12.5MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":3209963,"total":88111129},"progress":"[=\u003e ] 3.21MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":13331140,"total":26683298},"progress":"[========================\u003e ] 13.33MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":4283115,"total":88111129},"progress":"[==\u003e ] 4.283MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":14166724,"total":26683298},"progress":"[==========================\u003e ] 14.17MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":5888747,"total":88111129},"progress":"[===\u003e ] 5.889MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":15280836,"total":26683298},"progress":"[============================\u003e ] 15.28MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":14318,"total":1391657},"progress":"[\u003e ] 14.32kB/1.392MB","id":"d837a2a1365e"} +{"status":"Downloading","progressDetail":{"current":6961899,"total":88111129},"progress":"[===\u003e ] 6.962MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":16116420,"total":26683298},"progress":"[==============================\u003e ] 16.12MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":936688,"total":1391657},"progress":"[=================================\u003e ] 936.7kB/1.392MB","id":"d837a2a1365e"} +{"status":"Verifying Checksum","progressDetail":{},"id":"d837a2a1365e"} +{"status":"Download complete","progressDetail":{},"id":"d837a2a1365e"} +{"status":"Downloading","progressDetail":{"current":8022763,"total":88111129},"progress":"[====\u003e ] 8.023MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":16931524,"total":26683298},"progress":"[===============================\u003e ] 16.93MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":18045636,"total":26683298},"progress":"[=================================\u003e ] 18.05MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":9632491,"total":88111129},"progress":"[=====\u003e ] 9.632MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":10709739,"total":88111129},"progress":"[======\u003e ] 10.71MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":19143364,"total":26683298},"progress":"[===================================\u003e ] 19.14MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":11778795,"total":88111129},"progress":"[======\u003e ] 11.78MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":20249284,"total":26683298},"progress":"[=====================================\u003e ] 20.25MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":12851947,"total":88111129},"progress":"[=======\u003e ] 12.85MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":21072580,"total":26683298},"progress":"[=======================================\u003e ] 21.07MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":14133,"total":1328346},"progress":"[\u003e ] 14.13kB/1.328MB","id":"988ae18fe41a"} +{"status":"Downloading","progressDetail":{"current":13933291,"total":88111129},"progress":"[=======\u003e ] 13.93MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":21908164,"total":26683298},"progress":"[=========================================\u003e ] 21.91MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":973511,"total":1328346},"progress":"[====================================\u003e ] 973.5kB/1.328MB","id":"988ae18fe41a"} +{"status":"Verifying Checksum","progressDetail":{},"id":"988ae18fe41a"} +{"status":"Download complete","progressDetail":{},"id":"988ae18fe41a"} +{"status":"Downloading","progressDetail":{"current":15014635,"total":88111129},"progress":"[========\u003e ] 15.01MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":22747844,"total":26683298},"progress":"[==========================================\u003e ] 22.75MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":16075499,"total":88111129},"progress":"[=========\u003e ] 16.08MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":23575236,"total":26683298},"progress":"[============================================\u003e ] 23.58MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":24414916,"total":26683298},"progress":"[=============================================\u003e ] 24.41MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":17132267,"total":88111129},"progress":"[=========\u003e ] 17.13MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":25250500,"total":26683298},"progress":"[===============================================\u003e ] 25.25MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":18213611,"total":88111129},"progress":"[==========\u003e ] 18.21MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":26073796,"total":26683298},"progress":"[================================================\u003e ] 26.07MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":19286763,"total":88111129},"progress":"[==========\u003e ] 19.29MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":490,"total":4478},"progress":"[=====\u003e ] 490B/4.478kB","id":"eeb8ef83b565"} +{"status":"Downloading","progressDetail":{"current":4478,"total":4478},"progress":"[==================================================\u003e] 4.478kB/4.478kB","id":"eeb8ef83b565"} +{"status":"Verifying Checksum","progressDetail":{},"id":"eeb8ef83b565"} +{"status":"Download complete","progressDetail":{},"id":"eeb8ef83b565"} +{"status":"Verifying Checksum","progressDetail":{},"id":"5667fdb72017"} +{"status":"Download complete","progressDetail":{},"id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":20892395,"total":88111129},"progress":"[===========\u003e ] 20.89MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Extracting","progressDetail":{"current":294912,"total":26683298},"progress":"[\u003e ] 294.9kB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":23050987,"total":88111129},"progress":"[=============\u003e ] 23.05MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Extracting","progressDetail":{"current":2654208,"total":26683298},"progress":"[====\u003e ] 2.654MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":25205483,"total":88111129},"progress":"[==============\u003e ] 25.21MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Extracting","progressDetail":{"current":6193152,"total":26683298},"progress":"[===========\u003e ] 6.193MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":27355883,"total":88111129},"progress":"[===============\u003e ] 27.36MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Extracting","progressDetail":{"current":8552448,"total":26683298},"progress":"[================\u003e ] 8.552MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":197,"total":197},"progress":"[==================================================\u003e] 197B/197B","id":"357fefdf9bc9"} +{"status":"Verifying Checksum","progressDetail":{},"id":"357fefdf9bc9"} +{"status":"Download complete","progressDetail":{},"id":"357fefdf9bc9"} +{"status":"Extracting","progressDetail":{"current":11796480,"total":26683298},"progress":"[======================\u003e ] 11.8MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":29510379,"total":88111129},"progress":"[================\u003e ] 29.51MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":277600,"total":27504647},"progress":"[\u003e ] 277.6kB/27.5MB","id":"45b746196f82"} +{"status":"Extracting","progressDetail":{"current":15040512,"total":26683298},"progress":"[============================\u003e ] 15.04MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":1391300,"total":27504647},"progress":"[==\u003e ] 1.391MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":31132395,"total":88111129},"progress":"[=================\u003e ] 31.13MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Extracting","progressDetail":{"current":17989632,"total":26683298},"progress":"[=================================\u003e ] 17.99MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":32754411,"total":88111129},"progress":"[==================\u003e ] 32.75MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":2230980,"total":27504647},"progress":"[====\u003e ] 2.231MB/27.5MB","id":"45b746196f82"} +{"status":"Extracting","progressDetail":{"current":22118400,"total":26683298},"progress":"[=========================================\u003e ] 22.12MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":33835755,"total":88111129},"progress":"[===================\u003e ] 33.84MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":3078852,"total":27504647},"progress":"[=====\u003e ] 3.079MB/27.5MB","id":"45b746196f82"} +{"status":"Extracting","progressDetail":{"current":24477696,"total":26683298},"progress":"[=============================================\u003e ] 24.48MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":52419,"total":5205016},"progress":"[\u003e ] 52.42kB/5.205MB","id":"fbf4ce20f8c2"} +{"status":"Downloading","progressDetail":{"current":34917099,"total":88111129},"progress":"[===================\u003e ] 34.92MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":3922628,"total":27504647},"progress":"[=======\u003e ] 3.923MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":912096,"total":5205016},"progress":"[========\u003e ] 912.1kB/5.205MB","id":"fbf4ce20f8c2"} +{"status":"Extracting","progressDetail":{"current":26247168,"total":26683298},"progress":"[=================================================\u003e ] 26.25MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":4487876,"total":27504647},"progress":"[========\u003e ] 4.488MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":35986155,"total":88111129},"progress":"[====================\u003e ] 35.99MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Extracting","progressDetail":{"current":26683298,"total":26683298},"progress":"[==================================================\u003e] 26.68MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":1805024,"total":5205016},"progress":"[=================\u003e ] 1.805MB/5.205MB","id":"fbf4ce20f8c2"} +{"status":"Downloading","progressDetail":{"current":5044932,"total":27504647},"progress":"[=========\u003e ] 5.045MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":36522731,"total":88111129},"progress":"[====================\u003e ] 36.52MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":2550496,"total":5205016},"progress":"[========================\u003e ] 2.55MB/5.205MB","id":"fbf4ce20f8c2"} +{"status":"Downloading","progressDetail":{"current":5601988,"total":27504647},"progress":"[==========\u003e ] 5.602MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":3381984,"total":5205016},"progress":"[================================\u003e ] 3.382MB/5.205MB","id":"fbf4ce20f8c2"} +{"status":"Downloading","progressDetail":{"current":37063403,"total":88111129},"progress":"[=====================\u003e ] 37.06MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":6159044,"total":27504647},"progress":"[===========\u003e ] 6.159MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":4152032,"total":5205016},"progress":"[=======================================\u003e ] 4.152MB/5.205MB","id":"fbf4ce20f8c2"} +{"status":"Downloading","progressDetail":{"current":37604075,"total":88111129},"progress":"[=====================\u003e ] 37.6MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Pull complete","progressDetail":{},"id":"5667fdb72017"} +{"status":"Extracting","progressDetail":{"current":32768,"total":35355},"progress":"[==============================================\u003e ] 32.77kB/35.35kB","id":"d83811f270d5"} +{"status":"Extracting","progressDetail":{"current":35355,"total":35355},"progress":"[==================================================\u003e] 35.35kB/35.35kB","id":"d83811f270d5"} +{"status":"Downloading","progressDetail":{"current":5004000,"total":5205016},"progress":"[================================================\u003e ] 5.004MB/5.205MB","id":"fbf4ce20f8c2"} +{"status":"Downloading","progressDetail":{"current":6716100,"total":27504647},"progress":"[============\u003e ] 6.716MB/27.5MB","id":"45b746196f82"} +{"status":"Verifying Checksum","progressDetail":{},"id":"fbf4ce20f8c2"} +{"status":"Download complete","progressDetail":{},"id":"fbf4ce20f8c2"} +{"status":"Downloading","progressDetail":{"current":38144747,"total":88111129},"progress":"[=====================\u003e ] 38.14MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Pull complete","progressDetail":{},"id":"d83811f270d5"} +{"status":"Extracting","progressDetail":{"current":850,"total":850},"progress":"[==================================================\u003e] 850B/850B","id":"ee671aafb583"} +{"status":"Extracting","progressDetail":{"current":850,"total":850},"progress":"[==================================================\u003e] 850B/850B","id":"ee671aafb583"} +{"status":"Downloading","progressDetail":{"current":7293636,"total":27504647},"progress":"[=============\u003e ] 7.294MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":39213803,"total":88111129},"progress":"[======================\u003e ] 39.21MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":8129220,"total":27504647},"progress":"[==============\u003e ] 8.129MB/27.5MB","id":"45b746196f82"} +{"status":"Pull complete","progressDetail":{},"id":"ee671aafb583"} +{"status":"Extracting","progressDetail":{"current":163,"total":163},"progress":"[==================================================\u003e] 163B/163B","id":"7fc152dfb3a6"} +{"status":"Extracting","progressDetail":{"current":163,"total":163},"progress":"[==================================================\u003e] 163B/163B","id":"7fc152dfb3a6"} +{"status":"Downloading","progressDetail":{"current":40295147,"total":88111129},"progress":"[======================\u003e ] 40.3MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":8964804,"total":27504647},"progress":"[================\u003e ] 8.965MB/27.5MB","id":"45b746196f82"} +{"status":"Pull complete","progressDetail":{},"id":"7fc152dfb3a6"} +{"status":"Downloading","progressDetail":{"current":9800388,"total":27504647},"progress":"[=================\u003e ] 9.8MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":41368299,"total":88111129},"progress":"[=======================\u003e ] 41.37MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":49680,"total":4964709},"progress":"[\u003e ] 49.68kB/4.965MB","id":"90aca3c647fe"} +{"status":"Downloading","progressDetail":{"current":10635972,"total":27504647},"progress":"[===================\u003e ] 10.64MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":908013,"total":4964709},"progress":"[=========\u003e ] 908kB/4.965MB","id":"90aca3c647fe"} +{"status":"Downloading","progressDetail":{"current":41908971,"total":88111129},"progress":"[=======================\u003e ] 41.91MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":11193028,"total":27504647},"progress":"[====================\u003e ] 11.19MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":2038509,"total":4964709},"progress":"[====================\u003e ] 2.039MB/4.965MB","id":"90aca3c647fe"} +{"status":"Downloading","progressDetail":{"current":42449643,"total":88111129},"progress":"[========================\u003e ] 42.45MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":11750084,"total":27504647},"progress":"[=====================\u003e ] 11.75MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":3316461,"total":4964709},"progress":"[=================================\u003e ] 3.316MB/4.965MB","id":"90aca3c647fe"} +{"status":"Downloading","progressDetail":{"current":4791021,"total":4964709},"progress":"[================================================\u003e ] 4.791MB/4.965MB","id":"90aca3c647fe"} +{"status":"Verifying Checksum","progressDetail":{},"id":"90aca3c647fe"} +{"status":"Download complete","progressDetail":{},"id":"90aca3c647fe"} +{"status":"Downloading","progressDetail":{"current":12315332,"total":27504647},"progress":"[======================\u003e ] 12.32MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":42990315,"total":88111129},"progress":"[========================\u003e ] 42.99MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":13155012,"total":27504647},"progress":"[=======================\u003e ] 13.16MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":43530987,"total":88111129},"progress":"[========================\u003e ] 43.53MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":13990596,"total":27504647},"progress":"[=========================\u003e ] 13.99MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":44063467,"total":88111129},"progress":"[=========================\u003e ] 44.06MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":15112900,"total":27504647},"progress":"[===========================\u003e ] 15.11MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":45132523,"total":88111129},"progress":"[=========================\u003e ] 45.13MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":16235204,"total":27504647},"progress":"[=============================\u003e ] 16.24MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":52418,"total":5149051},"progress":"[\u003e ] 52.42kB/5.149MB","id":"1dd62f37c84c"} +{"status":"Downloading","progressDetail":{"current":1195147,"total":5149051},"progress":"[===========\u003e ] 1.195MB/5.149MB","id":"1dd62f37c84c"} +{"status":"Downloading","progressDetail":{"current":16792260,"total":27504647},"progress":"[==============================\u003e ] 16.79MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":45673195,"total":88111129},"progress":"[=========================\u003e ] 45.67MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":2702475,"total":5149051},"progress":"[==========================\u003e ] 2.702MB/5.149MB","id":"1dd62f37c84c"} +{"status":"Downloading","progressDetail":{"current":4078320,"total":5149051},"progress":"[=======================================\u003e ] 4.078MB/5.149MB","id":"1dd62f37c84c"} +{"status":"Downloading","progressDetail":{"current":17349316,"total":27504647},"progress":"[===============================\u003e ] 17.35MB/27.5MB","id":"45b746196f82"} +{"status":"Verifying Checksum","progressDetail":{},"id":"1dd62f37c84c"} +{"status":"Download complete","progressDetail":{},"id":"1dd62f37c84c"} +{"status":"Downloading","progressDetail":{"current":46213867,"total":88111129},"progress":"[==========================\u003e ] 46.21MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":17918660,"total":27504647},"progress":"[================================\u003e ] 17.92MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":19040964,"total":27504647},"progress":"[==================================\u003e ] 19.04MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":47295211,"total":88111129},"progress":"[==========================\u003e ] 47.3MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":20183748,"total":27504647},"progress":"[====================================\u003e ] 20.18MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":48368363,"total":88111129},"progress":"[===========================\u003e ] 48.37MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":21301956,"total":27504647},"progress":"[======================================\u003e ] 21.3MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":22432452,"total":27504647},"progress":"[========================================\u003e ] 22.43MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":38884,"total":3855277},"progress":"[\u003e ] 38.88kB/3.855MB","id":"3192b2fa42db"} +{"status":"Downloading","progressDetail":{"current":49445611,"total":88111129},"progress":"[============================\u003e ] 49.45MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":977632,"total":3855277},"progress":"[============\u003e ] 977.6kB/3.855MB","id":"3192b2fa42db"} +{"status":"Downloading","progressDetail":{"current":23268036,"total":27504647},"progress":"[==========================================\u003e ] 23.27MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":49986283,"total":88111129},"progress":"[============================\u003e ] 49.99MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":1895136,"total":3855277},"progress":"[========================\u003e ] 1.895MB/3.855MB","id":"3192b2fa42db"} +{"status":"Downloading","progressDetail":{"current":23833284,"total":27504647},"progress":"[===========================================\u003e ] 23.83MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":2939616,"total":3855277},"progress":"[======================================\u003e ] 2.94MB/3.855MB","id":"3192b2fa42db"} +{"status":"Downloading","progressDetail":{"current":24390340,"total":27504647},"progress":"[============================================\u003e ] 24.39MB/27.5MB","id":"45b746196f82"} +{"status":"Download complete","progressDetail":{},"id":"3192b2fa42db"} +{"status":"Downloading","progressDetail":{"current":50518763,"total":88111129},"progress":"[============================\u003e ] 50.52MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":24947396,"total":27504647},"progress":"[=============================================\u003e ] 24.95MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":51059435,"total":88111129},"progress":"[============================\u003e ] 51.06MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":25803460,"total":27504647},"progress":"[==============================================\u003e ] 25.8MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":26942148,"total":27504647},"progress":"[================================================\u003e ] 26.94MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":52140779,"total":88111129},"progress":"[=============================\u003e ] 52.14MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":27504647,"total":27504647},"progress":"[==================================================\u003e] 27.5MB/27.5MB","id":"45b746196f82"} +{"status":"Verifying Checksum","progressDetail":{},"id":"45b746196f82"} +{"status":"Download complete","progressDetail":{},"id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":53222123,"total":88111129},"progress":"[==============================\u003e ] 53.22MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":51194,"total":4983195},"progress":"[\u003e ] 51.19kB/4.983MB","id":"ae190b8f66a7"} +{"status":"Downloading","progressDetail":{"current":54299371,"total":88111129},"progress":"[==============================\u003e ] 54.3MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":1268464,"total":4983195},"progress":"[============\u003e ] 1.268MB/4.983MB","id":"ae190b8f66a7"} +{"status":"Downloading","progressDetail":{"current":54827755,"total":88111129},"progress":"[===============================\u003e ] 54.83MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":2767600,"total":4983195},"progress":"[===========================\u003e ] 2.768MB/4.983MB","id":"ae190b8f66a7"} +{"status":"Downloading","progressDetail":{"current":4528880,"total":4983195},"progress":"[=============================================\u003e ] 4.529MB/4.983MB","id":"ae190b8f66a7"} +{"status":"Downloading","progressDetail":{"current":55368427,"total":88111129},"progress":"[===============================\u003e ] 55.37MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Verifying Checksum","progressDetail":{},"id":"ae190b8f66a7"} +{"status":"Download complete","progressDetail":{},"id":"ae190b8f66a7"} +{"status":"Downloading","progressDetail":{"current":63614,"total":6103207},"progress":"[\u003e ] 63.61kB/6.103MB","id":"97bb6e138460"} +{"status":"Downloading","progressDetail":{"current":56449771,"total":88111129},"progress":"[================================\u003e ] 56.45MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":1530606,"total":6103207},"progress":"[============\u003e ] 1.531MB/6.103MB","id":"97bb6e138460"} +{"status":"Downloading","progressDetail":{"current":3193582,"total":6103207},"progress":"[==========================\u003e ] 3.194MB/6.103MB","id":"97bb6e138460"} +{"status":"Downloading","progressDetail":{"current":56990443,"total":88111129},"progress":"[================================\u003e ] 56.99MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":4786926,"total":6103207},"progress":"[=======================================\u003e ] 4.787MB/6.103MB","id":"97bb6e138460"} +{"status":"Downloading","progressDetail":{"current":57531115,"total":88111129},"progress":"[================================\u003e ] 57.53MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Download complete","progressDetail":{},"id":"97bb6e138460"} +{"status":"Downloading","progressDetail":{"current":489,"total":787},"progress":"[===============================\u003e ] 489B/787B","id":"2edb982d5170"} +{"status":"Downloading","progressDetail":{"current":58612459,"total":88111129},"progress":"[=================================\u003e ] 58.61MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":787,"total":787},"progress":"[==================================================\u003e] 787B/787B","id":"2edb982d5170"} +{"status":"Verifying Checksum","progressDetail":{},"id":"2edb982d5170"} +{"status":"Download complete","progressDetail":{},"id":"2edb982d5170"} +{"status":"Downloading","progressDetail":{"current":60213995,"total":88111129},"progress":"[==================================\u003e ] 60.21MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":61827819,"total":88111129},"progress":"[===================================\u003e ] 61.83MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":63449835,"total":88111129},"progress":"[====================================\u003e ] 63.45MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":65071851,"total":88111129},"progress":"[====================================\u003e ] 65.07MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":49803,"total":4894860},"progress":"[\u003e ] 49.8kB/4.895MB","id":"7ddc8e6d6da9"} +{"status":"Downloading","progressDetail":{"current":49681,"total":4953791},"progress":"[\u003e ] 49.68kB/4.954MB","id":"0df6fd234b59"} +{"status":"Downloading","progressDetail":{"current":912099,"total":4894860},"progress":"[=========\u003e ] 912.1kB/4.895MB","id":"7ddc8e6d6da9"} +{"status":"Downloading","progressDetail":{"current":66145003,"total":88111129},"progress":"[=====================================\u003e ] 66.15MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":748270,"total":4953791},"progress":"[=======\u003e ] 748.3kB/4.954MB","id":"0df6fd234b59"} +{"status":"Downloading","progressDetail":{"current":1702627,"total":4894860},"progress":"[=================\u003e ] 1.703MB/4.895MB","id":"7ddc8e6d6da9"} +{"status":"Downloading","progressDetail":{"current":67205867,"total":88111129},"progress":"[======================================\u003e ] 67.21MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":1678062,"total":4953791},"progress":"[================\u003e ] 1.678MB/4.954MB","id":"0df6fd234b59"} +{"status":"Downloading","progressDetail":{"current":2194147,"total":4894860},"progress":"[======================\u003e ] 2.194MB/4.895MB","id":"7ddc8e6d6da9"} +{"status":"Downloading","progressDetail":{"current":67746539,"total":88111129},"progress":"[======================================\u003e ] 67.75MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":2648814,"total":4953791},"progress":"[==========================\u003e ] 2.649MB/4.954MB","id":"0df6fd234b59"} +{"status":"Downloading","progressDetail":{"current":2743011,"total":4894860},"progress":"[============================\u003e ] 2.743MB/4.895MB","id":"7ddc8e6d6da9"} +{"status":"Downloading","progressDetail":{"current":68287211,"total":88111129},"progress":"[======================================\u003e ] 68.29MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":3697390,"total":4953791},"progress":"[=====================================\u003e ] 3.697MB/4.954MB","id":"0df6fd234b59"} +{"status":"Downloading","progressDetail":{"current":3381987,"total":4894860},"progress":"[==================================\u003e ] 3.382MB/4.895MB","id":"7ddc8e6d6da9"} +{"status":"Downloading","progressDetail":{"current":4774638,"total":4953791},"progress":"[================================================\u003e ] 4.775MB/4.954MB","id":"0df6fd234b59"} +{"status":"Downloading","progressDetail":{"current":68827883,"total":88111129},"progress":"[=======================================\u003e ] 68.83MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":4953791,"total":4953791},"progress":"[==================================================\u003e] 4.954MB/4.954MB","id":"0df6fd234b59"} +{"status":"Verifying Checksum","progressDetail":{},"id":"0df6fd234b59"} +{"status":"Download complete","progressDetail":{},"id":"0df6fd234b59"} +{"status":"Downloading","progressDetail":{"current":4004579,"total":4894860},"progress":"[========================================\u003e ] 4.005MB/4.895MB","id":"7ddc8e6d6da9"} +{"status":"Downloading","progressDetail":{"current":4893411,"total":4894860},"progress":"[=================================================\u003e ] 4.893MB/4.895MB","id":"7ddc8e6d6da9"} +{"status":"Verifying Checksum","progressDetail":{},"id":"7ddc8e6d6da9"} +{"status":"Download complete","progressDetail":{},"id":"7ddc8e6d6da9"} +{"status":"Downloading","progressDetail":{"current":69909227,"total":88111129},"progress":"[=======================================\u003e ] 69.91MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":71527147,"total":88111129},"progress":"[========================================\u003e ] 71.53MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":73149163,"total":88111129},"progress":"[=========================================\u003e ] 73.15MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":74771179,"total":88111129},"progress":"[==========================================\u003e ] 74.77MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":63573,"total":6137526},"progress":"[\u003e ] 63.57kB/6.138MB","id":"8fc1ba8efe21"} +{"status":"Downloading","progressDetail":{"current":75311851,"total":88111129},"progress":"[==========================================\u003e ] 75.31MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":1317559,"total":6137526},"progress":"[==========\u003e ] 1.318MB/6.138MB","id":"8fc1ba8efe21"} +{"status":"Downloading","progressDetail":{"current":2710199,"total":6137526},"progress":"[======================\u003e ] 2.71MB/6.138MB","id":"8fc1ba8efe21"} +{"status":"Downloading","progressDetail":{"current":38729,"total":3854415},"progress":"[\u003e ] 38.73kB/3.854MB","id":"1f6f45e783b5"} +{"status":"Downloading","progressDetail":{"current":76368619,"total":88111129},"progress":"[===========================================\u003e ] 76.37MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":3783351,"total":6137526},"progress":"[==============================\u003e ] 3.783MB/6.138MB","id":"8fc1ba8efe21"} +{"status":"Downloading","progressDetail":{"current":658157,"total":3854415},"progress":"[========\u003e ] 658.2kB/3.854MB","id":"1f6f45e783b5"} +{"status":"Downloading","progressDetail":{"current":4520631,"total":6137526},"progress":"[====================================\u003e ] 4.521MB/6.138MB","id":"8fc1ba8efe21"} +{"status":"Downloading","progressDetail":{"current":1350381,"total":3854415},"progress":"[=================\u003e ] 1.35MB/3.854MB","id":"1f6f45e783b5"} +{"status":"Downloading","progressDetail":{"current":5364407,"total":6137526},"progress":"[===========================================\u003e ] 5.364MB/6.138MB","id":"8fc1ba8efe21"} +{"status":"Downloading","progressDetail":{"current":77445867,"total":88111129},"progress":"[===========================================\u003e ] 77.45MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":2153197,"total":3854415},"progress":"[===========================\u003e ] 2.153MB/3.854MB","id":"1f6f45e783b5"} +{"status":"Verifying Checksum","progressDetail":{},"id":"8fc1ba8efe21"} +{"status":"Download complete","progressDetail":{},"id":"8fc1ba8efe21"} +{"status":"Downloading","progressDetail":{"current":77986539,"total":88111129},"progress":"[============================================\u003e ] 77.99MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":3021549,"total":3854415},"progress":"[=======================================\u003e ] 3.022MB/3.854MB","id":"1f6f45e783b5"} +{"status":"Verifying Checksum","progressDetail":{},"id":"1f6f45e783b5"} +{"status":"Download complete","progressDetail":{},"id":"1f6f45e783b5"} +{"status":"Downloading","progressDetail":{"current":79067883,"total":88111129},"progress":"[============================================\u003e ] 79.07MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":80149227,"total":88111129},"progress":"[=============================================\u003e ] 80.15MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":81767147,"total":88111129},"progress":"[==============================================\u003e ] 81.77MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":52419,"total":5222290},"progress":"[\u003e ] 52.42kB/5.222MB","id":"43ea61082f68"} +{"status":"Downloading","progressDetail":{"current":1055455,"total":5222290},"progress":"[==========\u003e ] 1.055MB/5.222MB","id":"43ea61082f68"} +{"status":"Downloading","progressDetail":{"current":83372779,"total":88111129},"progress":"[===============================================\u003e ] 83.37MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":2333407,"total":5222290},"progress":"[======================\u003e ] 2.333MB/5.222MB","id":"43ea61082f68"} +{"status":"Downloading","progressDetail":{"current":35991,"total":3564359},"progress":"[\u003e ] 35.99kB/3.564MB","id":"b8cf53bbc6ba"} +{"status":"Downloading","progressDetail":{"current":84454123,"total":88111129},"progress":"[===============================================\u003e ] 84.45MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":3300063,"total":5222290},"progress":"[===============================\u003e ] 3.3MB/5.222MB","id":"43ea61082f68"} +{"status":"Downloading","progressDetail":{"current":752366,"total":3564359},"progress":"[==========\u003e ] 752.4kB/3.564MB","id":"b8cf53bbc6ba"} +{"status":"Downloading","progressDetail":{"current":3979999,"total":5222290},"progress":"[======================================\u003e ] 3.98MB/5.222MB","id":"43ea61082f68"} +{"status":"Downloading","progressDetail":{"current":1743598,"total":3564359},"progress":"[========================\u003e ] 1.744MB/3.564MB","id":"b8cf53bbc6ba"} +{"status":"Downloading","progressDetail":{"current":85527275,"total":88111129},"progress":"[================================================\u003e ] 85.53MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":4537055,"total":5222290},"progress":"[===========================================\u003e ] 4.537MB/5.222MB","id":"43ea61082f68"} +{"status":"Downloading","progressDetail":{"current":2833134,"total":3564359},"progress":"[=======================================\u003e ] 2.833MB/3.564MB","id":"b8cf53bbc6ba"} +{"status":"Downloading","progressDetail":{"current":5077727,"total":5222290},"progress":"[================================================\u003e ] 5.078MB/5.222MB","id":"43ea61082f68"} +{"status":"Verifying Checksum","progressDetail":{},"id":"b8cf53bbc6ba"} +{"status":"Download complete","progressDetail":{},"id":"b8cf53bbc6ba"} +{"status":"Verifying Checksum","progressDetail":{},"id":"43ea61082f68"} +{"status":"Download complete","progressDetail":{},"id":"43ea61082f68"} +{"status":"Downloading","progressDetail":{"current":86067947,"total":88111129},"progress":"[================================================\u003e ] 86.07MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":87132907,"total":88111129},"progress":"[=================================================\u003e ] 87.13MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Verifying Checksum","progressDetail":{},"id":"4ab897fa6fbf"} +{"status":"Download complete","progressDetail":{},"id":"4ab897fa6fbf"} +{"status":"Extracting","progressDetail":{"current":557056,"total":88111129},"progress":"[\u003e ] 557.1kB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":52418,"total":5120108},"progress":"[\u003e ] 52.42kB/5.12MB","id":"1c3245356213"} +{"status":"Downloading","progressDetail":{"current":489,"total":790},"progress":"[==============================\u003e ] 489B/790B","id":"25efb07e4521"} +{"status":"Extracting","progressDetail":{"current":5013504,"total":88111129},"progress":"[==\u003e ] 5.014MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":790,"total":790},"progress":"[==================================================\u003e] 790B/790B","id":"25efb07e4521"} +{"status":"Verifying Checksum","progressDetail":{},"id":"25efb07e4521"} +{"status":"Download complete","progressDetail":{},"id":"25efb07e4521"} +{"status":"Downloading","progressDetail":{"current":1764079,"total":5120108},"progress":"[=================\u003e ] 1.764MB/5.12MB","id":"1c3245356213"} +{"status":"Extracting","progressDetail":{"current":8355840,"total":88111129},"progress":"[====\u003e ] 8.356MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":3635951,"total":5120108},"progress":"[===================================\u003e ] 3.636MB/5.12MB","id":"1c3245356213"} +{"status":"Verifying Checksum","progressDetail":{},"id":"1c3245356213"} +{"status":"Download complete","progressDetail":{},"id":"1c3245356213"} +{"status":"Extracting","progressDetail":{"current":11141120,"total":88111129},"progress":"[======\u003e ] 11.14MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":52419,"total":5117023},"progress":"[\u003e ] 52.42kB/5.117MB","id":"61ebb123c1eb"} +{"status":"Extracting","progressDetail":{"current":13369344,"total":88111129},"progress":"[=======\u003e ] 13.37MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":1596142,"total":5117023},"progress":"[===============\u003e ] 1.596MB/5.117MB","id":"61ebb123c1eb"} +{"status":"Extracting","progressDetail":{"current":13926400,"total":88111129},"progress":"[=======\u003e ] 13.93MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":3242734,"total":5117023},"progress":"[===============================\u003e ] 3.243MB/5.117MB","id":"61ebb123c1eb"} +{"status":"Downloading","progressDetail":{"current":55157,"total":5384215},"progress":"[\u003e ] 55.16kB/5.384MB","id":"0964b769d2c9"} +{"status":"Downloading","progressDetail":{"current":4635374,"total":5117023},"progress":"[=============================================\u003e ] 4.635MB/5.117MB","id":"61ebb123c1eb"} +{"status":"Extracting","progressDetail":{"current":15040512,"total":88111129},"progress":"[========\u003e ] 15.04MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Verifying Checksum","progressDetail":{},"id":"61ebb123c1eb"} +{"status":"Download complete","progressDetail":{},"id":"61ebb123c1eb"} +{"status":"Downloading","progressDetail":{"current":989937,"total":5384215},"progress":"[=========\u003e ] 989.9kB/5.384MB","id":"0964b769d2c9"} +{"status":"Extracting","progressDetail":{"current":15597568,"total":88111129},"progress":"[========\u003e ] 15.6MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":2558705,"total":5384215},"progress":"[=======================\u003e ] 2.559MB/5.384MB","id":"0964b769d2c9"} +{"status":"Extracting","progressDetail":{"current":18382848,"total":88111129},"progress":"[==========\u003e ] 18.38MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":4311793,"total":5384215},"progress":"[========================================\u003e ] 4.312MB/5.384MB","id":"0964b769d2c9"} +{"status":"Downloading","progressDetail":{"current":53788,"total":5252487},"progress":"[\u003e ] 53.79kB/5.252MB","id":"87f7843f43cd"} +{"status":"Extracting","progressDetail":{"current":22839296,"total":88111129},"progress":"[============\u003e ] 22.84MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":5212913,"total":5384215},"progress":"[================================================\u003e ] 5.213MB/5.384MB","id":"0964b769d2c9"} +{"status":"Downloading","progressDetail":{"current":846577,"total":5252487},"progress":"[========\u003e ] 846.6kB/5.252MB","id":"87f7843f43cd"} +{"status":"Verifying Checksum","progressDetail":{},"id":"0964b769d2c9"} +{"status":"Download complete","progressDetail":{},"id":"0964b769d2c9"} +{"status":"Extracting","progressDetail":{"current":26181632,"total":88111129},"progress":"[==============\u003e ] 26.18MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":2628337,"total":5252487},"progress":"[=========================\u003e ] 2.628MB/5.252MB","id":"87f7843f43cd"} +{"status":"Extracting","progressDetail":{"current":30638080,"total":88111129},"progress":"[=================\u003e ] 30.64MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":4340465,"total":5252487},"progress":"[=========================================\u003e ] 4.34MB/5.252MB","id":"87f7843f43cd"} +{"status":"Download complete","progressDetail":{},"id":"87f7843f43cd"} +{"status":"Extracting","progressDetail":{"current":33423360,"total":88111129},"progress":"[==================\u003e ] 33.42MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":51204,"total":5015856},"progress":"[\u003e ] 51.2kB/5.016MB","id":"a89dbf94d794"} +{"status":"Extracting","progressDetail":{"current":36208640,"total":88111129},"progress":"[====================\u003e ] 36.21MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":1624816,"total":5015856},"progress":"[================\u003e ] 1.625MB/5.016MB","id":"a89dbf94d794"} +{"status":"Extracting","progressDetail":{"current":38436864,"total":88111129},"progress":"[=====================\u003e ] 38.44MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":3373808,"total":5015856},"progress":"[=================================\u003e ] 3.374MB/5.016MB","id":"a89dbf94d794"} +{"status":"Extracting","progressDetail":{"current":40665088,"total":88111129},"progress":"[=======================\u003e ] 40.67MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":53910,"total":5310566},"progress":"[\u003e ] 53.91kB/5.311MB","id":"f0d43ddca77f"} +{"status":"Downloading","progressDetail":{"current":4905712,"total":5015856},"progress":"[================================================\u003e ] 4.906MB/5.016MB","id":"a89dbf94d794"} +{"status":"Verifying Checksum","progressDetail":{},"id":"a89dbf94d794"} +{"status":"Download complete","progressDetail":{},"id":"a89dbf94d794"} +{"status":"Downloading","progressDetail":{"current":1313521,"total":5310566},"progress":"[============\u003e ] 1.314MB/5.311MB","id":"f0d43ddca77f"} +{"status":"Extracting","progressDetail":{"current":44007424,"total":88111129},"progress":"[========================\u003e ] 44.01MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Extracting","progressDetail":{"current":46792704,"total":88111129},"progress":"[==========================\u003e ] 46.79MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":3082993,"total":5310566},"progress":"[=============================\u003e ] 3.083MB/5.311MB","id":"f0d43ddca77f"} +{"status":"Downloading","progressDetail":{"current":49836,"total":4915049},"progress":"[\u003e ] 49.84kB/4.915MB","id":"7c674f0cb40c"} +{"status":"Downloading","progressDetail":{"current":4373233,"total":5310566},"progress":"[=========================================\u003e ] 4.373MB/5.311MB","id":"f0d43ddca77f"} +{"status":"Extracting","progressDetail":{"current":48463872,"total":88111129},"progress":"[===========================\u003e ] 48.46MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":711407,"total":4915049},"progress":"[=======\u003e ] 711.4kB/4.915MB","id":"7c674f0cb40c"} +{"status":"Verifying Checksum","progressDetail":{},"id":"f0d43ddca77f"} +{"status":"Download complete","progressDetail":{},"id":"f0d43ddca77f"} +{"status":"Downloading","progressDetail":{"current":1710831,"total":4915049},"progress":"[=================\u003e ] 1.711MB/4.915MB","id":"7c674f0cb40c"} +{"status":"Extracting","progressDetail":{"current":52363264,"total":88111129},"progress":"[=============================\u003e ] 52.36MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":3504879,"total":4915049},"progress":"[===================================\u003e ] 3.505MB/4.915MB","id":"7c674f0cb40c"} +{"status":"Extracting","progressDetail":{"current":55705600,"total":88111129},"progress":"[===============================\u003e ] 55.71MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":4905711,"total":4915049},"progress":"[=================================================\u003e ] 4.906MB/4.915MB","id":"7c674f0cb40c"} +{"status":"Verifying Checksum","progressDetail":{},"id":"7c674f0cb40c"} +{"status":"Download complete","progressDetail":{},"id":"7c674f0cb40c"} +{"status":"Extracting","progressDetail":{"current":58490880,"total":88111129},"progress":"[=================================\u003e ] 58.49MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":52419,"total":5119213},"progress":"[\u003e ] 52.42kB/5.119MB","id":"b48a885b52bc"} +{"status":"Extracting","progressDetail":{"current":61276160,"total":88111129},"progress":"[==================================\u003e ] 61.28MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":1333999,"total":5119213},"progress":"[=============\u003e ] 1.334MB/5.119MB","id":"b48a885b52bc"} +{"status":"Downloading","progressDetail":{"current":2657007,"total":5119213},"progress":"[=========================\u003e ] 2.657MB/5.119MB","id":"b48a885b52bc"} +{"status":"Extracting","progressDetail":{"current":64061440,"total":88111129},"progress":"[====================================\u003e ] 64.06MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Download complete","progressDetail":{},"id":"272cdf839cbb"} +{"status":"Downloading","progressDetail":{"current":4344559,"total":5119213},"progress":"[==========================================\u003e ] 4.345MB/5.119MB","id":"b48a885b52bc"} +{"status":"Extracting","progressDetail":{"current":66289664,"total":88111129},"progress":"[=====================================\u003e ] 66.29MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Verifying Checksum","progressDetail":{},"id":"b48a885b52bc"} +{"status":"Download complete","progressDetail":{},"id":"b48a885b52bc"} +{"status":"Extracting","progressDetail":{"current":70746112,"total":88111129},"progress":"[========================================\u003e ] 70.75MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Extracting","progressDetail":{"current":73531392,"total":88111129},"progress":"[=========================================\u003e ] 73.53MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Extracting","progressDetail":{"current":77430784,"total":88111129},"progress":"[===========================================\u003e ] 77.43MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Download complete","progressDetail":{},"id":"50d054c97f4f"} +{"status":"Downloading","progressDetail":{"current":488,"total":1069},"progress":"[======================\u003e ] 488B/1.069kB","id":"4c6bbd90b64d"} +{"status":"Extracting","progressDetail":{"current":80216064,"total":88111129},"progress":"[=============================================\u003e ] 80.22MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":1069,"total":1069},"progress":"[==================================================\u003e] 1.069kB/1.069kB","id":"4c6bbd90b64d"} +{"status":"Verifying Checksum","progressDetail":{},"id":"4c6bbd90b64d"} +{"status":"Download complete","progressDetail":{},"id":"4c6bbd90b64d"} +{"status":"Downloading","progressDetail":{"current":32,"total":32},"progress":"[==================================================\u003e] 32B/32B","id":"4f4fb700ef54"} +{"status":"Verifying Checksum","progressDetail":{},"id":"4f4fb700ef54"} +{"status":"Download complete","progressDetail":{},"id":"4f4fb700ef54"} +{"status":"Extracting","progressDetail":{"current":81887232,"total":88111129},"progress":"[==============================================\u003e ] 81.89MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Extracting","progressDetail":{"current":83558400,"total":88111129},"progress":"[===============================================\u003e ] 83.56MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Extracting","progressDetail":{"current":85229568,"total":88111129},"progress":"[================================================\u003e ] 85.23MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Extracting","progressDetail":{"current":86900736,"total":88111129},"progress":"[=================================================\u003e ] 86.9MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Extracting","progressDetail":{"current":88014848,"total":88111129},"progress":"[=================================================\u003e ] 88.01MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Extracting","progressDetail":{"current":88111129,"total":88111129},"progress":"[==================================================\u003e] 88.11MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Pull complete","progressDetail":{},"id":"4ab897fa6fbf"} +{"status":"Extracting","progressDetail":{"current":32768,"total":1391657},"progress":"[=\u003e ] 32.77kB/1.392MB","id":"d837a2a1365e"} +{"status":"Extracting","progressDetail":{"current":327680,"total":1391657},"progress":"[===========\u003e ] 327.7kB/1.392MB","id":"d837a2a1365e"} +{"status":"Extracting","progressDetail":{"current":1391657,"total":1391657},"progress":"[==================================================\u003e] 1.392MB/1.392MB","id":"d837a2a1365e"} +{"status":"Extracting","progressDetail":{"current":1391657,"total":1391657},"progress":"[==================================================\u003e] 1.392MB/1.392MB","id":"d837a2a1365e"} +{"status":"Pull complete","progressDetail":{},"id":"d837a2a1365e"} +{"status":"Extracting","progressDetail":{"current":32768,"total":1328346},"progress":"[=\u003e ] 32.77kB/1.328MB","id":"988ae18fe41a"} +{"status":"Extracting","progressDetail":{"current":753664,"total":1328346},"progress":"[============================\u003e ] 753.7kB/1.328MB","id":"988ae18fe41a"} +{"status":"Extracting","progressDetail":{"current":1328346,"total":1328346},"progress":"[==================================================\u003e] 1.328MB/1.328MB","id":"988ae18fe41a"} +{"status":"Extracting","progressDetail":{"current":1328346,"total":1328346},"progress":"[==================================================\u003e] 1.328MB/1.328MB","id":"988ae18fe41a"} +{"status":"Pull complete","progressDetail":{},"id":"988ae18fe41a"} +{"status":"Extracting","progressDetail":{"current":4478,"total":4478},"progress":"[==================================================\u003e] 4.478kB/4.478kB","id":"eeb8ef83b565"} +{"status":"Extracting","progressDetail":{"current":4478,"total":4478},"progress":"[==================================================\u003e] 4.478kB/4.478kB","id":"eeb8ef83b565"} +{"status":"Pull complete","progressDetail":{},"id":"eeb8ef83b565"} +{"status":"Extracting","progressDetail":{"current":197,"total":197},"progress":"[==================================================\u003e] 197B/197B","id":"357fefdf9bc9"} +{"status":"Extracting","progressDetail":{"current":197,"total":197},"progress":"[==================================================\u003e] 197B/197B","id":"357fefdf9bc9"} +{"status":"Pull complete","progressDetail":{},"id":"357fefdf9bc9"} +{"status":"Extracting","progressDetail":{"current":294912,"total":27504647},"progress":"[\u003e ] 294.9kB/27.5MB","id":"45b746196f82"} +{"status":"Extracting","progressDetail":{"current":589824,"total":27504647},"progress":"[=\u003e ] 589.8kB/27.5MB","id":"45b746196f82"} +{"status":"Extracting","progressDetail":{"current":5013504,"total":27504647},"progress":"[=========\u003e ] 5.014MB/27.5MB","id":"45b746196f82"} +{"status":"Extracting","progressDetail":{"current":9142272,"total":27504647},"progress":"[================\u003e ] 9.142MB/27.5MB","id":"45b746196f82"} +{"status":"Extracting","progressDetail":{"current":13565952,"total":27504647},"progress":"[========================\u003e ] 13.57MB/27.5MB","id":"45b746196f82"} +{"status":"Extracting","progressDetail":{"current":16515072,"total":27504647},"progress":"[==============================\u003e ] 16.52MB/27.5MB","id":"45b746196f82"} +{"status":"Extracting","progressDetail":{"current":18579456,"total":27504647},"progress":"[=================================\u003e ] 18.58MB/27.5MB","id":"45b746196f82"} +{"status":"Extracting","progressDetail":{"current":21528576,"total":27504647},"progress":"[=======================================\u003e ] 21.53MB/27.5MB","id":"45b746196f82"} +{"status":"Extracting","progressDetail":{"current":25657344,"total":27504647},"progress":"[==============================================\u003e ] 25.66MB/27.5MB","id":"45b746196f82"} +{"status":"Extracting","progressDetail":{"current":27504647,"total":27504647},"progress":"[==================================================\u003e] 27.5MB/27.5MB","id":"45b746196f82"} +{"status":"Pull complete","progressDetail":{},"id":"45b746196f82"} +{"status":"Extracting","progressDetail":{"current":65536,"total":5205016},"progress":"[\u003e ] 65.54kB/5.205MB","id":"fbf4ce20f8c2"} +{"status":"Extracting","progressDetail":{"current":1048576,"total":5205016},"progress":"[==========\u003e ] 1.049MB/5.205MB","id":"fbf4ce20f8c2"} +{"status":"Extracting","progressDetail":{"current":5205016,"total":5205016},"progress":"[==================================================\u003e] 5.205MB/5.205MB","id":"fbf4ce20f8c2"} +{"status":"Pull complete","progressDetail":{},"id":"fbf4ce20f8c2"} +{"status":"Extracting","progressDetail":{"current":65536,"total":4964709},"progress":"[\u003e ] 65.54kB/4.965MB","id":"90aca3c647fe"} +{"status":"Extracting","progressDetail":{"current":1245184,"total":4964709},"progress":"[============\u003e ] 1.245MB/4.965MB","id":"90aca3c647fe"} +{"status":"Extracting","progressDetail":{"current":4964709,"total":4964709},"progress":"[==================================================\u003e] 4.965MB/4.965MB","id":"90aca3c647fe"} +{"status":"Pull complete","progressDetail":{},"id":"90aca3c647fe"} +{"status":"Extracting","progressDetail":{"current":65536,"total":5149051},"progress":"[\u003e ] 65.54kB/5.149MB","id":"1dd62f37c84c"} +{"status":"Extracting","progressDetail":{"current":393216,"total":5149051},"progress":"[===\u003e ] 393.2kB/5.149MB","id":"1dd62f37c84c"} +{"status":"Extracting","progressDetail":{"current":5149051,"total":5149051},"progress":"[==================================================\u003e] 5.149MB/5.149MB","id":"1dd62f37c84c"} +{"status":"Pull complete","progressDetail":{},"id":"1dd62f37c84c"} +{"status":"Extracting","progressDetail":{"current":65536,"total":3855277},"progress":"[\u003e ] 65.54kB/3.855MB","id":"3192b2fa42db"} +{"status":"Extracting","progressDetail":{"current":851968,"total":3855277},"progress":"[===========\u003e ] 852kB/3.855MB","id":"3192b2fa42db"} +{"status":"Extracting","progressDetail":{"current":3855277,"total":3855277},"progress":"[==================================================\u003e] 3.855MB/3.855MB","id":"3192b2fa42db"} +{"status":"Extracting","progressDetail":{"current":3855277,"total":3855277},"progress":"[==================================================\u003e] 3.855MB/3.855MB","id":"3192b2fa42db"} +{"status":"Pull complete","progressDetail":{},"id":"3192b2fa42db"} +{"status":"Extracting","progressDetail":{"current":65536,"total":4983195},"progress":"[\u003e ] 65.54kB/4.983MB","id":"ae190b8f66a7"} +{"status":"Extracting","progressDetail":{"current":327680,"total":4983195},"progress":"[===\u003e ] 327.7kB/4.983MB","id":"ae190b8f66a7"} +{"status":"Extracting","progressDetail":{"current":4980736,"total":4983195},"progress":"[=================================================\u003e ] 4.981MB/4.983MB","id":"ae190b8f66a7"} +{"status":"Extracting","progressDetail":{"current":4983195,"total":4983195},"progress":"[==================================================\u003e] 4.983MB/4.983MB","id":"ae190b8f66a7"} +{"status":"Pull complete","progressDetail":{},"id":"ae190b8f66a7"} +{"status":"Extracting","progressDetail":{"current":65536,"total":6103207},"progress":"[\u003e ] 65.54kB/6.103MB","id":"97bb6e138460"} +{"status":"Extracting","progressDetail":{"current":327680,"total":6103207},"progress":"[==\u003e ] 327.7kB/6.103MB","id":"97bb6e138460"} +{"status":"Extracting","progressDetail":{"current":3670016,"total":6103207},"progress":"[==============================\u003e ] 3.67MB/6.103MB","id":"97bb6e138460"} +{"status":"Extracting","progressDetail":{"current":6103207,"total":6103207},"progress":"[==================================================\u003e] 6.103MB/6.103MB","id":"97bb6e138460"} +{"status":"Pull complete","progressDetail":{},"id":"97bb6e138460"} +{"status":"Extracting","progressDetail":{"current":787,"total":787},"progress":"[==================================================\u003e] 787B/787B","id":"2edb982d5170"} +{"status":"Extracting","progressDetail":{"current":787,"total":787},"progress":"[==================================================\u003e] 787B/787B","id":"2edb982d5170"} +{"status":"Pull complete","progressDetail":{},"id":"2edb982d5170"} +{"status":"Extracting","progressDetail":{"current":65536,"total":4894860},"progress":"[\u003e ] 65.54kB/4.895MB","id":"7ddc8e6d6da9"} +{"status":"Extracting","progressDetail":{"current":327680,"total":4894860},"progress":"[===\u003e ] 327.7kB/4.895MB","id":"7ddc8e6d6da9"} +{"status":"Extracting","progressDetail":{"current":3735552,"total":4894860},"progress":"[======================================\u003e ] 3.736MB/4.895MB","id":"7ddc8e6d6da9"} +{"status":"Extracting","progressDetail":{"current":4894860,"total":4894860},"progress":"[==================================================\u003e] 4.895MB/4.895MB","id":"7ddc8e6d6da9"} +{"status":"Pull complete","progressDetail":{},"id":"7ddc8e6d6da9"} +{"status":"Extracting","progressDetail":{"current":65536,"total":4953791},"progress":"[\u003e ] 65.54kB/4.954MB","id":"0df6fd234b59"} +{"status":"Extracting","progressDetail":{"current":327680,"total":4953791},"progress":"[===\u003e ] 327.7kB/4.954MB","id":"0df6fd234b59"} +{"status":"Extracting","progressDetail":{"current":4325376,"total":4953791},"progress":"[===========================================\u003e ] 4.325MB/4.954MB","id":"0df6fd234b59"} +{"status":"Extracting","progressDetail":{"current":4953791,"total":4953791},"progress":"[==================================================\u003e] 4.954MB/4.954MB","id":"0df6fd234b59"} +{"status":"Pull complete","progressDetail":{},"id":"0df6fd234b59"} +{"status":"Extracting","progressDetail":{"current":65536,"total":6137526},"progress":"[\u003e ] 65.54kB/6.138MB","id":"8fc1ba8efe21"} +{"status":"Extracting","progressDetail":{"current":327680,"total":6137526},"progress":"[==\u003e ] 327.7kB/6.138MB","id":"8fc1ba8efe21"} +{"status":"Extracting","progressDetail":{"current":3801088,"total":6137526},"progress":"[==============================\u003e ] 3.801MB/6.138MB","id":"8fc1ba8efe21"} +{"status":"Extracting","progressDetail":{"current":6137526,"total":6137526},"progress":"[==================================================\u003e] 6.138MB/6.138MB","id":"8fc1ba8efe21"} +{"status":"Pull complete","progressDetail":{},"id":"8fc1ba8efe21"} +{"status":"Extracting","progressDetail":{"current":65536,"total":3854415},"progress":"[\u003e ] 65.54kB/3.854MB","id":"1f6f45e783b5"} +{"status":"Extracting","progressDetail":{"current":851968,"total":3854415},"progress":"[===========\u003e ] 852kB/3.854MB","id":"1f6f45e783b5"} +{"status":"Extracting","progressDetail":{"current":3854415,"total":3854415},"progress":"[==================================================\u003e] 3.854MB/3.854MB","id":"1f6f45e783b5"} +{"status":"Extracting","progressDetail":{"current":3854415,"total":3854415},"progress":"[==================================================\u003e] 3.854MB/3.854MB","id":"1f6f45e783b5"} +{"status":"Pull complete","progressDetail":{},"id":"1f6f45e783b5"} +{"status":"Extracting","progressDetail":{"current":65536,"total":5222290},"progress":"[\u003e ] 65.54kB/5.222MB","id":"43ea61082f68"} +{"status":"Extracting","progressDetail":{"current":458752,"total":5222290},"progress":"[====\u003e ] 458.8kB/5.222MB","id":"43ea61082f68"} +{"status":"Extracting","progressDetail":{"current":4849664,"total":5222290},"progress":"[==============================================\u003e ] 4.85MB/5.222MB","id":"43ea61082f68"} +{"status":"Extracting","progressDetail":{"current":5222290,"total":5222290},"progress":"[==================================================\u003e] 5.222MB/5.222MB","id":"43ea61082f68"} +{"status":"Pull complete","progressDetail":{},"id":"43ea61082f68"} +{"status":"Extracting","progressDetail":{"current":65536,"total":3564359},"progress":"[\u003e ] 65.54kB/3.564MB","id":"b8cf53bbc6ba"} +{"status":"Extracting","progressDetail":{"current":327680,"total":3564359},"progress":"[====\u003e ] 327.7kB/3.564MB","id":"b8cf53bbc6ba"} +{"status":"Extracting","progressDetail":{"current":3564359,"total":3564359},"progress":"[==================================================\u003e] 3.564MB/3.564MB","id":"b8cf53bbc6ba"} +{"status":"Pull complete","progressDetail":{},"id":"b8cf53bbc6ba"} +{"status":"Extracting","progressDetail":{"current":790,"total":790},"progress":"[==================================================\u003e] 790B/790B","id":"25efb07e4521"} +{"status":"Extracting","progressDetail":{"current":790,"total":790},"progress":"[==================================================\u003e] 790B/790B","id":"25efb07e4521"} +{"status":"Pull complete","progressDetail":{},"id":"25efb07e4521"} +{"status":"Extracting","progressDetail":{"current":65536,"total":5120108},"progress":"[\u003e ] 65.54kB/5.12MB","id":"1c3245356213"} +{"status":"Extracting","progressDetail":{"current":327680,"total":5120108},"progress":"[===\u003e ] 327.7kB/5.12MB","id":"1c3245356213"} +{"status":"Extracting","progressDetail":{"current":5111808,"total":5120108},"progress":"[=================================================\u003e ] 5.112MB/5.12MB","id":"1c3245356213"} +{"status":"Extracting","progressDetail":{"current":5120108,"total":5120108},"progress":"[==================================================\u003e] 5.12MB/5.12MB","id":"1c3245356213"} +{"status":"Pull complete","progressDetail":{},"id":"1c3245356213"} +{"status":"Extracting","progressDetail":{"current":65536,"total":5117023},"progress":"[\u003e ] 65.54kB/5.117MB","id":"61ebb123c1eb"} +{"status":"Extracting","progressDetail":{"current":655360,"total":5117023},"progress":"[======\u003e ] 655.4kB/5.117MB","id":"61ebb123c1eb"} +{"status":"Extracting","progressDetail":{"current":4259840,"total":5117023},"progress":"[=========================================\u003e ] 4.26MB/5.117MB","id":"61ebb123c1eb"} +{"status":"Extracting","progressDetail":{"current":5117023,"total":5117023},"progress":"[==================================================\u003e] 5.117MB/5.117MB","id":"61ebb123c1eb"} +{"status":"Pull complete","progressDetail":{},"id":"61ebb123c1eb"} +{"status":"Extracting","progressDetail":{"current":65536,"total":5384215},"progress":"[\u003e ] 65.54kB/5.384MB","id":"0964b769d2c9"} +{"status":"Extracting","progressDetail":{"current":327680,"total":5384215},"progress":"[===\u003e ] 327.7kB/5.384MB","id":"0964b769d2c9"} +{"status":"Extracting","progressDetail":{"current":5177344,"total":5384215},"progress":"[================================================\u003e ] 5.177MB/5.384MB","id":"0964b769d2c9"} +{"status":"Extracting","progressDetail":{"current":5384215,"total":5384215},"progress":"[==================================================\u003e] 5.384MB/5.384MB","id":"0964b769d2c9"} +{"status":"Pull complete","progressDetail":{},"id":"0964b769d2c9"} +{"status":"Extracting","progressDetail":{"current":65536,"total":5252487},"progress":"[\u003e ] 65.54kB/5.252MB","id":"87f7843f43cd"} +{"status":"Extracting","progressDetail":{"current":655360,"total":5252487},"progress":"[======\u003e ] 655.4kB/5.252MB","id":"87f7843f43cd"} +{"status":"Extracting","progressDetail":{"current":5252487,"total":5252487},"progress":"[==================================================\u003e] 5.252MB/5.252MB","id":"87f7843f43cd"} +{"status":"Pull complete","progressDetail":{},"id":"87f7843f43cd"} +{"status":"Extracting","progressDetail":{"current":65536,"total":5015856},"progress":"[\u003e ] 65.54kB/5.016MB","id":"a89dbf94d794"} +{"status":"Extracting","progressDetail":{"current":327680,"total":5015856},"progress":"[===\u003e ] 327.7kB/5.016MB","id":"a89dbf94d794"} +{"status":"Extracting","progressDetail":{"current":3997696,"total":5015856},"progress":"[=======================================\u003e ] 3.998MB/5.016MB","id":"a89dbf94d794"} +{"status":"Extracting","progressDetail":{"current":5015856,"total":5015856},"progress":"[==================================================\u003e] 5.016MB/5.016MB","id":"a89dbf94d794"} +{"status":"Pull complete","progressDetail":{},"id":"a89dbf94d794"} +{"status":"Extracting","progressDetail":{"current":65536,"total":5310566},"progress":"[\u003e ] 65.54kB/5.311MB","id":"f0d43ddca77f"} +{"status":"Extracting","progressDetail":{"current":393216,"total":5310566},"progress":"[===\u003e ] 393.2kB/5.311MB","id":"f0d43ddca77f"} +{"status":"Extracting","progressDetail":{"current":3407872,"total":5310566},"progress":"[================================\u003e ] 3.408MB/5.311MB","id":"f0d43ddca77f"} +{"status":"Extracting","progressDetail":{"current":5310566,"total":5310566},"progress":"[==================================================\u003e] 5.311MB/5.311MB","id":"f0d43ddca77f"} +{"status":"Pull complete","progressDetail":{},"id":"f0d43ddca77f"} +{"status":"Extracting","progressDetail":{"current":65536,"total":4915049},"progress":"[\u003e ] 65.54kB/4.915MB","id":"7c674f0cb40c"} +{"status":"Extracting","progressDetail":{"current":786432,"total":4915049},"progress":"[========\u003e ] 786.4kB/4.915MB","id":"7c674f0cb40c"} +{"status":"Extracting","progressDetail":{"current":4915049,"total":4915049},"progress":"[==================================================\u003e] 4.915MB/4.915MB","id":"7c674f0cb40c"} +{"status":"Extracting","progressDetail":{"current":4915049,"total":4915049},"progress":"[==================================================\u003e] 4.915MB/4.915MB","id":"7c674f0cb40c"} +{"status":"Pull complete","progressDetail":{},"id":"7c674f0cb40c"} +{"status":"Extracting","progressDetail":{"current":65536,"total":5119213},"progress":"[\u003e ] 65.54kB/5.119MB","id":"b48a885b52bc"} +{"status":"Extracting","progressDetail":{"current":327680,"total":5119213},"progress":"[===\u003e ] 327.7kB/5.119MB","id":"b48a885b52bc"} +{"status":"Extracting","progressDetail":{"current":4390912,"total":5119213},"progress":"[==========================================\u003e ] 4.391MB/5.119MB","id":"b48a885b52bc"} +{"status":"Extracting","progressDetail":{"current":5119213,"total":5119213},"progress":"[==================================================\u003e] 5.119MB/5.119MB","id":"b48a885b52bc"} +{"status":"Pull complete","progressDetail":{},"id":"b48a885b52bc"} +{"status":"Extracting","progressDetail":{"current":395,"total":395},"progress":"[==================================================\u003e] 395B/395B","id":"272cdf839cbb"} +{"status":"Extracting","progressDetail":{"current":395,"total":395},"progress":"[==================================================\u003e] 395B/395B","id":"272cdf839cbb"} +{"status":"Pull complete","progressDetail":{},"id":"272cdf839cbb"} +{"status":"Extracting","progressDetail":{"current":155,"total":155},"progress":"[==================================================\u003e] 155B/155B","id":"50d054c97f4f"} +{"status":"Extracting","progressDetail":{"current":155,"total":155},"progress":"[==================================================\u003e] 155B/155B","id":"50d054c97f4f"} +{"status":"Pull complete","progressDetail":{},"id":"50d054c97f4f"} +{"status":"Extracting","progressDetail":{"current":1069,"total":1069},"progress":"[==================================================\u003e] 1.069kB/1.069kB","id":"4c6bbd90b64d"} +{"status":"Extracting","progressDetail":{"current":1069,"total":1069},"progress":"[==================================================\u003e] 1.069kB/1.069kB","id":"4c6bbd90b64d"} +{"status":"Pull complete","progressDetail":{},"id":"4c6bbd90b64d"} +{"status":"Extracting","progressDetail":{"current":32,"total":32},"progress":"[==================================================\u003e] 32B/32B","id":"4f4fb700ef54"} +{"status":"Extracting","progressDetail":{"current":32,"total":32},"progress":"[==================================================\u003e] 32B/32B","id":"4f4fb700ef54"} +{"status":"Pull complete","progressDetail":{},"id":"4f4fb700ef54"} +{"status":"Digest: sha256:4acb6bfd6c4f0cabaf7f3690e444afe51f1c7de54d51da7e63fac709c56f1c30"} +{"status":"Status: Downloaded newer image for paketo-buildpacks/cnb:base"} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/pull-update-full.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/pull-update-full.json new file mode 100644 index 000000000000..5f72bb0a352f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/pull-update-full.json @@ -0,0 +1,9 @@ +{ + "status": "Extracting", + "progressDetail": { + "current": 16, + "total": 32 + }, + "progress": "[==================================================\u003e] 32B/32B", + "id": "4f4fb700ef54" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/pull-update-minimal.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/pull-update-minimal.json new file mode 100644 index 000000000000..e897de7faff9 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/pull-update-minimal.json @@ -0,0 +1,3 @@ +{ + "status": "Status: Downloaded newer image for paketo-buildpacks/cnb:base" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/pull-with-empty-details.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/pull-with-empty-details.json new file mode 100644 index 000000000000..c7b6075e6cde --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/pull-with-empty-details.json @@ -0,0 +1,6 @@ +{ + "status": "Pulling fs layer", + "progressDetail": { + }, + "id": "d837a2a1365e" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/push-stream-with-error.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/push-stream-with-error.json new file mode 100644 index 000000000000..30ace62eedd4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/push-stream-with-error.json @@ -0,0 +1,7 @@ +{ + "status":"The push refers to repository [localhost:5000/ubuntu]" +} +{"status":"Preparing","progressDetail":{},"id":"782f5f011dda"} +{"status":"Preparing","progressDetail":{},"id":"90ac32a0d9ab"} +{"status":"Preparing","progressDetail":{},"id":"d42a4fdf4b2a"} +{"errorDetail":{"message":"test message"},"error":"test error"} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/push-stream.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/push-stream.json new file mode 100644 index 000000000000..2f9acafca7c0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/push-stream.json @@ -0,0 +1,46 @@ +{ + "status":"The push refers to repository [localhost:5000/ubuntu]" +} +{"status":"Preparing","progressDetail":{},"id":"782f5f011dda"} +{"status":"Preparing","progressDetail":{},"id":"90ac32a0d9ab"} +{"status":"Preparing","progressDetail":{},"id":"d42a4fdf4b2a"} +{"status":"Pushing","progressDetail":{"current":512,"total":7},"progress":"[==================================================\u003e] 512B","id":"782f5f011dda"} +{"status":"Pushing","progressDetail":{"current":512,"total":811},"progress":"[===============================\u003e ] 512B/811B","id":"90ac32a0d9ab"} +{"status":"Pushing","progressDetail":{"current":3072,"total":7},"progress":"[==================================================\u003e] 3.072kB","id":"782f5f011dda"} +{"status":"Pushing","progressDetail":{"current":15360,"total":811},"progress":"[==================================================\u003e] 15.36kB","id":"90ac32a0d9ab"} +{"status":"Pushing","progressDetail":{"current":543232,"total":72874905},"progress":"[\u003e ] 543.2kB/72.87MB","id":"d42a4fdf4b2a"} +{"status":"Pushed","progressDetail":{},"id":"90ac32a0d9ab"} +{"status":"Pushed","progressDetail":{},"id":"782f5f011dda"} +{"status":"Pushing","progressDetail":{"current":2713600,"total":72874905},"progress":"[=\u003e ] 2.714MB/72.87MB","id":"d42a4fdf4b2a"} +{"status":"Pushing","progressDetail":{"current":4870656,"total":72874905},"progress":"[===\u003e ] 4.871MB/72.87MB","id":"d42a4fdf4b2a"} +{"status":"Pushing","progressDetail":{"current":7069184,"total":72874905},"progress":"[====\u003e ] 7.069MB/72.87MB","id":"d42a4fdf4b2a"} +{"status":"Pushing","progressDetail":{"current":9238528,"total":72874905},"progress":"[======\u003e ] 9.239MB/72.87MB","id":"d42a4fdf4b2a"} +{"status":"Pushing","progressDetail":{"current":11354112,"total":72874905},"progress":"[=======\u003e ] 11.35MB/72.87MB","id":"d42a4fdf4b2a"} +{"status":"Pushing","progressDetail":{"current":13582336,"total":72874905},"progress":"[=========\u003e ] 13.58MB/72.87MB","id":"d42a4fdf4b2a"} +{"status":"Pushing","progressDetail":{"current":16336248,"total":72874905},"progress":"[===========\u003e ] 16.34MB/72.87MB","id":"d42a4fdf4b2a"} +{"status":"Pushing","progressDetail":{"current":19036160,"total":72874905},"progress":"[=============\u003e ] 19.04MB/72.87MB","id":"d42a4fdf4b2a"} +{"status":"Pushing","progressDetail":{"current":21762560,"total":72874905},"progress":"[==============\u003e ] 21.76MB/72.87MB","id":"d42a4fdf4b2a"} +{"status":"Pushing","progressDetail":{"current":24480256,"total":72874905},"progress":"[================\u003e ] 24.48MB/72.87MB","id":"d42a4fdf4b2a"} +{"status":"Pushing","progressDetail":{"current":28756480,"total":72874905},"progress":"[===================\u003e ] 28.76MB/72.87MB","id":"d42a4fdf4b2a"} +{"status":"Pushing","progressDetail":{"current":32001024,"total":72874905},"progress":"[=====================\u003e ] 32MB/72.87MB","id":"d42a4fdf4b2a"} +{"status":"Pushing","progressDetail":{"current":34195456,"total":72874905},"progress":"[=======================\u003e ] 34.2MB/72.87MB","id":"d42a4fdf4b2a"} +{"status":"Pushing","progressDetail":{"current":36393984,"total":72874905},"progress":"[========================\u003e ] 36.39MB/72.87MB","id":"d42a4fdf4b2a"} +{"status":"Pushing","progressDetail":{"current":38587904,"total":72874905},"progress":"[==========================\u003e ] 38.59MB/72.87MB","id":"d42a4fdf4b2a"} +{"status":"Pushing","progressDetail":{"current":41290752,"total":72874905},"progress":"[============================\u003e ] 41.29MB/72.87MB","id":"d42a4fdf4b2a"} +{"status":"Pushing","progressDetail":{"current":43487744,"total":72874905},"progress":"[=============================\u003e ] 43.49MB/72.87MB","id":"d42a4fdf4b2a"} +{"status":"Pushing","progressDetail":{"current":45683200,"total":72874905},"progress":"[===============================\u003e ] 45.68MB/72.87MB","id":"d42a4fdf4b2a"} +{"status":"Pushing","progressDetail":{"current":48413184,"total":72874905},"progress":"[=================================\u003e ] 48.41MB/72.87MB","id":"d42a4fdf4b2a"} +{"status":"Pushing","progressDetail":{"current":51119104,"total":72874905},"progress":"[===================================\u003e ] 51.12MB/72.87MB","id":"d42a4fdf4b2a"} +{"status":"Pushing","progressDetail":{"current":53327360,"total":72874905},"progress":"[====================================\u003e ] 53.33MB/72.87MB","id":"d42a4fdf4b2a"} +{"status":"Pushing","progressDetail":{"current":54964224,"total":72874905},"progress":"[=====================================\u003e ] 54.96MB/72.87MB","id":"d42a4fdf4b2a"} +{"status":"Pushing","progressDetail":{"current":57169408,"total":72874905},"progress":"[=======================================\u003e ] 57.17MB/72.87MB","id":"d42a4fdf4b2a"} +{"status":"Pushing","progressDetail":{"current":59355825,"total":72874905},"progress":"[========================================\u003e ] 59.36MB/72.87MB","id":"d42a4fdf4b2a"} +{"status":"Pushing","progressDetail":{"current":62002592,"total":72874905},"progress":"[==========================================\u003e ] 62MB/72.87MB","id":"d42a4fdf4b2a"} +{"status":"Pushing","progressDetail":{"current":64700928,"total":72874905},"progress":"[============================================\u003e ] 64.7MB/72.87MB","id":"d42a4fdf4b2a"} +{"status":"Pushing","progressDetail":{"current":67435688,"total":72874905},"progress":"[==============================================\u003e ] 67.44MB/72.87MB","id":"d42a4fdf4b2a"} +{"status":"Pushing","progressDetail":{"current":70095743,"total":72874905},"progress":"[================================================\u003e ] 70.1MB/72.87MB","id":"d42a4fdf4b2a"} +{"status":"Pushing","progressDetail":{"current":72823808,"total":72874905},"progress":"[=================================================\u003e ] 72.82MB/72.87MB","id":"d42a4fdf4b2a"} +{"status":"Pushing","progressDetail":{"current":75247104,"total":72874905},"progress":"[==================================================\u003e] 75.25MB","id":"d42a4fdf4b2a"} +{"status":"Pushed","progressDetail":{},"id":"d42a4fdf4b2a"} +{"status":"latest: digest: sha256:2e70e9c81838224b5311970dbf7ed16802fbfe19e7a70b3cbfa3d7522aa285b4 size: 943"} +{"progressDetail":{},"aux":{"Tag":"latest","Digest":"sha256:2e70e9c81838224b5311970dbf7ed16802fbfe19e7a70b3cbfa3d7522aa285b4","Size":943}} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/transport/errors.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/transport/errors.json new file mode 100644 index 000000000000..f8b04fefcc4d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/transport/errors.json @@ -0,0 +1,14 @@ +{ + "errors": [ + { + "code": "TEST1", + "message": "Test One", + "detail": 123 + }, + { + "code": "TEST2", + "message": "Test Two", + "detail": "fail" + } + ] +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/transport/message.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/transport/message.json new file mode 100644 index 000000000000..59580d061236 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/transport/message.json @@ -0,0 +1,3 @@ +{ + "message": "test message" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/container-config.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/container-config.json new file mode 100644 index 000000000000..2c034b776712 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/container-config.json @@ -0,0 +1,21 @@ +{ + "User": "root", + "Image": "docker.io/library/ubuntu:bionic", + "Cmd": [ + "ls", + "-l", + "-h" + ], + "Env": [ + "name1=value1", + "name2=value2" + ], + "Labels": { + "spring": "boot" + }, + "HostConfig": { + "Binds": [ + "bind-source:bind-dest" + ] + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/container-status-error.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/container-status-error.json new file mode 100644 index 000000000000..3e81ae903fc5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/container-status-error.json @@ -0,0 +1,6 @@ +{ + "StatusCode": 1, + "Error": { + "Message": "error detail" + } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/container-status-success.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/container-status-success.json new file mode 100644 index 000000000000..ad2069b286f1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/container-status-success.json @@ -0,0 +1,3 @@ +{ + "StatusCode": 0 +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-archive-config.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-archive-config.json new file mode 100644 index 000000000000..395bbcda70b1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-archive-config.json @@ -0,0 +1,224 @@ +{ + "config": { + "Hostname": "", + "Domainname": "", + "User": "vcap", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "CNB_USER_ID=2000", + "CNB_GROUP_ID=2000", + "CNB_STACK_ID=org.cloudfoundry.stacks.cflinuxfs3" + ], + "Cmd": null, + "Image": "sha256:523c8ade6e06f814469b2cf04c8045a74becee17088955f2657958476d3fba1f", + "Volumes": null, + "WorkingDir": "/layers", + "Entrypoint": null, + "OnBuild": null, + "Labels": { + "io.buildpacks.stack.id": "org.cloudfoundry.stacks.cflinuxfs3" + } + }, + "created": "1980-01-01T00:00:01Z", + "history": [ + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + }, + { + + } + ], + "os": "linux", + "rootfs": { + "diff_ids": [ + "sha256:733a8e5ce32984099ef675fce04730f6e2a6dcfdf5bd292fea01a8f936265342", + "sha256:7755b972f0b4f49de73ef5114fb3ba9c69d80f217e80da99f56f0d0a5dcb3d70", + "sha256:8f0b2d09ab4b38530a1630403967d11a601e56e02e79d3f56370d34fd071fe38", + "sha256:8debe4b6b4290dbbfecea9edea61c22fb455e69e3cbc7d63b17f8e1ab8ea669b", + "sha256:0c6ddab305e5452850f3c09fe15310dff8dc7221702d736dc7705882c1df9658", + "sha256:a9527973bb5d7ccdf88b5be8eb81e024094be1709df659af3127865463c1c188", + "sha256:480cd420e43c6895240c87c88969b87417549c02393cde1b6f71a3a3d5a2a620", + "sha256:391d950d763a33d8ae0373f218aa59907599f51e42cd864129591887e1291034", + "sha256:5b3ec0a6ed9e3de93bb082151f56b1cde5d7e31f2809039a1b5b55a5052fe873", + "sha256:ef935546e2c99da3e8962f2eb3cd6813e9e9a8b19bc8d15b56d1cac37f0342d5", + "sha256:6d644992d62bd09a2bbf490b7fe3aa1e35e6d0d2479583c2decec7092f193310", + "sha256:59d817c36a25078c8ec1f6de0d8336aec598037f89708ed13dbf661557a25084", + "sha256:6636ce01d12372e56a89ec77ea8d9ed510f8c701df1220750add4613764c05a4", + "sha256:96c7f369c29bbf11b971e4dbb6473e8991b666f9de046a414317634eb0a25d2a", + "sha256:3544ba1fa82d1e89619ed04c2485fab3445b1603959d224792d1183dd658033d", + "sha256:12d99406b52b526af152628cd72ba6eacf5d18484dc79cfdacd4b38a21620a2b", + "sha256:3b3cda9eceb0fca56c274e3be93daf53f59501e6b3628fabbaea8ea416eb757a", + "sha256:27ad0fc48c381eb77f69b4e80edccb4d8a2399f5cebd5a8c5a3e1c32313343a6", + "sha256:52845fb94361dad36cc4136e49b92c79ca59c16c579e2f51df0c58ba355c4367", + "sha256:7bf3a57229276fb913155b077d00a18ec6cba92c7f062728ca1c3bc3503c0b55", + "sha256:e5df92d3db931488225ca9f7290de0334225d4bd7c48fc2dcd380d0921bb6680", + "sha256:290ac64fbae3288821551371c8dda38fcf5dfa063a54cb270dcc395a090f5173", + "sha256:996aee90e29ed78d80a5a0c0e50d60a732a18fddae06f87b68bef183beddd2c4", + "sha256:30f6a316d4da01d694d8c17aa84b37f468cccc7184248e255486eb3095ebb87c", + "sha256:c694476a7241ba4e4a0663606d4d6eec7ed8624252c010fbef2713968e8f9436", + "sha256:ab9aaff2160873663388faea6d987cd8f2b5935137b81c64fde145bf2a330d54", + "sha256:e1f3ab860045b96235cbc1b89a3e73add955a303eb42905b570b6012b73b9184", + "sha256:0b260d90d097379d4351132b45110d013b98f4a335795baeb95788fcebcb7f3c", + "sha256:f0f5ecd72b4e0a38d3ad73b5756d8f209955932e9615715502a61dffe56f401a", + "sha256:b4cd790490e41c808e8d65f9ac8f2e58c79bc1a9919a713c4519e77b26dc2053", + "sha256:16b88c0e7f950c32c7496117d1efad90a8557a2badcb267d99a19676b1f0b76a", + "sha256:49d36ba00b17fb605f374ca7877ae129678de925d10fd1955f07c2b6f74dd1c9", + "sha256:b31d189a88ca43fee6077c25bcb623582d569193ed6ac11b4e5623558911e3de", + "sha256:3ecfd2822cf64c609c9c8489e2accfbc0b1de0f2a3637ff1b5d30768fb34b40c", + "sha256:a7f09c3e09b29c5503962a068f29e8726cb91d1dbce2fab688aee0a98189b2be", + "sha256:3d12e651068a0ff19afdd568b5d14ee5292f849542b31d6c9b099a09344e1f4d", + "sha256:f01e41975a9335f5983021b081bc700e46b85efb262670223c4db61eea0a3ebd", + "sha256:2b1b655bb8752f631e786c4c55670315d8569acccfe26402942977c216f2803a", + "sha256:0943c634f5c24311ebdeca6fef5682a4a374c89a831700d188bff7f987470004", + "sha256:9a183e56c86d376b408bdf922746d0a657f62b0e18c7c8f82a496b87710c576f", + "sha256:d919f3c2f534ddbb0b6057f82bca36051ce80a2a9cd3016c320ae276884311f5", + "sha256:108a3eb288f8094aab6ffd822c593902e48e85c8a37b7da2bd21b15f785d92c5", + "sha256:f8b5dcfa1d082af23bb2b2c08526131921329d48d1614d9f2f163a997176087a", + "sha256:ee13e75c33e0af49fbf6c3aaa5bbd102fc468c2d554c4f94763d35a33964dfe4", + "sha256:2571abab1776d4c2e427fba10d61531afff2ab0789f89ef46ce925b6a5d98e0f", + "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef", + "sha256:bb09e17fd1bd2ee47155f1349645fcd9fff31e1247c7ed99cad469f1c16a4216" + ] + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-archive-manifest.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-archive-manifest.json new file mode 100644 index 000000000000..0e3a641407ac --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-archive-manifest.json @@ -0,0 +1,57 @@ +[ + { + "Config": "/682f8d24b9d9c313d1190a0e955dcb5e65ec9beea40420999839c6f0cbb38382.json", + "Layers": [ + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "bb09e17fd1bd2ee47155f1349645fcd9fff31e1247c7ed99cad469f1c16a4216.tar" + ], + "RepoTags": [ + "pack.local/builder/6b7874626575656b6162:latest" + ] + } +] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-config.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-config.json new file mode 100644 index 000000000000..e5a13dbb071f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-config.json @@ -0,0 +1,29 @@ +{ + "Hostname": "", + "Domainname": "", + "User": "vcap", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "CNB_USER_ID=2000", + "CNB_GROUP_ID=2000", + "CNB_STACK_ID=org.cloudfoundry.stacks.cflinuxfs3" + ], + "Cmd": null, + "Image": "sha256:523c8ade6e06f814469b2cf04c8045a74becee17088955f2657958476d3fba1f", + "Volumes": null, + "WorkingDir": "/layers", + "Entrypoint": null, + "OnBuild": null, + "Labels": { + "io.buildpacks.builder.metadata": "{\"description\":\"cflinuxfs3 base image with buildpacks for Java, .NET, NodeJS, Python, Golang, PHP, HTTPD and NGINX\",\"buildpacks\":[{\"id\":\"org.cloudfoundry.googlestackdriver\",\"version\":\"v1.0.40\",\"latest\":true},{\"id\":\"org.cloudfoundry.buildsystem\",\"version\":\"v1.0.114\",\"latest\":true},{\"id\":\"org.cloudfoundry.jdbc\",\"version\":\"v1.0.94\",\"latest\":true},{\"id\":\"org.cloudfoundry.springautoreconfiguration\",\"version\":\"v1.0.100\",\"latest\":true},{\"id\":\"org.cloudfoundry.archiveexpanding\",\"version\":\"v1.0.87\",\"latest\":true},{\"id\":\"org.cloudfoundry.jvmapplication\",\"version\":\"v1.0.72\",\"latest\":true},{\"id\":\"org.cloudfoundry.debug\",\"version\":\"v1.0.92\",\"latest\":true},{\"id\":\"org.cloudfoundry.go\",\"version\":\"v0.0.1\",\"latest\":true},{\"id\":\"org.cloudfoundry.openjdk\",\"version\":\"v1.0.53\",\"latest\":true},{\"id\":\"org.cloudfoundry.procfile\",\"version\":\"v1.0.37\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core\",\"version\":\"v0.0.2\",\"latest\":true},{\"id\":\"org.cloudfoundry.azureapplicationinsights\",\"version\":\"v1.0.94\",\"latest\":true},{\"id\":\"org.cloudfoundry.php\",\"version\":\"v0.0.0-RC1\",\"latest\":true},{\"id\":\"org.cloudfoundry.tomcat\",\"version\":\"v1.1.9\",\"latest\":true},{\"id\":\"org.cloudfoundry.nodejs\",\"version\":\"v0.0.3\",\"latest\":true},{\"id\":\"org.cloudfoundry.jmx\",\"version\":\"v1.0.94\",\"latest\":true},{\"id\":\"org.cloudfoundry.springboot\",\"version\":\"v1.0.97\",\"latest\":true},{\"id\":\"org.cloudfoundry.distzip\",\"version\":\"v1.0.89\",\"latest\":true},{\"id\":\"org.cloudfoundry.python\",\"version\":\"v0.0.1\",\"latest\":true},{\"id\":\"org.cloudfoundry.dep\",\"version\":\"0.0.51\",\"latest\":true},{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.48\",\"latest\":true},{\"id\":\"org.cloudfoundry.go-mod\",\"version\":\"0.0.44\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-aspnet\",\"version\":\"0.0.53\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-build\",\"version\":\"0.0.18\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-conf\",\"version\":\"0.0.57\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-runtime\",\"version\":\"0.0.66\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-sdk\",\"version\":\"0.0.55\",\"latest\":true},{\"id\":\"org.cloudfoundry.httpd\",\"version\":\"0.0.21\",\"latest\":true},{\"id\":\"org.cloudfoundry.nginx\",\"version\":\"0.0.25\",\"latest\":true},{\"id\":\"org.cloudfoundry.php-composer\",\"version\":\"0.0.16\",\"latest\":true},{\"id\":\"org.cloudfoundry.php-dist\",\"version\":\"0.0.30\",\"latest\":true},{\"id\":\"org.cloudfoundry.php-web\",\"version\":\"0.0.24\",\"latest\":true},{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.85\",\"latest\":true},{\"id\":\"org.cloudfoundry.npm\",\"version\":\"0.0.53\",\"latest\":true},{\"id\":\"org.cloudfoundry.yarn\",\"version\":\"0.0.58\",\"latest\":true},{\"id\":\"org.cloudfoundry.conda\",\"version\":\"0.0.37\",\"latest\":true},{\"id\":\"org.cloudfoundry.pip\",\"version\":\"0.0.53\",\"latest\":true},{\"id\":\"org.cloudfoundry.pipenv\",\"version\":\"0.0.38\",\"latest\":true},{\"id\":\"org.cloudfoundry.python-runtime\",\"version\":\"0.0.57\",\"latest\":true}],\"groups\":[{\"buildpacks\":[{\"id\":\"org.cloudfoundry.archiveexpanding\",\"version\":\"v1.0.87\",\"optional\":true},{\"id\":\"org.cloudfoundry.openjdk\",\"version\":\"v1.0.53\"},{\"id\":\"org.cloudfoundry.buildsystem\",\"version\":\"v1.0.114\",\"optional\":true},{\"id\":\"org.cloudfoundry.jvmapplication\",\"version\":\"v1.0.72\"},{\"id\":\"org.cloudfoundry.tomcat\",\"version\":\"v1.1.9\",\"optional\":true},{\"id\":\"org.cloudfoundry.springboot\",\"version\":\"v1.0.97\",\"optional\":true},{\"id\":\"org.cloudfoundry.distzip\",\"version\":\"v1.0.89\",\"optional\":true},{\"id\":\"org.cloudfoundry.procfile\",\"version\":\"v1.0.37\",\"optional\":true},{\"id\":\"org.cloudfoundry.azureapplicationinsights\",\"version\":\"v1.0.94\",\"optional\":true},{\"id\":\"org.cloudfoundry.debug\",\"version\":\"v1.0.92\",\"optional\":true},{\"id\":\"org.cloudfoundry.googlestackdriver\",\"version\":\"v1.0.40\",\"optional\":true},{\"id\":\"org.cloudfoundry.jdbc\",\"version\":\"v1.0.94\",\"optional\":true},{\"id\":\"org.cloudfoundry.jmx\",\"version\":\"v1.0.94\",\"optional\":true},{\"id\":\"org.cloudfoundry.springautoreconfiguration\",\"version\":\"v1.0.100\",\"optional\":true}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.nodejs\",\"version\":\"v0.0.3\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.python\",\"version\":\"v0.0.1\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.go\",\"version\":\"v0.0.1\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.dotnet-core\",\"version\":\"v0.0.2\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.php\",\"version\":\"v0.0.0-RC1\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.httpd\",\"version\":\"0.0.21\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.nginx\",\"version\":\"0.0.25\"}]}],\"stack\":{\"runImage\":{\"image\":\"cloudfoundry/run:full-cnb\",\"mirrors\":null}},\"lifecycle\":{\"version\":\"0.5.0\",\"api\":{\"buildpack\":\"0.2\",\"platform\":\"0.1\"}},\"createdBy\":{\"name\":\"Pack CLI\",\"version\":\"v0.5.0 (git sha: c9cfac75b49609524e1ea33f809c12071406547c)\"}}", + "io.buildpacks.buildpack.layers": "{\"org.cloudfoundry.archiveexpanding\":{\"v1.0.87\":{\"layerDiffID\":\"sha256:391d950d763a33d8ae0373f218aa59907599f51e42cd864129591887e1291034\"}},\"org.cloudfoundry.azureapplicationinsights\":{\"v1.0.94\":{\"layerDiffID\":\"sha256:3544ba1fa82d1e89619ed04c2485fab3445b1603959d224792d1183dd658033d\"}},\"org.cloudfoundry.buildsystem\":{\"v1.0.114\":{\"layerDiffID\":\"sha256:0c6ddab305e5452850f3c09fe15310dff8dc7221702d736dc7705882c1df9658\"}},\"org.cloudfoundry.conda\":{\"0.0.37\":{\"layerDiffID\":\"sha256:0943c634f5c24311ebdeca6fef5682a4a374c89a831700d188bff7f987470004\"}},\"org.cloudfoundry.debug\":{\"v1.0.92\":{\"layerDiffID\":\"sha256:ef935546e2c99da3e8962f2eb3cd6813e9e9a8b19bc8d15b56d1cac37f0342d5\"}},\"org.cloudfoundry.dep\":{\"0.0.51\":{\"layerDiffID\":\"sha256:996aee90e29ed78d80a5a0c0e50d60a732a18fddae06f87b68bef183beddd2c4\"}},\"org.cloudfoundry.distzip\":{\"v1.0.89\":{\"layerDiffID\":\"sha256:e5df92d3db931488225ca9f7290de0334225d4bd7c48fc2dcd380d0921bb6680\"}},\"org.cloudfoundry.dotnet-core\":{\"v0.0.2\":{\"layerDiffID\":\"sha256:96c7f369c29bbf11b971e4dbb6473e8991b666f9de046a414317634eb0a25d2a\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.dotnet-core-runtime\",\"version\":\"0.0.66\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-aspnet\",\"version\":\"0.0.53\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-sdk\",\"version\":\"0.0.55\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-build\",\"version\":\"0.0.18\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-conf\",\"version\":\"0.0.57\"}]}]}},\"org.cloudfoundry.dotnet-core-aspnet\":{\"0.0.53\":{\"layerDiffID\":\"sha256:ab9aaff2160873663388faea6d987cd8f2b5935137b81c64fde145bf2a330d54\"}},\"org.cloudfoundry.dotnet-core-build\":{\"0.0.18\":{\"layerDiffID\":\"sha256:e1f3ab860045b96235cbc1b89a3e73add955a303eb42905b570b6012b73b9184\"}},\"org.cloudfoundry.dotnet-core-conf\":{\"0.0.57\":{\"layerDiffID\":\"sha256:0b260d90d097379d4351132b45110d013b98f4a335795baeb95788fcebcb7f3c\"}},\"org.cloudfoundry.dotnet-core-runtime\":{\"0.0.66\":{\"layerDiffID\":\"sha256:f0f5ecd72b4e0a38d3ad73b5756d8f209955932e9615715502a61dffe56f401a\"}},\"org.cloudfoundry.dotnet-core-sdk\":{\"0.0.55\":{\"layerDiffID\":\"sha256:b4cd790490e41c808e8d65f9ac8f2e58c79bc1a9919a713c4519e77b26dc2053\"}},\"org.cloudfoundry.go\":{\"v0.0.1\":{\"layerDiffID\":\"sha256:6d644992d62bd09a2bbf490b7fe3aa1e35e6d0d2479583c2decec7092f193310\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.48\"},{\"id\":\"org.cloudfoundry.go-mod\",\"version\":\"0.0.44\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.48\"},{\"id\":\"org.cloudfoundry.dep\",\"version\":\"0.0.51\"}]}]}},\"org.cloudfoundry.go-compiler\":{\"0.0.48\":{\"layerDiffID\":\"sha256:30f6a316d4da01d694d8c17aa84b37f468cccc7184248e255486eb3095ebb87c\"}},\"org.cloudfoundry.go-mod\":{\"0.0.44\":{\"layerDiffID\":\"sha256:c694476a7241ba4e4a0663606d4d6eec7ed8624252c010fbef2713968e8f9436\"}},\"org.cloudfoundry.googlestackdriver\":{\"v1.0.40\":{\"layerDiffID\":\"sha256:8debe4b6b4290dbbfecea9edea61c22fb455e69e3cbc7d63b17f8e1ab8ea669b\"}},\"org.cloudfoundry.httpd\":{\"0.0.21\":{\"layerDiffID\":\"sha256:16b88c0e7f950c32c7496117d1efad90a8557a2badcb267d99a19676b1f0b76a\"}},\"org.cloudfoundry.jdbc\":{\"v1.0.94\":{\"layerDiffID\":\"sha256:a9527973bb5d7ccdf88b5be8eb81e024094be1709df659af3127865463c1c188\"}},\"org.cloudfoundry.jmx\":{\"v1.0.94\":{\"layerDiffID\":\"sha256:52845fb94361dad36cc4136e49b92c79ca59c16c579e2f51df0c58ba355c4367\"}},\"org.cloudfoundry.jvmapplication\":{\"v1.0.72\":{\"layerDiffID\":\"sha256:5b3ec0a6ed9e3de93bb082151f56b1cde5d7e31f2809039a1b5b55a5052fe873\"}},\"org.cloudfoundry.nginx\":{\"0.0.25\":{\"layerDiffID\":\"sha256:49d36ba00b17fb605f374ca7877ae129678de925d10fd1955f07c2b6f74dd1c9\"}},\"org.cloudfoundry.node-engine\":{\"0.0.85\":{\"layerDiffID\":\"sha256:3d12e651068a0ff19afdd568b5d14ee5292f849542b31d6c9b099a09344e1f4d\"}},\"org.cloudfoundry.nodejs\":{\"v0.0.3\":{\"layerDiffID\":\"sha256:27ad0fc48c381eb77f69b4e80edccb4d8a2399f5cebd5a8c5a3e1c32313343a6\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.85\"},{\"id\":\"org.cloudfoundry.yarn\",\"version\":\"0.0.58\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.85\"},{\"id\":\"org.cloudfoundry.npm\",\"version\":\"0.0.53\"}]}]}},\"org.cloudfoundry.npm\":{\"0.0.53\":{\"layerDiffID\":\"sha256:f01e41975a9335f5983021b081bc700e46b85efb262670223c4db61eea0a3ebd\"}},\"org.cloudfoundry.openjdk\":{\"v1.0.53\":{\"layerDiffID\":\"sha256:59d817c36a25078c8ec1f6de0d8336aec598037f89708ed13dbf661557a25084\"}},\"org.cloudfoundry.php\":{\"v0.0.0-RC1\":{\"layerDiffID\":\"sha256:12d99406b52b526af152628cd72ba6eacf5d18484dc79cfdacd4b38a21620a2b\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.php-dist\",\"version\":\"0.0.30\"},{\"id\":\"org.cloudfoundry.php-composer\",\"version\":\"0.0.16\",\"optional\":true},{\"id\":\"org.cloudfoundry.httpd\",\"version\":\"0.0.21\",\"optional\":true},{\"id\":\"org.cloudfoundry.php-web\",\"version\":\"0.0.24\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.php-dist\",\"version\":\"0.0.30\"},{\"id\":\"org.cloudfoundry.php-composer\",\"version\":\"0.0.16\",\"optional\":true},{\"id\":\"org.cloudfoundry.nginx\",\"version\":\"0.0.25\",\"optional\":true},{\"id\":\"org.cloudfoundry.php-web\",\"version\":\"0.0.24\"}]}]}},\"org.cloudfoundry.php-composer\":{\"0.0.16\":{\"layerDiffID\":\"sha256:b31d189a88ca43fee6077c25bcb623582d569193ed6ac11b4e5623558911e3de\"}},\"org.cloudfoundry.php-dist\":{\"0.0.30\":{\"layerDiffID\":\"sha256:3ecfd2822cf64c609c9c8489e2accfbc0b1de0f2a3637ff1b5d30768fb34b40c\"}},\"org.cloudfoundry.php-web\":{\"0.0.24\":{\"layerDiffID\":\"sha256:a7f09c3e09b29c5503962a068f29e8726cb91d1dbce2fab688aee0a98189b2be\"}},\"org.cloudfoundry.pip\":{\"0.0.53\":{\"layerDiffID\":\"sha256:9a183e56c86d376b408bdf922746d0a657f62b0e18c7c8f82a496b87710c576f\"}},\"org.cloudfoundry.pipenv\":{\"0.0.38\":{\"layerDiffID\":\"sha256:d919f3c2f534ddbb0b6057f82bca36051ce80a2a9cd3016c320ae276884311f5\"}},\"org.cloudfoundry.procfile\":{\"v1.0.37\":{\"layerDiffID\":\"sha256:6636ce01d12372e56a89ec77ea8d9ed510f8c701df1220750add4613764c05a4\"}},\"org.cloudfoundry.python\":{\"v0.0.1\":{\"layerDiffID\":\"sha256:290ac64fbae3288821551371c8dda38fcf5dfa063a54cb270dcc395a090f5173\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.python-runtime\",\"version\":\"0.0.57\"},{\"id\":\"org.cloudfoundry.pipenv\",\"version\":\"0.0.38\",\"optional\":true},{\"id\":\"org.cloudfoundry.pip\",\"version\":\"0.0.53\",\"optional\":true}]},{\"group\":[{\"id\":\"org.cloudfoundry.conda\",\"version\":\"0.0.37\"}]}]}},\"org.cloudfoundry.python-runtime\":{\"0.0.57\":{\"layerDiffID\":\"sha256:108a3eb288f8094aab6ffd822c593902e48e85c8a37b7da2bd21b15f785d92c5\"}},\"org.cloudfoundry.springautoreconfiguration\":{\"v1.0.100\":{\"layerDiffID\":\"sha256:480cd420e43c6895240c87c88969b87417549c02393cde1b6f71a3a3d5a2a620\"}},\"org.cloudfoundry.springboot\":{\"v1.0.97\":{\"layerDiffID\":\"sha256:7bf3a57229276fb913155b077d00a18ec6cba92c7f062728ca1c3bc3503c0b55\"}},\"org.cloudfoundry.tomcat\":{\"v1.1.9\":{\"layerDiffID\":\"sha256:3b3cda9eceb0fca56c274e3be93daf53f59501e6b3628fabbaea8ea416eb757a\"}},\"org.cloudfoundry.yarn\":{\"0.0.58\":{\"layerDiffID\":\"sha256:2b1b655bb8752f631e786c4c55670315d8569acccfe26402942977c216f2803a\"}}}", + "io.buildpacks.buildpack.order": "[{\"group\":[{\"id\":\"org.cloudfoundry.archiveexpanding\",\"optional\":true},{\"id\":\"org.cloudfoundry.openjdk\"},{\"id\":\"org.cloudfoundry.buildsystem\",\"optional\":true},{\"id\":\"org.cloudfoundry.jvmapplication\"},{\"id\":\"org.cloudfoundry.tomcat\",\"optional\":true},{\"id\":\"org.cloudfoundry.springboot\",\"optional\":true},{\"id\":\"org.cloudfoundry.distzip\",\"optional\":true},{\"id\":\"org.cloudfoundry.procfile\",\"optional\":true},{\"id\":\"org.cloudfoundry.azureapplicationinsights\",\"optional\":true},{\"id\":\"org.cloudfoundry.debug\",\"optional\":true},{\"id\":\"org.cloudfoundry.googlestackdriver\",\"optional\":true},{\"id\":\"org.cloudfoundry.jdbc\",\"optional\":true},{\"id\":\"org.cloudfoundry.jmx\",\"optional\":true},{\"id\":\"org.cloudfoundry.springautoreconfiguration\",\"optional\":true}]},{\"group\":[{\"id\":\"org.cloudfoundry.nodejs\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.python\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.go\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.dotnet-core\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.php\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.httpd\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.nginx\"}]}]", + "io.buildpacks.stack.id": "org.cloudfoundry.stacks.cflinuxfs3" + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image.json new file mode 100644 index 000000000000..8fbaf3155686 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image.json @@ -0,0 +1,142 @@ +{ + "Id": "sha256:9b450bffdb05bcf660d464d0bfdf344ee6ca38e9b8de4f408c8080b0c9319349", + "RepoTags": [ + "paketo-buildpacks/cnb:latest" + ], + "RepoDigests": [ + "paketo-buildpacks/cnb@sha256:915802bb193b66e3fc1a5a8f5584c6a1b6db05425e573887673bddcf426f1b90" + ], + "Parent": "", + "Comment": "", + "Created": "2019-10-30T19:34:56.296666503Z", + "Container": "84597380a7968131ab47dd1b8183a96dcfe9e1e4acff1efe5824dcd762184a67", + "ContainerConfig": { + "Hostname": "84597380a796", + "Domainname": "", + "User": "vcap", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "CNB_USER_ID=2000", + "CNB_GROUP_ID=2000", + "CNB_STACK_ID=org.cloudfoundry.stacks.cflinuxfs3" + ], + "Cmd": [ + "/bin/sh", + "-c", + "#(nop) ", + "LABEL io.buildpacks.stack.id=org.cloudfoundry.stacks.cflinuxfs3" + ], + "Image": "sha256:523c8ade6e06f814469b2cf04c8045a74becee17088955f2657958476d3fba1f", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": { + "io.buildpacks.stack.id": "org.cloudfoundry.stacks.cflinuxfs3" + } + }, + "DockerVersion": "18.09.6", + "Author": "", + "Config": { + "Hostname": "", + "Domainname": "", + "User": "vcap", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "CNB_USER_ID=2000", + "CNB_GROUP_ID=2000", + "CNB_STACK_ID=org.cloudfoundry.stacks.cflinuxfs3" + ], + "Cmd": null, + "Image": "sha256:523c8ade6e06f814469b2cf04c8045a74becee17088955f2657958476d3fba1f", + "Volumes": null, + "WorkingDir": "/layers", + "Entrypoint": null, + "OnBuild": null, + "Labels": { + "io.buildpacks.builder.metadata": "{\"description\":\"cflinuxfs3 base image with buildpacks for Java, .NET, NodeJS, Python, Golang, PHP, HTTPD and NGINX\",\"buildpacks\":[{\"id\":\"org.cloudfoundry.googlestackdriver\",\"version\":\"v1.0.40\",\"latest\":true},{\"id\":\"org.cloudfoundry.buildsystem\",\"version\":\"v1.0.114\",\"latest\":true},{\"id\":\"org.cloudfoundry.jdbc\",\"version\":\"v1.0.94\",\"latest\":true},{\"id\":\"org.cloudfoundry.springautoreconfiguration\",\"version\":\"v1.0.100\",\"latest\":true},{\"id\":\"org.cloudfoundry.archiveexpanding\",\"version\":\"v1.0.87\",\"latest\":true},{\"id\":\"org.cloudfoundry.jvmapplication\",\"version\":\"v1.0.72\",\"latest\":true},{\"id\":\"org.cloudfoundry.debug\",\"version\":\"v1.0.92\",\"latest\":true},{\"id\":\"org.cloudfoundry.go\",\"version\":\"v0.0.1\",\"latest\":true},{\"id\":\"org.cloudfoundry.openjdk\",\"version\":\"v1.0.53\",\"latest\":true},{\"id\":\"org.cloudfoundry.procfile\",\"version\":\"v1.0.37\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core\",\"version\":\"v0.0.2\",\"latest\":true},{\"id\":\"org.cloudfoundry.azureapplicationinsights\",\"version\":\"v1.0.94\",\"latest\":true},{\"id\":\"org.cloudfoundry.php\",\"version\":\"v0.0.0-RC1\",\"latest\":true},{\"id\":\"org.cloudfoundry.tomcat\",\"version\":\"v1.1.9\",\"latest\":true},{\"id\":\"org.cloudfoundry.nodejs\",\"version\":\"v0.0.3\",\"latest\":true},{\"id\":\"org.cloudfoundry.jmx\",\"version\":\"v1.0.94\",\"latest\":true},{\"id\":\"org.cloudfoundry.springboot\",\"version\":\"v1.0.97\",\"latest\":true},{\"id\":\"org.cloudfoundry.distzip\",\"version\":\"v1.0.89\",\"latest\":true},{\"id\":\"org.cloudfoundry.python\",\"version\":\"v0.0.1\",\"latest\":true},{\"id\":\"org.cloudfoundry.dep\",\"version\":\"0.0.51\",\"latest\":true},{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.48\",\"latest\":true},{\"id\":\"org.cloudfoundry.go-mod\",\"version\":\"0.0.44\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-aspnet\",\"version\":\"0.0.53\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-build\",\"version\":\"0.0.18\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-conf\",\"version\":\"0.0.57\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-runtime\",\"version\":\"0.0.66\",\"latest\":true},{\"id\":\"org.cloudfoundry.dotnet-core-sdk\",\"version\":\"0.0.55\",\"latest\":true},{\"id\":\"org.cloudfoundry.httpd\",\"version\":\"0.0.21\",\"latest\":true},{\"id\":\"org.cloudfoundry.nginx\",\"version\":\"0.0.25\",\"latest\":true},{\"id\":\"org.cloudfoundry.php-composer\",\"version\":\"0.0.16\",\"latest\":true},{\"id\":\"org.cloudfoundry.php-dist\",\"version\":\"0.0.30\",\"latest\":true},{\"id\":\"org.cloudfoundry.php-web\",\"version\":\"0.0.24\",\"latest\":true},{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.85\",\"latest\":true},{\"id\":\"org.cloudfoundry.npm\",\"version\":\"0.0.53\",\"latest\":true},{\"id\":\"org.cloudfoundry.yarn\",\"version\":\"0.0.58\",\"latest\":true},{\"id\":\"org.cloudfoundry.conda\",\"version\":\"0.0.37\",\"latest\":true},{\"id\":\"org.cloudfoundry.pip\",\"version\":\"0.0.53\",\"latest\":true},{\"id\":\"org.cloudfoundry.pipenv\",\"version\":\"0.0.38\",\"latest\":true},{\"id\":\"org.cloudfoundry.python-runtime\",\"version\":\"0.0.57\",\"latest\":true}],\"groups\":[{\"buildpacks\":[{\"id\":\"org.cloudfoundry.archiveexpanding\",\"version\":\"v1.0.87\",\"optional\":true},{\"id\":\"org.cloudfoundry.openjdk\",\"version\":\"v1.0.53\"},{\"id\":\"org.cloudfoundry.buildsystem\",\"version\":\"v1.0.114\",\"optional\":true},{\"id\":\"org.cloudfoundry.jvmapplication\",\"version\":\"v1.0.72\"},{\"id\":\"org.cloudfoundry.tomcat\",\"version\":\"v1.1.9\",\"optional\":true},{\"id\":\"org.cloudfoundry.springboot\",\"version\":\"v1.0.97\",\"optional\":true},{\"id\":\"org.cloudfoundry.distzip\",\"version\":\"v1.0.89\",\"optional\":true},{\"id\":\"org.cloudfoundry.procfile\",\"version\":\"v1.0.37\",\"optional\":true},{\"id\":\"org.cloudfoundry.azureapplicationinsights\",\"version\":\"v1.0.94\",\"optional\":true},{\"id\":\"org.cloudfoundry.debug\",\"version\":\"v1.0.92\",\"optional\":true},{\"id\":\"org.cloudfoundry.googlestackdriver\",\"version\":\"v1.0.40\",\"optional\":true},{\"id\":\"org.cloudfoundry.jdbc\",\"version\":\"v1.0.94\",\"optional\":true},{\"id\":\"org.cloudfoundry.jmx\",\"version\":\"v1.0.94\",\"optional\":true},{\"id\":\"org.cloudfoundry.springautoreconfiguration\",\"version\":\"v1.0.100\",\"optional\":true}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.nodejs\",\"version\":\"v0.0.3\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.python\",\"version\":\"v0.0.1\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.go\",\"version\":\"v0.0.1\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.dotnet-core\",\"version\":\"v0.0.2\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.php\",\"version\":\"v0.0.0-RC1\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.httpd\",\"version\":\"0.0.21\"}]},{\"buildpacks\":[{\"id\":\"org.cloudfoundry.nginx\",\"version\":\"0.0.25\"}]}],\"stack\":{\"runImage\":{\"image\":\"cloudfoundry/run:full-cnb\",\"mirrors\":null}},\"lifecycle\":{\"version\":\"0.5.0\",\"api\":{\"buildpack\":\"0.2\",\"platform\":\"0.1\"}},\"createdBy\":{\"name\":\"Pack CLI\",\"version\":\"v0.5.0 (git sha: c9cfac75b49609524e1ea33f809c12071406547c)\"}}", + "io.buildpacks.buildpack.layers": "{\"org.cloudfoundry.archiveexpanding\":{\"v1.0.87\":{\"layerDiffID\":\"sha256:391d950d763a33d8ae0373f218aa59907599f51e42cd864129591887e1291034\"}},\"org.cloudfoundry.azureapplicationinsights\":{\"v1.0.94\":{\"layerDiffID\":\"sha256:3544ba1fa82d1e89619ed04c2485fab3445b1603959d224792d1183dd658033d\"}},\"org.cloudfoundry.buildsystem\":{\"v1.0.114\":{\"layerDiffID\":\"sha256:0c6ddab305e5452850f3c09fe15310dff8dc7221702d736dc7705882c1df9658\"}},\"org.cloudfoundry.conda\":{\"0.0.37\":{\"layerDiffID\":\"sha256:0943c634f5c24311ebdeca6fef5682a4a374c89a831700d188bff7f987470004\"}},\"org.cloudfoundry.debug\":{\"v1.0.92\":{\"layerDiffID\":\"sha256:ef935546e2c99da3e8962f2eb3cd6813e9e9a8b19bc8d15b56d1cac37f0342d5\"}},\"org.cloudfoundry.dep\":{\"0.0.51\":{\"layerDiffID\":\"sha256:996aee90e29ed78d80a5a0c0e50d60a732a18fddae06f87b68bef183beddd2c4\"}},\"org.cloudfoundry.distzip\":{\"v1.0.89\":{\"layerDiffID\":\"sha256:e5df92d3db931488225ca9f7290de0334225d4bd7c48fc2dcd380d0921bb6680\"}},\"org.cloudfoundry.dotnet-core\":{\"v0.0.2\":{\"layerDiffID\":\"sha256:96c7f369c29bbf11b971e4dbb6473e8991b666f9de046a414317634eb0a25d2a\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.dotnet-core-runtime\",\"version\":\"0.0.66\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-aspnet\",\"version\":\"0.0.53\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-sdk\",\"version\":\"0.0.55\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-build\",\"version\":\"0.0.18\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-conf\",\"version\":\"0.0.57\"}]}]}},\"org.cloudfoundry.dotnet-core-aspnet\":{\"0.0.53\":{\"layerDiffID\":\"sha256:ab9aaff2160873663388faea6d987cd8f2b5935137b81c64fde145bf2a330d54\"}},\"org.cloudfoundry.dotnet-core-build\":{\"0.0.18\":{\"layerDiffID\":\"sha256:e1f3ab860045b96235cbc1b89a3e73add955a303eb42905b570b6012b73b9184\"}},\"org.cloudfoundry.dotnet-core-conf\":{\"0.0.57\":{\"layerDiffID\":\"sha256:0b260d90d097379d4351132b45110d013b98f4a335795baeb95788fcebcb7f3c\"}},\"org.cloudfoundry.dotnet-core-runtime\":{\"0.0.66\":{\"layerDiffID\":\"sha256:f0f5ecd72b4e0a38d3ad73b5756d8f209955932e9615715502a61dffe56f401a\"}},\"org.cloudfoundry.dotnet-core-sdk\":{\"0.0.55\":{\"layerDiffID\":\"sha256:b4cd790490e41c808e8d65f9ac8f2e58c79bc1a9919a713c4519e77b26dc2053\"}},\"org.cloudfoundry.go\":{\"v0.0.1\":{\"layerDiffID\":\"sha256:6d644992d62bd09a2bbf490b7fe3aa1e35e6d0d2479583c2decec7092f193310\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.48\"},{\"id\":\"org.cloudfoundry.go-mod\",\"version\":\"0.0.44\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.48\"},{\"id\":\"org.cloudfoundry.dep\",\"version\":\"0.0.51\"}]}]}},\"org.cloudfoundry.go-compiler\":{\"0.0.48\":{\"layerDiffID\":\"sha256:30f6a316d4da01d694d8c17aa84b37f468cccc7184248e255486eb3095ebb87c\"}},\"org.cloudfoundry.go-mod\":{\"0.0.44\":{\"layerDiffID\":\"sha256:c694476a7241ba4e4a0663606d4d6eec7ed8624252c010fbef2713968e8f9436\"}},\"org.cloudfoundry.googlestackdriver\":{\"v1.0.40\":{\"layerDiffID\":\"sha256:8debe4b6b4290dbbfecea9edea61c22fb455e69e3cbc7d63b17f8e1ab8ea669b\"}},\"org.cloudfoundry.httpd\":{\"0.0.21\":{\"layerDiffID\":\"sha256:16b88c0e7f950c32c7496117d1efad90a8557a2badcb267d99a19676b1f0b76a\"}},\"org.cloudfoundry.jdbc\":{\"v1.0.94\":{\"layerDiffID\":\"sha256:a9527973bb5d7ccdf88b5be8eb81e024094be1709df659af3127865463c1c188\"}},\"org.cloudfoundry.jmx\":{\"v1.0.94\":{\"layerDiffID\":\"sha256:52845fb94361dad36cc4136e49b92c79ca59c16c579e2f51df0c58ba355c4367\"}},\"org.cloudfoundry.jvmapplication\":{\"v1.0.72\":{\"layerDiffID\":\"sha256:5b3ec0a6ed9e3de93bb082151f56b1cde5d7e31f2809039a1b5b55a5052fe873\"}},\"org.cloudfoundry.nginx\":{\"0.0.25\":{\"layerDiffID\":\"sha256:49d36ba00b17fb605f374ca7877ae129678de925d10fd1955f07c2b6f74dd1c9\"}},\"org.cloudfoundry.node-engine\":{\"0.0.85\":{\"layerDiffID\":\"sha256:3d12e651068a0ff19afdd568b5d14ee5292f849542b31d6c9b099a09344e1f4d\"}},\"org.cloudfoundry.nodejs\":{\"v0.0.3\":{\"layerDiffID\":\"sha256:27ad0fc48c381eb77f69b4e80edccb4d8a2399f5cebd5a8c5a3e1c32313343a6\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.85\"},{\"id\":\"org.cloudfoundry.yarn\",\"version\":\"0.0.58\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.85\"},{\"id\":\"org.cloudfoundry.npm\",\"version\":\"0.0.53\"}]}]}},\"org.cloudfoundry.npm\":{\"0.0.53\":{\"layerDiffID\":\"sha256:f01e41975a9335f5983021b081bc700e46b85efb262670223c4db61eea0a3ebd\"}},\"org.cloudfoundry.openjdk\":{\"v1.0.53\":{\"layerDiffID\":\"sha256:59d817c36a25078c8ec1f6de0d8336aec598037f89708ed13dbf661557a25084\"}},\"org.cloudfoundry.php\":{\"v0.0.0-RC1\":{\"layerDiffID\":\"sha256:12d99406b52b526af152628cd72ba6eacf5d18484dc79cfdacd4b38a21620a2b\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.php-dist\",\"version\":\"0.0.30\"},{\"id\":\"org.cloudfoundry.php-composer\",\"version\":\"0.0.16\",\"optional\":true},{\"id\":\"org.cloudfoundry.httpd\",\"version\":\"0.0.21\",\"optional\":true},{\"id\":\"org.cloudfoundry.php-web\",\"version\":\"0.0.24\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.php-dist\",\"version\":\"0.0.30\"},{\"id\":\"org.cloudfoundry.php-composer\",\"version\":\"0.0.16\",\"optional\":true},{\"id\":\"org.cloudfoundry.nginx\",\"version\":\"0.0.25\",\"optional\":true},{\"id\":\"org.cloudfoundry.php-web\",\"version\":\"0.0.24\"}]}]}},\"org.cloudfoundry.php-composer\":{\"0.0.16\":{\"layerDiffID\":\"sha256:b31d189a88ca43fee6077c25bcb623582d569193ed6ac11b4e5623558911e3de\"}},\"org.cloudfoundry.php-dist\":{\"0.0.30\":{\"layerDiffID\":\"sha256:3ecfd2822cf64c609c9c8489e2accfbc0b1de0f2a3637ff1b5d30768fb34b40c\"}},\"org.cloudfoundry.php-web\":{\"0.0.24\":{\"layerDiffID\":\"sha256:a7f09c3e09b29c5503962a068f29e8726cb91d1dbce2fab688aee0a98189b2be\"}},\"org.cloudfoundry.pip\":{\"0.0.53\":{\"layerDiffID\":\"sha256:9a183e56c86d376b408bdf922746d0a657f62b0e18c7c8f82a496b87710c576f\"}},\"org.cloudfoundry.pipenv\":{\"0.0.38\":{\"layerDiffID\":\"sha256:d919f3c2f534ddbb0b6057f82bca36051ce80a2a9cd3016c320ae276884311f5\"}},\"org.cloudfoundry.procfile\":{\"v1.0.37\":{\"layerDiffID\":\"sha256:6636ce01d12372e56a89ec77ea8d9ed510f8c701df1220750add4613764c05a4\"}},\"org.cloudfoundry.python\":{\"v0.0.1\":{\"layerDiffID\":\"sha256:290ac64fbae3288821551371c8dda38fcf5dfa063a54cb270dcc395a090f5173\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.python-runtime\",\"version\":\"0.0.57\"},{\"id\":\"org.cloudfoundry.pipenv\",\"version\":\"0.0.38\",\"optional\":true},{\"id\":\"org.cloudfoundry.pip\",\"version\":\"0.0.53\",\"optional\":true}]},{\"group\":[{\"id\":\"org.cloudfoundry.conda\",\"version\":\"0.0.37\"}]}]}},\"org.cloudfoundry.python-runtime\":{\"0.0.57\":{\"layerDiffID\":\"sha256:108a3eb288f8094aab6ffd822c593902e48e85c8a37b7da2bd21b15f785d92c5\"}},\"org.cloudfoundry.springautoreconfiguration\":{\"v1.0.100\":{\"layerDiffID\":\"sha256:480cd420e43c6895240c87c88969b87417549c02393cde1b6f71a3a3d5a2a620\"}},\"org.cloudfoundry.springboot\":{\"v1.0.97\":{\"layerDiffID\":\"sha256:7bf3a57229276fb913155b077d00a18ec6cba92c7f062728ca1c3bc3503c0b55\"}},\"org.cloudfoundry.tomcat\":{\"v1.1.9\":{\"layerDiffID\":\"sha256:3b3cda9eceb0fca56c274e3be93daf53f59501e6b3628fabbaea8ea416eb757a\"}},\"org.cloudfoundry.yarn\":{\"0.0.58\":{\"layerDiffID\":\"sha256:2b1b655bb8752f631e786c4c55670315d8569acccfe26402942977c216f2803a\"}}}", + "io.buildpacks.buildpack.order": "[{\"group\":[{\"id\":\"org.cloudfoundry.archiveexpanding\",\"optional\":true},{\"id\":\"org.cloudfoundry.openjdk\"},{\"id\":\"org.cloudfoundry.buildsystem\",\"optional\":true},{\"id\":\"org.cloudfoundry.jvmapplication\"},{\"id\":\"org.cloudfoundry.tomcat\",\"optional\":true},{\"id\":\"org.cloudfoundry.springboot\",\"optional\":true},{\"id\":\"org.cloudfoundry.distzip\",\"optional\":true},{\"id\":\"org.cloudfoundry.procfile\",\"optional\":true},{\"id\":\"org.cloudfoundry.azureapplicationinsights\",\"optional\":true},{\"id\":\"org.cloudfoundry.debug\",\"optional\":true},{\"id\":\"org.cloudfoundry.googlestackdriver\",\"optional\":true},{\"id\":\"org.cloudfoundry.jdbc\",\"optional\":true},{\"id\":\"org.cloudfoundry.jmx\",\"optional\":true},{\"id\":\"org.cloudfoundry.springautoreconfiguration\",\"optional\":true}]},{\"group\":[{\"id\":\"org.cloudfoundry.nodejs\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.python\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.go\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.dotnet-core\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.php\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.httpd\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.nginx\"}]}]", + "io.buildpacks.stack.id": "org.cloudfoundry.stacks.cflinuxfs3" + } + }, + "Architecture": "amd64", + "Os": "linux", + "Size": 1559461360, + "VirtualSize": 1559461360, + "GraphDriver": { + "Data": { + "LowerDir": "/var/lib/docker/overlay2/58e30cd9f3a4da4e0d30f20c3b50de7655e261fb3d32f04818f1bd960c1e8b6c/diff:/var/lib/docker/overlay2/ad95d738069aa405ff17a9ebb1fdc32f8490b0dd885c3ba3a28e2c3b25d64641/diff:/var/lib/docker/overlay2/74d2896cfe9efc6945ff18870a7213583b987ecf4306e189ff6b793f77af5dcd/diff:/var/lib/docker/overlay2/1052615e5c240724e10928048f735cc9e7a7676a9af5f173b895df57c6921a40/diff:/var/lib/docker/overlay2/b5a62216c4282e7568e84427073f096551977c8c6f80d3a04ebb04c25730edde/diff:/var/lib/docker/overlay2/016a36bf7d7d7258eca08da62c01e47bf8e531531f914dde7cae33e191ab2218/diff:/var/lib/docker/overlay2/a585012bf1cf9da0472b2bbe86c4919355593e1a02cf399a9b012928eb816bcd/diff:/var/lib/docker/overlay2/b4aa8b70bd59d7b7dc6d6fb2e655c2334dc8360c764232f83d036d1f241e3298/diff:/var/lib/docker/overlay2/5f4cab16092522163e2dba6587b48d53ee3b09c8778b0736999bc120dd3753b1/diff:/var/lib/docker/overlay2/90e60622603d230f238976f4d9f65797fc9f070df62b1d2ccad0cefe4e205b43/diff:/var/lib/docker/overlay2/c43877934a580e47cc477ed46e71246468d7b6d7151abc5f1a97bb1e8c8104cf/diff:/var/lib/docker/overlay2/8734b165cabb3ff234a08d488f622135aeae9b7347cf41273445ff7d07aa4565/diff:/var/lib/docker/overlay2/2743cd9d4b7da84925b1b530732dad97108fe77e75865de580255579ba2cdb92/diff:/var/lib/docker/overlay2/68308d057b24bbcde7a4880f5db0e653743debdcc0ff3e736d1776296c4168a1/diff:/var/lib/docker/overlay2/7a4411dc4ac1ed7a1da9aabf088985b8b131e0db047e513f9890eb9c001c1895/diff:/var/lib/docker/overlay2/7f7c262fea8dea5ec86507188848ea391354a76468b09ec93523920e18a400ea/diff:/var/lib/docker/overlay2/8b3bfa567fb956204ad866e49489dacd2fdf5fbfa4f9b05ed3668e1106a5383b/diff:/var/lib/docker/overlay2/31bbc4f1616a35b7ce157266e44513963502e30d836a8fd7b7ee18436a8c46cf/diff:/var/lib/docker/overlay2/149b8e9f1142cdf6dcdfe17ea286ec17197f1a329cf23d5c82958a2032facf54/diff:/var/lib/docker/overlay2/92fb1e680083eb8314c5310bf10ced63ec2b0a98afbf84cc5175a98b3d44507a/diff:/var/lib/docker/overlay2/175a35b6f7af6eb91ca500dbd3d7e798f6d174cf8549881ffe5eed8e92a70b9f/diff:/var/lib/docker/overlay2/48ca54bbd27f7df19acf2b6cc719d05dd3b63f8133038a55d216a4498d4dc913/diff:/var/lib/docker/overlay2/ffe3cc3b93c9030f9dcb0e64c258d1e554f1f0cf27a0f8d4e98bb7ece5ffe882/diff:/var/lib/docker/overlay2/1fb2d962bb27e95c40a9a2c1aa910ca847d186d04e3d7dcdf93967101cc30dde/diff:/var/lib/docker/overlay2/10b34138f9e9e8d70c684d0a564452b1309363441b9d7e048f75e0e1179411dc/diff:/var/lib/docker/overlay2/1d888c7e9c62c22ccda6478f03f3df4b43d43fa3b32a2c2fdc9345fdc7193cd9/diff:/var/lib/docker/overlay2/649fc275c002d7336b277365636e1c8e5651bb3ed1557806d26dd6dfa1d9119a/diff:/var/lib/docker/overlay2/4484c2c0ee4a20aa17017c8cd54c842c876fea32afb297e88614d759ec5410dc/diff:/var/lib/docker/overlay2/bd5f374e0ea6749c90535d778f2689c076b7290ad9d3f050af0a40c9626fdea4/diff:/var/lib/docker/overlay2/c6ba97531b15be65bccaf7ebc866d8bc0b88ce838b224aceb196a55824b289a5/diff:/var/lib/docker/overlay2/6c65fab249fe652cd20a6391b2e0786379b6d2c7d4fde02914dfb4fac84035bd/diff:/var/lib/docker/overlay2/f391b54493024e0183331b8ec7835107bc1b84b8a6e77d852f5357724eb940ff/diff:/var/lib/docker/overlay2/8044f9e3ceb529c80531fa2fe52ad550286f788e69843f235e7d756b90c213b8/diff:/var/lib/docker/overlay2/7d3b5539c46c9f0e7c4f6f733f435d1bf6428a8ca81ba71f4da1031cef58aa6c/diff:/var/lib/docker/overlay2/b8080b36b0ddec4e4d738571ddf9d89815f6a95a555d282cfebb73519b4835a0/diff:/var/lib/docker/overlay2/8a737007d5862aa43119254122eb7050c8bd110a3b653c8d6afca23e76fc4042/diff:/var/lib/docker/overlay2/3bb8f3670831e2031be2173381caf02874ad72e664716a990a330bcc3454f4a2/diff:/var/lib/docker/overlay2/cbd675efde19ccac72d3566404e5df8b152a9063c1668d8154711c7db398f852/diff:/var/lib/docker/overlay2/84fb9095136cb645f7f15aeeeba1db6fae3999cb48a559daf8dd46bf3befbeba/diff:/var/lib/docker/overlay2/cbc51912822c4a3fb8624e0cf678e5dedeb76dc2fa0e5bc56f3cbfbfefb26d68/diff:/var/lib/docker/overlay2/d08d5bdcf39aaf46bdf1e0f4576bb64931af646213ff350065b4d306e00f7e28/diff:/var/lib/docker/overlay2/cf180c218fe181bdf836065c5e85103816ea9e8dbb8ab54fb311209c33455eb2/diff:/var/lib/docker/overlay2/b0aef801fd38973eaf116001e05e7c3f8e2eb58ccc7ed37a4bd8d4fcc2ad172b/diff:/var/lib/docker/overlay2/f73c585ae34bd962e1fee2c3e2d95d47b9daf68b23cf469fb13bc3282cf77238/diff:/var/lib/docker/overlay2/c071c8471b26e55a90b6573a21c581dec43b6c7683a3fe87cb33a0734c83342a/diff", + "MergedDir": "/var/lib/docker/overlay2/41ced64ea40f3382f7a475030a5bc89b9c86e2a03d43031c5eba3c5c72616c2b/merged", + "UpperDir": "/var/lib/docker/overlay2/41ced64ea40f3382f7a475030a5bc89b9c86e2a03d43031c5eba3c5c72616c2b/diff", + "WorkDir": "/var/lib/docker/overlay2/41ced64ea40f3382f7a475030a5bc89b9c86e2a03d43031c5eba3c5c72616c2b/work" + }, + "Name": "overlay2" + }, + "RootFS": { + "Type": "layers", + "Layers": [ + "sha256:733a8e5ce32984099ef675fce04730f6e2a6dcfdf5bd292fea01a8f936265342", + "sha256:7755b972f0b4f49de73ef5114fb3ba9c69d80f217e80da99f56f0d0a5dcb3d70", + "sha256:8f0b2d09ab4b38530a1630403967d11a601e56e02e79d3f56370d34fd071fe38", + "sha256:8debe4b6b4290dbbfecea9edea61c22fb455e69e3cbc7d63b17f8e1ab8ea669b", + "sha256:0c6ddab305e5452850f3c09fe15310dff8dc7221702d736dc7705882c1df9658", + "sha256:a9527973bb5d7ccdf88b5be8eb81e024094be1709df659af3127865463c1c188", + "sha256:480cd420e43c6895240c87c88969b87417549c02393cde1b6f71a3a3d5a2a620", + "sha256:391d950d763a33d8ae0373f218aa59907599f51e42cd864129591887e1291034", + "sha256:5b3ec0a6ed9e3de93bb082151f56b1cde5d7e31f2809039a1b5b55a5052fe873", + "sha256:ef935546e2c99da3e8962f2eb3cd6813e9e9a8b19bc8d15b56d1cac37f0342d5", + "sha256:6d644992d62bd09a2bbf490b7fe3aa1e35e6d0d2479583c2decec7092f193310", + "sha256:59d817c36a25078c8ec1f6de0d8336aec598037f89708ed13dbf661557a25084", + "sha256:6636ce01d12372e56a89ec77ea8d9ed510f8c701df1220750add4613764c05a4", + "sha256:96c7f369c29bbf11b971e4dbb6473e8991b666f9de046a414317634eb0a25d2a", + "sha256:3544ba1fa82d1e89619ed04c2485fab3445b1603959d224792d1183dd658033d", + "sha256:12d99406b52b526af152628cd72ba6eacf5d18484dc79cfdacd4b38a21620a2b", + "sha256:3b3cda9eceb0fca56c274e3be93daf53f59501e6b3628fabbaea8ea416eb757a", + "sha256:27ad0fc48c381eb77f69b4e80edccb4d8a2399f5cebd5a8c5a3e1c32313343a6", + "sha256:52845fb94361dad36cc4136e49b92c79ca59c16c579e2f51df0c58ba355c4367", + "sha256:7bf3a57229276fb913155b077d00a18ec6cba92c7f062728ca1c3bc3503c0b55", + "sha256:e5df92d3db931488225ca9f7290de0334225d4bd7c48fc2dcd380d0921bb6680", + "sha256:290ac64fbae3288821551371c8dda38fcf5dfa063a54cb270dcc395a090f5173", + "sha256:996aee90e29ed78d80a5a0c0e50d60a732a18fddae06f87b68bef183beddd2c4", + "sha256:30f6a316d4da01d694d8c17aa84b37f468cccc7184248e255486eb3095ebb87c", + "sha256:c694476a7241ba4e4a0663606d4d6eec7ed8624252c010fbef2713968e8f9436", + "sha256:ab9aaff2160873663388faea6d987cd8f2b5935137b81c64fde145bf2a330d54", + "sha256:e1f3ab860045b96235cbc1b89a3e73add955a303eb42905b570b6012b73b9184", + "sha256:0b260d90d097379d4351132b45110d013b98f4a335795baeb95788fcebcb7f3c", + "sha256:f0f5ecd72b4e0a38d3ad73b5756d8f209955932e9615715502a61dffe56f401a", + "sha256:b4cd790490e41c808e8d65f9ac8f2e58c79bc1a9919a713c4519e77b26dc2053", + "sha256:16b88c0e7f950c32c7496117d1efad90a8557a2badcb267d99a19676b1f0b76a", + "sha256:49d36ba00b17fb605f374ca7877ae129678de925d10fd1955f07c2b6f74dd1c9", + "sha256:b31d189a88ca43fee6077c25bcb623582d569193ed6ac11b4e5623558911e3de", + "sha256:3ecfd2822cf64c609c9c8489e2accfbc0b1de0f2a3637ff1b5d30768fb34b40c", + "sha256:a7f09c3e09b29c5503962a068f29e8726cb91d1dbce2fab688aee0a98189b2be", + "sha256:3d12e651068a0ff19afdd568b5d14ee5292f849542b31d6c9b099a09344e1f4d", + "sha256:f01e41975a9335f5983021b081bc700e46b85efb262670223c4db61eea0a3ebd", + "sha256:2b1b655bb8752f631e786c4c55670315d8569acccfe26402942977c216f2803a", + "sha256:0943c634f5c24311ebdeca6fef5682a4a374c89a831700d188bff7f987470004", + "sha256:9a183e56c86d376b408bdf922746d0a657f62b0e18c7c8f82a496b87710c576f", + "sha256:d919f3c2f534ddbb0b6057f82bca36051ce80a2a9cd3016c320ae276884311f5", + "sha256:108a3eb288f8094aab6ffd822c593902e48e85c8a37b7da2bd21b15f785d92c5", + "sha256:f8b5dcfa1d082af23bb2b2c08526131921329d48d1614d9f2f163a997176087a", + "sha256:ee13e75c33e0af49fbf6c3aaa5bbd102fc468c2d554c4f94763d35a33964dfe4", + "sha256:2571abab1776d4c2e427fba10d61531afff2ab0789f89ef46ce925b6a5d98e0f", + "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + ] + }, + "Metadata": { + "LastTagTime": "0001-01-01T00:00:00Z" + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/manifest.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/manifest.json new file mode 100644 index 000000000000..10a8be5477ac --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/manifest.json @@ -0,0 +1,51 @@ +[ + { + "Config": "fdc5f384ea0818dd99462e53bf2088a0fa42ad4de5878fdf078935192604da6d.json", + "Layers": [ + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "/e39b9186d3d35693645f81db5ec6ced177c4da2d26f71a55de7834fc3b161a60.tar", + "/791b31c608b369f0d6e23aaf55dd6bae76ffd92292afd3eb4dd35f8a389636fb.tar", + "/66d1ab676a2ecb3852104177d2fd9499d90bbbd97984bccb62180502e15a7086.tar", + "/b5787d8d30d02769ebbe6b1ac32d37764feef3cd5cdc68aeffd72bb27d1886e5.tar" + ], + "RepoTags": [ + "pack.local/builder/6b7874626575656b6162:latest" + ] + } +] + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/minimal-image-config.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/minimal-image-config.json new file mode 100644 index 000000000000..4949addaaf02 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/minimal-image-config.json @@ -0,0 +1,19 @@ +{ + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": null, + "Image": "", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": null +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/json/stream.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/json/stream.json new file mode 100644 index 000000000000..f198286bd3b0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/json/stream.json @@ -0,0 +1,598 @@ +{ + "status": "Pulling from paketo-buildpacks/cnb", + "id": "base" +} +{"status":"Pulling fs layer","progressDetail":{},"id":"5667fdb72017"} +{"status":"Pulling fs layer","progressDetail":{},"id":"d83811f270d5"} +{"status":"Pulling fs layer","progressDetail":{},"id":"ee671aafb583"} +{"status":"Pulling fs layer","progressDetail":{},"id":"7fc152dfb3a6"} +{"status":"Pulling fs layer","progressDetail":{},"id":"4ab897fa6fbf"} +{"status":"Pulling fs layer","progressDetail":{},"id":"d837a2a1365e"} +{"status":"Pulling fs layer","progressDetail":{},"id":"988ae18fe41a"} +{"status":"Pulling fs layer","progressDetail":{},"id":"eeb8ef83b565"} +{"status":"Pulling fs layer","progressDetail":{},"id":"357fefdf9bc9"} +{"status":"Pulling fs layer","progressDetail":{},"id":"45b746196f82"} +{"status":"Pulling fs layer","progressDetail":{},"id":"fbf4ce20f8c2"} +{"status":"Pulling fs layer","progressDetail":{},"id":"90aca3c647fe"} +{"status":"Pulling fs layer","progressDetail":{},"id":"1dd62f37c84c"} +{"status":"Pulling fs layer","progressDetail":{},"id":"3192b2fa42db"} +{"status":"Pulling fs layer","progressDetail":{},"id":"ae190b8f66a7"} +{"status":"Pulling fs layer","progressDetail":{},"id":"97bb6e138460"} +{"status":"Waiting","progressDetail":{},"id":"eeb8ef83b565"} +{"status":"Pulling fs layer","progressDetail":{},"id":"2edb982d5170"} +{"status":"Waiting","progressDetail":{},"id":"357fefdf9bc9"} +{"status":"Pulling fs layer","progressDetail":{},"id":"7ddc8e6d6da9"} +{"status":"Pulling fs layer","progressDetail":{},"id":"0df6fd234b59"} +{"status":"Waiting","progressDetail":{},"id":"45b746196f82"} +{"status":"Pulling fs layer","progressDetail":{},"id":"8fc1ba8efe21"} +{"status":"Pulling fs layer","progressDetail":{},"id":"1f6f45e783b5"} +{"status":"Pulling fs layer","progressDetail":{},"id":"43ea61082f68"} +{"status":"Waiting","progressDetail":{},"id":"7fc152dfb3a6"} +{"status":"Pulling fs layer","progressDetail":{},"id":"b8cf53bbc6ba"} +{"status":"Waiting","progressDetail":{},"id":"fbf4ce20f8c2"} +{"status":"Pulling fs layer","progressDetail":{},"id":"25efb07e4521"} +{"status":"Waiting","progressDetail":{},"id":"90aca3c647fe"} +{"status":"Pulling fs layer","progressDetail":{},"id":"1c3245356213"} +{"status":"Waiting","progressDetail":{},"id":"1dd62f37c84c"} +{"status":"Pulling fs layer","progressDetail":{},"id":"61ebb123c1eb"} +{"status":"Pulling fs layer","progressDetail":{},"id":"0964b769d2c9"} +{"status":"Waiting","progressDetail":{},"id":"3192b2fa42db"} +{"status":"Pulling fs layer","progressDetail":{},"id":"87f7843f43cd"} +{"status":"Pulling fs layer","progressDetail":{},"id":"a89dbf94d794"} +{"status":"Waiting","progressDetail":{},"id":"ae190b8f66a7"} +{"status":"Pulling fs layer","progressDetail":{},"id":"f0d43ddca77f"} +{"status":"Pulling fs layer","progressDetail":{},"id":"7c674f0cb40c"} +{"status":"Waiting","progressDetail":{},"id":"97bb6e138460"} +{"status":"Pulling fs layer","progressDetail":{},"id":"b48a885b52bc"} +{"status":"Pulling fs layer","progressDetail":{},"id":"272cdf839cbb"} +{"status":"Pulling fs layer","progressDetail":{},"id":"50d054c97f4f"} +{"status":"Pulling fs layer","progressDetail":{},"id":"4c6bbd90b64d"} +{"status":"Waiting","progressDetail":{},"id":"2edb982d5170"} +{"status":"Pulling fs layer","progressDetail":{},"id":"4f4fb700ef54"} +{"status":"Waiting","progressDetail":{},"id":"7ddc8e6d6da9"} +{"status":"Waiting","progressDetail":{},"id":"b8cf53bbc6ba"} +{"status":"Waiting","progressDetail":{},"id":"25efb07e4521"} +{"status":"Waiting","progressDetail":{},"id":"0df6fd234b59"} +{"status":"Waiting","progressDetail":{},"id":"1c3245356213"} +{"status":"Waiting","progressDetail":{},"id":"61ebb123c1eb"} +{"status":"Waiting","progressDetail":{},"id":"8fc1ba8efe21"} +{"status":"Waiting","progressDetail":{},"id":"0964b769d2c9"} +{"status":"Waiting","progressDetail":{},"id":"87f7843f43cd"} +{"status":"Waiting","progressDetail":{},"id":"1f6f45e783b5"} +{"status":"Waiting","progressDetail":{},"id":"a89dbf94d794"} +{"status":"Waiting","progressDetail":{},"id":"43ea61082f68"} +{"status":"Waiting","progressDetail":{},"id":"f0d43ddca77f"} +{"status":"Waiting","progressDetail":{},"id":"7c674f0cb40c"} +{"status":"Waiting","progressDetail":{},"id":"b48a885b52bc"} +{"status":"Waiting","progressDetail":{},"id":"272cdf839cbb"} +{"status":"Waiting","progressDetail":{},"id":"50d054c97f4f"} +{"status":"Waiting","progressDetail":{},"id":"4c6bbd90b64d"} +{"status":"Waiting","progressDetail":{},"id":"4f4fb700ef54"} +{"status":"Waiting","progressDetail":{},"id":"4ab897fa6fbf"} +{"status":"Waiting","progressDetail":{},"id":"d837a2a1365e"} +{"status":"Waiting","progressDetail":{},"id":"988ae18fe41a"} +{"status":"Downloading","progressDetail":{"current":487,"total":850},"progress":"[============================\u003e ] 487B/850B","id":"ee671aafb583"} +{"status":"Downloading","progressDetail":{"current":485,"total":35355},"progress":"[\u003e ] 485B/35.35kB","id":"d83811f270d5"} +{"status":"Downloading","progressDetail":{"current":35355,"total":35355},"progress":"[==================================================\u003e] 35.35kB/35.35kB","id":"d83811f270d5"} +{"status":"Verifying Checksum","progressDetail":{},"id":"d83811f270d5"} +{"status":"Download complete","progressDetail":{},"id":"d83811f270d5"} +{"status":"Downloading","progressDetail":{"current":277600,"total":26683298},"progress":"[\u003e ] 277.6kB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":850,"total":850},"progress":"[==================================================\u003e] 850B/850B","id":"ee671aafb583"} +{"status":"Verifying Checksum","progressDetail":{},"id":"ee671aafb583"} +{"status":"Download complete","progressDetail":{},"id":"ee671aafb583"} +{"status":"Downloading","progressDetail":{"current":2218692,"total":26683298},"progress":"[====\u003e ] 2.219MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":4160196,"total":26683298},"progress":"[=======\u003e ] 4.16MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":6109892,"total":26683298},"progress":"[===========\u003e ] 6.11MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":7772868,"total":26683298},"progress":"[==============\u003e ] 7.773MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":9444036,"total":26683298},"progress":"[=================\u003e ] 9.444MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":163,"total":163},"progress":"[==================================================\u003e] 163B/163B","id":"7fc152dfb3a6"} +{"status":"Verifying Checksum","progressDetail":{},"id":"7fc152dfb3a6"} +{"status":"Download complete","progressDetail":{},"id":"7fc152dfb3a6"} +{"status":"Downloading","progressDetail":{"current":10832580,"total":26683298},"progress":"[====================\u003e ] 10.83MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":531179,"total":88111129},"progress":"[\u003e ] 531.2kB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":11668164,"total":26683298},"progress":"[=====================\u003e ] 11.67MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":1604331,"total":88111129},"progress":"[\u003e ] 1.604MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":12495556,"total":26683298},"progress":"[=======================\u003e ] 12.5MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":3209963,"total":88111129},"progress":"[=\u003e ] 3.21MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":13331140,"total":26683298},"progress":"[========================\u003e ] 13.33MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":4283115,"total":88111129},"progress":"[==\u003e ] 4.283MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":14166724,"total":26683298},"progress":"[==========================\u003e ] 14.17MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":5888747,"total":88111129},"progress":"[===\u003e ] 5.889MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":15280836,"total":26683298},"progress":"[============================\u003e ] 15.28MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":14318,"total":1391657},"progress":"[\u003e ] 14.32kB/1.392MB","id":"d837a2a1365e"} +{"status":"Downloading","progressDetail":{"current":6961899,"total":88111129},"progress":"[===\u003e ] 6.962MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":16116420,"total":26683298},"progress":"[==============================\u003e ] 16.12MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":936688,"total":1391657},"progress":"[=================================\u003e ] 936.7kB/1.392MB","id":"d837a2a1365e"} +{"status":"Verifying Checksum","progressDetail":{},"id":"d837a2a1365e"} +{"status":"Download complete","progressDetail":{},"id":"d837a2a1365e"} +{"status":"Downloading","progressDetail":{"current":8022763,"total":88111129},"progress":"[====\u003e ] 8.023MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":16931524,"total":26683298},"progress":"[===============================\u003e ] 16.93MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":18045636,"total":26683298},"progress":"[=================================\u003e ] 18.05MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":9632491,"total":88111129},"progress":"[=====\u003e ] 9.632MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":10709739,"total":88111129},"progress":"[======\u003e ] 10.71MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":19143364,"total":26683298},"progress":"[===================================\u003e ] 19.14MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":11778795,"total":88111129},"progress":"[======\u003e ] 11.78MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":20249284,"total":26683298},"progress":"[=====================================\u003e ] 20.25MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":12851947,"total":88111129},"progress":"[=======\u003e ] 12.85MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":21072580,"total":26683298},"progress":"[=======================================\u003e ] 21.07MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":14133,"total":1328346},"progress":"[\u003e ] 14.13kB/1.328MB","id":"988ae18fe41a"} +{"status":"Downloading","progressDetail":{"current":13933291,"total":88111129},"progress":"[=======\u003e ] 13.93MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":21908164,"total":26683298},"progress":"[=========================================\u003e ] 21.91MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":973511,"total":1328346},"progress":"[====================================\u003e ] 973.5kB/1.328MB","id":"988ae18fe41a"} +{"status":"Verifying Checksum","progressDetail":{},"id":"988ae18fe41a"} +{"status":"Download complete","progressDetail":{},"id":"988ae18fe41a"} +{"status":"Downloading","progressDetail":{"current":15014635,"total":88111129},"progress":"[========\u003e ] 15.01MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":22747844,"total":26683298},"progress":"[==========================================\u003e ] 22.75MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":16075499,"total":88111129},"progress":"[=========\u003e ] 16.08MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":23575236,"total":26683298},"progress":"[============================================\u003e ] 23.58MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":24414916,"total":26683298},"progress":"[=============================================\u003e ] 24.41MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":17132267,"total":88111129},"progress":"[=========\u003e ] 17.13MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":25250500,"total":26683298},"progress":"[===============================================\u003e ] 25.25MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":18213611,"total":88111129},"progress":"[==========\u003e ] 18.21MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":26073796,"total":26683298},"progress":"[================================================\u003e ] 26.07MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":19286763,"total":88111129},"progress":"[==========\u003e ] 19.29MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":490,"total":4478},"progress":"[=====\u003e ] 490B/4.478kB","id":"eeb8ef83b565"} +{"status":"Downloading","progressDetail":{"current":4478,"total":4478},"progress":"[==================================================\u003e] 4.478kB/4.478kB","id":"eeb8ef83b565"} +{"status":"Verifying Checksum","progressDetail":{},"id":"eeb8ef83b565"} +{"status":"Download complete","progressDetail":{},"id":"eeb8ef83b565"} +{"status":"Verifying Checksum","progressDetail":{},"id":"5667fdb72017"} +{"status":"Download complete","progressDetail":{},"id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":20892395,"total":88111129},"progress":"[===========\u003e ] 20.89MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Extracting","progressDetail":{"current":294912,"total":26683298},"progress":"[\u003e ] 294.9kB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":23050987,"total":88111129},"progress":"[=============\u003e ] 23.05MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Extracting","progressDetail":{"current":2654208,"total":26683298},"progress":"[====\u003e ] 2.654MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":25205483,"total":88111129},"progress":"[==============\u003e ] 25.21MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Extracting","progressDetail":{"current":6193152,"total":26683298},"progress":"[===========\u003e ] 6.193MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":27355883,"total":88111129},"progress":"[===============\u003e ] 27.36MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Extracting","progressDetail":{"current":8552448,"total":26683298},"progress":"[================\u003e ] 8.552MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":197,"total":197},"progress":"[==================================================\u003e] 197B/197B","id":"357fefdf9bc9"} +{"status":"Verifying Checksum","progressDetail":{},"id":"357fefdf9bc9"} +{"status":"Download complete","progressDetail":{},"id":"357fefdf9bc9"} +{"status":"Extracting","progressDetail":{"current":11796480,"total":26683298},"progress":"[======================\u003e ] 11.8MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":29510379,"total":88111129},"progress":"[================\u003e ] 29.51MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":277600,"total":27504647},"progress":"[\u003e ] 277.6kB/27.5MB","id":"45b746196f82"} +{"status":"Extracting","progressDetail":{"current":15040512,"total":26683298},"progress":"[============================\u003e ] 15.04MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":1391300,"total":27504647},"progress":"[==\u003e ] 1.391MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":31132395,"total":88111129},"progress":"[=================\u003e ] 31.13MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Extracting","progressDetail":{"current":17989632,"total":26683298},"progress":"[=================================\u003e ] 17.99MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":32754411,"total":88111129},"progress":"[==================\u003e ] 32.75MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":2230980,"total":27504647},"progress":"[====\u003e ] 2.231MB/27.5MB","id":"45b746196f82"} +{"status":"Extracting","progressDetail":{"current":22118400,"total":26683298},"progress":"[=========================================\u003e ] 22.12MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":33835755,"total":88111129},"progress":"[===================\u003e ] 33.84MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":3078852,"total":27504647},"progress":"[=====\u003e ] 3.079MB/27.5MB","id":"45b746196f82"} +{"status":"Extracting","progressDetail":{"current":24477696,"total":26683298},"progress":"[=============================================\u003e ] 24.48MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":52419,"total":5205016},"progress":"[\u003e ] 52.42kB/5.205MB","id":"fbf4ce20f8c2"} +{"status":"Downloading","progressDetail":{"current":34917099,"total":88111129},"progress":"[===================\u003e ] 34.92MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":3922628,"total":27504647},"progress":"[=======\u003e ] 3.923MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":912096,"total":5205016},"progress":"[========\u003e ] 912.1kB/5.205MB","id":"fbf4ce20f8c2"} +{"status":"Extracting","progressDetail":{"current":26247168,"total":26683298},"progress":"[=================================================\u003e ] 26.25MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":4487876,"total":27504647},"progress":"[========\u003e ] 4.488MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":35986155,"total":88111129},"progress":"[====================\u003e ] 35.99MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Extracting","progressDetail":{"current":26683298,"total":26683298},"progress":"[==================================================\u003e] 26.68MB/26.68MB","id":"5667fdb72017"} +{"status":"Downloading","progressDetail":{"current":1805024,"total":5205016},"progress":"[=================\u003e ] 1.805MB/5.205MB","id":"fbf4ce20f8c2"} +{"status":"Downloading","progressDetail":{"current":5044932,"total":27504647},"progress":"[=========\u003e ] 5.045MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":36522731,"total":88111129},"progress":"[====================\u003e ] 36.52MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":2550496,"total":5205016},"progress":"[========================\u003e ] 2.55MB/5.205MB","id":"fbf4ce20f8c2"} +{"status":"Downloading","progressDetail":{"current":5601988,"total":27504647},"progress":"[==========\u003e ] 5.602MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":3381984,"total":5205016},"progress":"[================================\u003e ] 3.382MB/5.205MB","id":"fbf4ce20f8c2"} +{"status":"Downloading","progressDetail":{"current":37063403,"total":88111129},"progress":"[=====================\u003e ] 37.06MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":6159044,"total":27504647},"progress":"[===========\u003e ] 6.159MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":4152032,"total":5205016},"progress":"[=======================================\u003e ] 4.152MB/5.205MB","id":"fbf4ce20f8c2"} +{"status":"Downloading","progressDetail":{"current":37604075,"total":88111129},"progress":"[=====================\u003e ] 37.6MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Pull complete","progressDetail":{},"id":"5667fdb72017"} +{"status":"Extracting","progressDetail":{"current":32768,"total":35355},"progress":"[==============================================\u003e ] 32.77kB/35.35kB","id":"d83811f270d5"} +{"status":"Extracting","progressDetail":{"current":35355,"total":35355},"progress":"[==================================================\u003e] 35.35kB/35.35kB","id":"d83811f270d5"} +{"status":"Downloading","progressDetail":{"current":5004000,"total":5205016},"progress":"[================================================\u003e ] 5.004MB/5.205MB","id":"fbf4ce20f8c2"} +{"status":"Downloading","progressDetail":{"current":6716100,"total":27504647},"progress":"[============\u003e ] 6.716MB/27.5MB","id":"45b746196f82"} +{"status":"Verifying Checksum","progressDetail":{},"id":"fbf4ce20f8c2"} +{"status":"Download complete","progressDetail":{},"id":"fbf4ce20f8c2"} +{"status":"Downloading","progressDetail":{"current":38144747,"total":88111129},"progress":"[=====================\u003e ] 38.14MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Pull complete","progressDetail":{},"id":"d83811f270d5"} +{"status":"Extracting","progressDetail":{"current":850,"total":850},"progress":"[==================================================\u003e] 850B/850B","id":"ee671aafb583"} +{"status":"Extracting","progressDetail":{"current":850,"total":850},"progress":"[==================================================\u003e] 850B/850B","id":"ee671aafb583"} +{"status":"Downloading","progressDetail":{"current":7293636,"total":27504647},"progress":"[=============\u003e ] 7.294MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":39213803,"total":88111129},"progress":"[======================\u003e ] 39.21MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":8129220,"total":27504647},"progress":"[==============\u003e ] 8.129MB/27.5MB","id":"45b746196f82"} +{"status":"Pull complete","progressDetail":{},"id":"ee671aafb583"} +{"status":"Extracting","progressDetail":{"current":163,"total":163},"progress":"[==================================================\u003e] 163B/163B","id":"7fc152dfb3a6"} +{"status":"Extracting","progressDetail":{"current":163,"total":163},"progress":"[==================================================\u003e] 163B/163B","id":"7fc152dfb3a6"} +{"status":"Downloading","progressDetail":{"current":40295147,"total":88111129},"progress":"[======================\u003e ] 40.3MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":8964804,"total":27504647},"progress":"[================\u003e ] 8.965MB/27.5MB","id":"45b746196f82"} +{"status":"Pull complete","progressDetail":{},"id":"7fc152dfb3a6"} +{"status":"Downloading","progressDetail":{"current":9800388,"total":27504647},"progress":"[=================\u003e ] 9.8MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":41368299,"total":88111129},"progress":"[=======================\u003e ] 41.37MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":49680,"total":4964709},"progress":"[\u003e ] 49.68kB/4.965MB","id":"90aca3c647fe"} +{"status":"Downloading","progressDetail":{"current":10635972,"total":27504647},"progress":"[===================\u003e ] 10.64MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":908013,"total":4964709},"progress":"[=========\u003e ] 908kB/4.965MB","id":"90aca3c647fe"} +{"status":"Downloading","progressDetail":{"current":41908971,"total":88111129},"progress":"[=======================\u003e ] 41.91MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":11193028,"total":27504647},"progress":"[====================\u003e ] 11.19MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":2038509,"total":4964709},"progress":"[====================\u003e ] 2.039MB/4.965MB","id":"90aca3c647fe"} +{"status":"Downloading","progressDetail":{"current":42449643,"total":88111129},"progress":"[========================\u003e ] 42.45MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":11750084,"total":27504647},"progress":"[=====================\u003e ] 11.75MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":3316461,"total":4964709},"progress":"[=================================\u003e ] 3.316MB/4.965MB","id":"90aca3c647fe"} +{"status":"Downloading","progressDetail":{"current":4791021,"total":4964709},"progress":"[================================================\u003e ] 4.791MB/4.965MB","id":"90aca3c647fe"} +{"status":"Verifying Checksum","progressDetail":{},"id":"90aca3c647fe"} +{"status":"Download complete","progressDetail":{},"id":"90aca3c647fe"} +{"status":"Downloading","progressDetail":{"current":12315332,"total":27504647},"progress":"[======================\u003e ] 12.32MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":42990315,"total":88111129},"progress":"[========================\u003e ] 42.99MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":13155012,"total":27504647},"progress":"[=======================\u003e ] 13.16MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":43530987,"total":88111129},"progress":"[========================\u003e ] 43.53MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":13990596,"total":27504647},"progress":"[=========================\u003e ] 13.99MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":44063467,"total":88111129},"progress":"[=========================\u003e ] 44.06MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":15112900,"total":27504647},"progress":"[===========================\u003e ] 15.11MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":45132523,"total":88111129},"progress":"[=========================\u003e ] 45.13MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":16235204,"total":27504647},"progress":"[=============================\u003e ] 16.24MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":52418,"total":5149051},"progress":"[\u003e ] 52.42kB/5.149MB","id":"1dd62f37c84c"} +{"status":"Downloading","progressDetail":{"current":1195147,"total":5149051},"progress":"[===========\u003e ] 1.195MB/5.149MB","id":"1dd62f37c84c"} +{"status":"Downloading","progressDetail":{"current":16792260,"total":27504647},"progress":"[==============================\u003e ] 16.79MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":45673195,"total":88111129},"progress":"[=========================\u003e ] 45.67MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":2702475,"total":5149051},"progress":"[==========================\u003e ] 2.702MB/5.149MB","id":"1dd62f37c84c"} +{"status":"Downloading","progressDetail":{"current":4078320,"total":5149051},"progress":"[=======================================\u003e ] 4.078MB/5.149MB","id":"1dd62f37c84c"} +{"status":"Downloading","progressDetail":{"current":17349316,"total":27504647},"progress":"[===============================\u003e ] 17.35MB/27.5MB","id":"45b746196f82"} +{"status":"Verifying Checksum","progressDetail":{},"id":"1dd62f37c84c"} +{"status":"Download complete","progressDetail":{},"id":"1dd62f37c84c"} +{"status":"Downloading","progressDetail":{"current":46213867,"total":88111129},"progress":"[==========================\u003e ] 46.21MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":17918660,"total":27504647},"progress":"[================================\u003e ] 17.92MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":19040964,"total":27504647},"progress":"[==================================\u003e ] 19.04MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":47295211,"total":88111129},"progress":"[==========================\u003e ] 47.3MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":20183748,"total":27504647},"progress":"[====================================\u003e ] 20.18MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":48368363,"total":88111129},"progress":"[===========================\u003e ] 48.37MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":21301956,"total":27504647},"progress":"[======================================\u003e ] 21.3MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":22432452,"total":27504647},"progress":"[========================================\u003e ] 22.43MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":38884,"total":3855277},"progress":"[\u003e ] 38.88kB/3.855MB","id":"3192b2fa42db"} +{"status":"Downloading","progressDetail":{"current":49445611,"total":88111129},"progress":"[============================\u003e ] 49.45MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":977632,"total":3855277},"progress":"[============\u003e ] 977.6kB/3.855MB","id":"3192b2fa42db"} +{"status":"Downloading","progressDetail":{"current":23268036,"total":27504647},"progress":"[==========================================\u003e ] 23.27MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":49986283,"total":88111129},"progress":"[============================\u003e ] 49.99MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":1895136,"total":3855277},"progress":"[========================\u003e ] 1.895MB/3.855MB","id":"3192b2fa42db"} +{"status":"Downloading","progressDetail":{"current":23833284,"total":27504647},"progress":"[===========================================\u003e ] 23.83MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":2939616,"total":3855277},"progress":"[======================================\u003e ] 2.94MB/3.855MB","id":"3192b2fa42db"} +{"status":"Downloading","progressDetail":{"current":24390340,"total":27504647},"progress":"[============================================\u003e ] 24.39MB/27.5MB","id":"45b746196f82"} +{"status":"Download complete","progressDetail":{},"id":"3192b2fa42db"} +{"status":"Downloading","progressDetail":{"current":50518763,"total":88111129},"progress":"[============================\u003e ] 50.52MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":24947396,"total":27504647},"progress":"[=============================================\u003e ] 24.95MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":51059435,"total":88111129},"progress":"[============================\u003e ] 51.06MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":25803460,"total":27504647},"progress":"[==============================================\u003e ] 25.8MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":26942148,"total":27504647},"progress":"[================================================\u003e ] 26.94MB/27.5MB","id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":52140779,"total":88111129},"progress":"[=============================\u003e ] 52.14MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":27504647,"total":27504647},"progress":"[==================================================\u003e] 27.5MB/27.5MB","id":"45b746196f82"} +{"status":"Verifying Checksum","progressDetail":{},"id":"45b746196f82"} +{"status":"Download complete","progressDetail":{},"id":"45b746196f82"} +{"status":"Downloading","progressDetail":{"current":53222123,"total":88111129},"progress":"[==============================\u003e ] 53.22MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":51194,"total":4983195},"progress":"[\u003e ] 51.19kB/4.983MB","id":"ae190b8f66a7"} +{"status":"Downloading","progressDetail":{"current":54299371,"total":88111129},"progress":"[==============================\u003e ] 54.3MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":1268464,"total":4983195},"progress":"[============\u003e ] 1.268MB/4.983MB","id":"ae190b8f66a7"} +{"status":"Downloading","progressDetail":{"current":54827755,"total":88111129},"progress":"[===============================\u003e ] 54.83MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":2767600,"total":4983195},"progress":"[===========================\u003e ] 2.768MB/4.983MB","id":"ae190b8f66a7"} +{"status":"Downloading","progressDetail":{"current":4528880,"total":4983195},"progress":"[=============================================\u003e ] 4.529MB/4.983MB","id":"ae190b8f66a7"} +{"status":"Downloading","progressDetail":{"current":55368427,"total":88111129},"progress":"[===============================\u003e ] 55.37MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Verifying Checksum","progressDetail":{},"id":"ae190b8f66a7"} +{"status":"Download complete","progressDetail":{},"id":"ae190b8f66a7"} +{"status":"Downloading","progressDetail":{"current":63614,"total":6103207},"progress":"[\u003e ] 63.61kB/6.103MB","id":"97bb6e138460"} +{"status":"Downloading","progressDetail":{"current":56449771,"total":88111129},"progress":"[================================\u003e ] 56.45MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":1530606,"total":6103207},"progress":"[============\u003e ] 1.531MB/6.103MB","id":"97bb6e138460"} +{"status":"Downloading","progressDetail":{"current":3193582,"total":6103207},"progress":"[==========================\u003e ] 3.194MB/6.103MB","id":"97bb6e138460"} +{"status":"Downloading","progressDetail":{"current":56990443,"total":88111129},"progress":"[================================\u003e ] 56.99MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":4786926,"total":6103207},"progress":"[=======================================\u003e ] 4.787MB/6.103MB","id":"97bb6e138460"} +{"status":"Downloading","progressDetail":{"current":57531115,"total":88111129},"progress":"[================================\u003e ] 57.53MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Download complete","progressDetail":{},"id":"97bb6e138460"} +{"status":"Downloading","progressDetail":{"current":489,"total":787},"progress":"[===============================\u003e ] 489B/787B","id":"2edb982d5170"} +{"status":"Downloading","progressDetail":{"current":58612459,"total":88111129},"progress":"[=================================\u003e ] 58.61MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":787,"total":787},"progress":"[==================================================\u003e] 787B/787B","id":"2edb982d5170"} +{"status":"Verifying Checksum","progressDetail":{},"id":"2edb982d5170"} +{"status":"Download complete","progressDetail":{},"id":"2edb982d5170"} +{"status":"Downloading","progressDetail":{"current":60213995,"total":88111129},"progress":"[==================================\u003e ] 60.21MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":61827819,"total":88111129},"progress":"[===================================\u003e ] 61.83MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":63449835,"total":88111129},"progress":"[====================================\u003e ] 63.45MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":65071851,"total":88111129},"progress":"[====================================\u003e ] 65.07MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":49803,"total":4894860},"progress":"[\u003e ] 49.8kB/4.895MB","id":"7ddc8e6d6da9"} +{"status":"Downloading","progressDetail":{"current":49681,"total":4953791},"progress":"[\u003e ] 49.68kB/4.954MB","id":"0df6fd234b59"} +{"status":"Downloading","progressDetail":{"current":912099,"total":4894860},"progress":"[=========\u003e ] 912.1kB/4.895MB","id":"7ddc8e6d6da9"} +{"status":"Downloading","progressDetail":{"current":66145003,"total":88111129},"progress":"[=====================================\u003e ] 66.15MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":748270,"total":4953791},"progress":"[=======\u003e ] 748.3kB/4.954MB","id":"0df6fd234b59"} +{"status":"Downloading","progressDetail":{"current":1702627,"total":4894860},"progress":"[=================\u003e ] 1.703MB/4.895MB","id":"7ddc8e6d6da9"} +{"status":"Downloading","progressDetail":{"current":67205867,"total":88111129},"progress":"[======================================\u003e ] 67.21MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":1678062,"total":4953791},"progress":"[================\u003e ] 1.678MB/4.954MB","id":"0df6fd234b59"} +{"status":"Downloading","progressDetail":{"current":2194147,"total":4894860},"progress":"[======================\u003e ] 2.194MB/4.895MB","id":"7ddc8e6d6da9"} +{"status":"Downloading","progressDetail":{"current":67746539,"total":88111129},"progress":"[======================================\u003e ] 67.75MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":2648814,"total":4953791},"progress":"[==========================\u003e ] 2.649MB/4.954MB","id":"0df6fd234b59"} +{"status":"Downloading","progressDetail":{"current":2743011,"total":4894860},"progress":"[============================\u003e ] 2.743MB/4.895MB","id":"7ddc8e6d6da9"} +{"status":"Downloading","progressDetail":{"current":68287211,"total":88111129},"progress":"[======================================\u003e ] 68.29MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":3697390,"total":4953791},"progress":"[=====================================\u003e ] 3.697MB/4.954MB","id":"0df6fd234b59"} +{"status":"Downloading","progressDetail":{"current":3381987,"total":4894860},"progress":"[==================================\u003e ] 3.382MB/4.895MB","id":"7ddc8e6d6da9"} +{"status":"Downloading","progressDetail":{"current":4774638,"total":4953791},"progress":"[================================================\u003e ] 4.775MB/4.954MB","id":"0df6fd234b59"} +{"status":"Downloading","progressDetail":{"current":68827883,"total":88111129},"progress":"[=======================================\u003e ] 68.83MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":4953791,"total":4953791},"progress":"[==================================================\u003e] 4.954MB/4.954MB","id":"0df6fd234b59"} +{"status":"Verifying Checksum","progressDetail":{},"id":"0df6fd234b59"} +{"status":"Download complete","progressDetail":{},"id":"0df6fd234b59"} +{"status":"Downloading","progressDetail":{"current":4004579,"total":4894860},"progress":"[========================================\u003e ] 4.005MB/4.895MB","id":"7ddc8e6d6da9"} +{"status":"Downloading","progressDetail":{"current":4893411,"total":4894860},"progress":"[=================================================\u003e ] 4.893MB/4.895MB","id":"7ddc8e6d6da9"} +{"status":"Verifying Checksum","progressDetail":{},"id":"7ddc8e6d6da9"} +{"status":"Download complete","progressDetail":{},"id":"7ddc8e6d6da9"} +{"status":"Downloading","progressDetail":{"current":69909227,"total":88111129},"progress":"[=======================================\u003e ] 69.91MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":71527147,"total":88111129},"progress":"[========================================\u003e ] 71.53MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":73149163,"total":88111129},"progress":"[=========================================\u003e ] 73.15MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":74771179,"total":88111129},"progress":"[==========================================\u003e ] 74.77MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":63573,"total":6137526},"progress":"[\u003e ] 63.57kB/6.138MB","id":"8fc1ba8efe21"} +{"status":"Downloading","progressDetail":{"current":75311851,"total":88111129},"progress":"[==========================================\u003e ] 75.31MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":1317559,"total":6137526},"progress":"[==========\u003e ] 1.318MB/6.138MB","id":"8fc1ba8efe21"} +{"status":"Downloading","progressDetail":{"current":2710199,"total":6137526},"progress":"[======================\u003e ] 2.71MB/6.138MB","id":"8fc1ba8efe21"} +{"status":"Downloading","progressDetail":{"current":38729,"total":3854415},"progress":"[\u003e ] 38.73kB/3.854MB","id":"1f6f45e783b5"} +{"status":"Downloading","progressDetail":{"current":76368619,"total":88111129},"progress":"[===========================================\u003e ] 76.37MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":3783351,"total":6137526},"progress":"[==============================\u003e ] 3.783MB/6.138MB","id":"8fc1ba8efe21"} +{"status":"Downloading","progressDetail":{"current":658157,"total":3854415},"progress":"[========\u003e ] 658.2kB/3.854MB","id":"1f6f45e783b5"} +{"status":"Downloading","progressDetail":{"current":4520631,"total":6137526},"progress":"[====================================\u003e ] 4.521MB/6.138MB","id":"8fc1ba8efe21"} +{"status":"Downloading","progressDetail":{"current":1350381,"total":3854415},"progress":"[=================\u003e ] 1.35MB/3.854MB","id":"1f6f45e783b5"} +{"status":"Downloading","progressDetail":{"current":5364407,"total":6137526},"progress":"[===========================================\u003e ] 5.364MB/6.138MB","id":"8fc1ba8efe21"} +{"status":"Downloading","progressDetail":{"current":77445867,"total":88111129},"progress":"[===========================================\u003e ] 77.45MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":2153197,"total":3854415},"progress":"[===========================\u003e ] 2.153MB/3.854MB","id":"1f6f45e783b5"} +{"status":"Verifying Checksum","progressDetail":{},"id":"8fc1ba8efe21"} +{"status":"Download complete","progressDetail":{},"id":"8fc1ba8efe21"} +{"status":"Downloading","progressDetail":{"current":77986539,"total":88111129},"progress":"[============================================\u003e ] 77.99MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":3021549,"total":3854415},"progress":"[=======================================\u003e ] 3.022MB/3.854MB","id":"1f6f45e783b5"} +{"status":"Verifying Checksum","progressDetail":{},"id":"1f6f45e783b5"} +{"status":"Download complete","progressDetail":{},"id":"1f6f45e783b5"} +{"status":"Downloading","progressDetail":{"current":79067883,"total":88111129},"progress":"[============================================\u003e ] 79.07MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":80149227,"total":88111129},"progress":"[=============================================\u003e ] 80.15MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":81767147,"total":88111129},"progress":"[==============================================\u003e ] 81.77MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":52419,"total":5222290},"progress":"[\u003e ] 52.42kB/5.222MB","id":"43ea61082f68"} +{"status":"Downloading","progressDetail":{"current":1055455,"total":5222290},"progress":"[==========\u003e ] 1.055MB/5.222MB","id":"43ea61082f68"} +{"status":"Downloading","progressDetail":{"current":83372779,"total":88111129},"progress":"[===============================================\u003e ] 83.37MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":2333407,"total":5222290},"progress":"[======================\u003e ] 2.333MB/5.222MB","id":"43ea61082f68"} +{"status":"Downloading","progressDetail":{"current":35991,"total":3564359},"progress":"[\u003e ] 35.99kB/3.564MB","id":"b8cf53bbc6ba"} +{"status":"Downloading","progressDetail":{"current":84454123,"total":88111129},"progress":"[===============================================\u003e ] 84.45MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":3300063,"total":5222290},"progress":"[===============================\u003e ] 3.3MB/5.222MB","id":"43ea61082f68"} +{"status":"Downloading","progressDetail":{"current":752366,"total":3564359},"progress":"[==========\u003e ] 752.4kB/3.564MB","id":"b8cf53bbc6ba"} +{"status":"Downloading","progressDetail":{"current":3979999,"total":5222290},"progress":"[======================================\u003e ] 3.98MB/5.222MB","id":"43ea61082f68"} +{"status":"Downloading","progressDetail":{"current":1743598,"total":3564359},"progress":"[========================\u003e ] 1.744MB/3.564MB","id":"b8cf53bbc6ba"} +{"status":"Downloading","progressDetail":{"current":85527275,"total":88111129},"progress":"[================================================\u003e ] 85.53MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":4537055,"total":5222290},"progress":"[===========================================\u003e ] 4.537MB/5.222MB","id":"43ea61082f68"} +{"status":"Downloading","progressDetail":{"current":2833134,"total":3564359},"progress":"[=======================================\u003e ] 2.833MB/3.564MB","id":"b8cf53bbc6ba"} +{"status":"Downloading","progressDetail":{"current":5077727,"total":5222290},"progress":"[================================================\u003e ] 5.078MB/5.222MB","id":"43ea61082f68"} +{"status":"Verifying Checksum","progressDetail":{},"id":"b8cf53bbc6ba"} +{"status":"Download complete","progressDetail":{},"id":"b8cf53bbc6ba"} +{"status":"Verifying Checksum","progressDetail":{},"id":"43ea61082f68"} +{"status":"Download complete","progressDetail":{},"id":"43ea61082f68"} +{"status":"Downloading","progressDetail":{"current":86067947,"total":88111129},"progress":"[================================================\u003e ] 86.07MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":87132907,"total":88111129},"progress":"[=================================================\u003e ] 87.13MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Verifying Checksum","progressDetail":{},"id":"4ab897fa6fbf"} +{"status":"Download complete","progressDetail":{},"id":"4ab897fa6fbf"} +{"status":"Extracting","progressDetail":{"current":557056,"total":88111129},"progress":"[\u003e ] 557.1kB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":52418,"total":5120108},"progress":"[\u003e ] 52.42kB/5.12MB","id":"1c3245356213"} +{"status":"Downloading","progressDetail":{"current":489,"total":790},"progress":"[==============================\u003e ] 489B/790B","id":"25efb07e4521"} +{"status":"Extracting","progressDetail":{"current":5013504,"total":88111129},"progress":"[==\u003e ] 5.014MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":790,"total":790},"progress":"[==================================================\u003e] 790B/790B","id":"25efb07e4521"} +{"status":"Verifying Checksum","progressDetail":{},"id":"25efb07e4521"} +{"status":"Download complete","progressDetail":{},"id":"25efb07e4521"} +{"status":"Downloading","progressDetail":{"current":1764079,"total":5120108},"progress":"[=================\u003e ] 1.764MB/5.12MB","id":"1c3245356213"} +{"status":"Extracting","progressDetail":{"current":8355840,"total":88111129},"progress":"[====\u003e ] 8.356MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":3635951,"total":5120108},"progress":"[===================================\u003e ] 3.636MB/5.12MB","id":"1c3245356213"} +{"status":"Verifying Checksum","progressDetail":{},"id":"1c3245356213"} +{"status":"Download complete","progressDetail":{},"id":"1c3245356213"} +{"status":"Extracting","progressDetail":{"current":11141120,"total":88111129},"progress":"[======\u003e ] 11.14MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":52419,"total":5117023},"progress":"[\u003e ] 52.42kB/5.117MB","id":"61ebb123c1eb"} +{"status":"Extracting","progressDetail":{"current":13369344,"total":88111129},"progress":"[=======\u003e ] 13.37MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":1596142,"total":5117023},"progress":"[===============\u003e ] 1.596MB/5.117MB","id":"61ebb123c1eb"} +{"status":"Extracting","progressDetail":{"current":13926400,"total":88111129},"progress":"[=======\u003e ] 13.93MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":3242734,"total":5117023},"progress":"[===============================\u003e ] 3.243MB/5.117MB","id":"61ebb123c1eb"} +{"status":"Downloading","progressDetail":{"current":55157,"total":5384215},"progress":"[\u003e ] 55.16kB/5.384MB","id":"0964b769d2c9"} +{"status":"Downloading","progressDetail":{"current":4635374,"total":5117023},"progress":"[=============================================\u003e ] 4.635MB/5.117MB","id":"61ebb123c1eb"} +{"status":"Extracting","progressDetail":{"current":15040512,"total":88111129},"progress":"[========\u003e ] 15.04MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Verifying Checksum","progressDetail":{},"id":"61ebb123c1eb"} +{"status":"Download complete","progressDetail":{},"id":"61ebb123c1eb"} +{"status":"Downloading","progressDetail":{"current":989937,"total":5384215},"progress":"[=========\u003e ] 989.9kB/5.384MB","id":"0964b769d2c9"} +{"status":"Extracting","progressDetail":{"current":15597568,"total":88111129},"progress":"[========\u003e ] 15.6MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":2558705,"total":5384215},"progress":"[=======================\u003e ] 2.559MB/5.384MB","id":"0964b769d2c9"} +{"status":"Extracting","progressDetail":{"current":18382848,"total":88111129},"progress":"[==========\u003e ] 18.38MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":4311793,"total":5384215},"progress":"[========================================\u003e ] 4.312MB/5.384MB","id":"0964b769d2c9"} +{"status":"Downloading","progressDetail":{"current":53788,"total":5252487},"progress":"[\u003e ] 53.79kB/5.252MB","id":"87f7843f43cd"} +{"status":"Extracting","progressDetail":{"current":22839296,"total":88111129},"progress":"[============\u003e ] 22.84MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":5212913,"total":5384215},"progress":"[================================================\u003e ] 5.213MB/5.384MB","id":"0964b769d2c9"} +{"status":"Downloading","progressDetail":{"current":846577,"total":5252487},"progress":"[========\u003e ] 846.6kB/5.252MB","id":"87f7843f43cd"} +{"status":"Verifying Checksum","progressDetail":{},"id":"0964b769d2c9"} +{"status":"Download complete","progressDetail":{},"id":"0964b769d2c9"} +{"status":"Extracting","progressDetail":{"current":26181632,"total":88111129},"progress":"[==============\u003e ] 26.18MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":2628337,"total":5252487},"progress":"[=========================\u003e ] 2.628MB/5.252MB","id":"87f7843f43cd"} +{"status":"Extracting","progressDetail":{"current":30638080,"total":88111129},"progress":"[=================\u003e ] 30.64MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":4340465,"total":5252487},"progress":"[=========================================\u003e ] 4.34MB/5.252MB","id":"87f7843f43cd"} +{"status":"Download complete","progressDetail":{},"id":"87f7843f43cd"} +{"status":"Extracting","progressDetail":{"current":33423360,"total":88111129},"progress":"[==================\u003e ] 33.42MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":51204,"total":5015856},"progress":"[\u003e ] 51.2kB/5.016MB","id":"a89dbf94d794"} +{"status":"Extracting","progressDetail":{"current":36208640,"total":88111129},"progress":"[====================\u003e ] 36.21MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":1624816,"total":5015856},"progress":"[================\u003e ] 1.625MB/5.016MB","id":"a89dbf94d794"} +{"status":"Extracting","progressDetail":{"current":38436864,"total":88111129},"progress":"[=====================\u003e ] 38.44MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":3373808,"total":5015856},"progress":"[=================================\u003e ] 3.374MB/5.016MB","id":"a89dbf94d794"} +{"status":"Extracting","progressDetail":{"current":40665088,"total":88111129},"progress":"[=======================\u003e ] 40.67MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":53910,"total":5310566},"progress":"[\u003e ] 53.91kB/5.311MB","id":"f0d43ddca77f"} +{"status":"Downloading","progressDetail":{"current":4905712,"total":5015856},"progress":"[================================================\u003e ] 4.906MB/5.016MB","id":"a89dbf94d794"} +{"status":"Verifying Checksum","progressDetail":{},"id":"a89dbf94d794"} +{"status":"Download complete","progressDetail":{},"id":"a89dbf94d794"} +{"status":"Downloading","progressDetail":{"current":1313521,"total":5310566},"progress":"[============\u003e ] 1.314MB/5.311MB","id":"f0d43ddca77f"} +{"status":"Extracting","progressDetail":{"current":44007424,"total":88111129},"progress":"[========================\u003e ] 44.01MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Extracting","progressDetail":{"current":46792704,"total":88111129},"progress":"[==========================\u003e ] 46.79MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":3082993,"total":5310566},"progress":"[=============================\u003e ] 3.083MB/5.311MB","id":"f0d43ddca77f"} +{"status":"Downloading","progressDetail":{"current":49836,"total":4915049},"progress":"[\u003e ] 49.84kB/4.915MB","id":"7c674f0cb40c"} +{"status":"Downloading","progressDetail":{"current":4373233,"total":5310566},"progress":"[=========================================\u003e ] 4.373MB/5.311MB","id":"f0d43ddca77f"} +{"status":"Extracting","progressDetail":{"current":48463872,"total":88111129},"progress":"[===========================\u003e ] 48.46MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":711407,"total":4915049},"progress":"[=======\u003e ] 711.4kB/4.915MB","id":"7c674f0cb40c"} +{"status":"Verifying Checksum","progressDetail":{},"id":"f0d43ddca77f"} +{"status":"Download complete","progressDetail":{},"id":"f0d43ddca77f"} +{"status":"Downloading","progressDetail":{"current":1710831,"total":4915049},"progress":"[=================\u003e ] 1.711MB/4.915MB","id":"7c674f0cb40c"} +{"status":"Extracting","progressDetail":{"current":52363264,"total":88111129},"progress":"[=============================\u003e ] 52.36MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":3504879,"total":4915049},"progress":"[===================================\u003e ] 3.505MB/4.915MB","id":"7c674f0cb40c"} +{"status":"Extracting","progressDetail":{"current":55705600,"total":88111129},"progress":"[===============================\u003e ] 55.71MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":4905711,"total":4915049},"progress":"[=================================================\u003e ] 4.906MB/4.915MB","id":"7c674f0cb40c"} +{"status":"Verifying Checksum","progressDetail":{},"id":"7c674f0cb40c"} +{"status":"Download complete","progressDetail":{},"id":"7c674f0cb40c"} +{"status":"Extracting","progressDetail":{"current":58490880,"total":88111129},"progress":"[=================================\u003e ] 58.49MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":52419,"total":5119213},"progress":"[\u003e ] 52.42kB/5.119MB","id":"b48a885b52bc"} +{"status":"Extracting","progressDetail":{"current":61276160,"total":88111129},"progress":"[==================================\u003e ] 61.28MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":1333999,"total":5119213},"progress":"[=============\u003e ] 1.334MB/5.119MB","id":"b48a885b52bc"} +{"status":"Downloading","progressDetail":{"current":2657007,"total":5119213},"progress":"[=========================\u003e ] 2.657MB/5.119MB","id":"b48a885b52bc"} +{"status":"Extracting","progressDetail":{"current":64061440,"total":88111129},"progress":"[====================================\u003e ] 64.06MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Download complete","progressDetail":{},"id":"272cdf839cbb"} +{"status":"Downloading","progressDetail":{"current":4344559,"total":5119213},"progress":"[==========================================\u003e ] 4.345MB/5.119MB","id":"b48a885b52bc"} +{"status":"Extracting","progressDetail":{"current":66289664,"total":88111129},"progress":"[=====================================\u003e ] 66.29MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Verifying Checksum","progressDetail":{},"id":"b48a885b52bc"} +{"status":"Download complete","progressDetail":{},"id":"b48a885b52bc"} +{"status":"Extracting","progressDetail":{"current":70746112,"total":88111129},"progress":"[========================================\u003e ] 70.75MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Extracting","progressDetail":{"current":73531392,"total":88111129},"progress":"[=========================================\u003e ] 73.53MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Extracting","progressDetail":{"current":77430784,"total":88111129},"progress":"[===========================================\u003e ] 77.43MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Download complete","progressDetail":{},"id":"50d054c97f4f"} +{"status":"Downloading","progressDetail":{"current":488,"total":1069},"progress":"[======================\u003e ] 488B/1.069kB","id":"4c6bbd90b64d"} +{"status":"Extracting","progressDetail":{"current":80216064,"total":88111129},"progress":"[=============================================\u003e ] 80.22MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Downloading","progressDetail":{"current":1069,"total":1069},"progress":"[==================================================\u003e] 1.069kB/1.069kB","id":"4c6bbd90b64d"} +{"status":"Verifying Checksum","progressDetail":{},"id":"4c6bbd90b64d"} +{"status":"Download complete","progressDetail":{},"id":"4c6bbd90b64d"} +{"status":"Downloading","progressDetail":{"current":32,"total":32},"progress":"[==================================================\u003e] 32B/32B","id":"4f4fb700ef54"} +{"status":"Verifying Checksum","progressDetail":{},"id":"4f4fb700ef54"} +{"status":"Download complete","progressDetail":{},"id":"4f4fb700ef54"} +{"status":"Extracting","progressDetail":{"current":81887232,"total":88111129},"progress":"[==============================================\u003e ] 81.89MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Extracting","progressDetail":{"current":83558400,"total":88111129},"progress":"[===============================================\u003e ] 83.56MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Extracting","progressDetail":{"current":85229568,"total":88111129},"progress":"[================================================\u003e ] 85.23MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Extracting","progressDetail":{"current":86900736,"total":88111129},"progress":"[=================================================\u003e ] 86.9MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Extracting","progressDetail":{"current":88014848,"total":88111129},"progress":"[=================================================\u003e ] 88.01MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Extracting","progressDetail":{"current":88111129,"total":88111129},"progress":"[==================================================\u003e] 88.11MB/88.11MB","id":"4ab897fa6fbf"} +{"status":"Pull complete","progressDetail":{},"id":"4ab897fa6fbf"} +{"status":"Extracting","progressDetail":{"current":32768,"total":1391657},"progress":"[=\u003e ] 32.77kB/1.392MB","id":"d837a2a1365e"} +{"status":"Extracting","progressDetail":{"current":327680,"total":1391657},"progress":"[===========\u003e ] 327.7kB/1.392MB","id":"d837a2a1365e"} +{"status":"Extracting","progressDetail":{"current":1391657,"total":1391657},"progress":"[==================================================\u003e] 1.392MB/1.392MB","id":"d837a2a1365e"} +{"status":"Extracting","progressDetail":{"current":1391657,"total":1391657},"progress":"[==================================================\u003e] 1.392MB/1.392MB","id":"d837a2a1365e"} +{"status":"Pull complete","progressDetail":{},"id":"d837a2a1365e"} +{"status":"Extracting","progressDetail":{"current":32768,"total":1328346},"progress":"[=\u003e ] 32.77kB/1.328MB","id":"988ae18fe41a"} +{"status":"Extracting","progressDetail":{"current":753664,"total":1328346},"progress":"[============================\u003e ] 753.7kB/1.328MB","id":"988ae18fe41a"} +{"status":"Extracting","progressDetail":{"current":1328346,"total":1328346},"progress":"[==================================================\u003e] 1.328MB/1.328MB","id":"988ae18fe41a"} +{"status":"Extracting","progressDetail":{"current":1328346,"total":1328346},"progress":"[==================================================\u003e] 1.328MB/1.328MB","id":"988ae18fe41a"} +{"status":"Pull complete","progressDetail":{},"id":"988ae18fe41a"} +{"status":"Extracting","progressDetail":{"current":4478,"total":4478},"progress":"[==================================================\u003e] 4.478kB/4.478kB","id":"eeb8ef83b565"} +{"status":"Extracting","progressDetail":{"current":4478,"total":4478},"progress":"[==================================================\u003e] 4.478kB/4.478kB","id":"eeb8ef83b565"} +{"status":"Pull complete","progressDetail":{},"id":"eeb8ef83b565"} +{"status":"Extracting","progressDetail":{"current":197,"total":197},"progress":"[==================================================\u003e] 197B/197B","id":"357fefdf9bc9"} +{"status":"Extracting","progressDetail":{"current":197,"total":197},"progress":"[==================================================\u003e] 197B/197B","id":"357fefdf9bc9"} +{"status":"Pull complete","progressDetail":{},"id":"357fefdf9bc9"} +{"status":"Extracting","progressDetail":{"current":294912,"total":27504647},"progress":"[\u003e ] 294.9kB/27.5MB","id":"45b746196f82"} +{"status":"Extracting","progressDetail":{"current":589824,"total":27504647},"progress":"[=\u003e ] 589.8kB/27.5MB","id":"45b746196f82"} +{"status":"Extracting","progressDetail":{"current":5013504,"total":27504647},"progress":"[=========\u003e ] 5.014MB/27.5MB","id":"45b746196f82"} +{"status":"Extracting","progressDetail":{"current":9142272,"total":27504647},"progress":"[================\u003e ] 9.142MB/27.5MB","id":"45b746196f82"} +{"status":"Extracting","progressDetail":{"current":13565952,"total":27504647},"progress":"[========================\u003e ] 13.57MB/27.5MB","id":"45b746196f82"} +{"status":"Extracting","progressDetail":{"current":16515072,"total":27504647},"progress":"[==============================\u003e ] 16.52MB/27.5MB","id":"45b746196f82"} +{"status":"Extracting","progressDetail":{"current":18579456,"total":27504647},"progress":"[=================================\u003e ] 18.58MB/27.5MB","id":"45b746196f82"} +{"status":"Extracting","progressDetail":{"current":21528576,"total":27504647},"progress":"[=======================================\u003e ] 21.53MB/27.5MB","id":"45b746196f82"} +{"status":"Extracting","progressDetail":{"current":25657344,"total":27504647},"progress":"[==============================================\u003e ] 25.66MB/27.5MB","id":"45b746196f82"} +{"status":"Extracting","progressDetail":{"current":27504647,"total":27504647},"progress":"[==================================================\u003e] 27.5MB/27.5MB","id":"45b746196f82"} +{"status":"Pull complete","progressDetail":{},"id":"45b746196f82"} +{"status":"Extracting","progressDetail":{"current":65536,"total":5205016},"progress":"[\u003e ] 65.54kB/5.205MB","id":"fbf4ce20f8c2"} +{"status":"Extracting","progressDetail":{"current":1048576,"total":5205016},"progress":"[==========\u003e ] 1.049MB/5.205MB","id":"fbf4ce20f8c2"} +{"status":"Extracting","progressDetail":{"current":5205016,"total":5205016},"progress":"[==================================================\u003e] 5.205MB/5.205MB","id":"fbf4ce20f8c2"} +{"status":"Pull complete","progressDetail":{},"id":"fbf4ce20f8c2"} +{"status":"Extracting","progressDetail":{"current":65536,"total":4964709},"progress":"[\u003e ] 65.54kB/4.965MB","id":"90aca3c647fe"} +{"status":"Extracting","progressDetail":{"current":1245184,"total":4964709},"progress":"[============\u003e ] 1.245MB/4.965MB","id":"90aca3c647fe"} +{"status":"Extracting","progressDetail":{"current":4964709,"total":4964709},"progress":"[==================================================\u003e] 4.965MB/4.965MB","id":"90aca3c647fe"} +{"status":"Pull complete","progressDetail":{},"id":"90aca3c647fe"} +{"status":"Extracting","progressDetail":{"current":65536,"total":5149051},"progress":"[\u003e ] 65.54kB/5.149MB","id":"1dd62f37c84c"} +{"status":"Extracting","progressDetail":{"current":393216,"total":5149051},"progress":"[===\u003e ] 393.2kB/5.149MB","id":"1dd62f37c84c"} +{"status":"Extracting","progressDetail":{"current":5149051,"total":5149051},"progress":"[==================================================\u003e] 5.149MB/5.149MB","id":"1dd62f37c84c"} +{"status":"Pull complete","progressDetail":{},"id":"1dd62f37c84c"} +{"status":"Extracting","progressDetail":{"current":65536,"total":3855277},"progress":"[\u003e ] 65.54kB/3.855MB","id":"3192b2fa42db"} +{"status":"Extracting","progressDetail":{"current":851968,"total":3855277},"progress":"[===========\u003e ] 852kB/3.855MB","id":"3192b2fa42db"} +{"status":"Extracting","progressDetail":{"current":3855277,"total":3855277},"progress":"[==================================================\u003e] 3.855MB/3.855MB","id":"3192b2fa42db"} +{"status":"Extracting","progressDetail":{"current":3855277,"total":3855277},"progress":"[==================================================\u003e] 3.855MB/3.855MB","id":"3192b2fa42db"} +{"status":"Pull complete","progressDetail":{},"id":"3192b2fa42db"} +{"status":"Extracting","progressDetail":{"current":65536,"total":4983195},"progress":"[\u003e ] 65.54kB/4.983MB","id":"ae190b8f66a7"} +{"status":"Extracting","progressDetail":{"current":327680,"total":4983195},"progress":"[===\u003e ] 327.7kB/4.983MB","id":"ae190b8f66a7"} +{"status":"Extracting","progressDetail":{"current":4980736,"total":4983195},"progress":"[=================================================\u003e ] 4.981MB/4.983MB","id":"ae190b8f66a7"} +{"status":"Extracting","progressDetail":{"current":4983195,"total":4983195},"progress":"[==================================================\u003e] 4.983MB/4.983MB","id":"ae190b8f66a7"} +{"status":"Pull complete","progressDetail":{},"id":"ae190b8f66a7"} +{"status":"Extracting","progressDetail":{"current":65536,"total":6103207},"progress":"[\u003e ] 65.54kB/6.103MB","id":"97bb6e138460"} +{"status":"Extracting","progressDetail":{"current":327680,"total":6103207},"progress":"[==\u003e ] 327.7kB/6.103MB","id":"97bb6e138460"} +{"status":"Extracting","progressDetail":{"current":3670016,"total":6103207},"progress":"[==============================\u003e ] 3.67MB/6.103MB","id":"97bb6e138460"} +{"status":"Extracting","progressDetail":{"current":6103207,"total":6103207},"progress":"[==================================================\u003e] 6.103MB/6.103MB","id":"97bb6e138460"} +{"status":"Pull complete","progressDetail":{},"id":"97bb6e138460"} +{"status":"Extracting","progressDetail":{"current":787,"total":787},"progress":"[==================================================\u003e] 787B/787B","id":"2edb982d5170"} +{"status":"Extracting","progressDetail":{"current":787,"total":787},"progress":"[==================================================\u003e] 787B/787B","id":"2edb982d5170"} +{"status":"Pull complete","progressDetail":{},"id":"2edb982d5170"} +{"status":"Extracting","progressDetail":{"current":65536,"total":4894860},"progress":"[\u003e ] 65.54kB/4.895MB","id":"7ddc8e6d6da9"} +{"status":"Extracting","progressDetail":{"current":327680,"total":4894860},"progress":"[===\u003e ] 327.7kB/4.895MB","id":"7ddc8e6d6da9"} +{"status":"Extracting","progressDetail":{"current":3735552,"total":4894860},"progress":"[======================================\u003e ] 3.736MB/4.895MB","id":"7ddc8e6d6da9"} +{"status":"Extracting","progressDetail":{"current":4894860,"total":4894860},"progress":"[==================================================\u003e] 4.895MB/4.895MB","id":"7ddc8e6d6da9"} +{"status":"Pull complete","progressDetail":{},"id":"7ddc8e6d6da9"} +{"status":"Extracting","progressDetail":{"current":65536,"total":4953791},"progress":"[\u003e ] 65.54kB/4.954MB","id":"0df6fd234b59"} +{"status":"Extracting","progressDetail":{"current":327680,"total":4953791},"progress":"[===\u003e ] 327.7kB/4.954MB","id":"0df6fd234b59"} +{"status":"Extracting","progressDetail":{"current":4325376,"total":4953791},"progress":"[===========================================\u003e ] 4.325MB/4.954MB","id":"0df6fd234b59"} +{"status":"Extracting","progressDetail":{"current":4953791,"total":4953791},"progress":"[==================================================\u003e] 4.954MB/4.954MB","id":"0df6fd234b59"} +{"status":"Pull complete","progressDetail":{},"id":"0df6fd234b59"} +{"status":"Extracting","progressDetail":{"current":65536,"total":6137526},"progress":"[\u003e ] 65.54kB/6.138MB","id":"8fc1ba8efe21"} +{"status":"Extracting","progressDetail":{"current":327680,"total":6137526},"progress":"[==\u003e ] 327.7kB/6.138MB","id":"8fc1ba8efe21"} +{"status":"Extracting","progressDetail":{"current":3801088,"total":6137526},"progress":"[==============================\u003e ] 3.801MB/6.138MB","id":"8fc1ba8efe21"} +{"status":"Extracting","progressDetail":{"current":6137526,"total":6137526},"progress":"[==================================================\u003e] 6.138MB/6.138MB","id":"8fc1ba8efe21"} +{"status":"Pull complete","progressDetail":{},"id":"8fc1ba8efe21"} +{"status":"Extracting","progressDetail":{"current":65536,"total":3854415},"progress":"[\u003e ] 65.54kB/3.854MB","id":"1f6f45e783b5"} +{"status":"Extracting","progressDetail":{"current":851968,"total":3854415},"progress":"[===========\u003e ] 852kB/3.854MB","id":"1f6f45e783b5"} +{"status":"Extracting","progressDetail":{"current":3854415,"total":3854415},"progress":"[==================================================\u003e] 3.854MB/3.854MB","id":"1f6f45e783b5"} +{"status":"Extracting","progressDetail":{"current":3854415,"total":3854415},"progress":"[==================================================\u003e] 3.854MB/3.854MB","id":"1f6f45e783b5"} +{"status":"Pull complete","progressDetail":{},"id":"1f6f45e783b5"} +{"status":"Extracting","progressDetail":{"current":65536,"total":5222290},"progress":"[\u003e ] 65.54kB/5.222MB","id":"43ea61082f68"} +{"status":"Extracting","progressDetail":{"current":458752,"total":5222290},"progress":"[====\u003e ] 458.8kB/5.222MB","id":"43ea61082f68"} +{"status":"Extracting","progressDetail":{"current":4849664,"total":5222290},"progress":"[==============================================\u003e ] 4.85MB/5.222MB","id":"43ea61082f68"} +{"status":"Extracting","progressDetail":{"current":5222290,"total":5222290},"progress":"[==================================================\u003e] 5.222MB/5.222MB","id":"43ea61082f68"} +{"status":"Pull complete","progressDetail":{},"id":"43ea61082f68"} +{"status":"Extracting","progressDetail":{"current":65536,"total":3564359},"progress":"[\u003e ] 65.54kB/3.564MB","id":"b8cf53bbc6ba"} +{"status":"Extracting","progressDetail":{"current":327680,"total":3564359},"progress":"[====\u003e ] 327.7kB/3.564MB","id":"b8cf53bbc6ba"} +{"status":"Extracting","progressDetail":{"current":3564359,"total":3564359},"progress":"[==================================================\u003e] 3.564MB/3.564MB","id":"b8cf53bbc6ba"} +{"status":"Pull complete","progressDetail":{},"id":"b8cf53bbc6ba"} +{"status":"Extracting","progressDetail":{"current":790,"total":790},"progress":"[==================================================\u003e] 790B/790B","id":"25efb07e4521"} +{"status":"Extracting","progressDetail":{"current":790,"total":790},"progress":"[==================================================\u003e] 790B/790B","id":"25efb07e4521"} +{"status":"Pull complete","progressDetail":{},"id":"25efb07e4521"} +{"status":"Extracting","progressDetail":{"current":65536,"total":5120108},"progress":"[\u003e ] 65.54kB/5.12MB","id":"1c3245356213"} +{"status":"Extracting","progressDetail":{"current":327680,"total":5120108},"progress":"[===\u003e ] 327.7kB/5.12MB","id":"1c3245356213"} +{"status":"Extracting","progressDetail":{"current":5111808,"total":5120108},"progress":"[=================================================\u003e ] 5.112MB/5.12MB","id":"1c3245356213"} +{"status":"Extracting","progressDetail":{"current":5120108,"total":5120108},"progress":"[==================================================\u003e] 5.12MB/5.12MB","id":"1c3245356213"} +{"status":"Pull complete","progressDetail":{},"id":"1c3245356213"} +{"status":"Extracting","progressDetail":{"current":65536,"total":5117023},"progress":"[\u003e ] 65.54kB/5.117MB","id":"61ebb123c1eb"} +{"status":"Extracting","progressDetail":{"current":655360,"total":5117023},"progress":"[======\u003e ] 655.4kB/5.117MB","id":"61ebb123c1eb"} +{"status":"Extracting","progressDetail":{"current":4259840,"total":5117023},"progress":"[=========================================\u003e ] 4.26MB/5.117MB","id":"61ebb123c1eb"} +{"status":"Extracting","progressDetail":{"current":5117023,"total":5117023},"progress":"[==================================================\u003e] 5.117MB/5.117MB","id":"61ebb123c1eb"} +{"status":"Pull complete","progressDetail":{},"id":"61ebb123c1eb"} +{"status":"Extracting","progressDetail":{"current":65536,"total":5384215},"progress":"[\u003e ] 65.54kB/5.384MB","id":"0964b769d2c9"} +{"status":"Extracting","progressDetail":{"current":327680,"total":5384215},"progress":"[===\u003e ] 327.7kB/5.384MB","id":"0964b769d2c9"} +{"status":"Extracting","progressDetail":{"current":5177344,"total":5384215},"progress":"[================================================\u003e ] 5.177MB/5.384MB","id":"0964b769d2c9"} +{"status":"Extracting","progressDetail":{"current":5384215,"total":5384215},"progress":"[==================================================\u003e] 5.384MB/5.384MB","id":"0964b769d2c9"} +{"status":"Pull complete","progressDetail":{},"id":"0964b769d2c9"} +{"status":"Extracting","progressDetail":{"current":65536,"total":5252487},"progress":"[\u003e ] 65.54kB/5.252MB","id":"87f7843f43cd"} +{"status":"Extracting","progressDetail":{"current":655360,"total":5252487},"progress":"[======\u003e ] 655.4kB/5.252MB","id":"87f7843f43cd"} +{"status":"Extracting","progressDetail":{"current":5252487,"total":5252487},"progress":"[==================================================\u003e] 5.252MB/5.252MB","id":"87f7843f43cd"} +{"status":"Pull complete","progressDetail":{},"id":"87f7843f43cd"} +{"status":"Extracting","progressDetail":{"current":65536,"total":5015856},"progress":"[\u003e ] 65.54kB/5.016MB","id":"a89dbf94d794"} +{"status":"Extracting","progressDetail":{"current":327680,"total":5015856},"progress":"[===\u003e ] 327.7kB/5.016MB","id":"a89dbf94d794"} +{"status":"Extracting","progressDetail":{"current":3997696,"total":5015856},"progress":"[=======================================\u003e ] 3.998MB/5.016MB","id":"a89dbf94d794"} +{"status":"Extracting","progressDetail":{"current":5015856,"total":5015856},"progress":"[==================================================\u003e] 5.016MB/5.016MB","id":"a89dbf94d794"} +{"status":"Pull complete","progressDetail":{},"id":"a89dbf94d794"} +{"status":"Extracting","progressDetail":{"current":65536,"total":5310566},"progress":"[\u003e ] 65.54kB/5.311MB","id":"f0d43ddca77f"} +{"status":"Extracting","progressDetail":{"current":393216,"total":5310566},"progress":"[===\u003e ] 393.2kB/5.311MB","id":"f0d43ddca77f"} +{"status":"Extracting","progressDetail":{"current":3407872,"total":5310566},"progress":"[================================\u003e ] 3.408MB/5.311MB","id":"f0d43ddca77f"} +{"status":"Extracting","progressDetail":{"current":5310566,"total":5310566},"progress":"[==================================================\u003e] 5.311MB/5.311MB","id":"f0d43ddca77f"} +{"status":"Pull complete","progressDetail":{},"id":"f0d43ddca77f"} +{"status":"Extracting","progressDetail":{"current":65536,"total":4915049},"progress":"[\u003e ] 65.54kB/4.915MB","id":"7c674f0cb40c"} +{"status":"Extracting","progressDetail":{"current":786432,"total":4915049},"progress":"[========\u003e ] 786.4kB/4.915MB","id":"7c674f0cb40c"} +{"status":"Extracting","progressDetail":{"current":4915049,"total":4915049},"progress":"[==================================================\u003e] 4.915MB/4.915MB","id":"7c674f0cb40c"} +{"status":"Extracting","progressDetail":{"current":4915049,"total":4915049},"progress":"[==================================================\u003e] 4.915MB/4.915MB","id":"7c674f0cb40c"} +{"status":"Pull complete","progressDetail":{},"id":"7c674f0cb40c"} +{"status":"Extracting","progressDetail":{"current":65536,"total":5119213},"progress":"[\u003e ] 65.54kB/5.119MB","id":"b48a885b52bc"} +{"status":"Extracting","progressDetail":{"current":327680,"total":5119213},"progress":"[===\u003e ] 327.7kB/5.119MB","id":"b48a885b52bc"} +{"status":"Extracting","progressDetail":{"current":4390912,"total":5119213},"progress":"[==========================================\u003e ] 4.391MB/5.119MB","id":"b48a885b52bc"} +{"status":"Extracting","progressDetail":{"current":5119213,"total":5119213},"progress":"[==================================================\u003e] 5.119MB/5.119MB","id":"b48a885b52bc"} +{"status":"Pull complete","progressDetail":{},"id":"b48a885b52bc"} +{"status":"Extracting","progressDetail":{"current":395,"total":395},"progress":"[==================================================\u003e] 395B/395B","id":"272cdf839cbb"} +{"status":"Extracting","progressDetail":{"current":395,"total":395},"progress":"[==================================================\u003e] 395B/395B","id":"272cdf839cbb"} +{"status":"Pull complete","progressDetail":{},"id":"272cdf839cbb"} +{"status":"Extracting","progressDetail":{"current":155,"total":155},"progress":"[==================================================\u003e] 155B/155B","id":"50d054c97f4f"} +{"status":"Extracting","progressDetail":{"current":155,"total":155},"progress":"[==================================================\u003e] 155B/155B","id":"50d054c97f4f"} +{"status":"Pull complete","progressDetail":{},"id":"50d054c97f4f"} +{"status":"Extracting","progressDetail":{"current":1069,"total":1069},"progress":"[==================================================\u003e] 1.069kB/1.069kB","id":"4c6bbd90b64d"} +{"status":"Extracting","progressDetail":{"current":1069,"total":1069},"progress":"[==================================================\u003e] 1.069kB/1.069kB","id":"4c6bbd90b64d"} +{"status":"Pull complete","progressDetail":{},"id":"4c6bbd90b64d"} +{"status":"Extracting","progressDetail":{"current":32,"total":32},"progress":"[==================================================\u003e] 32B/32B","id":"4f4fb700ef54"} +{"status":"Extracting","progressDetail":{"current":32,"total":32},"progress":"[==================================================\u003e] 32B/32B","id":"4f4fb700ef54"} +{"status":"Pull complete","progressDetail":{},"id":"4f4fb700ef54"} +{"status":"Digest: sha256:4acb6bfd6c4f0cabaf7f3690e444afe51f1c7de54d51da7e63fac709c56f1c30"} +{"status":"Status: Downloaded newer image for paketo-buildpacks/cnb:base"} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/json/test-mapped-object.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/json/test-mapped-object.json new file mode 100644 index 000000000000..fffc71c67a51 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/json/test-mapped-object.json @@ -0,0 +1,15 @@ +{ + "string": "stringvalue", + "stringarray": [ + "a", + "b" + ], + "StartsWithUppercase": "value", + "person": { + "name": { + "title": "dr", + "first": "spring", + "last": "boot" + } + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/pom.xml deleted file mode 100644 index a2af1b413cc5..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/pom.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-tools - ${revision} - - spring-boot-configuration-docs - Spring Boot Configuration Docs - Spring Boot Configuration Docs - - ${basedir}/../../.. - - - - org.springframework.boot - spring-boot-configuration-metadata - - - org.springframework.boot - spring-boot-test-support - test - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/src/main/java/org/springframework/boot/configurationdocs/AsciidocBuilder.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/src/main/java/org/springframework/boot/configurationdocs/AsciidocBuilder.java deleted file mode 100644 index e4cab45468db..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/src/main/java/org/springframework/boot/configurationdocs/AsciidocBuilder.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.configurationdocs; - -/** - * Simple builder to help construct Asciidoc markup. - * - * @author Phillip Webb - */ -class AsciidocBuilder { - - private final StringBuilder content; - - AsciidocBuilder() { - this.content = new StringBuilder(); - } - - AsciidocBuilder appendKey(Object... items) { - for (Object item : items) { - appendln("`+", item, "+` +"); - } - return this; - } - - AsciidocBuilder newLine() { - return append(System.lineSeparator()); - } - - AsciidocBuilder appendln(Object... items) { - return append(items).newLine(); - } - - AsciidocBuilder append(Object... items) { - for (Object item : items) { - this.content.append(item); - } - return this; - } - - @Override - public String toString() { - return this.content.toString(); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/src/main/java/org/springframework/boot/configurationdocs/CompoundConfigurationTableEntry.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/src/main/java/org/springframework/boot/configurationdocs/CompoundConfigurationTableEntry.java deleted file mode 100644 index 603e902f5dce..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/src/main/java/org/springframework/boot/configurationdocs/CompoundConfigurationTableEntry.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.configurationdocs; - -import java.util.Set; -import java.util.TreeSet; -import java.util.stream.Stream; - -import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty; - -/** - * Table entry regrouping a list of configuration properties sharing the same description. - * - * @author Brian Clozel - */ -class CompoundConfigurationTableEntry extends ConfigurationTableEntry { - - private Set configurationKeys; - - private String description; - - CompoundConfigurationTableEntry(String key, String description) { - this.key = key; - this.description = description; - this.configurationKeys = new TreeSet<>(); - } - - void addConfigurationKeys(ConfigurationMetadataProperty... properties) { - Stream.of(properties).map(ConfigurationMetadataProperty::getId).forEach(this.configurationKeys::add); - } - - @Override - void write(AsciidocBuilder builder) { - builder.append("|"); - this.configurationKeys.forEach(builder::appendKey); - builder.newLine().appendln("|").appendln("|+++", this.description, "+++"); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/src/main/java/org/springframework/boot/configurationdocs/ConfigurationMetadataDocumentWriter.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/src/main/java/org/springframework/boot/configurationdocs/ConfigurationMetadataDocumentWriter.java deleted file mode 100644 index 0269c91cf4db..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/src/main/java/org/springframework/boot/configurationdocs/ConfigurationMetadataDocumentWriter.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.configurationdocs; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty; -import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepositoryJsonBuilder; - -/** - * Write Asciidoc documents with configuration properties listings. - * - * @author Brian Clozel - * @since 2.0.0 - */ -public class ConfigurationMetadataDocumentWriter { - - public void writeDocument(Path outputDirectory, DocumentOptions options, InputStream... metadata) - throws IOException { - assertValidOutputDirectory(outputDirectory); - if (!Files.exists(outputDirectory)) { - Files.createDirectory(outputDirectory); - } - assertMetadata(metadata); - List tables = createConfigTables(getMetadataProperties(metadata), options); - for (ConfigurationTable table : tables) { - writeConfigurationTable(table, outputDirectory); - } - } - - private void assertValidOutputDirectory(Path outputDirPath) { - if (outputDirPath == null) { - throw new IllegalArgumentException("output path should not be null"); - } - if (Files.exists(outputDirPath) && !Files.isDirectory(outputDirPath)) { - throw new IllegalArgumentException("output path already exists and is not a directory"); - } - } - - private void assertMetadata(InputStream... metadata) { - if (metadata == null || metadata.length < 1) { - throw new IllegalArgumentException("missing input metadata"); - } - } - - private Map getMetadataProperties(InputStream... metadata) - throws IOException { - ConfigurationMetadataRepositoryJsonBuilder builder = ConfigurationMetadataRepositoryJsonBuilder - .create(metadata); - return builder.build().getAllProperties(); - } - - private List createConfigTables(Map metadataProperties, - DocumentOptions options) { - List tables = new ArrayList<>(); - List unmappedKeys = metadataProperties.values().stream().filter((property) -> !property.isDeprecated()) - .map(ConfigurationMetadataProperty::getId).collect(Collectors.toList()); - Map overrides = getOverrides(metadataProperties, unmappedKeys, - options); - options.getMetadataSections().forEach((id, keyPrefixes) -> tables - .add(createConfigTable(metadataProperties, unmappedKeys, overrides, id, keyPrefixes))); - if (!unmappedKeys.isEmpty()) { - throw new IllegalStateException( - "The following keys were not written to the documentation: " + String.join(", ", unmappedKeys)); - } - if (!overrides.isEmpty()) { - throw new IllegalStateException("The following keys were not written to the documentation: " - + String.join(", ", overrides.keySet())); - } - return tables; - } - - private Map getOverrides( - Map metadataProperties, List unmappedKeys, - DocumentOptions options) { - Map overrides = new HashMap<>(); - options.getOverrides().forEach((keyPrefix, description) -> { - CompoundConfigurationTableEntry entry = new CompoundConfigurationTableEntry(keyPrefix, description); - List matchingKeys = unmappedKeys.stream().filter((key) -> key.startsWith(keyPrefix)) - .collect(Collectors.toList()); - for (String matchingKey : matchingKeys) { - entry.addConfigurationKeys(metadataProperties.get(matchingKey)); - } - overrides.put(keyPrefix, entry); - unmappedKeys.removeAll(matchingKeys); - }); - return overrides; - } - - private ConfigurationTable createConfigTable(Map metadataProperties, - List unmappedKeys, Map overrides, String id, - List keyPrefixes) { - ConfigurationTable table = new ConfigurationTable(id); - for (String keyPrefix : keyPrefixes) { - List matchingOverrides = overrides.keySet().stream() - .filter((overrideKey) -> overrideKey.startsWith(keyPrefix)).collect(Collectors.toList()); - matchingOverrides.forEach((match) -> table.addEntry(overrides.remove(match))); - } - List matchingKeys = unmappedKeys.stream() - .filter((key) -> keyPrefixes.stream().anyMatch(key::startsWith)).collect(Collectors.toList()); - for (String matchingKey : matchingKeys) { - ConfigurationMetadataProperty property = metadataProperties.get(matchingKey); - table.addEntry(new SingleConfigurationTableEntry(property)); - } - unmappedKeys.removeAll(matchingKeys); - return table; - } - - private void writeConfigurationTable(ConfigurationTable table, Path outputDirectory) throws IOException { - Path outputFilePath = outputDirectory.resolve(table.getId() + ".adoc"); - Files.deleteIfExists(outputFilePath); - Files.createFile(outputFilePath); - try (OutputStream outputStream = Files.newOutputStream(outputFilePath)) { - outputStream.write(table.toAsciidocTable().getBytes(StandardCharsets.UTF_8)); - } - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/src/main/java/org/springframework/boot/configurationdocs/ConfigurationTable.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/src/main/java/org/springframework/boot/configurationdocs/ConfigurationTable.java deleted file mode 100644 index 83a36d9bb1d2..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/src/main/java/org/springframework/boot/configurationdocs/ConfigurationTable.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.configurationdocs; - -import java.util.Arrays; -import java.util.Set; -import java.util.TreeSet; - -/** - * Asciidoctor table listing configuration properties sharing to a common theme. - * - * @author Brian Clozel - */ -class ConfigurationTable { - - private final String id; - - private final Set entries; - - ConfigurationTable(String id) { - this.id = id; - this.entries = new TreeSet<>(); - } - - String getId() { - return this.id; - } - - void addEntry(ConfigurationTableEntry... entries) { - this.entries.addAll(Arrays.asList(entries)); - } - - String toAsciidocTable() { - AsciidocBuilder builder = new AsciidocBuilder(); - builder.appendln("[cols=\"1,1,2\", options=\"header\"]"); - builder.appendln("|==="); - builder.appendln("|Key|Default Value|Description"); - builder.appendln(); - this.entries.forEach((entry) -> { - entry.write(builder); - builder.appendln(); - }); - return builder.appendln("|===").toString(); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/src/main/java/org/springframework/boot/configurationdocs/ConfigurationTableEntry.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/src/main/java/org/springframework/boot/configurationdocs/ConfigurationTableEntry.java deleted file mode 100644 index 8c81a9601738..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/src/main/java/org/springframework/boot/configurationdocs/ConfigurationTableEntry.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.configurationdocs; - -/** - * Abstract class for entries in {@link ConfigurationTable}. - * - * @author Brian Clozel - */ -abstract class ConfigurationTableEntry implements Comparable { - - protected String key; - - String getKey() { - return this.key; - } - - abstract void write(AsciidocBuilder builder); - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - ConfigurationTableEntry other = (ConfigurationTableEntry) obj; - return this.key.equals(other.key); - } - - @Override - public int hashCode() { - return this.key.hashCode(); - } - - @Override - public int compareTo(ConfigurationTableEntry other) { - return this.key.compareTo(other.getKey()); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/src/main/java/org/springframework/boot/configurationdocs/DocumentOptions.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/src/main/java/org/springframework/boot/configurationdocs/DocumentOptions.java deleted file mode 100644 index 6e96e8d94bb2..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/src/main/java/org/springframework/boot/configurationdocs/DocumentOptions.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.configurationdocs; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Options for generating documentation for configuration properties. - * - * @author Brian Clozel - * @since 2.0.0 - */ -public final class DocumentOptions { - - private final Map> metadataSections; - - private final Map overrides; - - private DocumentOptions(Map> metadataSections, Map overrides) { - this.metadataSections = metadataSections; - this.overrides = overrides; - } - - Map> getMetadataSections() { - return this.metadataSections; - } - - Map getOverrides() { - return this.overrides; - } - - static Builder builder() { - return new Builder(); - } - - /** - * Builder for DocumentOptions. - */ - public static class Builder { - - Map> metadataSections = new HashMap<>(); - - Map overrides = new HashMap<>(); - - SectionSpec addSection(String name) { - return new SectionSpec(this, name); - } - - Builder addOverride(String keyPrefix, String description) { - this.overrides.put(keyPrefix, description); - return this; - } - - DocumentOptions build() { - return new DocumentOptions(this.metadataSections, this.overrides); - } - - } - - /** - * Configuration for a documentation section listing properties for a specific theme. - */ - public static class SectionSpec { - - private final String name; - - private final Builder builder; - - SectionSpec(Builder builder, String name) { - this.builder = builder; - this.name = name; - } - - Builder withKeyPrefixes(String... keyPrefixes) { - this.builder.metadataSections.put(this.name, Arrays.asList(keyPrefixes)); - return this.builder; - } - - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/src/main/java/org/springframework/boot/configurationdocs/SingleConfigurationTableEntry.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/src/main/java/org/springframework/boot/configurationdocs/SingleConfigurationTableEntry.java deleted file mode 100644 index 17ac8bc32a21..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/src/main/java/org/springframework/boot/configurationdocs/SingleConfigurationTableEntry.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.configurationdocs; - -import java.util.Arrays; -import java.util.stream.Collectors; - -import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty; - -/** - * Table entry containing a single configuration property. - * - * @author Brian Clozel - */ -class SingleConfigurationTableEntry extends ConfigurationTableEntry { - - private final String description; - - private final String defaultValue; - - SingleConfigurationTableEntry(ConfigurationMetadataProperty property) { - this.key = property.getId(); - if (property.getType() != null && property.getType().startsWith("java.util.Map")) { - this.key += ".*"; - } - this.description = property.getDescription(); - this.defaultValue = getDefaultValue(property.getDefaultValue()); - } - - private String getDefaultValue(Object defaultValue) { - if (defaultValue == null) { - return null; - } - if (defaultValue.getClass().isArray()) { - return Arrays.stream((Object[]) defaultValue).map(Object::toString) - .collect(Collectors.joining("," + System.lineSeparator())); - } - return defaultValue.toString(); - } - - @Override - void write(AsciidocBuilder builder) { - builder.appendln("|`+", this.key, "+`"); - writeDefaultValue(builder); - writeDescription(builder); - builder.appendln(); - } - - private void writeDefaultValue(AsciidocBuilder builder) { - String defaultValue = (this.defaultValue != null) ? this.defaultValue : ""; - if (defaultValue.isEmpty()) { - builder.appendln("|"); - } - else { - defaultValue = defaultValue.replace("\\", "\\\\").replace("|", "\\|"); - builder.appendln("|`+", defaultValue, "+`"); - } - } - - private void writeDescription(AsciidocBuilder builder) { - if (this.description == null || this.description.isEmpty()) { - builder.append("|"); - } - else { - String cleanedDescription = this.description.replace("|", "\\|"); - builder.append("|+++", cleanedDescription, "+++"); - } - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/src/main/java/org/springframework/boot/configurationdocs/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/src/main/java/org/springframework/boot/configurationdocs/package-info.java deleted file mode 100644 index 1c9c21ac81fa..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/src/main/java/org/springframework/boot/configurationdocs/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Support for generating documentation of configuration properties. - */ -package org.springframework.boot.configurationdocs; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/src/test/java/org/springframework/boot/configurationdocs/CompoundConfigurationTableEntryTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/src/test/java/org/springframework/boot/configurationdocs/CompoundConfigurationTableEntryTests.java deleted file mode 100644 index 0e06b5950992..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/src/test/java/org/springframework/boot/configurationdocs/CompoundConfigurationTableEntryTests.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.configurationdocs; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link CompoundConfigurationTableEntry}. - * - * @author Brian Clozel - */ -class CompoundConfigurationTableEntryTests { - - private static String NEWLINE = System.lineSeparator(); - - @Test - void simpleProperty() { - ConfigurationMetadataProperty firstProp = new ConfigurationMetadataProperty(); - firstProp.setId("spring.test.first"); - firstProp.setType("java.lang.String"); - ConfigurationMetadataProperty secondProp = new ConfigurationMetadataProperty(); - secondProp.setId("spring.test.second"); - secondProp.setType("java.lang.String"); - ConfigurationMetadataProperty thirdProp = new ConfigurationMetadataProperty(); - thirdProp.setId("spring.test.third"); - thirdProp.setType("java.lang.String"); - CompoundConfigurationTableEntry entry = new CompoundConfigurationTableEntry("spring.test", - "This is a description."); - entry.addConfigurationKeys(firstProp, secondProp, thirdProp); - AsciidocBuilder builder = new AsciidocBuilder(); - entry.write(builder); - assertThat(builder.toString()).isEqualTo( - "|`+spring.test.first+` +" + NEWLINE + "`+spring.test.second+` +" + NEWLINE + "`+spring.test.third+` +" - + NEWLINE + NEWLINE + "|" + NEWLINE + "|+++This is a description.+++" + NEWLINE); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/src/test/java/org/springframework/boot/configurationdocs/ConfigurationTableTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/src/test/java/org/springframework/boot/configurationdocs/ConfigurationTableTests.java deleted file mode 100644 index 2b15a4767584..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/src/test/java/org/springframework/boot/configurationdocs/ConfigurationTableTests.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.configurationdocs; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link ConfigurationTable}. - * - * @author Brian Clozel - */ -class ConfigurationTableTests { - - private static String NEWLINE = System.lineSeparator(); - - @Test - void simpleTable() { - ConfigurationTable table = new ConfigurationTable("test"); - ConfigurationMetadataProperty first = new ConfigurationMetadataProperty(); - first.setId("spring.test.prop"); - first.setDefaultValue("something"); - first.setDescription("This is a description."); - first.setType("java.lang.String"); - ConfigurationMetadataProperty second = new ConfigurationMetadataProperty(); - second.setId("spring.test.other"); - second.setDefaultValue("other value"); - second.setDescription("This is another description."); - second.setType("java.lang.String"); - table.addEntry(new SingleConfigurationTableEntry(first)); - table.addEntry(new SingleConfigurationTableEntry(second)); - assertThat(table.toAsciidocTable()).isEqualTo("[cols=\"1,1,2\", options=\"header\"]" + NEWLINE + "|===" - + NEWLINE + "|Key|Default Value|Description" + NEWLINE + NEWLINE + "|`+spring.test.other+`" + NEWLINE - + "|`+other value+`" + NEWLINE + "|+++This is another description.+++" + NEWLINE + NEWLINE - + "|`+spring.test.prop+`" + NEWLINE + "|`+something+`" + NEWLINE + "|+++This is a description.+++" - + NEWLINE + NEWLINE + "|===" + NEWLINE); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/src/test/java/org/springframework/boot/configurationdocs/SingleConfigurationTableEntryTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/src/test/java/org/springframework/boot/configurationdocs/SingleConfigurationTableEntryTests.java deleted file mode 100644 index 013320c729b3..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-docs/src/test/java/org/springframework/boot/configurationdocs/SingleConfigurationTableEntryTests.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.configurationdocs; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link SingleConfigurationTableEntry}. - * - * @author Brian Clozel - */ -class SingleConfigurationTableEntryTests { - - private static String NEWLINE = System.lineSeparator(); - - @Test - void simpleProperty() { - ConfigurationMetadataProperty property = new ConfigurationMetadataProperty(); - property.setId("spring.test.prop"); - property.setDefaultValue("something"); - property.setDescription("This is a description."); - property.setType("java.lang.String"); - SingleConfigurationTableEntry entry = new SingleConfigurationTableEntry(property); - AsciidocBuilder builder = new AsciidocBuilder(); - entry.write(builder); - assertThat(builder.toString()).isEqualTo("|`+spring.test.prop+`" + NEWLINE + "|`+something+`" + NEWLINE - + "|+++This is a description.+++" + NEWLINE); - } - - @Test - void noDefaultValue() { - ConfigurationMetadataProperty property = new ConfigurationMetadataProperty(); - property.setId("spring.test.prop"); - property.setDescription("This is a description."); - property.setType("java.lang.String"); - SingleConfigurationTableEntry entry = new SingleConfigurationTableEntry(property); - AsciidocBuilder builder = new AsciidocBuilder(); - entry.write(builder); - assertThat(builder.toString()).isEqualTo( - "|`+spring.test.prop+`" + NEWLINE + "|" + NEWLINE + "|+++This is a description.+++" + NEWLINE); - } - - @Test - void defaultValueWithPipes() { - ConfigurationMetadataProperty property = new ConfigurationMetadataProperty(); - property.setId("spring.test.prop"); - property.setDefaultValue("first|second"); - property.setDescription("This is a description."); - property.setType("java.lang.String"); - SingleConfigurationTableEntry entry = new SingleConfigurationTableEntry(property); - AsciidocBuilder builder = new AsciidocBuilder(); - entry.write(builder); - assertThat(builder.toString()).isEqualTo("|`+spring.test.prop+`" + NEWLINE + "|`+first\\|second+`" + NEWLINE - + "|+++This is a description.+++" + NEWLINE); - } - - @Test - void defaultValueWithBackslash() { - ConfigurationMetadataProperty property = new ConfigurationMetadataProperty(); - property.setId("spring.test.prop"); - property.setDefaultValue("first\\second"); - property.setDescription("This is a description."); - property.setType("java.lang.String"); - SingleConfigurationTableEntry entry = new SingleConfigurationTableEntry(property); - AsciidocBuilder builder = new AsciidocBuilder(); - entry.write(builder); - assertThat(builder.toString()).isEqualTo("|`+spring.test.prop+`" + NEWLINE + "|`+first\\\\second+`" + NEWLINE - + "|+++This is a description.+++" + NEWLINE); - } - - @Test - void descriptionWithPipe() { - ConfigurationMetadataProperty property = new ConfigurationMetadataProperty(); - property.setId("spring.test.prop"); - property.setDescription("This is a description with a | pipe."); - property.setType("java.lang.String"); - SingleConfigurationTableEntry entry = new SingleConfigurationTableEntry(property); - AsciidocBuilder builder = new AsciidocBuilder(); - entry.write(builder); - assertThat(builder.toString()).isEqualTo("|`+spring.test.prop+`" + NEWLINE + "|" + NEWLINE - + "|+++This is a description with a \\| pipe.+++" + NEWLINE); - } - - @Test - void mapProperty() { - ConfigurationMetadataProperty property = new ConfigurationMetadataProperty(); - property.setId("spring.test.prop"); - property.setDescription("This is a description."); - property.setType("java.util.Map"); - SingleConfigurationTableEntry entry = new SingleConfigurationTableEntry(property); - AsciidocBuilder builder = new AsciidocBuilder(); - entry.write(builder); - assertThat(builder.toString()).isEqualTo( - "|`+spring.test.prop.*+`" + NEWLINE + "|" + NEWLINE + "|+++This is a description.+++" + NEWLINE); - } - - @Test - void listProperty() { - String[] defaultValue = new String[] { "first", "second", "third" }; - ConfigurationMetadataProperty property = new ConfigurationMetadataProperty(); - property.setId("spring.test.prop"); - property.setDescription("This is a description."); - property.setType("java.util.List"); - property.setDefaultValue(defaultValue); - SingleConfigurationTableEntry entry = new SingleConfigurationTableEntry(property); - AsciidocBuilder builder = new AsciidocBuilder(); - entry.write(builder); - assertThat(builder.toString()).isEqualTo("|`+spring.test.prop+`" + NEWLINE + "|`+first," + NEWLINE + "second," - + NEWLINE + "third+`" + NEWLINE + "|+++This is a description.+++" + NEWLINE); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata/build.gradle new file mode 100644 index 000000000000..7b8fbb193c68 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata/build.gradle @@ -0,0 +1,15 @@ +plugins { + id "java-library" + id "org.springframework.boot.conventions" + id "org.springframework.boot.deployed" +} + +description = "Spring Boot Configuration Metadata" + +dependencies { + implementation("com.vaadin.external.google:android-json") + + testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.assertj:assertj-core") + testImplementation("org.springframework:spring-core") +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata/pom.xml deleted file mode 100644 index 68862f4c88b8..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata/pom.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-tools - ${revision} - - spring-boot-configuration-metadata - Spring Boot Configuration Metadata - Spring Boot Configuration Metadata - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - - com.vaadin.external.google - android-json - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/SimpleConfigurationMetadataRepository.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/SimpleConfigurationMetadataRepository.java index 5becb4d6906d..bba6c2562787 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/SimpleConfigurationMetadataRepository.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/SimpleConfigurationMetadataRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,7 +61,7 @@ public void add(Collection sources) { } String sourceType = source.getType(); if (sourceType != null) { - putIfAbsent(group.getSources(), sourceType, source); + addOrMergeSource(group.getSources(), sourceType, source); } } } @@ -93,7 +93,7 @@ public void include(ConfigurationMetadataRepository repository) { // Merge properties group.getProperties().forEach((name, value) -> putIfAbsent(existingGroup.getProperties(), name, value)); // Merge sources - group.getSources().forEach((name, value) -> putIfAbsent(existingGroup.getSources(), name, value)); + group.getSources().forEach((name, value) -> addOrMergeSource(existingGroup.getSources(), name, value)); } } @@ -111,6 +111,17 @@ private ConfigurationMetadataGroup getGroup(ConfigurationMetadataSource source) return this.allGroups.get(source.getGroupId()); } + private void addOrMergeSource(Map sources, String name, + ConfigurationMetadataSource source) { + ConfigurationMetadataSource existingSource = sources.get(name); + if (existingSource == null) { + sources.put(name, source); + } + else { + source.getProperties().forEach((k, v) -> putIfAbsent(existingSource.getProperties(), k, v)); + } + } + private void putIfAbsent(Map map, String key, V value) { if (!map.containsKey(key)) { map.put(key, value); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata/src/test/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataRepositoryJsonBuilderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata/src/test/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataRepositoryJsonBuilderTests.java index 0706b9036d17..123a8f10cf60 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata/src/test/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataRepositoryJsonBuilderTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata/src/test/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataRepositoryJsonBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.io.IOException; import java.io.InputStream; +import java.util.Arrays; import java.util.Map; import org.junit.jupiter.api.Test; @@ -33,7 +34,7 @@ class ConfigurationMetadataRepositoryJsonBuilderTests extends AbstractConfigurationMetadataTests { @Test - void nullResource() throws IOException { + void nullResource() { assertThatIllegalArgumentException() .isThrownBy(() -> ConfigurationMetadataRepositoryJsonBuilder.create().withJsonResource(null)); } @@ -63,53 +64,77 @@ void hintsOnMaps() throws IOException { @Test void severalRepositoriesNoConflict() throws IOException { - try (InputStream foo = getInputStreamFor("foo")) { - try (InputStream bar = getInputStreamFor("bar")) { - ConfigurationMetadataRepository repo = ConfigurationMetadataRepositoryJsonBuilder.create(foo, bar) - .build(); - validateFoo(repo); - validateBar(repo); - assertThat(repo.getAllGroups()).hasSize(2); - contains(repo.getAllProperties(), "spring.foo.name", "spring.foo.description", "spring.foo.counter", - "spring.bar.name", "spring.bar.description", "spring.bar.counter"); - assertThat(repo.getAllProperties()).hasSize(6); - } + try (InputStream foo = getInputStreamFor("foo"); InputStream bar = getInputStreamFor("bar")) { + ConfigurationMetadataRepository repo = ConfigurationMetadataRepositoryJsonBuilder.create(foo, bar).build(); + validateFoo(repo); + validateBar(repo); + assertThat(repo.getAllGroups()).hasSize(2); + contains(repo.getAllProperties(), "spring.foo.name", "spring.foo.description", "spring.foo.counter", + "spring.bar.name", "spring.bar.description", "spring.bar.counter"); + assertThat(repo.getAllProperties()).hasSize(6); } } @Test void repositoryWithRoot() throws IOException { - try (InputStream foo = getInputStreamFor("foo")) { - try (InputStream root = getInputStreamFor("root")) { - ConfigurationMetadataRepository repo = ConfigurationMetadataRepositoryJsonBuilder.create(foo, root) - .build(); - validateFoo(repo); - assertThat(repo.getAllGroups()).hasSize(2); - - contains(repo.getAllProperties(), "spring.foo.name", "spring.foo.description", "spring.foo.counter", - "spring.root.name", "spring.root2.name"); - assertThat(repo.getAllProperties()).hasSize(5); - } + try (InputStream foo = getInputStreamFor("foo"); InputStream root = getInputStreamFor("root")) { + ConfigurationMetadataRepository repo = ConfigurationMetadataRepositoryJsonBuilder.create(foo, root).build(); + validateFoo(repo); + assertThat(repo.getAllGroups()).hasSize(2); + + contains(repo.getAllProperties(), "spring.foo.name", "spring.foo.description", "spring.foo.counter", + "spring.root.name", "spring.root2.name"); + assertThat(repo.getAllProperties()).hasSize(5); } } @Test void severalRepositoriesIdenticalGroups() throws IOException { - try (InputStream foo = getInputStreamFor("foo")) { - try (InputStream foo2 = getInputStreamFor("foo2")) { - ConfigurationMetadataRepository repo = ConfigurationMetadataRepositoryJsonBuilder.create(foo, foo2) - .build(); - assertThat(repo.getAllGroups()).hasSize(1); - ConfigurationMetadataGroup group = repo.getAllGroups().get("spring.foo"); - contains(group.getSources(), "org.acme.Foo", "org.acme.Foo2", "org.springframework.boot.FooProperties"); - assertThat(group.getSources()).hasSize(3); - contains(group.getProperties(), "spring.foo.name", "spring.foo.description", "spring.foo.counter", - "spring.foo.enabled", "spring.foo.type"); - assertThat(group.getProperties()).hasSize(5); - contains(repo.getAllProperties(), "spring.foo.name", "spring.foo.description", "spring.foo.counter", - "spring.foo.enabled", "spring.foo.type"); - assertThat(repo.getAllProperties()).hasSize(5); - } + try (InputStream foo = getInputStreamFor("foo"); InputStream foo2 = getInputStreamFor("foo2")) { + ConfigurationMetadataRepository repo = ConfigurationMetadataRepositoryJsonBuilder.create(foo, foo2).build(); + Iterable allKeys = Arrays.asList("spring.foo.name", "spring.foo.description", "spring.foo.counter", + "spring.foo.enabled", "spring.foo.type"); + assertThat(repo.getAllProperties()).containsOnlyKeys(allKeys); + assertThat(repo.getAllGroups()).containsOnlyKeys("spring.foo"); + ConfigurationMetadataGroup group = repo.getAllGroups().get("spring.foo"); + assertThat(group.getProperties()).containsOnlyKeys(allKeys); + assertThat(group.getSources()).containsOnlyKeys("org.acme.Foo", "org.acme.Foo2", + "org.springframework.boot.FooProperties"); + assertThat(group.getSources().get("org.acme.Foo").getProperties()).containsOnlyKeys("spring.foo.name", + "spring.foo.description"); + assertThat(group.getSources().get("org.acme.Foo2").getProperties()).containsOnlyKeys("spring.foo.enabled", + "spring.foo.type"); + assertThat(group.getSources().get("org.springframework.boot.FooProperties").getProperties()) + .containsOnlyKeys("spring.foo.name", "spring.foo.counter"); + } + } + + @Test + void severalRepositoriesIdenticalGroupsWithSameType() throws IOException { + try (InputStream foo = getInputStreamFor("foo"); InputStream foo3 = getInputStreamFor("foo3")) { + ConfigurationMetadataRepository repo = ConfigurationMetadataRepositoryJsonBuilder.create(foo, foo3).build(); + Iterable allKeys = Arrays.asList("spring.foo.name", "spring.foo.description", "spring.foo.counter", + "spring.foo.enabled", "spring.foo.type"); + assertThat(repo.getAllProperties()).containsOnlyKeys(allKeys); + assertThat(repo.getAllGroups()).containsOnlyKeys("spring.foo"); + ConfigurationMetadataGroup group = repo.getAllGroups().get("spring.foo"); + assertThat(group.getProperties()).containsOnlyKeys(allKeys); + assertThat(group.getSources()).containsOnlyKeys("org.acme.Foo", "org.springframework.boot.FooProperties"); + assertThat(group.getSources().get("org.acme.Foo").getProperties()).containsOnlyKeys("spring.foo.name", + "spring.foo.description", "spring.foo.enabled", "spring.foo.type"); + assertThat(group.getSources().get("org.springframework.boot.FooProperties").getProperties()) + .containsOnlyKeys("spring.foo.name", "spring.foo.counter"); + } + } + + @Test + void severalRepositoriesIdenticalGroupsWithSameTypeDoesNotOverrideSource() throws IOException { + try (InputStream foo = getInputStreamFor("foo"); InputStream foo3 = getInputStreamFor("foo3")) { + ConfigurationMetadataRepository repo = ConfigurationMetadataRepositoryJsonBuilder.create(foo, foo3).build(); + ConfigurationMetadataGroup group = repo.getAllGroups().get("spring.foo"); + ConfigurationMetadataSource fooSource = group.getSources().get("org.acme.Foo"); + assertThat(fooSource.getSourceMethod()).isEqualTo("foo()"); + assertThat(fooSource.getDescription()).isEqualTo("This is Foo."); } } @@ -144,22 +169,19 @@ void multiGroups() throws IOException { @Test void builderInstancesAreIsolated() throws IOException { - try (InputStream foo = getInputStreamFor("foo")) { - try (InputStream bar = getInputStreamFor("bar")) { - ConfigurationMetadataRepositoryJsonBuilder builder = ConfigurationMetadataRepositoryJsonBuilder - .create(); - ConfigurationMetadataRepository firstRepo = builder.withJsonResource(foo).build(); - validateFoo(firstRepo); - ConfigurationMetadataRepository secondRepo = builder.withJsonResource(bar).build(); - validateFoo(secondRepo); - validateBar(secondRepo); - // first repo not impacted by second build - assertThat(secondRepo).isNotEqualTo(firstRepo); - assertThat(firstRepo.getAllGroups()).hasSize(1); - assertThat(firstRepo.getAllProperties()).hasSize(3); - assertThat(secondRepo.getAllGroups()).hasSize(2); - assertThat(secondRepo.getAllProperties()).hasSize(6); - } + try (InputStream foo = getInputStreamFor("foo"); InputStream bar = getInputStreamFor("bar")) { + ConfigurationMetadataRepositoryJsonBuilder builder = ConfigurationMetadataRepositoryJsonBuilder.create(); + ConfigurationMetadataRepository firstRepo = builder.withJsonResource(foo).build(); + validateFoo(firstRepo); + ConfigurationMetadataRepository secondRepo = builder.withJsonResource(bar).build(); + validateFoo(secondRepo); + validateBar(secondRepo); + // first repo not impacted by second build + assertThat(secondRepo).isNotEqualTo(firstRepo); + assertThat(firstRepo.getAllGroups()).hasSize(1); + assertThat(firstRepo.getAllProperties()).hasSize(3); + assertThat(secondRepo.getAllGroups()).hasSize(2); + assertThat(secondRepo.getAllProperties()).hasSize(6); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata/src/test/java/org/springframework/boot/configurationmetadata/JsonReaderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata/src/test/java/org/springframework/boot/configurationmetadata/JsonReaderTests.java index e7e232054a05..68550fb34fa8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata/src/test/java/org/springframework/boot/configurationmetadata/JsonReaderTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata/src/test/java/org/springframework/boot/configurationmetadata/JsonReaderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,7 +46,7 @@ void emptyMetadata() throws IOException { } @Test - void invalidMetadata() throws IOException { + void invalidMetadata() { assertThatIllegalStateException().isThrownBy(() -> readFor("invalid")).withCauseInstanceOf(JSONException.class); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata/src/test/resources/metadata/configuration-metadata-foo3.json b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata/src/test/resources/metadata/configuration-metadata-foo3.json new file mode 100644 index 000000000000..e3ea2f120ce7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata/src/test/resources/metadata/configuration-metadata-foo3.json @@ -0,0 +1,23 @@ +{ + "groups": [ + { + "name": "spring.foo", + "type": "org.acme.Foo", + "sourceType": "org.acme.config.FooApp", + "sourceMethod": "foo3()", + "description": "This is Foo3." + } + ], + "properties": [ + { + "name": "spring.foo.enabled", + "type": "java.lang.Boolean", + "sourceType": "org.acme.Foo" + }, + { + "name": "spring.foo.type", + "type": "java.lang.String", + "sourceType": "org.acme.Foo" + } + ] +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/build.gradle new file mode 100644 index 000000000000..684cae0a3422 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/build.gradle @@ -0,0 +1,29 @@ +plugins { + id "java-library" + id "org.springframework.boot.conventions" + id "org.springframework.boot.deployed" + id "org.springframework.boot.annotation-processor" +} + +description = "Spring Boot Configuration Annotation Processor" + +sourceSets { + main { + java { + srcDir file("src/json-shade/java") + } + } +} + +dependencies { + testCompileOnly("com.google.code.findbugs:jsr305:3.0.2") + testImplementation(enforcedPlatform(project(":spring-boot-project:spring-boot-dependencies"))) + testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) + testImplementation("jakarta.validation:jakarta.validation-api") + testImplementation("org.assertj:assertj-core") + testImplementation("org.hamcrest:hamcrest-library") + testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.mockito:mockito-core") + testImplementation("org.projectlombok:lombok") + testImplementation("org.springframework:spring-core") +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/pom.xml deleted file mode 100644 index 09c2e751120a..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/pom.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-tools - ${revision} - - spring-boot-configuration-processor - Spring Boot Configuration Processor - Spring Boot Configuration Processor - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - - org.projectlombok - lombok - test - - - jakarta.validation - jakarta.validation-api - test - - - org.springframework.boot - spring-boot-test-support - test - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - - none - - - - org.codehaus.mojo - build-helper-maven-plugin - - - add-json-shade-source - generate-sources - - add-source - - - - ${basedir}/src/json-shade/java - - - - - - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/json-shade/java/org/springframework/boot/configurationprocessor/json/JSON.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/json-shade/java/org/springframework/boot/configurationprocessor/json/JSON.java index f8876a599d5b..f207c3912d1f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/json-shade/java/org/springframework/boot/configurationprocessor/json/JSON.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/json-shade/java/org/springframework/boot/configurationprocessor/json/JSON.java @@ -102,24 +102,21 @@ static String toString(Object value) { return null; } - public static JSONException typeMismatch(Object indexOrName, Object actual, - String requiredType) throws JSONException { + public static JSONException typeMismatch(Object indexOrName, Object actual, String requiredType) + throws JSONException { if (actual == null) { throw new JSONException("Value at " + indexOrName + " is null."); } - throw new JSONException("Value " + actual + " at " + indexOrName + " of type " - + actual.getClass().getName() + " cannot be converted to " - + requiredType); + throw new JSONException("Value " + actual + " at " + indexOrName + " of type " + actual.getClass().getName() + + " cannot be converted to " + requiredType); } - public static JSONException typeMismatch(Object actual, String requiredType) - throws JSONException { + public static JSONException typeMismatch(Object actual, String requiredType) throws JSONException { if (actual == null) { throw new JSONException("Value is null."); } - throw new JSONException( - "Value " + actual + " of type " + actual.getClass().getName() - + " cannot be converted to " + requiredType); + throw new JSONException("Value " + actual + " of type " + actual.getClass().getName() + + " cannot be converted to " + requiredType); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/json-shade/java/org/springframework/boot/configurationprocessor/json/JSONArray.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/json-shade/java/org/springframework/boot/configurationprocessor/json/JSONArray.java index 06d3a2c4b7de..ebd63c4be025 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/json-shade/java/org/springframework/boot/configurationprocessor/json/JSONArray.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/json-shade/java/org/springframework/boot/configurationprocessor/json/JSONArray.java @@ -127,7 +127,6 @@ public int length() { /** * Appends {@code value} to the end of this array. - * * @param value the value * @return this array. */ @@ -138,7 +137,6 @@ public JSONArray put(boolean value) { /** * Appends {@code value} to the end of this array. - * * @param value a finite value. May not be {@link Double#isNaN() NaNs} or * {@link Double#isInfinite() infinities}. * @return this array. @@ -171,7 +169,6 @@ public JSONArray put(long value) { /** * Appends {@code value} to the end of this array. - * * @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean, Integer, * Long, Double, {@link JSONObject#NULL}, or {@code null}. May not be * {@link Double#isNaN() NaNs} or {@link Double#isInfinite() infinities}. Unsupported @@ -288,8 +285,7 @@ public Object get(int index) throws JSONException { return value; } catch (IndexOutOfBoundsException e) { - throw new JSONException( - "Index " + index + " out of range [0.." + this.values.size() + ")"); + throw new JSONException("Index " + index + " out of range [0.." + this.values.size() + ")"); } } @@ -444,7 +440,6 @@ public int optInt(int index, int fallback) { * a long. * @param index the index to get the value from * @return the {@code value} - * * @throws JSONException if the value at {@code index} doesn't exist or cannot be * coerced to a long. */ @@ -642,7 +637,6 @@ public String toString() { * 94043, * 90210 * ] - * * @param indentSpaces the number of spaces to indent for each level of nesting. * @return a human readable JSON string of this array * @throws JSONException if processing of json failed diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/json-shade/java/org/springframework/boot/configurationprocessor/json/JSONObject.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/json-shade/java/org/springframework/boot/configurationprocessor/json/JSONObject.java index e2377bec6d73..b7a34606bb65 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/json-shade/java/org/springframework/boot/configurationprocessor/json/JSONObject.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/json-shade/java/org/springframework/boot/configurationprocessor/json/JSONObject.java @@ -41,11 +41,11 @@ *
  • When the requested type is an int, other {@link Number} types will be coerced using * {@link Number#intValue() intValue}. Strings that can be coerced using * {@link Double#valueOf(String)} will be, and then cast to int. - *
  • When the requested type is a long, other {@link Number} types will - * be coerced using {@link Number#longValue() longValue}. Strings that can be coerced - * using {@link Double#valueOf(String)} will be, and then cast to long. This two-step - * conversion is lossy for very large values. For example, the string - * "9223372036854775806" yields the long 9223372036854775807. + *
  • When the requested type is a long, other {@link Number} types will be + * coerced using {@link Number#longValue() longValue}. Strings that can be coerced using + * {@link Double#valueOf(String)} will be, and then cast to long. This two-step conversion + * is lossy for very large values. For example, the string "9223372036854775806" yields + * the long 9223372036854775807. *
  • When the requested type is a String, other non-null values will be coerced using * {@link String#valueOf(Object)}. Although null cannot be coerced, the sentinel value * {@link JSONObject#NULL} is coerced to the string "null". @@ -117,7 +117,6 @@ public JSONObject() { /** * Creates a new {@code JSONObject} by copying all name/value mappings from the given * map. - * * @param copyFrom a map whose keys are of type {@link String} and whose values are of * supported types. * @throws NullPointerException if any of the map's keys are null. @@ -334,7 +333,6 @@ String checkName(String name) throws JSONException { /** * Removes the named mapping if it exists; does nothing otherwise. - * * @param name the name of the property * @return the value previously mapped by {@code name}, or null if there was no such * mapping. @@ -430,7 +428,6 @@ public boolean optBoolean(String name, boolean fallback) { /** * Returns the value mapped by {@code name} if it exists and is a double or can be * coerced to a double. - * * @param name the name of the property * @return the value * @throws JSONException if the mapping doesn't exist or cannot be coerced to a @@ -690,8 +687,7 @@ public Iterator keys() { * @return the array */ public JSONArray names() { - return this.nameValuePairs.isEmpty() ? null - : new JSONArray(new ArrayList<>(this.nameValuePairs.keySet())); + return this.nameValuePairs.isEmpty() ? null : new JSONArray(new ArrayList<>(this.nameValuePairs.keySet())); } /** @@ -823,9 +819,9 @@ else if (o.getClass().isArray()) { if (o instanceof Map) { return new JSONObject((Map) o); } - if (o instanceof Boolean || o instanceof Byte || o instanceof Character - || o instanceof Double || o instanceof Float || o instanceof Integer - || o instanceof Long || o instanceof Short || o instanceof String) { + if (o instanceof Boolean || o instanceof Byte || o instanceof Character || o instanceof Double + || o instanceof Float || o instanceof Integer || o instanceof Long || o instanceof Short + || o instanceof String) { return o; } if (o.getClass().getPackage().getName().startsWith("java.")) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/json-shade/java/org/springframework/boot/configurationprocessor/json/JSONStringer.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/json-shade/java/org/springframework/boot/configurationprocessor/json/JSONStringer.java index df2c8d7ec493..c3d73a970ace 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/json-shade/java/org/springframework/boot/configurationprocessor/json/JSONStringer.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/json-shade/java/org/springframework/boot/configurationprocessor/json/JSONStringer.java @@ -191,8 +191,7 @@ JSONStringer open(Scope empty, String openBracket) throws JSONException { * @return the JSON stringer * @throws JSONException if processing of json failed */ - JSONStringer close(Scope empty, Scope nonempty, String closeBracket) - throws JSONException { + JSONStringer close(Scope empty, Scope nonempty, String closeBracket) throws JSONException { Scope context = peek(); if (context != nonempty && context != empty) { throw new JSONException("Nesting problem"); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/json-shade/java/org/springframework/boot/configurationprocessor/json/JSONTokener.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/json-shade/java/org/springframework/boot/configurationprocessor/json/JSONTokener.java index 6bc692e71ef5..2a47e73e94ad 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/json-shade/java/org/springframework/boot/configurationprocessor/json/JSONTokener.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/json-shade/java/org/springframework/boot/configurationprocessor/json/JSONTokener.java @@ -375,8 +375,8 @@ else if (first != -1) { throw syntaxError("Names cannot be null"); } else { - throw syntaxError("Names must be strings, but " + name - + " is of type " + name.getClass().getName()); + throw syntaxError( + "Names must be strings, but " + name + " is of type " + name.getClass().getName()); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java index e619f75bb7d4..7cc52dfac797 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,9 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.time.Duration; +import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -57,29 +59,43 @@ * @author Jonas Keßler * @since 1.2.0 */ -@SupportedAnnotationTypes({ "*" }) +@SupportedAnnotationTypes({ ConfigurationMetadataAnnotationProcessor.CONFIGURATION_PROPERTIES_ANNOTATION, + ConfigurationMetadataAnnotationProcessor.CONTROLLER_ENDPOINT_ANNOTATION, + ConfigurationMetadataAnnotationProcessor.ENDPOINT_ANNOTATION, + ConfigurationMetadataAnnotationProcessor.JMX_ENDPOINT_ANNOTATION, + ConfigurationMetadataAnnotationProcessor.REST_CONTROLLER_ENDPOINT_ANNOTATION, + ConfigurationMetadataAnnotationProcessor.SERVLET_ENDPOINT_ANNOTATION, + ConfigurationMetadataAnnotationProcessor.WEB_ENDPOINT_ANNOTATION, + "org.springframework.context.annotation.Configuration" }) public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor { - static final String ADDITIONAL_METADATA_LOCATIONS_OPTION = "org.springframework.boot." - + "configurationprocessor.additionalMetadataLocations"; + static final String ADDITIONAL_METADATA_LOCATIONS_OPTION = "org.springframework.boot.configurationprocessor.additionalMetadataLocations"; - static final String CONFIGURATION_PROPERTIES_ANNOTATION = "org.springframework.boot." - + "context.properties.ConfigurationProperties"; + static final String CONFIGURATION_PROPERTIES_ANNOTATION = "org.springframework.boot.context.properties.ConfigurationProperties"; - static final String NESTED_CONFIGURATION_PROPERTY_ANNOTATION = "org.springframework.boot." - + "context.properties.NestedConfigurationProperty"; + static final String NESTED_CONFIGURATION_PROPERTY_ANNOTATION = "org.springframework.boot.context.properties.NestedConfigurationProperty"; - static final String DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION = "org.springframework.boot." - + "context.properties.DeprecatedConfigurationProperty"; + static final String DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION = "org.springframework.boot.context.properties.DeprecatedConfigurationProperty"; static final String CONSTRUCTOR_BINDING_ANNOTATION = "org.springframework.boot.context.properties.ConstructorBinding"; static final String DEFAULT_VALUE_ANNOTATION = "org.springframework.boot.context.properties.bind.DefaultValue"; + static final String CONTROLLER_ENDPOINT_ANNOTATION = "org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint"; + static final String ENDPOINT_ANNOTATION = "org.springframework.boot.actuate.endpoint.annotation.Endpoint"; - static final String READ_OPERATION_ANNOTATION = "org.springframework.boot.actuate." - + "endpoint.annotation.ReadOperation"; + static final String JMX_ENDPOINT_ANNOTATION = "org.springframework.boot.actuate.endpoint.jmx.annotation.JmxEndpoint"; + + static final String REST_CONTROLLER_ENDPOINT_ANNOTATION = "org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint"; + + static final String SERVLET_ENDPOINT_ANNOTATION = "org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpoint"; + + static final String WEB_ENDPOINT_ANNOTATION = "org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint"; + + static final String READ_OPERATION_ANNOTATION = "org.springframework.boot.actuate.endpoint.annotation.ReadOperation"; + + static final String NAME_ANNOTATION = "org.springframework.boot.context.properties.bind.Name"; private static final Set SUPPORTED_OPTIONS = Collections .unmodifiableSet(Collections.singleton(ADDITIONAL_METADATA_LOCATIONS_OPTION)); @@ -110,14 +126,19 @@ protected String defaultValueAnnotation() { return DEFAULT_VALUE_ANNOTATION; } - protected String endpointAnnotation() { - return ENDPOINT_ANNOTATION; + protected Set endpointAnnotations() { + return new HashSet<>(Arrays.asList(CONTROLLER_ENDPOINT_ANNOTATION, ENDPOINT_ANNOTATION, JMX_ENDPOINT_ANNOTATION, + REST_CONTROLLER_ENDPOINT_ANNOTATION, SERVLET_ENDPOINT_ANNOTATION, WEB_ENDPOINT_ANNOTATION)); } protected String readOperationAnnotation() { return READ_OPERATION_ANNOTATION; } + protected String nameAnnotation() { + return NAME_ANNOTATION; + } + @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); @@ -135,8 +156,8 @@ public synchronized void init(ProcessingEnvironment env) { this.metadataCollector = new MetadataCollector(env, this.metadataStore.readMetadata()); this.metadataEnv = new MetadataGenerationEnvironment(env, configurationPropertiesAnnotation(), nestedConfigurationPropertyAnnotation(), deprecatedConfigurationPropertyAnnotation(), - constructorBindingAnnotation(), defaultValueAnnotation(), endpointAnnotation(), - readOperationAnnotation()); + constructorBindingAnnotation(), defaultValueAnnotation(), endpointAnnotations(), + readOperationAnnotation(), nameAnnotation()); } @Override @@ -148,9 +169,11 @@ public boolean process(Set annotations, RoundEnvironment processElement(element); } } - TypeElement endpointType = this.metadataEnv.getEndpointAnnotationElement(); - if (endpointType != null) { // Is @Endpoint available - getElementsAnnotatedOrMetaAnnotatedWith(roundEnv, endpointType).forEach(this::processEndpoint); + Set endpointTypes = this.metadataEnv.getEndpointAnnotationElements(); + if (!endpointTypes.isEmpty()) { // Are endpoint annotations available + for (TypeElement endpointType : endpointTypes) { + getElementsAnnotatedOrMetaAnnotatedWith(roundEnv, endpointType).forEach(this::processEndpoint); + } } if (roundEnv.processingOver()) { try { @@ -181,10 +204,10 @@ private void processElement(Element element) { if (annotation != null) { String prefix = getPrefix(annotation); if (element instanceof TypeElement) { - processAnnotatedTypeElement(prefix, (TypeElement) element, new Stack()); + processAnnotatedTypeElement(prefix, (TypeElement) element, new Stack<>()); } else if (element instanceof ExecutableElement) { - processExecutableElement(prefix, (ExecutableElement) element, new Stack()); + processExecutableElement(prefix, (ExecutableElement) element, new Stack<>()); } } } @@ -210,7 +233,7 @@ private void processExecutableElement(String prefix, ExecutableElement element, element.toString()); if (this.metadataCollector.hasSimilarGroup(group)) { this.processingEnv.getMessager().printMessage(Kind.ERROR, - "Duplicate `@ConfigurationProperties` definition for prefix '" + prefix + "'", element); + "Duplicate @ConfigurationProperties definition for prefix '" + prefix + "'", element); } else { this.metadataCollector.add(group); @@ -253,7 +276,7 @@ private void processEndpoint(Element element, List annotations) { private void processEndpoint(AnnotationMirror annotation, TypeElement element) { Map elementValues = this.metadataEnv.getAnnotationElementValues(annotation); String endpointId = (String) elementValues.get("id"); - if (endpointId == null || "".equals(endpointId)) { + if (endpointId == null || endpointId.isEmpty()) { return; // Can't process that endpoint } String endpointKey = ItemMetadata.newItemMetadataPrefix("management.endpoint.", endpointId); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptor.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptor.java index d2fdc0787d19..dfc6f006530d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptor.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.util.List; import java.util.Map; +import java.util.function.Function; import java.util.stream.Collectors; import javax.lang.model.element.AnnotationMirror; @@ -106,21 +107,14 @@ private static class DefaultValueCoercionTypeVisitor extends TypeKindVisitor8 T parseNumber(String value, Function parser, + PrimitiveType primitiveType) { try { - return Integer.valueOf(value); + return parser.apply(value); } catch (NumberFormatException ex) { - throw new IllegalArgumentException(String.format("Invalid number representation '%s'", value)); - } - } - - private Double parseFloatingPoint(String value) { - try { - return Double.valueOf(value); - } - catch (NumberFormatException ex) { - throw new IllegalArgumentException(String.format("Invalid floating point representation '%s'", value)); + throw new IllegalArgumentException( + String.format("Invalid %s representation '%s'", primitiveType, value)); } } @@ -131,22 +125,22 @@ public Object visitPrimitiveAsBoolean(PrimitiveType t, String value) { @Override public Object visitPrimitiveAsByte(PrimitiveType t, String value) { - return parseInteger(value); + return parseNumber(value, Byte::parseByte, t); } @Override public Object visitPrimitiveAsShort(PrimitiveType t, String value) { - return parseInteger(value); + return parseNumber(value, Short::parseShort, t); } @Override public Object visitPrimitiveAsInt(PrimitiveType t, String value) { - return parseInteger(value); + return parseNumber(value, Integer::parseInt, t); } @Override public Object visitPrimitiveAsLong(PrimitiveType t, String value) { - return parseInteger(value); + return parseNumber(value, Long::parseLong, t); } @Override @@ -159,12 +153,12 @@ public Object visitPrimitiveAsChar(PrimitiveType t, String value) { @Override public Object visitPrimitiveAsFloat(PrimitiveType t, String value) { - return parseFloatingPoint(value); + return parseNumber(value, Float::parseFloat, t); } @Override public Object visitPrimitiveAsDouble(PrimitiveType t, String value) { - return parseFloatingPoint(value); + return parseNumber(value, Double::parseDouble, t); } } @@ -180,12 +174,12 @@ public Object visitPrimitiveAsBoolean(PrimitiveType t, Void ignore) { @Override public Object visitPrimitiveAsByte(PrimitiveType t, Void ignore) { - return 0; + return (byte) 0; } @Override public Object visitPrimitiveAsShort(PrimitiveType t, Void ignore) { - return 0; + return (short) 0; } @Override @@ -205,7 +199,7 @@ public Object visitPrimitiveAsChar(PrimitiveType t, Void ignore) { @Override public Object visitPrimitiveAsFloat(PrimitiveType t, Void ignore) { - return 0; + return 0F; } @Override diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataCollector.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataCollector.java index b9e4f0e3c6f9..c6fe7f81d79e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataCollector.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataCollector.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -98,7 +98,7 @@ public ConfigurationMetadata getMetadata() { List items = this.previousMetadata.getItems(); for (ItemMetadata item : items) { if (shouldBeMerged(item)) { - metadata.add(item); + metadata.addIfMissing(item); } } } @@ -111,7 +111,7 @@ private boolean shouldBeMerged(ItemMetadata itemMetadata) { } private boolean deletedInCurrentBuild(String sourceType) { - return this.processingEnvironment.getElementUtils().getTypeElement(sourceType) == null; + return this.processingEnvironment.getElementUtils().getTypeElement(sourceType.replace('$', '.')) == null; } private boolean processedInCurrentBuild(String sourceType) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java index 0fe689c165ef..4e5ad965c555 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,9 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; import javax.annotation.processing.Messager; import javax.annotation.processing.ProcessingEnvironment; @@ -91,14 +93,16 @@ class MetadataGenerationEnvironment { private final String defaultValueAnnotation; - private final String endpointAnnotation; + private final Set endpointAnnotations; private final String readOperationAnnotation; + private final String nameAnnotation; + MetadataGenerationEnvironment(ProcessingEnvironment environment, String configurationPropertiesAnnotation, String nestedConfigurationPropertyAnnotation, String deprecatedConfigurationPropertyAnnotation, - String constructorBindingAnnotation, String defaultValueAnnotation, String endpointAnnotation, - String readOperationAnnotation) { + String constructorBindingAnnotation, String defaultValueAnnotation, Set endpointAnnotations, + String readOperationAnnotation, String nameAnnotation) { this.typeUtils = new TypeUtils(environment); this.elements = environment.getElementUtils(); this.messager = environment.getMessager(); @@ -108,8 +112,9 @@ class MetadataGenerationEnvironment { this.deprecatedConfigurationPropertyAnnotation = deprecatedConfigurationPropertyAnnotation; this.constructorBindingAnnotation = constructorBindingAnnotation; this.defaultValueAnnotation = defaultValueAnnotation; - this.endpointAnnotation = endpointAnnotation; + this.endpointAnnotations = endpointAnnotations; this.readOperationAnnotation = readOperationAnnotation; + this.nameAnnotation = nameAnnotation; } private static FieldValuesParser resolveFieldValuesParser(ProcessingEnvironment env) { @@ -170,8 +175,8 @@ ItemDeprecation resolveItemDeprecation(Element element) { reason = (String) elementValues.get("reason"); replacement = (String) elementValues.get("replacement"); } - reason = "".equals(reason) ? null : reason; - replacement = "".equals(replacement) ? null : replacement; + reason = (reason == null || reason.isEmpty()) ? null : reason; + replacement = (replacement == null || replacement.isEmpty()) ? null : replacement; return new ItemDeprecation(reason, replacement); } @@ -267,14 +272,19 @@ AnnotationMirror getDefaultValueAnnotation(Element element) { return getAnnotation(element, this.defaultValueAnnotation); } - TypeElement getEndpointAnnotationElement() { - return this.elements.getTypeElement(this.endpointAnnotation); + Set getEndpointAnnotationElements() { + return this.endpointAnnotations.stream().map(this.elements::getTypeElement).filter(Objects::nonNull) + .collect(Collectors.toSet()); } AnnotationMirror getReadOperationAnnotation(Element element) { return getAnnotation(element, this.readOperationAnnotation); } + AnnotationMirror getNameAnnotation(Element element) { + return getAnnotation(element, this.nameAnnotation); + } + boolean hasNullableAnnotation(Element element) { return getAnnotation(element, NULLABLE_ANNOTATION) != null; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataStore.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataStore.java index b30927d63c6b..c44724f9b11f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataStore.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataStore.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,9 +44,9 @@ public class MetadataStore { private static final String ADDITIONAL_METADATA_PATH = "META-INF/additional-spring-configuration-metadata.json"; - private static final String RESOURCES_FOLDER = "resources"; + private static final String RESOURCES_DIRECTORY = "resources"; - private static final String CLASSES_FOLDER = "classes"; + private static final String CLASSES_DIRECTORY = "classes"; private final ProcessingEnvironment environment; @@ -122,18 +122,18 @@ File locateAdditionalMetadataFile(File standardLocation) throws IOException { } } } - return new File(locateGradleResourcesFolder(standardLocation), ADDITIONAL_METADATA_PATH); + return new File(locateGradleResourcesDirectory(standardLocation), ADDITIONAL_METADATA_PATH); } - private File locateGradleResourcesFolder(File standardAdditionalMetadataLocation) throws FileNotFoundException { + private File locateGradleResourcesDirectory(File standardAdditionalMetadataLocation) throws FileNotFoundException { String path = standardAdditionalMetadataLocation.getPath(); - int index = path.lastIndexOf(CLASSES_FOLDER); + int index = path.lastIndexOf(CLASSES_DIRECTORY); if (index < 0) { throw new FileNotFoundException(); } - String buildFolderPath = path.substring(0, index); + String buildDirectoryPath = path.substring(0, index); File classOutputLocation = standardAdditionalMetadataLocation.getParentFile().getParentFile(); - return new File(buildFolderPath, RESOURCES_FOLDER + '/' + classOutputLocation.getName()); + return new File(buildDirectoryPath, RESOURCES_DIRECTORY + '/' + classOutputLocation.getName()); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java index fc8572d131a0..7c3691ae0423 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.NestingKind; import javax.lang.model.element.TypeElement; @@ -76,7 +77,7 @@ Stream> resolveConstructorProperties(TypeElement type, Exe TypeElementMembers members, ExecutableElement constructor) { Map> candidates = new LinkedHashMap<>(); constructor.getParameters().forEach((parameter) -> { - String name = parameter.getSimpleName().toString(); + String name = getParameterName(parameter); TypeMirror propertyType = parameter.asType(); ExecutableElement getter = members.getPublicGetter(name, propertyType); ExecutableElement setter = members.getPublicSetter(name, propertyType); @@ -87,14 +88,24 @@ Stream> resolveConstructorProperties(TypeElement type, Exe return candidates.values().stream(); } + private String getParameterName(VariableElement parameter) { + AnnotationMirror nameAnnotation = this.environment.getNameAnnotation(parameter); + if (nameAnnotation != null) { + return (String) this.environment.getAnnotationElementValues(nameAnnotation).get("value"); + } + return parameter.getSimpleName().toString(); + } + Stream> resolveJavaBeanProperties(TypeElement type, ExecutableElement factoryMethod, TypeElementMembers members) { // First check if we have regular java bean properties there Map> candidates = new LinkedHashMap<>(); - members.getPublicGetters().forEach((name, getter) -> { + members.getPublicGetters().forEach((name, getters) -> { + VariableElement field = members.getFields().get(name); + ExecutableElement getter = findMatchingGetter(members, getters, field); TypeMirror propertyType = getter.getReturnType(); - register(candidates, new JavaBeanPropertyDescriptor(type, factoryMethod, getter, name, propertyType, - members.getFields().get(name), members.getPublicSetter(name, propertyType))); + register(candidates, new JavaBeanPropertyDescriptor(type, factoryMethod, getter, name, propertyType, field, + members.getPublicSetter(name, propertyType))); }); // Then check for Lombok ones members.getFields().forEach((name, field) -> { @@ -107,6 +118,14 @@ Stream> resolveJavaBeanProperties(TypeElement type, Execut return candidates.values().stream(); } + private ExecutableElement findMatchingGetter(TypeElementMembers members, List candidates, + VariableElement field) { + if (candidates.size() > 1 && field != null) { + return members.getMatchingGetter(candidates, field.asType()); + } + return candidates.get(0); + } + private void register(Map> candidates, PropertyDescriptor descriptor) { if (!candidates.containsKey(descriptor.getName()) && isCandidate(descriptor)) { candidates.put(descriptor.getName(), descriptor); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeElementMembers.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeElementMembers.java index 75be606b3bc1..f51290c654b7 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeElementMembers.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeElementMembers.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Function; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; @@ -44,15 +45,18 @@ class TypeElementMembers { private final MetadataGenerationEnvironment env; + private final TypeElement targetType; + private final Map fields = new LinkedHashMap<>(); - private final Map publicGetters = new LinkedHashMap<>(); + private final Map> publicGetters = new LinkedHashMap<>(); private final Map> publicSetters = new LinkedHashMap<>(); - TypeElementMembers(MetadataGenerationEnvironment env, TypeElement element) { + TypeElementMembers(MetadataGenerationEnvironment env, TypeElement targetType) { this.env = env; - process(element); + this.targetType = targetType; + process(targetType); } private void process(TypeElement element) { @@ -71,8 +75,14 @@ private void process(TypeElement element) { private void processMethod(ExecutableElement method) { if (isPublic(method)) { String name = method.getSimpleName().toString(); - if (isGetter(method) && !this.publicGetters.containsKey(name)) { - this.publicGetters.put(getAccessorName(name), method); + if (isGetter(method)) { + String propertyName = getAccessorName(name); + List matchingGetters = this.publicGetters.computeIfAbsent(propertyName, + (k) -> new ArrayList<>()); + TypeMirror returnType = method.getReturnType(); + if (getMatchingGetter(matchingGetters, returnType) == null) { + matchingGetters.add(method); + } } else if (isSetter(method)) { String propertyName = getAccessorName(name); @@ -92,10 +102,19 @@ private boolean isPublic(ExecutableElement method) { && !modifiers.contains(Modifier.STATIC); } + ExecutableElement getMatchingGetter(List candidates, TypeMirror type) { + return getMatchingAccessor(candidates, type, ExecutableElement::getReturnType); + } + private ExecutableElement getMatchingSetter(List candidates, TypeMirror type) { + return getMatchingAccessor(candidates, type, (candidate) -> candidate.getParameters().get(0).asType()); + } + + private ExecutableElement getMatchingAccessor(List candidates, TypeMirror type, + Function typeExtractor) { for (ExecutableElement candidate : candidates) { - TypeMirror paramType = candidate.getParameters().get(0).asType(); - if (this.env.getTypeUtils().isSameType(paramType, type)) { + TypeMirror candidateType = typeExtractor.apply(candidate); + if (this.env.getTypeUtils().isSameType(candidateType, type)) { return candidate; } } @@ -116,8 +135,19 @@ private boolean isSetter(ExecutableElement method) { private boolean isSetterReturnType(ExecutableElement method) { TypeMirror returnType = method.getReturnType(); - return (TypeKind.VOID == returnType.getKind() - || this.env.getTypeUtils().isSameType(method.getEnclosingElement().asType(), returnType)); + if (TypeKind.VOID == returnType.getKind()) { + return true; + } + if (TypeKind.DECLARED == returnType.getKind() + && this.env.getTypeUtils().isSameType(method.getEnclosingElement().asType(), returnType)) { + return true; + } + if (TypeKind.TYPEVAR == returnType.getKind()) { + String resolvedType = this.env.getTypeUtils().getType(this.targetType, returnType); + return (resolvedType != null + && resolvedType.equals(this.env.getTypeUtils().getQualifiedName(this.targetType))); + } + return false; } private String getAccessorName(String methodName) { @@ -137,35 +167,30 @@ Map getFields() { return Collections.unmodifiableMap(this.fields); } - Map getPublicGetters() { + Map> getPublicGetters() { return Collections.unmodifiableMap(this.publicGetters); } ExecutableElement getPublicGetter(String name, TypeMirror type) { - ExecutableElement candidate = this.publicGetters.get(name); - if (candidate != null) { - TypeMirror returnType = candidate.getReturnType(); - if (this.env.getTypeUtils().isSameType(returnType, type)) { - return candidate; - } - TypeMirror alternative = this.env.getTypeUtils().getWrapperOrPrimitiveFor(type); - if (alternative != null && this.env.getTypeUtils().isSameType(returnType, alternative)) { - return candidate; - } - } - return null; + List candidates = this.publicGetters.get(name); + return getPublicAccessor(candidates, type, (specificType) -> getMatchingGetter(candidates, specificType)); } ExecutableElement getPublicSetter(String name, TypeMirror type) { List candidates = this.publicSetters.get(name); + return getPublicAccessor(candidates, type, (specificType) -> getMatchingSetter(candidates, specificType)); + } + + private ExecutableElement getPublicAccessor(List candidates, TypeMirror type, + Function matchingAccessorExtractor) { if (candidates != null) { - ExecutableElement matching = getMatchingSetter(candidates, type); + ExecutableElement matching = matchingAccessorExtractor.apply(type); if (matching != null) { return matching; } TypeMirror alternative = this.env.getTypeUtils().getWrapperOrPrimitiveFor(type); if (alternative != null) { - return getMatchingSetter(candidates, alternative); + return matchingAccessorExtractor.apply(alternative); } } return null; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeUtils.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeUtils.java index 21039f7af15e..3e2810e3185b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeUtils.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -154,7 +154,7 @@ private TypeMirror getCollectionElementType(TypeMirror type) { if (((TypeElement) this.types.asElement(type)).getQualifiedName().contentEquals(Collection.class.getName())) { DeclaredType declaredType = (DeclaredType) type; // raw type, just "Collection" - if (declaredType.getTypeArguments().size() == 0) { + if (declaredType.getTypeArguments().isEmpty()) { return this.types.getDeclaredType(this.env.getElementUtils().getTypeElement(Object.class.getName())); } // return type argument to Collection<...> @@ -180,7 +180,7 @@ String getJavaDoc(Element element) { if (javadoc != null) { javadoc = NEW_LINE_PATTERN.matcher(javadoc).replaceAll("").trim(); } - return "".equals(javadoc) ? null : javadoc; + return (javadoc == null || javadoc.isEmpty()) ? null : javadoc; } /** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/JavaCompilerFieldValuesParser.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/JavaCompilerFieldValuesParser.java index 5c15c4f6b61f..7e574f1db331 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/JavaCompilerFieldValuesParser.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/JavaCompilerFieldValuesParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -98,6 +98,8 @@ private static class FieldCollector implements TreeVisitor { values.put("StandardCharsets.UTF_8", "UTF-8"); values.put("StandardCharsets.UTF_16", "UTF-16"); values.put("StandardCharsets.US_ASCII", "US-ASCII"); + values.put("Duration.ZERO", 0); + values.put("Period.ZERO", 0); WELL_KNOWN_STATIC_FINALS = Collections.unmodifiableMap(values); } @@ -116,6 +118,19 @@ private static class FieldCollector implements TreeVisitor { DURATION_SUFFIX = Collections.unmodifiableMap(values); } + private static final String PERIOD_OF = "Period.of"; + + private static final Map PERIOD_SUFFIX; + + static { + Map values = new HashMap<>(); + values.put("Days", "d"); + values.put("Weeks", "w"); + values.put("Months", "m"); + values.put("Years", "y"); + PERIOD_SUFFIX = Collections.unmodifiableMap(values); + } + private static final String DATA_SIZE_OF = "DataSize.of"; private static final Map DATA_SIZE_SUFFIX; @@ -194,6 +209,10 @@ private Object getFactoryValue(ExpressionTree expression, Object factoryValue) { if (dataSizeValue != null) { return dataSizeValue; } + Object periodValue = getFactoryValue(expression, factoryValue, PERIOD_OF, PERIOD_SUFFIX); + if (periodValue != null) { + return periodValue; + } return factoryValue; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/ReflectionWrapper.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/ReflectionWrapper.java index 92e3edeb53d6..152750385c02 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/ReflectionWrapper.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/ReflectionWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,7 +54,7 @@ protected Method findMethod(String name, Class... parameterTypes) { protected static Class findClass(ClassLoader classLoader, String name) { try { - return classLoader.loadClass(name); + return Class.forName(name, false, classLoader); } catch (ClassNotFoundException ex) { throw new IllegalStateException(ex); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/Trees.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/Trees.java index c958203d989d..f635b8e9701c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/Trees.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/Trees.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.boot.configurationprocessor.fieldvalues.javac; +import java.lang.reflect.Field; import java.lang.reflect.Method; import javax.annotation.processing.ProcessingEnvironment; @@ -38,10 +39,21 @@ Tree getTree(Element element) throws Exception { } static Trees instance(ProcessingEnvironment env) throws Exception { - ClassLoader classLoader = env.getClass().getClassLoader(); - Class type = findClass(classLoader, "com.sun.source.util.Trees"); - Method method = findMethod(type, "instance", ProcessingEnvironment.class); - return new Trees(method.invoke(null, env)); + try { + ClassLoader classLoader = env.getClass().getClassLoader(); + Class type = findClass(classLoader, "com.sun.source.util.Trees"); + Method method = findMethod(type, "instance", ProcessingEnvironment.class); + return new Trees(method.invoke(null, env)); + } + catch (Exception ex) { + return instance(unwrap(env)); + } + } + + private static ProcessingEnvironment unwrap(ProcessingEnvironment wrapper) throws Exception { + Field delegateField = wrapper.getClass().getDeclaredField("delegate"); + delegateField.setAccessible(true); + return (ProcessingEnvironment) delegateField.get(wrapper); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ConfigurationMetadata.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ConfigurationMetadata.java index 048d4e0fb0b3..1281954f591c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ConfigurationMetadata.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ConfigurationMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -62,7 +62,16 @@ public ConfigurationMetadata(ConfigurationMetadata metadata) { * @param itemMetadata the meta-data to add */ public void add(ItemMetadata itemMetadata) { - add(this.items, itemMetadata.getName(), itemMetadata); + add(this.items, itemMetadata.getName(), itemMetadata, false); + } + + /** + * Add item meta-data if it's not already present. + * @param itemMetadata the meta-data to add + * @since 2.4.0 + */ + public void addIfMissing(ItemMetadata itemMetadata) { + add(this.items, itemMetadata.getName(), itemMetadata, true); } /** @@ -70,7 +79,7 @@ public void add(ItemMetadata itemMetadata) { * @param itemHint the item hint to add */ public void add(ItemHint itemHint) { - add(this.hints, itemHint.getName(), itemHint); + add(this.hints, itemHint.getName(), itemHint, false); } /** @@ -131,13 +140,15 @@ protected void mergeItemMetadata(ItemMetadata metadata) { } } else { - add(this.items, metadata.getName(), metadata); + add(this.items, metadata.getName(), metadata, false); } } - private void add(Map> map, K key, V value) { + private void add(Map> map, K key, V value, boolean ifMissing) { List values = map.computeIfAbsent(key, (k) -> new ArrayList<>()); - values.add(value); + if (!ifMissing || values.isEmpty()) { + values.add(value); + } } private ItemMetadata findMatchingItemMetadata(ItemMetadata metadata) { @@ -171,14 +182,15 @@ private boolean nullSafeEquals(Object o1, Object o2) { public static String nestedPrefix(String prefix, String name) { String nestedPrefix = (prefix != null) ? prefix : ""; String dashedName = toDashedCase(name); - nestedPrefix += "".equals(nestedPrefix) ? dashedName : "." + dashedName; + nestedPrefix += (nestedPrefix == null || nestedPrefix.isEmpty()) ? dashedName : "." + dashedName; return nestedPrefix; } static String toDashedCase(String name) { StringBuilder dashed = new StringBuilder(); Character previous = null; - for (char current : name.toCharArray()) { + for (int i = 0; i < name.length(); i++) { + char current = name.charAt(i); if (SEPARATORS.contains(current)) { dashed.append("-"); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ItemMetadata.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ItemMetadata.java index 781653bd078d..315a281774ef 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ItemMetadata.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ItemMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ */ public final class ItemMetadata implements Comparable { - private ItemType itemType; + private final ItemType itemType; private String name; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonConverter.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonConverter.java index df0f0f006137..de97b71f3293 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonConverter.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -160,29 +160,22 @@ private Object extractItemValue(Object value) { private static class ItemMetadataComparator implements Comparator { + private static final Comparator GROUP = Comparator.comparing(ItemMetadata::getName) + .thenComparing(ItemMetadata::getSourceType, Comparator.nullsFirst(Comparator.naturalOrder())); + + private static final Comparator ITEM = Comparator.comparing(ItemMetadataComparator::isDeprecated) + .thenComparing(ItemMetadata::getName) + .thenComparing(ItemMetadata::getSourceType, Comparator.nullsFirst(Comparator.naturalOrder())); + @Override public int compare(ItemMetadata o1, ItemMetadata o2) { if (o1.isOfItemType(ItemType.GROUP)) { - return compareGroup(o1, o2); - } - return compareProperty(o1, o2); - } - - private int compareGroup(ItemMetadata o1, ItemMetadata o2) { - return o1.getName().compareTo(o2.getName()); - } - - private int compareProperty(ItemMetadata o1, ItemMetadata o2) { - if (isDeprecated(o1) && !isDeprecated(o2)) { - return 1; - } - if (isDeprecated(o2) && !isDeprecated(o1)) { - return -1; + return GROUP.compare(o1, o2); } - return o1.getName().compareTo(o2.getName()); + return ITEM.compare(o1, o2); } - private boolean isDeprecated(ItemMetadata item) { + private static boolean isDeprecated(ItemMetadata item) { return item.getDeprecation() != null; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/resources/META-INF/gradle/incremental.annotation.processors b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/resources/META-INF/gradle/incremental.annotation.processors new file mode 100644 index 000000000000..cd04b210b1d4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/resources/META-INF/gradle/incremental.annotation.processors @@ -0,0 +1 @@ +org.springframework.boot.configurationprocessor.ConfigurationMetadataAnnotationProcessor,aggregating \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/AbstractMetadataGenerationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/AbstractMetadataGenerationTests.java index 1af2311b2a11..8cd35bd6fa1c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/AbstractMetadataGenerationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/AbstractMetadataGenerationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.io.File; import java.io.IOException; +import java.util.Arrays; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.io.TempDir; @@ -54,4 +55,11 @@ protected ConfigurationMetadata compile(Class... types) { return processor.getMetadata(); } + protected ConfigurationMetadata compile(File... sources) { + TestConfigurationMetadataAnnotationProcessor processor = new TestConfigurationMetadataAnnotationProcessor( + this.compiler.getOutputLocation()); + this.compiler.getTask(Arrays.asList(sources)).call(processor); + return processor.getMetadata(); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java index c1fee73ad3a6..b4425f1edf9e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,18 @@ package org.springframework.boot.configurationprocessor; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; + import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; +import org.junit.jupiter.api.io.TempDir; import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata; +import org.springframework.boot.configurationprocessor.metadata.ItemMetadata; import org.springframework.boot.configurationprocessor.metadata.Metadata; import org.springframework.boot.configurationsample.recursive.RecursiveProperties; import org.springframework.boot.configurationsample.simple.ClassWithNestedProperties; @@ -37,8 +46,10 @@ import org.springframework.boot.configurationsample.specific.AnnotatedGetter; import org.springframework.boot.configurationsample.specific.BoxingPojo; import org.springframework.boot.configurationsample.specific.BuilderPojo; +import org.springframework.boot.configurationsample.specific.DeprecatedLessPreciseTypePojo; import org.springframework.boot.configurationsample.specific.DeprecatedUnrelatedMethodPojo; import org.springframework.boot.configurationsample.specific.DoubleRegistrationProperties; +import org.springframework.boot.configurationsample.specific.EmptyDefaultValueProperties; import org.springframework.boot.configurationsample.specific.ExcludedTypesPojo; import org.springframework.boot.configurationsample.specific.InnerClassAnnotatedGetterConfig; import org.springframework.boot.configurationsample.specific.InnerClassHierarchicalProperties; @@ -66,10 +77,23 @@ */ class ConfigurationMetadataAnnotationProcessorTests extends AbstractMetadataGenerationTests { + @Test + void supportedAnnotations() { + assertThat(new ConfigurationMetadataAnnotationProcessor().getSupportedAnnotationTypes()) + .containsExactlyInAnyOrder("org.springframework.boot.context.properties.ConfigurationProperties", + "org.springframework.context.annotation.Configuration", + "org.springframework.boot.actuate.endpoint.annotation.Endpoint", + "org.springframework.boot.actuate.endpoint.jmx.annotation.JmxEndpoint", + "org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint", + "org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint", + "org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpoint", + "org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint"); + } + @Test void notAnnotated() { ConfigurationMetadata metadata = compile(NotAnnotated.class); - assertThat(metadata.getItems()).isEmpty(); + assertThat(metadata).isNull(); } @Test @@ -190,12 +214,22 @@ void deprecatedOnUnrelatedSetter() { } @Test - void boxingOnSetter() { + void deprecatedWithLessPreciseType() { + Class type = DeprecatedLessPreciseTypePojo.class; + ConfigurationMetadata metadata = compile(type); + assertThat(metadata).has(Metadata.withGroup("not.deprecated").fromSource(type)); + assertThat(metadata).has(Metadata.withProperty("not.deprecated.flag", Boolean.class).withDefaultValue(false) + .withNoDeprecation().fromSource(type)); + } + + @Test + void typBoxing() { Class type = BoxingPojo.class; ConfigurationMetadata metadata = compile(type); assertThat(metadata).has(Metadata.withGroup("boxing").fromSource(type)); assertThat(metadata) .has(Metadata.withProperty("boxing.flag", Boolean.class).withDefaultValue(false).fromSource(type)); + assertThat(metadata).has(Metadata.withProperty("boxing.another-flag", Boolean.class).fromSource(type)); assertThat(metadata).has(Metadata.withProperty("boxing.counter", Integer.class).fromSource(type)); } @@ -217,7 +251,7 @@ void parseCollectionConfig() { } @Test - void parseArrayConfig() throws Exception { + void parseArrayConfig() { ConfigurationMetadata metadata = compile(SimpleArrayProperties.class); assertThat(metadata).has(Metadata.withGroup("array").ofType(SimpleArrayProperties.class)); assertThat(metadata).has(Metadata.withProperty("array.primitive", "java.lang.Integer[]")); @@ -362,9 +396,54 @@ void constructorParameterPropertyWithInvalidDefaultValueOnCharacter() { .withMessageContaining("Compilation failed"); } + @Test + void constructorParameterPropertyWithEmptyDefaultValueOnProperty() { + ConfigurationMetadata metadata = compile(EmptyDefaultValueProperties.class); + assertThat(metadata).has(Metadata.withProperty("test.name")); + ItemMetadata nameMetadata = metadata.getItems().stream().filter((item) -> item.getName().equals("test.name")) + .findFirst().get(); + assertThat(nameMetadata.getDefaultValue()).isNull(); + } + @Test void recursivePropertiesDoNotCauseAStackOverflow() { compile(RecursiveProperties.class); } + @Test + @EnabledForJreRange(min = JRE.JAVA_16) + void explicityBoundRecordProperties(@TempDir File temp) throws IOException { + File exampleRecord = new File(temp, "ExampleRecord.java"); + try (PrintWriter writer = new PrintWriter(new FileWriter(exampleRecord))) { + writer.println("@org.springframework.boot.configurationsample.ConstructorBinding"); + writer.println("@org.springframework.boot.configurationsample.ConfigurationProperties(\"explicit\")"); + writer.println("public record ExampleRecord(String someString, Integer someInteger) {"); + writer.println("}"); + } + ConfigurationMetadata metadata = compile(exampleRecord); + assertThat(metadata).has(Metadata.withProperty("explicit.some-string")); + assertThat(metadata).has(Metadata.withProperty("explicit.some-integer")); + } + + @Test + @EnabledForJreRange(min = JRE.JAVA_16) + void explicitlyBoundRecordPropertiesWithDefaultValues(@TempDir File temp) throws IOException { + File exampleRecord = new File(temp, "ExampleRecord.java"); + try (PrintWriter writer = new PrintWriter(new FileWriter(exampleRecord))) { + writer.println("@org.springframework.boot.configurationsample.ConstructorBinding"); + writer.println( + "@org.springframework.boot.configurationsample.ConfigurationProperties(\"record.defaults\")"); + writer.println("public record ExampleRecord("); + writer.println("@org.springframework.boot.configurationsample.DefaultValue(\"An1s9n\") String someString,"); + writer.println("@org.springframework.boot.configurationsample.DefaultValue(\"594\") Integer someInteger"); + writer.println(") {"); + writer.println("}"); + } + ConfigurationMetadata metadata = compile(exampleRecord); + assertThat(metadata) + .has(Metadata.withProperty("record.defaults.some-string", String.class).withDefaultValue("An1s9n")); + assertThat(metadata) + .has(Metadata.withProperty("record.defaults.some-integer", Integer.class).withDefaultValue(594)); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptorTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptorTests.java index d3d6589315c7..c4a01ad1ca89 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptorTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -155,12 +155,13 @@ void constructorParameterPropertyWithPrimitiveTypes() throws IOException { process(ImmutablePrimitiveProperties.class, (roundEnv, metadataEnv) -> { TypeElement ownerElement = roundEnv.getRootElement(ImmutablePrimitiveProperties.class); assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "flag")).hasDefaultValue(false); - assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "octet")).hasDefaultValue(0); + assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "octet")).hasDefaultValue((byte) 0); assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "letter")).hasDefaultValue(null); - assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "number")).hasDefaultValue(0); + assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "number")) + .hasDefaultValue((short) 0); assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "counter")).hasDefaultValue(0); assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "value")).hasDefaultValue(0L); - assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "percentage")).hasDefaultValue(0); + assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "percentage")).hasDefaultValue(0F); assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "ratio")).hasDefaultValue(0D); }); } @@ -170,12 +171,14 @@ void constructorParameterPropertyWithPrimitiveTypesAndDefaultValues() throws IOE process(ImmutablePrimitiveWithDefaultsProperties.class, (roundEnv, metadataEnv) -> { TypeElement ownerElement = roundEnv.getRootElement(ImmutablePrimitiveWithDefaultsProperties.class); assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "flag")).hasDefaultValue(true); - assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "octet")).hasDefaultValue(120); + assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "octet")) + .hasDefaultValue((byte) 120); assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "letter")).hasDefaultValue("a"); - assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "number")).hasDefaultValue(1000); + assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "number")) + .hasDefaultValue((short) 1000); assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "counter")).hasDefaultValue(42); - assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "value")).hasDefaultValue(2000); - assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "percentage")).hasDefaultValue(0.5); + assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "value")).hasDefaultValue(2000L); + assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "percentage")).hasDefaultValue(0.5F); assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "ratio")).hasDefaultValue(42.42); }); } @@ -185,12 +188,14 @@ void constructorParameterPropertyWithPrimitiveWrapperTypesAndDefaultValues() thr process(ImmutablePrimitiveWrapperWithDefaultsProperties.class, (roundEnv, metadataEnv) -> { TypeElement ownerElement = roundEnv.getRootElement(ImmutablePrimitiveWrapperWithDefaultsProperties.class); assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "flag")).hasDefaultValue(true); - assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "octet")).hasDefaultValue(120); + assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "octet")) + .hasDefaultValue((byte) 120); assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "letter")).hasDefaultValue("a"); - assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "number")).hasDefaultValue(1000); + assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "number")) + .hasDefaultValue((short) 1000); assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "counter")).hasDefaultValue(42); - assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "value")).hasDefaultValue(2000); - assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "percentage")).hasDefaultValue(0.5); + assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "value")).hasDefaultValue(2000L); + assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "percentage")).hasDefaultValue(0.5F); assertItemMetadata(metadataEnv, createPropertyDescriptor(ownerElement, "ratio")).hasDefaultValue(42.42); }); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/DeducedImmutablePropertiesMetadataGenerationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/DeducedImmutablePropertiesMetadataGenerationTests.java index 84bc1f22ae11..bc74aca30477 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/DeducedImmutablePropertiesMetadataGenerationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/DeducedImmutablePropertiesMetadataGenerationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata; +import org.springframework.boot.configurationprocessor.metadata.ItemMetadata; import org.springframework.boot.configurationprocessor.metadata.Metadata; import org.springframework.boot.configurationsample.immutable.DeducedImmutableClassProperties; @@ -28,6 +29,7 @@ * Metadata generation tests for immutable properties deduced because they're nested. * * @author Phillip Webb + * @author Stephane Nicoll */ class DeducedImmutablePropertiesMetadataGenerationTests extends AbstractMetadataGenerationTests { @@ -35,7 +37,13 @@ class DeducedImmutablePropertiesMetadataGenerationTests extends AbstractMetadata void immutableSimpleProperties() { ConfigurationMetadata metadata = compile(DeducedImmutableClassProperties.class); assertThat(metadata).has(Metadata.withGroup("test").fromSource(DeducedImmutableClassProperties.class)); - assertThat(metadata).has(Metadata.withProperty("test.nested.name", String.class)); + assertThat(metadata).has(Metadata.withGroup("test.nested", DeducedImmutableClassProperties.Nested.class) + .fromSource(DeducedImmutableClassProperties.class)); + assertThat(metadata).has(Metadata.withProperty("test.nested.name", String.class) + .fromSource(DeducedImmutableClassProperties.Nested.class)); + ItemMetadata nestedMetadata = metadata.getItems().stream() + .filter((item) -> item.getName().equals("test.nested")).findFirst().get(); + assertThat(nestedMetadata.getDefaultValue()).isNull(); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/GenericsMetadataGenerationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/GenericsMetadataGenerationTests.java index 0643c22cc0e3..575ea90cc17a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/GenericsMetadataGenerationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/GenericsMetadataGenerationTests.java @@ -22,6 +22,7 @@ import org.springframework.boot.configurationprocessor.metadata.Metadata; import org.springframework.boot.configurationsample.generic.AbstractGenericProperties; import org.springframework.boot.configurationsample.generic.ComplexGenericProperties; +import org.springframework.boot.configurationsample.generic.ConcreteBuilderProperties; import org.springframework.boot.configurationsample.generic.GenericConfig; import org.springframework.boot.configurationsample.generic.SimpleGenericProperties; import org.springframework.boot.configurationsample.generic.UnresolvedGenericProperties; @@ -110,4 +111,15 @@ void wildcardTypes() { assertThat(metadata.getItems()).hasSize(3); } + @Test + void builderPatternWithGenericReturnType() { + ConfigurationMetadata metadata = compile(ConcreteBuilderProperties.class); + assertThat(metadata).has(Metadata.withGroup("builder").fromSource(ConcreteBuilderProperties.class)); + assertThat(metadata).has( + Metadata.withProperty("builder.number", Integer.class).fromSource(ConcreteBuilderProperties.class)); + assertThat(metadata).has( + Metadata.withProperty("builder.description", String.class).fromSource(ConcreteBuilderProperties.class)); + assertThat(metadata.getItems()).hasSize(3); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ImmutableNameAnnotationPropertiesTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ImmutableNameAnnotationPropertiesTests.java new file mode 100644 index 000000000000..8000658dd9e2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ImmutableNameAnnotationPropertiesTests.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.configurationprocessor; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata; +import org.springframework.boot.configurationprocessor.metadata.Metadata; +import org.springframework.boot.configurationsample.immutable.ImmutableNameAnnotationProperties; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Metadata generation tests for immutable properties using {@code @Name}. + * + * @author Phillip Webb + */ +class ImmutableNameAnnotationPropertiesTests extends AbstractMetadataGenerationTests { + + @Test + void immutableNameAnnotationProperties() { + ConfigurationMetadata metadata = compile(ImmutableNameAnnotationProperties.class); + assertThat(metadata).has(Metadata.withProperty("named.import", String.class) + .fromSource(ImmutableNameAnnotationProperties.class)); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/IncrementalBuildMetadataGenerationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/IncrementalBuildMetadataGenerationTests.java index dd7b1b6e789d..150e9b4813c8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/IncrementalBuildMetadataGenerationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/IncrementalBuildMetadataGenerationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.boot.configurationprocessor; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata; @@ -23,6 +24,7 @@ import org.springframework.boot.configurationsample.incremental.BarProperties; import org.springframework.boot.configurationsample.incremental.FooProperties; import org.springframework.boot.configurationsample.incremental.RenamedBarProperties; +import org.springframework.boot.configurationsample.simple.ClassWithNestedProperties; import static org.assertj.core.api.Assertions.assertThat; @@ -68,11 +70,11 @@ void incrementalBuildAnnotationRemoved() throws Exception { assertThat(metadata).has(Metadata.withProperty("bar.counter").withDefaultValue(0)); project.replaceText(BarProperties.class, "@ConfigurationProperties", "//@ConfigurationProperties"); metadata = project.incrementalBuild(BarProperties.class); - assertThat(metadata).has(Metadata.withProperty("foo.counter").withDefaultValue(0)); - assertThat(metadata).isNotEqualTo(Metadata.withProperty("bar.counter")); + assertThat(metadata).isNull(); } @Test + @Disabled("gh-26271") void incrementalBuildTypeRenamed() throws Exception { TestProject project = new TestProject(this.tempDir, FooProperties.class, BarProperties.class); ConfigurationMetadata metadata = project.fullBuild(); @@ -92,4 +94,12 @@ void incrementalBuildTypeRenamed() throws Exception { .has(Metadata.withProperty("bar.counter").withDefaultValue(0).fromSource(RenamedBarProperties.class)); } + @Test + void incrementalBuildDoesNotDeleteItems() throws Exception { + TestProject project = new TestProject(this.tempDir, ClassWithNestedProperties.class, FooProperties.class); + ConfigurationMetadata initialMetadata = project.fullBuild(); + ConfigurationMetadata updatedMetadata = project.incrementalBuild(FooProperties.class); + assertThat(initialMetadata.getItems()).isEqualTo(updatedMetadata.getItems()); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/MergeMetadataGenerationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/MergeMetadataGenerationTests.java index 005c7c98b71b..7b439437c80c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/MergeMetadataGenerationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/MergeMetadataGenerationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -203,9 +203,9 @@ void mergingOfAdditionalDeprecation() throws Exception { @Test void mergingOfAdditionalMetadata() throws Exception { - File metaInfFolder = new File(getCompiler().getOutputLocation(), "META-INF"); - metaInfFolder.mkdirs(); - File additionalMetadataFile = new File(metaInfFolder, "additional-spring-configuration-metadata.json"); + File metaInfDirectory = new File(getCompiler().getOutputLocation(), "META-INF"); + metaInfDirectory.mkdirs(); + File additionalMetadataFile = new File(metaInfDirectory, "additional-spring-configuration-metadata.json"); additionalMetadataFile.createNewFile(); JSONObject property = new JSONObject(); property.put("name", "foo"); @@ -273,9 +273,9 @@ private void writePropertyDeprecation(ItemMetadata... items) throws Exception { } private File createAdditionalMetadataFile() throws IOException { - File metaInfFolder = new File(getCompiler().getOutputLocation(), "META-INF"); - metaInfFolder.mkdirs(); - File additionalMetadataFile = new File(metaInfFolder, "additional-spring-configuration-metadata.json"); + File metaInfDirectory = new File(getCompiler().getOutputLocation(), "META-INF"); + metaInfDirectory.mkdirs(); + File additionalMetadataFile = new File(metaInfDirectory, "additional-spring-configuration-metadata.json"); additionalMetadataFile.createNewFile(); return additionalMetadataFile; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironmentFactory.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironmentFactory.java index c8cf16531ebe..6fbd0e42a2de 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironmentFactory.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironmentFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,9 @@ package org.springframework.boot.configurationprocessor; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; import java.util.function.Function; import javax.annotation.processing.ProcessingEnvironment; @@ -31,14 +34,20 @@ class MetadataGenerationEnvironmentFactory implements Function endpointAnnotations = new HashSet<>( + Arrays.asList(TestConfigurationMetadataAnnotationProcessor.CONTROLLER_ENDPOINT_ANNOTATION, + TestConfigurationMetadataAnnotationProcessor.ENDPOINT_ANNOTATION, + TestConfigurationMetadataAnnotationProcessor.REST_CONTROLLER_ENDPOINT_ANNOTATION, + TestConfigurationMetadataAnnotationProcessor.SERVLET_ENDPOINT_ANNOTATION, + TestConfigurationMetadataAnnotationProcessor.WEB_ENDPOINT_ANNOTATION)); return new MetadataGenerationEnvironment(environment, TestConfigurationMetadataAnnotationProcessor.CONFIGURATION_PROPERTIES_ANNOTATION, TestConfigurationMetadataAnnotationProcessor.NESTED_CONFIGURATION_PROPERTY_ANNOTATION, TestConfigurationMetadataAnnotationProcessor.DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION, TestConfigurationMetadataAnnotationProcessor.CONSTRUCTOR_BINDING_ANNOTATION, - TestConfigurationMetadataAnnotationProcessor.DEFAULT_VALUE_ANNOTATION, - TestConfigurationMetadataAnnotationProcessor.ENDPOINT_ANNOTATION, - TestConfigurationMetadataAnnotationProcessor.READ_OPERATION_ANNOTATION); + TestConfigurationMetadataAnnotationProcessor.DEFAULT_VALUE_ANNOTATION, endpointAnnotations, + TestConfigurationMetadataAnnotationProcessor.READ_OPERATION_ANNOTATION, + TestConfigurationMetadataAnnotationProcessor.NAME_ANNOTATION); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolverTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolverTests.java index 50ccb916c0cd..6e3bb6312c63 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolverTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,7 @@ import org.springframework.boot.configurationprocessor.test.TestableAnnotationProcessor; import org.springframework.boot.configurationsample.immutable.ImmutableClassConstructorBindingProperties; import org.springframework.boot.configurationsample.immutable.ImmutableMultiConstructorProperties; +import org.springframework.boot.configurationsample.immutable.ImmutableNameAnnotationProperties; import org.springframework.boot.configurationsample.immutable.ImmutableSimpleProperties; import org.springframework.boot.configurationsample.lombok.LombokExplicitProperties; import org.springframework.boot.configurationsample.lombok.LombokSimpleDataProperties; @@ -75,6 +76,10 @@ void propertiesWithJavaBeanHierarchicalProperties() throws IOException { PropertyDescriptorResolver resolver = new PropertyDescriptorResolver(metadataEnv); assertThat(resolver.resolve(type, null).map(PropertyDescriptor::getName)).containsExactly("third", "second", "first"); + assertThat(resolver.resolve(type, null).map( + (descriptor) -> descriptor.getGetter().getEnclosingElement().getSimpleName().toString())) + .containsExactly("HierarchicalProperties", "HierarchicalPropertiesParent", + "HierarchicalPropertiesParent"); assertThat(resolver.resolve(type, null) .map((descriptor) -> descriptor.resolveItemMetadata("test", metadataEnv)) .map(ItemMetadata::getDefaultValue)).containsExactly("three", "two", "one"); @@ -144,6 +149,12 @@ void propertiesWithMultiConstructorNoDirective() throws IOException { properties((stream) -> assertThat(stream).element(0).isInstanceOf(JavaBeanPropertyDescriptor.class))); } + @Test + void propertiesWithNameAnnotationParameter() throws IOException { + process(ImmutableNameAnnotationProperties.class, + propertyNames((stream) -> assertThat(stream).containsExactly("import"))); + } + private BiConsumer properties( Consumer>> stream) { return (element, metadataEnv) -> { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/TestProject.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/TestProject.java index e96b14f3a8c5..c8442e974ce3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/TestProject.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/TestProject.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -56,19 +56,21 @@ public class TestProject { * Contains copies of the original source so we can modify it safely to test * incremental builds. */ - private File sourceFolder; + private File sourceDirectory; private TestCompiler compiler; private Set sourceFiles = new LinkedHashSet<>(); - public TestProject(File tempFolder, Class... classes) throws IOException { - this.sourceFolder = new File(tempFolder, "src"); - this.compiler = new TestCompiler(new File(tempFolder, "build")) { + public TestProject(File tempDirectory, Class... classes) throws IOException { + this.sourceDirectory = new File(tempDirectory, "src"); + this.compiler = new TestCompiler(new File(tempDirectory, "build")) { + @Override - protected File getSourceFolder() { - return TestProject.this.sourceFolder; + protected File getSourceDirectory() { + return TestProject.this.sourceDirectory; } + }; Set> contents = new HashSet<>(Arrays.asList(classes)); contents.addAll(Arrays.asList(ALWAYS_INCLUDE)); @@ -90,14 +92,14 @@ private void copySources(Class type) throws IOException { } public File getSourceFile(Class type) { - return new File(this.sourceFolder, TestCompiler.sourcePathFor(type)); + return new File(this.sourceDirectory, TestCompiler.sourcePathFor(type)); } public ConfigurationMetadata fullBuild() { TestConfigurationMetadataAnnotationProcessor processor = new TestConfigurationMetadataAnnotationProcessor( this.compiler.getOutputLocation()); TestCompilationTask task = this.compiler.getTask(this.sourceFiles); - deleteFolderContents(this.compiler.getOutputLocation()); + deleteDirectoryContents(this.compiler.getOutputLocation()); task.call(processor); return processor.getMetadata(); } @@ -110,13 +112,13 @@ public ConfigurationMetadata incrementalBuild(Class... toRecompile) { return processor.getMetadata(); } - private void deleteFolderContents(File outputFolder) { - FileSystemUtils.deleteRecursively(outputFolder); - outputFolder.mkdirs(); + private void deleteDirectoryContents(File outputDirectory) { + FileSystemUtils.deleteRecursively(outputDirectory); + outputDirectory.mkdirs(); } /** - * Retrieve File relative to project's output folder. + * Retrieve File relative to project's output directory. * @param relativePath the relative path * @return the output file */ @@ -183,7 +185,7 @@ public void replaceText(Class type, String find, String replace) throws Excep * code. */ private File getOriginalSourceFile(Class type) { - return new File(TestCompiler.SOURCE_FOLDER, TestCompiler.sourcePathFor(type)); + return new File(TestCompiler.SOURCE_DIRECTORY, TestCompiler.sourcePathFor(type)); } private static void putContents(File targetFile, String contents) throws IOException { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/fieldvalues/AbstractFieldValuesProcessorTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/fieldvalues/AbstractFieldValuesProcessorTests.java index 29ba8dccd6fb..883d2542b39d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/fieldvalues/AbstractFieldValuesProcessorTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/fieldvalues/AbstractFieldValuesProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -94,12 +94,19 @@ void getFieldValues() throws Exception { assertThat(values.get("durationMinutes")).isEqualTo("30m"); assertThat(values.get("durationHours")).isEqualTo("40h"); assertThat(values.get("durationDays")).isEqualTo("50d"); + assertThat(values.get("durationZero")).isEqualTo(0); assertThat(values.get("dataSizeNone")).isNull(); assertThat(values.get("dataSizeBytes")).isEqualTo("5B"); assertThat(values.get("dataSizeKilobytes")).isEqualTo("10KB"); assertThat(values.get("dataSizeMegabytes")).isEqualTo("20MB"); assertThat(values.get("dataSizeGigabytes")).isEqualTo("30GB"); assertThat(values.get("dataSizeTerabytes")).isEqualTo("40TB"); + assertThat(values.get("periodNone")).isNull(); + assertThat(values.get("periodDays")).isEqualTo("3d"); + assertThat(values.get("periodWeeks")).isEqualTo("2w"); + assertThat(values.get("periodMonths")).isEqualTo("10m"); + assertThat(values.get("periodYears")).isEqualTo("15y"); + assertThat(values.get("periodZero")).isEqualTo(0); } @SupportedAnnotationTypes({ "org.springframework.boot.configurationsample.ConfigurationProperties" }) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/JavaCompilerFieldValuesProcessorTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/JavaCompilerFieldValuesProcessorTests.java index 259bf3f62372..2965ce4a2e24 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/JavaCompilerFieldValuesProcessorTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/JavaCompilerFieldValuesProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ * * @author Phillip Webb */ -public class JavaCompilerFieldValuesProcessorTests extends AbstractFieldValuesProcessorTests { +class JavaCompilerFieldValuesProcessorTests extends AbstractFieldValuesProcessorTests { @Override protected FieldValuesParser createProcessor(ProcessingEnvironment env) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshallerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshallerTests.java index 4f3aa76175bc..1f9c8d241e60 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshallerTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshallerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -82,7 +82,7 @@ void marshallOrderItems() throws IOException { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); JsonMarshaller marshaller = new JsonMarshaller(); marshaller.write(metadata, outputStream); - String json = new String(outputStream.toByteArray()); + String json = outputStream.toString(); assertThat(json).containsSubsequence("\"groups\"", "\"com.acme.alpha\"", "\"com.acme.bravo\"", "\"properties\"", "\"com.example.alpha.ccc\"", "\"com.example.alpha.ddd\"", "\"com.example.bravo.aaa\"", "\"com.example.bravo.bbb\"", "\"hints\"", "\"eee\"", "\"fff\""); @@ -100,9 +100,71 @@ void marshallPutDeprecatedItemsAtTheEnd() throws IOException { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); JsonMarshaller marshaller = new JsonMarshaller(); marshaller.write(metadata, outputStream); - String json = new String(outputStream.toByteArray()); + String json = outputStream.toString(); assertThat(json).containsSubsequence("\"properties\"", "\"com.example.alpha.ddd\"", "\"com.example.bravo.bbb\"", "\"com.example.alpha.ccc\"", "\"com.example.bravo.aaa\""); } + @Test + void orderingForSameGroupNames() throws IOException { + ConfigurationMetadata metadata = new ConfigurationMetadata(); + metadata.add(ItemMetadata.newGroup("com.acme.alpha", null, "com.example.Foo", null)); + metadata.add(ItemMetadata.newGroup("com.acme.alpha", null, "com.example.Bar", null)); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + JsonMarshaller marshaller = new JsonMarshaller(); + marshaller.write(metadata, outputStream); + String json = outputStream.toString(); + assertThat(json).containsSubsequence("\"groups\"", "\"name\": \"com.acme.alpha\"", + "\"sourceType\": \"com.example.Bar\"", "\"name\": \"com.acme.alpha\"", + "\"sourceType\": \"com.example.Foo\""); + } + + @Test + void orderingForSamePropertyNames() throws IOException { + ConfigurationMetadata metadata = new ConfigurationMetadata(); + metadata.add(ItemMetadata.newProperty("com.example.bravo", "aaa", "java.lang.Boolean", "com.example.Foo", null, + null, null, null)); + metadata.add(ItemMetadata.newProperty("com.example.bravo", "aaa", "java.lang.Integer", "com.example.Bar", null, + null, null, null)); + metadata.add( + ItemMetadata.newProperty("com.example.alpha", "ddd", null, "com.example.Bar", null, null, null, null)); + metadata.add( + ItemMetadata.newProperty("com.example.alpha", "ccc", null, "com.example.Foo", null, null, null, null)); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + JsonMarshaller marshaller = new JsonMarshaller(); + marshaller.write(metadata, outputStream); + String json = outputStream.toString(); + assertThat(json).containsSubsequence("\"groups\"", "\"properties\"", "\"com.example.alpha.ccc\"", + "com.example.Foo", "\"com.example.alpha.ddd\"", "com.example.Bar", "\"com.example.bravo.aaa\"", + "com.example.Bar", "\"com.example.bravo.aaa\"", "com.example.Foo"); + } + + @Test + void orderingForSameGroupWithNullSourceType() throws IOException { + ConfigurationMetadata metadata = new ConfigurationMetadata(); + metadata.add(ItemMetadata.newGroup("com.acme.alpha", null, "com.example.Foo", null)); + metadata.add(ItemMetadata.newGroup("com.acme.alpha", null, null, null)); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + JsonMarshaller marshaller = new JsonMarshaller(); + marshaller.write(metadata, outputStream); + String json = outputStream.toString(); + assertThat(json).containsSubsequence("\"groups\"", "\"name\": \"com.acme.alpha\"", + "\"name\": \"com.acme.alpha\"", "\"sourceType\": \"com.example.Foo\""); + } + + @Test + void orderingForSamePropertyNamesWithNullSourceType() throws IOException { + ConfigurationMetadata metadata = new ConfigurationMetadata(); + metadata.add(ItemMetadata.newProperty("com.example.bravo", "aaa", "java.lang.Boolean", null, null, null, null, + null)); + metadata.add(ItemMetadata.newProperty("com.example.bravo", "aaa", "java.lang.Integer", "com.example.Bar", null, + null, null, null)); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + JsonMarshaller marshaller = new JsonMarshaller(); + marshaller.write(metadata, outputStream); + String json = outputStream.toString(); + assertThat(json).containsSubsequence("\"groups\"", "\"properties\"", "\"com.example.bravo.aaa\"", + "\"java.lang.Boolean\"", "\"com.example.bravo.aaa\"", "\"java.lang.Integer\"", "\"com.example.Bar"); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/Metadata.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/Metadata.java index 69e3d914cfb8..05c52deb66ae 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/Metadata.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/Metadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import org.assertj.core.api.Condition; import org.hamcrest.collection.IsMapContaining; @@ -131,7 +132,7 @@ private String createDescription() { @Override public boolean matches(ConfigurationMetadata value) { - ItemMetadata itemMetadata = getFirstItemWithName(value, this.name); + ItemMetadata itemMetadata = findItem(value, this.name); if (itemMetadata == null) { return false; } @@ -207,13 +208,14 @@ public MetadataItemCondition withNoDeprecation() { this.description, this.defaultValue, null); } - private ItemMetadata getFirstItemWithName(ConfigurationMetadata metadata, String name) { - for (ItemMetadata item : metadata.getItems()) { - if (item.isOfItemType(this.itemType) && name.equals(item.getName())) { - return item; - } + private ItemMetadata findItem(ConfigurationMetadata metadata, String name) { + List candidates = metadata.getItems().stream() + .filter((item) -> item.isOfItemType(this.itemType) && name.equals(item.getName())) + .collect(Collectors.toList()); + if (candidates.size() > 1) { + throw new IllegalStateException("More that one metadata item with name '" + name + "': " + candidates); } - return null; + return (candidates.size() == 1) ? candidates.get(0) : null; } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/test/TestConfigurationMetadataAnnotationProcessor.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/test/TestConfigurationMetadataAnnotationProcessor.java index aeed80fdd7f9..592a5a5f45c1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/test/TestConfigurationMetadataAnnotationProcessor.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/test/TestConfigurationMetadataAnnotationProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,9 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; import javax.annotation.processing.SupportedAnnotationTypes; import javax.annotation.processing.SupportedSourceVersion; @@ -37,7 +40,14 @@ * @author Andy Wilkinson * @author Kris De Volder */ -@SupportedAnnotationTypes({ "*" }) +@SupportedAnnotationTypes({ TestConfigurationMetadataAnnotationProcessor.CONFIGURATION_PROPERTIES_ANNOTATION, + TestConfigurationMetadataAnnotationProcessor.CONTROLLER_ENDPOINT_ANNOTATION, + TestConfigurationMetadataAnnotationProcessor.ENDPOINT_ANNOTATION, + TestConfigurationMetadataAnnotationProcessor.JMX_ENDPOINT_ANNOTATION, + TestConfigurationMetadataAnnotationProcessor.REST_CONTROLLER_ENDPOINT_ANNOTATION, + TestConfigurationMetadataAnnotationProcessor.SERVLET_ENDPOINT_ANNOTATION, + TestConfigurationMetadataAnnotationProcessor.WEB_ENDPOINT_ANNOTATION, + "org.springframework.context.annotation.Configuration" }) @SupportedSourceVersion(SourceVersion.RELEASE_6) public class TestConfigurationMetadataAnnotationProcessor extends ConfigurationMetadataAnnotationProcessor { @@ -51,10 +61,22 @@ public class TestConfigurationMetadataAnnotationProcessor extends ConfigurationM public static final String DEFAULT_VALUE_ANNOTATION = "org.springframework.boot.configurationsample.DefaultValue"; + public static final String CONTROLLER_ENDPOINT_ANNOTATION = "org.springframework.boot.configurationsample.ControllerEndpoint"; + public static final String ENDPOINT_ANNOTATION = "org.springframework.boot.configurationsample.Endpoint"; + public static final String JMX_ENDPOINT_ANNOTATION = "org.springframework.boot.configurationsample.JmxEndpoint"; + + public static final String REST_CONTROLLER_ENDPOINT_ANNOTATION = "org.springframework.boot.configurationsample.RestControllerEndpoint"; + + public static final String SERVLET_ENDPOINT_ANNOTATION = "org.springframework.boot.configurationsample.ServletEndpoint"; + + public static final String WEB_ENDPOINT_ANNOTATION = "org.springframework.boot.configurationsample.WebEndpoint"; + public static final String READ_OPERATION_ANNOTATION = "org.springframework.boot.configurationsample.ReadOperation"; + public static final String NAME_ANNOTATION = "org.springframework.boot.configurationsample.Name"; + private ConfigurationMetadata metadata; private final File outputLocation; @@ -89,8 +111,9 @@ protected String defaultValueAnnotation() { } @Override - protected String endpointAnnotation() { - return ENDPOINT_ANNOTATION; + protected Set endpointAnnotations() { + return new HashSet<>(Arrays.asList(CONTROLLER_ENDPOINT_ANNOTATION, ENDPOINT_ANNOTATION, JMX_ENDPOINT_ANNOTATION, + REST_CONTROLLER_ENDPOINT_ANNOTATION, SERVLET_ENDPOINT_ANNOTATION, WEB_ENDPOINT_ANNOTATION)); } @Override @@ -98,6 +121,11 @@ protected String readOperationAnnotation() { return READ_OPERATION_ANNOTATION; } + @Override + protected String nameAnnotation() { + return NAME_ANNOTATION; + } + @Override protected ConfigurationMetadata writeMetaData() throws Exception { super.writeMetaData(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/ControllerEndpoint.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/ControllerEndpoint.java new file mode 100644 index 000000000000..5f055fbe02a6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/ControllerEndpoint.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.configurationsample; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Alternative to Spring Boot's {@code @ControllerEndpoint} for testing (removes the need + * for a dependency on the real annotation). + * + * @author Andy Wilkinson + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ControllerEndpoint { + + String id() default ""; + + boolean enableByDefault() default true; + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/DefaultValue.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/DefaultValue.java index 32604a46210c..359f8d4c07b4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/DefaultValue.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/DefaultValue.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,6 @@ @Documented public @interface DefaultValue { - String[] value(); + String[] value() default {}; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/JmxEndpoint.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/JmxEndpoint.java new file mode 100644 index 000000000000..a1e62e6321a1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/JmxEndpoint.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.configurationsample; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Alternative to Spring Boot's {@code @JmxEndpoint} for testing (removes the need for a + * dependency on the real annotation). + * + * @author Andy Wilkinson + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface JmxEndpoint { + + String id() default ""; + + boolean enableByDefault() default true; + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/MetaEndpoint.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/MetaEndpoint.java deleted file mode 100644 index c2d2fea316a7..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/MetaEndpoint.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.configurationsample; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import org.springframework.core.annotation.AliasFor; - -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -@Documented -@Endpoint -public @interface MetaEndpoint { - - @AliasFor(annotation = Endpoint.class) - String id(); - - @AliasFor(annotation = Endpoint.class) - boolean enableByDefault() default true; - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/Name.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/Name.java new file mode 100644 index 000000000000..965f8f4c0fb5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/Name.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.configurationsample; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Alternative to Spring Boot's {@code @Name} for testing (removes the need for a + * dependency on the real annotation). + * + * @author Phillip Webb + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Name { + + String value(); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/RestControllerEndpoint.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/RestControllerEndpoint.java new file mode 100644 index 000000000000..283b90e567c9 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/RestControllerEndpoint.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.configurationsample; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Alternative to Spring Boot's {@code @RestControllerEndpoint} for testing (removes the + * need for a dependency on the real annotation). + * + * @author Andy Wilkinson + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RestControllerEndpoint { + + String id() default ""; + + boolean enableByDefault() default true; + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/ServletEndpoint.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/ServletEndpoint.java new file mode 100644 index 000000000000..fecf8b08a30f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/ServletEndpoint.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.configurationsample; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Alternative to Spring Boot's {@code @ServletEndpoint} for testing (removes the need for + * a dependency on the real annotation). + * + * @author Andy Wilkinson + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ServletEndpoint { + + String id() default ""; + + boolean enableByDefault() default true; + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/WebEndpoint.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/WebEndpoint.java new file mode 100644 index 000000000000..48b3ee40914f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/WebEndpoint.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.configurationsample; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Alternative to Spring Boot's {@code @WebEndpoint} for testing (removes the need for a + * dependency on the real annotation). + * + * @author Andy Wilkinson + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface WebEndpoint { + + String id() default ""; + + boolean enableByDefault() default true; + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/endpoint/SpecificEndpoint.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/endpoint/SpecificEndpoint.java index d5a3cb38384f..49a003a71afc 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/endpoint/SpecificEndpoint.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/endpoint/SpecificEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,17 +16,17 @@ package org.springframework.boot.configurationsample.endpoint; -import org.springframework.boot.configurationsample.MetaEndpoint; import org.springframework.boot.configurationsample.ReadOperation; +import org.springframework.boot.configurationsample.WebEndpoint; import org.springframework.lang.Nullable; /** - * A meta-annotated endpoint similar to {@code @WebEndpoint} or {@code @JmxEndpoint} in - * Spring Boot. Also with a package private read operation that has an optional argument. + * A meta-annotated endpoint. Also with a package private read operation that has an + * optional argument. * * @author Stephane Nicoll */ -@MetaEndpoint(id = "specific", enableByDefault = true) +@WebEndpoint(id = "specific", enableByDefault = true) public class SpecificEndpoint { @ReadOperation diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/endpoint/incremental/IncrementalSpecificEndpoint.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/endpoint/incremental/IncrementalSpecificEndpoint.java index 5c6d25cc15b1..4f682d4b8c63 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/endpoint/incremental/IncrementalSpecificEndpoint.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/endpoint/incremental/IncrementalSpecificEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,15 +16,15 @@ package org.springframework.boot.configurationsample.endpoint.incremental; -import org.springframework.boot.configurationsample.MetaEndpoint; +import org.springframework.boot.configurationsample.JmxEndpoint; /** - * A meta-annotated endpoint similar to {@code @WebEndpoint} or {@code @JmxEndpoint} in - * Spring Boot. + * A meta-annotated endpoint. * * @author Stephane Nicoll + * @author Andy Wilkinson */ -@MetaEndpoint(id = "incremental") +@JmxEndpoint(id = "incremental") public class IncrementalSpecificEndpoint { } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/fieldvalues/FieldValues.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/fieldvalues/FieldValues.java index 7dbdc96de1f6..ee8578c8a25f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/fieldvalues/FieldValues.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/fieldvalues/FieldValues.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.time.Period; import org.springframework.boot.configurationsample.ConfigurationProperties; import org.springframework.util.MimeType; @@ -108,7 +109,7 @@ public class FieldValues { private Integer[] integerArray = new Integer[] { 42, 24 }; - private FieldValues[] unknownArray = new FieldValues[] { new FieldValues() }; + private UnknownElementType[] unknownArray = new UnknownElementType[] { new UnknownElementType() }; private Duration durationNone; @@ -124,6 +125,8 @@ public class FieldValues { private Duration durationDays = Duration.ofDays(50); + private Duration durationZero = Duration.ZERO; + private DataSize dataSizeNone; private DataSize dataSizeBytes = DataSize.ofBytes(5); @@ -136,4 +139,16 @@ public class FieldValues { private DataSize dataSizeTerabytes = DataSize.ofTerabytes(40); + private Period periodNone; + + private Period periodDays = Period.ofDays(3); + + private Period periodWeeks = Period.ofWeeks(2); + + private Period periodMonths = Period.ofMonths(10); + + private Period periodYears = Period.ofYears(15); + + private Period periodZero = Period.ZERO; + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/fieldvalues/UnknownElementType.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/fieldvalues/UnknownElementType.java new file mode 100644 index 000000000000..06b7d2b0ff19 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/fieldvalues/UnknownElementType.java @@ -0,0 +1,26 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.configurationsample.fieldvalues; + +/** + * Type used to check unknown array element types. + * + * @author Phillip Webb + */ +public class UnknownElementType { + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/generic/ComplexGenericProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/generic/ComplexGenericProperties.java index 3e1d358c98ee..40e3b6ce2158 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/generic/ComplexGenericProperties.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/generic/ComplexGenericProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ public class ComplexGenericProperties { @NestedConfigurationProperty - private UpperBoundGenericPojo test = new UpperBoundGenericPojo<>(); + private final UpperBoundGenericPojo test = new UpperBoundGenericPojo<>(); public UpperBoundGenericPojo getTest() { return this.test; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/generic/ConcreteBuilderProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/generic/ConcreteBuilderProperties.java new file mode 100644 index 000000000000..94e78ee0b142 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/generic/ConcreteBuilderProperties.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.configurationsample.generic; + +import org.springframework.boot.configurationsample.ConfigurationProperties; + +/** + * Builder pattern with a resolved generic + * + * @author Stephane Nicoll + */ +@ConfigurationProperties("builder") +public class ConcreteBuilderProperties extends GenericBuilderProperties { + + private String description; + + public String getDescription() { + return this.description; + } + + public ConcreteBuilderProperties setDescription(String description) { + this.description = description; + return this; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/generic/GenericBuilderProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/generic/GenericBuilderProperties.java new file mode 100644 index 000000000000..8bf042ef5543 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/generic/GenericBuilderProperties.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.configurationsample.generic; + +/** + * A configuration properties that uses the builder pattern with a generic. + * + * @param the type of the return type + * @author Stephane Nicoll + */ +public class GenericBuilderProperties> { + + private int number; + + public int getNumber() { + return this.number; + } + + @SuppressWarnings("unchecked") + public T setNumber(int number) { + this.number = number; + return (T) this; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/DeducedImmutableClassProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/DeducedImmutableClassProperties.java index 5a5d799cd780..dbef1d34efad 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/DeducedImmutableClassProperties.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/DeducedImmutableClassProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import org.springframework.boot.configurationsample.ConfigurationProperties; import org.springframework.boot.configurationsample.ConstructorBinding; +import org.springframework.boot.configurationsample.DefaultValue; /** * Inner properties, in immutable format. @@ -30,7 +31,7 @@ public class DeducedImmutableClassProperties { private final Nested nested; - public DeducedImmutableClassProperties(Nested nested) { + public DeducedImmutableClassProperties(@DefaultValue Nested nested) { this.nested = nested; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/ImmutableNameAnnotationProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/ImmutableNameAnnotationProperties.java new file mode 100644 index 000000000000..0e4d05198311 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/ImmutableNameAnnotationProperties.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.configurationsample.immutable; + +import org.springframework.boot.configurationsample.ConfigurationProperties; +import org.springframework.boot.configurationsample.ConstructorBinding; +import org.springframework.boot.configurationsample.Name; + +/** + * Immutable properties making use of {@code @Name}. + * + * @author Phillip Webb + */ +@ConfigurationProperties("named") +@ConstructorBinding +public class ImmutableNameAnnotationProperties { + + private final String imports; + + public ImmutableNameAnnotationProperties(@Name("import") String imports) { + this.imports = imports; + } + + public String getImports() { + return this.imports; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/method/SingleConstructorMethodConfig.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/method/SingleConstructorMethodConfig.java index e2d23226c541..7bea9f880b30 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/method/SingleConstructorMethodConfig.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/method/SingleConstructorMethodConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ * * @author Stephane Nicoll */ +@SuppressWarnings("unused") public class SingleConstructorMethodConfig { @ConfigurationProperties(prefix = "foo") diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/recursive/RecursiveProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/recursive/RecursiveProperties.java index 2d2d63c97e44..245672960359 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/recursive/RecursiveProperties.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/recursive/RecursiveProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2018 the original author or authors. + * Copyright 2012-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/HierarchicalPropertiesParent.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/HierarchicalPropertiesParent.java index 364ba5611951..b436470f051d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/HierarchicalPropertiesParent.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/HierarchicalPropertiesParent.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,8 @@ public void setSecond(String second) { this.second = second; } - // Useless override + // Overridden properties should belong to this class, not + // HierarchicalPropertiesGrandparent @Override public String getFirst() { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/BoxingPojo.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/BoxingPojo.java index 00ab84ff1be9..5484bb941cbe 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/BoxingPojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/BoxingPojo.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,8 @@ public class BoxingPojo { private boolean flag; + private Boolean anotherFlag; + private Integer counter; public boolean isFlag() { @@ -40,6 +42,14 @@ public void setFlag(Boolean flag) { this.flag = flag; } + public boolean isAnotherFlag() { + return Boolean.TRUE.equals(this.anotherFlag); + } + + public void setAnotherFlag(boolean anotherFlag) { + this.anotherFlag = anotherFlag; + } + public Integer getCounter() { return this.counter; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/DeprecatedLessPreciseTypePojo.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/DeprecatedLessPreciseTypePojo.java new file mode 100644 index 000000000000..ff1e2421ef21 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/DeprecatedLessPreciseTypePojo.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.configurationsample.specific; + +import org.springframework.boot.configurationsample.ConfigurationProperties; + +/** + * Demonstrate that deprecating accessor with not the same type is not taken into account + * to detect the deprecated flag. + * + * @author Stephane Nicoll + */ +@ConfigurationProperties("not.deprecated") +public class DeprecatedLessPreciseTypePojo { + + private boolean flag; + + @Deprecated + public Boolean getFlag() { + return this.flag; + } + + public boolean isFlag() { + return this.flag; + } + + public void setFlag(boolean flag) { + this.flag = flag; + } + + @Deprecated + public void setFlag(Boolean flag) { + this.flag = flag; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/EmptyDefaultValueProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/EmptyDefaultValueProperties.java new file mode 100644 index 000000000000..fc55bd950969 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/EmptyDefaultValueProperties.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.configurationsample.specific; + +import org.springframework.boot.configurationsample.ConfigurationProperties; +import org.springframework.boot.configurationsample.ConstructorBinding; +import org.springframework.boot.configurationsample.DefaultValue; + +/** + * Demonstrates that an empty default value on a property leads to no default value. + * + * @author Stephane Nicoll + */ +@ConfigurationProperties("test") +public class EmptyDefaultValueProperties { + + private final String name; + + @ConstructorBinding + public EmptyDefaultValueProperties(@DefaultValue String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/TwoConstructorsClassConstructorBindingExample.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/TwoConstructorsClassConstructorBindingExample.java index 6738f15d8145..7e755ef9c9ea 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/TwoConstructorsClassConstructorBindingExample.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/TwoConstructorsClassConstructorBindingExample.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ * @author Stephane Nicoll */ @MetaConstructorBinding +@SuppressWarnings("unused") public class TwoConstructorsClassConstructorBindingExample { private String name; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/build.gradle index 85454c1f9db8..23032f6e90f9 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/build.gradle @@ -1,71 +1,119 @@ -buildscript { - repositories { - mavenLocal() - mavenCentral() +plugins { + id "java-gradle-plugin" + id "maven-publish" + id "org.asciidoctor.jvm.convert" + id "org.springframework.boot.conventions" + id "org.springframework.boot.maven-repository" + id "org.springframework.boot.optional-dependencies" +} + +description = "Spring Boot Gradle Plugin" + +configurations { + documentation +} + +dependencies { + asciidoctorExtensions("io.spring.asciidoctor:spring-asciidoctor-extensions-section-ids") + + implementation(project(":spring-boot-project:spring-boot-tools:spring-boot-buildpack-platform")) + implementation(project(":spring-boot-project:spring-boot-tools:spring-boot-loader-tools")) + implementation("io.spring.gradle:dependency-management-plugin") + implementation("org.apache.commons:commons-compress") + implementation("org.springframework:spring-core") + + optional("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") { + exclude(group: "commons-logging", module: "commons-logging") + } + + testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) + testImplementation("com.tngtech.archunit:archunit-junit5:0.22.0") + testImplementation("org.assertj:assertj-core") + testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.mockito:mockito-core") + testImplementation("org.testcontainers:junit-jupiter") + testImplementation("org.testcontainers:testcontainers") +} + +gradlePlugin { + plugins { + springBootPlugin { + id = "org.springframework.boot" + displayName = "Spring Boot Gradle Plugin" + description = "Spring Boot Gradle Plugin" + implementationClass = "org.springframework.boot.gradle.plugin.SpringBootPlugin" + } } - dependencies { - classpath("io.spring.javaformat:spring-javaformat-gradle-plugin:0.0.15") +} + +task preparePluginValidationClasses(type: Copy) { + destinationDir = file("$buildDir/classes/java/pluginValidation") + from(sourceSets.main.output.classesDirs) { + exclude "**/CreateBootStartScripts.class" } } -plugins { - id 'java' - id 'eclipse' +validatePlugins { + classes.setFrom preparePluginValidationClasses + enableStricterValidation = true } -apply plugin: 'io.spring.javaformat' +task dependencyVersions(type: org.springframework.boot.build.constraints.ExtractVersionConstraints) { + enforcedPlatform(":spring-boot-project:spring-boot-dependencies") +} -repositories { - mavenLocal() - mavenCentral() +tasks.withType(org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask) { + dependsOn dependencyVersions + inputs.dir('src/docs/gradle').withPathSensitivity(PathSensitivity.RELATIVE).withPropertyName('buildScripts') + doFirst { + attributes "dependency-management-plugin-version": dependencyVersions.versionConstraints["io.spring.gradle:dependency-management-plugin"] + } } -dependencies { - implementation localGroovy() - implementation gradleApi() - implementation fileTree(dir: 'target/dependencies/compile', include: '*.jar') - testImplementation gradleTestKit() - testImplementation 'org.apache.commons:commons-compress:1.13' - testImplementation fileTree(dir: 'target/dependencies/test', include: '*.jar') +tasks.named('test') { + inputs.dir('src/docs/gradle').withPathSensitivity(PathSensitivity.RELATIVE).withPropertyName('buildScripts') } -jar { - manifest { - attributes 'Implementation-Version': (version ? version : 'unknown') +asciidoctor { + sources { + include "index.adoc" } } -test { - useJUnitPlatform() - testLogging { - events "passed", "skipped", "failed" +task asciidoctorPdf(type: org.asciidoctor.gradle.jvm.AsciidoctorTask) { + sources { + include "index.adoc" } } javadoc { options { - author() - stylesheetFile = file('src/main/javadoc/spring-javadoc.css') - links = [ - 'https://docs.oracle.com/javase/8/docs/api/', - 'https://docs.gradle.org/current/javadoc/' - ] - source = '8' + author = true + docTitle = "Spring Boot Gradle Plugin ${project.version} API" + encoding = "UTF-8" + memberLevel = "protected" + outputLevel = "quiet" + splitIndex = true + use = true + windowTitle = "Spring Boot Gradle Plugin ${project.version} API" } - title = "${project.description} $version API" -} - -task sourcesJar(type: Jar) { - classifier = 'sources' - from sourceSets.main.allSource } -task javadocJar(type: Jar) { - classifier = "javadoc" - from javadoc +task zip(type: Zip) { + dependsOn asciidoctor, asciidoctorPdf + duplicatesStrategy "fail" + from(asciidoctorPdf.outputDir) { + into "reference/pdf" + rename "index.pdf", "${project.name}-reference.pdf" + } + from(asciidoctor.outputDir) { + into "reference/htmlsingle" + } + from(javadoc) { + into "api" + } } artifacts { - archives sourcesJar - archives javadocJar + "documentation" zip } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/gradle.properties b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/gradle.properties deleted file mode 100644 index 6b1823d86a69..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.daemon=false diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/gradle/wrapper/gradle-wrapper.jar b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 135367700529..000000000000 Binary files a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/gradlew b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/gradlew deleted file mode 100755 index cccdd3d517fc..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/gradlew +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env sh - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - -exec "$JAVACMD" "$@" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/gradlew.bat b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/gradlew.bat deleted file mode 100644 index f9553162f122..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/gradlew.bat +++ /dev/null @@ -1,84 +0,0 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/pom.xml deleted file mode 100644 index 94a48efafa53..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/pom.xml +++ /dev/null @@ -1,399 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-tools - ${revision} - - spring-boot-gradle-plugin - pom - Spring Boot Gradle Plugin - Spring Boot Gradle Plugin - - ${basedir}/../../.. - ./gradlew - build - ${project.build.directory}/refdocs/ - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - org.springframework.boot - spring-boot-loader-tools - - - io.spring.gradle - dependency-management-plugin - - - org.apache.commons - commons-compress - - - org.jetbrains.kotlin - kotlin-gradle-plugin - ${kotlin.version} - true - - - - - - com.googlecode.maven-download-plugin - download-maven-plugin - - - unpack-doc-resources - generate-resources - - wget - - - ${spring-doc-resources.url} - true - ${refdocs.build.directory}/asciidoc - - - - - - org.apache.maven.plugins - maven-dependency-plugin - - - copy-compile-dependencies - generate-resources - - copy-dependencies - - false - - compile - ${project.build.directory}/dependencies/compile - - - - copy-test-dependencies - generate-resources - - copy-dependencies - - false - - test - ${project.build.directory}/dependencies/test - - - - - - org.apache.maven.plugins - maven-resources-plugin - - - copy-asciidoc-resources - generate-resources - - copy-resources - - - ${refdocs.build.directory}/asciidoc - - - src/main/asciidoc - false - - - - - - copy-gradle-resources - generate-resources - - copy-resources - - - ${refdocs.build.directory}/gradle - - - src/main/gradle - false - - - - - - - - org.codehaus.mojo - exec-maven-plugin - - - gradle - prepare-package - - ${gradle.executable} - - clean - ${gradle.task} - -Pversion=${project.version} - -Pdescription=${project.description} - -S - - - - exec - - - - - - org.codehaus.mojo - build-helper-maven-plugin - - - attach-artifacts - package - - attach-artifact - - - - - build/libs/${project.artifactId}-${project.version}.jar - jar - - - build/libs/${project.artifactId}-${project.version}-javadoc.jar - jar - javadoc - - - build/libs/${project.artifactId}-${project.version}-sources.jar - jar - sources - - - - - - - - - - - windows - - - windows - - - - gradlew.bat - - - - skipTests - - - skipTests - true - - - - assemble - - - - full - - - full - - - - - - org.apache.maven.plugins - maven-antrun-plugin - - - ant-contrib - ant-contrib - 1.0b3 - - - ant - ant - - - - - org.apache.ant - ant-nodeps - 1.8.1 - - - org.tigris.antelope - antelopetasks - 3.2.10 - - - - - set-up-maven-properties - prepare-package - - run - - - true - - - - - - - - - - - - - - - package-docs-zip - package - - run - - - - - - - - - - - - - - - - - - org.asciidoctor - asciidoctor-maven-plugin - - - generate-html-documentation - prepare-package - - process-asciidoc - - - html5 - ${project.build.directory}/generated-docs/reference/html - highlight.js - book - - js/highlight - github - true - ./images - font - css/ - style.css - warn - - - true - - DEBUG - - - - - - generate-pdf-documentation - prepare-package - - process-asciidoc - - - pdf - ${project.build.directory}/generated-docs/reference/pdf - - - - - ${refdocs.build.directory}/asciidoc - index.adoc - - ${github-tag} - ${version-type} - ${project.version} - ${dependency-management-plugin.version} - - - - - io.spring.asciidoctor - spring-asciidoctor-extensions-block-switch - ${spring-asciidoctor-extensions.version} - - - - - org.codehaus.mojo - build-helper-maven-plugin - - - attach-zip - - attach-artifact - - - - - ${project.build.directory}/${project.artifactId}-${project.version}-docs.zip - zip - docs - - - - - - - - - - - java13 - - 13 - - - assemble - - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/anchor-rewrite.properties b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/anchor-rewrite.properties new file mode 100644 index 000000000000..777eb3fcf8a2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/anchor-rewrite.properties @@ -0,0 +1,48 @@ +integrating-with-actuator=integrating-with-actuator +integrating-with-actuator-build-info=integrating-with-actuator.build-info +managing-dependencies=managing-dependencies +managing-dependencies-dependency-management-plugin=managing-dependencies.dependency-management-plugin +managing-dependencies-dependency-management-plugin-customizing=managing-dependencies.dependency-management-plugin.customizing +managing-dependencies-dependency-management-plugin-using-in-isolation=managing-dependencies.dependency-management-plugin.using-in-isolation +managing-dependencies-dependency-management-plugin-learning-more=managing-dependencies.dependency-management-plugin.learning-more +managing-dependencies-gradle-bom-support=managing-dependencies.gradle-bom-support +managing-dependencies-gradle-bom-support-customizing=managing-dependencies.gradle-bom-support.customizing +build-image=build-image +build-image-docker-daemon=build-image.docker-daemon +build-image-docker-registry=build-image.docker-registry +build-image-customization=build-image.customization +build-image-examples=build-image.examples +build-image-example-custom-image-builder=build-image.examples.custom-image-builder +build-image-example-builder-configuration=build-image.examples.builder-configuration +build-image-example-runtime-jvm-configuration=build-image.examples.runtime-jvm-configuration +build-image-example-custom-image-name=build-image.examples.custom-image-name +build-image-example-buildpacks=build-image.examples.buildpacks +build-image-example-publish=build-image.examples.publish +build-image-example-docker=build-image.examples.docker +packaging-executable=packaging-executable +packaging-executable-jars=packaging-executable.jars +packaging-executable-wars=packaging-executable.wars +packaging-executable-wars-deployable=packaging-executable.wars.deployable +packaging-executable-and-plain=packaging-executable.and-plain-archives +packaging-executable-configuring=packaging-executable.configuring +packaging-executable-configuring-main-class=packaging-executable.configuring.main-class +packaging-executable-configuring-including-development-only-dependencies=packaging-executable.configuring.including-development-only-dependencies +packaging-executable-configuring-unpacking=packaging-executable.configuring.unpacking +packaging-executable-configuring-launch-script=packaging-executable.configuring.launch-script +packaging-executable-configuring-properties-launcher=packaging-executable.configuring.properties-launcher +packaging-layered-archives=packaging-executable.configuring.layered-archives +packaging-layers-configuration=packaging-executable.configuring.layered-archives.configuration +publishing-your-application=publishing-your-application +publishing-your-application-maven-publish=publishing-your-application.maven-publish +publishing-your-application-maven=publishing-your-application.maven +publishing-your-application-distribution=publishing-your-application.distribution +reacting-to-other-plugins=reacting-to-other-plugins +reacting-to-other-plugins-java=reacting-to-other-plugins.java +reacting-to-other-plugins-kotlin=reacting-to-other-plugins.kotlin +reacting-to-other-plugins-war=reacting-to-other-plugins.war +reacting-to-other-plugins-dependency-management=reacting-to-other-plugins.dependency-management +reacting-to-other-plugins-application=reacting-to-other-plugins.application +running-your-application=running-your-application +running-your-application-passing-arguments=running-your-application.passing-arguments +running-your-application-passing-system-properties=running-your-application.passing-system-properties +running-your-application-reloading-resources=running-your-application.reloading-resources diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/getting-started.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/getting-started.adoc new file mode 100644 index 000000000000..2c5d3dd5df42 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/getting-started.adoc @@ -0,0 +1,100 @@ +[[getting-started]] += Getting Started +To get started with the plugin it needs to be applied to your project. + +ifeval::["{spring-boot-artifactory-repo}" == "release"] +The plugin is https://plugins.gradle.org/plugin/org.springframework.boot[published to Gradle's plugin portal] and can be applied using the `plugins` block: +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/getting-started/apply-plugin-release.gradle[] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/getting-started/apply-plugin-release.gradle.kts[] +---- +endif::[] +ifeval::["{spring-boot-artifactory-repo}" == "milestone"] +The plugin is published to the Spring milestones repository. +Gradle can be configured to use the milestones repository and the plugin can then be applied using the `plugins` block. +To configure Gradle to use the milestones repository, add the following to your `settings.gradle` (Groovy) or `settings.gradle.kts` (Kotlin): + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/getting-started/milestone-settings.gradle[] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/getting-started/milestone-settings.gradle.kts[] +---- + +The plugin can then be applied using the `plugins` block: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/getting-started/apply-plugin-release.gradle[] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/getting-started/apply-plugin-release.gradle.kts[] +---- +endif::[] +ifeval::["{spring-boot-artifactory-repo}" == "snapshot"] +The plugin is published to the Spring snapshots repository. +Gradle can be configured to use the snapshots repository and the plugin can then be applied using the `plugins` block. +To configure Gradle to use the snapshots repository, add the following to your `settings.gradle` (Groovy) or `settings.gradle.kts` (Kotlin): + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/getting-started/snapshot-settings.gradle[] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/getting-started/snapshot-settings.gradle.kts[] +---- + +The plugin can then be applied using the `plugins` block: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/getting-started/apply-plugin-release.gradle[] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/getting-started/apply-plugin-release.gradle.kts[] +---- +endif::[] + +Applied in isolation the plugin makes few changes to a project. +Instead, the plugin detects when certain other plugins are applied and reacts accordingly. +For example, when the `java` plugin is applied a task for building an executable jar is automatically configured. +A typical Spring Boot project will apply the {groovy-plugin}[`groovy`], {java-plugin}[`java`], or {kotlin-plugin}[`org.jetbrains.kotlin.jvm`] plugin as a minimum and also use the {dependency-management-plugin}[`io.spring.dependency-management`] plugin or Gradle's native bom support for dependency management. +For example: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/getting-started/typical-plugins.gradle[tags=apply] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/getting-started/typical-plugins.gradle.kts[tags=apply] +---- + +To learn more about how the Spring Boot plugin behaves when other plugins are applied please see the section on <>. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/index.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/index.adoc new file mode 100644 index 000000000000..0558da915d18 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/index.adoc @@ -0,0 +1,61 @@ +[[spring-boot-gradle-plugin-documentation]] += Spring Boot Gradle Plugin Reference Guide +Andy Wilkinson; Scott Frederick +v{gradle-project-version} +:!version-label: +:doctype: book +:toc: left +:toclevels: 4 +:numbered: +:sectanchors: +:icons: font +:hide-uri-scheme: +:docinfo: shared,private +:attribute-missing: warn +:dependency-management-plugin: https://github.com/spring-gradle-plugins/dependency-management-plugin +:dependency-management-plugin-documentation: https://docs.spring.io/dependency-management-plugin/docs/current/reference/html/ +:gradle-userguide: https://docs.gradle.org/current/userguide +:gradle-dsl: https://docs.gradle.org/current/dsl +:gradle-api: https://docs.gradle.org/current/javadoc +:application-plugin: {gradle-userguide}/application_plugin.html +:groovy-plugin: {gradle-userguide}/groovy_plugin.html +:java-plugin: {gradle-userguide}/java_plugin.html +:war-plugin: {gradle-userguide}/war_plugin.html +:maven-plugin: {gradle-userguide}/maven_plugin.html +:maven-publish-plugin: {gradle-userguide}/maven_publish_plugin.html +:software-component: {gradle-userguide}/software_model_extend.html +:kotlin-plugin: https://kotlinlang.org/docs/reference/using-gradle.html +:spring-boot-docs: https://docs.spring.io/spring-boot/docs/{gradle-project-version} +:api-documentation: {spring-boot-docs}/gradle-plugin/api +:spring-boot-reference: {spring-boot-docs}/reference/htmlsingle +:spring-boot-api: {spring-boot-docs}/api/org/springframework/boot +:version-properties-appendix: {spring-boot-reference}/#dependency-versions-properties +:build-info-javadoc: {api-documentation}/org/springframework/boot/gradle/tasks/buildinfo/BuildInfo.html +:boot-build-image-javadoc: {api-documentation}/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.html +:boot-jar-javadoc: {api-documentation}/org/springframework/boot/gradle/tasks/bundling/BootJar.html +:boot-war-javadoc: {api-documentation}/org/springframework/boot/gradle/tasks/bundling/BootWar.html +:boot-run-javadoc: {api-documentation}/org/springframework/boot/gradle/tasks/run/BootRun.html +:github-code: https://github.com/spring-projects/spring-boot/tree/{github-tag} +:buildpacks-reference: https://buildpacks.io/docs +:paketo-reference: https://paketo.io/docs +:paketo-java-reference: {paketo-reference}/buildpacks/language-family-buildpacks/java + + + +include::introduction.adoc[leveloffset=+1] + +include::getting-started.adoc[leveloffset=+1] + +include::managing-dependencies.adoc[leveloffset=+1] + +include::packaging.adoc[leveloffset=+1] + +include::packaging-oci-image.adoc[leveloffset=+1] + +include::publishing.adoc[leveloffset=+1] + +include::running.adoc[leveloffset=+1] + +include::integrating-with-actuator.adoc[leveloffset=+1] + +include::reacting.adoc[leveloffset=+1] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/asciidoc/integrating-with-actuator.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/integrating-with-actuator.adoc similarity index 96% rename from spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/asciidoc/integrating-with-actuator.adoc rename to spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/integrating-with-actuator.adoc index 3d78937edc5b..928448551e8a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/asciidoc/integrating-with-actuator.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/integrating-with-actuator.adoc @@ -1,10 +1,10 @@ [[integrating-with-actuator]] -== Integrating with Actuator += Integrating with Actuator -[[integrating-with-actuator-build-info]] -=== Generating build information +[[integrating-with-actuator.build-info]] +== Generating Build Information Spring Boot Actuator's `info` endpoint automatically publishes information about your build in the presence of a `META-INF/build-info.properties` file. A {build-info-javadoc}[`BuildInfo`] task is provided to generate this file. The easiest way to use the task is via the plugin's DSL: @@ -21,7 +21,6 @@ include::../gradle/integrating-with-actuator/build-info-basic.gradle[tags=build- include::../gradle/integrating-with-actuator/build-info-basic.gradle.kts[tags=build-info] ---- - This will configure a {build-info-javadoc}[`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`). @@ -62,7 +61,6 @@ include::../gradle/integrating-with-actuator/build-info-custom-values.gradle[tag include::../gradle/integrating-with-actuator/build-info-custom-values.gradle.kts[tags=custom-values] ---- - The default value for `build.time` is the instant at which the project is being built. A side-effect of this is that the task will never be up-to-date. As a result, builds will take longer as more tasks, including the project's tests, will have to be executed. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/introduction.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/introduction.adoc new file mode 100644 index 000000000000..6b3e2ee3b30e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/introduction.adoc @@ -0,0 +1,7 @@ +[[introduction]] += Introduction +The Spring Boot Gradle Plugin provides Spring Boot support in https://gradle.org[Gradle]. +It allows you to package executable jar or war archives, run Spring Boot applications, and use the dependency management provided by `spring-boot-dependencies`. +Spring Boot's Gradle plugin requires Gradle 6.8, 6.9, or 7.x and can be used with Gradle's {gradle-userguide}/configuration_cache.html[configuration cache]. + +In addition to this user guide, {api-documentation}[API documentation] is also available. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/managing-dependencies.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/managing-dependencies.adoc new file mode 100644 index 000000000000..68d42db0001d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/managing-dependencies.adoc @@ -0,0 +1,180 @@ +[[managing-dependencies]] += Managing Dependencies +To manage dependencies in your Spring Boot application, you can either apply the {dependency-management-plugin}[`io.spring.dependency-management`] plugin or use Gradle's native bom support. +The primary benefit of the former is that it offers property-based customization of managed versions, while using the latter will likely result in faster builds. + + + +[[managing-dependencies.dependency-management-plugin]] +== Managing Dependencies with the Dependency Management Plugin +When you apply the {dependency-management-plugin}[`io.spring.dependency-management`] plugin, Spring Boot's plugin will automatically <> from the version of Spring Boot that you are using. +This provides a similar dependency management experience to the one that's enjoyed by Maven users. +For example, it allows you to omit version numbers when declaring dependencies that are managed in the bom. +To make use of this functionality, declare dependencies in the usual way but omit the version number: + +[source,groovy,indent=0,subs="verbatim",role="primary"] +.Groovy +---- +include::../gradle/managing-dependencies/dependencies.gradle[tags=dependencies] +---- + +[source,kotlin,indent=0,subs="verbatim",role="secondary"] +.Kotlin +---- +include::../gradle/managing-dependencies/dependencies.gradle.kts[tags=dependencies] +---- + + + +[[managing-dependencies.dependency-management-plugin.customizing]] +=== Customizing Managed Versions +The `spring-boot-dependencies` bom that is automatically imported when the dependency management plugin is applied uses properties to control the versions of the dependencies that it manages. +Browse the {version-properties-appendix}[`Dependency versions Appendix`] in the Spring Boot reference for a complete list of these properties. + +To customize a managed version you set its corresponding property. +For example, to customize the version of SLF4J which is controlled by the `slf4j.version` property: + +[source,groovy,indent=0,subs="verbatim",role="primary"] +.Groovy +---- +include::../gradle/managing-dependencies/custom-version.gradle[tags=custom-version] +---- + +[source,kotlin,indent=0,subs="verbatim",role="secondary"] +.Kotlin +---- +include::../gradle/managing-dependencies/custom-version.gradle.kts[tags=custom-version] +---- + +WARNING: Each Spring Boot release is designed and tested against a specific set of third-party dependencies. +Overriding versions may cause compatibility issues and should be done with care. + + + +[[managing-dependencies.dependency-management-plugin.using-in-isolation]] +=== Using Spring Boot's Dependency Management in Isolation +Spring Boot's dependency management can be used in a project without applying Spring Boot's plugin to that project. +The `SpringBootPlugin` class provides a `BOM_COORDINATES` constant that can be used to import the bom without having to know its group ID, artifact ID, or version. + +First, configure the project to depend on the Spring Boot plugin but do not apply it: + +ifeval::["{spring-boot-artifactory-repo}" == "release"] +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/managing-dependencies/depend-on-plugin-release.gradle[] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/managing-dependencies/depend-on-plugin-release.gradle.kts[] +---- +endif::[] +ifeval::["{spring-boot-artifactory-repo}" == "milestone"] +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/managing-dependencies/depend-on-plugin-milestone.gradle[] +---- +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/managing-dependencies/depend-on-plugin-release.gradle.kts[] +---- +endif::[] +ifeval::["{spring-boot-artifactory-repo}" == "snapshot"] +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/managing-dependencies/depend-on-plugin-snapshot.gradle[] +---- +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/managing-dependencies/depend-on-plugin-release.gradle.kts[] +---- +endif::[] + +The Spring Boot plugin's dependency on the dependency management plugin means that you can use the dependency management plugin without having to declare a dependency on it. +This also means that you will automatically use the same version of the dependency management plugin as Spring Boot uses. + +Apply the dependency management plugin and then configure it to import Spring Boot's bom: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/managing-dependencies/configure-bom.gradle[tags=configure-bom] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/managing-dependencies/configure-bom.gradle.kts[tags=configure-bom] +---- + +The Kotlin code above is a bit awkward. +That's because we're using the imperative way of applying the dependency management plugin. + +We can make the code less awkward by applying the plugin from the root parent project, or by using the `plugins` block as we're doing for the Spring Boot plugin. +A downside of this method is that it forces us to specify the version of the dependency management plugin: + +[source,kotlin,indent=0,subs="verbatim,attributes"] +---- +include::../gradle/managing-dependencies/configure-bom-with-plugins.gradle.kts[tags=configure-bom] +---- + + + +[[managing-dependencies.dependency-management-plugin.learning-more]] +=== Learning More +To learn more about the capabilities of the dependency management plugin, please refer to its {dependency-management-plugin-documentation}[documentation]. + + + +[[managing-dependencies.gradle-bom-support]] +== Managing Dependencies with Gradle's Bom Support +Gradle allows a bom to be used to manage a project's versions by declaring it as a `platform` or `enforcedPlatform` dependency. +A `platform` dependency treats the versions in the bom as recommendations and other versions and constraints in the dependency graph may cause a version of a dependency other than that declared in the bom to be used. +An `enforcedPlatform` dependency treats the versions in the bom as requirements and they will override any other version found in the dependency graph. + +The `SpringBootPlugin` class provides a `BOM_COORDINATES` constant that can be used to declare a dependency upon Spring Boot's bom without having to know its group ID, artifact ID, or version, as shown in the following example: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/managing-dependencies/configure-platform.gradle[tags=configure-platform] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/managing-dependencies/configure-platform.gradle.kts[tags=configure-platform] +---- + +A platform or enforced platform will only constrain the versions of the configuration in which it has been declared or that extend from the configuration in which it has been declared. +As a result, in may be necessary to declare the same dependency in more than one configuration. + + + +[[managing-dependencies.gradle-bom-support.customizing]] +=== Customizing Managed Versions +When using Gradle's bom support, you cannot use the properties from `spring-boot-dependencies` to control the versions of the dependencies that it manages. +Instead, you must use one of the mechanisms that Gradle provides. +One such mechanism is a resolution strategy. +SLF4J's modules are all in the `org.slf4j` group so their version can be controlled by configuring every dependency in that group to use a particular version, as shown in the following example: + +[source,groovy,indent=0,subs="verbatim",role="primary"] +.Groovy +---- +include::../gradle/managing-dependencies/custom-version-with-platform.gradle[tags=custom-version] +---- + +[source,kotlin,indent=0,subs="verbatim",role="secondary"] +.Kotlin +---- +include::../gradle/managing-dependencies/custom-version-with-platform.gradle.kts[tags=custom-version] +---- + +WARNING: Each Spring Boot release is designed and tested against a specific set of third-party dependencies. +Overriding versions may cause compatibility issues and should be done with care. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc new file mode 100644 index 000000000000..ce2ca57a4b04 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc @@ -0,0 +1,417 @@ +[[build-image]] += Packaging OCI Images +The plugin can create an https://github.com/opencontainers/image-spec[OCI image] from a jar or war file using https://buildpacks.io[Cloud Native Buildpacks] (CNB). +Images can be built using the `bootBuildImage` task. + +NOTE: For security reasons, images build and run as non-root users. +See the {buildpacks-reference}/reference/spec/platform-api/#users[CNB specification] for more details. + +The task is automatically created when the `java` or `war` plugin is applied and is an instance of {boot-build-image-javadoc}[`BootBuildImage`]. + +NOTE: The `bootBuildImage` task can not be used with a <> that includes a launch script. +Disable launch script configuration in the `bootJar` task when building a jar file that is intended to be used with `bootBuildImage`. + + + +[[build-image.docker-daemon]] +== Docker Daemon +The `bootBuildImage` task requires access to a Docker daemon. +By default, it will communicate with a Docker daemon over a local connection. +This works with https://docs.docker.com/install/[Docker Engine] on all supported platforms without configuration. + +Environment variables can be set to configure the `bootBuildImage` task to use the https://minikube.sigs.k8s.io/docs/tasks/docker_daemon/[Docker daemon provided by minikube]. +The following table shows the environment variables and their values: + +|=== +| Environment variable | Description + +| DOCKER_HOST +| URL containing the host and port for the Docker daemon - e.g. `tcp://192.168.99.100:2376` + +| DOCKER_TLS_VERIFY +| Enable secure HTTPS protocol when set to `1` (optional) + +| DOCKER_CERT_PATH +| Path to certificate and key files for HTTPS (required if `DOCKER_TLS_VERIFY=1`, ignored otherwise) +|=== + +On Linux and macOS, these environment variables can be set using the command `eval $(minikube docker-env)` after minikube has been started. + +Docker daemon connection information can also be provided using `docker` properties in the plugin configuration. +The following table summarizes the available properties: + +|=== +| Property | Description + +| `host` +| URL containing the host and port for the Docker daemon - e.g. `tcp://192.168.99.100:2376` + +| `tlsVerify` +| Enable secure HTTPS protocol when set to `true` (optional) + +| `certPath` +| Path to certificate and key files for HTTPS (required if `tlsVerify` is `true`, ignored otherwise) +|=== + +For more details, see also <>. + + + +[[build-image.docker-registry]] +== Docker Registry +If the Docker images specified by the `builder` or `runImage` properties are stored in a private Docker image registry that requires authentication, the authentication credentials can be provided using `docker.builderRegistry` properties. + +If the generated Docker image is to be published to a Docker image registry, the authentication credentials can be provided using `docker.publishRegistry` properties. + +Properties are provided for user authentication or identity token authentication. +Consult the documentation for the Docker registry being used to store images for further information on supported authentication methods. + +The following table summarizes the available properties for `docker.builderRegistry` and `docker.publishRegistry`: + +|=== +| Property | Description + +| `username` +| Username for the Docker image registry user. Required for user authentication. + +| `password` +| Password for the Docker image registry user. Required for user authentication. + +| `url` +| Address of the Docker image registry. Optional for user authentication. + +| `email` +| E-mail address for the Docker image registry user. Optional for user authentication. + +| `token` +| Identity token for the Docker image registry user. Required for token authentication. +|=== + +For more details, see also <>. + + + +[[build-image.customization]] +== Image Customizations +The plugin invokes a {buildpacks-reference}/concepts/components/builder/[builder] to orchestrate the generation of an image. +The builder includes multiple {buildpacks-reference}/concepts/components/buildpack[buildpacks] that can inspect the application to influence the generated image. +By default, the plugin chooses a builder image. +The name of the generated image is deduced from project properties. + +Task properties can be used to configure how the builder should operate on the project. +The following table summarizes the available properties and their default values: + +|=== +| Property | Command-line option | Description | Default value + +| `builder` +| `--builder` +| Name of the Builder image to use. +| `paketobuildpacks/builder:base` + +| `runImage` +| `--runImage` +| Name of the run image to use. +| No default value, indicating the run image specified in Builder metadata should be used. + +| `imageName` +| `--imageName` +| {spring-boot-api}/buildpack/platform/docker/type/ImageReference.html#of-java.lang.String-[Image name] for the generated image. +| `docker.io/library/${project.name}:${project.version}` + +| `pullPolicy` +| `--pullPolicy` +| {spring-boot-api}/buildpack/platform/build/PullPolicy.html[Policy] used to determine when to pull the builder and run images from the registry. +Acceptable values are `ALWAYS`, `NEVER`, and `IF_NOT_PRESENT`. +| `ALWAYS` + +| `environment` +| +| Environment variables that should be passed to the builder. +| + +| `buildpacks` +| +a|Buildpacks that the builder should use when building the image. +Only the specified buildpacks will be used, overriding the default buildpacks included in the builder. +Buildpack references must be in one of the following forms: + +* Buildpack in the builder - `[urn:cnb:builder:][@]` +* Buildpack in a directory on the file system - `[file://]` +* Buildpack in a gzipped tar (.tgz) file on the file system - `[file://]/` +* Buildpack in an OCI image - `[docker://]/[:][@]` +| None, indicating the builder should use the buildpacks included in it. + +| `bindings` +| +a|https://docs.docker.com/storage/bind-mounts/[Volume bind mounts] that should be mounted to the builder container when building the image. +The bindings will be passed unparsed and unvalidated to Docker when creating the builder container. +Bindings must be in one of the following forms: + +* `:[:]` +* `:[:]` + +Where `` can contain: + +* `ro` to mount the volume as read-only in the container +* `rw` to mount the volume as readable and writable in the container +* `volume-opt=key=value` to specify key-value pairs consisting of an option name and its value +| + +| `cleanCache` +| `--cleanCache` +| Whether to clean the cache before building. +| `false` + +| `verboseLogging` +| +| Enables verbose logging of builder operations. +| `false` + +| `publish` +| `--publishImage` +| Whether to publish the generated image to a Docker registry. +| `false` +|=== + +NOTE: The plugin detects the target Java compatibility of the project using the JavaPlugin's `targetCompatibility` property. +When using the default Paketo builder and buildpacks, the plugin instructs the buildpacks to install the same Java version. +You can override this behaviour as shown in the <> examples. + + + +[[build-image.examples]] +== Examples + + + +[[build-image.examples.custom-image-builder]] +=== Custom Image Builder and Run Image +If you need to customize the builder used to create the image or the run image used to launch the built image, configure the task as shown in the following example: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/packaging/boot-build-image-builder.gradle[tags=builder] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/packaging/boot-build-image-builder.gradle.kts[tags=builder] +---- + +This configuration will use a builder image with the name `mine/java-cnb-builder` and the tag `latest`, and the run image named `mine/java-cnb-run` and the tag `latest`. + +The builder and run image can be specified on the command line as well, as shown in this example: + +[indent=0] +---- + $ gradle bootBuildImage --builder=mine/java-cnb-builder --runImage=mine/java-cnb-run +---- + + + +[[build-image.examples.builder-configuration]] +=== Builder Configuration +If the builder exposes configuration options, those can be set using the `environment` property. + +The following is an example of {paketo-java-reference}/#configuring-the-jvm-version[configuring the JVM version] used by the Paketo Java buildpacks at build time: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/packaging/boot-build-image-env.gradle[tags=env] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/packaging/boot-build-image-env.gradle.kts[tags=env] +---- + +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: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/packaging/boot-build-image-env-proxy.gradle[tags=env] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/packaging/boot-build-image-env-proxy.gradle.kts[tags=env] +---- + + + +[[build-image.examples.runtime-jvm-configuration]] +=== Runtime JVM Configuration +Paketo Java buildpacks {paketo-java-reference}/#runtime-jvm-configuration[configure the JVM runtime environment] by setting the `JAVA_TOOL_OPTIONS` environment variable. +The buildpack-provided `JAVA_TOOL_OPTIONS` value can be modified to customize JVM runtime behavior when the application image is launched in a container. + +Environment variable modifications that should be stored in the image and applied to every deployment can be set as described in the {paketo-reference}/buildpacks/configuration/#environment-variables[Paketo documentation] and shown in the following example: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/packaging/boot-build-image-env-runtime.gradle[tags=env-runtime] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/packaging/boot-build-image-env-runtime.gradle.kts[tags=env-runtime] +---- + + + +[[build-image.examples.custom-image-name]] +=== Custom Image Name +By default, the image name is inferred from the `name` and the `version` of the project, something like `docker.io/library/${project.name}:${project.version}`. +You can take control over the name by setting task properties, as shown in the following example: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/packaging/boot-build-image-name.gradle[tags=image-name] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/packaging/boot-build-image-name.gradle.kts[tags=image-name] +---- + +Note that this configuration does not provide an explicit tag so `latest` is used. +It is possible to specify a tag as well, either using `${project.version}`, any property available in the build or a hardcoded version. + +The image name can be specified on the command line as well, as shown in this example: + +[indent=0] +---- + $ gradle bootBuildImage --imageName=example.com/library/my-app:v1 +---- + + + +[[build-image.examples.buildpacks]] +=== Buildpacks +By default, the builder will use buildpacks included in the builder image and apply them in a pre-defined order. +An alternative set of buildpacks can be provided to apply buildpacks that are not included in the builder, or to change the order of included buildpacks. +When one or more buildpacks are provided, only the specified buildpacks will be applied. + +The following example instructs the builder to use a custom buildpack packaged in a `.tgz` file, followed by a buildpack included in the builder. + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/packaging/boot-build-image-buildpacks.gradle[tags=buildpacks] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/packaging/boot-build-image-buildpacks.gradle.kts[tags=buildpacks] +---- + +Buildpacks can be specified in any of the forms shown below. + +A buildpack located in a CNB Builder (version may be omitted if there is only one buildpack in the builder matching the `buildpack-id`): + +* `urn:cnb:builder:buildpack-id` +* `urn:cnb:builder:buildpack-id@0.0.1` +* `buildpack-id` +* `buildpack-id@0.0.1` + +A path to a directory containing buildpack content (not supported on Windows): + +* `\file:///path/to/buildpack/` +* `/path/to/buildpack/` + +A path to a gzipped tar file containing buildpack content: + +* `\file:///path/to/buildpack.tgz` +* `/path/to/buildpack.tgz` + +An OCI image containing a https://buildpacks.io/docs/buildpack-author-guide/package-a-buildpack/[packaged buildpack]: + +* `docker://example/buildpack` +* `docker:///example/buildpack:latest` +* `docker:///example/buildpack@sha256:45b23dee08...` +* `example/buildpack` +* `example/buildpack:latest` +* `example/buildpack@sha256:45b23dee08...` + + + +[[build-image.examples.publish]] +=== Image Publishing +The generated image can be published to a Docker registry by enabling a `publish` option and configuring authentication for the registry using `docker.publishRegistry` properties. + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/packaging/boot-build-image-publish.gradle[tags=publish] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/packaging/boot-build-image-publish.gradle.kts[tags=publish] +---- + +The publish option can be specified on the command line as well, as shown in this example: + +[indent=0] +---- + $ gradle bootBuildImage --imageName=docker.example.com/library/my-app:v1 --publishImage +---- + + + +[[build-image.examples.docker]] +=== Docker Configuration +If you need the plugin to communicate with the Docker daemon using a remote connection instead of the default local connection, the connection details can be provided using `docker` properties as shown in the following example: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/packaging/boot-build-image-docker-host.gradle[tags=docker-host] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/packaging/boot-build-image-docker-host.gradle.kts[tags=docker-host] +---- + +If the builder or run image are stored in a private Docker registry that supports user authentication, authentication details can be provided using `docker.buiderRegistry` properties as shown in the following example: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/packaging/boot-build-image-docker-auth-user.gradle[tags=docker-auth-user] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/packaging/boot-build-image-docker-auth-user.gradle.kts[tags=docker-auth-user] +---- + +If the builder or run image is stored in a private Docker registry that supports token authentication, the token value can be provided using `docker.builderRegistry` as shown in the following example: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/packaging/boot-build-image-docker-auth-token.gradle[tags=docker-auth-token] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/packaging/boot-build-image-docker-auth-token.gradle.kts[tags=docker-auth-token] +---- diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging.adoc new file mode 100644 index 000000000000..962a0edc04cd --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging.adoc @@ -0,0 +1,373 @@ +[[packaging-executable]] += Packaging Executable Archives +The plugin can create executable archives (jar files and war files) that contain all of an application's dependencies and can then be run with `java -jar`. + + + +[[packaging-executable.jars]] +== Packaging Executable Jars +Executable jars can be built using the `bootJar` task. +The task is automatically created when the `java` plugin is applied and is an instance of {boot-jar-javadoc}[`BootJar`]. +The `assemble` task is automatically configured to depend upon the `bootJar` task so running `assemble` (or `build`) will also run the `bootJar` task. + + + +[[packaging-executable.wars]] +== Packaging Executable Wars +Executable wars can be built using the `bootWar` task. +The task is automatically created when the `war` plugin is applied and is an instance of {boot-war-javadoc}[`BootWar`]. +The `assemble` task is automatically configured to depend upon the `bootWar` task so running `assemble` (or `build`) will also run the `bootWar` task. + + + +[[packaging-executable.wars.deployable]] +=== Packaging Executable and Deployable Wars +A war file can be packaged such that it can be executed using `java -jar` and deployed to an external container. +To do so, the embedded servlet container dependencies should be added to the `providedRuntime` configuration, for example: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/packaging/war-container-dependency.gradle[tags=dependencies] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/packaging/war-container-dependency.gradle.kts[tags=dependencies] +---- + +This ensures that they are package in the war file's `WEB-INF/lib-provided` directory from where they will not conflict with the external container's own classes. + +NOTE: `providedRuntime` is preferred to Gradle's `compileOnly` configuration as, among other limitations, `compileOnly` dependencies are not on the test classpath so any web-based integration tests will fail. + + + +[[packaging-executable.and-plain-archives]] +== Packaging Executable and Plain Archives +By default, when the `bootJar` or `bootWar` tasks are configured, the `jar` or `war` tasks are configured to use `plain` as the convention for their archive classifier. +This ensures that `bootJar` and `jar` or `bootWar` and `war` have different output locations, allowing both the executable archive and the plain archive to be built at the same time. + +If you prefer that the executable archive, rather than the plain archive, uses a classifier, configure the classifiers as shown in the following example for the `jar` and `bootJar` tasks: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/packaging/boot-jar-and-jar-classifiers.gradle[tags=classifiers] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/packaging/boot-jar-and-jar-classifiers.gradle.kts[tags=classifiers] +---- + +Alternatively, if you prefer that the plain archive isn't built at all, disable its task as shown in the following example for the `jar` task: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/packaging/only-boot-jar.gradle[tags=disable-jar] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/packaging/only-boot-jar.gradle.kts[tags=disable-jar] +---- + + + +[[packaging-executable.configuring]] +== Configuring Executable Archive Packaging +The {boot-jar-javadoc}[`BootJar`] and {boot-war-javadoc}[`BootWar`] tasks are subclasses of Gradle's `Jar` and `War` tasks respectively. +As a result, all of the standard configuration options that are available when packaging a jar or war are also available when packaging an executable jar or war. +A number of configuration options that are specific to executable jars and wars are also provided. + + + +[[packaging-executable.configuring.main-class]] +=== Configuring the Main Class +By default, the executable archive's main class will be configured automatically by looking for a class with a `public static void main(String[])` method in directories on the task's classpath. + +The main class can also be configured explicitly using the task's `mainClass` property: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/packaging/boot-jar-main-class.gradle[tags=main-class] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/packaging/boot-jar-main-class.gradle.kts[tags=main-class] +---- + +Alternatively, the main class name can be configured project-wide using the `mainClass` property of the Spring Boot DSL: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/packaging/spring-boot-dsl-main-class.gradle[tags=main-class] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/packaging/spring-boot-dsl-main-class.gradle.kts[tags=main-class] +---- + +If the {application-plugin}[`application` plugin] has been applied its `mainClass` property must be configured and can be used for the same purpose: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/packaging/application-plugin-main-class.gradle[tags=main-class] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/packaging/application-plugin-main-class.gradle.kts[tags=main-class] +---- + +Lastly, the `Start-Class` attribute can be configured on the task's manifest: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/packaging/boot-jar-manifest-main-class.gradle[tags=main-class] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/packaging/boot-jar-manifest-main-class.gradle.kts[tags=main-class] +---- + +NOTE: If the main class is written in Kotlin, the name of the generated Java class should be used. +By default, this is the name of the Kotlin class with the `Kt` suffix added. +For example, `ExampleApplication` becomes `ExampleApplicationKt`. +If another name is defined using `@JvmName` then that name should be used. + + + +[[packaging-executable.configuring.including-development-only-dependencies]] +=== Including Development-only Dependencies +By default all dependencies declared in the `developmentOnly` configuration will be excluded from an executable jar or war. + +If you want to include dependencies declared in the `developmentOnly` configuration in your archive, configure the classpath of its task to include the configuration, as shown in the following example for the `bootWar` task: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/packaging/boot-war-include-devtools.gradle[tags=include-devtools] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/packaging/boot-war-include-devtools.gradle.kts[tags=include-devtools] +---- + + + +[[packaging-executable.configuring.unpacking]] +=== Configuring Libraries that Require Unpacking +Most libraries can be used directly when nested in an executable archive, however certain libraries can have problems. +For example, JRuby includes its own nested jar support which assumes that `jruby-complete.jar` is always directly available on the file system. + +To deal with any problematic libraries, an executable archive can be configured to unpack specific nested jars to a temporary directory when the executable archive is run. +Libraries can be identified as requiring unpacking using Ant-style patterns that match against the absolute path of the source jar file: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/packaging/boot-jar-requires-unpack.gradle[tags=requires-unpack] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/packaging/boot-jar-requires-unpack.gradle.kts[tags=requires-unpack] +---- + +For more control a closure can also be used. +The closure is passed a `FileTreeElement` and should return a `boolean` indicating whether or not unpacking is required. + + + +[[packaging-executable.configuring.launch-script]] +=== Making an Archive Fully Executable +Spring Boot provides support for fully executable archives. +An archive is made fully executable by prepending a shell script that knows how to launch the application. +On Unix-like platforms, this launch script allows the archive to be run directly like any other executable or to be installed as a service. + +NOTE: Currently, some tools do not accept this format so you may not always be able to use this technique. +For example, `jar -xf` may silently fail to extract a jar or war that has been made fully-executable. +It is recommended that you only enable this option if you intend to execute it directly, rather than running it with `java -jar`, deploying it to a servlet container, or including it in an OCI image. + +To use this feature, the inclusion of the launch script must be enabled: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/packaging/boot-jar-include-launch-script.gradle[tags=include-launch-script] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/packaging/boot-jar-include-launch-script.gradle.kts[tags=include-launch-script] +---- + +This will add Spring Boot's default launch script to the archive. +The default launch script includes several properties with sensible default values. +The values can be customized using the `properties` property: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/packaging/boot-jar-launch-script-properties.gradle[tags=launch-script-properties] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/packaging/boot-jar-launch-script-properties.gradle.kts[tags=launch-script-properties] +---- + +If the default launch script does not meet your needs, the `script` property can be used to provide a custom launch script: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/packaging/boot-jar-custom-launch-script.gradle[tags=custom-launch-script] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/packaging/boot-jar-custom-launch-script.gradle.kts[tags=custom-launch-script] +---- + + + +[[packaging-executable.configuring.properties-launcher]] +=== Using the PropertiesLauncher +To use the `PropertiesLauncher` to launch an executable jar or war, configure the task's manifest to set the `Main-Class` attribute: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/packaging/boot-war-properties-launcher.gradle[tags=properties-launcher] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/packaging/boot-war-properties-launcher.gradle.kts[tags=properties-launcher] +---- + + + +[[packaging-executable.configuring.layered-archives]] +=== Packaging Layered Jar or War +By default, the `bootJar` task builds an archive that contains the application's classes and dependencies in `BOOT-INF/classes` and `BOOT-INF/lib` respectively. +Similarly, `bootWar` builds an archive that contains the application's classes in `WEB-INF/classes` and dependencies in `WEB-INF/lib` and `WEB-INF/lib-provided`. +For cases where a docker image needs to be built from the contents of the jar, it's useful to be able to separate these directories further so that they can be written into distinct layers. + +Layered jars use the same layout as regular boot packaged jars, but include an additional meta-data file that describes each layer. + +By default, the following layers are defined: + +* `dependencies` for any non-project dependency whose version does not contain `SNAPSHOT`. +* `spring-boot-loader` for the jar loader classes. +* `snapshot-dependencies` for any non-project dependency whose version contains `SNAPSHOT`. +* `application` for project dependencies, application classes, and resources. + +The layers order is important as it determines how likely previous layers can be cached when part of the application changes. +The default order is `dependencies`, `spring-boot-loader`, `snapshot-dependencies`, `application`. +Content that is least likely to change should be added first, followed by layers that are more likely to change. + +To disable this feature, you can do so in the following manner: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/packaging/boot-jar-layered-disabled.gradle[tags=layered] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/packaging/boot-jar-layered-disabled.gradle.kts[tags=layered] +---- + +When a layered jar or war is created, the `spring-boot-jarmode-layertools` jar will be added as a dependency to your archive. +With this jar on the classpath, you can launch your application in a special mode which allows the bootstrap code to run something entirely different from your application, for example, something that extracts the layers. +If you wish to exclude this dependency, you can do so in the following manner: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/packaging/boot-jar-layered-exclude-tools.gradle[tags=layered] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/packaging/boot-jar-layered-exclude-tools.gradle.kts[tags=layered] +---- + + + +[[packaging-executable.configuring.layered-archives.configuration]] +==== 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 configuration that describes how the jar or war 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: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/packaging/boot-jar-layered-custom.gradle[tags=layered] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/packaging/boot-jar-layered-custom.gradle.kts[tags=layered] +---- + +The `layered` DSL is defined using three parts: + +* The `application` closure defines how the application classes and resources should be layered. +* The `dependencies` closure defines how dependencies should be layered. +* The `layerOrder` method defines the order that the layers should be written. + +Nested `intoLayer` closures are used within `application` and `dependencies` sections to claim content for a layer. +These closures are evaluated in the order that they are defined, from top to bottom. +Any content not claimed by an earlier `intoLayer` closure remains available for subsequent ones to consider. + +The `intoLayer` closure claims content using nested `include` and `exclude` calls. +The `application` closure uses Ant-style path matching for include/exclude parameters. +The `dependencies` section uses `group:artifact[:version]` patterns. +It also provides `includeProjectDependencies()` and `excludeProjectDependencies()` methods that can be used to include or exclude project dependencies. + +If no `include` call is made, then all content (not claimed by an earlier closure) is considered. + +If no `exclude` call is made, then no exclusions are applied. + +Looking at the `dependencies` closure in the example above, we can see that the first `intoLayer` will claim all project dependencies for the `application` layer. +The next `intoLayer` will claim all SNAPSHOT dependencies for the `snapshot-dependencies` layer. +The third and final `intoLayer` will claim anything left (in this case, any dependency that is not a project dependency or a SNAPSHOT) for the `dependencies` layer. + +The `application` closure has similar rules. +First claiming `org/springframework/boot/loader/**` content for the `spring-boot-loader` layer. +Then claiming any remaining classes and resources for the `application` layer. + +NOTE: The order that `intoLayer` closures are added is often different from the order that the layers are written. +For this reason the `layerOrder` method must always be called and _must_ cover all layers referenced by the `intoLayer` calls. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/publishing.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/publishing.adoc new file mode 100644 index 000000000000..afcef18b121d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/publishing.adoc @@ -0,0 +1,54 @@ +[[publishing-your-application]] += Publishing your Application + + + +[[publishing-your-application.maven-publish]] +== Publishing with the Maven-publish Plugin +To publish your Spring Boot jar or war, add it to the publication using the `artifact` method on `MavenPublication`. +Pass the task that produces that artifact that you wish to publish to the `artifact` method. +For example, to publish the artifact produced by the default `bootJar` task: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/publishing/maven-publish.gradle[tags=publishing] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/publishing/maven-publish.gradle.kts[tags=publishing] +---- + + + +[[publishing-your-application.maven]] +== Publishing with the Maven Plugin +WARNING: Due to its deprecation in Gradle 6, this plugin's support for publishing with Gradle's `maven` plugin is deprecated and will be removed in a future release. +Please use the `maven-publish` plugin instead. + +When the {maven-plugin}[`maven` plugin] is applied, an `Upload` task for the `bootArchives` configuration named `uploadBootArchives` is automatically created. +By default, the `bootArchives` configuration contains the archive produced by the `bootJar` or `bootWar` task. +The `uploadBootArchives` task can be configured to publish the archive to a Maven repository: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/publishing/maven.gradle[tags=upload] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/publishing/maven.gradle.kts[tags=upload] +---- + + + +[[publishing-your-application.distribution]] +== Distributing with the Application Plugin +When the {application-plugin}[`application` plugin] is applied a distribution named `boot` is created. +This distribution contains the archive produced by the `bootJar` or `bootWar` task and scripts to launch it on Unix-like platforms and Windows. +Zip and tar distributions can be built by the `bootDistZip` and `bootDistTar` tasks respectively. +To use the `application` plugin, its `mainClassName` property must be configured with the name of your application's main class. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc new file mode 100644 index 000000000000..a9edeb4066ca --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc @@ -0,0 +1,73 @@ +[[reacting-to-other-plugins]] += Reacting to Other Plugins +When another plugin is applied the Spring Boot plugin reacts by making various changes to the project's configuration. +This section describes those changes. + + + +[[reacting-to-other-plugins.java]] +== Reacting to the Java Plugin +When Gradle's {java-plugin}[`java` plugin] is applied to a project, the Spring Boot plugin: + +1. Creates a {boot-jar-javadoc}[`BootJar`] task named `bootJar` that will create an executable, fat jar for the project. + The jar will contain everything on the runtime classpath of the main source set; classes are packaged in `BOOT-INF/classes` and jars are packaged in `BOOT-INF/lib` +2. Configures the `assemble` task to depend on the `bootJar` task. +3. Configures the `jar` task to use `plain` as the convention for its archive classifier. +4. Creates a {boot-build-image-javadoc}[`BootBuildImage`] task named `bootBuildImage` that will create a OCI image using a https://buildpacks.io[buildpack]. +5. Creates a {boot-run-javadoc}[`BootRun`] task named `bootRun` that can be used to run your application. +6. Creates a configuration named `bootArchives` that contains the artifact produced by the `bootJar` task. +7. Creates a configuration named `developmentOnly` for dependencies that are only required at development time, such as Spring Boot's Devtools, and should not be packaged in executable jars and wars. +8. Creates a configuration named `productionRuntimeClasspath`. It is equivalent to `runtimeClasspath` minus any dependencies that only appear in the `developmentOnly` configuration. +9. Configures any `JavaCompile` tasks with no configured encoding to use `UTF-8`. +10. Configures any `JavaCompile` tasks to use the `-parameters` compiler argument. + + + +[[reacting-to-other-plugins.kotlin]] +== Reacting to the Kotlin Plugin +When {kotlin-plugin}[Kotlin's Gradle plugin] is applied to a project, the Spring Boot plugin: + +1. Aligns the Kotlin version used in Spring Boot's dependency management with the version of the plugin. + This is achieved by setting the `kotlin.version` property with a value that matches the version of the Kotlin plugin. +2. Configures any `KotlinCompile` tasks to use the `-java-parameters` compiler argument. + + + +[[reacting-to-other-plugins.war]] +== Reacting to the War Plugin +When Gradle's {war-plugin}[`war` plugin] is applied to a project, the Spring Boot plugin: + +1. Creates a {boot-war-javadoc}[`BootWar`] task named `bootWar` that will create an executable, fat war for the project. + In addition to the standard packaging, everything in the `providedRuntime` configuration will be packaged in `WEB-INF/lib-provided`. +2. Configures the `assemble` task to depend on the `bootWar` task. +3. Configures the `war` task to use `plain` as the convention for its archive classifier. +4. Configures the `bootArchives` configuration to contain the artifact produced by the `bootWar` task. + + + +[[reacting-to-other-plugins.dependency-management]] +== Reacting to the Dependency Management Plugin +When the {dependency-management-plugin}[`io.spring.dependency-management` plugin] is applied to a project, the Spring Boot plugin will automatically import the `spring-boot-dependencies` bom. + + + +[[reacting-to-other-plugins.application]] +== Reacting to the Application Plugin +When Gradle's {application-plugin}[`application` plugin] is applied to a project, the Spring Boot plugin: + +1. Creates a `CreateStartScripts` task named `bootStartScripts` that will create scripts that launch the artifact in the `bootArchives` configuration using `java -jar`. + The task is configured to use the `applicationDefaultJvmArgs` property as a convention for its `defaultJvmOpts` property. +2. Creates a new distribution named `boot` and configures it to contain the artifact in the `bootArchives` configuration in its `lib` directory and the start scripts in its `bin` directory. +3. Configures the `bootRun` task to use the `mainClassName` property as a convention for its `main` property. +4. Configures the `bootRun` task to use the `applicationDefaultJvmArgs` property as a convention for its `jvmArgs` property. +5. Configures the `bootJar` task to use the `mainClassName` property as a convention for the `Start-Class` entry in its manifest. +6. Configures the `bootWar` task to use the `mainClassName` property as a convention for the `Start-Class` entry in its manifest. + + + +[[reacting-to-other-plugins.maven]] +== Reacting to the Maven plugin +WARNING: Support for reacting to Gradle's `maven` plugin is deprecated and will be removed in a future release. +Please use the `maven-publish` plugin instead. + +When Gradle's {maven-plugin}[`maven` plugin] is applied to a project, the Spring Boot plugin will configure the `uploadBootArchives` `Upload` task to ensure that no dependencies are declared in the pom that it generates. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/running.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/running.adoc new file mode 100644 index 000000000000..7853abde1015 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/running.adoc @@ -0,0 +1,143 @@ +[[running-your-application]] += Running your Application with Gradle +To run your application without first building an archive use the `bootRun` task: + +[source,bash,indent=0,subs="verbatim"] +---- + $ ./gradlew bootRun +---- + +The `bootRun` task is an instance of {boot-run-javadoc}[`BootRun`] which is a `JavaExec` subclass. +As such, all of the {gradle-dsl}/org.gradle.api.tasks.JavaExec.html[usual configuration options] for executing a Java process in Gradle are available to you. +The task is automatically configured to use the runtime classpath of the main source set. + +By default, the main class will be configured automatically by looking for a class with a `public static void main(String[])` method in directories on the task's classpath. + +The main class can also be configured explicitly using the task's `main` property: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/running/boot-run-main.gradle[tags=main] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/running/boot-run-main.gradle.kts[tags=main] +---- + +Alternatively, the main class name can be configured project-wide using the `mainClass` property of the Spring Boot DSL: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/running/spring-boot-dsl-main-class-name.gradle[tags=main-class] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/running/spring-boot-dsl-main-class-name.gradle.kts[tags=main-class] +---- + +By default, `bootRun` will configure the JVM to optimize its launch for faster startup during development. +This behavior can be disabled by using the `optimizedLaunch` property, as shown in the following example: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/running/boot-run-disable-optimized-launch.gradle[tags=launch] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/running/boot-run-disable-optimized-launch.gradle.kts[tags=launch] +---- + +If the {application-plugin}[`application` plugin] has been applied, its `mainClass` property must be configured and can be used for the same purpose: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/running/application-plugin-main-class-name.gradle[tags=main-class] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/running/application-plugin-main-class-name.gradle.kts[tags=main-class] +---- + + + +[[running-your-application.passing-arguments]] +== Passing Arguments to your Application +Like all `JavaExec` tasks, arguments can be passed into `bootRun` from the command line using `--args=''` when using Gradle 4.9 or later. +For example, to run your application with a profile named `dev` active the following command can be used: + +[source,bash,indent=0,subs="verbatim"] +---- + $ ./gradlew bootRun --args='--spring.profiles.active=dev' +---- + +See {gradle-api}/org/gradle/api/tasks/JavaExec.html#setArgsString-java.lang.String-[the javadoc for `JavaExec.setArgsString`] for further details. + + + +[[running-your-application.passing-system-properties]] +== Passing System properties to your application +Since `bootRun` is a standard `JavaExec` task, system properties can be passed to the application's JVM by specifying them in the build script. +To make that value of a system property to be configurable set its value using a {gradle-dsl}/org.gradle.api.Project.html#N14FE1[project property]. +To allow a project property to be optional, reference it using `findProperty`. +Doing so also allows a default value to be provided using the `?:` Elvis operator, as shown in the following example: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/running/boot-run-system-property.gradle[tags=system-property] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/running/boot-run-system-property.gradle.kts[tags=system-property] +---- + +The preceding example sets that `com.example.property` system property to the value of the `example` project property. +If the `example` project property has not been set, the value of the system property will be `default`. + +Gradle allows project properties to be set in a variety of ways, including on the command line using the `-P` flag, as shown in the following example: + +[source,bash,indent=0,subs="verbatim,attributes"] +---- +$ ./gradlew bootRun -Pexample=custom +---- + +The preceding example sets the value of the `example` project property to `custom`. +`bootRun` will then use this as the value of the `com.example.property` system property. + + + +[[running-your-application.reloading-resources]] +== Reloading Resources +If devtools has been added to your project it will automatically monitor your application's classpath for changes. +Note that modified files need to be recompiled for the classpath to update inorder to trigger reloading with devtools. +For more details on using devtools, refer to {spring-boot-reference}#using.devtools.restart[this section of the reference documentation]. + +Alternatively, you can configure `bootRun` such that your application's static resources are loaded from their source location: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/running/boot-run-source-resources.gradle[tags=source-resources] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/running/boot-run-source-resources.gradle.kts[tags=source-resources] +---- + +This makes them reloadable in the live application which can be helpful at development time. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/getting-started/apply-plugin-release.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/getting-started/apply-plugin-release.gradle new file mode 100644 index 000000000000..eea03ac0f688 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/getting-started/apply-plugin-release.gradle @@ -0,0 +1,3 @@ +plugins { + id 'org.springframework.boot' version '{gradle-project-version}' +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/getting-started/apply-plugin-release.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/getting-started/apply-plugin-release.gradle.kts new file mode 100644 index 000000000000..fead5b05c83c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/getting-started/apply-plugin-release.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("org.springframework.boot") version "{gradle-project-version}" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/getting-started/apply-plugin-snapshot.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/getting-started/apply-plugin-snapshot.gradle similarity index 89% rename from spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/getting-started/apply-plugin-snapshot.gradle rename to spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/getting-started/apply-plugin-snapshot.gradle index 5872a19b7739..5ffae4676a74 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/getting-started/apply-plugin-snapshot.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/getting-started/apply-plugin-snapshot.gradle @@ -4,7 +4,7 @@ buildscript { } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:{version}' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:{gradle-project-version}' } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/getting-started/milestone-settings.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/getting-started/milestone-settings.gradle similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/getting-started/milestone-settings.gradle rename to spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/getting-started/milestone-settings.gradle diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/getting-started/milestone-settings.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/getting-started/milestone-settings.gradle.kts similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/getting-started/milestone-settings.gradle.kts rename to spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/getting-started/milestone-settings.gradle.kts diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/getting-started/snapshot-settings.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/getting-started/snapshot-settings.gradle similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/getting-started/snapshot-settings.gradle rename to spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/getting-started/snapshot-settings.gradle diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/getting-started/snapshot-settings.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/getting-started/snapshot-settings.gradle.kts similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/getting-started/snapshot-settings.gradle.kts rename to spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/getting-started/snapshot-settings.gradle.kts diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/getting-started/typical-plugins.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/getting-started/typical-plugins.gradle similarity index 81% rename from spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/getting-started/typical-plugins.gradle rename to spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/getting-started/typical-plugins.gradle index 8484298eab9e..2ac90b8a46af 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/getting-started/typical-plugins.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/getting-started/typical-plugins.gradle @@ -1,5 +1,5 @@ plugins { - id 'org.springframework.boot' version '{version}' + id 'org.springframework.boot' version '{gradle-project-version}' } // tag::apply[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/getting-started/typical-plugins.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/getting-started/typical-plugins.gradle.kts new file mode 100644 index 000000000000..754080730e32 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/getting-started/typical-plugins.gradle.kts @@ -0,0 +1,15 @@ +// tag::apply[] +plugins { + java + id("org.springframework.boot") version "{gradle-project-version}" +} + +apply(plugin = "io.spring.dependency-management") +// end::apply[] + +tasks.register("verify") { + doLast { + project.plugins.getPlugin(JavaPlugin::class) + project.plugins.getPlugin(io.spring.gradle.dependencymanagement.DependencyManagementPlugin::class) + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/integrating-with-actuator/build-info-additional.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/integrating-with-actuator/build-info-additional.gradle new file mode 100644 index 000000000000..0412ea4ea883 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/integrating-with-actuator/build-info-additional.gradle @@ -0,0 +1,17 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +// tag::additional[] +springBoot { + buildInfo { + properties { + additional = [ + 'a': 'alpha', + 'b': 'bravo' + ] + } + } +} +// end::additional[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/integrating-with-actuator/build-info-additional.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/integrating-with-actuator/build-info-additional.gradle.kts new file mode 100644 index 000000000000..3e8c00e8a517 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/integrating-with-actuator/build-info-additional.gradle.kts @@ -0,0 +1,18 @@ +plugins { + java + id("org.springframework.boot") version "{gradle-project-version}" +} + +// tag::additional[] +springBoot { + buildInfo { + properties { + additional = mapOf( + "a" to "alpha", + "b" to "bravo" + ) + } + } +} +// end::additional[] + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/integrating-with-actuator/build-info-basic.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/integrating-with-actuator/build-info-basic.gradle new file mode 100644 index 000000000000..b8554d14026f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/integrating-with-actuator/build-info-basic.gradle @@ -0,0 +1,10 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +// tag::build-info[] +springBoot { + buildInfo() +} +// end::build-info[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/integrating-with-actuator/build-info-basic.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/integrating-with-actuator/build-info-basic.gradle.kts new file mode 100644 index 000000000000..7eb212645a74 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/integrating-with-actuator/build-info-basic.gradle.kts @@ -0,0 +1,10 @@ +plugins { + java + id("org.springframework.boot") version "{gradle-project-version}" +} + +// tag::build-info[] +springBoot { + buildInfo() +} +// end::build-info[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/integrating-with-actuator/build-info-custom-values.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/integrating-with-actuator/build-info-custom-values.gradle similarity index 77% rename from spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/integrating-with-actuator/build-info-custom-values.gradle rename to spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/integrating-with-actuator/build-info-custom-values.gradle index c52d515f2867..961789da4b45 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/integrating-with-actuator/build-info-custom-values.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/integrating-with-actuator/build-info-custom-values.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '{version}' + id 'org.springframework.boot' version '{gradle-project-version}' } // tag::custom-values[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/integrating-with-actuator/build-info-custom-values.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/integrating-with-actuator/build-info-custom-values.gradle.kts similarity index 76% rename from spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/integrating-with-actuator/build-info-custom-values.gradle.kts rename to spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/integrating-with-actuator/build-info-custom-values.gradle.kts index eabad8c2845d..da5f8717cb44 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/integrating-with-actuator/build-info-custom-values.gradle.kts +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/integrating-with-actuator/build-info-custom-values.gradle.kts @@ -1,6 +1,6 @@ plugins { java - id("org.springframework.boot") version "{version}" + id("org.springframework.boot") version "{gradle-project-version}" } // tag::custom-values[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/managing-dependencies/configure-bom-with-plugins.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/configure-bom-with-plugins.gradle.kts similarity index 88% rename from spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/managing-dependencies/configure-bom-with-plugins.gradle.kts rename to spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/configure-bom-with-plugins.gradle.kts index 3dfcde8bb2dd..9f22625463d4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/managing-dependencies/configure-bom-with-plugins.gradle.kts +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/configure-bom-with-plugins.gradle.kts @@ -3,7 +3,7 @@ import io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension // tag::configure-bom[] plugins { java - id("org.springframework.boot") version "{version}" apply false + id("org.springframework.boot") version "{gradle-project-version}" apply false id("io.spring.dependency-management") version "{dependency-management-plugin-version}" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/managing-dependencies/configure-bom.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/configure-bom.gradle similarity index 87% rename from spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/managing-dependencies/configure-bom.gradle rename to spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/configure-bom.gradle index c2de91e1e874..2c11b81900b1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/managing-dependencies/configure-bom.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/configure-bom.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '{version}' + id 'org.springframework.boot' version '{gradle-project-version}' } // tag::configure-bom[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/managing-dependencies/configure-bom.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/configure-bom.gradle.kts similarity index 89% rename from spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/managing-dependencies/configure-bom.gradle.kts rename to spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/configure-bom.gradle.kts index cd20e55cc070..43dd49deed86 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/managing-dependencies/configure-bom.gradle.kts +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/configure-bom.gradle.kts @@ -2,7 +2,7 @@ import io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension plugins { java - id("org.springframework.boot") version "{version}" + id("org.springframework.boot") version "{gradle-project-version}" } // tag::configure-bom[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/configure-platform.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/configure-platform.gradle new file mode 100644 index 000000000000..25a265efe187 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/configure-platform.gradle @@ -0,0 +1,28 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +// tag::configure-platform[] +dependencies { + implementation platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES) +} +// end::configure-platform[] + +dependencies { + implementation "org.springframework.boot:spring-boot-starter" +} + +repositories { + maven { url 'file:repository' } +} + +configurations.all { + resolutionStrategy { + eachDependency { + if (it.requested.group == 'org.springframework.boot') { + it.useVersion 'TEST-SNAPSHOT' + } + } + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/configure-platform.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/configure-platform.gradle.kts new file mode 100644 index 000000000000..30cfc8ab044b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/configure-platform.gradle.kts @@ -0,0 +1,30 @@ +plugins { + java + id("org.springframework.boot") version "{gradle-project-version}" +} + +// tag::configure-platform[] +dependencies { + implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) +} +// end::configure-platform[] + +dependencies { + implementation("org.springframework.boot:spring-boot-starter") +} + +repositories { + maven { + url = uri("file:repository") + } +} + +configurations.all { + resolutionStrategy { + eachDependency { + if (requested.group == "org.springframework.boot") { + useVersion("TEST-SNAPSHOT") + } + } + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/custom-version-with-platform.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/custom-version-with-platform.gradle new file mode 100644 index 000000000000..d877c3df16df --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/custom-version-with-platform.gradle @@ -0,0 +1,33 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +dependencies { + implementation platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES) + implementation "org.slf4j:slf4j-api" +} + +repositories { + maven { url 'file:repository' } +} + +configurations.all { + resolutionStrategy { + eachDependency { + if (it.requested.group == 'org.springframework.boot') { + it.useVersion 'TEST-SNAPSHOT' + } + } + } +} + +// tag::custom-version[] +configurations.all { + resolutionStrategy.eachDependency { DependencyResolveDetails details -> + if (details.requested.group == 'org.slf4j') { + details.useVersion '1.7.20' + } + } +} +// end::custom-version[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/custom-version-with-platform.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/custom-version-with-platform.gradle.kts new file mode 100644 index 000000000000..a0262204e945 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/custom-version-with-platform.gradle.kts @@ -0,0 +1,35 @@ +plugins { + java + id("org.springframework.boot") version "{gradle-project-version}" +} + +dependencies { + implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) + implementation("org.slf4j:slf4j-api") +} + +repositories { + maven { + url = uri("file:repository") + } +} + +configurations.all { + resolutionStrategy { + eachDependency { + if (requested.group == "org.springframework.boot") { + useVersion("TEST-SNAPSHOT") + } + } + } +} + +// tag::custom-version[] +configurations.all { + resolutionStrategy.eachDependency { + if (requested.group == "org.slf4j") { + useVersion("1.7.20") + } + } +} +// end::custom-version[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/managing-dependencies/custom-version.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/custom-version.gradle similarity index 87% rename from spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/managing-dependencies/custom-version.gradle rename to spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/custom-version.gradle index 211f37a40405..ab3d25436f4f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/managing-dependencies/custom-version.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/custom-version.gradle @@ -1,5 +1,5 @@ plugins { - id 'org.springframework.boot' version '{version}' + id 'org.springframework.boot' version '{gradle-project-version}' } apply plugin: 'io.spring.dependency-management' diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/managing-dependencies/custom-version.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/custom-version.gradle.kts similarity index 85% rename from spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/managing-dependencies/custom-version.gradle.kts rename to spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/custom-version.gradle.kts index c7695d6f696b..a8359facf42d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/managing-dependencies/custom-version.gradle.kts +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/custom-version.gradle.kts @@ -1,7 +1,7 @@ import io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension plugins { - id("org.springframework.boot") version "{version}" + id("org.springframework.boot") version "{gradle-project-version}" } apply(plugin = "io.spring.dependency-management") @@ -26,7 +26,7 @@ the().apply { } } -task("slf4jVersion") { +tasks.register("slf4jVersion") { doLast { println(project.the().managedVersions["org.slf4j:slf4j-api"]) } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/managing-dependencies/depend-on-plugin-milestone.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/depend-on-plugin-milestone.gradle similarity index 87% rename from spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/managing-dependencies/depend-on-plugin-milestone.gradle rename to spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/depend-on-plugin-milestone.gradle index 9b8007b8dd37..7aa042b4c673 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/managing-dependencies/depend-on-plugin-milestone.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/depend-on-plugin-milestone.gradle @@ -4,6 +4,6 @@ buildscript { } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:{version}' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:{gradle-project-version}' } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/depend-on-plugin-release.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/depend-on-plugin-release.gradle new file mode 100644 index 000000000000..88fba72d152b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/depend-on-plugin-release.gradle @@ -0,0 +1,3 @@ +plugins { + id 'org.springframework.boot' version '{gradle-project-version}' apply false +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/depend-on-plugin-release.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/depend-on-plugin-release.gradle.kts new file mode 100644 index 000000000000..5bebec31c3f8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/depend-on-plugin-release.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("org.springframework.boot") version "{gradle-project-version}" apply false +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/managing-dependencies/depend-on-plugin-snapshot.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/depend-on-plugin-snapshot.gradle similarity index 87% rename from spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/managing-dependencies/depend-on-plugin-snapshot.gradle rename to spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/depend-on-plugin-snapshot.gradle index ab0eb4b2cb48..82b97382ce66 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/managing-dependencies/depend-on-plugin-snapshot.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/depend-on-plugin-snapshot.gradle @@ -4,6 +4,6 @@ buildscript { } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:{version}' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:{gradle-project-version}' } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/dependencies.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/dependencies.gradle new file mode 100644 index 000000000000..64e49cc30790 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/dependencies.gradle @@ -0,0 +1,13 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +apply plugin: 'io.spring.dependency-management' + +// tag::dependencies[] +dependencies { + implementation('org.springframework.boot:spring-boot-starter-web') + implementation('org.springframework.boot:spring-boot-starter-data-jpa') +} +// end::dependencies[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/managing-dependencies/dependencies.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/dependencies.gradle.kts similarity index 80% rename from spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/managing-dependencies/dependencies.gradle.kts rename to spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/dependencies.gradle.kts index fb31765267ea..c1392e1cfe2e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/managing-dependencies/dependencies.gradle.kts +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/managing-dependencies/dependencies.gradle.kts @@ -1,6 +1,6 @@ plugins { java - id("org.springframework.boot") version "{version}" + id("org.springframework.boot") version "{gradle-project-version}" } apply(plugin = "io.spring.dependency-management") diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/application-plugin-main-class.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/application-plugin-main-class.gradle new file mode 100644 index 000000000000..02a24dda6794 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/application-plugin-main-class.gradle @@ -0,0 +1,11 @@ +plugins { + id 'java' + id 'application' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +// tag::main-class[] +application { + mainClass = 'com.example.ExampleApplication' +} +// end::main-class[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/application-plugin-main-class.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/application-plugin-main-class.gradle.kts new file mode 100644 index 000000000000..23a84fbc2576 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/application-plugin-main-class.gradle.kts @@ -0,0 +1,11 @@ +plugins { + java + application + id("org.springframework.boot") version "{gradle-project-version}" +} + +// tag::main-class[] +application { + mainClass.set("com.example.ExampleApplication") +} +// end::main-class[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-builder.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-builder.gradle new file mode 100644 index 000000000000..520787b2edc5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-builder.gradle @@ -0,0 +1,22 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +tasks.named("bootJar") { + mainClass = 'com.example.ExampleApplication' +} + +// tag::builder[] +tasks.named("bootBuildImage") { + builder = "mine/java-cnb-builder" + runImage = "mine/java-cnb-run" +} +// end::builder[] + +tasks.register("bootBuildImageBuilder") { + doFirst { + println("builder=${tasks.bootBuildImage.builder}") + println("runImage=${tasks.bootBuildImage.runImage}") + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-builder.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-builder.gradle.kts new file mode 100644 index 000000000000..23aa9bd0105d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-builder.gradle.kts @@ -0,0 +1,25 @@ +import org.springframework.boot.gradle.tasks.bundling.BootJar +import org.springframework.boot.gradle.tasks.bundling.BootBuildImage + +plugins { + java + id("org.springframework.boot") version "{gradle-project-version}" +} + +tasks.named("bootJar") { + mainClass.set("com.example.ExampleApplication") +} + +// tag::builder[] +tasks.named("bootBuildImage") { + builder = "mine/java-cnb-builder" + runImage = "mine/java-cnb-run" +} +// end::builder[] + +tasks.register("bootBuildImageBuilder") { + doFirst { + println("builder=${tasks.getByName("bootBuildImage").builder}") + println("runImage=${tasks.getByName("bootBuildImage").runImage}") + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-buildpacks.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-buildpacks.gradle new file mode 100644 index 000000000000..ab0293db49ba --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-buildpacks.gradle @@ -0,0 +1,16 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +// tag::buildpacks[] +tasks.named("bootBuildImage") { + buildpacks = ["file:///path/to/example-buildpack.tgz", "urn:cnb:builder:paketo-buildpacks/java"] +} +// end::buildpacks[] + +tasks.register("bootBuildImageBuildpacks") { + doFirst { + bootBuildImage.buildpacks.each { reference -> println "$reference" } + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-buildpacks.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-buildpacks.gradle.kts new file mode 100644 index 000000000000..2de322927210 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-buildpacks.gradle.kts @@ -0,0 +1,20 @@ +import org.springframework.boot.gradle.tasks.bundling.BootBuildImage + +plugins { + java + id("org.springframework.boot") version "{gradle-project-version}" +} + +// tag::buildpacks[] +tasks.named("bootBuildImage") { + buildpacks = listOf("file:///path/to/example-buildpack.tgz", "urn:cnb:builder:paketo-buildpacks/java") +} +// end::buildpacks[] + +tasks.register("bootBuildImageBuildpacks") { + doFirst { + for(reference in tasks.getByName("bootBuildImage").buildpacks) { + print(reference) + } + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-docker-auth-token.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-docker-auth-token.gradle new file mode 100644 index 000000000000..4e917c2e3f3d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-docker-auth-token.gradle @@ -0,0 +1,24 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +tasks.named("bootJar") { + mainClass = 'com.example.ExampleApplication' +} + +// tag::docker-auth-token[] +tasks.named("bootBuildImage") { + docker { + builderRegistry { + token = "9cbaf023786cd7..." + } + } +} +// end::docker-auth-token[] + +tasks.register("bootBuildImageDocker") { + doFirst { + println("token=${tasks.bootBuildImage.docker.builderRegistry.token}") + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-docker-auth-token.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-docker-auth-token.gradle.kts new file mode 100644 index 000000000000..44a7c26b2a38 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-docker-auth-token.gradle.kts @@ -0,0 +1,27 @@ +import org.springframework.boot.gradle.tasks.bundling.BootJar +import org.springframework.boot.gradle.tasks.bundling.BootBuildImage + +plugins { + java + id("org.springframework.boot") version "{gradle-project-version}" +} + +tasks.named("bootJar") { + mainClass.set("com.example.ExampleApplication") +} + +// tag::docker-auth-token[] +tasks.named("bootBuildImage") { + docker { + builderRegistry { + token = "9cbaf023786cd7..." + } + } +} +// end::docker-auth-token[] + +tasks.register("bootBuildImageDocker") { + doFirst { + println("token=${tasks.getByName("bootBuildImage").docker.builderRegistry.token}") + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-docker-auth-user.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-docker-auth-user.gradle new file mode 100644 index 000000000000..a51c09bd98a4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-docker-auth-user.gradle @@ -0,0 +1,30 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +tasks.named("bootJar") { + mainClass = 'com.example.ExampleApplication' +} + +// tag::docker-auth-user[] +tasks.named("bootBuildImage") { + docker { + builderRegistry { + username = "user" + password = "secret" + url = "https://docker.example.com/v1/" + email = "user@example.com" + } + } +} +// end::docker-auth-user[] + +tasks.register("bootBuildImageDocker") { + doFirst { + println("username=${tasks.bootBuildImage.docker.builderRegistry.username}") + println("password=${tasks.bootBuildImage.docker.builderRegistry.password}") + println("url=${tasks.bootBuildImage.docker.builderRegistry.url}") + println("email=${tasks.bootBuildImage.docker.builderRegistry.email}") + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-docker-auth-user.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-docker-auth-user.gradle.kts new file mode 100644 index 000000000000..737fe63b377c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-docker-auth-user.gradle.kts @@ -0,0 +1,33 @@ +import org.springframework.boot.gradle.tasks.bundling.BootJar +import org.springframework.boot.gradle.tasks.bundling.BootBuildImage + +plugins { + java + id("org.springframework.boot") version "{gradle-project-version}" +} + +tasks.named("bootJar") { + mainClass.set("com.example.ExampleApplication") +} + +// tag::docker-auth-user[] +tasks.named("bootBuildImage") { + docker { + builderRegistry { + username = "user" + password = "secret" + url = "https://docker.example.com/v1/" + email = "user@example.com" + } + } +} +// end::docker-auth-user[] + +tasks.register("bootBuildImageDocker") { + doFirst { + println("username=${tasks.getByName("bootBuildImage").docker.builderRegistry.username}") + println("password=${tasks.getByName("bootBuildImage").docker.builderRegistry.password}") + println("url=${tasks.getByName("bootBuildImage").docker.builderRegistry.url}") + println("email=${tasks.getByName("bootBuildImage").docker.builderRegistry.email}") + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-docker-host.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-docker-host.gradle new file mode 100644 index 000000000000..8ee8a2d67dce --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-docker-host.gradle @@ -0,0 +1,26 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +tasks.named("bootJar") { + mainClass = 'com.example.ExampleApplication' +} + +// tag::docker-host[] +tasks.named("bootBuildImage") { + docker { + host = "tcp://192.168.99.100:2376" + tlsVerify = true + certPath = "/home/users/.minikube/certs" + } +} +// end::docker-host[] + +tasks.register("bootBuildImageDocker") { + doFirst { + println("host=${tasks.bootBuildImage.docker.host}") + println("tlsVerify=${tasks.bootBuildImage.docker.tlsVerify}") + println("certPath=${tasks.bootBuildImage.docker.certPath}") + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-docker-host.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-docker-host.gradle.kts new file mode 100644 index 000000000000..3b726997456d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-docker-host.gradle.kts @@ -0,0 +1,29 @@ +import org.springframework.boot.gradle.tasks.bundling.BootJar +import org.springframework.boot.gradle.tasks.bundling.BootBuildImage + +plugins { + java + id("org.springframework.boot") version "{gradle-project-version}" +} + +tasks.named("bootJar") { + mainClass.set("com.example.ExampleApplication") +} + +// tag::docker-host[] +tasks.named("bootBuildImage") { + docker { + host = "tcp://192.168.99.100:2376" + isTlsVerify = true + certPath = "/home/users/.minikube/certs" + } +} +// end::docker-host[] + +tasks.register("bootBuildImageDocker") { + doFirst { + println("host=${tasks.getByName("bootBuildImage").docker.host}") + println("tlsVerify=${tasks.getByName("bootBuildImage").docker.isTlsVerify}") + println("certPath=${tasks.getByName("bootBuildImage").docker.certPath}") + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-env-proxy.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-env-proxy.gradle new file mode 100644 index 000000000000..71922f44902d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-env-proxy.gradle @@ -0,0 +1,19 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +// tag::env[] +tasks.named("bootBuildImage") { + environment = [ + "HTTP_PROXY" : "http://proxy.example.com", + "HTTPS_PROXY": "https://proxy.example.com" + ] +} +// end::env[] + +tasks.register("bootBuildImageEnvironment") { + doFirst { + bootBuildImage.environment.each { name, value -> println "$name=$value" } + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-env-proxy.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-env-proxy.gradle.kts new file mode 100644 index 000000000000..bf265f8b5c5f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-env-proxy.gradle.kts @@ -0,0 +1,21 @@ +import org.springframework.boot.gradle.tasks.bundling.BootBuildImage + +plugins { + java + id("org.springframework.boot") version "{gradle-project-version}" +} + +// tag::env[] +tasks.named("bootBuildImage") { + environment = mapOf("HTTP_PROXY" to "http://proxy.example.com", + "HTTPS_PROXY" to "https://proxy.example.com") +} +// end::env[] + +tasks.register("bootBuildImageEnvironment") { + doFirst { + for((name, value) in tasks.getByName("bootBuildImage").environment) { + print(name + "=" + value) + } + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-env-runtime.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-env-runtime.gradle new file mode 100644 index 000000000000..3b0c454d7fa3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-env-runtime.gradle @@ -0,0 +1,23 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +tasks.named("bootJar") { + mainClass = 'com.example.ExampleApplication' +} + +// tag::env-runtime[] +tasks.named("bootBuildImage") { + environment = [ + "BPE_DELIM_JAVA_TOOL_OPTIONS" : " ", + "BPE_APPEND_JAVA_TOOL_OPTIONS" : "-XX:+HeapDumpOnOutOfMemoryError" + ] +} +// end::env-runtime[] + +tasks.register("bootBuildImageEnvironment") { + doFirst { + bootBuildImage.environment.each { name, value -> println "$name=$value" } + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-env-runtime.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-env-runtime.gradle.kts new file mode 100644 index 000000000000..9140d8182dd9 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-env-runtime.gradle.kts @@ -0,0 +1,24 @@ +import org.springframework.boot.gradle.tasks.bundling.BootBuildImage + +plugins { + java + id("org.springframework.boot") version "{gradle-project-version}" +} + +// tag::env-runtime[] +tasks.named("bootBuildImage") { + environment = mapOf( + "BPE_DELIM_JAVA_TOOL_OPTIONS" to " ", + "BPE_APPEND_JAVA_TOOL_OPTIONS" to "-XX:+HeapDumpOnOutOfMemoryError" + ) +} +// end::env-runtime[] + +tasks.register("bootBuildImageEnvironment") { + doFirst { + for((name, value) in tasks.getByName("bootBuildImage").environment) { + print(name + "=" + value) + } + } +} + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-env.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-env.gradle new file mode 100644 index 000000000000..71d3948193c7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-env.gradle @@ -0,0 +1,16 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +// tag::env[] +tasks.named("bootBuildImage") { + environment = ["BP_JVM_VERSION" : "8.*"] +} +// end::env[] + +tasks.register("bootBuildImageEnvironment") { + doFirst { + bootBuildImage.environment.each { name, value -> println "$name=$value" } + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-env.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-env.gradle.kts new file mode 100644 index 000000000000..9613fe8c05fc --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-env.gradle.kts @@ -0,0 +1,21 @@ +import org.springframework.boot.gradle.tasks.bundling.BootBuildImage + +plugins { + java + id("org.springframework.boot") version "{gradle-project-version}" +} + +// tag::env[] +tasks.named("bootBuildImage") { + environment = mapOf("BP_JVM_VERSION" to "8.*") +} +// end::env[] + +tasks.register("bootBuildImageEnvironment") { + doFirst { + for((name, value) in tasks.getByName("bootBuildImage").environment) { + print(name + "=" + value) + } + } +} + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-name.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-name.gradle new file mode 100644 index 000000000000..1eaa8d03d799 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-name.gradle @@ -0,0 +1,16 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +// tag::image-name[] +tasks.named("bootBuildImage") { + imageName = "example.com/library/${project.name}" +} +// end::image-name[] + +tasks.register("bootBuildImageName") { + doFirst { + println(tasks.bootBuildImage.imageName) + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-name.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-name.gradle.kts new file mode 100644 index 000000000000..d043937d0c40 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-name.gradle.kts @@ -0,0 +1,18 @@ +import org.springframework.boot.gradle.tasks.bundling.BootBuildImage + +plugins { + java + id("org.springframework.boot") version "{gradle-project-version}" +} + +// tag::image-name[] +tasks.named("bootBuildImage") { + imageName = "example.com/library/${project.name}" +} +// end::image-name[] + +tasks.register("bootBuildImageName") { + doFirst { + println(tasks.getByName("bootBuildImage").imageName) + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-publish.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-publish.gradle new file mode 100644 index 000000000000..f3603a4405f9 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-publish.gradle @@ -0,0 +1,29 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +tasks.named("bootJar") { + mainClass = 'com.example.ExampleApplication' +} + +// tag::publish[] +tasks.named("bootBuildImage") { + imageName = "docker.example.com/library/${project.name}" + publish = true + docker { + publishRegistry { + username = "user" + password = "secret" + url = "https://docker.example.com/v1/" + email = "user@example.com" + } + } +} +// end::publish[] + +tasks.register("bootBuildImagePublish") { + doFirst { + println(tasks.bootBuildImage.publish) + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-publish.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-publish.gradle.kts new file mode 100644 index 000000000000..6c6d11423019 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-publish.gradle.kts @@ -0,0 +1,32 @@ +import org.springframework.boot.gradle.tasks.bundling.BootJar +import org.springframework.boot.gradle.tasks.bundling.BootBuildImage + +plugins { + java + id("org.springframework.boot") version "{gradle-project-version}" +} + +tasks.named("bootJar") { + mainClass.set("com.example.ExampleApplication") +} + +// tag::publish[] +tasks.named("bootBuildImage") { + imageName = "docker.example.com/library/${project.name}" + isPublish = true + docker { + publishRegistry { + username = "user" + password = "secret" + url = "https://docker.example.com/v1/" + email = "user@example.com" + } + } +} +// end::publish[] + +tasks.register("bootBuildImagePublish") { + doFirst { + println(tasks.getByName("bootBuildImage").isPublish) + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-and-jar-classifiers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-and-jar-classifiers.gradle new file mode 100644 index 000000000000..024266d70125 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-and-jar-classifiers.gradle @@ -0,0 +1,18 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +// tag::classifiers[] +tasks.named("bootJar") { + archiveClassifier = 'boot' +} + +tasks.named("jar") { + archiveClassifier = '' +} +// end::classifiers[] + +tasks.named("bootJar") { + mainClass = 'com.example.Application' +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-and-jar-classifiers.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-and-jar-classifiers.gradle.kts new file mode 100644 index 000000000000..f54362da9c40 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-and-jar-classifiers.gradle.kts @@ -0,0 +1,20 @@ +import org.springframework.boot.gradle.tasks.bundling.BootJar + +plugins { + java + id("org.springframework.boot") version "{gradle-project-version}" +} + +// tag::classifiers[] +tasks.named("bootJar") { + archiveClassifier.set("boot") +} + +tasks.named("jar") { + archiveClassifier.set("") +} +// end::classifiers[] + +tasks.named("bootJar") { + mainClass.set("com.example.Application") +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-custom-launch-script.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-custom-launch-script.gradle new file mode 100644 index 000000000000..da6918c0762f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-custom-launch-script.gradle @@ -0,0 +1,16 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +tasks.named("bootJar") { + mainClass = 'com.example.ExampleApplication' +} + +// tag::custom-launch-script[] +tasks.named("bootJar") { + launchScript { + script = file('src/custom.script') + } +} +// end::custom-launch-script[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-custom-launch-script.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-custom-launch-script.gradle.kts new file mode 100644 index 000000000000..a3e15f1f5eea --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-custom-launch-script.gradle.kts @@ -0,0 +1,18 @@ +import org.springframework.boot.gradle.tasks.bundling.BootJar + +plugins { + java + id("org.springframework.boot") version "{gradle-project-version}" +} + +tasks.named("bootJar") { + mainClass.set("com.example.ExampleApplication") +} + +// tag::custom-launch-script[] +tasks.named("bootJar") { + launchScript { + script = file("src/custom.script") + } +} +// end::custom-launch-script[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-include-launch-script.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-include-launch-script.gradle new file mode 100644 index 000000000000..c1f3d348a844 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-include-launch-script.gradle @@ -0,0 +1,14 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +tasks.named("bootJar") { + mainClass = 'com.example.ExampleApplication' +} + +// tag::include-launch-script[] +tasks.named("bootJar") { + launchScript() +} +// end::include-launch-script[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-include-launch-script.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-include-launch-script.gradle.kts new file mode 100644 index 000000000000..7ffdf42e4f96 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-include-launch-script.gradle.kts @@ -0,0 +1,16 @@ +import org.springframework.boot.gradle.tasks.bundling.BootJar + +plugins { + java + id("org.springframework.boot") version "{gradle-project-version}" +} + +tasks.named("bootJar") { + mainClass.set("com.example.ExampleApplication") +} + +// tag::include-launch-script[] +tasks.named("bootJar") { + launchScript() +} +// end::include-launch-script[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-launch-script-properties.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-launch-script-properties.gradle new file mode 100644 index 000000000000..6f1df662beb4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-launch-script-properties.gradle @@ -0,0 +1,16 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +tasks.named("bootJar") { + mainClass = 'com.example.ExampleApplication' +} + +// tag::launch-script-properties[] +tasks.named("bootJar") { + launchScript { + properties 'logFilename': 'example-app.log' + } +} +// end::launch-script-properties[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-launch-script-properties.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-launch-script-properties.gradle.kts new file mode 100644 index 000000000000..b3e4206ca958 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-launch-script-properties.gradle.kts @@ -0,0 +1,18 @@ +import org.springframework.boot.gradle.tasks.bundling.BootJar + +plugins { + java + id("org.springframework.boot") version "{gradle-project-version}" +} + +tasks.named("bootJar") { + mainClass.set("com.example.ExampleApplication") +} + +// tag::launch-script-properties[] +tasks.named("bootJar") { + launchScript { + properties(mapOf("logFilename" to "example-app.log")) + } +} +// end::launch-script-properties[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-custom.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-custom.gradle new file mode 100644 index 000000000000..ba0dde4dc051 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-custom.gradle @@ -0,0 +1,31 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +tasks.named("bootJar") { + mainClass = 'com.example.ExampleApplication' +} + +// tag::layered[] +tasks.named("bootJar") { + layered { + application { + intoLayer("spring-boot-loader") { + include "org/springframework/boot/loader/**" + } + intoLayer("application") + } + dependencies { + intoLayer("application") { + includeProjectDependencies() + } + intoLayer("snapshot-dependencies") { + include "*:*:*SNAPSHOT" + } + intoLayer("dependencies") + } + layerOrder = ["dependencies", "spring-boot-loader", "snapshot-dependencies", "application"] + } +} +// end::layered[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-custom.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-custom.gradle.kts new file mode 100644 index 000000000000..851ee90179f5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-custom.gradle.kts @@ -0,0 +1,30 @@ +import org.springframework.boot.gradle.tasks.bundling.BootJar + +plugins { + java + id("org.springframework.boot") version "{gradle-project-version}" +} + +tasks.named("bootJar") { + mainClass.set("com.example.ExampleApplication") +} + +// tag::layered[] +tasks.named("bootJar") { + layered { + application { + intoLayer("spring-boot-loader") { + include("org/springframework/boot/loader/**") + } + intoLayer("application") + } + dependencies { + intoLayer("snapshot-dependencies") { + include("*:*:*SNAPSHOT") + } + intoLayer("dependencies") + } + layerOrder = listOf("dependencies", "spring-boot-loader", "snapshot-dependencies", "application") + } +} +// end::layered[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-disabled.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-disabled.gradle new file mode 100644 index 000000000000..3ea9a6277269 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-disabled.gradle @@ -0,0 +1,16 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +tasks.named("bootJar") { + mainClass = 'com.example.ExampleApplication' +} + +// tag::layered[] +tasks.named("bootJar") { + layered { + enabled = false + } +} +// end::layered[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-disabled.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-disabled.gradle.kts new file mode 100644 index 000000000000..9ced3e713840 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-disabled.gradle.kts @@ -0,0 +1,18 @@ +import org.springframework.boot.gradle.tasks.bundling.BootJar + +plugins { + java + id("org.springframework.boot") version "{gradle-project-version}" +} + +tasks.named("bootJar") { + mainClass.set("com.example.ExampleApplication") +} + +// tag::layered[] +tasks.named("bootJar") { + layered { + isEnabled = false + } +} +// end::layered[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-exclude-tools.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-exclude-tools.gradle new file mode 100644 index 000000000000..7175092af835 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-exclude-tools.gradle @@ -0,0 +1,16 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +tasks.named("bootJar") { + mainClass = 'com.example.ExampleApplication' +} + +// tag::layered[] +tasks.named("bootJar") { + layered { + includeLayerTools = false + } +} +// end::layered[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-exclude-tools.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-exclude-tools.gradle.kts new file mode 100644 index 000000000000..bababaa61b31 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-layered-exclude-tools.gradle.kts @@ -0,0 +1,18 @@ +import org.springframework.boot.gradle.tasks.bundling.BootJar + +plugins { + java + id("org.springframework.boot") version "{gradle-project-version}" +} + +tasks.named("bootJar") { + mainClass.set("com.example.ExampleApplication") +} + +// tag::layered[] +tasks.named("bootJar") { + layered { + isIncludeLayerTools = false + } +} +// end::layered[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-main-class.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-main-class.gradle new file mode 100644 index 000000000000..55b9de16f35c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-main-class.gradle @@ -0,0 +1,10 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +// tag::main-class[] +tasks.named("bootJar") { + mainClass = 'com.example.ExampleApplication' +} +// end::main-class[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-main-class.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-main-class.gradle.kts new file mode 100644 index 000000000000..48aea9b011a4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-main-class.gradle.kts @@ -0,0 +1,12 @@ +import org.springframework.boot.gradle.tasks.bundling.BootJar + +plugins { + java + id("org.springframework.boot") version "{gradle-project-version}" +} + +// tag::main-class[] +tasks.named("bootJar") { + mainClass.set("com.example.ExampleApplication") +} +// end::main-class[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-manifest-main-class.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-manifest-main-class.gradle new file mode 100644 index 000000000000..f384d8e6a6b5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-manifest-main-class.gradle @@ -0,0 +1,12 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +// tag::main-class[] +tasks.named("bootJar") { + manifest { + attributes 'Start-Class': 'com.example.ExampleApplication' + } +} +// end::main-class[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-manifest-main-class.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-manifest-main-class.gradle.kts new file mode 100644 index 000000000000..a79bc048cb7f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-manifest-main-class.gradle.kts @@ -0,0 +1,14 @@ +import org.springframework.boot.gradle.tasks.bundling.BootJar + +plugins { + java + id("org.springframework.boot") version "{gradle-project-version}" +} + +// tag::main-class[] +tasks.named("bootJar") { + manifest { + attributes("Start-Class" to "com.example.ExampleApplication") + } +} +// end::main-class[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-requires-unpack.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-requires-unpack.gradle new file mode 100644 index 000000000000..3274bdb59461 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-requires-unpack.gradle @@ -0,0 +1,22 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +repositories { + mavenCentral() +} + +dependencies { + runtimeOnly('org.jruby:jruby-complete:1.7.25') +} + +tasks.named("bootJar") { + mainClass = 'com.example.ExampleApplication' +} + +// tag::requires-unpack[] +tasks.named("bootJar") { + requiresUnpack '**/jruby-complete-*.jar' +} +// end::requires-unpack[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-requires-unpack.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-requires-unpack.gradle.kts new file mode 100644 index 000000000000..4f68e068e6a3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-jar-requires-unpack.gradle.kts @@ -0,0 +1,24 @@ +import org.springframework.boot.gradle.tasks.bundling.BootJar + +plugins { + java + id("org.springframework.boot") version "{gradle-project-version}" +} + +repositories { + mavenCentral() +} + +dependencies { + runtimeOnly("org.jruby:jruby-complete:1.7.25") +} + +tasks.named("bootJar") { + mainClass.set("com.example.ExampleApplication") +} + +// tag::requires-unpack[] +tasks.named("bootJar") { + requiresUnpack("**/jruby-complete-*.jar") +} +// end::requires-unpack[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-war-include-devtools.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-war-include-devtools.gradle new file mode 100644 index 000000000000..1f12601ca74f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-war-include-devtools.gradle @@ -0,0 +1,18 @@ +plugins { + id 'war' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +tasks.named("bootWar") { + mainClass = 'com.example.ExampleApplication' +} + +dependencies { + developmentOnly files("spring-boot-devtools-1.2.3.RELEASE.jar") +} + +// tag::include-devtools[] +tasks.named("bootWar") { + classpath configurations.developmentOnly +} +// end::include-devtools[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-war-include-devtools.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-war-include-devtools.gradle.kts new file mode 100644 index 000000000000..c151efe9e5ec --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-war-include-devtools.gradle.kts @@ -0,0 +1,20 @@ +import org.springframework.boot.gradle.tasks.bundling.BootWar + +plugins { + war + id("org.springframework.boot") version "{gradle-project-version}" +} + +tasks.named("bootWar") { + mainClass.set("com.example.ExampleApplication") +} + +dependencies { + "developmentOnly"(files("spring-boot-devtools-1.2.3.RELEASE.jar")) +} + +// tag::include-devtools[] +tasks.named("bootWar") { + classpath(configurations["developmentOnly"]) +} +// end::include-devtools[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-war-properties-launcher.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-war-properties-launcher.gradle new file mode 100644 index 000000000000..2872469f60fb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-war-properties-launcher.gradle @@ -0,0 +1,16 @@ +plugins { + id 'war' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +tasks.named("bootWar") { + mainClass = 'com.example.ExampleApplication' +} + +// tag::properties-launcher[] +tasks.named("bootWar") { + manifest { + attributes 'Main-Class': 'org.springframework.boot.loader.PropertiesLauncher' + } +} +// end::properties-launcher[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-war-properties-launcher.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-war-properties-launcher.gradle.kts new file mode 100644 index 000000000000..19d723b795fa --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-war-properties-launcher.gradle.kts @@ -0,0 +1,18 @@ +import org.springframework.boot.gradle.tasks.bundling.BootWar + +plugins { + war + id("org.springframework.boot") version "{gradle-project-version}" +} + +tasks.named("bootWar") { + mainClass.set("com.example.ExampleApplication") +} + +// tag::properties-launcher[] +tasks.named("bootWar") { + manifest { + attributes("Main-Class" to "org.springframework.boot.loader.PropertiesLauncher") + } +} +// end::properties-launcher[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/only-boot-jar.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/only-boot-jar.gradle new file mode 100644 index 000000000000..748aa957f381 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/only-boot-jar.gradle @@ -0,0 +1,14 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +// tag::disable-jar[] +tasks.named("jar") { + enabled = false +} +// end::disable-jar[] + +tasks.named("bootJar") { + mainClass = 'com.example.Application' +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/only-boot-jar.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/only-boot-jar.gradle.kts new file mode 100644 index 000000000000..c15f189fdca4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/only-boot-jar.gradle.kts @@ -0,0 +1,16 @@ +import org.springframework.boot.gradle.tasks.bundling.BootJar + +plugins { + java + id("org.springframework.boot") version "{gradle-project-version}" +} + +// tag::disable-jar[] +tasks.named("jar") { + enabled = false +} +// end::disable-jar[] + +tasks.named("bootJar") { + mainClass.set("com.example.Application") +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/spring-boot-dsl-main-class.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/spring-boot-dsl-main-class.gradle new file mode 100644 index 000000000000..c84dffd88e47 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/spring-boot-dsl-main-class.gradle @@ -0,0 +1,10 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +// tag::main-class[] +springBoot { + mainClass = 'com.example.ExampleApplication' +} +// end::main-class[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/spring-boot-dsl-main-class.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/spring-boot-dsl-main-class.gradle.kts new file mode 100644 index 000000000000..a16e4502fb28 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/spring-boot-dsl-main-class.gradle.kts @@ -0,0 +1,10 @@ +plugins { + war + id("org.springframework.boot") version "{gradle-project-version}" +} + +// tag::main-class[] +springBoot { + mainClass.set("com.example.ExampleApplication") +} +// end::main-class[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/war-container-dependency.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/war-container-dependency.gradle new file mode 100644 index 000000000000..ad0f0b53e0fa --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/war-container-dependency.gradle @@ -0,0 +1,13 @@ +plugins { + id 'war' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +apply plugin: 'io.spring.dependency-management' + +// tag::dependencies[] +dependencies { + implementation('org.springframework.boot:spring-boot-starter-web') + providedRuntime('org.springframework.boot:spring-boot-starter-tomcat') +} +// end::dependencies[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/war-container-dependency.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/war-container-dependency.gradle.kts similarity index 80% rename from spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/war-container-dependency.gradle.kts rename to spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/war-container-dependency.gradle.kts index 92c4cb080ccc..7ea6688bcb05 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/war-container-dependency.gradle.kts +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/war-container-dependency.gradle.kts @@ -1,6 +1,6 @@ plugins { war - id("org.springframework.boot") version "{version}" + id("org.springframework.boot") version "{gradle-project-version}" } apply(plugin = "io.spring.dependency-management") diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/publishing/maven-publish.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/publishing/maven-publish.gradle new file mode 100644 index 000000000000..418c5f101fa6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/publishing/maven-publish.gradle @@ -0,0 +1,27 @@ +plugins { + id 'java' + id 'maven-publish' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +// tag::publishing[] +publishing { + publications { + bootJava(MavenPublication) { + artifact tasks.named("bootJar") + } + } + repositories { + maven { + url 'https://repo.example.com' + } + } +} +// end::publishing[] + +tasks.register("publishingConfiguration") { + doLast { + println publishing.publications.bootJava + println publishing.repositories.maven.url + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/publishing/maven-publish.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/publishing/maven-publish.gradle.kts new file mode 100644 index 000000000000..f11d2344cf65 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/publishing/maven-publish.gradle.kts @@ -0,0 +1,27 @@ +plugins { + java + `maven-publish` + id("org.springframework.boot") version "{gradle-project-version}" +} + +// tag::publishing[] +publishing { + publications { + create("bootJava") { + artifact(tasks.named("bootJar")) + } + } + repositories { + maven { + url = uri("https://repo.example.com") + } + } +} +// end::publishing[] + +tasks.register("publishingConfiguration") { + doLast { + println(publishing.publications["bootJava"]) + println(publishing.repositories.getByName("maven").url) + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/publishing/maven.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/publishing/maven.gradle new file mode 100644 index 000000000000..f568417d8816 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/publishing/maven.gradle @@ -0,0 +1,21 @@ +plugins { + id 'java' + id 'maven' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +// tag::upload[] +tasks.named("uploadBootArchives") { + repositories { + mavenDeployer { + repository url: 'https://repo.example.com' + } + } +} +// end::upload[] + +tasks.register("deployerRepository") { + doLast { + println uploadBootArchives.repositories.mavenDeployer.repository.url + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/publishing/maven.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/publishing/maven.gradle.kts new file mode 100644 index 000000000000..7bb2c9043775 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/publishing/maven.gradle.kts @@ -0,0 +1,26 @@ +plugins { + java + maven + id("org.springframework.boot") version "{gradle-project-version}" +} + +// tag::upload[] +tasks.named("uploadBootArchives") { + repositories.withGroovyBuilder { + "mavenDeployer" { + "repository"("url" to "https://repo.example.com") + } + } +} +// end::upload[] + +tasks.register("deployerRepository") { + doLast { + val url = tasks.getByName("uploadBootArchives") + .repositories + .withGroovyBuilder { getProperty("mavenDeployer") } + .withGroovyBuilder { getProperty("repository") } + .withGroovyBuilder { getProperty("url") } + println(url) + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/application-plugin-main-class-name.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/application-plugin-main-class-name.gradle new file mode 100644 index 000000000000..1ee1d23abaef --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/application-plugin-main-class-name.gradle @@ -0,0 +1,12 @@ +plugins { + id 'java' + id 'application' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +// tag::main-class[] +application { + mainClass = 'com.example.ExampleApplication' +} +// end::main-class[] + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/application-plugin-main-class-name.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/application-plugin-main-class-name.gradle.kts new file mode 100644 index 000000000000..9f9d02f2b089 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/application-plugin-main-class-name.gradle.kts @@ -0,0 +1,13 @@ +import org.springframework.boot.gradle.tasks.run.BootRun + +plugins { + java + application + id("org.springframework.boot") version "{gradle-project-version}" +} + +// tag::main-class[] +application { + mainClass.set("com.example.ExampleApplication") +} +// end::main-class[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/boot-run-disable-optimized-launch.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/boot-run-disable-optimized-launch.gradle new file mode 100644 index 000000000000..e1529d17cfca --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/boot-run-disable-optimized-launch.gradle @@ -0,0 +1,16 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +// tag::launch[] +tasks.named("bootRun") { + optimizedLaunch = false +} +// end::launch[] + +tasks.register("optimizedLaunch") { + doLast { + println bootRun.optimizedLaunch + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/boot-run-disable-optimized-launch.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/boot-run-disable-optimized-launch.gradle.kts new file mode 100644 index 000000000000..7efa52cd2a83 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/boot-run-disable-optimized-launch.gradle.kts @@ -0,0 +1,18 @@ +import org.springframework.boot.gradle.tasks.run.BootRun + +plugins { + java + id("org.springframework.boot") version "{gradle-project-version}" +} + +// tag::launch[] +tasks.named("bootRun") { + isOptimizedLaunch = false +} +// end::launch[] + +tasks.register("optimizedLaunch") { + doLast { + println(tasks.getByName("bootRun").isOptimizedLaunch) + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/boot-run-main.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/boot-run-main.gradle new file mode 100644 index 000000000000..292a53689777 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/boot-run-main.gradle @@ -0,0 +1,16 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +// tag::main[] +tasks.named("bootRun") { + mainClass = 'com.example.ExampleApplication' +} +// end::main[] + +tasks.register("configuredMainClass") { + doLast { + println bootRun.mainClass + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/boot-run-main.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/boot-run-main.gradle.kts new file mode 100644 index 000000000000..2639f88e38b0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/boot-run-main.gradle.kts @@ -0,0 +1,18 @@ +import org.springframework.boot.gradle.tasks.run.BootRun + +plugins { + java + id("org.springframework.boot") version "{gradle-project-version}" +} + +// tag::main[] +tasks.named("bootRun") { + mainClass.set("com.example.ExampleApplication") +} +// end::main[] + +tasks.register("configuredMainClass") { + doLast { + println(tasks.getByName("bootRun").mainClass) + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/boot-run-source-resources.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/boot-run-source-resources.gradle new file mode 100644 index 000000000000..e09dac63f809 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/boot-run-source-resources.gradle @@ -0,0 +1,16 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +// tag::source-resources[] +tasks.named("bootRun") { + sourceResources sourceSets.main +} +// end::source-resources[] + +tasks.register("configuredClasspath") { + doLast { + println bootRun.classpath.files + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/boot-run-source-resources.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/boot-run-source-resources.gradle.kts new file mode 100644 index 000000000000..73a907b4f35c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/boot-run-source-resources.gradle.kts @@ -0,0 +1,18 @@ +import org.springframework.boot.gradle.tasks.run.BootRun + +plugins { + java + id("org.springframework.boot") version "{gradle-project-version}" +} + +// tag::source-resources[] +tasks.named("bootRun") { + sourceResources(sourceSets["main"]) +} +// end::source-resources[] + +tasks.register("configuredClasspath") { + doLast { + println(tasks.getByName("bootRun").classpath.files) + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/boot-run-system-property.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/boot-run-system-property.gradle new file mode 100644 index 000000000000..34e44d56469f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/boot-run-system-property.gradle @@ -0,0 +1,18 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +// tag::system-property[] +tasks.named("bootRun") { + systemProperty 'com.example.property', findProperty('example') ?: 'default' +} +// end::system-property[] + +tasks.register("configuredSystemProperties") { + doLast { + bootRun.systemProperties.each { k, v -> + println "$k = $v" + } + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/boot-run-system-property.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/boot-run-system-property.gradle.kts new file mode 100644 index 000000000000..71aab129cde3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/boot-run-system-property.gradle.kts @@ -0,0 +1,20 @@ +import org.springframework.boot.gradle.tasks.run.BootRun + +plugins { + java + id("org.springframework.boot") version "{version}" +} + +// tag::system-property[] +tasks.named("bootRun") { + systemProperty("com.example.property", findProperty("example") ?: "default") +} +// end::system-property[] + +tasks.register("configuredSystemProperties") { + doLast { + tasks.getByName("bootRun").systemProperties.forEach { k, v -> + println("$k = $v") + } + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/spring-boot-dsl-main-class-name.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/spring-boot-dsl-main-class-name.gradle new file mode 100644 index 000000000000..6703507d6d28 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/spring-boot-dsl-main-class-name.gradle @@ -0,0 +1,11 @@ +plugins { + id 'java' + id 'application' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +// tag::main-class[] +springBoot { + mainClass = 'com.example.ExampleApplication' +} +// end::main-class[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/spring-boot-dsl-main-class-name.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/spring-boot-dsl-main-class-name.gradle.kts new file mode 100644 index 000000000000..318eb9dfd792 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/running/spring-boot-dsl-main-class-name.gradle.kts @@ -0,0 +1,13 @@ +import org.springframework.boot.gradle.tasks.run.BootRun + +plugins { + java + application + id("org.springframework.boot") version "{gradle-project-version}" +} + +// tag::main-class[] +springBoot { + mainClass.set("com.example.ExampleApplication") +} +// end::main-class[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/asciidoc/css/style.css b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/asciidoc/css/style.css deleted file mode 100644 index edaad0a410ac..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/asciidoc/css/style.css +++ /dev/null @@ -1,20 +0,0 @@ -@import url("spring.css"); - -div .switch { - margin-left: 8px; - border-color: #406A2A; - border-radius: 4px 4px 0 0; -} - -div .switch--item { - color: #406A2A; - background-color: transparent; -} - -div .switch--item.selected { - background-color: #406A2A; -} - -div .switch--item:not(:first-child) { - border-color: #406A2A; -} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/asciidoc/getting-started.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/asciidoc/getting-started.adoc deleted file mode 100644 index 709dfe360d1c..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/asciidoc/getting-started.adoc +++ /dev/null @@ -1,102 +0,0 @@ -[[getting-started]] -== Getting started - -To get started with the plugin it needs to be applied to your project. - -ifeval::["{version-type}" == "RELEASE"] -The plugin is https://plugins.gradle.org/plugin/org.springframework.boot[published to Gradle's plugin portal] and can be applied using the `plugins` block: -[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] -.Groovy ----- -include::../gradle/getting-started/apply-plugin-release.gradle[] ----- - -[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] -.Kotlin ----- -include::../gradle/getting-started/apply-plugin-release.gradle.kts[] ----- -endif::[] -ifeval::["{version-type}" == "MILESTONE"] -The plugin is published to the Spring milestones repository. -Gradle can be configured to use the milestones repository and the plugin can then be applied using the `plugins` block. -To configure Gradle to use the milestones repository, add the following to your `settings.gradle` (Groovy) or `settings.gradle.kts` (Kotlin): - -[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] -.Groovy ----- -include::../gradle/getting-started/milestone-settings.gradle[] ----- - -[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] -.Kotlin ----- -include::../gradle/getting-started/milestone-settings.gradle.kts[] ----- - -The plugin can then be applied using the `plugins` block: - -[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] -.Groovy ----- -include::../gradle/getting-started/apply-plugin-release.gradle[] ----- - -[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] -.Kotlin ----- -include::../gradle/getting-started/apply-plugin-release.gradle.kts[] ----- -endif::[] -ifeval::["{version-type}" == "SNAPSHOT"] -The plugin is published to the Spring snapshots repository. -Gradle can be configured to use the snapshots repository and the plugin can then be applied using the `plugins` block. -To configure Gradle to use the snapshots repository, add the following to your `settings.gradle` (Groovy) or `settings.gradle.kts` (Kotlin): - -[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] -.Groovy ----- -include::../gradle/getting-started/snapshot-settings.gradle[] ----- - -[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] -.Kotlin ----- -include::../gradle/getting-started/snapshot-settings.gradle.kts[] ----- - -The plugin can then be applied using the `plugins` block: - -[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] -.Groovy ----- -include::../gradle/getting-started/apply-plugin-release.gradle[] ----- - -[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] -.Kotlin ----- -include::../gradle/getting-started/apply-plugin-release.gradle.kts[] ----- -endif::[] - -Applied in isolation the plugin makes few changes to a project. -Instead, the plugin detects when certain other plugins are applied and reacts accordingly. -For example, when the `java` plugin is applied a task for building an executable jar is automatically configured. -A typical Spring Boot project will apply the {groovy-plugin}[`groovy`], {java-plugin}java_plugin.html[`java`], or {kotlin-plugin}[`org.jetbrains.kotlin.jvm`] plugin and the {dependency-management-plugin}[`io.spring.dependency-management`] plugin as a minimum. -For example: - - -[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] -.Groovy ----- -include::../gradle/getting-started/typical-plugins.gradle[tags=apply] ----- - -[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] -.Kotlin ----- -include::../gradle/getting-started/typical-plugins.gradle.kts[tags=apply] ----- - -To learn more about how the Spring Boot plugin behaves when other plugins are applied please see the section on <>. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/asciidoc/index.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/asciidoc/index.adoc deleted file mode 100644 index 7f0b02c34fcb..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/asciidoc/index.adoc +++ /dev/null @@ -1,51 +0,0 @@ -= Spring Boot Gradle Plugin Reference Guide -Andy Wilkinson -:doctype: book -:toc: left -:toclevels: 4 -:source-highlighter: prettify -:numbered: -:icons: font -:hide-uri-scheme: -:docinfo: shared,private - -:dependency-management-plugin: https://github.com/spring-gradle-plugins/dependency-management-plugin -:dependency-management-plugin-documentation: {dependency-management-plugin}/blob/master/README.md -:gradle-userguide: https://docs.gradle.org/current/userguide -:gradle-dsl: https://docs.gradle.org/current/dsl -:gradle-api: https://docs.gradle.org/current/javadoc -:application-plugin: {gradle-userguide}/application_plugin.html -:groovy-plugin: {gradle-userguide}/groovy_plugin.html -:java-plugin: {gradle-userguide}/java_plugin.html -:war-plugin: {gradle-userguide}/war_plugin.html -:maven-plugin: {gradle-userguide}/maven_plugin.html -:maven-publish-plugin: {gradle-userguide}/maven_publish_plugin.html -:software-component: {gradle-userguide}/software_model_extend.html -:kotlin-plugin: https://kotlinlang.org/docs/reference/using-gradle.html -:spring-boot-docs: https://docs.spring.io/spring-boot/docs/{version} -:api-documentation: {spring-boot-docs}/gradle-plugin/api -:spring-boot-reference: {spring-boot-docs}/reference/htmlsingle -:build-info-javadoc: {api-documentation}/org/springframework/boot/gradle/tasks/buildinfo/BuildInfo.html -:boot-jar-javadoc: {api-documentation}/org/springframework/boot/gradle/tasks/bundling/BootJar.html -:boot-war-javadoc: {api-documentation}/org/springframework/boot/gradle/tasks/bundling/BootWar.html -:boot-run-javadoc: {api-documentation}/org/springframework/boot/gradle/tasks/run/BootRun.html -:github-code: https://github.com/spring-projects/spring-boot/tree/{github-tag} - - - -[[introduction]] -== Introduction - -The Spring Boot Gradle Plugin provides Spring Boot support in https://gradle.org[Gradle]. -It allows you to package executable jar or war archives, run Spring Boot applications, and use the dependency management provided by `spring-boot-dependencies`. -Spring Boot's Gradle plugin requires Gradle 5.x (4.10 is also supported but this support is deprecated and will be removed in a future release). - -In addition to this user guide, {api-documentation}[API documentation] is also available. - -include::getting-started.adoc[] -include::managing-dependencies.adoc[] -include::packaging.adoc[] -include::publishing.adoc[] -include::running.adoc[] -include::integrating-with-actuator.adoc[] -include::reacting.adoc[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/asciidoc/managing-dependencies.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/asciidoc/managing-dependencies.adoc deleted file mode 100644 index 9dd99322e678..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/asciidoc/managing-dependencies.adoc +++ /dev/null @@ -1,128 +0,0 @@ -[[managing-dependencies]] -== Managing dependencies - -When you apply the {dependency-management-plugin}[`io.spring.dependency-management`] plugin, Spring Boot's plugin will automatically <> from the version of Spring Boot that you are using. -This provides a similar dependency management experience to the one that's enjoyed by Maven users. -For example, it allows you to omit version numbers when declaring dependencies that are managed in the bom. -To make use of this functionality, simply declare dependencies in the usual way but omit the version number: - -[source,groovy,indent=0,subs="verbatim",role="primary"] -.Groovy ----- -include::../gradle/managing-dependencies/dependencies.gradle[tags=dependencies] ----- - -[source,kotlin,indent=0,subs="verbatim",role="secondary"] -.Kotlin ----- -include::../gradle/managing-dependencies/dependencies.gradle.kts[tags=dependencies] ----- - - -[[managing-dependencies-customizing]] -=== Customizing managed versions - -The `spring-boot-dependencies` bom that is automatically imported when the dependency management plugin is applied uses properties to control the versions of the dependencies that it manages. -Please refer to the {github-code}/spring-boot-project/spring-boot-dependencies/pom.xml[bom] for a complete list of these properties. - -To customize a managed version you set its corresponding property. -For example, to customize the version of SLF4J which is controlled by the `slf4j.version` property: - -[source,groovy,indent=0,subs="verbatim",role="primary"] -.Groovy ----- -include::../gradle/managing-dependencies/custom-version.gradle[tags=custom-version] ----- - -[source,kotlin,indent=0,subs="verbatim",role="secondary"] -.Kotlin ----- -include::../gradle/managing-dependencies/custom-version.gradle.kts[tags=custom-version] ----- - - -WARNING: Each Spring Boot release is designed and tested against a specific set of third-party dependencies. -Overriding versions may cause compatibility issues and should be done with care. - - - -[[managing-dependencies-using-in-isolation]] -=== Using Spring Boot's dependency management in isolation - -Spring Boot's dependency management can be used in a project without applying Spring Boot's plugin to that project. -The `SpringBootPlugin` class provides a `BOM_COORDINATES` constant that can be used to import the bom without having to know its group ID, artifact ID, or version. - -First, configure the project to depend on the Spring Boot plugin but do not apply it: - -ifeval::["{version-type}" == "RELEASE"] -[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] -.Groovy ----- -include::../gradle/managing-dependencies/depend-on-plugin-release.gradle[] ----- - -[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] -.Kotlin ----- -include::../gradle/managing-dependencies/depend-on-plugin-release.gradle.kts[] ----- -endif::[] -ifeval::["{version-type}" == "MILESTONE"] -[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] -.Groovy ----- -include::../gradle/managing-dependencies/depend-on-plugin-milestone.gradle[] ----- -[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] -.Kotlin ----- -include::../gradle/managing-dependencies/depend-on-plugin-release.gradle.kts[] ----- -endif::[] -ifeval::["{version-type}" == "SNAPSHOT"] -[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] -.Groovy ----- -include::../gradle/managing-dependencies/depend-on-plugin-snapshot.gradle[] ----- -[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] -.Kotlin ----- -include::../gradle/managing-dependencies/depend-on-plugin-release.gradle.kts[] ----- -endif::[] - -The Spring Boot plugin's dependency on the dependency management plugin means that you can use the dependency management plugin without having to declare a dependency on it. -This also means that you will automatically use the same version of the dependency management plugin as Spring Boot uses. - -Apply the dependency management plugin and then configure it to import Spring Boot's bom: - -[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] -.Groovy ----- -include::../gradle/managing-dependencies/configure-bom.gradle[tags=configure-bom] ----- - -[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] -.Kotlin ----- -include::../gradle/managing-dependencies/configure-bom.gradle.kts[tags=configure-bom] ----- - - -The Kotlin code above is a bit awkward. -That's because we're using the imperative way of applying the dependency management plugin. - -We can make the code less awkward by applying the plugin from the root parent project, or by using the `plugins` block as we're doing for the Spring Boot plugin. -A downside of this method is that it forces us to specify the version of the dependency management plugin: - -[source,kotlin,indent=0,subs="verbatim,attributes"] ----- -include::../gradle/managing-dependencies/configure-bom-with-plugins.gradle.kts[tags=configure-bom] ----- - - -[[managing-dependencies-learning-more]] -=== Learning more - -To learn more about the capabilities of the dependency management plugin, please refer to its {dependency-management-plugin-documentation}[documentation]. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/asciidoc/packaging.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/asciidoc/packaging.adoc deleted file mode 100644 index 8a76e0a512d8..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/asciidoc/packaging.adoc +++ /dev/null @@ -1,274 +0,0 @@ -[[packaging-executable]] -== Packaging executable archives - -The plugin can create executable archives (jar files and war files) that contain all of an application's dependencies and can then be run with `java -jar`. - - - -[[packaging-executable-jars]] -=== Packaging executable jars - -Executable jars can be built using the `bootJar` task. -The task is automatically created when the `java` plugin is applied and is an instance of {boot-jar-javadoc}[`BootJar`]. -The `assemble` task is automatically configured to depend upon the `bootJar` task so running `assemble` (or `build`) will also run the `bootJar` task. - - - -[[packaging-executable-wars]] -=== Packaging executable wars - -Executable wars can be built using the `bootWar` task. -The task is automatically created when the `war` plugin is applied and is an instance of {boot-war-javadoc}[`BootWar`]. -The `assemble` task is automatically configured to depend upon the `bootWar` task so running `assemble` (or `build`) will also run the `bootWar` task. - - - -[[packaging-executable-wars-deployable]] -==== Packaging executable and deployable wars - -A war file can be packaged such that it can be executed using `java -jar` and deployed to an external container. -To do so, the embedded servlet container dependencies should be added to the `providedRuntime` configuration, for example: - -[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] -.Groovy ----- -include::../gradle/packaging/war-container-dependency.gradle[tags=dependencies] ----- - -[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] -.Kotlin ----- -include::../gradle/packaging/war-container-dependency.gradle.kts[tags=dependencies] ----- - - -This ensures that they are package in the war file's `WEB-INF/lib-provided` directory from where they will not conflict with the external container's own classes. - -NOTE: `providedRuntime` is preferred to Gradle's `compileOnly` configuration as, among other limitations, `compileOnly` dependencies are not on the test classpath so any web-based integration tests will fail. - - - -[[packaging-executable-and-normal]] -=== Packaging executable and normal archives - -By default, when the `bootJar` or `bootWar` tasks are configured, the `jar` or `war` tasks are disabled. -A project can be configured to build both an executable archive and a normal archive at the same time by enabling the `jar` or `war` task: - -[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] -.Groovy ----- -include::../gradle/packaging/boot-jar-and-jar.gradle[tags=enable-jar] ----- - -[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] -.Kotlin ----- -include::../gradle/packaging/boot-jar-and-jar.gradle.kts[tags=enable-jar] ----- - - -To avoid the executable archive and the normal archive from being written to the same location, one or the other should be configured to use a different location. -One way to do so is by configuring a classifier: - -[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] -.Groovy ----- -include::../gradle/packaging/boot-jar-and-jar.gradle[tags=classifier] ----- - -[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] -.Kotlin ----- -include::../gradle/packaging/boot-jar-and-jar.gradle.kts[tags=classifier] ----- - - -[[packaging-executable-configuring]] -=== Configuring executable archive packaging - -The {boot-jar-javadoc}[`BootJar`] and {boot-war-javadoc}[`BootWar`] tasks are subclasses of Gradle's `Jar` and `War` tasks respectively. -As a result, all of the standard configuration options that are available when packaging a jar or war are also available when packaging an executable jar or war. -A number of configuration options that are specific to executable jars and wars are also provided. - - -[[packaging-executable-configuring-main-class]] -==== Configuring the main class - -By default, the executable archive's main class will be configured automatically by looking for a class with a `public static void main(String[])` method in directories on the task's classpath. - -The main class can also be configured explicitly using the task's `mainClassName` property: - -[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] -.Groovy ----- -include::../gradle/packaging/boot-jar-main-class.gradle[tags=main-class] ----- - -[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] -.Kotlin ----- -include::../gradle/packaging/boot-jar-main-class.gradle.kts[tags=main-class] ----- - - -Alternatively, the main class name can be configured project-wide using the `mainClassName` property of the Spring Boot DSL: - -[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] -.Groovy ----- -include::../gradle/packaging/spring-boot-dsl-main-class.gradle[tags=main-class] ----- - -[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] -.Kotlin ----- -include::../gradle/packaging/spring-boot-dsl-main-class.gradle.kts[tags=main-class] ----- - - -If the {application-plugin}[`application` plugin] has been applied its `mainClassName` project property must be configured and can be used for the same purpose: - -[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] -.Groovy ----- -include::../gradle/packaging/application-plugin-main-class.gradle[tags=main-class] ----- - -[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] -.Kotlin ----- -include::../gradle/packaging/application-plugin-main-class.gradle.kts[tags=main-class] ----- - -Lastly, the `Start-Class` attribute can be configured on the task's manifest: - -[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] -.Groovy ----- -include::../gradle/packaging/boot-jar-manifest-main-class.gradle[tags=main-class] ----- - -[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] -.Kotlin ----- -include::../gradle/packaging/boot-jar-manifest-main-class.gradle.kts[tags=main-class] ----- - - -[[packaging-executable-configuring-excluding-devtools]] -==== Excluding Devtools - -By default, Spring Boot's Devtools module, `org.springframework.boot:spring-boot-devtools`, will be excluded from an executable jar or war. -If you want to include Devtools in your archive set the `excludeDevtools` property to `false`: - -[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] -.Groovy ----- -include::../gradle/packaging/boot-war-include-devtools.gradle[tags=include-devtools] ----- - -[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] -.Kotlin ----- -include::../gradle/packaging/boot-war-include-devtools.gradle.kts[tags=include-devtools] ----- - - -[[packaging-executable-configuring-unpacking]] -==== Configuring libraries that require unpacking - -Most libraries can be used directly when nested in an executable archive, however certain libraries can have problems. -For example, JRuby includes its own nested jar support which assumes that `jruby-complete.jar` is always directly available on the file system. - -To deal with any problematic libraries, an executable archive can be configured to unpack specific nested jars to a temporary folder when the executable archive is run. -Libraries can be identified as requiring unpacking using Ant-style patterns that match against the absolute path of the source jar file: - -[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] -.Groovy ----- -include::../gradle/packaging/boot-jar-requires-unpack.gradle[tags=requires-unpack] ----- - -[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] -.Kotlin ----- -include::../gradle/packaging/boot-jar-requires-unpack.gradle.kts[tags=requires-unpack] ----- - - -For more control a closure can also be used. -The closure is passed a `FileTreeElement` and should return a `boolean` indicating whether or not unpacking is required. - - - -[[packaging-executable-configuring-launch-script]] -==== Making an archive fully executable - -Spring Boot provides support for fully executable archives. -An archive is made fully executable by prepending a shell script that knows how to launch the application. -On Unix-like platforms, this launch script allows the archive to be run directly like any other executable or to be installed as a service. - -To use this feature, the inclusion of the launch script must be enabled: - -[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] -.Groovy ----- -include::../gradle/packaging/boot-jar-include-launch-script.gradle[tags=include-launch-script] ----- - -[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] -.Kotlin ----- -include::../gradle/packaging/boot-jar-include-launch-script.gradle.kts[tags=include-launch-script] ----- - - -This will add Spring Boot's default launch script to the archive. -The default launch script includes several properties with sensible default values. -The values can be customized using the `properties` property: - -[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] -.Groovy ----- -include::../gradle/packaging/boot-jar-launch-script-properties.gradle[tags=launch-script-properties] ----- - -[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] -.Kotlin ----- -include::../gradle/packaging/boot-jar-launch-script-properties.gradle.kts[tags=launch-script-properties] ----- - - -If the default launch script does not meet your needs, the `script` property can be used to provide a custom launch script: - -[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] -.Groovy ----- -include::../gradle/packaging/boot-jar-custom-launch-script.gradle[tags=custom-launch-script] ----- - -[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] -.Kotlin ----- -include::../gradle/packaging/boot-jar-custom-launch-script.gradle.kts[tags=custom-launch-script] ----- - - -[[packaging-executable-configuring-properties-launcher]] -==== Using the `PropertiesLauncher` - -To use the `PropertiesLauncher` to launch an executable jar or war, configure the task's manifest to set the `Main-Class` attribute: - -[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] -.Groovy ----- -include::../gradle/packaging/boot-war-properties-launcher.gradle[tags=properties-launcher] ----- - -[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] -.Kotlin ----- -include::../gradle/packaging/boot-war-properties-launcher.gradle.kts[tags=properties-launcher] ----- - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/asciidoc/publishing.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/asciidoc/publishing.adoc deleted file mode 100644 index 0b78cf3f1802..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/asciidoc/publishing.adoc +++ /dev/null @@ -1,52 +0,0 @@ -[[publishing-your-application]] -== Publishing your application - - - -[[publishing-your-application-maven]] -=== Publishing with the `maven` plugin - -When the {maven-plugin}[`maven` plugin] is applied, an `Upload` task for the `bootArchives` configuration named `uploadBootArchives` is automatically created. -By default, the `bootArchives` configuration contains the archive produced by the `bootJar` or `bootWar` task. -The `uploadBootArchives` task can be configured to publish the archive to a Maven repository: - -[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] -.Groovy ----- -include::../gradle/publishing/maven.gradle[tags=upload] ----- - -[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] -.Kotlin ----- -include::../gradle/publishing/maven.gradle.kts[tags=upload] ----- - - -[[publishing-your-application-maven-publish]] -=== Publishing with the `maven-publish` plugin - -To publish your Spring Boot jar or war, add it to the publication using the `artifact` method on `MavenPublication`. -Pass the task that produces that artifact that you wish to publish to the `artifact` method. -For example, to publish the artifact produced by the default `bootJar` task: - -[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] -.Groovy ----- -include::../gradle/publishing/maven-publish.gradle[tags=publishing] ----- - -[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] -.Kotlin ----- -include::../gradle/publishing/maven-publish.gradle.kts[tags=publishing] ----- - - -[[publishing-your-application-distribution]] -=== Distributing with the `application` plugin - -When the {application-plugin}[`application` plugin] is applied a distribution named `boot` is created. -This distribution contains the archive produced by the `bootJar` or `bootWar` task and scripts to launch it on Unix-like platforms and Windows. -Zip and tar distributions can be built by the `bootDistZip` and `bootDistTar` tasks respectively. -To use the `application` plugin, its `mainClassName` project property must be configured with the name of your application's main class. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/asciidoc/reacting.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/asciidoc/reacting.adoc deleted file mode 100644 index 0bd78bf0cb73..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/asciidoc/reacting.adoc +++ /dev/null @@ -1,74 +0,0 @@ -[[reacting-to-other-plugins]] -== Reacting to other plugins - -When another plugin is applied the Spring Boot plugin reacts by making various changes to the project's configuration. -This section describes those changes. - - - -[[reacting-to-other-plugins-java]] -=== Reacting to the Java plugin - -When Gradle's {java-plugin}[`java` plugin] is applied to a project, the Spring Boot plugin: - -1. Creates a {boot-jar-javadoc}[`BootJar`] task named `bootJar` that will create an executable, fat jar for the project. - The jar will contain everything on the runtime classpath of the main source set; classes are packaged in `BOOT-INF/classes` and jars are packaged in `BOOT-INF/lib` -2. Configures the `assemble` task to depend on the `bootJar` task. -3. Disables the `jar` task. -4. Creates a {boot-run-javadoc}[`BootRun`] task named `bootRun` that can be used to run your application. -5. Creates a configuration named `bootArchives` that contains the artifact produced by the `bootJar` task. -6. Configures any `JavaCompile` tasks with no configured encoding to use `UTF-8`. -7. Configures any `JavaCompile` tasks to use the `-parameters` compiler argument. - - - -[[reacting-to-other-plugins-kotlin]] -=== Reacting to the Kotlin plugin - -When {kotlin-plugin}[Kotlin's Gradle plugin] is applied to a project, the Spring Boot plugin: - -1. Aligns the Kotlin version used in Spring Boot's dependency management with the version of the plugin. - This is achieved by setting the `kotlin.version` property with a value that matches the version of the Kotlin plugin. -2. Configures any `KotlinCompile` tasks to use the `-java-parameters` compiler argument. - - - -[[reacting-to-other-plugins-war]] -=== Reacting to the war plugin - -When Gradle's {war-plugin}[`war` plugin] is applied to a project, the Spring Boot plugin: - -1. Creates a {boot-war-javadoc}[`BootWar`] task named `bootWar` that will create an executable, fat war for the project. - In addition to the standard packaging, everything in the `providedRuntime` configuration will be packaged in `WEB-INF/lib-provided`. -2. Configures the `assemble` task to depend on the `bootWar` task. -3. Disables the `war` task. -4. Configures the `bootArchives` configuration to contain the artifact produced by the `bootWar` task. - - - -[[reacting-to-other-plugins-dependency-management]] -=== Reacting to the dependency management plugin - -When the {dependency-management-plugin}[`io.spring.dependency-management` plugin] is applied to a project, the Spring Boot plugin will automatically import the `spring-boot-dependencies` bom. - - - -[[reacting-to-other-plugins-application]] -=== Reacting to the application plugin - -When Gradle's {application-plugin}[`application` plugin] is applied to a project, the Spring Boot plugin: - -1. Creates a `CreateStartScripts` task named `bootStartScripts` that will create scripts that launch the artifact in the `bootArchives` configuration using `java -jar`. - The task is configured to use the `applicationDefaultJvmArgs` property as a convention for its `defaultJvmOpts` property. -2. Creates a new distribution named `boot` and configures it to contain the artifact in the `bootArchives` configuration in its `lib` directory and the start scripts in its `bin` directory. -3. Configures the `bootRun` task to use the `mainClassName` property as a convention for its `main` property. -4. Configures the `bootRun` task to use the `applicationDefaultJvmArgs` property as a convention for its `jvmArgs` property. -5. Configures the `bootJar` task to use the `mainClassName` property as a convention for the `Start-Class` entry in its manifest. -6. Configures the `bootWar` task to use the `mainClassName` property as a convention for the `Start-Class` entry in its manifest. - - - -[[reacting-to-other-plugins-maven]] -=== Reacting to the Maven plugin - -When Gradle's {maven-plugin}[`maven` plugin] is applied to a project, the Spring Boot plugin will configure the `uploadBootArchives` `Upload` task to ensure that no dependencies are declared in the pom that it generates. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/asciidoc/running.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/asciidoc/running.adoc deleted file mode 100644 index 868b901ceeb3..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/asciidoc/running.adoc +++ /dev/null @@ -1,110 +0,0 @@ -[[running-your-application]] -== Running your application with Gradle - -To run your application without first building an archive use the `bootRun` task: - -[source,bash,indent=0,subs="verbatim"] ----- - $ ./gradlew bootRun ----- - -The `bootRun` task is an instance of {boot-run-javadoc}[`BootRun`] which is a `JavaExec` subclass. -As such, all of the {gradle-dsl}/org.gradle.api.tasks.JavaExec.html[usual configuration options] for executing a Java process in Gradle are available to you. -The task is automatically configured to use the runtime classpath of the main source set. - -By default, the main class will be configured automatically by looking for a class with a `public static void main(String[])` method in directories on the task's classpath. - -The main class can also be configured explicitly using the task's `main` property: - -[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] -.Groovy ----- -include::../gradle/running/boot-run-main.gradle[tags=main] ----- - -[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] -.Kotlin ----- -include::../gradle/running/boot-run-main.gradle.kts[tags=main] ----- - - -Alternatively, the main class name can be configured project-wide using the `mainClassName` property of the Spring Boot DSL: - -[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] -.Groovy ----- -include::../gradle/running/spring-boot-dsl-main-class-name.gradle[tags=main-class] ----- - -[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] -.Kotlin ----- -include::../gradle/running/spring-boot-dsl-main-class-name.gradle.kts[tags=main-class] ----- - - -By default, `bootRun` will configure the JVM to optimize its launch for faster startup during development. -This behavior can be disabled by using the `optimizedLaunch` property, as shown in the following example: - -[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] -.Groovy ----- -include::../gradle/running/boot-run-disable-optimized-launch.gradle[tags=launch] ----- - -[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] -.Kotlin ----- -include::../gradle/running/boot-run-disable-optimized-launch.gradle.kts[tags=launch] ----- - - -If the {application-plugin}[`application` plugin] has been applied, its `mainClassName` project property must be configured and can be used for the same purpose: - -[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] -.Groovy ----- -include::../gradle/running/application-plugin-main-class-name.gradle[tags=main-class] ----- - -[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] -.Kotlin ----- -include::../gradle/running/application-plugin-main-class-name.gradle.kts[tags=main-class] ----- - - -[[running-your-application-passing-arguments]] -=== Passing arguments to your application -Like all `JavaExec` tasks, arguments can be passed into `bootRun` from the command line using `--args=''` when using Gradle 4.9 or later. -For example, to run your application with a profile named `dev` active the following command can be used: - -[source,bash,indent=0,subs="verbatim"] ----- - $ ./gradlew bootRun --args='--spring.profiles.active=dev' ----- - -See {gradle-api}/org/gradle/api/tasks/JavaExec.html#setArgsString-java.lang.String-[the javadoc for `JavaExec.setArgsString`] for further details. - - - -[[running-your-application-reloading-resources]] -=== Reloading resources -If devtools has been added to your project it will automatically monitor your application for changes. -Alternatively, you can configure `bootRun` such that your application's static resources are loaded from their source location: - -[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] -.Groovy ----- -include::../gradle/running/boot-run-source-resources.gradle[tags=source-resources] ----- - -[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] -.Kotlin ----- -include::../gradle/running/boot-run-source-resources.gradle.kts[tags=source-resources] ----- - - -This makes them reloadable in the live application which can be helpful at development time. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/getting-started/apply-plugin-release.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/getting-started/apply-plugin-release.gradle deleted file mode 100644 index cf13509ffddd..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/getting-started/apply-plugin-release.gradle +++ /dev/null @@ -1,3 +0,0 @@ -plugins { - id 'org.springframework.boot' version '{version}' -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/getting-started/apply-plugin-release.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/getting-started/apply-plugin-release.gradle.kts deleted file mode 100644 index 5f006ab6d2e3..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/getting-started/apply-plugin-release.gradle.kts +++ /dev/null @@ -1,3 +0,0 @@ -plugins { - id("org.springframework.boot") version "{version}" -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/getting-started/typical-plugins.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/getting-started/typical-plugins.gradle.kts deleted file mode 100644 index c36c96646616..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/getting-started/typical-plugins.gradle.kts +++ /dev/null @@ -1,15 +0,0 @@ -// tag::apply[] -plugins { - java - id("org.springframework.boot") version "{version}" -} - -apply(plugin = "io.spring.dependency-management") -// end::apply[] - -task("verify") { - doLast { - project.plugins.getPlugin(JavaPlugin::class) - project.plugins.getPlugin(io.spring.gradle.dependencymanagement.DependencyManagementPlugin::class) - } -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/integrating-with-actuator/build-info-additional.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/integrating-with-actuator/build-info-additional.gradle deleted file mode 100644 index 229b16459133..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/integrating-with-actuator/build-info-additional.gradle +++ /dev/null @@ -1,17 +0,0 @@ -plugins { - id 'java' - id 'org.springframework.boot' version '{version}' -} - -// tag::additional[] -springBoot { - buildInfo { - properties { - additional = [ - 'a': 'alpha', - 'b': 'bravo' - ] - } - } -} -// end::additional[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/integrating-with-actuator/build-info-additional.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/integrating-with-actuator/build-info-additional.gradle.kts deleted file mode 100644 index 3010287baa3e..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/integrating-with-actuator/build-info-additional.gradle.kts +++ /dev/null @@ -1,18 +0,0 @@ -plugins { - java - id("org.springframework.boot") version "{version}" -} - -// tag::additional[] -springBoot { - buildInfo { - properties { - additional = mapOf( - "a" to "alpha", - "b" to "bravo" - ) - } - } -} -// end::additional[] - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/integrating-with-actuator/build-info-basic.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/integrating-with-actuator/build-info-basic.gradle deleted file mode 100644 index ffa081cbdbcd..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/integrating-with-actuator/build-info-basic.gradle +++ /dev/null @@ -1,10 +0,0 @@ -plugins { - id 'java' - id 'org.springframework.boot' version '{version}' -} - -// tag::build-info[] -springBoot { - buildInfo() -} -// end::build-info[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/integrating-with-actuator/build-info-basic.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/integrating-with-actuator/build-info-basic.gradle.kts deleted file mode 100644 index 73741e41c902..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/integrating-with-actuator/build-info-basic.gradle.kts +++ /dev/null @@ -1,10 +0,0 @@ -plugins { - java - id("org.springframework.boot") version "{version}" -} - -// tag::build-info[] -springBoot { - buildInfo() -} -// end::build-info[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/managing-dependencies/depend-on-plugin-release.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/managing-dependencies/depend-on-plugin-release.gradle deleted file mode 100644 index 9bde7c2d3fa6..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/managing-dependencies/depend-on-plugin-release.gradle +++ /dev/null @@ -1,3 +0,0 @@ -plugins { - id 'org.springframework.boot' version '{version}' apply false -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/managing-dependencies/depend-on-plugin-release.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/managing-dependencies/depend-on-plugin-release.gradle.kts deleted file mode 100644 index b8acfc1f5adb..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/managing-dependencies/depend-on-plugin-release.gradle.kts +++ /dev/null @@ -1,3 +0,0 @@ -plugins { - id("org.springframework.boot") version "{version}" apply false -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/managing-dependencies/dependencies.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/managing-dependencies/dependencies.gradle deleted file mode 100644 index 44ff92d1ad1e..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/managing-dependencies/dependencies.gradle +++ /dev/null @@ -1,13 +0,0 @@ -plugins { - id 'java' - id 'org.springframework.boot' version '{version}' -} - -apply plugin: 'io.spring.dependency-management' - -// tag::dependencies[] -dependencies { - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' -} -// end::dependencies[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/application-plugin-main-class.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/application-plugin-main-class.gradle deleted file mode 100644 index 992486aeae6d..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/application-plugin-main-class.gradle +++ /dev/null @@ -1,9 +0,0 @@ -plugins { - id 'java' - id 'application' - id 'org.springframework.boot' version '{version}' -} - -// tag::main-class[] -mainClassName = 'com.example.ExampleApplication' -// end::main-class[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/application-plugin-main-class.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/application-plugin-main-class.gradle.kts deleted file mode 100644 index d5d442769e46..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/application-plugin-main-class.gradle.kts +++ /dev/null @@ -1,11 +0,0 @@ -plugins { - java - application - id("org.springframework.boot") version "{version}" -} - -// tag::main-class[] -application { - mainClassName = "com.example.ExampleApplication" -} -// end::main-class[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-and-jar.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-and-jar.gradle deleted file mode 100644 index 17345aae90bf..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-and-jar.gradle +++ /dev/null @@ -1,20 +0,0 @@ -plugins { - id 'java' - id 'org.springframework.boot' version '{version}' -} - -// tag::enable-jar[] -jar { - enabled = true -} -// end::enable-jar[] - -// tag::classifier[] -bootJar { - classifier = 'boot' -} -// end::classifier[] - -bootJar { - mainClassName = 'com.example.Application' -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-and-jar.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-and-jar.gradle.kts deleted file mode 100644 index 50291ab48886..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-and-jar.gradle.kts +++ /dev/null @@ -1,22 +0,0 @@ -import org.springframework.boot.gradle.tasks.bundling.BootJar - -plugins { - java - id("org.springframework.boot") version "{version}" -} - -// tag::enable-jar[] -tasks.getByName("jar") { - enabled = true -} -// end::enable-jar[] - -// tag::classifier[] -tasks.getByName("bootJar") { - classifier = "boot" -} -// end::classifier[] - -tasks.getByName("bootJar") { - mainClassName = "com.example.Application" -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-custom-launch-script.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-custom-launch-script.gradle deleted file mode 100644 index b2f0c04e2ce2..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-custom-launch-script.gradle +++ /dev/null @@ -1,16 +0,0 @@ -plugins { - id 'java' - id 'org.springframework.boot' version '{version}' -} - -bootJar { - mainClassName 'com.example.ExampleApplication' -} - -// tag::custom-launch-script[] -bootJar { - launchScript { - script = file('src/custom.script') - } -} -// end::custom-launch-script[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-custom-launch-script.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-custom-launch-script.gradle.kts deleted file mode 100644 index 9e20e7fa15f2..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-custom-launch-script.gradle.kts +++ /dev/null @@ -1,18 +0,0 @@ -import org.springframework.boot.gradle.tasks.bundling.BootJar - -plugins { - java - id("org.springframework.boot") version "{version}" -} - -tasks.getByName("bootJar") { - mainClassName = "com.example.ExampleApplication" -} - -// tag::custom-launch-script[] -tasks.getByName("bootJar") { - launchScript { - script = file("src/custom.script") - } -} -// end::custom-launch-script[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-include-launch-script.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-include-launch-script.gradle deleted file mode 100644 index fb503be90011..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-include-launch-script.gradle +++ /dev/null @@ -1,14 +0,0 @@ -plugins { - id 'java' - id 'org.springframework.boot' version '{version}' -} - -bootJar { - mainClassName 'com.example.ExampleApplication' -} - -// tag::include-launch-script[] -bootJar { - launchScript() -} -// end::include-launch-script[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-include-launch-script.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-include-launch-script.gradle.kts deleted file mode 100644 index 4a7306e08957..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-include-launch-script.gradle.kts +++ /dev/null @@ -1,16 +0,0 @@ -import org.springframework.boot.gradle.tasks.bundling.BootJar - -plugins { - java - id("org.springframework.boot") version "{version}" -} - -tasks.getByName("bootJar") { - mainClassName = "com.example.ExampleApplication" -} - -// tag::include-launch-script[] -tasks.getByName("bootJar") { - launchScript() -} -// end::include-launch-script[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-launch-script-properties.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-launch-script-properties.gradle deleted file mode 100644 index 09dc1b3c168a..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-launch-script-properties.gradle +++ /dev/null @@ -1,16 +0,0 @@ -plugins { - id 'java' - id 'org.springframework.boot' version '{version}' -} - -bootJar { - mainClassName 'com.example.ExampleApplication' -} - -// tag::launch-script-properties[] -bootJar { - launchScript { - properties 'logFilename': 'example-app.log' - } -} -// end::launch-script-properties[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-launch-script-properties.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-launch-script-properties.gradle.kts deleted file mode 100644 index a389cf44df69..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-launch-script-properties.gradle.kts +++ /dev/null @@ -1,18 +0,0 @@ -import org.springframework.boot.gradle.tasks.bundling.BootJar - -plugins { - java - id("org.springframework.boot") version "{version}" -} - -tasks.getByName("bootJar") { - mainClassName = "com.example.ExampleApplication" -} - -// tag::launch-script-properties[] -tasks.getByName("bootJar") { - launchScript { - properties(mapOf("logFilename" to "example-app.log")) - } -} -// end::launch-script-properties[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-main-class.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-main-class.gradle deleted file mode 100644 index 09a972481f1c..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-main-class.gradle +++ /dev/null @@ -1,10 +0,0 @@ -plugins { - id 'java' - id 'org.springframework.boot' version '{version}' -} - -// tag::main-class[] -bootJar { - mainClassName = 'com.example.ExampleApplication' -} -// end::main-class[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-main-class.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-main-class.gradle.kts deleted file mode 100644 index 306d8774bd3d..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-main-class.gradle.kts +++ /dev/null @@ -1,12 +0,0 @@ -import org.springframework.boot.gradle.tasks.bundling.BootJar - -plugins { - java - id("org.springframework.boot") version "{version}" -} - -// tag::main-class[] -tasks.getByName("bootJar") { - mainClassName = "com.example.ExampleApplication" -} -// end::main-class[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-manifest-main-class.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-manifest-main-class.gradle deleted file mode 100644 index 1e983b5f0b19..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-manifest-main-class.gradle +++ /dev/null @@ -1,12 +0,0 @@ -plugins { - id 'java' - id 'org.springframework.boot' version '{version}' -} - -// tag::main-class[] -bootJar { - manifest { - attributes 'Start-Class': 'com.example.ExampleApplication' - } -} -// end::main-class[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-manifest-main-class.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-manifest-main-class.gradle.kts deleted file mode 100644 index b0f704004621..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-manifest-main-class.gradle.kts +++ /dev/null @@ -1,14 +0,0 @@ -import org.springframework.boot.gradle.tasks.bundling.BootJar - -plugins { - java - id("org.springframework.boot") version "{version}" -} - -// tag::main-class[] -tasks.getByName("bootJar") { - manifest { - attributes("Start-Class" to "com.example.ExampleApplication") - } -} -// end::main-class[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-requires-unpack.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-requires-unpack.gradle deleted file mode 100644 index 683cdca4b881..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-requires-unpack.gradle +++ /dev/null @@ -1,22 +0,0 @@ -plugins { - id 'java' - id 'org.springframework.boot' version '{version}' -} - -repositories { - mavenCentral() -} - -dependencies { - runtimeOnly 'org.jruby:jruby-complete:1.7.25' -} - -bootJar { - mainClassName 'com.example.ExampleApplication' -} - -// tag::requires-unpack[] -bootJar { - requiresUnpack '**/jruby-complete-*.jar' -} -// end::requires-unpack[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-requires-unpack.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-requires-unpack.gradle.kts deleted file mode 100644 index 32e238e010b4..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-jar-requires-unpack.gradle.kts +++ /dev/null @@ -1,24 +0,0 @@ -import org.springframework.boot.gradle.tasks.bundling.BootJar - -plugins { - java - id("org.springframework.boot") version "{version}" -} - -repositories { - mavenCentral() -} - -dependencies { - runtimeOnly("org.jruby:jruby-complete:1.7.25") -} - -tasks.getByName("bootJar") { - mainClassName = "com.example.ExampleApplication" -} - -// tag::requires-unpack[] -tasks.getByName("bootJar") { - requiresUnpack("**/jruby-complete-*.jar") -} -// end::requires-unpack[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-war-include-devtools.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-war-include-devtools.gradle deleted file mode 100644 index f30b9e0355fb..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-war-include-devtools.gradle +++ /dev/null @@ -1,15 +0,0 @@ -plugins { - id 'war' - id 'org.springframework.boot' version '{version}' -} - -bootWar { - mainClassName 'com.example.ExampleApplication' - classpath file('spring-boot-devtools-1.2.3.RELEASE.jar') -} - -// tag::include-devtools[] -bootWar { - excludeDevtools = false -} -// end::include-devtools[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-war-include-devtools.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-war-include-devtools.gradle.kts deleted file mode 100644 index aab3f6ed6eba..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-war-include-devtools.gradle.kts +++ /dev/null @@ -1,17 +0,0 @@ -import org.springframework.boot.gradle.tasks.bundling.BootWar - -plugins { - war - id("org.springframework.boot") version "{version}" -} - -tasks.getByName("bootWar") { - mainClassName = "com.example.ExampleApplication" - classpath(file("spring-boot-devtools-1.2.3.RELEASE.jar")) -} - -// tag::include-devtools[] -tasks.getByName("bootWar") { - isExcludeDevtools = false -} -// end::include-devtools[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-war-properties-launcher.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-war-properties-launcher.gradle deleted file mode 100644 index 1996ff5a0f27..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-war-properties-launcher.gradle +++ /dev/null @@ -1,16 +0,0 @@ -plugins { - id 'war' - id 'org.springframework.boot' version '{version}' -} - -bootWar { - mainClassName 'com.example.ExampleApplication' -} - -// tag::properties-launcher[] -bootWar { - manifest { - attributes 'Main-Class': 'org.springframework.boot.loader.PropertiesLauncher' - } -} -// end::properties-launcher[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-war-properties-launcher.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-war-properties-launcher.gradle.kts deleted file mode 100644 index e2f78bbb65df..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/boot-war-properties-launcher.gradle.kts +++ /dev/null @@ -1,18 +0,0 @@ -import org.springframework.boot.gradle.tasks.bundling.BootWar - -plugins { - war - id("org.springframework.boot") version "{version}" -} - -tasks.getByName("bootWar") { - mainClassName = "com.example.ExampleApplication" -} - -// tag::properties-launcher[] -tasks.getByName("bootWar") { - manifest { - attributes("Main-Class" to "org.springframework.boot.loader.PropertiesLauncher") - } -} -// end::properties-launcher[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/spring-boot-dsl-main-class.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/spring-boot-dsl-main-class.gradle deleted file mode 100644 index 6ef52ffaff8d..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/spring-boot-dsl-main-class.gradle +++ /dev/null @@ -1,10 +0,0 @@ -plugins { - id 'java' - id 'org.springframework.boot' version '{version}' -} - -// tag::main-class[] -springBoot { - mainClassName = 'com.example.ExampleApplication' -} -// end::main-class[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/spring-boot-dsl-main-class.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/spring-boot-dsl-main-class.gradle.kts deleted file mode 100644 index c07ab49cfa1f..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/spring-boot-dsl-main-class.gradle.kts +++ /dev/null @@ -1,10 +0,0 @@ -plugins { - war - id("org.springframework.boot") version "{version}" -} - -// tag::main-class[] -springBoot { - mainClassName = "com.example.ExampleApplication" -} -// end::main-class[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/war-container-dependency.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/war-container-dependency.gradle deleted file mode 100644 index 5901f7c4f51c..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/packaging/war-container-dependency.gradle +++ /dev/null @@ -1,13 +0,0 @@ -plugins { - id 'war' - id 'org.springframework.boot' version '{version}' -} - -apply plugin: 'io.spring.dependency-management' - -// tag::dependencies[] -dependencies { - implementation 'org.springframework.boot:spring-boot-starter-web' - providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat' -} -// end::dependencies[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/publishing/maven-publish.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/publishing/maven-publish.gradle deleted file mode 100644 index b3b06bbd22c2..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/publishing/maven-publish.gradle +++ /dev/null @@ -1,27 +0,0 @@ -plugins { - id 'java' - id 'maven-publish' - id 'org.springframework.boot' version '{version}' -} - -// tag::publishing[] -publishing { - publications { - bootJava(MavenPublication) { - artifact bootJar - } - } - repositories { - maven { - url 'https://repo.example.com' - } - } -} -// end::publishing[] - -task publishingConfiguration { - doLast { - println publishing.publications.bootJava - println publishing.repositories.maven.url - } -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/publishing/maven-publish.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/publishing/maven-publish.gradle.kts deleted file mode 100644 index b07e1b908400..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/publishing/maven-publish.gradle.kts +++ /dev/null @@ -1,27 +0,0 @@ -plugins { - java - `maven-publish` - id("org.springframework.boot") version "{version}" -} - -// tag::publishing[] -publishing { - publications { - create("bootJava") { - artifact(tasks.getByName("bootJar")) - } - } - repositories { - maven { - url = uri("https://repo.example.com") - } - } -} -// end::publishing[] - -task("publishingConfiguration") { - doLast { - println(publishing.publications["bootJava"]) - println(publishing.repositories.getByName("maven").url) - } -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/publishing/maven.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/publishing/maven.gradle deleted file mode 100644 index 37e5d90df21c..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/publishing/maven.gradle +++ /dev/null @@ -1,21 +0,0 @@ -plugins { - id 'java' - id 'maven' - id 'org.springframework.boot' version '{version}' -} - -// tag::upload[] -uploadBootArchives { - repositories { - mavenDeployer { - repository url: 'https://repo.example.com' - } - } -} -// end::upload[] - -task deployerRepository { - doLast { - println uploadBootArchives.repositories.mavenDeployer.repository.url - } -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/publishing/maven.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/publishing/maven.gradle.kts deleted file mode 100644 index 35caea204351..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/publishing/maven.gradle.kts +++ /dev/null @@ -1,26 +0,0 @@ -plugins { - java - maven - id("org.springframework.boot") version "{version}" -} - -// tag::upload[] -tasks.getByName("uploadBootArchives") { - repositories.withGroovyBuilder { - "mavenDeployer" { - "repository"("url" to "https://repo.example.com") - } - } -} -// end::upload[] - -val url = tasks.getByName("uploadBootArchives") - .repositories - .withGroovyBuilder { getProperty("mavenDeployer") } - .withGroovyBuilder { getProperty("repository") } - .withGroovyBuilder { getProperty("url") } -task("deployerRepository") { - doLast { - println(url) - } -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/running/application-plugin-main-class-name.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/running/application-plugin-main-class-name.gradle deleted file mode 100644 index 894cb9b75437..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/running/application-plugin-main-class-name.gradle +++ /dev/null @@ -1,15 +0,0 @@ -plugins { - id 'java' - id 'application' - id 'org.springframework.boot' version '{version}' -} - -// tag::main-class[] -mainClassName = 'com.example.ExampleApplication' -// end::main-class[] - -task configuredMainClass { - doLast { - println bootRun.main - } -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/running/application-plugin-main-class-name.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/running/application-plugin-main-class-name.gradle.kts deleted file mode 100644 index 6cf2ddcc6246..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/running/application-plugin-main-class-name.gradle.kts +++ /dev/null @@ -1,19 +0,0 @@ -import org.springframework.boot.gradle.tasks.run.BootRun - -plugins { - java - application - id("org.springframework.boot") version "{version}" -} - -// tag::main-class[] -application { - mainClassName = "com.example.ExampleApplication" -} -// end::main-class[] - -task("configuredMainClass") { - doLast { - println(tasks.getByName("bootRun").main) - } -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/running/boot-run-disable-optimized-launch.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/running/boot-run-disable-optimized-launch.gradle deleted file mode 100644 index 2c6b6499b118..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/running/boot-run-disable-optimized-launch.gradle +++ /dev/null @@ -1,16 +0,0 @@ -plugins { - id 'java' - id 'org.springframework.boot' version '{version}' -} - -// tag::launch[] -bootRun { - optimizedLaunch = false -} -// end::launch[] - -task optimizedLaunch { - doLast { - println bootRun.optimizedLaunch - } -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/running/boot-run-disable-optimized-launch.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/running/boot-run-disable-optimized-launch.gradle.kts deleted file mode 100644 index ac80c50b3a4c..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/running/boot-run-disable-optimized-launch.gradle.kts +++ /dev/null @@ -1,18 +0,0 @@ -import org.springframework.boot.gradle.tasks.run.BootRun - -plugins { - java - id("org.springframework.boot") version "{version}" -} - -// tag::launch[] -tasks.getByName("bootRun") { - isOptimizedLaunch = false -} -// end::launch[] - -task("optimizedLaunch") { - doLast { - println(tasks.getByName("bootRun").isOptimizedLaunch) - } -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/running/boot-run-main.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/running/boot-run-main.gradle deleted file mode 100644 index aad2b96a43f7..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/running/boot-run-main.gradle +++ /dev/null @@ -1,16 +0,0 @@ -plugins { - id 'java' - id 'org.springframework.boot' version '{version}' -} - -// tag::main[] -bootRun { - main = 'com.example.ExampleApplication' -} -// end::main[] - -task configuredMainClass { - doLast { - println bootRun.main - } -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/running/boot-run-main.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/running/boot-run-main.gradle.kts deleted file mode 100644 index 380ef98f5fb1..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/running/boot-run-main.gradle.kts +++ /dev/null @@ -1,18 +0,0 @@ -import org.springframework.boot.gradle.tasks.run.BootRun - -plugins { - java - id("org.springframework.boot") version "{version}" -} - -// tag::main[] -tasks.getByName("bootRun") { - main = "com.example.ExampleApplication" -} -// end::main[] - -task("configuredMainClass") { - doLast { - println(tasks.getByName("bootRun").main) - } -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/running/boot-run-source-resources.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/running/boot-run-source-resources.gradle deleted file mode 100644 index ba8bd7ace8a9..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/running/boot-run-source-resources.gradle +++ /dev/null @@ -1,16 +0,0 @@ -plugins { - id 'java' - id 'org.springframework.boot' version '{version}' -} - -// tag::source-resources[] -bootRun { - sourceResources sourceSets.main -} -// end::source-resources[] - -task configuredClasspath { - doLast { - println bootRun.classpath.files - } -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/running/boot-run-source-resources.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/running/boot-run-source-resources.gradle.kts deleted file mode 100644 index 9e8f5ec4bf6c..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/running/boot-run-source-resources.gradle.kts +++ /dev/null @@ -1,18 +0,0 @@ -import org.springframework.boot.gradle.tasks.run.BootRun - -plugins { - java - id("org.springframework.boot") version "{version}" -} - -// tag::source-resources[] -tasks.getByName("bootRun") { - sourceResources(sourceSets["main"]) -} -// end::source-resources[] - -task("configuredClasspath") { - doLast { - println(tasks.getByName("bootRun").classpath.files) - } -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/running/spring-boot-dsl-main-class-name.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/running/spring-boot-dsl-main-class-name.gradle deleted file mode 100644 index 793ac5bacde1..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/running/spring-boot-dsl-main-class-name.gradle +++ /dev/null @@ -1,17 +0,0 @@ -plugins { - id 'java' - id 'application' - id 'org.springframework.boot' version '{version}' -} - -// tag::main-class[] -springBoot { - mainClassName = 'com.example.ExampleApplication' -} -// end::main-class[] - -task configuredMainClass { - doLast { - println bootRun.main - } -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/running/spring-boot-dsl-main-class-name.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/running/spring-boot-dsl-main-class-name.gradle.kts deleted file mode 100644 index 9c0e2371adfb..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/gradle/running/spring-boot-dsl-main-class-name.gradle.kts +++ /dev/null @@ -1,19 +0,0 @@ -import org.springframework.boot.gradle.tasks.run.BootRun - -plugins { - java - application - id("org.springframework.boot") version "{version}" -} - -// tag::main-class[] -springBoot { - mainClassName = "com.example.ExampleApplication" -} -// end::main-class[] - -task("configuredMainClass") { - doLast { - println(tasks.getByName("bootRun").main) - } -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/dsl/SpringBootExtension.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/dsl/SpringBootExtension.java index 2fac0c020ef8..fa81dcf3444b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/dsl/SpringBootExtension.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/dsl/SpringBootExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,10 +20,14 @@ import org.gradle.api.Action; import org.gradle.api.Project; +import org.gradle.api.model.ReplacedBy; import org.gradle.api.plugins.BasePlugin; import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPluginConvention; +import org.gradle.api.provider.Property; import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskContainer; +import org.gradle.api.tasks.TaskProvider; import org.gradle.jvm.tasks.Jar; import org.springframework.boot.gradle.tasks.buildinfo.BuildInfo; @@ -33,13 +37,14 @@ * Entry point to Spring Boot's Gradle DSL. * * @author Andy Wilkinson + * @author Scott Frederick * @since 2.0.0 */ public class SpringBootExtension { private final Project project; - private String mainClassName; + private final Property mainClass; /** * Creates a new {@code SpringBootPluginExtension} that is associated with the given @@ -48,22 +53,38 @@ public class SpringBootExtension { */ public SpringBootExtension(Project project) { this.project = project; + this.mainClass = this.project.getObjects().property(String.class); } /** - * Returns the main class name of the application. - * @return the name of the application's main class + * Returns the fully-qualified name of the application's main class. + * @return the fully-qualified name of the application's main class + * @since 2.4.0 */ + public Property getMainClass() { + return this.mainClass; + } + + /** + * Returns the fully-qualified main class name of the application. + * @return the fully-qualified name of the application's main class + * @deprecated since 2.4.0 for removal in 2.6.0 in favor of {@link #getMainClass()}. + */ + @Deprecated + @ReplacedBy("mainClass") public String getMainClassName() { - return this.mainClassName; + return this.mainClass.getOrNull(); } /** - * Sets the main class name of the application. - * @param mainClassName the name of the application's main class + * Sets the fully-qualified main class name of the application. + * @param mainClassName the fully-qualified name of the application's main class + * @deprecated since 2.4.0 for removal in 2.6.0 in favor of {@link #getMainClass} and + * {@link Property#set(Object)} */ + @Deprecated public void setMainClassName(String mainClassName) { - this.mainClassName = mainClassName; + this.mainClass.set(mainClassName); } /** @@ -75,7 +96,7 @@ public void setMainClassName(String mainClassName) { * artifact will be the base name of the {@code bootWar} or {@code bootJar} task. */ public void buildInfo() { - this.buildInfo(null); + buildInfo(null); } /** @@ -89,25 +110,30 @@ public void buildInfo() { * @param configurer the task configurer */ public void buildInfo(Action configurer) { - BuildInfo bootBuildInfo = this.project.getTasks().create("bootBuildInfo", BuildInfo.class); - bootBuildInfo.setGroup(BasePlugin.BUILD_GROUP); - bootBuildInfo.setDescription("Generates a META-INF/build-info.properties file."); + TaskContainer tasks = this.project.getTasks(); + TaskProvider bootBuildInfo = tasks.register("bootBuildInfo", BuildInfo.class, + this::configureBuildInfoTask); this.project.getPlugins().withType(JavaPlugin.class, (plugin) -> { - this.project.getTasks().getByName(JavaPlugin.CLASSES_TASK_NAME).dependsOn(bootBuildInfo); - this.project.afterEvaluate((evaluated) -> { - BuildInfoProperties properties = bootBuildInfo.getProperties(); + tasks.named(JavaPlugin.CLASSES_TASK_NAME).configure((task) -> task.dependsOn(bootBuildInfo)); + this.project.afterEvaluate((evaluated) -> bootBuildInfo.configure((buildInfo) -> { + BuildInfoProperties properties = buildInfo.getProperties(); if (properties.getArtifact() == null) { properties.setArtifact(determineArtifactBaseName()); } - }); - bootBuildInfo.getConventionMapping().map("destinationDir", - () -> new File(determineMainSourceSetResourcesOutputDir(), "META-INF")); + })); }); if (configurer != null) { - configurer.execute(bootBuildInfo); + bootBuildInfo.configure(configurer); } } + private void configureBuildInfoTask(BuildInfo task) { + task.setGroup(BasePlugin.BUILD_GROUP); + task.setDescription("Generates a META-INF/build-info.properties file."); + task.getConventionMapping().map("destinationDir", + () -> new File(determineMainSourceSetResourcesOutputDir(), "META-INF")); + } + private File determineMainSourceSetResourcesOutputDir() { return this.project.getConvention().getPlugin(JavaPluginConvention.class).getSourceSets() .getByName(SourceSet.MAIN_SOURCE_SET_NAME).getOutput().getResourcesDir(); @@ -115,7 +141,7 @@ private File determineMainSourceSetResourcesOutputDir() { private String determineArtifactBaseName() { Jar artifactTask = findArtifactTask(); - return (artifactTask != null) ? artifactTask.getBaseName() : null; + return (artifactTask != null) ? artifactTask.getArchiveBaseName().get() : null; } private Jar findArtifactTask() { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ApplicationPluginAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ApplicationPluginAction.java index 4d226179f24b..dd5017a68a95 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ApplicationPluginAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ApplicationPluginAction.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,16 +25,16 @@ import org.gradle.api.GradleException; import org.gradle.api.Plugin; import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; import org.gradle.api.distribution.Distribution; import org.gradle.api.distribution.DistributionContainer; import org.gradle.api.file.CopySpec; import org.gradle.api.file.FileCollection; -import org.gradle.api.internal.IConventionAware; import org.gradle.api.plugins.ApplicationPlugin; import org.gradle.api.plugins.ApplicationPluginConvention; +import org.gradle.api.tasks.TaskProvider; import org.gradle.jvm.application.scripts.TemplateBasedScriptGenerator; - -import org.springframework.boot.gradle.tasks.application.CreateBootStartScripts; +import org.gradle.jvm.application.tasks.CreateStartScripts; /** * Action that is executed in response to the {@link ApplicationPlugin} being applied. @@ -49,34 +49,45 @@ public void execute(Project project) { .getPlugin(ApplicationPluginConvention.class); DistributionContainer distributions = project.getExtensions().getByType(DistributionContainer.class); Distribution distribution = distributions.create("boot"); - if (distribution instanceof IConventionAware) { - ((IConventionAware) distribution).getConventionMapping().map("baseName", - () -> applicationConvention.getApplicationName() + "-boot"); - } - CreateBootStartScripts bootStartScripts = project.getTasks().create("bootStartScripts", - CreateBootStartScripts.class); - bootStartScripts + distribution.getDistributionBaseName() + .convention((project.provider(() -> applicationConvention.getApplicationName() + "-boot"))); + TaskProvider bootStartScripts = project.getTasks().register("bootStartScripts", + CreateStartScripts.class, + (task) -> configureCreateStartScripts(project, applicationConvention, distribution, task)); + CopySpec binCopySpec = project.copySpec().into("bin").from(bootStartScripts); + binCopySpec.setFileMode(0755); + distribution.getContents().with(binCopySpec); + } + + private void configureCreateStartScripts(Project project, ApplicationPluginConvention applicationConvention, + Distribution distribution, CreateStartScripts createStartScripts) { + createStartScripts .setDescription("Generates OS-specific start scripts to run the project as a Spring Boot application."); - ((TemplateBasedScriptGenerator) bootStartScripts.getUnixStartScriptGenerator()) + ((TemplateBasedScriptGenerator) createStartScripts.getUnixStartScriptGenerator()) .setTemplate(project.getResources().getText().fromString(loadResource("/unixStartScript.txt"))); - ((TemplateBasedScriptGenerator) bootStartScripts.getWindowsStartScriptGenerator()) + ((TemplateBasedScriptGenerator) createStartScripts.getWindowsStartScriptGenerator()) .setTemplate(project.getResources().getText().fromString(loadResource("/windowsStartScript.txt"))); project.getConfigurations().all((configuration) -> { if ("bootArchives".equals(configuration.getName())) { - CopySpec libCopySpec = project.copySpec().into("lib") - .from((Callable) () -> configuration.getArtifacts().getFiles()); - libCopySpec.setFileMode(0644); - distribution.getContents().with(libCopySpec); - bootStartScripts.setClasspath(configuration.getArtifacts().getFiles()); + distribution.getContents().with(artifactFilesToLibCopySpec(project, configuration)); + createStartScripts.setClasspath(configuration.getArtifacts().getFiles()); } }); - bootStartScripts.getConventionMapping().map("outputDir", () -> new File(project.getBuildDir(), "bootScripts")); - bootStartScripts.getConventionMapping().map("applicationName", applicationConvention::getApplicationName); - bootStartScripts.getConventionMapping().map("defaultJvmOpts", + createStartScripts.getConventionMapping().map("outputDir", + () -> new File(project.getBuildDir(), "bootScripts")); + createStartScripts.getConventionMapping().map("applicationName", applicationConvention::getApplicationName); + createStartScripts.getConventionMapping().map("defaultJvmOpts", applicationConvention::getApplicationDefaultJvmArgs); - CopySpec binCopySpec = project.copySpec().into("bin").from(bootStartScripts); - binCopySpec.setFileMode(0755); - distribution.getContents().with(binCopySpec); + } + + private CopySpec artifactFilesToLibCopySpec(Project project, Configuration configuration) { + CopySpec copySpec = project.copySpec().into("lib").from(artifactFiles(configuration)); + copySpec.setFileMode(0644); + return copySpec; + } + + private Callable artifactFiles(Configuration configuration) { + return () -> configuration.getArtifacts().getFiles(); } @Override @@ -87,7 +98,7 @@ public Class> getPluginClass() { private String loadResource(String name) { try (InputStreamReader reader = new InputStreamReader(getClass().getResourceAsStream(name))) { char[] buffer = new char[4096]; - int read = 0; + int read; StringWriter writer = new StringWriter(); while ((read = reader.read(buffer)) > 0) { writer.write(buffer, 0, read); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JarTypeFileSpec.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JarTypeFileSpec.java new file mode 100644 index 000000000000..1b5d38ea32a7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JarTypeFileSpec.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.gradle.plugin; + +import java.io.File; +import java.util.Collections; +import java.util.Set; +import java.util.jar.JarFile; + +import org.gradle.api.file.FileCollection; +import org.gradle.api.specs.Spec; + +/** + * A {@link Spec} for {@link FileCollection#filter(Spec) filtering} {@code FileCollection} + * to remove jar files based on their {@code Spring-Boot-Jar-Type} as defined in the + * manifest. Jars of type {@code dependencies-starter} are excluded. + * + * @author Andy Wilkinson + */ +class JarTypeFileSpec implements Spec { + + private static final Set EXCLUDED_JAR_TYPES = Collections.singleton("dependencies-starter"); + + @Override + public boolean isSatisfiedBy(File file) { + try (JarFile jar = new JarFile(file)) { + String jarType = jar.getManifest().getMainAttributes().getValue("Spring-Boot-Jar-Type"); + if (jarType != null && EXCLUDED_JAR_TYPES.contains(jarType)) { + return false; + } + } + catch (Exception ex) { + // Continue + } + return true; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JavaPluginAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JavaPluginAction.java index 15280ae155bb..593a568c1e34 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JavaPluginAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JavaPluginAction.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ import java.io.File; import java.util.Collections; import java.util.List; -import java.util.Optional; import java.util.Set; import java.util.concurrent.Callable; @@ -27,15 +26,28 @@ import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.Task; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.attributes.AttributeContainer; +import org.gradle.api.attributes.Bundling; +import org.gradle.api.attributes.LibraryElements; +import org.gradle.api.attributes.Usage; import org.gradle.api.file.FileCollection; -import org.gradle.api.internal.artifacts.publish.ArchivePublishArtifact; +import org.gradle.api.model.ObjectFactory; import org.gradle.api.plugins.ApplicationPlugin; import org.gradle.api.plugins.BasePlugin; import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPluginConvention; +import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.provider.Provider; import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.api.tasks.bundling.Jar; import org.gradle.api.tasks.compile.JavaCompile; +import org.gradle.jvm.toolchain.JavaToolchainService; +import org.gradle.jvm.toolchain.JavaToolchainSpec; +import org.springframework.boot.gradle.tasks.bundling.BootBuildImage; import org.springframework.boot.gradle.tasks.bundling.BootJar; import org.springframework.boot.gradle.tasks.run.BootRun; import org.springframework.util.StringUtils; @@ -44,6 +56,7 @@ * {@link Action} that is executed in response to the {@link JavaPlugin} being applied. * * @author Andy Wilkinson + * @author Scott Frederick */ final class JavaPluginAction implements PluginApplicationAction { @@ -62,68 +75,107 @@ public Class> getPluginClass() { @Override public void execute(Project project) { - disableJarTask(project); + classifyJarTask(project); configureBuildTask(project); - BootJar bootJar = configureBootJarTask(project); + configureDevelopmentOnlyConfiguration(project); + TaskProvider bootJar = configureBootJarTask(project); + configureBootBuildImageTask(project, bootJar); configureArtifactPublication(bootJar); configureBootRunTask(project); - configureUtf8Encoding(project); + project.afterEvaluate(this::configureUtf8Encoding); configureParametersCompilerArg(project); configureAdditionalMetadataLocations(project); } - private void disableJarTask(Project project) { - project.getTasks().getByName(JavaPlugin.JAR_TASK_NAME).setEnabled(false); + private void classifyJarTask(Project project) { + project.getTasks().named(JavaPlugin.JAR_TASK_NAME, Jar.class) + .configure((task) -> task.getArchiveClassifier().convention("plain")); } private void configureBuildTask(Project project) { - project.getTasks().getByName(BasePlugin.ASSEMBLE_TASK_NAME).dependsOn(this.singlePublishedArtifact); - } - - private BootJar configureBootJarTask(Project project) { - BootJar bootJar = project.getTasks().create(SpringBootPlugin.BOOT_JAR_TASK_NAME, BootJar.class); - bootJar.setDescription( - "Assembles an executable jar archive containing the main classes and their dependencies."); - bootJar.setGroup(BasePlugin.BUILD_GROUP); - bootJar.classpath((Callable) () -> { - JavaPluginConvention convention = project.getConvention().getPlugin(JavaPluginConvention.class); - SourceSet mainSourceSet = convention.getSourceSets().getByName(SourceSet.MAIN_SOURCE_SET_NAME); - return mainSourceSet.getRuntimeClasspath(); + project.getTasks().named(BasePlugin.ASSEMBLE_TASK_NAME) + .configure((task) -> task.dependsOn(this.singlePublishedArtifact)); + } + + private TaskProvider configureBootJarTask(Project project) { + SourceSet mainSourceSet = javaPluginConvention(project).getSourceSets() + .getByName(SourceSet.MAIN_SOURCE_SET_NAME); + Configuration developmentOnly = project.getConfigurations() + .getByName(SpringBootPlugin.DEVELOPMENT_ONLY_CONFIGURATION_NAME); + Configuration productionRuntimeClasspath = project.getConfigurations() + .getByName(SpringBootPlugin.PRODUCTION_RUNTIME_CLASSPATH_CONFIGURATION_NAME); + Callable classpath = () -> mainSourceSet.getRuntimeClasspath() + .minus((developmentOnly.minus(productionRuntimeClasspath))).filter(new JarTypeFileSpec()); + TaskProvider resolveMainClassName = ResolveMainClassName + .registerForTask(SpringBootPlugin.BOOT_JAR_TASK_NAME, project, classpath); + return project.getTasks().register(SpringBootPlugin.BOOT_JAR_TASK_NAME, BootJar.class, (bootJar) -> { + bootJar.setDescription( + "Assembles an executable jar archive containing the main classes and their dependencies."); + bootJar.setGroup(BasePlugin.BUILD_GROUP); + bootJar.classpath(classpath); + Provider manifestStartClass = project + .provider(() -> (String) bootJar.getManifest().getAttributes().get("Start-Class")); + bootJar.getMainClass().convention(resolveMainClassName.flatMap((resolver) -> manifestStartClass.isPresent() + ? manifestStartClass : resolveMainClassName.get().readMainClassName())); + }); + } + + private void configureBootBuildImageTask(Project project, TaskProvider bootJar) { + project.getTasks().register(SpringBootPlugin.BOOT_BUILD_IMAGE_TASK_NAME, BootBuildImage.class, (buildImage) -> { + buildImage.setDescription("Builds an OCI image of the application using the output of the bootJar task"); + buildImage.setGroup(BasePlugin.BUILD_GROUP); + buildImage.getArchiveFile().set(bootJar.get().getArchiveFile()); + buildImage.getTargetJavaVersion() + .set(project.provider(() -> javaPluginConvention(project).getTargetCompatibility())); }); - bootJar.conventionMapping("mainClassName", new MainClassConvention(project, bootJar::getClasspath)); - return bootJar; } - private void configureArtifactPublication(BootJar bootJar) { - ArchivePublishArtifact artifact = new ArchivePublishArtifact(bootJar); - this.singlePublishedArtifact.addCandidate(artifact); + private void configureArtifactPublication(TaskProvider bootJar) { + this.singlePublishedArtifact.addJarCandidate(bootJar); } private void configureBootRunTask(Project project) { - JavaPluginConvention javaConvention = project.getConvention().getPlugin(JavaPluginConvention.class); - BootRun run = project.getTasks().create("bootRun", BootRun.class); - run.setDescription("Runs this project as a Spring Boot application."); - run.setGroup(ApplicationPlugin.APPLICATION_GROUP); - run.classpath(javaConvention.getSourceSets().findByName(SourceSet.MAIN_SOURCE_SET_NAME).getRuntimeClasspath()); - run.getConventionMapping().map("jvmArgs", () -> { - if (project.hasProperty("applicationDefaultJvmArgs")) { - return project.property("applicationDefaultJvmArgs"); - } - return Collections.emptyList(); + Callable classpath = () -> javaPluginConvention(project).getSourceSets() + .findByName(SourceSet.MAIN_SOURCE_SET_NAME).getRuntimeClasspath().filter(new JarTypeFileSpec()); + TaskProvider resolveProvider = ResolveMainClassName.registerForTask("bootRun", project, + classpath); + project.getTasks().register("bootRun", BootRun.class, (run) -> { + run.setDescription("Runs this project as a Spring Boot application."); + run.setGroup(ApplicationPlugin.APPLICATION_GROUP); + run.classpath(classpath); + run.getConventionMapping().map("jvmArgs", () -> { + if (project.hasProperty("applicationDefaultJvmArgs")) { + return project.property("applicationDefaultJvmArgs"); + } + return Collections.emptyList(); + }); + run.getMainClass().convention(resolveProvider.flatMap(ResolveMainClassName::readMainClassName)); + configureToolchainConvention(project, run); }); - run.conventionMapping("main", new MainClassConvention(project, run::getClasspath)); } - private void configureUtf8Encoding(Project project) { - project.afterEvaluate((evaluated) -> evaluated.getTasks().withType(JavaCompile.class, (compile) -> { - if (compile.getOptions().getEncoding() == null) { - compile.getOptions().setEncoding("UTF-8"); - } - })); + private void configureToolchainConvention(Project project, BootRun run) { + JavaToolchainSpec toolchain = project.getExtensions().getByType(JavaPluginExtension.class).getToolchain(); + JavaToolchainService toolchainService = project.getExtensions().getByType(JavaToolchainService.class); + run.getJavaLauncher().convention(toolchainService.launcherFor(toolchain)); + } + + private JavaPluginConvention javaPluginConvention(Project project) { + return project.getConvention().getPlugin(JavaPluginConvention.class); + } + + private void configureUtf8Encoding(Project evaluatedProject) { + evaluatedProject.getTasks().withType(JavaCompile.class).configureEach(this::configureUtf8Encoding); + } + + private void configureUtf8Encoding(JavaCompile compile) { + if (compile.getOptions().getEncoding() == null) { + compile.getOptions().setEncoding("UTF-8"); + } } private void configureParametersCompilerArg(Project project) { - project.getTasks().withType(JavaCompile.class, (compile) -> { + project.getTasks().withType(JavaCompile.class).configureEach((compile) -> { List compilerArgs = compile.getOptions().getCompilerArgs(); if (!compilerArgs.contains(PARAMETERS_COMPILER_ARG)) { compilerArgs.add(PARAMETERS_COMPILER_ARG); @@ -132,12 +184,38 @@ private void configureParametersCompilerArg(Project project) { } private void configureAdditionalMetadataLocations(Project project) { - project.afterEvaluate((evaluated) -> evaluated.getTasks().withType(JavaCompile.class, - this::configureAdditionalMetadataLocations)); + project.afterEvaluate((evaluated) -> evaluated.getTasks().withType(JavaCompile.class) + .configureEach(this::configureAdditionalMetadataLocations)); } private void configureAdditionalMetadataLocations(JavaCompile compile) { - compile.doFirst(new AdditionalMetadataLocationsConfigurer()); + SourceSetContainer sourceSets = compile.getProject().getConvention().getPlugin(JavaPluginConvention.class) + .getSourceSets(); + sourceSets.stream().filter((candidate) -> candidate.getCompileJavaTaskName().equals(compile.getName())) + .map((match) -> match.getResources().getSrcDirs()).findFirst() + .ifPresent((locations) -> compile.doFirst(new AdditionalMetadataLocationsConfigurer(locations))); + } + + private void configureDevelopmentOnlyConfiguration(Project project) { + Configuration developmentOnly = project.getConfigurations() + .create(SpringBootPlugin.DEVELOPMENT_ONLY_CONFIGURATION_NAME); + developmentOnly + .setDescription("Configuration for development-only dependencies such as Spring Boot's DevTools."); + Configuration runtimeClasspath = project.getConfigurations() + .getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME); + Configuration productionRuntimeClasspath = project.getConfigurations() + .create(SpringBootPlugin.PRODUCTION_RUNTIME_CLASSPATH_CONFIGURATION_NAME); + AttributeContainer attributes = productionRuntimeClasspath.getAttributes(); + ObjectFactory objectFactory = project.getObjects(); + attributes.attribute(Usage.USAGE_ATTRIBUTE, objectFactory.named(Usage.class, Usage.JAVA_RUNTIME)); + attributes.attribute(Bundling.BUNDLING_ATTRIBUTE, objectFactory.named(Bundling.class, Bundling.EXTERNAL)); + attributes.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, + objectFactory.named(LibraryElements.class, LibraryElements.JAR)); + productionRuntimeClasspath.setVisible(false); + productionRuntimeClasspath.setExtendsFrom(runtimeClasspath.getExtendsFrom()); + productionRuntimeClasspath.setCanBeResolved(runtimeClasspath.isCanBeResolved()); + productionRuntimeClasspath.setCanBeConsumed(runtimeClasspath.isCanBeConsumed()); + runtimeClasspath.extendsFrom(developmentOnly); } /** @@ -145,7 +223,13 @@ private void configureAdditionalMetadataLocations(JavaCompile compile) { * inner-class rather than a lambda due to * https://github.com/gradle/gradle/issues/5510. */ - private static class AdditionalMetadataLocationsConfigurer implements Action { + private static final class AdditionalMetadataLocationsConfigurer implements Action { + + private final Set locations; + + private AdditionalMetadataLocationsConfigurer(Set locations) { + this.locations = locations; + } @Override public void execute(Task task) { @@ -154,8 +238,7 @@ public void execute(Task task) { } JavaCompile compile = (JavaCompile) task; if (hasConfigurationProcessorOnClasspath(compile)) { - findMatchingSourceSet(compile) - .ifPresent((sourceSet) -> configureAdditionalMetadataLocations(compile, sourceSet)); + configureAdditionalMetadataLocations(compile); } } @@ -166,15 +249,10 @@ private boolean hasConfigurationProcessorOnClasspath(JavaCompile compile) { .anyMatch((name) -> name.startsWith("spring-boot-configuration-processor")); } - private Optional findMatchingSourceSet(JavaCompile compile) { - return compile.getProject().getConvention().getPlugin(JavaPluginConvention.class).getSourceSets().stream() - .filter((sourceSet) -> sourceSet.getCompileJavaTaskName().equals(compile.getName())).findFirst(); - } - - private void configureAdditionalMetadataLocations(JavaCompile compile, SourceSet sourceSet) { - String locations = StringUtils.collectionToCommaDelimitedString(sourceSet.getResources().getSrcDirs()); + private void configureAdditionalMetadataLocations(JavaCompile compile) { compile.getOptions().getCompilerArgs() - .add("-Aorg.springframework.boot.configurationprocessor.additionalMetadataLocations=" + locations); + .add("-Aorg.springframework.boot.configurationprocessor.additionalMetadataLocations=" + + StringUtils.collectionToCommaDelimitedString(this.locations)); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/KotlinPluginAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/KotlinPluginAction.java index ccf0d0cb4d6a..a4bce532bd03 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/KotlinPluginAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/KotlinPluginAction.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import org.gradle.api.Project; import org.gradle.api.plugins.ExtraPropertiesExtension; import org.jetbrains.kotlin.gradle.plugin.KotlinPluginWrapper; +import org.jetbrains.kotlin.gradle.plugin.KotlinPluginWrapperKt; import org.jetbrains.kotlin.gradle.tasks.KotlinCompile; /** @@ -33,29 +34,26 @@ class KotlinPluginAction implements PluginApplicationAction { @Override public void execute(Project project) { - String kotlinVersion = project.getPlugins().getPlugin(KotlinPluginWrapper.class).getKotlinPluginVersion(); ExtraPropertiesExtension extraProperties = project.getExtensions().getExtraProperties(); if (!extraProperties.has("kotlin.version")) { + String kotlinVersion = getKotlinVersion(project); extraProperties.set("kotlin.version", kotlinVersion); } enableJavaParametersOption(project); } + private String getKotlinVersion(Project project) { + return KotlinPluginWrapperKt.getKotlinPluginVersion(project); + } + private void enableJavaParametersOption(Project project) { - project.getTasks().withType(KotlinCompile.class, - (compile) -> compile.getKotlinOptions().setJavaParameters(true)); + project.getTasks().withType(KotlinCompile.class) + .configureEach((compile) -> compile.getKotlinOptions().setJavaParameters(true)); } @Override - @SuppressWarnings("unchecked") public Class> getPluginClass() { - try { - return (Class>) Class - .forName("org.jetbrains.kotlin.gradle.plugin.KotlinPluginWrapper"); - } - catch (Throwable ex) { - return null; - } + return KotlinPluginWrapper.class; } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/MainClassConvention.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/MainClassConvention.java deleted file mode 100644 index b929b0f0fe00..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/MainClassConvention.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.gradle.plugin; - -import java.io.File; -import java.io.IOException; -import java.util.Objects; -import java.util.concurrent.Callable; -import java.util.function.Supplier; - -import org.gradle.api.InvalidUserDataException; -import org.gradle.api.Project; -import org.gradle.api.file.FileCollection; - -import org.springframework.boot.gradle.dsl.SpringBootExtension; -import org.springframework.boot.loader.tools.MainClassFinder; - -/** - * A {@link Callable} that provides a convention for the project's main class name. - * - * @author Andy Wilkinson - */ -final class MainClassConvention implements Callable { - - private static final String SPRING_BOOT_APPLICATION_CLASS_NAME = "org.springframework.boot.autoconfigure.SpringBootApplication"; - - private final Project project; - - private final Supplier classpathSupplier; - - MainClassConvention(Project project, Supplier classpathSupplier) { - this.project = project; - this.classpathSupplier = classpathSupplier; - } - - @Override - public Object call() throws Exception { - SpringBootExtension springBootExtension = this.project.getExtensions().findByType(SpringBootExtension.class); - if (springBootExtension != null && springBootExtension.getMainClassName() != null) { - return springBootExtension.getMainClassName(); - } - if (this.project.hasProperty("mainClassName")) { - Object mainClassName = this.project.property("mainClassName"); - if (mainClassName != null) { - return mainClassName; - } - } - return resolveMainClass(); - } - - private String resolveMainClass() { - return this.classpathSupplier.get().filter(File::isDirectory).getFiles().stream().map(this::findMainClass) - .filter(Objects::nonNull).findFirst().orElseThrow(() -> new InvalidUserDataException( - "Main class name has not been configured and it could not be resolved")); - } - - private String findMainClass(File file) { - try { - return MainClassFinder.findSingleMainClass(file, SPRING_BOOT_APPLICATION_CLASS_NAME); - } - catch (IOException ex) { - return null; - } - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/MavenPluginAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/MavenPluginAction.java index 2cd8f1dedc11..65772274e31c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/MavenPluginAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/MavenPluginAction.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,15 +19,17 @@ import org.gradle.api.Action; import org.gradle.api.Plugin; import org.gradle.api.Project; -import org.gradle.api.artifacts.maven.MavenResolver; -import org.gradle.api.plugins.MavenPlugin; +import org.gradle.api.publish.maven.plugins.MavenPublishPlugin; import org.gradle.api.tasks.Upload; /** - * {@link Action} that is executed in response to the {@link MavenPlugin} being applied. + * {@link Action} that is executed in response to the + * {@link org.gradle.api.plugins.MavenPlugin} being applied. * * @author Andy Wilkinson + * @deprecated since 2.5.0 in favor of using the {@link MavenPublishPlugin} */ +@Deprecated final class MavenPluginAction implements PluginApplicationAction { private final String uploadTaskName; @@ -38,20 +40,22 @@ final class MavenPluginAction implements PluginApplicationAction { @Override public Class> getPluginClass() { - return MavenPlugin.class; + return org.gradle.api.plugins.MavenPlugin.class; } @Override public void execute(Project project) { - project.getTasks().withType(Upload.class, (upload) -> { - if (this.uploadTaskName.equals(upload.getName())) { - project.afterEvaluate((evaluated) -> clearConfigurationMappings(upload)); - } + project.afterEvaluate((evaluated) -> { + project.getTasks().withType(Upload.class).configureEach((upload) -> { + if (this.uploadTaskName.equals(upload.getName())) { + clearConfigurationMappings(upload); + } + }); }); } private void clearConfigurationMappings(Upload upload) { - upload.getRepositories().withType(MavenResolver.class, + upload.getRepositories().withType(org.gradle.api.artifacts.maven.MavenResolver.class, (resolver) -> resolver.getPom().getScopeMappings().getMappings().clear()); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/PluginApplicationAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/PluginApplicationAction.java index 96bccf32a37f..f3584f3943dd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/PluginApplicationAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/PluginApplicationAction.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,9 +30,11 @@ interface PluginApplicationAction extends Action { /** * The class of the {@code Plugin} that, when applied, will trigger the execution of - * this action. May return {@code null} if the plugin class is not on the classpath. - * @return the plugin class or {@code null} + * this action. + * @return the plugin class + * @throws ClassNotFoundException if the plugin class cannot be found + * @throws NoClassDefFoundError if an error occurs when defining the plugin class */ - Class> getPluginClass(); + Class> getPluginClass() throws ClassNotFoundException, NoClassDefFoundError; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ResolveMainClassName.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ResolveMainClassName.java new file mode 100644 index 000000000000..edd8fa710e57 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ResolveMainClassName.java @@ -0,0 +1,206 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.gradle.plugin; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Objects; +import java.util.concurrent.Callable; + +import org.gradle.api.DefaultTask; +import org.gradle.api.InvalidUserDataException; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.Transformer; +import org.gradle.api.file.FileCollection; +import org.gradle.api.file.RegularFile; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.plugins.BasePlugin; +import org.gradle.api.plugins.Convention; +import org.gradle.api.plugins.JavaApplication; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.Classpath; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; +import org.gradle.api.tasks.TaskProvider; + +import org.springframework.boot.gradle.dsl.SpringBootExtension; +import org.springframework.boot.loader.tools.MainClassFinder; + +/** + * {@link Task} for resolving the name of the application's main class. + * + * @author Andy Wilkinson + * @since 2.4 + */ +public class ResolveMainClassName extends DefaultTask { + + private static final String SPRING_BOOT_APPLICATION_CLASS_NAME = "org.springframework.boot.autoconfigure.SpringBootApplication"; + + private final RegularFileProperty outputFile; + + private final Property configuredMainClass; + + private FileCollection classpath; + + /** + * Creates a new instance of the {@code ResolveMainClassName} task. + */ + public ResolveMainClassName() { + this.outputFile = getProject().getObjects().fileProperty(); + this.configuredMainClass = getProject().getObjects().property(String.class); + } + + /** + * Returns the classpath that the task will examine when resolving the main class + * name. + * @return the classpath + */ + @Classpath + public FileCollection getClasspath() { + return this.classpath; + } + + /** + * Sets the classpath that the task will examine when resolving the main class name. + * @param classpath the classpath + */ + public void setClasspath(FileCollection classpath) { + setClasspath((Object) classpath); + } + + /** + * Sets the classpath that the task will examine when resolving the main class name. + * The given {@code classpath} is evaluated as per {@link Project#files(Object...)}. + * @param classpath the classpath + * @since 2.5.10 + */ + public void setClasspath(Object classpath) { + this.classpath = getProject().files(classpath); + } + + /** + * Returns the property for the task's output file that will contain the name of the + * main class. + * @return the output file + */ + @OutputFile + public RegularFileProperty getOutputFile() { + return this.outputFile; + } + + /** + * Returns the property for the explicitly configured main class name that should be + * used in favor of resolving the main class name from the classpath. + * @return the configured main class name property + */ + @Input + @Optional + public Property getConfiguredMainClassName() { + return this.configuredMainClass; + } + + @TaskAction + void resolveAndStoreMainClassName() throws IOException { + File outputFile = this.outputFile.getAsFile().get(); + outputFile.getParentFile().mkdirs(); + String mainClassName = resolveMainClassName(); + Files.write(outputFile.toPath(), mainClassName.getBytes(StandardCharsets.UTF_8), StandardOpenOption.WRITE, + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + } + + private String resolveMainClassName() { + String configuredMainClass = this.configuredMainClass.getOrNull(); + if (configuredMainClass != null) { + return configuredMainClass; + } + return getClasspath().filter(File::isDirectory).getFiles().stream().map(this::findMainClass) + .filter(Objects::nonNull).findFirst().orElse(""); + } + + private String findMainClass(File file) { + try { + return MainClassFinder.findSingleMainClass(file, SPRING_BOOT_APPLICATION_CLASS_NAME); + } + catch (IOException ex) { + return null; + } + } + + Provider readMainClassName() { + return this.outputFile.map(new ClassNameReader()); + } + + static TaskProvider registerForTask(String taskName, Project project, + Callable classpath) { + TaskProvider resolveMainClassNameProvider = project.getTasks() + .register(taskName + "MainClassName", ResolveMainClassName.class, (resolveMainClassName) -> { + Convention convention = project.getConvention(); + resolveMainClassName.setDescription( + "Resolves the name of the application's main class for the " + taskName + " task."); + resolveMainClassName.setGroup(BasePlugin.BUILD_GROUP); + resolveMainClassName.setClasspath(classpath); + resolveMainClassName.getConfiguredMainClassName().convention(project.provider(() -> { + String javaApplicationMainClass = getJavaApplicationMainClass(convention); + if (javaApplicationMainClass != null) { + return javaApplicationMainClass; + } + SpringBootExtension springBootExtension = project.getExtensions() + .findByType(SpringBootExtension.class); + return springBootExtension.getMainClass().getOrNull(); + })); + resolveMainClassName.getOutputFile() + .set(project.getLayout().getBuildDirectory().file(taskName + "MainClassName")); + }); + return resolveMainClassNameProvider; + } + + private static String getJavaApplicationMainClass(Convention convention) { + JavaApplication javaApplication = convention.findByType(JavaApplication.class); + if (javaApplication == null) { + return null; + } + return javaApplication.getMainClass().getOrNull(); + } + + private static final class ClassNameReader implements Transformer { + + @Override + public String transform(RegularFile file) { + if (file.getAsFile().length() == 0) { + throw new InvalidUserDataException( + "Main class name has not been configured and it could not be resolved"); + } + Path output = file.getAsFile().toPath(); + try { + return new String(Files.readAllBytes(output), StandardCharsets.UTF_8); + } + catch (IOException ex) { + throw new RuntimeException("Failed to read main class name from '" + output + "'"); + } + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SinglePublishedArtifact.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SinglePublishedArtifact.java index eb107ca50c66..8c9b7a6905f7 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SinglePublishedArtifact.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SinglePublishedArtifact.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,37 +17,55 @@ package org.springframework.boot.gradle.plugin; import org.gradle.api.Buildable; +import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.PublishArtifact; import org.gradle.api.artifacts.PublishArtifactSet; +import org.gradle.api.artifacts.dsl.ArtifactHandler; import org.gradle.api.tasks.TaskDependency; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.api.tasks.bundling.Jar; + +import org.springframework.boot.gradle.tasks.bundling.BootJar; +import org.springframework.boot.gradle.tasks.bundling.BootWar; /** * A wrapper for a {@link PublishArtifactSet} that ensures that only a single artifact is * published, with a war file taking precedence over a jar file. * * @author Andy Wilkinson + * @author Scott Frederick */ final class SinglePublishedArtifact implements Buildable { - private final PublishArtifactSet artifacts; + private final Configuration configuration; + + private final ArtifactHandler handler; private PublishArtifact currentArtifact; - SinglePublishedArtifact(PublishArtifactSet artifacts) { - this.artifacts = artifacts; + SinglePublishedArtifact(Configuration configuration, ArtifactHandler handler) { + this.configuration = configuration; + this.handler = handler; + } + + void addWarCandidate(TaskProvider candidate) { + add(candidate); } - void addCandidate(PublishArtifact candidate) { - if (this.currentArtifact == null || "war".equals(candidate.getExtension())) { - this.artifacts.remove(this.currentArtifact); - this.artifacts.add(candidate); - this.currentArtifact = candidate; + void addJarCandidate(TaskProvider candidate) { + if (this.currentArtifact == null) { + add(candidate); } } + private void add(TaskProvider artifact) { + this.configuration.getArtifacts().remove(this.currentArtifact); + this.currentArtifact = this.handler.add(this.configuration.getName(), artifact); + } + @Override public TaskDependency getBuildDependencies() { - return this.currentArtifact.getBuildDependencies(); + return this.configuration.getArtifacts().getBuildDependencies(); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootPlugin.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootPlugin.java index 10433d672ccf..0403a324ad26 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootPlugin.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootPlugin.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,26 +16,21 @@ package org.springframework.boot.gradle.plugin; -import java.io.File; -import java.io.IOException; -import java.net.JarURLConnection; -import java.net.URL; -import java.net.URLConnection; import java.util.Arrays; import java.util.List; -import java.util.jar.Attributes; -import java.util.jar.JarFile; +import java.util.function.Consumer; import org.gradle.api.GradleException; import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.artifacts.Configuration; -import org.gradle.api.artifacts.ResolvableDependencies; import org.gradle.util.GradleVersion; import org.springframework.boot.gradle.dsl.SpringBootExtension; +import org.springframework.boot.gradle.tasks.bundling.BootBuildImage; import org.springframework.boot.gradle.tasks.bundling.BootJar; import org.springframework.boot.gradle.tasks.bundling.BootWar; +import org.springframework.boot.gradle.util.VersionExtractor; /** * Gradle plugin for Spring Boot. @@ -44,11 +39,12 @@ * @author Dave Syer * @author Andy Wilkinson * @author Danny Hyun + * @author Scott Frederick * @since 1.2.7 */ public class SpringBootPlugin implements Plugin { - private static final String SPRING_BOOT_VERSION = determineSpringBootVersion(); + private static final String SPRING_BOOT_VERSION = VersionExtractor.forClass(DependencyManagementPluginAction.class); /** * The name of the {@link Configuration} that contains Spring Boot archives. @@ -68,6 +64,23 @@ public class SpringBootPlugin implements Plugin { */ public static final String BOOT_WAR_TASK_NAME = "bootWar"; + /** + * The name of the default {@link BootBuildImage} task. + * @since 2.3.0 + */ + public static final String BOOT_BUILD_IMAGE_TASK_NAME = "bootBuildImage"; + + /** + * The name of the {@code developmentOnly} configuration. + * @since 2.3.0 + */ + public static final String DEVELOPMENT_ONLY_CONFIGURATION_NAME = "developmentOnly"; + + /** + * The name of the {@code productionRuntimeClasspath} configuration. + */ + public static final String PRODUCTION_RUNTIME_CLASSPATH_CONFIGURATION_NAME = "productionRuntimeClasspath"; + /** * The coordinates {@code (group:name:version)} of the * {@code spring-boot-dependencies} bom. @@ -81,13 +94,13 @@ public void apply(Project project) { createExtension(project); Configuration bootArchives = createBootArchivesConfiguration(project); registerPluginActions(project, bootArchives); - unregisterUnresolvedDependenciesAnalyzer(project); } private void verifyGradleVersion() { - if (GradleVersion.current().compareTo(GradleVersion.version("4.10")) < 0) { - throw new GradleException("Spring Boot plugin requires Gradle 4.10 or later. The current version is " - + GradleVersion.current()); + GradleVersion currentVersion = GradleVersion.current(); + if (currentVersion.compareTo(GradleVersion.version("6.8")) < 0) { + throw new GradleException("Spring Boot plugin requires Gradle 6.8.x, 6.9.x, or 7.x. " + + "The current version is " + currentVersion); } } @@ -98,59 +111,31 @@ private void createExtension(Project project) { private Configuration createBootArchivesConfiguration(Project project) { Configuration bootArchives = project.getConfigurations().create(BOOT_ARCHIVES_CONFIGURATION_NAME); bootArchives.setDescription("Configuration for Spring Boot archive artifacts."); + bootArchives.setCanBeResolved(false); return bootArchives; } private void registerPluginActions(Project project, Configuration bootArchives) { - SinglePublishedArtifact singlePublishedArtifact = new SinglePublishedArtifact(bootArchives.getArtifacts()); + SinglePublishedArtifact singlePublishedArtifact = new SinglePublishedArtifact(bootArchives, + project.getArtifacts()); + @SuppressWarnings("deprecation") List actions = Arrays.asList(new JavaPluginAction(singlePublishedArtifact), new WarPluginAction(singlePublishedArtifact), new MavenPluginAction(bootArchives.getUploadTaskName()), new DependencyManagementPluginAction(), new ApplicationPluginAction(), new KotlinPluginAction()); for (PluginApplicationAction action : actions) { - Class> pluginClass = action.getPluginClass(); - if (pluginClass != null) { - project.getPlugins().withType(pluginClass, (plugin) -> action.execute(project)); - } + withPluginClassOfAction(action, + (pluginClass) -> project.getPlugins().withType(pluginClass, (plugin) -> action.execute(project))); } } - private void unregisterUnresolvedDependenciesAnalyzer(Project project) { - UnresolvedDependenciesAnalyzer unresolvedDependenciesAnalyzer = new UnresolvedDependenciesAnalyzer(); - project.getConfigurations().all((configuration) -> { - ResolvableDependencies incoming = configuration.getIncoming(); - incoming.afterResolve((resolvableDependencies) -> { - if (incoming.equals(resolvableDependencies)) { - unresolvedDependenciesAnalyzer.analyze(configuration.getResolvedConfiguration() - .getLenientConfiguration().getUnresolvedModuleDependencies()); - } - }); - }); - project.getGradle().buildFinished((buildResult) -> unresolvedDependenciesAnalyzer.buildFinished(project)); - } - - private static String determineSpringBootVersion() { - String implementationVersion = DependencyManagementPluginAction.class.getPackage().getImplementationVersion(); - if (implementationVersion != null) { - return implementationVersion; - } - URL codeSourceLocation = DependencyManagementPluginAction.class.getProtectionDomain().getCodeSource() - .getLocation(); + private void withPluginClassOfAction(PluginApplicationAction action, + Consumer>> consumer) { try { - URLConnection connection = codeSourceLocation.openConnection(); - if (connection instanceof JarURLConnection) { - return getImplementationVersion(((JarURLConnection) connection).getJarFile()); - } - try (JarFile jarFile = new JarFile(new File(codeSourceLocation.toURI()))) { - return getImplementationVersion(jarFile); - } + consumer.accept(action.getPluginClass()); } - catch (Exception ex) { - return null; + catch (Throwable ex) { + // Plugin class unavailable. Continue. } } - private static String getImplementationVersion(JarFile jarFile) throws IOException { - return jarFile.getManifest().getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION); - } - } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/UnresolvedDependenciesAnalyzer.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/UnresolvedDependenciesAnalyzer.java deleted file mode 100644 index 8da6fdbef5a3..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/UnresolvedDependenciesAnalyzer.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.gradle.plugin; - -import java.util.HashSet; -import java.util.Set; -import java.util.stream.Collectors; - -import io.spring.gradle.dependencymanagement.DependencyManagementPlugin; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.gradle.api.Project; -import org.gradle.api.artifacts.ModuleVersionSelector; -import org.gradle.api.artifacts.UnresolvedDependency; - -/** - * An analyzer for {@link UnresolvedDependency unresolvable dependencies} that logs a - * warning suggesting that the {@code io.spring.dependency-management} plugin is applied - * when one or more versionless dependencies fails to resolve. - * - * @author Andy Wilkinson - */ -class UnresolvedDependenciesAnalyzer { - - private static final Log logger = LogFactory.getLog(SpringBootPlugin.class); - - private Set dependenciesWithNoVersion = new HashSet<>(); - - void analyze(Set unresolvedDependencies) { - this.dependenciesWithNoVersion = unresolvedDependencies.stream() - .map((unresolvedDependency) -> unresolvedDependency.getSelector()).filter(this::hasNoVersion) - .collect(Collectors.toSet()); - } - - void buildFinished(Project project) { - if (!this.dependenciesWithNoVersion.isEmpty() - && !project.getPlugins().hasPlugin(DependencyManagementPlugin.class)) { - StringBuilder message = new StringBuilder(); - message.append("\nDuring the build, one or more dependencies that were " - + "declared without a version failed to resolve:\n"); - this.dependenciesWithNoVersion - .forEach((dependency) -> message.append(" ").append(dependency).append("\n")); - message.append("\nDid you forget to apply the io.spring.dependency-management plugin to the "); - message.append(project.getName()).append(" project?\n"); - logger.warn(message.toString()); - } - } - - private boolean hasNoVersion(ModuleVersionSelector selector) { - String version = selector.getVersion(); - return version == null || version.trim().isEmpty(); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/WarPluginAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/WarPluginAction.java index b406948c8f5b..acc9f19793fd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/WarPluginAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/WarPluginAction.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,20 +16,30 @@ package org.springframework.boot.gradle.plugin; +import java.util.concurrent.Callable; + import org.gradle.api.Action; import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.artifacts.Configuration; -import org.gradle.api.internal.artifacts.publish.ArchivePublishArtifact; +import org.gradle.api.artifacts.ConfigurationContainer; +import org.gradle.api.file.FileCollection; import org.gradle.api.plugins.BasePlugin; import org.gradle.api.plugins.WarPlugin; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.api.tasks.bundling.War; +import org.springframework.boot.gradle.tasks.bundling.BootBuildImage; import org.springframework.boot.gradle.tasks.bundling.BootWar; /** * {@link Action} that is executed in response to the {@link WarPlugin} being applied. * * @author Andy Wilkinson + * @author Scott Frederick */ class WarPluginAction implements PluginApplicationAction { @@ -46,19 +56,57 @@ public Class> getPluginClass() { @Override public void execute(Project project) { - project.getTasks().getByName(WarPlugin.WAR_TASK_NAME).setEnabled(false); - BootWar bootWar = project.getTasks().create(SpringBootPlugin.BOOT_WAR_TASK_NAME, BootWar.class); - bootWar.setGroup(BasePlugin.BUILD_GROUP); - bootWar.setDescription("Assembles an executable war archive containing webapp" - + " content, and the main classes and their dependencies."); - bootWar.providedClasspath(providedRuntimeConfiguration(project)); - ArchivePublishArtifact artifact = new ArchivePublishArtifact(bootWar); - this.singlePublishedArtifact.addCandidate(artifact); - bootWar.conventionMapping("mainClassName", new MainClassConvention(project, bootWar::getClasspath)); + classifyWarTask(project); + TaskProvider bootWar = configureBootWarTask(project); + configureBootBuildImageTask(project, bootWar); + configureArtifactPublication(bootWar); + } + + private void classifyWarTask(Project project) { + project.getTasks().named(WarPlugin.WAR_TASK_NAME, War.class) + .configure((war) -> war.getArchiveClassifier().convention("plain")); + } + + private TaskProvider configureBootWarTask(Project project) { + Configuration developmentOnly = project.getConfigurations() + .getByName(SpringBootPlugin.DEVELOPMENT_ONLY_CONFIGURATION_NAME); + Configuration productionRuntimeClasspath = project.getConfigurations() + .getByName(SpringBootPlugin.PRODUCTION_RUNTIME_CLASSPATH_CONFIGURATION_NAME); + Callable classpath = () -> project.getConvention().getByType(SourceSetContainer.class) + .getByName(SourceSet.MAIN_SOURCE_SET_NAME).getRuntimeClasspath() + .minus(providedRuntimeConfiguration(project)).minus((developmentOnly.minus(productionRuntimeClasspath))) + .filter(new JarTypeFileSpec()); + TaskProvider resolveMainClassName = ResolveMainClassName + .registerForTask(SpringBootPlugin.BOOT_WAR_TASK_NAME, project, classpath); + TaskProvider bootWarProvider = project.getTasks().register(SpringBootPlugin.BOOT_WAR_TASK_NAME, + BootWar.class, (bootWar) -> { + bootWar.setGroup(BasePlugin.BUILD_GROUP); + bootWar.setDescription("Assembles an executable war archive containing webapp" + + " content, and the main classes and their dependencies."); + bootWar.providedClasspath(providedRuntimeConfiguration(project)); + bootWar.setClasspath(classpath); + Provider manifestStartClass = project + .provider(() -> (String) bootWar.getManifest().getAttributes().get("Start-Class")); + bootWar.getMainClass() + .convention(resolveMainClassName.flatMap((resolver) -> manifestStartClass.isPresent() + ? manifestStartClass : resolveMainClassName.get().readMainClassName())); + }); + bootWarProvider.map((bootWar) -> bootWar.getClasspath()); + return bootWarProvider; + } + + private FileCollection providedRuntimeConfiguration(Project project) { + ConfigurationContainer configurations = project.getConfigurations(); + return configurations.getByName(WarPlugin.PROVIDED_RUNTIME_CONFIGURATION_NAME); + } + + private void configureBootBuildImageTask(Project project, TaskProvider bootWar) { + project.getTasks().named(SpringBootPlugin.BOOT_BUILD_IMAGE_TASK_NAME, BootBuildImage.class) + .configure((buildImage) -> buildImage.getArchiveFile().set(bootWar.get().getArchiveFile())); } - private Configuration providedRuntimeConfiguration(Project project) { - return project.getConfigurations().getByName(WarPlugin.PROVIDED_RUNTIME_CONFIGURATION_NAME); + private void configureArtifactPublication(TaskProvider bootWar) { + this.singlePublishedArtifact.addWarCandidate(bootWar); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/application/CreateBootStartScripts.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/application/CreateBootStartScripts.java index 634904624198..2fd040e7c8d4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/application/CreateBootStartScripts.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/application/CreateBootStartScripts.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,9 @@ * * @author Andy Wilkinson * @since 2.0.0 + * @deprecated since 2.5.10 for removal in 2.8.0 in favor of {@link CreateStartScripts}. */ +@Deprecated public class CreateBootStartScripts extends CreateStartScripts { @Override diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfo.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfo.java index b81f7463c896..2246248790fd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfo.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,8 +24,9 @@ import org.gradle.api.Action; import org.gradle.api.Project; import org.gradle.api.Task; +import org.gradle.api.file.DirectoryProperty; import org.gradle.api.internal.ConventionTask; -import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.OutputDirectory; import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.TaskExecutionException; @@ -44,7 +45,12 @@ public class BuildInfo extends ConventionTask { private final BuildInfoProperties properties = new BuildInfoProperties(getProject()); - private File destinationDir; + private final DirectoryProperty destinationDir; + + public BuildInfo() { + this.destinationDir = getProject().getObjects().directoryProperty() + .convention(getProject().getLayout().getBuildDirectory()); + } /** * Generates the {@code build-info.properties} file in the configured @@ -73,7 +79,7 @@ public void generateBuildProperties() { */ @OutputDirectory public File getDestinationDir() { - return (this.destinationDir != null) ? this.destinationDir : getProject().getBuildDir(); + return this.destinationDir.getAsFile().get(); } /** @@ -81,7 +87,7 @@ public File getDestinationDir() { * @param destinationDir the destination directory */ public void setDestinationDir(File destinationDir) { - this.destinationDir = destinationDir; + this.destinationDir.set(destinationDir); } /** @@ -89,7 +95,7 @@ public void setDestinationDir(File destinationDir) { * {@code build-info.properties} file. * @return the properties */ - @Input + @Nested public BuildInfoProperties getProperties() { return this.properties; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoProperties.java index 5d2041a547e6..d3a0bcedfbc3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoProperties.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,18 @@ package org.springframework.boot.gradle.tasks.buildinfo; +import java.io.IOException; +import java.io.ObjectInputStream; import java.io.Serializable; import java.time.Instant; import java.util.HashMap; import java.util.Map; import org.gradle.api.Project; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Optional; /** * The properties that are written into the {@code build-info.properties} file. @@ -32,23 +38,38 @@ @SuppressWarnings("serial") public class BuildInfoProperties implements Serializable { - private final transient Project project; + private transient Instant creationTime = Instant.now(); - private String group; + private final Property group; - private String artifact; + private final Property artifact; - private String version; + private final Property version; - private String name; + private final Property name; - private Instant time; + private final Property time; + + private boolean timeConfigured = false; private Map additionalProperties = new HashMap<>(); BuildInfoProperties(Project project) { - this.project = project; - this.time = Instant.now(); + this.time = project.getObjects().property(Long.class); + this.group = project.getObjects().property(String.class); + this.group.set(project.provider(() -> project.getGroup().toString())); + this.artifact = project.getObjects().property(String.class); + this.version = project.getObjects().property(String.class); + this.version.set(projectVersion(project)); + this.name = project.getObjects().property(String.class); + this.name.set(project.provider(project::getName)); + } + + private Provider projectVersion(Project project) { + Provider externalVersionProperty = project.getProviders().gradleProperty("version") + .forUseAtConfigurationTime(); + externalVersionProperty.getOrNull(); + return project.provider(() -> project.getVersion().toString()); } /** @@ -56,11 +77,10 @@ public class BuildInfoProperties implements Serializable { * {@link Project#getGroup() Project's group}. * @return the group */ + @Input + @Optional public String getGroup() { - if (this.group == null) { - this.group = this.project.getGroup().toString(); - } - return this.group; + return this.group.getOrNull(); } /** @@ -68,15 +88,17 @@ public String getGroup() { * @param group the group name */ public void setGroup(String group) { - this.group = group; + this.group.set(group); } /** * Returns the value used for the {@code build.artifact} property. * @return the artifact */ + @Input + @Optional public String getArtifact() { - return this.artifact; + return this.artifact.getOrNull(); } /** @@ -84,7 +106,7 @@ public String getArtifact() { * @param artifact the artifact */ public void setArtifact(String artifact) { - this.artifact = artifact; + this.artifact.set(artifact); } /** @@ -92,11 +114,10 @@ public void setArtifact(String artifact) { * {@link Project#getVersion() Project's version}. * @return the version */ + @Input + @Optional public String getVersion() { - if (this.version == null) { - this.version = this.project.getVersion().toString(); - } - return this.version; + return this.version.getOrNull(); } /** @@ -104,7 +125,7 @@ public String getVersion() { * @param version the version */ public void setVersion(String version) { - this.version = version; + this.version.set(version); } /** @@ -112,11 +133,10 @@ public void setVersion(String version) { * {@link Project#getDisplayName() Project's display name}. * @return the name */ + @Input + @Optional public String getName() { - if (this.name == null) { - this.name = this.project.getName(); - } - return this.name; + return this.name.getOrNull(); } /** @@ -124,7 +144,7 @@ public String getName() { * @param name the name */ public void setName(String name) { - this.name = name; + this.name.set(name); } /** @@ -132,8 +152,17 @@ public void setName(String name) { * {@link Instant#now} when the {@code BuildInfoProperties} instance was created. * @return the time */ + @Input + @Optional public Instant getTime() { - return this.time; + Long epochMillis = this.time.getOrNull(); + if (epochMillis != null) { + return Instant.ofEpochMilli(epochMillis); + } + if (this.timeConfigured) { + return null; + } + return this.creationTime; } /** @@ -141,7 +170,8 @@ public Instant getTime() { * @param time the build time */ public void setTime(Instant time) { - this.time = time; + this.timeConfigured = true; + this.time.set((time != null) ? time.toEpochMilli() : null); } /** @@ -149,6 +179,8 @@ public void setTime(Instant time) { * each additional property is prefixed with {@code build.}. * @return the additional properties */ + @Input + @Optional public Map getAdditional() { return this.additionalProperties; } @@ -162,46 +194,9 @@ public void setAdditional(Map additionalProperties) { this.additionalProperties = additionalProperties; } - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - BuildInfoProperties other = (BuildInfoProperties) obj; - boolean result = true; - result = result && nullSafeEquals(this.additionalProperties, other.additionalProperties); - result = result && nullSafeEquals(this.artifact, other.artifact); - result = result && nullSafeEquals(this.group, other.group); - result = result && nullSafeEquals(this.name, other.name); - result = result && nullSafeEquals(this.version, other.version); - result = result && nullSafeEquals(this.time, other.time); - return result; - } - - private boolean nullSafeEquals(Object o1, Object o2) { - if (o1 == o2) { - return true; - } - if (o1 == null || o2 == null) { - return false; - } - return (o1.equals(o2)); - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((this.additionalProperties == null) ? 0 : this.additionalProperties.hashCode()); - result = prime * result + ((this.artifact == null) ? 0 : this.artifact.hashCode()); - result = prime * result + ((this.group == null) ? 0 : this.group.hashCode()); - result = prime * result + ((this.name == null) ? 0 : this.name.hashCode()); - result = prime * result + ((this.version == null) ? 0 : this.version.hashCode()); - result = prime * result + ((this.time == null) ? 0 : this.time.hashCode()); - return result; + private void readObject(ObjectInputStream input) throws ClassNotFoundException, IOException { + input.defaultReadObject(); + this.creationTime = Instant.now(); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchive.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchive.java index bf2403a4a711..758295ababb3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchive.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchive.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,9 +21,12 @@ import org.gradle.api.Task; import org.gradle.api.file.FileCollection; import org.gradle.api.file.FileTreeElement; +import org.gradle.api.model.ReplacedBy; +import org.gradle.api.provider.Property; import org.gradle.api.specs.Spec; import org.gradle.api.tasks.Classpath; import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.Optional; /** @@ -35,16 +38,29 @@ public interface BootArchive extends Task { /** - * Returns the name of the main class of the application. - * @return the main class name + * Returns the fully-qualified name of the application's main class. + * @return the fully-qualified name of the application's main class + * @since 2.4.0 */ @Input + Property getMainClass(); + + /** + * Returns the fully-qualified main class name of the application. + * @return the fully-qualified name of the application's main class + * @deprecated since 2.4.0 for removal in 2.6.0 in favor of {@link #getMainClass()}. + */ + @Deprecated + @ReplacedBy("mainClass") String getMainClassName(); /** - * Sets the name of the main class of the application. - * @param mainClassName the name of the main class of the application + * Sets the fully-qualified main class name of the application. + * @param mainClassName the fully-qualified name of the application's main class + * @deprecated since 2.4.0 for removal in 2.6.0 in favor of {@link #getMainClass} and + * {@link Property#set(Object)} */ + @Deprecated void setMainClassName(String mainClassName); /** @@ -67,7 +83,7 @@ public interface BootArchive extends Task { * @return the launch script configuration, or {@code null} if the launch script has * not been configured. */ - @Input + @Nested @Optional LaunchScriptConfiguration getLaunchScript(); @@ -113,20 +129,4 @@ public interface BootArchive extends Task { */ void setClasspath(FileCollection classpath); - /** - * Returns {@code true} if the Devtools jar should be excluded, otherwise - * {@code false}. - * @return {@code true} if the Devtools jar should be excluded, or {@code false} if - * not - */ - @Input - boolean isExcludeDevtools(); - - /** - * Sets whether or not the Devtools jar should be excluded. - * @param excludeDevtools {@code true} if the Devtools jar should be excluded, or - * {@code false} if not - */ - void setExcludeDevtools(boolean excludeDevtools); - } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java index 330b31439858..a143a63647f0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import java.util.TreeMap; import java.util.function.Function; +import org.gradle.api.file.CopySpec; import org.gradle.api.file.FileCopyDetails; import org.gradle.api.file.FileTreeElement; import org.gradle.api.file.RelativePath; @@ -34,6 +35,7 @@ import org.gradle.api.internal.file.copy.CopyActionProcessingStream; import org.gradle.api.internal.file.copy.FileCopyDetailsInternal; import org.gradle.api.java.archives.Attributes; +import org.gradle.api.java.archives.Manifest; import org.gradle.api.specs.Spec; import org.gradle.api.specs.Specs; import org.gradle.api.tasks.WorkResult; @@ -44,6 +46,9 @@ * Support class for implementations of {@link BootArchive}. * * @author Andy Wilkinson + * @author Phillip Webb + * @see BootJar + * @see BootWar */ class BootArchiveSupport { @@ -61,45 +66,64 @@ class BootArchiveSupport { private final PatternSet requiresUnpack = new PatternSet(); - private final Function compressionResolver; - private final PatternSet exclusions = new PatternSet(); private final String loaderMainClass; - private LaunchScriptConfiguration launchScript; + private final Spec librarySpec; - private boolean excludeDevtools = true; + private final Function compressionResolver; - BootArchiveSupport(String loaderMainClass, Function compressionResolver) { + private LaunchScriptConfiguration launchScript; + + BootArchiveSupport(String loaderMainClass, Spec librarySpec, + Function compressionResolver) { this.loaderMainClass = loaderMainClass; + this.librarySpec = librarySpec; this.compressionResolver = compressionResolver; this.requiresUnpack.include(Specs.satisfyNone()); - configureExclusions(); } - void configureManifest(Jar jar, String mainClassName, String springBootClasses, String springBootLib) { - Attributes attributes = jar.getManifest().getAttributes(); + void configureManifest(Manifest manifest, String mainClass, String classes, String lib, String classPathIndex, + String layersIndex) { + Attributes attributes = manifest.getAttributes(); attributes.putIfAbsent("Main-Class", this.loaderMainClass); - attributes.putIfAbsent("Start-Class", mainClassName); - attributes.computeIfAbsent("Spring-Boot-Version", (key) -> determineSpringBootVersion()); - attributes.putIfAbsent("Spring-Boot-Classes", springBootClasses); - attributes.putIfAbsent("Spring-Boot-Lib", springBootLib); + attributes.putIfAbsent("Start-Class", mainClass); + attributes.computeIfAbsent("Spring-Boot-Version", (name) -> determineSpringBootVersion()); + attributes.putIfAbsent("Spring-Boot-Classes", classes); + attributes.putIfAbsent("Spring-Boot-Lib", lib); + if (classPathIndex != null) { + attributes.putIfAbsent("Spring-Boot-Classpath-Index", classPathIndex); + } + if (layersIndex != null) { + attributes.putIfAbsent("Spring-Boot-Layers-Index", layersIndex); + } } private String determineSpringBootVersion() { - String implementationVersion = getClass().getPackage().getImplementationVersion(); - return (implementationVersion != null) ? implementationVersion : "unknown"; + String version = getClass().getPackage().getImplementationVersion(); + return (version != null) ? version : "unknown"; } CopyAction createCopyAction(Jar jar) { - CopyAction copyAction = new BootZipCopyAction(jar.getArchivePath(), jar.isPreserveFileTimestamps(), - isUsingDefaultLoader(jar), this.requiresUnpack.getAsSpec(), this.exclusions.getAsExcludeSpec(), - this.launchScript, this.compressionResolver, jar.getMetadataCharset()); - if (!jar.isReproducibleFileOrder()) { - return copyAction; - } - return new ReproducibleOrderingCopyAction(copyAction); + return createCopyAction(jar, null, null); + } + + CopyAction createCopyAction(Jar jar, LayerResolver layerResolver, String layerToolsLocation) { + File output = jar.getArchiveFile().get().getAsFile(); + Manifest manifest = jar.getManifest(); + boolean preserveFileTimestamps = jar.isPreserveFileTimestamps(); + boolean includeDefaultLoader = isUsingDefaultLoader(jar); + Spec requiresUnpack = this.requiresUnpack.getAsSpec(); + Spec exclusions = this.exclusions.getAsExcludeSpec(); + LaunchScriptConfiguration launchScript = this.launchScript; + Spec librarySpec = this.librarySpec; + Function compressionResolver = this.compressionResolver; + String encoding = jar.getMetadataCharset(); + CopyAction action = new BootZipCopyAction(output, manifest, preserveFileTimestamps, includeDefaultLoader, + layerToolsLocation, requiresUnpack, exclusions, launchScript, librarySpec, compressionResolver, + encoding, layerResolver); + return jar.isReproducibleFileOrder() ? new ReproducibleOrderingCopyAction(action) : action; } private boolean isUsingDefaultLoader(Jar jar) { @@ -122,16 +146,19 @@ void requiresUnpack(Spec spec) { this.requiresUnpack.include(spec); } - boolean isExcludeDevtools() { - return this.excludeDevtools; + void excludeNonZipLibraryFiles(FileCopyDetails details) { + if (this.librarySpec.isSatisfiedBy(details)) { + excludeNonZipFiles(details); + } } - void setExcludeDevtools(boolean excludeDevtools) { - this.excludeDevtools = excludeDevtools; - configureExclusions(); + void excludeNonZipFiles(FileCopyDetails details) { + if (!isZip(details.getFile())) { + details.exclude(); + } } - boolean isZip(File file) { + private boolean isZip(File file) { try { try (FileInputStream fileInputStream = new FileInputStream(file)) { return isZip(fileInputStream); @@ -151,14 +178,17 @@ private boolean isZip(InputStream inputStream) throws IOException { return true; } - private void configureExclusions() { - Set excludes = new HashSet<>(); - if (this.excludeDevtools) { - excludes.add("**/spring-boot-devtools-*.jar"); - } - this.exclusions.setExcludes(excludes); + void moveModuleInfoToRoot(CopySpec spec) { + spec.filesMatching("module-info.class", this::moveToRoot); + } + + void moveToRoot(FileCopyDetails details) { + details.setRelativePath(details.getRelativeSourcePath()); } + /** + * {@link CopyAction} variant that sorts entries to ensure reproducible ordering. + */ private static final class ReproducibleOrderingCopyAction implements CopyAction { private final CopyAction delegate; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java new file mode 100644 index 000000000000..bdd8cdb2e231 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java @@ -0,0 +1,513 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.gradle.tasks.bundling; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import groovy.lang.Closure; +import org.gradle.api.Action; +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.JavaVersion; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Nested; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.TaskAction; +import org.gradle.api.tasks.options.Option; +import org.gradle.util.ConfigureUtil; + +import org.springframework.boot.buildpack.platform.build.BuildRequest; +import org.springframework.boot.buildpack.platform.build.Builder; +import org.springframework.boot.buildpack.platform.build.BuildpackReference; +import org.springframework.boot.buildpack.platform.build.Creator; +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; +import org.springframework.boot.buildpack.platform.docker.type.ImageName; +import org.springframework.boot.buildpack.platform.docker.type.ImageReference; +import org.springframework.boot.buildpack.platform.io.ZipFileTarArchive; +import org.springframework.boot.gradle.util.VersionExtractor; +import org.springframework.util.StringUtils; + +/** + * A {@link Task} for bundling an application into an OCI image using a + * buildpack. + * + * @author Andy Wilkinson + * @author Scott Frederick + * @since 2.3.0 + */ +public class BootBuildImage extends DefaultTask { + + private static final String BUILDPACK_JVM_VERSION_KEY = "BP_JVM_VERSION"; + + private final String projectName; + + private final Property projectVersion; + + private RegularFileProperty archiveFile; + + private Property targetJavaVersion; + + private String imageName; + + private String builder; + + private String runImage; + + private Map environment = new HashMap<>(); + + private boolean cleanCache; + + private boolean verboseLogging; + + private PullPolicy pullPolicy; + + private boolean publish; + + private final ListProperty buildpacks; + + private final ListProperty bindings; + + private final DockerSpec docker = new DockerSpec(); + + public BootBuildImage() { + this.archiveFile = getProject().getObjects().fileProperty(); + this.targetJavaVersion = getProject().getObjects().property(JavaVersion.class); + this.projectName = getProject().getName(); + this.projectVersion = getProject().getObjects().property(String.class); + Project project = getProject(); + this.projectVersion.set(getProject().provider(() -> project.getVersion().toString())); + this.buildpacks = getProject().getObjects().listProperty(String.class); + this.bindings = getProject().getObjects().listProperty(String.class); + } + + /** + * Returns the property for the archive file from which the image will be built. + * @return the archive file property + */ + @Input + public RegularFileProperty getArchiveFile() { + return this.archiveFile; + } + + /** + * Returns the property for the archive file from which the image will be built. + * @return the archive file property + * @deprecated since 2.5.0 for removal in 2.7.0 in favor of {@link #getArchiveFile()} + */ + @Deprecated + @Input + public RegularFileProperty getJar() { + return this.archiveFile; + } + + /** + * Returns the target Java version of the project (e.g. as provided by the + * {@code targetCompatibility} build property). + * @return the target Java version + */ + @Input + @Optional + public Property getTargetJavaVersion() { + return this.targetJavaVersion; + } + + /** + * Returns the name of the image that will be built. When {@code null}, the name will + * be derived from the {@link Project Project's} {@link Project#getName() name} and + * {@link Project#getVersion version}. + * @return name of the image + */ + @Input + @Optional + public String getImageName() { + return this.imageName; + } + + /** + * Sets the name of the image that will be built. + * @param imageName name of the image + */ + @Option(option = "imageName", description = "The name of the image to generate") + public void setImageName(String imageName) { + this.imageName = imageName; + } + + /** + * Returns the builder that will be used to build the image. When {@code null}, the + * default builder will be used. + * @return the builder + */ + @Input + @Optional + public String getBuilder() { + return this.builder; + } + + /** + * Sets the builder that will be used to build the image. + * @param builder the builder + */ + @Option(option = "builder", description = "The name of the builder image to use") + public void setBuilder(String builder) { + this.builder = builder; + } + + /** + * Returns the run image that will be included in the built image. When {@code null}, + * the run image bundled with the builder will be used. + * @return the run image + */ + @Input + @Optional + public String getRunImage() { + return this.runImage; + } + + /** + * Sets the run image that will be included in the built image. + * @param runImage the run image + */ + @Option(option = "runImage", description = "The name of the run image to use") + public void setRunImage(String runImage) { + this.runImage = runImage; + } + + /** + * Returns the environment that will be used when building the image. + * @return the environment + */ + @Input + public Map getEnvironment() { + return this.environment; + } + + /** + * Sets the environment that will be used when building the image. + * @param environment the environment + */ + public void setEnvironment(Map environment) { + this.environment = environment; + } + + /** + * Add an entry to the environment that will be used when building the image. + * @param name the name of the entry + * @param value the value of the entry + */ + public void environment(String name, String value) { + this.environment.put(name, value); + } + + /** + * Adds entries to the environment that will be used when building the image. + * @param entries the entries to add to the environment + */ + public void environment(Map entries) { + this.environment.putAll(entries); + } + + /** + * Returns whether caches should be cleaned before packaging. + * @return whether caches should be cleaned + */ + @Input + public boolean isCleanCache() { + return this.cleanCache; + } + + /** + * Sets whether caches should be cleaned before packaging. + * @param cleanCache {@code true} to clean the cache, otherwise {@code false}. + */ + @Option(option = "cleanCache", description = "Clean caches before packaging") + public void setCleanCache(boolean cleanCache) { + this.cleanCache = cleanCache; + } + + /** + * Whether verbose logging should be enabled while building the image. + * @return whether verbose logging should be enabled + */ + @Input + public boolean isVerboseLogging() { + return this.verboseLogging; + } + + /** + * Sets whether verbose logging should be enabled while building the image. + * @param verboseLogging {@code true} to enable verbose logging, otherwise + * {@code false}. + */ + public void setVerboseLogging(boolean verboseLogging) { + this.verboseLogging = verboseLogging; + } + + /** + * Returns image pull policy that will be used when building the image. + * @return whether images should be pulled + */ + @Input + @Optional + public PullPolicy getPullPolicy() { + return this.pullPolicy; + } + + /** + * Sets image pull policy that will be used when building the image. + * @param pullPolicy image pull policy {@link PullPolicy} + */ + @Option(option = "pullPolicy", description = "The image pull policy") + public void setPullPolicy(PullPolicy pullPolicy) { + this.pullPolicy = pullPolicy; + } + + /** + * Whether the built image should be pushed to a registry. + * @return whether the built image should be pushed + */ + @Input + public boolean isPublish() { + return this.publish; + } + + /** + * Sets whether the built image should be pushed to a registry. + * @param publish {@code true} the push the built image to a registry. {@code false}. + */ + @Option(option = "publishImage", description = "Publish the built image to a registry") + public void setPublish(boolean publish) { + this.publish = publish; + } + + /** + * Returns the buildpacks that will be used when building the image. + * @return the buildpack references + */ + @Input + @Optional + public List getBuildpacks() { + return this.buildpacks.getOrNull(); + } + + /** + * Sets the buildpacks that will be used when building the image. + * @param buildpacks the buildpack references + */ + public void setBuildpacks(List buildpacks) { + this.buildpacks.set(buildpacks); + } + + /** + * Add an entry to the buildpacks that will be used when building the image. + * @param buildpack the buildpack reference + */ + public void buildpack(String buildpack) { + this.buildpacks.add(buildpack); + } + + /** + * Adds entries to the buildpacks that will be used when building the image. + * @param buildpacks the buildpack references + */ + public void buildpacks(List buildpacks) { + this.buildpacks.addAll(buildpacks); + } + + /** + * Returns the volume bindings that will be mounted to the container when building the + * image. + * @return the bindings + */ + @Input + @Optional + public List getBindings() { + return this.bindings.getOrNull(); + } + + /** + * Sets the volume bindings that will be mounted to the container when building the + * image. + * @param bindings the bindings + */ + public void setBindings(List bindings) { + this.bindings.set(bindings); + } + + /** + * Add an entry to the volume bindings that will be mounted to the container when + * building the image. + * @param binding the binding + */ + public void binding(String binding) { + this.bindings.add(binding); + } + + /** + * Add entries to the volume bindings that will be mounted to the container when + * building the image. + * @param bindings the bindings + */ + public void bindings(List bindings) { + this.bindings.addAll(bindings); + } + + /** + * Returns the Docker configuration the builder will use. + * @return docker configuration. + * @since 2.4.0 + */ + @Nested + public DockerSpec getDocker() { + return this.docker; + } + + /** + * Configures the Docker connection using the given {@code action}. + * @param action the action to apply + * @since 2.4.0 + */ + public void docker(Action action) { + action.execute(this.docker); + } + + /** + * Configures the Docker connection using the given {@code closure}. + * @param closure the closure to apply + * @since 2.4.0 + */ + public void docker(Closure closure) { + docker(ConfigureUtil.configureUsing(closure)); + } + + @TaskAction + void buildImage() throws DockerEngineException, IOException { + Builder builder = new Builder(this.docker.asDockerConfiguration()); + BuildRequest request = createRequest(); + builder.build(request); + } + + BuildRequest createRequest() { + return customize(BuildRequest.of(determineImageReference(), + (owner) -> new ZipFileTarArchive(this.archiveFile.get().getAsFile(), owner))); + } + + private ImageReference determineImageReference() { + if (StringUtils.hasText(this.imageName)) { + return ImageReference.of(this.imageName); + } + ImageName imageName = ImageName.of(this.projectName); + if ("unspecified".equals(this.projectVersion.get())) { + return ImageReference.of(imageName); + } + return ImageReference.of(imageName, this.projectVersion.get()); + } + + private BuildRequest customize(BuildRequest request) { + request = customizeBuilder(request); + request = customizeRunImage(request); + request = customizeEnvironment(request); + request = customizeCreator(request); + request = request.withCleanCache(this.cleanCache); + request = request.withVerboseLogging(this.verboseLogging); + request = customizePullPolicy(request); + request = customizePublish(request); + request = customizeBuildpacks(request); + request = customizeBindings(request); + return request; + } + + private BuildRequest customizeBuilder(BuildRequest request) { + if (StringUtils.hasText(this.builder)) { + return request.withBuilder(ImageReference.of(this.builder)); + } + return request; + } + + private BuildRequest customizeRunImage(BuildRequest request) { + if (StringUtils.hasText(this.runImage)) { + return request.withRunImage(ImageReference.of(this.runImage)); + } + return request; + } + + private BuildRequest customizeEnvironment(BuildRequest request) { + if (this.environment != null && !this.environment.isEmpty()) { + request = request.withEnv(this.environment); + } + if (this.targetJavaVersion.isPresent() && !request.getEnv().containsKey(BUILDPACK_JVM_VERSION_KEY)) { + request = request.withEnv(BUILDPACK_JVM_VERSION_KEY, translateTargetJavaVersion()); + } + return request; + } + + private BuildRequest customizeCreator(BuildRequest request) { + String springBootVersion = VersionExtractor.forClass(BootBuildImage.class); + if (StringUtils.hasText(springBootVersion)) { + return request.withCreator(Creator.withVersion(springBootVersion)); + } + return request; + } + + private BuildRequest customizePullPolicy(BuildRequest request) { + if (this.pullPolicy != null) { + request = request.withPullPolicy(this.pullPolicy); + } + return request; + } + + private BuildRequest customizePublish(BuildRequest request) { + boolean publishRegistryAuthNotConfigured = this.docker == null || this.docker.getPublishRegistry() == null + || this.docker.getPublishRegistry().hasEmptyAuth(); + if (this.publish && publishRegistryAuthNotConfigured) { + throw new GradleException("Publishing an image requires docker.publishRegistry to be configured"); + } + request = request.withPublish(this.publish); + return request; + } + + private BuildRequest customizeBuildpacks(BuildRequest request) { + List buildpacks = this.buildpacks.getOrNull(); + if (buildpacks != null && !buildpacks.isEmpty()) { + return request.withBuildpacks(buildpacks.stream().map(BuildpackReference::of).collect(Collectors.toList())); + } + return request; + } + + private BuildRequest customizeBindings(BuildRequest request) { + List bindings = this.bindings.getOrNull(); + if (bindings != null && !bindings.isEmpty()) { + return request.withBindings(bindings.stream().map(Binding::of).collect(Collectors.toList())); + } + return request; + } + + private String translateTargetJavaVersion() { + return this.targetJavaVersion.get().getMajorVersion() + ".*"; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java index 8f488ec50f0b..229ee3fb813d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,82 +19,139 @@ import java.io.File; import java.util.Collections; import java.util.concurrent.Callable; +import java.util.function.Function; import org.gradle.api.Action; +import org.gradle.api.Project; +import org.gradle.api.artifacts.ResolvableDependencies; import org.gradle.api.file.CopySpec; import org.gradle.api.file.FileCollection; import org.gradle.api.file.FileCopyDetails; import org.gradle.api.file.FileTreeElement; import org.gradle.api.internal.file.copy.CopyAction; +import org.gradle.api.provider.Property; import org.gradle.api.specs.Spec; import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.bundling.Jar; /** * A custom {@link Jar} task that produces a Spring Boot executable jar. * * @author Andy Wilkinson + * @author Madhura Bhave + * @author Scott Frederick + * @author Phillip Webb * @since 2.0.0 */ public class BootJar extends Jar implements BootArchive { - private final BootArchiveSupport support = new BootArchiveSupport("org.springframework.boot.loader.JarLauncher", - this::resolveZipCompression); + private static final String LAUNCHER = "org.springframework.boot.loader.JarLauncher"; - private final CopySpec bootInf; + private static final String CLASSES_DIRECTORY = "BOOT-INF/classes/"; - private String mainClassName; + private static final String LIB_DIRECTORY = "BOOT-INF/lib/"; + + private static final String LAYERS_INDEX = "BOOT-INF/layers.idx"; + + private static final String CLASSPATH_INDEX = "BOOT-INF/classpath.idx"; + + private final ResolvedDependencies resolvedDependencies = new ResolvedDependencies(); + + private final BootArchiveSupport support; + + private final CopySpec bootInfSpec; + + private final Property mainClass; private FileCollection classpath; + private LayeredSpec layered = new LayeredSpec(); + /** * Creates a new {@code BootJar} task. */ public BootJar() { - this.bootInf = getProject().copySpec().into("BOOT-INF"); - getMainSpec().with(this.bootInf); - this.bootInf.into("classes", classpathFiles(File::isDirectory)); - this.bootInf.into("lib", classpathFiles(File::isFile)); - this.bootInf.filesMatching("module-info.class", - (details) -> details.setRelativePath(details.getRelativeSourcePath())); - getRootSpec().eachFile((details) -> { - String pathString = details.getRelativePath().getPathString(); - if (pathString.startsWith("BOOT-INF/lib/") && !this.support.isZip(details.getFile())) { - details.exclude(); - } + this.support = new BootArchiveSupport(LAUNCHER, new LibrarySpec(), new ZipCompressionResolver()); + Project project = getProject(); + this.bootInfSpec = project.copySpec().into("BOOT-INF"); + this.mainClass = project.getObjects().property(String.class); + configureBootInfSpec(this.bootInfSpec); + getMainSpec().with(this.bootInfSpec); + project.getConfigurations().all((configuration) -> { + ResolvableDependencies incoming = configuration.getIncoming(); + incoming.afterResolve((resolvableDependencies) -> { + if (resolvableDependencies == incoming) { + this.resolvedDependencies.processConfiguration(project, configuration); + } + }); }); } - private Action classpathFiles(Spec filter) { - return (copySpec) -> copySpec.from((Callable>) () -> (this.classpath != null) - ? this.classpath.filter(filter) : Collections.emptyList()); + private void configureBootInfSpec(CopySpec bootInfSpec) { + bootInfSpec.into("classes", fromCallTo(this::classpathDirectories)); + bootInfSpec.into("lib", fromCallTo(this::classpathFiles)).eachFile(this.support::excludeNonZipFiles); + this.support.moveModuleInfoToRoot(bootInfSpec); + moveMetaInfToRoot(bootInfSpec); + } + + private Iterable classpathDirectories() { + return classpathEntries(File::isDirectory); + } + + private Iterable classpathFiles() { + return classpathEntries(File::isFile); + } + + private Iterable classpathEntries(Spec filter) { + return (this.classpath != null) ? this.classpath.filter(filter) : Collections.emptyList(); + } + + private void moveMetaInfToRoot(CopySpec spec) { + spec.eachFile((file) -> { + String path = file.getRelativeSourcePath().getPathString(); + if (path.startsWith("META-INF/") && !path.equals("META-INF/aop.xml") && !path.endsWith(".kotlin_module")) { + this.support.moveToRoot(file); + } + }); } @Override public void copy() { - this.support.configureManifest(this, getMainClassName(), "BOOT-INF/classes/", "BOOT-INF/lib/"); + this.support.configureManifest(getManifest(), getMainClass().get(), CLASSES_DIRECTORY, LIB_DIRECTORY, + CLASSPATH_INDEX, (isLayeredDisabled()) ? null : LAYERS_INDEX); super.copy(); } + private boolean isLayeredDisabled() { + return this.layered != null && !this.layered.isEnabled(); + } + @Override protected CopyAction createCopyAction() { + if (!isLayeredDisabled()) { + LayerResolver layerResolver = new LayerResolver(this.resolvedDependencies, this.layered, this::isLibrary); + String layerToolsLocation = this.layered.isIncludeLayerTools() ? LIB_DIRECTORY : null; + return this.support.createCopyAction(this, layerResolver, layerToolsLocation); + } return this.support.createCopyAction(this); } @Override + public Property getMainClass() { + return this.mainClass; + } + + @Override + @Deprecated public String getMainClassName() { - if (this.mainClassName == null) { - String manifestStartClass = (String) getManifest().getAttributes().get("Start-Class"); - if (manifestStartClass != null) { - setMainClassName(manifestStartClass); - } - } - return this.mainClassName; + return this.mainClass.getOrNull(); } @Override + @Deprecated public void setMainClassName(String mainClassName) { - this.mainClassName = mainClassName; + this.mainClass.set(mainClassName); } @Override @@ -122,6 +179,34 @@ public void launchScript(Action action) { action.execute(enableLaunchScriptIfNecessary()); } + /** + * Returns the spec that describes the layers in a layered jar. + * @return the spec for the layers + * @since 2.3.0 + */ + @Nested + public LayeredSpec getLayered() { + return this.layered; + } + + /** + * Configures the jar to be layered using the default layering. + * @since 2.3.0 + * @deprecated since 2.4.0 for removal in 2.6.0 as layering as now enabled by default. + */ + @Deprecated + public void layered() { + } + + /** + * Configures the jar's layering using the given {@code action}. + * @param action the action to apply + * @since 2.3.0 + */ + public void layered(Action action) { + action.execute(this.layered); + } + @Override public FileCollection getClasspath() { return this.classpath; @@ -144,16 +229,6 @@ public void setClasspath(FileCollection classpath) { this.classpath = getProject().files(classpath); } - @Override - public boolean isExcludeDevtools() { - return this.support.isExcludeDevtools(); - } - - @Override - public void setExcludeDevtools(boolean excludeDevtools) { - this.support.setExcludeDevtools(excludeDevtools); - } - /** * Returns a {@code CopySpec} that can be used to add content to the {@code BOOT-INF} * directory of the jar. @@ -163,7 +238,7 @@ public void setExcludeDevtools(boolean excludeDevtools) { @Internal public CopySpec getBootInf() { CopySpec child = getProject().copySpec(); - this.bootInf.with(child); + this.bootInfSpec.with(child); return child; } @@ -182,19 +257,27 @@ public CopySpec bootInf(Action action) { } /** - * Returns the {@link ZipCompression} that should be used when adding the file - * represented by the given {@code details} to the jar. - *

    - * By default, any file in {@code BOOT-INF/lib/} is stored and all other files are - * deflated. - * @param details the details + * Return the {@link ZipCompression} that should be used when adding the file + * represented by the given {@code details} to the jar. By default, any + * {@link #isLibrary(FileCopyDetails) library} is {@link ZipCompression#STORED stored} + * and all other files are {@link ZipCompression#DEFLATED deflated}. + * @param details the file copy details * @return the compression to use */ protected ZipCompression resolveZipCompression(FileCopyDetails details) { - if (details.getRelativePath().getPathString().startsWith("BOOT-INF/lib/")) { - return ZipCompression.STORED; - } - return ZipCompression.DEFLATED; + return isLibrary(details) ? ZipCompression.STORED : ZipCompression.DEFLATED; + } + + /** + * Return if the {@link FileCopyDetails} are for a library. By default any file in + * {@code BOOT-INF/lib} is considered to be a library. + * @param details the file copy details + * @return {@code true} if the details are for a library + * @since 2.3.0 + */ + protected boolean isLibrary(FileCopyDetails details) { + String path = details.getRelativePath().getPathString(); + return path.startsWith(LIB_DIRECTORY); } private LaunchScriptConfiguration enableLaunchScriptIfNecessary() { @@ -206,4 +289,47 @@ private LaunchScriptConfiguration enableLaunchScriptIfNecessary() { return launchScript; } + /** + * Syntactic sugar that makes {@link CopySpec#into} calls a little easier to read. + * @param the result type + * @param callable the callable + * @return an action to add the callable to the spec + */ + private static Action fromCallTo(Callable callable) { + return (spec) -> spec.from(callTo(callable)); + } + + /** + * Syntactic sugar that makes {@link CopySpec#from} calls a little easier to read. + * @param the result type + * @param callable the callable + * @return the callable + */ + private static Callable callTo(Callable callable) { + return callable; + } + + @Internal + ResolvedDependencies getResolvedDependencies() { + return this.resolvedDependencies; + } + + private final class LibrarySpec implements Spec { + + @Override + public boolean isSatisfiedBy(FileCopyDetails details) { + return isLibrary(details); + } + + } + + private final class ZipCompressionResolver implements Function { + + @Override + public ZipCompression apply(FileCopyDetails details) { + return resolveZipCompression(details); + } + + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java index de30351b27a7..6a9b5878bf82 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,18 +16,23 @@ package org.springframework.boot.gradle.tasks.bundling; -import java.io.File; import java.util.Collections; import java.util.concurrent.Callable; +import java.util.function.Function; import org.gradle.api.Action; import org.gradle.api.Project; +import org.gradle.api.artifacts.ResolvableDependencies; +import org.gradle.api.file.CopySpec; import org.gradle.api.file.FileCollection; import org.gradle.api.file.FileCopyDetails; import org.gradle.api.file.FileTreeElement; import org.gradle.api.internal.file.copy.CopyAction; +import org.gradle.api.provider.Property; import org.gradle.api.specs.Spec; import org.gradle.api.tasks.Classpath; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.bundling.War; @@ -35,60 +40,91 @@ * A custom {@link War} task that produces a Spring Boot executable war. * * @author Andy Wilkinson + * @author Phillip Webb * @since 2.0.0 */ public class BootWar extends War implements BootArchive { - private final BootArchiveSupport support = new BootArchiveSupport("org.springframework.boot.loader.WarLauncher", - this::resolveZipCompression); + private static final String LAUNCHER = "org.springframework.boot.loader.WarLauncher"; - private String mainClassName; + private static final String CLASSES_DIRECTORY = "WEB-INF/classes/"; + + private static final String LIB_PROVIDED_DIRECTORY = "WEB-INF/lib-provided/"; + + private static final String LIB_DIRECTORY = "WEB-INF/lib/"; + + private static final String LAYERS_INDEX = "WEB-INF/layers.idx"; + + private final BootArchiveSupport support; + + private final Property mainClass; private FileCollection providedClasspath; + private final ResolvedDependencies resolvedDependencies = new ResolvedDependencies(); + + private LayeredSpec layered = new LayeredSpec(); + /** * Creates a new {@code BootWar} task. */ public BootWar() { - getWebInf().into("lib-provided", - (copySpec) -> copySpec.from((Callable>) () -> (this.providedClasspath != null) - ? this.providedClasspath : Collections.emptyList())); - getRootSpec().filesMatching("module-info.class", - (details) -> details.setRelativePath(details.getRelativeSourcePath())); - getRootSpec().eachFile((details) -> { - String pathString = details.getRelativePath().getPathString(); - if ((pathString.startsWith("WEB-INF/lib/") || pathString.startsWith("WEB-INF/lib-provided/")) - && !this.support.isZip(details.getFile())) { - details.exclude(); - } + this.support = new BootArchiveSupport(LAUNCHER, new LibrarySpec(), new ZipCompressionResolver()); + Project project = getProject(); + this.mainClass = project.getObjects().property(String.class); + getWebInf().into("lib-provided", fromCallTo(this::getProvidedLibFiles)); + this.support.moveModuleInfoToRoot(getRootSpec()); + getRootSpec().eachFile(this.support::excludeNonZipLibraryFiles); + project.getConfigurations().all((configuration) -> { + ResolvableDependencies incoming = configuration.getIncoming(); + incoming.afterResolve((resolvableDependencies) -> { + if (resolvableDependencies == incoming) { + this.resolvedDependencies.processConfiguration(project, configuration); + } + }); }); } + private Object getProvidedLibFiles() { + return (this.providedClasspath != null) ? this.providedClasspath : Collections.emptyList(); + } + @Override public void copy() { - this.support.configureManifest(this, getMainClassName(), "WEB-INF/classes/", "WEB-INF/lib/"); + this.support.configureManifest(getManifest(), getMainClass().get(), CLASSES_DIRECTORY, LIB_DIRECTORY, null, + (isLayeredDisabled()) ? null : LAYERS_INDEX); super.copy(); } + private boolean isLayeredDisabled() { + return this.layered != null && !this.layered.isEnabled(); + } + @Override protected CopyAction createCopyAction() { + if (!isLayeredDisabled()) { + LayerResolver layerResolver = new LayerResolver(this.resolvedDependencies, this.layered, this::isLibrary); + String layerToolsLocation = this.layered.isIncludeLayerTools() ? LIB_DIRECTORY : null; + return this.support.createCopyAction(this, layerResolver, layerToolsLocation); + } return this.support.createCopyAction(this); } @Override + public Property getMainClass() { + return this.mainClass; + } + + @Override + @Deprecated public String getMainClassName() { - if (this.mainClassName == null) { - String manifestStartClass = (String) getManifest().getAttributes().get("Start-Class"); - if (manifestStartClass != null) { - setMainClassName(manifestStartClass); - } - } - return this.mainClassName; + return this.mainClass.getOrNull(); } @Override - public void setMainClassName(String mainClass) { - this.mainClassName = mainClass; + @Deprecated + public void setMainClassName(String mainClassName) { + this.mainClass.set(mainClassName); } @Override @@ -160,31 +196,46 @@ public void setProvidedClasspath(Object classpath) { this.providedClasspath = getProject().files(classpath); } - @Override - public boolean isExcludeDevtools() { - return this.support.isExcludeDevtools(); + /** + * Return the {@link ZipCompression} that should be used when adding the file + * represented by the given {@code details} to the jar. By default, any + * {@link #isLibrary(FileCopyDetails) library} is {@link ZipCompression#STORED stored} + * and all other files are {@link ZipCompression#DEFLATED deflated}. + * @param details the file copy details + * @return the compression to use + */ + protected ZipCompression resolveZipCompression(FileCopyDetails details) { + return isLibrary(details) ? ZipCompression.STORED : ZipCompression.DEFLATED; } - @Override - public void setExcludeDevtools(boolean excludeDevtools) { - this.support.setExcludeDevtools(excludeDevtools); + /** + * Returns the spec that describes the layers in a layered jar. + * @return the spec for the layers + * @since 2.5.0 + */ + @Nested + public LayeredSpec getLayered() { + return this.layered; } /** - * Returns the {@link ZipCompression} that should be used when adding the file - * represented by the given {@code details} to the jar. - *

    - * By default, any file in {@code WEB-INF/lib/} or {@code WEB-INF/lib-provided/} is - * stored and all other files are deflated. - * @param details the details - * @return the compression to use + * Configures the war's layering using the given {@code action}. + * @param action the action to apply + * @since 2.5.0 */ - protected ZipCompression resolveZipCompression(FileCopyDetails details) { - String relativePath = details.getRelativePath().getPathString(); - if (relativePath.startsWith("WEB-INF/lib/") || relativePath.startsWith("WEB-INF/lib-provided/")) { - return ZipCompression.STORED; - } - return ZipCompression.DEFLATED; + public void layered(Action action) { + action.execute(this.layered); + } + + /** + * Return if the {@link FileCopyDetails} are for a library. By default any file in + * {@code WEB-INF/lib} or {@code WEB-INF/lib-provided} is considered to be a library. + * @param details the file copy details + * @return {@code true} if the details are for a library + */ + protected boolean isLibrary(FileCopyDetails details) { + String path = details.getRelativePath().getPathString(); + return path.startsWith(LIB_DIRECTORY) || path.startsWith(LIB_PROVIDED_DIRECTORY); } private LaunchScriptConfiguration enableLaunchScriptIfNecessary() { @@ -196,4 +247,47 @@ private LaunchScriptConfiguration enableLaunchScriptIfNecessary() { return launchScript; } + @Internal + ResolvedDependencies getResolvedDependencies() { + return this.resolvedDependencies; + } + + /** + * Syntactic sugar that makes {@link CopySpec#into} calls a little easier to read. + * @param the result type + * @param callable the callable + * @return an action to add the callable to the spec + */ + private static Action fromCallTo(Callable callable) { + return (spec) -> spec.from(callTo(callable)); + } + + /** + * Syntactic sugar that makes {@link CopySpec#from} calls a little easier to read. + * @param the result type + * @param callable the callable + * @return the callable + */ + private static Callable callTo(Callable callable) { + return callable; + } + + private final class LibrarySpec implements Spec { + + @Override + public boolean isSatisfiedBy(FileCopyDetails details) { + return isLibrary(details); + } + + } + + private final class ZipCompressionResolver implements Function { + + @Override + public ZipCompression apply(FileCopyDetails details) { + return resolveZipCompression(details); + } + + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java index 6528ae7b5c29..4453843c64d5 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,12 +19,20 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; +import java.io.OutputStreamWriter; import java.util.Calendar; +import java.util.Collection; import java.util.GregorianCalendar; +import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Function; +import java.util.stream.Collectors; import java.util.zip.CRC32; +import java.util.zip.ZipEntry; import org.apache.commons.compress.archivers.zip.UnixStat; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; @@ -34,11 +42,20 @@ import org.gradle.api.file.FileTreeElement; import org.gradle.api.internal.file.copy.CopyAction; import org.gradle.api.internal.file.copy.CopyActionProcessingStream; +import org.gradle.api.java.archives.Attributes; +import org.gradle.api.java.archives.Manifest; import org.gradle.api.specs.Spec; import org.gradle.api.tasks.WorkResult; +import org.gradle.api.tasks.WorkResults; import org.springframework.boot.loader.tools.DefaultLaunchScript; import org.springframework.boot.loader.tools.FileUtils; +import org.springframework.boot.loader.tools.JarModeLibrary; +import org.springframework.boot.loader.tools.Layer; +import org.springframework.boot.loader.tools.LayersIndex; +import org.springframework.util.Assert; +import org.springframework.util.StreamUtils; +import org.springframework.util.StringUtils; /** * A {@link CopyAction} for creating a Spring Boot zip archive (typically a jar or war). @@ -46,6 +63,7 @@ * * @author Andy Wilkinson * @author Phillip Webb + * @author Scott Frederick */ class BootZipCopyAction implements CopyAction { @@ -54,64 +72,79 @@ class BootZipCopyAction implements CopyAction { private final File output; + private final Manifest manifest; + private final boolean preserveFileTimestamps; private final boolean includeDefaultLoader; + private final String layerToolsLocation; + private final Spec requiresUnpack; private final Spec exclusions; private final LaunchScriptConfiguration launchScript; + private final Spec librarySpec; + private final Function compressionResolver; private final String encoding; - BootZipCopyAction(File output, boolean preserveFileTimestamps, boolean includeDefaultLoader, - Spec requiresUnpack, Spec exclusions, - LaunchScriptConfiguration launchScript, Function compressionResolver, - String encoding) { + private final LayerResolver layerResolver; + + BootZipCopyAction(File output, Manifest manifest, boolean preserveFileTimestamps, boolean includeDefaultLoader, + String layerToolsLocation, Spec requiresUnpack, Spec exclusions, + LaunchScriptConfiguration launchScript, Spec librarySpec, + Function compressionResolver, String encoding, + LayerResolver layerResolver) { this.output = output; + this.manifest = manifest; this.preserveFileTimestamps = preserveFileTimestamps; this.includeDefaultLoader = includeDefaultLoader; + this.layerToolsLocation = layerToolsLocation; this.requiresUnpack = requiresUnpack; this.exclusions = exclusions; this.launchScript = launchScript; + this.librarySpec = librarySpec; this.compressionResolver = compressionResolver; this.encoding = encoding; + this.layerResolver = layerResolver; } @Override - public WorkResult execute(CopyActionProcessingStream stream) { + public WorkResult execute(CopyActionProcessingStream copyActions) { try { - writeArchive(stream); - return () -> true; + writeArchive(copyActions); + return WorkResults.didWork(true); } catch (IOException ex) { throw new GradleException("Failed to create " + this.output, ex); } } - private void writeArchive(CopyActionProcessingStream stream) throws IOException { - OutputStream outputStream = new FileOutputStream(this.output); + private void writeArchive(CopyActionProcessingStream copyActions) throws IOException { + OutputStream output = new FileOutputStream(this.output); try { - writeLaunchScriptIfNecessary(outputStream); - ZipArchiveOutputStream zipOutputStream = new ZipArchiveOutputStream(outputStream); - try { - if (this.encoding != null) { - zipOutputStream.setEncoding(this.encoding); - } - Processor processor = new Processor(zipOutputStream); - stream.process(processor::process); - processor.finish(); - } - finally { - closeQuietly(zipOutputStream); - } + writeArchive(copyActions, output); } finally { - closeQuietly(outputStream); + closeQuietly(output); + } + } + + private void writeArchive(CopyActionProcessingStream copyActions, OutputStream output) throws IOException { + writeLaunchScriptIfNecessary(output); + ZipArchiveOutputStream zipOutput = new ZipArchiveOutputStream(output); + try { + setEncodingIfNecessary(zipOutput); + Processor processor = new Processor(zipOutput); + copyActions.process(processor::process); + processor.finish(); + } + finally { + closeQuietly(zipOutput); } } @@ -131,6 +164,12 @@ private void writeLaunchScriptIfNecessary(OutputStream outputStream) { } } + private void setEncodingIfNecessary(ZipArchiveOutputStream zipOutputStream) { + if (this.encoding != null) { + zipOutputStream.setEncoding(this.encoding); + } + } + private void closeQuietly(OutputStream outputStream) { try { outputStream.close(); @@ -144,17 +183,24 @@ private void closeQuietly(OutputStream outputStream) { */ private class Processor { - private ZipArchiveOutputStream outputStream; + private final ZipArchiveOutputStream out; - private Spec writtenLoaderEntries; + private final LayersIndex layerIndex; - Processor(ZipArchiveOutputStream outputStream) { - this.outputStream = outputStream; + private LoaderZipEntries.WrittenEntries writtenLoaderEntries; + + private final Set writtenDirectories = new LinkedHashSet<>(); + + private final Set writtenLibraries = new LinkedHashSet<>(); + + Processor(ZipArchiveOutputStream out) { + this.out = out; + this.layerIndex = (BootZipCopyAction.this.layerResolver != null) + ? new LayersIndex(BootZipCopyAction.this.layerResolver.getLayers()) : null; } void process(FileCopyDetails details) { - if (BootZipCopyAction.this.exclusions.isSatisfiedBy(details) - || (this.writtenLoaderEntries != null && this.writtenLoaderEntries.isSatisfiedBy(details))) { + if (skipProcessing(details)) { return; } try { @@ -171,8 +217,64 @@ void process(FileCopyDetails details) { } } + private boolean skipProcessing(FileCopyDetails details) { + return BootZipCopyAction.this.exclusions.isSatisfiedBy(details) + || (this.writtenLoaderEntries != null && this.writtenLoaderEntries.isWrittenDirectory(details)); + } + + private void processDirectory(FileCopyDetails details) throws IOException { + String name = details.getRelativePath().getPathString(); + ZipArchiveEntry entry = new ZipArchiveEntry(name + '/'); + prepareEntry(entry, name, getTime(details), UnixStat.FILE_FLAG | details.getMode()); + this.out.putArchiveEntry(entry); + this.out.closeArchiveEntry(); + this.writtenDirectories.add(name); + } + + private void processFile(FileCopyDetails details) throws IOException { + String name = details.getRelativePath().getPathString(); + ZipArchiveEntry entry = new ZipArchiveEntry(name); + prepareEntry(entry, name, getTime(details), UnixStat.FILE_FLAG | details.getMode()); + ZipCompression compression = BootZipCopyAction.this.compressionResolver.apply(details); + if (compression == ZipCompression.STORED) { + prepareStoredEntry(details, entry); + } + this.out.putArchiveEntry(entry); + details.copyTo(this.out); + this.out.closeArchiveEntry(); + if (BootZipCopyAction.this.librarySpec.isSatisfiedBy(details)) { + this.writtenLibraries.add(name); + } + if (BootZipCopyAction.this.layerResolver != null) { + Layer layer = BootZipCopyAction.this.layerResolver.getLayer(details); + this.layerIndex.add(layer, name); + } + } + + private void writeParentDirectoriesIfNecessary(String name, Long time) throws IOException { + String parentDirectory = getParentDirectory(name); + if (parentDirectory != null && this.writtenDirectories.add(parentDirectory)) { + ZipArchiveEntry entry = new ZipArchiveEntry(parentDirectory + '/'); + prepareEntry(entry, parentDirectory, time, UnixStat.DIR_FLAG | UnixStat.DEFAULT_DIR_PERM); + this.out.putArchiveEntry(entry); + this.out.closeArchiveEntry(); + } + } + + private String getParentDirectory(String name) { + int lastSlash = name.lastIndexOf('/'); + if (lastSlash == -1) { + return null; + } + return name.substring(0, lastSlash); + } + void finish() throws IOException { writeLoaderEntriesIfNecessary(null); + writeJarToolsIfNecessary(); + writeClassPathIndexIfNecessary(); + // We must write the layer index last + writeLayersIndexIfNecessary(); } private void writeLoaderEntriesIfNecessary(FileCopyDetails details) throws IOException { @@ -180,12 +282,17 @@ private void writeLoaderEntriesIfNecessary(FileCopyDetails details) throws IOExc return; } if (isInMetaInf(details)) { - // Don't write loader entries until after META-INF folder (see gh-16698) + // Always write loader entries after META-INF directory (see gh-16698) return; } - LoaderZipEntries loaderEntries = new LoaderZipEntries( - BootZipCopyAction.this.preserveFileTimestamps ? null : CONSTANT_TIME_FOR_ZIP_ENTRIES); - this.writtenLoaderEntries = loaderEntries.writeTo(this.outputStream); + LoaderZipEntries loaderEntries = new LoaderZipEntries(getTime()); + this.writtenLoaderEntries = loaderEntries.writeTo(this.out); + if (BootZipCopyAction.this.layerResolver != null) { + for (String name : this.writtenLoaderEntries.getFiles()) { + Layer layer = BootZipCopyAction.this.layerResolver.getLayer(name); + this.layerIndex.add(layer, name); + } + } } private boolean isInMetaInf(FileCopyDetails details) { @@ -196,71 +303,195 @@ private boolean isInMetaInf(FileCopyDetails details) { return segments.length > 0 && "META-INF".equals(segments[0]); } - private void processDirectory(FileCopyDetails details) throws IOException { - ZipArchiveEntry archiveEntry = new ZipArchiveEntry(details.getRelativePath().getPathString() + '/'); - archiveEntry.setUnixMode(UnixStat.DIR_FLAG | details.getMode()); - archiveEntry.setTime(getTime(details)); - this.outputStream.putArchiveEntry(archiveEntry); - this.outputStream.closeArchiveEntry(); + private void writeJarToolsIfNecessary() throws IOException { + if (BootZipCopyAction.this.layerToolsLocation != null) { + writeJarModeLibrary(BootZipCopyAction.this.layerToolsLocation, JarModeLibrary.LAYER_TOOLS); + } } - private void processFile(FileCopyDetails details) throws IOException { - String relativePath = details.getRelativePath().getPathString(); - ZipArchiveEntry archiveEntry = new ZipArchiveEntry(relativePath); - archiveEntry.setUnixMode(UnixStat.FILE_FLAG | details.getMode()); - archiveEntry.setTime(getTime(details)); - ZipCompression compression = BootZipCopyAction.this.compressionResolver.apply(details); - if (compression == ZipCompression.STORED) { - prepareStoredEntry(details, archiveEntry); + private void writeJarModeLibrary(String location, JarModeLibrary library) throws IOException { + String name = location + library.getName(); + writeEntry(name, ZipEntryContentWriter.fromInputStream(library.openStream()), false, + (entry) -> prepareStoredEntry(library.openStream(), entry)); + if (BootZipCopyAction.this.layerResolver != null) { + Layer layer = BootZipCopyAction.this.layerResolver.getLayer(library); + this.layerIndex.add(layer, name); + } + } + + private void writeClassPathIndexIfNecessary() throws IOException { + Attributes manifestAttributes = BootZipCopyAction.this.manifest.getAttributes(); + String classPathIndex = (String) manifestAttributes.get("Spring-Boot-Classpath-Index"); + if (classPathIndex != null) { + List lines = this.writtenLibraries.stream().map((line) -> "- \"" + line + "\"") + .collect(Collectors.toList()); + writeEntry(classPathIndex, ZipEntryContentWriter.fromLines(BootZipCopyAction.this.encoding, lines), + true); + } + } + + private void writeLayersIndexIfNecessary() throws IOException { + if (BootZipCopyAction.this.layerResolver != null) { + Attributes manifestAttributes = BootZipCopyAction.this.manifest.getAttributes(); + String name = (String) manifestAttributes.get("Spring-Boot-Layers-Index"); + Assert.state(StringUtils.hasText(name), "Missing layer index manifest attribute"); + Layer layer = BootZipCopyAction.this.layerResolver.getLayer(name); + this.layerIndex.add(layer, name); + writeEntry(name, this.layerIndex::writeTo, false); + } + } + + private void writeEntry(String name, ZipEntryContentWriter entryWriter, boolean addToLayerIndex) + throws IOException { + writeEntry(name, entryWriter, addToLayerIndex, ZipEntryCustomizer.NONE); + } + + private void writeEntry(String name, ZipEntryContentWriter entryWriter, boolean addToLayerIndex, + ZipEntryCustomizer entryCustomizer) throws IOException { + ZipArchiveEntry entry = new ZipArchiveEntry(name); + prepareEntry(entry, name, getTime(), UnixStat.FILE_FLAG | UnixStat.DEFAULT_FILE_PERM); + entryCustomizer.customize(entry); + this.out.putArchiveEntry(entry); + entryWriter.writeTo(this.out); + this.out.closeArchiveEntry(); + if (addToLayerIndex && BootZipCopyAction.this.layerResolver != null) { + Layer layer = BootZipCopyAction.this.layerResolver.getLayer(name); + this.layerIndex.add(layer, name); + } + } + + private void prepareEntry(ZipArchiveEntry entry, String name, Long time, int mode) throws IOException { + writeParentDirectoriesIfNecessary(name, time); + entry.setUnixMode(mode); + if (time != null) { + entry.setTime(time); } - this.outputStream.putArchiveEntry(archiveEntry); - details.copyTo(this.outputStream); - this.outputStream.closeArchiveEntry(); } private void prepareStoredEntry(FileCopyDetails details, ZipArchiveEntry archiveEntry) throws IOException { - archiveEntry.setMethod(java.util.zip.ZipEntry.STORED); - archiveEntry.setSize(details.getSize()); - archiveEntry.setCompressedSize(details.getSize()); - Crc32OutputStream crcStream = new Crc32OutputStream(); - details.copyTo(crcStream); - archiveEntry.setCrc(crcStream.getCrc()); + prepareStoredEntry(details.open(), archiveEntry); if (BootZipCopyAction.this.requiresUnpack.isSatisfiedBy(details)) { archiveEntry.setComment("UNPACK:" + FileUtils.sha1Hash(details.getFile())); } } - private long getTime(FileCopyDetails details) { - return BootZipCopyAction.this.preserveFileTimestamps ? details.getLastModified() - : CONSTANT_TIME_FOR_ZIP_ENTRIES; + private void prepareStoredEntry(InputStream input, ZipArchiveEntry archiveEntry) throws IOException { + new CrcAndSize(input).setUpStoredEntry(archiveEntry); + } + + private Long getTime() { + return getTime(null); + } + + private Long getTime(FileCopyDetails details) { + if (!BootZipCopyAction.this.preserveFileTimestamps) { + return CONSTANT_TIME_FOR_ZIP_ENTRIES; + } + if (details != null) { + return details.getLastModified(); + } + return null; } } /** - * An {@code OutputStream} that provides a CRC-32 of the data that is written to it. + * Callback interface used to customize a {@link ZipArchiveEntry}. */ - private static final class Crc32OutputStream extends OutputStream { + @FunctionalInterface + private interface ZipEntryCustomizer { - private final CRC32 crc = new CRC32(); + ZipEntryCustomizer NONE = (entry) -> { + }; - @Override - public void write(int b) throws IOException { - this.crc.update(b); + /** + * Customize the entry. + * @param entry the entry to customize + * @throws IOException on IO error + */ + void customize(ZipArchiveEntry entry) throws IOException; + + } + + /** + * Callback used to write a zip entry data. + */ + @FunctionalInterface + private interface ZipEntryContentWriter { + + /** + * Write the entry data. + * @param out the output stream used to write the data + * @throws IOException on IO error + */ + void writeTo(ZipArchiveOutputStream out) throws IOException; + + /** + * Create a new {@link ZipEntryContentWriter} that will copy content from the + * given {@link InputStream}. + * @param in the source input stream + * @return a new {@link ZipEntryContentWriter} instance + */ + static ZipEntryContentWriter fromInputStream(InputStream in) { + return (out) -> { + StreamUtils.copy(in, out); + in.close(); + }; } - @Override - public void write(byte[] b) throws IOException { - this.crc.update(b); + /** + * Create a new {@link ZipEntryContentWriter} that will copy content from the + * given lines. + * @param encoding the required character encoding + * @param lines the lines to write + * @return a new {@link ZipEntryContentWriter} instance + */ + static ZipEntryContentWriter fromLines(String encoding, Collection lines) { + return (out) -> { + OutputStreamWriter writer = new OutputStreamWriter(out, encoding); + for (String line : lines) { + writer.append(line).append("\n"); + } + writer.flush(); + }; + } + + } + + /** + * Data holder for CRC and Size. + */ + private static class CrcAndSize { + + private static final int BUFFER_SIZE = 32 * 1024; + + private final CRC32 crc = new CRC32(); + + private long size; + + CrcAndSize(InputStream inputStream) throws IOException { + try { + load(inputStream); + } + finally { + inputStream.close(); + } } - @Override - public void write(byte[] b, int off, int len) throws IOException { - this.crc.update(b, off, len); + private void load(InputStream inputStream) throws IOException { + byte[] buffer = new byte[BUFFER_SIZE]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + this.crc.update(buffer, 0, bytesRead); + this.size += bytesRead; + } } - private long getCrc() { - return this.crc.getValue(); + void setUpStoredEntry(ZipArchiveEntry entry) { + entry.setSize(this.size); + entry.setCompressedSize(this.size); + entry.setCrc(this.crc.getValue()); + entry.setMethod(ZipEntry.STORED); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/DockerSpec.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/DockerSpec.java new file mode 100644 index 000000000000..b1a4d133ca76 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/DockerSpec.java @@ -0,0 +1,329 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.gradle.tasks.bundling; + +import groovy.lang.Closure; +import org.gradle.api.Action; +import org.gradle.api.GradleException; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Nested; +import org.gradle.api.tasks.Optional; +import org.gradle.util.ConfigureUtil; + +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration; + +/** + * Encapsulates Docker configuration options. + * + * @author Wei Jiang + * @author Scott Frederick + * @since 2.4.0 + */ +public class DockerSpec { + + private String host; + + private boolean tlsVerify; + + private String certPath; + + private final DockerRegistrySpec builderRegistry; + + private final DockerRegistrySpec publishRegistry; + + public DockerSpec() { + this.builderRegistry = new DockerRegistrySpec(); + this.publishRegistry = new DockerRegistrySpec(); + } + + DockerSpec(DockerRegistrySpec builderRegistry, DockerRegistrySpec publishRegistry) { + this.builderRegistry = builderRegistry; + this.publishRegistry = publishRegistry; + } + + @Input + @Optional + public String getHost() { + return this.host; + } + + public void setHost(String host) { + this.host = host; + } + + @Input + @Optional + public Boolean isTlsVerify() { + return this.tlsVerify; + } + + public void setTlsVerify(boolean tlsVerify) { + this.tlsVerify = tlsVerify; + } + + @Input + @Optional + public String getCertPath() { + return this.certPath; + } + + public void setCertPath(String certPath) { + this.certPath = certPath; + } + + /** + * Returns the {@link DockerRegistrySpec} that configures authentication to the + * builder registry. + * @return the registry spec + */ + @Nested + public DockerRegistrySpec getBuilderRegistry() { + return this.builderRegistry; + } + + /** + * Customizes the {@link DockerRegistrySpec} that configures authentication to the + * builder registry. + * @param action the action to apply + */ + public void builderRegistry(Action action) { + action.execute(this.builderRegistry); + } + + /** + * Customizes the {@link DockerRegistrySpec} that configures authentication to the + * builder registry. + * @param closure the closure to apply + */ + public void builderRegistry(Closure closure) { + builderRegistry(ConfigureUtil.configureUsing(closure)); + } + + /** + * Returns the {@link DockerRegistrySpec} that configures authentication to the + * publishing registry. + * @return the registry spec + */ + @Nested + public DockerRegistrySpec getPublishRegistry() { + return this.publishRegistry; + } + + /** + * Customizes the {@link DockerRegistrySpec} that configures authentication to the + * publishing registry. + * @param action the action to apply + */ + public void publishRegistry(Action action) { + action.execute(this.publishRegistry); + } + + /** + * Customizes the {@link DockerRegistrySpec} that configures authentication to the + * publishing registry. + * @param closure the closure to apply + */ + public void publishRegistry(Closure closure) { + publishRegistry(ConfigureUtil.configureUsing(closure)); + } + + /** + * Returns this configuration as a {@link DockerConfiguration} instance. This method + * should only be called when the configuration is complete and will no longer be + * changed. + * @return the Docker configuration + */ + DockerConfiguration asDockerConfiguration() { + DockerConfiguration dockerConfiguration = new DockerConfiguration(); + dockerConfiguration = customizeHost(dockerConfiguration); + dockerConfiguration = customizeBuilderAuthentication(dockerConfiguration); + dockerConfiguration = customizePublishAuthentication(dockerConfiguration); + return dockerConfiguration; + } + + private DockerConfiguration customizeHost(DockerConfiguration dockerConfiguration) { + if (this.host != null) { + return dockerConfiguration.withHost(this.host, this.tlsVerify, this.certPath); + } + return dockerConfiguration; + } + + private DockerConfiguration customizeBuilderAuthentication(DockerConfiguration dockerConfiguration) { + if (this.builderRegistry == null || this.builderRegistry.hasEmptyAuth()) { + return dockerConfiguration; + } + if (this.builderRegistry.hasTokenAuth() && !this.builderRegistry.hasUserAuth()) { + return dockerConfiguration.withBuilderRegistryTokenAuthentication(this.builderRegistry.getToken()); + } + if (this.builderRegistry.hasUserAuth() && !this.builderRegistry.hasTokenAuth()) { + return dockerConfiguration.withBuilderRegistryUserAuthentication(this.builderRegistry.getUsername(), + this.builderRegistry.getPassword(), this.builderRegistry.getUrl(), this.builderRegistry.getEmail()); + } + throw new GradleException( + "Invalid Docker builder registry configuration, either token or username/password must be provided"); + } + + private DockerConfiguration customizePublishAuthentication(DockerConfiguration dockerConfiguration) { + if (this.publishRegistry == null || this.publishRegistry.hasEmptyAuth()) { + return dockerConfiguration; + } + if (this.publishRegistry.hasTokenAuth() && !this.publishRegistry.hasUserAuth()) { + return dockerConfiguration.withPublishRegistryTokenAuthentication(this.publishRegistry.getToken()); + } + if (this.publishRegistry.hasUserAuth() && !this.publishRegistry.hasTokenAuth()) { + return dockerConfiguration.withPublishRegistryUserAuthentication(this.publishRegistry.getUsername(), + this.publishRegistry.getPassword(), this.publishRegistry.getUrl(), this.publishRegistry.getEmail()); + } + throw new GradleException( + "Invalid Docker publish registry configuration, either token or username/password must be provided"); + } + + /** + * Encapsulates Docker registry authentication configuration options. + */ + public static class DockerRegistrySpec { + + private String username; + + private String password; + + private String url; + + private String email; + + private String token; + + public DockerRegistrySpec() { + } + + DockerRegistrySpec(String username, String password, String url, String email) { + this.username = username; + this.password = password; + this.url = url; + this.email = email; + } + + DockerRegistrySpec(String token) { + this.token = token; + } + + /** + * Returns the username to use when authenticating to the Docker registry. + * @return the registry username + */ + @Input + @Optional + public String getUsername() { + return this.username; + } + + /** + * Sets the username to use when authenticating to the Docker registry. + * @param username the registry username + */ + public void setUsername(String username) { + this.username = username; + } + + /** + * Returns the password to use when authenticating to the Docker registry. + * @return the registry password + */ + @Input + @Optional + public String getPassword() { + return this.password; + } + + /** + * Sets the password to use when authenticating to the Docker registry. + * @param password the registry username + */ + public void setPassword(String password) { + this.password = password; + } + + /** + * Returns the Docker registry URL. + * @return the registry URL + */ + @Input + @Optional + public String getUrl() { + return this.url; + } + + /** + * Sets the Docker registry URL. + * @param url the registry URL + */ + public void setUrl(String url) { + this.url = url; + } + + /** + * Returns the email address associated with the Docker registry username. + * @return the registry email address + */ + @Input + @Optional + public String getEmail() { + return this.email; + } + + /** + * Sets the email address associated with the Docker registry username. + * @param email the registry email address + */ + public void setEmail(String email) { + this.email = email; + } + + /** + * Returns the identity token to use when authenticating to the Docker registry. + * @return the registry identity token + */ + @Input + @Optional + public String getToken() { + return this.token; + } + + /** + * Sets the identity token to use when authenticating to the Docker registry. + * @param token the registry identity token + */ + public void setToken(String token) { + this.token = token; + } + + boolean hasEmptyAuth() { + return this.username == null && this.password == null && this.url == null && this.email == null + && this.token == null; + } + + boolean hasUserAuth() { + return this.getUsername() != null && this.getPassword() != null; + } + + boolean hasTokenAuth() { + return this.getToken() != null; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LaunchScriptConfiguration.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LaunchScriptConfiguration.java index c084cecaa504..b53ef7b5ad39 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LaunchScriptConfiguration.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LaunchScriptConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,17 +17,19 @@ package org.springframework.boot.gradle.tasks.bundling; import java.io.File; -import java.io.IOException; import java.io.Serializable; -import java.util.HashMap; import java.util.Map; +import java.util.TreeMap; import java.util.regex.Pattern; import org.gradle.api.Project; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; import org.gradle.api.tasks.bundling.AbstractArchiveTask; -import org.springframework.boot.loader.tools.FileUtils; - /** * Encapsulates the configuration of the launch script for an executable jar or war. * @@ -41,7 +43,9 @@ public class LaunchScriptConfiguration implements Serializable { private static final Pattern LINE_FEED_PATTERN = Pattern.compile("\n"); - private final Map properties = new HashMap<>(); + // We don't care about the order, but Gradle's configuration cache currently does. + // https://github.com/gradle/gradle/pull/17863 + private final Map properties = new TreeMap<>(); private File script; @@ -50,11 +54,10 @@ public LaunchScriptConfiguration() { LaunchScriptConfiguration(AbstractArchiveTask archiveTask) { Project project = archiveTask.getProject(); - putIfMissing(this.properties, "initInfoProvides", archiveTask.getBaseName()); - putIfMissing(this.properties, "initInfoShortDescription", removeLineBreaks(project.getDescription()), - archiveTask.getBaseName()); - putIfMissing(this.properties, "initInfoDescription", augmentLineBreaks(project.getDescription()), - archiveTask.getBaseName()); + String baseName = archiveTask.getArchiveBaseName().get(); + putIfMissing(this.properties, "initInfoProvides", baseName); + putIfMissing(this.properties, "initInfoShortDescription", removeLineBreaks(project.getDescription()), baseName); + putIfMissing(this.properties, "initInfoDescription", augmentLineBreaks(project.getDescription()), baseName); } /** @@ -62,6 +65,7 @@ public LaunchScriptConfiguration() { * including in the executable archive. * @return the properties */ + @Input public Map getProperties() { return this.properties; } @@ -80,6 +84,9 @@ public void properties(Map properties) { * When {@code null}, the default launch script will be used. * @return the script file */ + @Optional + @InputFile + @PathSensitive(PathSensitivity.RELATIVE) public File getScript() { return this.script; } @@ -93,47 +100,6 @@ public void setScript(File script) { this.script = script; } - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - LaunchScriptConfiguration other = (LaunchScriptConfiguration) obj; - if (!this.properties.equals(other.properties)) { - return false; - } - if (this.script == null) { - return other.script == null; - } - else if (!this.script.equals(other.script)) { - return false; - } - else { - return equalContents(this.script, other.script); - } - } - - private boolean equalContents(File one, File two) { - try { - return FileUtils.sha1Hash(one).equals(FileUtils.sha1Hash(two)); - } - catch (IOException ex) { - return false; - } - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + this.properties.hashCode(); - result = prime * result + ((this.script == null) ? 0 : this.script.hashCode()); - return result; - } - private String removeLineBreaks(String string) { return (string != null) ? WHITE_SPACE_PATTERN.matcher(string).replaceAll(" ") : null; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayerResolver.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayerResolver.java new file mode 100644 index 000000000000..a68a1007c09d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayerResolver.java @@ -0,0 +1,89 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.gradle.tasks.bundling; + +import java.io.File; + +import org.gradle.api.file.FileCopyDetails; +import org.gradle.api.specs.Spec; + +import org.springframework.boot.gradle.tasks.bundling.ResolvedDependencies.DependencyDescriptor; +import org.springframework.boot.loader.tools.Layer; +import org.springframework.boot.loader.tools.Library; +import org.springframework.boot.loader.tools.LibraryCoordinates; + +/** + * Resolver backed by a {@link LayeredSpec} that provides the destination {@link Layer} + * for each copied {@link FileCopyDetails}. + * + * @author Madhura Bhave + * @author Scott Frederick + * @author Phillip Webb + * @author Paddy Drury + * @see BootZipCopyAction + */ +class LayerResolver { + + private final ResolvedDependencies resolvedDependencies; + + private final LayeredSpec layeredConfiguration; + + private final Spec librarySpec; + + LayerResolver(ResolvedDependencies resolvedDependencies, LayeredSpec layeredConfiguration, + Spec librarySpec) { + this.resolvedDependencies = resolvedDependencies; + this.layeredConfiguration = layeredConfiguration; + this.librarySpec = librarySpec; + } + + Layer getLayer(FileCopyDetails details) { + try { + if (this.librarySpec.isSatisfiedBy(details)) { + return getLayer(asLibrary(details)); + } + return getLayer(details.getSourcePath()); + } + catch (UnsupportedOperationException ex) { + return null; + } + } + + Layer getLayer(Library library) { + return this.layeredConfiguration.asLayers().getLayer(library); + } + + Layer getLayer(String applicationResource) { + return this.layeredConfiguration.asLayers().getLayer(applicationResource); + } + + Iterable getLayers() { + return this.layeredConfiguration.asLayers(); + } + + private Library asLibrary(FileCopyDetails details) { + File file = details.getFile(); + DependencyDescriptor dependency = this.resolvedDependencies.find(file); + if (dependency == null) { + return new Library(null, file, null, null, false, false, true); + } + LibraryCoordinates coordinates = dependency.getCoordinates(); + boolean projectDependency = dependency.isProjectDependency(); + return new Library(null, file, null, coordinates, false, projectDependency, true); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayeredSpec.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayeredSpec.java new file mode 100644 index 000000000000..4f60b88a0618 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayeredSpec.java @@ -0,0 +1,441 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.gradle.tasks.bundling; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +import groovy.lang.Closure; +import org.gradle.api.Action; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Optional; +import org.gradle.util.ConfigureUtil; + +import org.springframework.boot.loader.tools.Layer; +import org.springframework.boot.loader.tools.Layers; +import org.springframework.boot.loader.tools.Library; +import org.springframework.boot.loader.tools.layer.ApplicationContentFilter; +import org.springframework.boot.loader.tools.layer.ContentFilter; +import org.springframework.boot.loader.tools.layer.ContentSelector; +import org.springframework.boot.loader.tools.layer.CustomLayers; +import org.springframework.boot.loader.tools.layer.IncludeExcludeContentSelector; +import org.springframework.boot.loader.tools.layer.LibraryContentFilter; +import org.springframework.util.Assert; + +/** + * Encapsulates the configuration for a layered archive. + * + * @author Madhura Bhave + * @author Scott Frederick + * @author Phillip Webb + * @since 2.3.0 + */ +public class LayeredSpec { + + private boolean includeLayerTools = true; + + private boolean enabled = true; + + private ApplicationSpec application = new ApplicationSpec(); + + private DependenciesSpec dependencies = new DependenciesSpec(); + + @Optional + private List layerOrder; + + private Layers layers; + + /** + * Returns whether the layer tools should be included as a dependency in the layered + * archive. + * @return whether the layer tools should be included + */ + @Input + public boolean isIncludeLayerTools() { + return this.includeLayerTools; + } + + /** + * Sets whether the layer tools should be included as a dependency in the layered + * archive. + * @param includeLayerTools {@code true} if the layer tools should be included, + * otherwise {@code false} + */ + public void setIncludeLayerTools(boolean includeLayerTools) { + this.includeLayerTools = includeLayerTools; + } + + /** + * Returns whether the layers.idx should be included in the archive. + * @return whether the layers.idx should be included + */ + @Input + public boolean isEnabled() { + return this.enabled; + } + + /** + * Sets whether the layers.idx should be included in the archive. + * @param enabled {@code true} layers.idx should be included in the archive, otherwise + * {@code false} + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + /** + * Returns the {@link ApplicationSpec} that controls the layers to which application + * classes and resources belong. + * @return the application spec + */ + @Input + public ApplicationSpec getApplication() { + return this.application; + } + + /** + * Sets the {@link ApplicationSpec} that controls the layers to which application + * classes are resources belong. + * @param spec the application spec + */ + public void setApplication(ApplicationSpec spec) { + this.application = spec; + } + + /** + * Customizes the {@link ApplicationSpec} using the given {@code action}. + * @param action the action + */ + public void application(Action action) { + action.execute(this.application); + } + + /** + * Customizes the {@link ApplicationSpec} using the given {@code closure}. + * @param closure the closure + */ + public void application(Closure closure) { + application(ConfigureUtil.configureUsing(closure)); + } + + /** + * Returns the {@link DependenciesSpec} that controls the layers to which dependencies + * belong. + * @return the dependencies spec + */ + @Input + public DependenciesSpec getDependencies() { + return this.dependencies; + } + + /** + * Sets the {@link DependenciesSpec} that controls the layers to which dependencies + * belong. + * @param spec the dependencies spec + */ + public void setDependencies(DependenciesSpec spec) { + this.dependencies = spec; + } + + /** + * Customizes the {@link DependenciesSpec} using the given {@code action}. + * @param action the action + */ + public void dependencies(Action action) { + action.execute(this.dependencies); + } + + /** + * Customizes the {@link DependenciesSpec} using the given {@code closure}. + * @param closure the closure + */ + public void dependencies(Closure closure) { + dependencies(ConfigureUtil.configureUsing(closure)); + } + + /** + * Returns the order of the layers in the archive from least to most frequently + * changing. + * @return the layer order + */ + @Input + public List getLayerOrder() { + return this.layerOrder; + } + + /** + * Sets the order of the layers in the archive from least to most frequently changing. + * @param layerOrder the layer order + */ + public void setLayerOrder(String... layerOrder) { + this.layerOrder = Arrays.asList(layerOrder); + } + + /** + * Sets the order of the layers in the archive from least to most frequently changing. + * @param layerOrder the layer order + */ + public void setLayerOrder(List layerOrder) { + this.layerOrder = layerOrder; + } + + /** + * Return this configuration as a {@link Layers} instance. This method should only be + * called when the configuration is complete and will no longer be changed. + * @return the layers + */ + Layers asLayers() { + Layers layers = this.layers; + if (layers == null) { + layers = createLayers(); + this.layers = layers; + } + return layers; + } + + private Layers createLayers() { + if (this.layerOrder == null || this.layerOrder.isEmpty()) { + Assert.state(this.application.isEmpty() && this.dependencies.isEmpty(), + "The 'layerOrder' must be defined when using custom layering"); + return Layers.IMPLICIT; + } + List layers = this.layerOrder.stream().map(Layer::new).collect(Collectors.toList()); + return new CustomLayers(layers, this.application.asSelectors(), this.dependencies.asSelectors()); + } + + /** + * Base class for specs that control the layers to which a category of content should + * belong. + */ + public abstract static class IntoLayersSpec implements Serializable { + + private final List intoLayers; + + private final Function specFactory; + + boolean isEmpty() { + return this.intoLayers.isEmpty(); + } + + IntoLayersSpec(Function specFactory, IntoLayerSpec... spec) { + this.intoLayers = new ArrayList<>(Arrays.asList(spec)); + this.specFactory = specFactory; + } + + public void intoLayer(String layer) { + this.intoLayers.add(this.specFactory.apply(layer)); + } + + public void intoLayer(String layer, Closure closure) { + intoLayer(layer, ConfigureUtil.configureUsing(closure)); + } + + public void intoLayer(String layer, Action action) { + IntoLayerSpec spec = this.specFactory.apply(layer); + action.execute(spec); + this.intoLayers.add(spec); + } + + List> asSelectors(Function> selectorFactory) { + return this.intoLayers.stream().map(selectorFactory).collect(Collectors.toList()); + } + + } + + /** + * Spec that controls the content that should be part of a particular layer. + */ + public static class IntoLayerSpec implements Serializable { + + private final String intoLayer; + + private final List includes = new ArrayList<>(); + + private final List excludes = new ArrayList<>(); + + /** + * Creates a new {@code IntoLayerSpec} that will control the content of the given + * layer. + * @param intoLayer the layer + */ + public IntoLayerSpec(String intoLayer) { + this.intoLayer = intoLayer; + } + + /** + * Adds patterns that control the content that is included in the layer. If no + * includes are specified then all content is included. If includes are specified + * then content must match an inclusion and not match any exclusions to be + * included. + * @param patterns the patterns to be included + */ + public void include(String... patterns) { + this.includes.addAll(Arrays.asList(patterns)); + } + + /** + * Adds patterns that control the content that is excluded from the layer. If no + * excludes a specified no content is excluded. If exclusions are specified then + * any content that matches an exclusion will be excluded irrespective of whether + * it matches an include. + * @param patterns the patterns to be excluded + */ + public void exclude(String... patterns) { + this.includes.addAll(Arrays.asList(patterns)); + } + + ContentSelector asSelector(Function> filterFactory) { + Layer layer = new Layer(this.intoLayer); + return new IncludeExcludeContentSelector<>(layer, this.includes, this.excludes, filterFactory); + } + + String getIntoLayer() { + return this.intoLayer; + } + + List getIncludes() { + return this.includes; + } + + List getExcludes() { + return this.excludes; + } + + } + + /** + * Spec that controls the dependencies that should be part of a particular layer. + * + * @since 2.4.0 + */ + public static class DependenciesIntoLayerSpec extends IntoLayerSpec { + + private boolean includeProjectDependencies; + + private boolean excludeProjectDependencies; + + /** + * Creates a new {@code IntoLayerSpec} that will control the content of the given + * layer. + * @param intoLayer the layer + */ + public DependenciesIntoLayerSpec(String intoLayer) { + super(intoLayer); + } + + /** + * Configures the layer to include project dependencies. If no includes are + * specified then all content is included. If includes are specified then content + * must match an inclusion and not match any exclusions to be included. + */ + public void includeProjectDependencies() { + this.includeProjectDependencies = true; + } + + /** + * Configures the layer to exclude project dependencies. If no excludes a + * specified no content is excluded. If exclusions are specified then any content + * that matches an exclusion will be excluded irrespective of whether it matches + * an include. + */ + public void excludeProjectDependencies() { + this.excludeProjectDependencies = true; + } + + ContentSelector asLibrarySelector(Function> filterFactory) { + Layer layer = new Layer(getIntoLayer()); + List> includeFilters = getIncludes().stream().map(filterFactory) + .collect(Collectors.toList()); + if (this.includeProjectDependencies) { + includeFilters = new ArrayList<>(includeFilters); + includeFilters.add(Library::isLocal); + } + List> excludeFilters = getExcludes().stream().map(filterFactory) + .collect(Collectors.toList()); + if (this.excludeProjectDependencies) { + excludeFilters = new ArrayList<>(excludeFilters); + excludeFilters.add(Library::isLocal); + } + return new IncludeExcludeContentSelector<>(layer, includeFilters, excludeFilters); + } + + } + + /** + * An {@link IntoLayersSpec} that controls the layers to which application classes and + * resources belong. + */ + public static class ApplicationSpec extends IntoLayersSpec { + + /** + * Creates a new {@code ApplicationSpec} with the given {@code contents}. + * @param contents specs for the layers in which application content should be + * included + */ + public ApplicationSpec(IntoLayerSpec... contents) { + super(new IntoLayerSpecFactory(), contents); + } + + List> asSelectors() { + return asSelectors((spec) -> spec.asSelector(ApplicationContentFilter::new)); + } + + private static final class IntoLayerSpecFactory implements Function, Serializable { + + @Override + public IntoLayerSpec apply(String layer) { + return new IntoLayerSpec(layer); + } + + } + + } + + /** + * An {@link IntoLayersSpec} that controls the layers to which dependencies belong. + */ + public static class DependenciesSpec extends IntoLayersSpec implements Serializable { + + /** + * Creates a new {@code DependenciesSpec} with the given {@code contents}. + * @param contents specs for the layers in which dependencies should be included + */ + public DependenciesSpec(DependenciesIntoLayerSpec... contents) { + super(new IntoLayerSpecFactory(), contents); + } + + List> asSelectors() { + return asSelectors( + (spec) -> ((DependenciesIntoLayerSpec) spec).asLibrarySelector(LibraryContentFilter::new)); + } + + private static final class IntoLayerSpecFactory implements Function, Serializable { + + @Override + public IntoLayerSpec apply(String layer) { + return new DependenciesIntoLayerSpec(layer); + } + + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java index b8331236331d..4fd6d854ff4a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @@ -28,39 +28,42 @@ import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; import org.gradle.api.file.FileTreeElement; -import org.gradle.api.specs.Spec; + +import org.springframework.util.StreamUtils; /** * Internal utility used to copy entries from the {@code spring-boot-loader.jar}. * * @author Andy Wilkinson * @author Phillip Webb + * @author Scott Frederick */ class LoaderZipEntries { - private Long entryTime; + private final Long entryTime; LoaderZipEntries(Long entryTime) { this.entryTime = entryTime; } - Spec writeTo(ZipArchiveOutputStream zipOutputStream) throws IOException { - WrittenDirectoriesSpec writtenDirectoriesSpec = new WrittenDirectoriesSpec(); + WrittenEntries writeTo(ZipArchiveOutputStream out) throws IOException { + WrittenEntries written = new WrittenEntries(); try (ZipInputStream loaderJar = new ZipInputStream( getClass().getResourceAsStream("/META-INF/loader/spring-boot-loader.jar"))) { java.util.zip.ZipEntry entry = loaderJar.getNextEntry(); while (entry != null) { if (entry.isDirectory() && !entry.getName().equals("META-INF/")) { - writeDirectory(new ZipArchiveEntry(entry), zipOutputStream); - writtenDirectoriesSpec.add(entry); + writeDirectory(new ZipArchiveEntry(entry), out); + written.addDirectory(entry); } else if (entry.getName().endsWith(".class")) { - writeClass(new ZipArchiveEntry(entry), loaderJar, zipOutputStream); + writeClass(new ZipArchiveEntry(entry), loaderJar, out); + written.addFile(entry); } entry = loaderJar.getNextEntry(); } } - return writtenDirectoriesSpec; + return written; } private void writeDirectory(ZipArchiveEntry entry, ZipArchiveOutputStream out) throws IOException { @@ -84,31 +87,36 @@ private void prepareEntry(ZipArchiveEntry entry, int unixMode) { } private void copy(InputStream in, OutputStream out) throws IOException { - byte[] buffer = new byte[4096]; - int bytesRead = -1; - while ((bytesRead = in.read(buffer)) != -1) { - out.write(buffer, 0, bytesRead); - } + StreamUtils.copy(in, out); } /** - * Spec to track directories that have been written. + * Tracks entries that have been written. */ - private static class WrittenDirectoriesSpec implements Spec { + static class WrittenEntries { + + private final Set directories = new LinkedHashSet<>(); + + private final Set files = new LinkedHashSet<>(); - private final Set entries = new HashSet<>(); + private void addDirectory(ZipEntry entry) { + this.directories.add(entry.getName()); + } + + private void addFile(ZipEntry entry) { + this.files.add(entry.getName()); + } - @Override - public boolean isSatisfiedBy(FileTreeElement element) { + boolean isWrittenDirectory(FileTreeElement element) { String path = element.getRelativePath().getPathString(); if (element.isDirectory() && !path.endsWith(("/"))) { path += "/"; } - return this.entries.contains(path); + return this.directories.contains(path); } - void add(ZipEntry entry) { - this.entries.add(entry.getName()); + Set getFiles() { + return this.files; } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/ResolvedDependencies.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/ResolvedDependencies.java new file mode 100644 index 000000000000..fc7bdb89dba7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/ResolvedDependencies.java @@ -0,0 +1,151 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.gradle.tasks.bundling; + +import java.io.File; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.ModuleVersionIdentifier; +import org.gradle.api.artifacts.ResolvedArtifact; +import org.gradle.api.artifacts.ResolvedConfiguration; + +import org.springframework.boot.loader.tools.LibraryCoordinates; + +/** + * Tracks and provides details of resolved dependencies in the project so we can find + * {@link LibraryCoordinates}. + * + * @author Madhura Bhave + * @author Scott Frederick + * @author Phillip Webb + * @author Paddy Drury + * @author Andy Wilkinson + */ +class ResolvedDependencies { + + private final Map configurationDependencies = new LinkedHashMap<>(); + + private String projectId(Project project) { + return project.getGroup() + ":" + project.getName() + ":" + project.getVersion(); + } + + void processConfiguration(Project project, Configuration configuration) { + Set localProjectIds = project.getRootProject().getAllprojects().stream().map(this::projectId) + .collect(Collectors.toSet()); + this.configurationDependencies.put(configuration, + new ResolvedConfigurationDependencies(localProjectIds, configuration.getResolvedConfiguration())); + } + + DependencyDescriptor find(File file) { + for (ResolvedConfigurationDependencies dependencies : this.configurationDependencies.values()) { + DependencyDescriptor dependency = dependencies.find(file); + if (dependency != null) { + return dependency; + } + } + return null; + } + + /** + * Stores details of resolved configuration dependencies. + */ + private static class ResolvedConfigurationDependencies { + + private final Map dependencies = new LinkedHashMap<>(); + + ResolvedConfigurationDependencies(Set projectDependencyIds, + ResolvedConfiguration resolvedConfiguration) { + if (!resolvedConfiguration.hasError()) { + for (ResolvedArtifact resolvedArtifact : resolvedConfiguration.getResolvedArtifacts()) { + ModuleVersionIdentifier id = resolvedArtifact.getModuleVersion().getId(); + boolean projectDependency = projectDependencyIds + .contains(id.getGroup() + ":" + id.getName() + ":" + id.getVersion()); + this.dependencies.put(resolvedArtifact.getFile(), new DependencyDescriptor( + new ModuleVersionIdentifierLibraryCoordinates(id), projectDependency)); + } + } + } + + DependencyDescriptor find(File file) { + return this.dependencies.get(file); + } + + } + + /** + * Adapts a {@link ModuleVersionIdentifier} to {@link LibraryCoordinates}. + */ + private static class ModuleVersionIdentifierLibraryCoordinates implements LibraryCoordinates { + + private final ModuleVersionIdentifier identifier; + + ModuleVersionIdentifierLibraryCoordinates(ModuleVersionIdentifier identifier) { + this.identifier = identifier; + } + + @Override + public String getGroupId() { + return this.identifier.getGroup(); + } + + @Override + public String getArtifactId() { + return this.identifier.getName(); + } + + @Override + public String getVersion() { + return this.identifier.getVersion(); + } + + @Override + public String toString() { + return this.identifier.toString(); + } + + } + + /** + * Describes a dependency in a {@link ResolvedConfiguration}. + */ + static final class DependencyDescriptor { + + private final LibraryCoordinates coordinates; + + private final boolean projectDependency; + + private DependencyDescriptor(LibraryCoordinates coordinates, boolean projectDependency) { + this.coordinates = coordinates; + this.projectDependency = projectDependency; + } + + LibraryCoordinates getCoordinates() { + return this.coordinates; + } + + boolean isProjectDependency() { + return this.projectDependency; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/run/BootRun.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/run/BootRun.java index 3a9b47832abd..e08531c414e6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/run/BootRun.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/run/BootRun.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,17 @@ package org.springframework.boot.gradle.tasks.run; +import java.io.File; import java.lang.reflect.Method; +import java.util.Set; import org.gradle.api.file.SourceDirectorySet; +import org.gradle.api.provider.Property; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.JavaExec; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.SourceSetOutput; +import org.gradle.jvm.toolchain.JavaLauncher; /** * Custom {@link JavaExec} task for running a Spring Boot application. @@ -63,8 +67,9 @@ public void setOptimizedLaunch(boolean optimizedLaunch) { * @param sourceSet the source set */ public void sourceResources(SourceSet sourceSet) { - setClasspath(getProject().files(sourceSet.getResources().getSrcDirs(), getClasspath()) - .filter((file) -> !file.equals(sourceSet.getOutput().getResourcesDir()))); + File resourcesDir = sourceSet.getOutput().getResourcesDir(); + Set srcDirs = sourceSet.getResources().getSrcDirs(); + setClasspath(getProject().files(srcDirs, getClasspath()).filter((file) -> !file.equals(resourcesDir))); } @Override @@ -78,12 +83,16 @@ public void exec() { } if (System.console() != null) { // Record that the console is available here for AnsiOutput to detect later - this.getEnvironment().put("spring.output.ansi.console-available", true); + getEnvironment().put("spring.output.ansi.console-available", true); } super.exec(); } private boolean isJava13OrLater() { + Property javaLauncher = this.getJavaLauncher(); + if (javaLauncher.isPresent()) { + return javaLauncher.get().getMetadata().getLanguageVersion().asInt() >= 13; + } for (Method method : String.class.getMethods()) { if (method.getName().equals("stripIndent")) { return true; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/util/VersionExtractor.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/util/VersionExtractor.java new file mode 100644 index 000000000000..72e779b8f97a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/util/VersionExtractor.java @@ -0,0 +1,68 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.gradle.util; + +import java.io.File; +import java.io.IOException; +import java.net.JarURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.util.jar.Attributes; +import java.util.jar.JarFile; + +/** + * Extracts version information for a Class. + * + * @author Andy Wilkinson + * @author Scott Frederick + * @since 2.3.0 + */ +public final class VersionExtractor { + + private VersionExtractor() { + } + + /** + * Return the version information for the provided {@link Class}. + * @param cls the Class to retrieve the version for + * @return the version, or {@code null} if a version can not be extracted + */ + public static String forClass(Class cls) { + String implementationVersion = cls.getPackage().getImplementationVersion(); + if (implementationVersion != null) { + return implementationVersion; + } + URL codeSourceLocation = cls.getProtectionDomain().getCodeSource().getLocation(); + try { + URLConnection connection = codeSourceLocation.openConnection(); + if (connection instanceof JarURLConnection) { + return getImplementationVersion(((JarURLConnection) connection).getJarFile()); + } + try (JarFile jarFile = new JarFile(new File(codeSourceLocation.toURI()))) { + return getImplementationVersion(jarFile); + } + } + catch (Exception ex) { + return null; + } + } + + private static String getImplementationVersion(JarFile jarFile) throws IOException { + return jarFile.getManifest().getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/util/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/util/package-info.java new file mode 100644 index 000000000000..c6c29f8ad739 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/util/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Shared utility classes. + */ +package org.springframework.boot.gradle.util; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/resources/META-INF/gradle-plugins/org.springframework.boot.properties b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/resources/META-INF/gradle-plugins/org.springframework.boot.properties deleted file mode 100644 index 730938f5c3b3..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/resources/META-INF/gradle-plugins/org.springframework.boot.properties +++ /dev/null @@ -1 +0,0 @@ -implementation-class=org.springframework.boot.gradle.plugin.SpringBootPlugin diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/com/example/bootjar/classpath/BootJarClasspathApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/com/example/bootjar/classpath/BootJarClasspathApplication.java new file mode 100644 index 000000000000..d4cdec72e0ef --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/com/example/bootjar/classpath/BootJarClasspathApplication.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.bootjar.classpath; + +import java.net.URL; +import java.net.URLClassLoader; + +/** + * Application used for testing classpath handling with BootJar. + * + * @author Andy Wilkinson + */ +public class BootJarClasspathApplication { + + protected BootJarClasspathApplication() { + + } + + public static void main(String[] args) { + int i = 1; + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + for (URL url : ((URLClassLoader) classLoader).getURLs()) { + System.out.println(i++ + ". " + url.getFile()); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/com/example/bootjar/main/CustomMainClass.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/com/example/bootjar/main/CustomMainClass.java new file mode 100644 index 000000000000..c1b044c95ac8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/com/example/bootjar/main/CustomMainClass.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.bootjar.main; + +/** + * Application used for testing {@code BootRun}'s main class configuration. + * + * @author Andy Wilkinson + */ +public class CustomMainClass { + + protected CustomMainClass() { + + } + + public static void main(String[] args) { + System.out.println(CustomMainClass.class.getName()); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/com/example/classpath/BootRunClasspathApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/com/example/bootrun/classpath/BootRunClasspathApplication.java similarity index 91% rename from spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/com/example/classpath/BootRunClasspathApplication.java rename to spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/com/example/bootrun/classpath/BootRunClasspathApplication.java index 44f579e99ee5..deaac0aafc43 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/com/example/classpath/BootRunClasspathApplication.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/com/example/bootrun/classpath/BootRunClasspathApplication.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example.classpath; +package com.example.bootrun.classpath; import java.io.File; import java.lang.management.ManagementFactory; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/com/example/jvmargs/BootRunJvmArgsApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/com/example/bootrun/jvmargs/BootRunJvmArgsApplication.java similarity index 91% rename from spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/com/example/jvmargs/BootRunJvmArgsApplication.java rename to spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/com/example/bootrun/jvmargs/BootRunJvmArgsApplication.java index f3b111f716e7..4257b5ad54de 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/com/example/jvmargs/BootRunJvmArgsApplication.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/com/example/bootrun/jvmargs/BootRunJvmArgsApplication.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example.jvmargs; +package com.example.bootrun.jvmargs; import java.lang.management.ManagementFactory; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/com/example/bootrun/main/CustomMainClass.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/com/example/bootrun/main/CustomMainClass.java new file mode 100644 index 000000000000..ccb847c99d7b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/com/example/bootrun/main/CustomMainClass.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.bootrun.main; + +/** + * Application used for testing {@code BootRun}'s main class configuration. + * + * @author Andy Wilkinson + */ +public class CustomMainClass { + + protected CustomMainClass() { + + } + + public static void main(String[] args) { + System.out.println(CustomMainClass.class.getName()); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/com/example/bootwar/main/CustomMainClass.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/com/example/bootwar/main/CustomMainClass.java new file mode 100644 index 000000000000..be96fb18e7b5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/com/example/bootwar/main/CustomMainClass.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.bootwar.main; + +/** + * Application used for testing {@code BootRun}'s main class configuration. + * + * @author Andy Wilkinson + */ +public class CustomMainClass { + + protected CustomMainClass() { + + } + + public static void main(String[] args) { + System.out.println(CustomMainClass.class.getName()); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/TaskConfigurationAvoidanceTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/TaskConfigurationAvoidanceTests.java new file mode 100644 index 000000000000..e8c779803bc6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/TaskConfigurationAvoidanceTests.java @@ -0,0 +1,156 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.gradle; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; + +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.base.Predicate; +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.domain.JavaMethodCall; +import com.tngtech.archunit.core.domain.JavaType; +import com.tngtech.archunit.core.importer.ImportOption; +import com.tngtech.archunit.core.importer.Location; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.syntax.ArchRuleDefinition; +import org.gradle.api.Action; +import org.gradle.api.tasks.TaskCollection; +import org.gradle.api.tasks.TaskContainer; + +/** + * Tests that verify the plugin's compliance with task configuration avoidance. + * + * @author Andy Wilkinson + */ +@AnalyzeClasses(packages = "org.springframework.boot.gradle", + importOptions = TaskConfigurationAvoidanceTests.DoNotIncludeTests.class) +class TaskConfigurationAvoidanceTests { + + @ArchTest + void noApisThatCauseEagerTaskConfigurationShouldBeCalled(JavaClasses classes) { + ProhibitedMethods prohibited = new ProhibitedMethods(); + prohibited.on(TaskContainer.class).methodsNamed("create", "findByPath, getByPath").method("withType", + Class.class, Action.class); + prohibited.on(TaskCollection.class).methodsNamed("findByName", "getByName"); + ArchRuleDefinition.noClasses().should() + .callMethodWhere(DescribedPredicate.describe("it would cause eager task configuration", prohibited)) + .check(classes); + } + + static class DoNotIncludeTests implements ImportOption { + + @Override + public boolean includes(Location location) { + return !location.matches(Pattern.compile(".*Tests\\.class")); + } + + } + + private static final class ProhibitedMethods implements Predicate { + + private final List> prohibited = new ArrayList<>(); + + private ProhibitedConfigurer on(Class type) { + return new ProhibitedConfigurer(type); + } + + @Override + public boolean apply(JavaMethodCall methodCall) { + for (Predicate spec : this.prohibited) { + if (spec.apply(methodCall)) { + return true; + } + } + return false; + } + + private final class ProhibitedConfigurer { + + private final Class type; + + private ProhibitedConfigurer(Class type) { + this.type = type; + } + + private ProhibitedConfigurer methodsNamed(String... names) { + for (String name : names) { + ProhibitedMethods.this.prohibited.add(new ProhibitMethodsNamed(this.type, name)); + } + return this; + } + + private ProhibitedConfigurer method(String name, Class... parameterTypes) { + ProhibitedMethods.this.prohibited + .add(new ProhibitMethod(this.type, name, Arrays.asList(parameterTypes))); + return this; + } + + } + + static class ProhibitMethodsNamed implements Predicate { + + private final Class owner; + + private final String name; + + ProhibitMethodsNamed(Class owner, String name) { + this.owner = owner; + this.name = name; + } + + @Override + public boolean apply(JavaMethodCall methodCall) { + return methodCall.getTargetOwner().isEquivalentTo(this.owner) && methodCall.getName().equals(this.name); + } + + } + + private static final class ProhibitMethod extends ProhibitMethodsNamed { + + private final List> parameterTypes; + + private ProhibitMethod(Class owner, String name, List> parameterTypes) { + super(owner, name); + this.parameterTypes = parameterTypes; + } + + @Override + public boolean apply(JavaMethodCall methodCall) { + return super.apply(methodCall) && match(methodCall.getTarget().getParameterTypes()); + } + + private boolean match(List callParameterTypes) { + if (this.parameterTypes.size() != callParameterTypes.size()) { + return false; + } + for (int i = 0; i < this.parameterTypes.size(); i++) { + if (!callParameterTypes.get(i).toErasure().isEquivalentTo(this.parameterTypes.get(i))) { + return false; + } + } + return true; + } + + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/GettingStartedDocumentationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/GettingStartedDocumentationTests.java index 288f0fce3a58..0471c9041e50 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/GettingStartedDocumentationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/GettingStartedDocumentationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,16 +29,16 @@ * @author Jean-Baptiste Nizet */ @ExtendWith(GradleMultiDslExtension.class) -public class GettingStartedDocumentationTests { +class GettingStartedDocumentationTests { GradleBuild gradleBuild; - // NOTE: We can't run any `apply-plugin` tests because during a release the + // NOTE: We can't run any 'apply-plugin' tests because during a release the // jar won't be there @TestTemplate - public void typicalPluginsAppliesExceptedPlugins() { - this.gradleBuild.script("src/main/gradle/getting-started/typical-plugins").build("verify"); + void typicalPluginsAppliesExceptedPlugins() { + this.gradleBuild.script("src/docs/gradle/getting-started/typical-plugins").build("verify"); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/IntegratingWithActuatorDocumentationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/IntegratingWithActuatorDocumentationTests.java index c877616a0bf3..da30a982fc20 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/IntegratingWithActuatorDocumentationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/IntegratingWithActuatorDocumentationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,20 +36,20 @@ * @author Jean-Baptiste Nizet */ @ExtendWith(GradleMultiDslExtension.class) -public class IntegratingWithActuatorDocumentationTests { +class IntegratingWithActuatorDocumentationTests { GradleBuild gradleBuild; @TestTemplate - public void basicBuildInfo() throws IOException { - this.gradleBuild.script("src/main/gradle/integrating-with-actuator/build-info-basic").build("bootBuildInfo"); + void basicBuildInfo() { + this.gradleBuild.script("src/docs/gradle/integrating-with-actuator/build-info-basic").build("bootBuildInfo"); assertThat(new File(this.gradleBuild.getProjectDir(), "build/resources/main/META-INF/build-info.properties")) .isFile(); } @TestTemplate - public void buildInfoCustomValues() throws IOException { - this.gradleBuild.script("src/main/gradle/integrating-with-actuator/build-info-custom-values") + void buildInfoCustomValues() { + this.gradleBuild.script("src/docs/gradle/integrating-with-actuator/build-info-custom-values") .build("bootBuildInfo"); File file = new File(this.gradleBuild.getProjectDir(), "build/resources/main/META-INF/build-info.properties"); assertThat(file).isFile(); @@ -61,8 +61,8 @@ public void buildInfoCustomValues() throws IOException { } @TestTemplate - public void buildInfoAdditional() throws IOException { - this.gradleBuild.script("src/main/gradle/integrating-with-actuator/build-info-additional") + void buildInfoAdditional() { + this.gradleBuild.script("src/docs/gradle/integrating-with-actuator/build-info-additional") .build("bootBuildInfo"); File file = new File(this.gradleBuild.getProjectDir(), "build/resources/main/META-INF/build-info.properties"); assertThat(file).isFile(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/ManagingDependenciesDocumentationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/ManagingDependenciesDocumentationTests.java index f2aa45dcb097..d2608379fa75 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/ManagingDependenciesDocumentationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/ManagingDependenciesDocumentationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,34 +33,47 @@ * @author Jean-Baptiste Nizet */ @ExtendWith(GradleMultiDslExtension.class) -public class ManagingDependenciesDocumentationTests { +class ManagingDependenciesDocumentationTests { GradleBuild gradleBuild; @TestTemplate - public void dependenciesExampleEvaluatesSuccessfully() { - this.gradleBuild.script("src/main/gradle/managing-dependencies/dependencies").build(); + void dependenciesExampleEvaluatesSuccessfully() { + this.gradleBuild.script("src/docs/gradle/managing-dependencies/dependencies").build(); } @TestTemplate - public void customManagedVersions() { - assertThat(this.gradleBuild.script("src/main/gradle/managing-dependencies/custom-version").build("slf4jVersion") + void customManagedVersions() { + assertThat(this.gradleBuild.script("src/docs/gradle/managing-dependencies/custom-version").build("slf4jVersion") .getOutput()).contains("1.7.20"); } @TestTemplate - public void dependencyManagementInIsolation() { - assertThat(this.gradleBuild.script("src/main/gradle/managing-dependencies/configure-bom") + void dependencyManagementInIsolation() { + assertThat(this.gradleBuild.script("src/docs/gradle/managing-dependencies/configure-bom") .build("dependencyManagement").getOutput()).contains("org.springframework.boot:spring-boot-starter "); } @TestTemplate - public void dependencyManagementInIsolationWithPluginsBlock() { + void dependencyManagementInIsolationWithPluginsBlock() { assumingThat(this.gradleBuild.getDsl() == Dsl.KOTLIN, () -> assertThat( - this.gradleBuild.script("src/main/gradle/managing-dependencies/configure-bom-with-plugins") + this.gradleBuild.script("src/docs/gradle/managing-dependencies/configure-bom-with-plugins") .build("dependencyManagement").getOutput()) .contains("org.springframework.boot:spring-boot-starter TEST-SNAPSHOT")); } + @TestTemplate + void configurePlatform() { + assertThat(this.gradleBuild.script("src/docs/gradle/managing-dependencies/configure-platform") + .build("dependencies", "--configuration", "compileClasspath").getOutput()) + .contains("org.springframework.boot:spring-boot-starter "); + } + + @TestTemplate + void customManagedVersionsWithPlatform() { + assertThat(this.gradleBuild.script("src/docs/gradle/managing-dependencies/custom-version-with-platform") + .build("dependencies", "--configuration", "compileClasspath").getOutput()).contains("1.7.20"); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PackagingDocumentationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PackagingDocumentationTests.java index e62426e3c90c..6b68af4c5bdf 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PackagingDocumentationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PackagingDocumentationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,12 +21,14 @@ import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; +import java.util.Collections; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; import java.util.zip.ZipEntry; +import org.gradle.testkit.runner.BuildResult; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; @@ -41,20 +43,21 @@ * * @author Andy Wilkinson * @author Jean-Baptiste Nizet + * @author Scott Frederick */ @ExtendWith(GradleMultiDslExtension.class) -public class PackagingDocumentationTests { +class PackagingDocumentationTests { GradleBuild gradleBuild; @TestTemplate - public void warContainerDependencyEvaluatesSuccessfully() { - this.gradleBuild.script("src/main/gradle/packaging/war-container-dependency").build(); + void warContainerDependencyEvaluatesSuccessfully() { + this.gradleBuild.script("src/docs/gradle/packaging/war-container-dependency").build(); } @TestTemplate - public void bootJarMainClass() throws IOException { - this.gradleBuild.script("src/main/gradle/packaging/boot-jar-main-class").build("bootJar"); + void bootJarMainClass() throws IOException { + this.gradleBuild.script("src/docs/gradle/packaging/boot-jar-main-class").build("bootJar"); File file = new File(this.gradleBuild.getProjectDir(), "build/libs/" + this.gradleBuild.getProjectDir().getName() + ".jar"); assertThat(file).isFile(); @@ -65,8 +68,8 @@ public void bootJarMainClass() throws IOException { } @TestTemplate - public void bootJarManifestMainClass() throws IOException { - this.gradleBuild.script("src/main/gradle/packaging/boot-jar-manifest-main-class").build("bootJar"); + void bootJarManifestMainClass() throws IOException { + this.gradleBuild.script("src/docs/gradle/packaging/boot-jar-manifest-main-class").build("bootJar"); File file = new File(this.gradleBuild.getProjectDir(), "build/libs/" + this.gradleBuild.getProjectDir().getName() + ".jar"); assertThat(file).isFile(); @@ -77,8 +80,8 @@ public void bootJarManifestMainClass() throws IOException { } @TestTemplate - public void applicationPluginMainClass() throws IOException { - this.gradleBuild.script("src/main/gradle/packaging/application-plugin-main-class").build("bootJar"); + void applicationPluginMainClass() throws IOException { + this.gradleBuild.script("src/docs/gradle/packaging/application-plugin-main-class").build("bootJar"); File file = new File(this.gradleBuild.getProjectDir(), "build/libs/" + this.gradleBuild.getProjectDir().getName() + ".jar"); assertThat(file).isFile(); @@ -89,8 +92,8 @@ public void applicationPluginMainClass() throws IOException { } @TestTemplate - public void springBootDslMainClass() throws IOException { - this.gradleBuild.script("src/main/gradle/packaging/spring-boot-dsl-main-class").build("bootJar"); + void springBootDslMainClass() throws IOException { + this.gradleBuild.script("src/docs/gradle/packaging/spring-boot-dsl-main-class").build("bootJar"); File file = new File(this.gradleBuild.getProjectDir(), "build/libs/" + this.gradleBuild.getProjectDir().getName() + ".jar"); assertThat(file).isFile(); @@ -101,9 +104,9 @@ public void springBootDslMainClass() throws IOException { } @TestTemplate - public void bootWarIncludeDevtools() throws IOException { + void bootWarIncludeDevtools() throws IOException { jarFile(new File(this.gradleBuild.getProjectDir(), "spring-boot-devtools-1.2.3.RELEASE.jar")); - this.gradleBuild.script("src/main/gradle/packaging/boot-war-include-devtools").build("bootWar"); + this.gradleBuild.script("src/docs/gradle/packaging/boot-war-include-devtools").build("bootWar"); File file = new File(this.gradleBuild.getProjectDir(), "build/libs/" + this.gradleBuild.getProjectDir().getName() + ".war"); assertThat(file).isFile(); @@ -113,8 +116,8 @@ public void bootWarIncludeDevtools() throws IOException { } @TestTemplate - public void bootJarRequiresUnpack() throws IOException { - this.gradleBuild.script("src/main/gradle/packaging/boot-jar-requires-unpack").build("bootJar"); + void bootJarRequiresUnpack() throws IOException { + this.gradleBuild.script("src/docs/gradle/packaging/boot-jar-requires-unpack").build("bootJar"); File file = new File(this.gradleBuild.getProjectDir(), "build/libs/" + this.gradleBuild.getProjectDir().getName() + ".jar"); assertThat(file).isFile(); @@ -126,8 +129,8 @@ public void bootJarRequiresUnpack() throws IOException { } @TestTemplate - public void bootJarIncludeLaunchScript() throws IOException { - this.gradleBuild.script("src/main/gradle/packaging/boot-jar-include-launch-script").build("bootJar"); + void bootJarIncludeLaunchScript() throws IOException { + this.gradleBuild.script("src/docs/gradle/packaging/boot-jar-include-launch-script").build("bootJar"); File file = new File(this.gradleBuild.getProjectDir(), "build/libs/" + this.gradleBuild.getProjectDir().getName() + ".jar"); assertThat(file).isFile(); @@ -135,8 +138,8 @@ public void bootJarIncludeLaunchScript() throws IOException { } @TestTemplate - public void bootJarLaunchScriptProperties() throws IOException { - this.gradleBuild.script("src/main/gradle/packaging/boot-jar-launch-script-properties").build("bootJar"); + void bootJarLaunchScriptProperties() throws IOException { + this.gradleBuild.script("src/docs/gradle/packaging/boot-jar-launch-script-properties").build("bootJar"); File file = new File(this.gradleBuild.getProjectDir(), "build/libs/" + this.gradleBuild.getProjectDir().getName() + ".jar"); assertThat(file).isFile(); @@ -144,11 +147,11 @@ public void bootJarLaunchScriptProperties() throws IOException { } @TestTemplate - public void bootJarCustomLaunchScript() throws IOException { + void bootJarCustomLaunchScript() throws IOException { File customScriptFile = new File(this.gradleBuild.getProjectDir(), "src/custom.script"); customScriptFile.getParentFile().mkdirs(); FileCopyUtils.copy("custom", new FileWriter(customScriptFile)); - this.gradleBuild.script("src/main/gradle/packaging/boot-jar-custom-launch-script").build("bootJar"); + this.gradleBuild.script("src/docs/gradle/packaging/boot-jar-custom-launch-script").build("bootJar"); File file = new File(this.gradleBuild.getProjectDir(), "build/libs/" + this.gradleBuild.getProjectDir().getName() + ".jar"); assertThat(file).isFile(); @@ -156,8 +159,8 @@ public void bootJarCustomLaunchScript() throws IOException { } @TestTemplate - public void bootWarPropertiesLauncher() throws IOException { - this.gradleBuild.script("src/main/gradle/packaging/boot-war-properties-launcher").build("bootWar"); + void bootWarPropertiesLauncher() throws IOException { + this.gradleBuild.script("src/docs/gradle/packaging/boot-war-properties-launcher").build("bootWar"); File file = new File(this.gradleBuild.getProjectDir(), "build/libs/" + this.gradleBuild.getProjectDir().getName() + ".war"); assertThat(file).isFile(); @@ -168,14 +171,149 @@ public void bootWarPropertiesLauncher() throws IOException { } @TestTemplate - public void bootJarAndJar() { - this.gradleBuild.script("src/main/gradle/packaging/boot-jar-and-jar").build("assemble"); - File jar = new File(this.gradleBuild.getProjectDir(), + void onlyBootJar() throws IOException { + this.gradleBuild.script("src/docs/gradle/packaging/only-boot-jar").build("assemble"); + File plainJar = new File(this.gradleBuild.getProjectDir(), + "build/libs/" + this.gradleBuild.getProjectDir().getName() + "-plain.jar"); + assertThat(plainJar).doesNotExist(); + File bootJar = new File(this.gradleBuild.getProjectDir(), "build/libs/" + this.gradleBuild.getProjectDir().getName() + ".jar"); - assertThat(jar).isFile(); + assertThat(bootJar).isFile(); + try (JarFile jar = new JarFile(bootJar)) { + assertThat(jar.getEntry("BOOT-INF/")).isNotNull(); + } + } + + @TestTemplate + void classifiedBootJar() throws IOException { + this.gradleBuild.script("src/docs/gradle/packaging/boot-jar-and-jar-classifiers").build("assemble"); + File plainJar = new File(this.gradleBuild.getProjectDir(), + "build/libs/" + this.gradleBuild.getProjectDir().getName() + ".jar"); + assertThat(plainJar).isFile(); + try (JarFile jar = new JarFile(plainJar)) { + assertThat(jar.getEntry("BOOT-INF/")).isNull(); + } File bootJar = new File(this.gradleBuild.getProjectDir(), "build/libs/" + this.gradleBuild.getProjectDir().getName() + "-boot.jar"); assertThat(bootJar).isFile(); + try (JarFile jar = new JarFile(bootJar)) { + assertThat(jar.getEntry("BOOT-INF/")).isNotNull(); + } + } + + @TestTemplate + void bootJarLayeredDisabled() throws IOException { + this.gradleBuild.script("src/docs/gradle/packaging/boot-jar-layered-disabled").build("bootJar"); + File file = new File(this.gradleBuild.getProjectDir(), + "build/libs/" + this.gradleBuild.getProjectDir().getName() + ".jar"); + assertThat(file).isFile(); + try (JarFile jar = new JarFile(file)) { + JarEntry entry = jar.getJarEntry("BOOT-INF/layers.idx"); + assertThat(entry).isNull(); + } + } + + @TestTemplate + void bootJarLayeredCustom() throws IOException { + this.gradleBuild.script("src/docs/gradle/packaging/boot-jar-layered-custom").build("bootJar"); + File file = new File(this.gradleBuild.getProjectDir(), + "build/libs/" + this.gradleBuild.getProjectDir().getName() + ".jar"); + assertThat(file).isFile(); + try (JarFile jar = new JarFile(file)) { + JarEntry entry = jar.getJarEntry("BOOT-INF/layers.idx"); + assertThat(entry).isNotNull(); + assertThat(Collections.list(jar.entries()).stream().map(JarEntry::getName) + .filter((name) -> name.startsWith("BOOT-INF/lib/spring-boot"))).isNotEmpty(); + } + } + + @TestTemplate + void bootJarLayeredExcludeTools() throws IOException { + this.gradleBuild.script("src/docs/gradle/packaging/boot-jar-layered-exclude-tools").build("bootJar"); + File file = new File(this.gradleBuild.getProjectDir(), + "build/libs/" + this.gradleBuild.getProjectDir().getName() + ".jar"); + assertThat(file).isFile(); + try (JarFile jar = new JarFile(file)) { + JarEntry entry = jar.getJarEntry("BOOT-INF/layers.idx"); + assertThat(entry).isNotNull(); + assertThat(Collections.list(jar.entries()).stream().map(JarEntry::getName) + .filter((name) -> name.startsWith("BOOT-INF/lib/spring-boot"))).isEmpty(); + } + } + + @TestTemplate + void bootBuildImageWithBuilder() { + BuildResult result = this.gradleBuild.script("src/docs/gradle/packaging/boot-build-image-builder") + .build("bootBuildImageBuilder"); + assertThat(result.getOutput()).contains("builder=mine/java-cnb-builder").contains("runImage=mine/java-cnb-run"); + } + + @TestTemplate + void bootBuildImageWithCustomBuildpackJvmVersion() { + BuildResult result = this.gradleBuild.script("src/docs/gradle/packaging/boot-build-image-env") + .build("bootBuildImageEnvironment"); + assertThat(result.getOutput()).contains("BP_JVM_VERSION=8.*"); + } + + @TestTemplate + void bootBuildImageWithCustomProxySettings() { + BuildResult result = this.gradleBuild.script("src/docs/gradle/packaging/boot-build-image-env-proxy") + .build("bootBuildImageEnvironment"); + assertThat(result.getOutput()).contains("HTTP_PROXY=http://proxy.example.com") + .contains("HTTPS_PROXY=https://proxy.example.com"); + } + + @TestTemplate + void bootBuildImageWithCustomRuntimeConfiguration() { + BuildResult result = this.gradleBuild.script("src/docs/gradle/packaging/boot-build-image-env-runtime") + .build("bootBuildImageEnvironment"); + assertThat(result.getOutput()).contains("BPE_DELIM_JAVA_TOOL_OPTIONS= ") + .contains("BPE_APPEND_JAVA_TOOL_OPTIONS=-XX:+HeapDumpOnOutOfMemoryError"); + } + + @TestTemplate + void bootBuildImageWithCustomImageName() { + BuildResult result = this.gradleBuild.script("src/docs/gradle/packaging/boot-build-image-name") + .build("bootBuildImageName"); + assertThat(result.getOutput()).contains("example.com/library/" + this.gradleBuild.getProjectDir().getName()); + } + + @TestTemplate + void bootBuildImageWithDockerHost() { + BuildResult result = this.gradleBuild.script("src/docs/gradle/packaging/boot-build-image-docker-host") + .build("bootBuildImageDocker"); + assertThat(result.getOutput()).contains("host=tcp://192.168.99.100:2376").contains("tlsVerify=true") + .contains("certPath=/home/users/.minikube/certs"); + } + + @TestTemplate + void bootBuildImageWithDockerUserAuth() { + BuildResult result = this.gradleBuild.script("src/docs/gradle/packaging/boot-build-image-docker-auth-user") + .build("bootBuildImageDocker"); + assertThat(result.getOutput()).contains("username=user").contains("password=secret") + .contains("url=https://docker.example.com/v1/").contains("email=user@example.com"); + } + + @TestTemplate + void bootBuildImageWithDockerTokenAuth() { + BuildResult result = this.gradleBuild.script("src/docs/gradle/packaging/boot-build-image-docker-auth-token") + .build("bootBuildImageDocker"); + assertThat(result.getOutput()).contains("token=9cbaf023786cd7..."); + } + + @TestTemplate + void bootBuildImagePublish() { + BuildResult result = this.gradleBuild.script("src/docs/gradle/packaging/boot-build-image-publish") + .build("bootBuildImagePublish"); + assertThat(result.getOutput()).contains("true"); + } + + @TestTemplate + void bootBuildImageWithBuildpacks() { + BuildResult result = this.gradleBuild.script("src/docs/gradle/packaging/boot-build-image-buildpacks") + .build("bootBuildImageBuildpacks"); + assertThat(result.getOutput()).contains("file:///path/to/example-buildpack.tgz") + .contains("urn:cnb:builder:paketo-buildpacks/java"); } protected void jarFile(File file) throws IOException { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PublishingDocumentationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PublishingDocumentationTests.java index 5d9439a93fb4..028dfdd1094a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PublishingDocumentationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PublishingDocumentationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,9 @@ package org.springframework.boot.gradle.docs; -import java.io.IOException; - import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.condition.DisabledForJreRange; +import org.junit.jupiter.api.condition.JRE; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.gradle.junit.GradleMultiDslExtension; @@ -33,19 +33,21 @@ * @author Jean-Baptiste Nizet */ @ExtendWith(GradleMultiDslExtension.class) -public class PublishingDocumentationTests { +class PublishingDocumentationTests { GradleBuild gradleBuild; + @DisabledForJreRange(min = JRE.JAVA_16) @TestTemplate - public void mavenUpload() throws IOException { - assertThat(this.gradleBuild.script("src/main/gradle/publishing/maven").build("deployerRepository").getOutput()) - .contains("https://repo.example.com"); + void mavenUpload() { + assertThat(this.gradleBuild.expectDeprecationWarningsWithAtLeastVersion("5.6") + .script("src/docs/gradle/publishing/maven").build("deployerRepository").getOutput()) + .contains("https://repo.example.com"); } @TestTemplate - public void mavenPublish() throws IOException { - assertThat(this.gradleBuild.script("src/main/gradle/publishing/maven-publish").build("publishingConfiguration") + void mavenPublish() { + assertThat(this.gradleBuild.script("src/docs/gradle/publishing/maven-publish").build("publishingConfiguration") .getOutput()).contains("MavenPublication").contains("https://repo.example.com"); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/RunningDocumentationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/RunningDocumentationTests.java index 20d295f4803c..87d597f384be 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/RunningDocumentationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/RunningDocumentationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,9 +17,13 @@ package org.springframework.boot.gradle.docs; import java.io.File; +import java.io.FileWriter; import java.io.IOException; +import java.io.PrintWriter; import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.condition.DisabledForJreRange; +import org.junit.jupiter.api.condition.JRE; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.gradle.junit.GradleMultiDslExtension; @@ -34,38 +38,69 @@ * @author Jean-Baptiste Nizet */ @ExtendWith(GradleMultiDslExtension.class) -public class RunningDocumentationTests { +class RunningDocumentationTests { GradleBuild gradleBuild; @TestTemplate - public void bootRunMain() throws IOException { - assertThat(this.gradleBuild.script("src/main/gradle/running/boot-run-main").build("configuredMainClass") - .getOutput()).contains("com.example.ExampleApplication"); + @DisabledForJreRange(min = JRE.JAVA_13) + void bootRunMain() throws IOException { + writeMainClass(); + assertThat(this.gradleBuild.script("src/docs/gradle/running/boot-run-main").build("bootRun").getOutput()) + .contains("com.example.ExampleApplication"); } @TestTemplate - public void applicationPluginMainClassName() { - assertThat(this.gradleBuild.script("src/main/gradle/running/application-plugin-main-class-name") - .build("configuredMainClass").getOutput()).contains("com.example.ExampleApplication"); + void applicationPluginMainClassName() throws IOException { + writeMainClass(); + assertThat(this.gradleBuild.script("src/docs/gradle/running/application-plugin-main-class-name") + .build("bootRun").getOutput()).contains("com.example.ExampleApplication"); } @TestTemplate - public void springBootDslMainClassName() throws IOException { - assertThat(this.gradleBuild.script("src/main/gradle/running/spring-boot-dsl-main-class-name") - .build("configuredMainClass").getOutput()).contains("com.example.ExampleApplication"); + void springBootDslMainClassName() throws IOException { + writeMainClass(); + assertThat(this.gradleBuild.script("src/docs/gradle/running/spring-boot-dsl-main-class-name").build("bootRun") + .getOutput()).contains("com.example.ExampleApplication"); } @TestTemplate - public void bootRunSourceResources() throws IOException { - assertThat(this.gradleBuild.script("src/main/gradle/running/boot-run-source-resources") + void bootRunSourceResources() { + assertThat(this.gradleBuild.script("src/docs/gradle/running/boot-run-source-resources") .build("configuredClasspath").getOutput()).contains(new File("src/main/resources").getPath()); } @TestTemplate - public void bootRunDisableOptimizedLaunch() throws IOException { - assertThat(this.gradleBuild.script("src/main/gradle/running/boot-run-disable-optimized-launch") + void bootRunDisableOptimizedLaunch() { + assertThat(this.gradleBuild.script("src/docs/gradle/running/boot-run-disable-optimized-launch") .build("optimizedLaunch").getOutput()).contains("false"); } + @TestTemplate + void bootRunSystemPropertyDefaultValue() { + assertThat(this.gradleBuild.script("src/docs/gradle/running/boot-run-system-property") + .build("configuredSystemProperties").getOutput()).contains("com.example.property = default"); + } + + @TestTemplate + void bootRunSystemPropetry() { + assertThat(this.gradleBuild.script("src/docs/gradle/running/boot-run-system-property") + .build("-Pexample=custom", "configuredSystemProperties").getOutput()) + .contains("com.example.property = custom"); + } + + private void writeMainClass() throws IOException { + File exampleApplication = new File(this.gradleBuild.getProjectDir(), + "src/main/java/com/example/ExampleApplication.java"); + exampleApplication.getParentFile().mkdirs(); + try (PrintWriter writer = new PrintWriter(new FileWriter(exampleApplication))) { + writer.println("package com.example;"); + writer.println("public class ExampleApplication {"); + writer.println(" public static void main(String[] args) {"); + writer.println(" System.out.println(ExampleApplication.class.getName());"); + writer.println(" }"); + writer.println("}"); + } + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests.java index cc7615d8e493..3ca113400007 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,12 +22,11 @@ import java.util.Properties; import org.gradle.testkit.runner.TaskOutcome; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.TestTemplate; +import org.springframework.boot.gradle.junit.GradleCompatibility; import org.springframework.boot.gradle.tasks.buildinfo.BuildInfo; import org.springframework.boot.gradle.testkit.GradleBuild; -import org.springframework.boot.gradle.testkit.GradleBuildExtension; import static org.assertj.core.api.Assertions.assertThat; @@ -37,13 +36,13 @@ * * @author Andy Wilkinson */ -@ExtendWith(GradleBuildExtension.class) +@GradleCompatibility class BuildInfoDslIntegrationTests { - final GradleBuild gradleBuild = new GradleBuild(); + GradleBuild gradleBuild; - @Test - void basicJar() throws IOException { + @TestTemplate + void basicJar() { assertThat(this.gradleBuild.build("bootBuildInfo", "--stacktrace").task(":bootBuildInfo").getOutcome()) .isEqualTo(TaskOutcome.SUCCESS); Properties properties = buildInfoProperties(); @@ -53,8 +52,8 @@ void basicJar() throws IOException { assertThat(properties).containsEntry("build.version", "1.0"); } - @Test - void jarWithCustomName() throws IOException { + @TestTemplate + void jarWithCustomName() { assertThat(this.gradleBuild.build("bootBuildInfo", "--stacktrace").task(":bootBuildInfo").getOutcome()) .isEqualTo(TaskOutcome.SUCCESS); Properties properties = buildInfoProperties(); @@ -64,8 +63,8 @@ void jarWithCustomName() throws IOException { assertThat(properties).containsEntry("build.version", "1.0"); } - @Test - void basicWar() throws IOException { + @TestTemplate + void basicWar() { assertThat(this.gradleBuild.build("bootBuildInfo", "--stacktrace").task(":bootBuildInfo").getOutcome()) .isEqualTo(TaskOutcome.SUCCESS); Properties properties = buildInfoProperties(); @@ -75,8 +74,8 @@ void basicWar() throws IOException { assertThat(properties).containsEntry("build.version", "1.0"); } - @Test - void warWithCustomName() throws IOException { + @TestTemplate + void warWithCustomName() { assertThat(this.gradleBuild.build("bootBuildInfo", "--stacktrace").task(":bootBuildInfo").getOutcome()) .isEqualTo(TaskOutcome.SUCCESS); Properties properties = buildInfoProperties(); @@ -86,8 +85,8 @@ void warWithCustomName() throws IOException { assertThat(properties).containsEntry("build.version", "1.0"); } - @Test - void additionalProperties() throws IOException { + @TestTemplate + void additionalProperties() { assertThat(this.gradleBuild.build("bootBuildInfo", "--stacktrace").task(":bootBuildInfo").getOutcome()) .isEqualTo(TaskOutcome.SUCCESS); Properties properties = buildInfoProperties(); @@ -99,8 +98,8 @@ void additionalProperties() throws IOException { assertThat(properties).containsEntry("build.b", "bravo"); } - @Test - void classesDependency() throws IOException { + @TestTemplate + void classesDependency() { assertThat(this.gradleBuild.build("classes", "--stacktrace").task(":bootBuildInfo").getOutcome()) .isEqualTo(TaskOutcome.SUCCESS); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/junit/GradleBuildFieldSetter.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/junit/GradleBuildFieldSetter.java index b4fafbf81e8b..19ea8e67ccf5 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/junit/GradleBuildFieldSetter.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/junit/GradleBuildFieldSetter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ /** * {@link BeforeEachCallback} to set a test class's {@code gradleBuild} field prior to - * test exection. + * test execution. * * @author Andy Wilkinson */ diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/junit/GradleCompatibility.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/junit/GradleCompatibility.java new file mode 100644 index 000000000000..9617f0d94de6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/junit/GradleCompatibility.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.gradle.junit; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.Extension; + +import org.springframework.boot.gradle.testkit.GradleBuild; + +/** + * {@link Extension} that runs {@link TestTemplate templated tests} against multiple + * versions of Gradle. Test classes using the extension must have a non-private and + * non-final {@link GradleBuild} field named {@code gradleBuild}. + * + * @author Andy Wilkinson + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@ExtendWith(GradleCompatibilityExtension.class) +public @interface GradleCompatibility { + + /** + * Whether to include running Gradle with {@code --cache-configuration} cache in the + * compatibility matrix. + * @return {@code true} to enable the configuration cache, {@code false} otherwise + */ + boolean configurationCache() default false; + + String versionsLessThan() default ""; + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/junit/GradleCompatibilityExtension.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/junit/GradleCompatibilityExtension.java index 30d63b5dda7e..e9f445c1f036 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/junit/GradleCompatibilityExtension.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/junit/GradleCompatibilityExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,18 +16,23 @@ package org.springframework.boot.gradle.junit; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Stream; +import org.gradle.api.JavaVersion; +import org.gradle.util.GradleVersion; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.Extension; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.TestTemplateInvocationContext; import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider; +import org.junit.platform.commons.util.AnnotationUtils; import org.springframework.boot.gradle.testkit.GradleBuild; import org.springframework.boot.gradle.testkit.GradleBuildExtension; +import org.springframework.util.StringUtils; /** * {@link Extension} that runs {@link TestTemplate templated tests} against multiple @@ -36,14 +41,50 @@ * * @author Andy Wilkinson */ -public final class GradleCompatibilityExtension implements TestTemplateInvocationContextProvider { +final class GradleCompatibilityExtension implements TestTemplateInvocationContextProvider { - private static final List GRADLE_VERSIONS = Arrays.asList("default", "5.0", "5.1.1", "5.2.1", "5.3.1", - "5.4.1", "5.5.1", "5.6.4"); + private static final List GRADLE_VERSIONS; + + static { + JavaVersion javaVersion = JavaVersion.current(); + if (javaVersion.isCompatibleWith(JavaVersion.VERSION_HIGHER)) { + GRADLE_VERSIONS = Arrays.asList("7.3.3", "7.4.1"); + } + else if (javaVersion.isCompatibleWith(JavaVersion.VERSION_17)) { + GRADLE_VERSIONS = Arrays.asList("7.2", "7.3.3", "7.4.1"); + } + else if (javaVersion.isCompatibleWith(JavaVersion.VERSION_16)) { + GRADLE_VERSIONS = Arrays.asList("7.0.2", "7.1.1", "7.2", "7.3.3", "7.4.1"); + } + else { + GRADLE_VERSIONS = Arrays.asList("6.8.3", "current", "7.0.2", "7.1.1", "7.2", "7.3.3", "7.4.1"); + } + } @Override public Stream provideTestTemplateInvocationContexts(ExtensionContext context) { - return GRADLE_VERSIONS.stream().map(GradleVersionTestTemplateInvocationContext::new); + Stream gradleVersions = GRADLE_VERSIONS.stream().map((version) -> { + if (version.equals("current")) { + return GradleVersion.current().getVersion(); + } + return version; + }); + GradleCompatibility gradleCompatibility = AnnotationUtils + .findAnnotation(context.getRequiredTestClass(), GradleCompatibility.class).get(); + if (StringUtils.hasText(gradleCompatibility.versionsLessThan())) { + GradleVersion upperExclusive = GradleVersion.version(gradleCompatibility.versionsLessThan()); + gradleVersions = gradleVersions + .filter((version) -> GradleVersion.version(version).compareTo(upperExclusive) < 0); + } + return gradleVersions.flatMap((version) -> { + List invocationContexts = new ArrayList<>(); + invocationContexts.add(new GradleVersionTestTemplateInvocationContext(version, false)); + boolean configurationCache = gradleCompatibility.configurationCache(); + if (configurationCache) { + invocationContexts.add(new GradleVersionTestTemplateInvocationContext(version, true)); + } + return invocationContexts.stream(); + }); } @Override @@ -55,20 +96,23 @@ private static final class GradleVersionTestTemplateInvocationContext implements private final String gradleVersion; - GradleVersionTestTemplateInvocationContext(String gradleVersion) { + private final boolean configurationCache; + + GradleVersionTestTemplateInvocationContext(String gradleVersion, boolean configurationCache) { this.gradleVersion = gradleVersion; + this.configurationCache = configurationCache; } @Override public String getDisplayName(int invocationIndex) { - return "Gradle " + this.gradleVersion; + return "Gradle " + this.gradleVersion + ((this.configurationCache) ? " --configuration-cache" : ""); } @Override public List getAdditionalExtensions() { - GradleBuild gradleBuild = new GradleBuild(); - if (!this.gradleVersion.equals("default")) { - gradleBuild.gradleVersion(this.gradleVersion); + GradleBuild gradleBuild = new GradleBuild().gradleVersion(this.gradleVersion); + if (this.configurationCache) { + gradleBuild.configurationCache(); } return Arrays.asList(new GradleBuildFieldSetter(gradleBuild), new GradleBuildExtension()); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/junit/GradleMultiDslExtension.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/junit/GradleMultiDslExtension.java index 34f91a54a012..22b6715aab79 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/junit/GradleMultiDslExtension.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/junit/GradleMultiDslExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import java.util.List; import java.util.stream.Stream; +import org.gradle.api.JavaVersion; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.Extension; import org.junit.jupiter.api.extension.ExtensionContext; @@ -59,7 +60,15 @@ private static final class DslTestTemplateInvocationContext implements TestTempl @Override public List getAdditionalExtensions() { - return Arrays.asList(new GradleBuildFieldSetter(new GradleBuild(this.dsl)), new GradleBuildExtension()); + GradleBuild gradleBuild = new GradleBuild(this.dsl); + JavaVersion javaVersion = JavaVersion.current(); + if (javaVersion.isCompatibleWith(JavaVersion.VERSION_17)) { + gradleBuild.gradleVersion("7.3.3"); + } + else if (javaVersion.isCompatibleWith(JavaVersion.VERSION_16)) { + gradleBuild.gradleVersion("7.0.2"); + } + return Arrays.asList(new GradleBuildFieldSetter(gradleBuild), new GradleBuildExtension()); } @Override diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/junit/GradleProjectBuilder.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/junit/GradleProjectBuilder.java new file mode 100644 index 000000000000..42dc5b7f223e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/junit/GradleProjectBuilder.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.gradle.junit; + +import java.io.File; + +import org.gradle.api.JavaVersion; +import org.gradle.api.Project; +import org.gradle.internal.nativeintegration.services.NativeServices; +import org.gradle.testfixtures.ProjectBuilder; +import org.gradle.testfixtures.internal.ProjectBuilderImpl; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Helper class to build Gradle {@link Project Projects} for test fixtures. Wraps + * functionality of Gradle's own {@link ProjectBuilder} in order to workaround an issue on + * JDK 17 and 18. + * + * @author Christoph Dreis + * @see Gradle Support JDK 17 + */ +public final class GradleProjectBuilder { + + private File projectDir; + + private String name; + + private GradleProjectBuilder() { + } + + public static GradleProjectBuilder builder() { + return new GradleProjectBuilder(); + } + + public GradleProjectBuilder withProjectDir(File dir) { + this.projectDir = dir; + return this; + } + + public GradleProjectBuilder withName(String name) { + this.name = name; + return this; + } + + public Project build() { + Assert.notNull(this.projectDir, "ProjectDir must not be null"); + ProjectBuilder builder = ProjectBuilder.builder(); + builder.withProjectDir(this.projectDir); + File userHome = new File(this.projectDir, "userHome"); + builder.withGradleUserHomeDir(userHome); + if (StringUtils.hasText(this.name)) { + builder.withName(this.name); + } + if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_17)) { + NativeServices.initialize(userHome); + try { + ProjectBuilderImpl.getGlobalServices(); + } + catch (Throwable ignore) { + } + } + return builder.build(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests.java index 430fe7356315..4492ebf04d1e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,23 +16,28 @@ package org.springframework.boot.gradle.plugin; +import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.io.StringReader; import java.util.ArrayList; import java.util.Enumeration; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.function.Consumer; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.gradle.testkit.runner.BuildResult; import org.gradle.testkit.runner.TaskOutcome; +import org.gradle.util.GradleVersion; import org.junit.jupiter.api.TestTemplate; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.boot.gradle.junit.GradleCompatibilityExtension; +import org.springframework.boot.gradle.junit.GradleCompatibility; import org.springframework.boot.gradle.testkit.GradleBuild; import static org.assertj.core.api.Assertions.assertThat; @@ -42,43 +47,43 @@ * * @author Andy Wilkinson */ -@ExtendWith(GradleCompatibilityExtension.class) -public class ApplicationPluginActionIntegrationTests { +@GradleCompatibility +class ApplicationPluginActionIntegrationTests { GradleBuild gradleBuild; @TestTemplate - public void noBootDistributionWithoutApplicationPluginApplied() { + void noBootDistributionWithoutApplicationPluginApplied() { assertThat(this.gradleBuild.build("distributionExists", "-PdistributionName=boot").getOutput()) .contains("boot exists = false"); } @TestTemplate - public void applyingApplicationPluginCreatesBootDistribution() { + void applyingApplicationPluginCreatesBootDistribution() { assertThat(this.gradleBuild.build("distributionExists", "-PdistributionName=boot", "-PapplyApplicationPlugin") .getOutput()).contains("boot exists = true"); } @TestTemplate - public void noBootStartScriptsTaskWithoutApplicationPluginApplied() { + void noBootStartScriptsTaskWithoutApplicationPluginApplied() { assertThat(this.gradleBuild.build("taskExists", "-PtaskName=bootStartScripts").getOutput()) .contains("bootStartScripts exists = false"); } @TestTemplate - public void applyingApplicationPluginCreatesBootStartScriptsTask() { + void applyingApplicationPluginCreatesBootStartScriptsTask() { assertThat(this.gradleBuild.build("taskExists", "-PtaskName=bootStartScripts", "-PapplyApplicationPlugin") .getOutput()).contains("bootStartScripts exists = true"); } @TestTemplate - public void createsBootStartScriptsTaskUsesApplicationPluginsDefaultJvmOpts() { + void createsBootStartScriptsTaskUsesApplicationPluginsDefaultJvmOpts() { assertThat(this.gradleBuild.build("startScriptsDefaultJvmOpts", "-PapplyApplicationPlugin").getOutput()) .contains("bootStartScripts defaultJvmOpts = [-Dcom.example.a=alpha, -Dcom.example.b=bravo]"); } @TestTemplate - public void zipDistributionForJarCanBeBuilt() throws IOException { + void zipDistributionForJarCanBeBuilt() throws IOException { assertThat(this.gradleBuild.build("bootDistZip").task(":bootDistZip").getOutcome()) .isEqualTo(TaskOutcome.SUCCESS); String name = this.gradleBuild.getProjectDir().getName(); @@ -90,7 +95,7 @@ public void zipDistributionForJarCanBeBuilt() throws IOException { } @TestTemplate - public void tarDistributionForJarCanBeBuilt() throws IOException { + void tarDistributionForJarCanBeBuilt() throws IOException { assertThat(this.gradleBuild.build("bootDistTar").task(":bootDistTar").getOutcome()) .isEqualTo(TaskOutcome.SUCCESS); String name = this.gradleBuild.getProjectDir().getName(); @@ -102,7 +107,7 @@ public void tarDistributionForJarCanBeBuilt() throws IOException { } @TestTemplate - public void zipDistributionForWarCanBeBuilt() throws IOException { + void zipDistributionForWarCanBeBuilt() throws IOException { assertThat(this.gradleBuild.build("bootDistZip").task(":bootDistZip").getOutcome()) .isEqualTo(TaskOutcome.SUCCESS); String name = this.gradleBuild.getProjectDir().getName(); @@ -114,7 +119,7 @@ public void zipDistributionForWarCanBeBuilt() throws IOException { } @TestTemplate - public void tarDistributionForWarCanBeBuilt() throws IOException { + void tarDistributionForWarCanBeBuilt() throws IOException { assertThat(this.gradleBuild.build("bootDistTar").task(":bootDistTar").getOutcome()) .isEqualTo(TaskOutcome.SUCCESS); String name = this.gradleBuild.getProjectDir().getName(); @@ -126,7 +131,7 @@ public void tarDistributionForWarCanBeBuilt() throws IOException { } @TestTemplate - public void applicationNameCanBeUsedToCustomizeDistributionName() throws IOException { + void applicationNameCanBeUsedToCustomizeDistributionName() throws IOException { assertThat(this.gradleBuild.build("bootDistTar").task(":bootDistTar").getOutcome()) .isEqualTo(TaskOutcome.SUCCESS); File distribution = new File(this.gradleBuild.getProjectDir(), "build/distributions/custom-boot.tar"); @@ -138,7 +143,7 @@ public void applicationNameCanBeUsedToCustomizeDistributionName() throws IOExcep } @TestTemplate - public void scriptsHaveCorrectPermissions() throws IOException { + void scriptsHaveCorrectPermissions() throws IOException { assertThat(this.gradleBuild.build("bootDistTar").task(":bootDistTar").getOutcome()) .isEqualTo(TaskOutcome.SUCCESS); String name = this.gradleBuild.getProjectDir().getName(); @@ -155,6 +160,26 @@ public void scriptsHaveCorrectPermissions() throws IOException { }); } + @TestTemplate + void taskConfigurationIsAvoided() throws IOException { + BuildResult result = this.gradleBuild.build("help"); + String output = result.getOutput(); + BufferedReader reader = new BufferedReader(new StringReader(output)); + String line; + Set configured = new HashSet<>(); + while ((line = reader.readLine()) != null) { + if (line.startsWith("Configuring :")) { + configured.add(line.substring("Configuring :".length())); + } + } + if (GradleVersion.version(this.gradleBuild.getGradleVersion()).compareTo(GradleVersion.version("7.3.3")) < 0) { + assertThat(configured).containsExactly("help"); + } + else { + assertThat(configured).containsExactlyInAnyOrder("help", "clean"); + } + } + private List zipEntryNames(File distribution) throws IOException { List entryNames = new ArrayList<>(); try (ZipFile zipFile = new ZipFile(distribution)) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/DependencyManagementPluginActionIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/DependencyManagementPluginActionIntegrationTests.java index d8533c6459fa..73975c143569 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/DependencyManagementPluginActionIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/DependencyManagementPluginActionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,17 +16,11 @@ package org.springframework.boot.gradle.plugin; -import java.io.File; -import java.io.IOException; - -import org.gradle.testkit.runner.BuildResult; import org.gradle.testkit.runner.TaskOutcome; import org.junit.jupiter.api.TestTemplate; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.boot.gradle.junit.GradleCompatibilityExtension; +import org.springframework.boot.gradle.junit.GradleCompatibility; import org.springframework.boot.gradle.testkit.GradleBuild; -import org.springframework.util.FileSystemUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -36,36 +30,21 @@ * * @author Andy Wilkinson */ -@ExtendWith(GradleCompatibilityExtension.class) -public class DependencyManagementPluginActionIntegrationTests { +@GradleCompatibility +class DependencyManagementPluginActionIntegrationTests { GradleBuild gradleBuild; @TestTemplate - public void noDependencyManagementIsAppliedByDefault() { + void noDependencyManagementIsAppliedByDefault() { assertThat(this.gradleBuild.build("doesNotHaveDependencyManagement").task(":doesNotHaveDependencyManagement") .getOutcome()).isEqualTo(TaskOutcome.SUCCESS); } @TestTemplate - public void bomIsImportedWhenDependencyManagementPluginIsApplied() { + void bomIsImportedWhenDependencyManagementPluginIsApplied() { assertThat(this.gradleBuild.build("hasDependencyManagement", "-PapplyDependencyManagementPlugin") .task(":hasDependencyManagement").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); } - @TestTemplate - public void helpfulErrorWhenVersionlessDependencyFailsToResolve() throws IOException { - File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/main/java/com/example"); - examplePackage.mkdirs(); - FileSystemUtils.copyRecursively(new File("src/test/java/com/example"), examplePackage); - BuildResult result = this.gradleBuild.buildAndFail("compileJava"); - assertThat(result.task(":compileJava").getOutcome()).isEqualTo(TaskOutcome.FAILED); - String output = result.getOutput(); - assertThat(output).contains("During the build, one or more dependencies that " - + "were declared without a version failed to resolve:"); - assertThat(output).contains("org.springframework.boot:spring-boot-starter-web:"); - assertThat(output).contains("Did you forget to apply the io.spring.dependency-management plugin to the " - + this.gradleBuild.getProjectDir().getName() + " project?"); - } - } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests.java index 71ebe701f18a..ca6ad16fd2f0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,109 +16,106 @@ package org.springframework.boot.gradle.plugin; +import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.io.StringReader; +import java.util.HashSet; +import java.util.Set; import java.util.jar.JarOutputStream; import org.gradle.testkit.runner.BuildResult; import org.gradle.testkit.runner.TaskOutcome; +import org.gradle.util.GradleVersion; import org.junit.jupiter.api.TestTemplate; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.boot.gradle.junit.GradleCompatibilityExtension; +import org.springframework.boot.gradle.junit.GradleCompatibility; import org.springframework.boot.gradle.testkit.GradleBuild; import static org.assertj.core.api.Assertions.assertThat; /** - * Integration tests for {@link WarPluginAction}. + * Integration tests for {@link JavaPluginAction}. * * @author Andy Wilkinson */ -@ExtendWith(GradleCompatibilityExtension.class) -public class JavaPluginActionIntegrationTests { +@GradleCompatibility(configurationCache = true) +class JavaPluginActionIntegrationTests { GradleBuild gradleBuild; @TestTemplate - public void noBootJarTaskWithoutJavaPluginApplied() { + void noBootJarTaskWithoutJavaPluginApplied() { assertThat(this.gradleBuild.build("taskExists", "-PtaskName=bootJar").getOutput()) .contains("bootJar exists = false"); } @TestTemplate - public void applyingJavaPluginCreatesBootJarTask() { + void applyingJavaPluginCreatesBootJarTask() { assertThat(this.gradleBuild.build("taskExists", "-PtaskName=bootJar", "-PapplyJavaPlugin").getOutput()) .contains("bootJar exists = true"); } @TestTemplate - public void noBootRunTaskWithoutJavaPluginApplied() { + void noBootRunTaskWithoutJavaPluginApplied() { assertThat(this.gradleBuild.build("taskExists", "-PtaskName=bootRun").getOutput()) .contains("bootRun exists = false"); } @TestTemplate - public void applyingJavaPluginCreatesBootRunTask() { + void applyingJavaPluginCreatesBootRunTask() { assertThat(this.gradleBuild.build("taskExists", "-PtaskName=bootRun", "-PapplyJavaPlugin").getOutput()) .contains("bootRun exists = true"); } @TestTemplate - public void javaCompileTasksUseUtf8Encoding() { + void javaCompileTasksUseUtf8Encoding() { assertThat(this.gradleBuild.build("javaCompileEncoding", "-PapplyJavaPlugin").getOutput()) .contains("compileJava = UTF-8").contains("compileTestJava = UTF-8"); } @TestTemplate - public void javaCompileTasksUseParametersCompilerFlagByDefault() { + void javaCompileTasksUseParametersCompilerFlagByDefault() { assertThat(this.gradleBuild.build("javaCompileTasksCompilerArgs").getOutput()) .contains("compileJava compiler args: [-parameters]") .contains("compileTestJava compiler args: [-parameters]"); } @TestTemplate - public void javaCompileTasksUseParametersAndAdditionalCompilerFlags() { + void javaCompileTasksUseParametersAndAdditionalCompilerFlags() { assertThat(this.gradleBuild.build("javaCompileTasksCompilerArgs").getOutput()) .contains("compileJava compiler args: [-parameters, -Xlint:all]") .contains("compileTestJava compiler args: [-parameters, -Xlint:all]"); } @TestTemplate - public void javaCompileTasksCanOverrideDefaultParametersCompilerFlag() { + void javaCompileTasksCanOverrideDefaultParametersCompilerFlag() { assertThat(this.gradleBuild.build("javaCompileTasksCompilerArgs").getOutput()) .contains("compileJava compiler args: [-Xlint:all]") .contains("compileTestJava compiler args: [-Xlint:all]"); } @TestTemplate - public void assembleRunsBootJarAndJarIsSkipped() { + void assembleRunsBootJarAndJar() { BuildResult result = this.gradleBuild.build("assemble"); assertThat(result.task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); - assertThat(result.task(":jar").getOutcome()).isEqualTo(TaskOutcome.SKIPPED); + assertThat(result.task(":jar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + File buildLibs = new File(this.gradleBuild.getProjectDir(), "build/libs"); + assertThat(buildLibs.listFiles()).containsExactlyInAnyOrder( + new File(buildLibs, this.gradleBuild.getProjectDir().getName() + ".jar"), + new File(buildLibs, this.gradleBuild.getProjectDir().getName() + "-plain.jar")); } @TestTemplate - public void errorMessageIsHelpfulWhenMainClassCannotBeResolved() { + void errorMessageIsHelpfulWhenMainClassCannotBeResolved() { BuildResult result = this.gradleBuild.buildAndFail("build", "-PapplyJavaPlugin"); assertThat(result.task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.FAILED); assertThat(result.getOutput()).contains("Main class name has not been configured and it could not be resolved"); } @TestTemplate - public void jarAndBootJarCanBothBeBuilt() { - BuildResult result = this.gradleBuild.build("assemble"); - assertThat(result.task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); - assertThat(result.task(":jar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); - File buildLibs = new File(this.gradleBuild.getProjectDir(), "build/libs"); - assertThat(buildLibs.listFiles()).containsExactlyInAnyOrder( - new File(buildLibs, this.gradleBuild.getProjectDir().getName() + ".jar"), - new File(buildLibs, this.gradleBuild.getProjectDir().getName() + "-boot.jar")); - } - - @TestTemplate - public void additionalMetadataLocationsConfiguredWhenProcessorIsPresent() throws IOException { + void additionalMetadataLocationsConfiguredWhenProcessorIsPresent() throws IOException { createMinimalMainSource(); File libs = new File(this.gradleBuild.getProjectDir(), "libs"); libs.mkdirs(); @@ -132,13 +129,62 @@ public void additionalMetadataLocationsConfiguredWhenProcessorIsPresent() throws } @TestTemplate - public void additionalMetadataLocationsNotConfiguredWhenProcessorIsAbsent() throws IOException { + void additionalMetadataLocationsNotConfiguredWhenProcessorIsAbsent() throws IOException { createMinimalMainSource(); BuildResult result = this.gradleBuild.build("compileJava"); assertThat(result.task(":compileJava").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); assertThat(result.getOutput()).contains("compileJava compiler args: [-parameters]"); } + @TestTemplate + void applyingJavaPluginCreatesDevelopmentOnlyConfiguration() { + assertThat(this.gradleBuild + .build("configurationExists", "-PconfigurationName=developmentOnly", "-PapplyJavaPlugin").getOutput()) + .contains("developmentOnly exists = true"); + } + + @TestTemplate + void productionRuntimeClasspathIsConfiguredWithAttributes() { + assertThat(this.gradleBuild + .build("configurationAttributes", "-PconfigurationName=productionRuntimeClasspath", "-PapplyJavaPlugin") + .getOutput()).contains("3 productionRuntimeClasspath attributes:") + .contains("org.gradle.usage: java-runtime").contains("org.gradle.libraryelements: jar") + .contains("org.gradle.dependency.bundling: external"); + } + + @TestTemplate + void productionRuntimeClasspathIsConfiguredWithResolvabilityAndConsumabilityThatMatchesRuntimeClasspath() { + String runtime = this.gradleBuild.build("configurationResolvabilityAndConsumability", + "-PconfigurationName=runtimeClasspath", "-PapplyJavaPlugin").getOutput(); + assertThat(runtime).contains("canBeResolved: true"); + assertThat(runtime).contains("canBeConsumed: false"); + String productionRuntime = this.gradleBuild.build("configurationResolvabilityAndConsumability", + "-PconfigurationName=productionRuntimeClasspath", "-PapplyJavaPlugin").getOutput(); + assertThat(productionRuntime).contains("canBeResolved: true"); + assertThat(productionRuntime).contains("canBeConsumed: false"); + } + + @TestTemplate + void taskConfigurationIsAvoided() throws IOException { + BuildResult result = this.gradleBuild.build("help"); + String output = result.getOutput(); + BufferedReader reader = new BufferedReader(new StringReader(output)); + String line; + Set configured = new HashSet<>(); + while ((line = reader.readLine()) != null) { + if (line.startsWith("Configuring :")) { + configured.add(line.substring("Configuring :".length())); + } + } + if (!this.gradleBuild.isConfigurationCache() && GradleVersion.version(this.gradleBuild.getGradleVersion()) + .compareTo(GradleVersion.version("7.3.3")) < 0) { + assertThat(configured).containsExactly("help"); + } + else { + assertThat(configured).containsExactlyInAnyOrder("help", "clean"); + } + } + private void createMinimalMainSource() throws IOException { File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/main/java/com/example"); examplePackage.mkdirs(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/KotlinPluginActionIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/KotlinPluginActionIntegrationTests.java index 0c34193164d5..e7d6205dd407 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/KotlinPluginActionIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/KotlinPluginActionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,17 @@ package org.springframework.boot.gradle.plugin; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.util.HashSet; +import java.util.Set; + +import org.gradle.testkit.runner.BuildResult; +import org.gradle.util.GradleVersion; import org.junit.jupiter.api.TestTemplate; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.boot.gradle.junit.GradleCompatibilityExtension; +import org.springframework.boot.gradle.junit.GradleCompatibility; import org.springframework.boot.gradle.testkit.GradleBuild; import static org.assertj.core.api.Assertions.assertThat; @@ -29,33 +36,53 @@ * * @author Andy Wilkinson */ -@ExtendWith(GradleCompatibilityExtension.class) -public class KotlinPluginActionIntegrationTests { +@GradleCompatibility +class KotlinPluginActionIntegrationTests { GradleBuild gradleBuild; @TestTemplate - public void noKotlinVersionPropertyWithoutKotlinPlugin() { + void noKotlinVersionPropertyWithoutKotlinPlugin() { assertThat(this.gradleBuild.build("kotlinVersion").getOutput()).contains("Kotlin version: none"); } @TestTemplate - public void kotlinVersionPropertyIsSet() { + void kotlinVersionPropertyIsSet() { String output = this.gradleBuild.build("kotlinVersion", "dependencies", "--configuration", "compileClasspath") .getOutput(); assertThat(output).containsPattern("Kotlin version: [0-9]\\.[0-9]\\.[0-9]+"); } @TestTemplate - public void kotlinCompileTasksUseJavaParametersFlagByDefault() { + void kotlinCompileTasksUseJavaParametersFlagByDefault() { assertThat(this.gradleBuild.build("kotlinCompileTasksJavaParameters").getOutput()) .contains("compileKotlin java parameters: true").contains("compileTestKotlin java parameters: true"); } @TestTemplate - public void kotlinCompileTasksCanOverrideDefaultJavaParametersFlag() { + void kotlinCompileTasksCanOverrideDefaultJavaParametersFlag() { assertThat(this.gradleBuild.build("kotlinCompileTasksJavaParameters").getOutput()) .contains("compileKotlin java parameters: false").contains("compileTestKotlin java parameters: false"); } + @TestTemplate + void taskConfigurationIsAvoided() throws IOException { + BuildResult result = this.gradleBuild.build("help"); + String output = result.getOutput(); + BufferedReader reader = new BufferedReader(new StringReader(output)); + String line; + Set configured = new HashSet<>(); + while ((line = reader.readLine()) != null) { + if (line.startsWith("Configuring :")) { + configured.add(line.substring("Configuring :".length())); + } + } + if (GradleVersion.version(this.gradleBuild.getGradleVersion()).compareTo(GradleVersion.version("7.3.3")) < 0) { + assertThat(configured).containsExactly("help"); + } + else { + assertThat(configured).containsExactlyInAnyOrder("help", "clean", "compileKotlin", "compileTestKotlin"); + } + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/MainClassConventionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/MainClassConventionTests.java deleted file mode 100644 index 34e6c115af68..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/MainClassConventionTests.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.gradle.plugin; - -import java.io.File; -import java.io.IOException; - -import org.gradle.api.Project; -import org.gradle.api.plugins.ExtraPropertiesExtension; -import org.gradle.testfixtures.ProjectBuilder; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import org.springframework.boot.gradle.dsl.SpringBootExtension; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Integration tests for {@link MainClassConvention}. - * - * @author Andy Wilkinson - */ -class MainClassConventionTests { - - @TempDir - File temp; - - private Project project; - - private MainClassConvention convention; - - @BeforeEach - void createConvention() throws IOException { - this.project = ProjectBuilder.builder().withProjectDir(this.temp).build(); - this.convention = new MainClassConvention(this.project, () -> null); - } - - @Test - void mainClassNameProjectPropertyIsUsed() throws Exception { - this.project.getExtensions().getByType(ExtraPropertiesExtension.class).set("mainClassName", - "com.example.MainClass"); - assertThat(this.convention.call()).isEqualTo("com.example.MainClass"); - } - - @Test - void springBootExtensionMainClassNameIsUsed() throws Exception { - SpringBootExtension extension = this.project.getExtensions().create("springBoot", SpringBootExtension.class, - this.project); - extension.setMainClassName("com.example.MainClass"); - assertThat(this.convention.call()).isEqualTo("com.example.MainClass"); - } - - @Test - void springBootExtensionMainClassNameIsUsedInPreferenceToMainClassNameProjectProperty() throws Exception { - this.project.getExtensions().getByType(ExtraPropertiesExtension.class).set("mainClassName", - "com.example.ProjectPropertyMainClass"); - SpringBootExtension extension = this.project.getExtensions().create("springBoot", SpringBootExtension.class, - this.project); - extension.setMainClassName("com.example.SpringBootExtensionMainClass"); - assertThat(this.convention.call()).isEqualTo("com.example.SpringBootExtensionMainClass"); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/MavenPluginActionIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/MavenPluginActionIntegrationTests.java index 97928fd2108c..c4672e80719a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/MavenPluginActionIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/MavenPluginActionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,9 +17,10 @@ package org.springframework.boot.gradle.plugin; import org.junit.jupiter.api.TestTemplate; -import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.condition.DisabledForJreRange; +import org.junit.jupiter.api.condition.JRE; -import org.springframework.boot.gradle.junit.GradleCompatibilityExtension; +import org.springframework.boot.gradle.junit.GradleCompatibility; import org.springframework.boot.gradle.testkit.GradleBuild; import static org.assertj.core.api.Assertions.assertThat; @@ -29,14 +30,16 @@ * * @author Andy Wilkinson */ -@ExtendWith(GradleCompatibilityExtension.class) -public class MavenPluginActionIntegrationTests { +@DisabledForJreRange(min = JRE.JAVA_16) +@GradleCompatibility(versionsLessThan = "7.0-milestone-1") +class MavenPluginActionIntegrationTests { GradleBuild gradleBuild; @TestTemplate - public void clearsConf2ScopeMappingsOfUploadBootArchivesTask() { - assertThat(this.gradleBuild.build("conf2ScopeMappings").getOutput()).contains("Conf2ScopeMappings = 0"); + void clearsConf2ScopeMappingsOfUploadBootArchivesTask() { + assertThat(this.gradleBuild.expectDeprecationWarningsWithAtLeastVersion("6.0.0").build("conf2ScopeMappings") + .getOutput()).contains("Conf2ScopeMappings = 0"); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/OnlyDependencyManagementIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/OnlyDependencyManagementIntegrationTests.java index a089067f4e31..ad8145a1f8d0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/OnlyDependencyManagementIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/OnlyDependencyManagementIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,9 +17,8 @@ package org.springframework.boot.gradle.plugin; import org.junit.jupiter.api.TestTemplate; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.boot.gradle.junit.GradleCompatibilityExtension; +import org.springframework.boot.gradle.junit.GradleCompatibility; import org.springframework.boot.gradle.testkit.GradleBuild; import static org.assertj.core.api.Assertions.assertThat; @@ -30,13 +29,13 @@ * * @author Andy Wilkinson */ -@ExtendWith(GradleCompatibilityExtension.class) -public class OnlyDependencyManagementIntegrationTests { +@GradleCompatibility +class OnlyDependencyManagementIntegrationTests { GradleBuild gradleBuild; @TestTemplate - public void dependencyManagementCanBeConfiguredUsingCoordinatesConstant() { + void dependencyManagementCanBeConfiguredUsingCoordinatesConstant() { assertThat(this.gradleBuild.build("dependencyManagement").getOutput()) .contains("org.springframework.boot:spring-boot-starter "); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/SpringBootPluginIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/SpringBootPluginIntegrationTests.java index f531c7747392..d51b3f85f080 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/SpringBootPluginIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/SpringBootPluginIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,10 @@ package org.springframework.boot.gradle.plugin; -import java.io.File; -import java.io.IOException; - import org.gradle.testkit.runner.BuildResult; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledForJreRange; +import org.junit.jupiter.api.condition.JRE; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.gradle.testkit.GradleBuild; @@ -38,37 +37,18 @@ class SpringBootPluginIntegrationTests { final GradleBuild gradleBuild = new GradleBuild(); + @DisabledForJreRange(min = JRE.JAVA_14) @Test - void failFastWithVersionOfGradleLowerThanRequired() { - BuildResult result = this.gradleBuild.gradleVersion("4.9").buildAndFail(); - assertThat(result.getOutput()) - .contains("Spring Boot plugin requires Gradle 4.10 or later. The current version is Gradle 4.9"); + void failFastWithVersionOfGradle6LowerThanRequired() { + BuildResult result = this.gradleBuild.gradleVersion("6.7.1").buildAndFail(); + assertThat(result.getOutput()).contains( + "Spring Boot plugin requires Gradle 6.8.x, 6.9.x, or 7.x. The current version is Gradle 6.7.1"); } + @DisabledForJreRange(min = JRE.JAVA_16) @Test - void succeedWithVersionOfGradleHigherThanRequired() { - this.gradleBuild.gradleVersion("4.10.1").build(); - } - - @Test - void succeedWithVersionOfGradleMatchingWhatIsRequired() { - this.gradleBuild.gradleVersion("4.10").build(); - } - - @Test - void unresolvedDependenciesAreAnalyzedWhenDependencyResolutionFails() throws IOException { - createMinimalMainSource(); - BuildResult result = this.gradleBuild.buildAndFail("compileJava"); - assertThat(result.getOutput()) - .contains("During the build, one or more dependencies that were declared without a" - + " version failed to resolve:") - .contains(" org.springframework.boot:spring-boot-starter:"); - } - - private void createMinimalMainSource() throws IOException { - File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/main/java/com/example"); - examplePackage.mkdirs(); - new File(examplePackage, "Application.java").createNewFile(); + void succeedWithVersionOfGradle6MatchingWithIsRequired() { + this.gradleBuild.gradleVersion("6.8").build(); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/SpringBootPluginTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/SpringBootPluginTests.java new file mode 100644 index 000000000000..072baa977d2e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/SpringBootPluginTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.gradle.plugin; + +import java.io.File; + +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.gradle.junit.GradleProjectBuilder; +import org.springframework.boot.testsupport.classpath.ClassPathExclusions; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SpringBootPlugin}. + * + * @author Martin Chalupa + * @author Andy Wilkinson + */ +@ClassPathExclusions("kotlin-daemon-client-*.jar") +class SpringBootPluginTests { + + @TempDir + File temp; + + @Test + void bootArchivesConfigurationsCannotBeResolved() { + Project project = GradleProjectBuilder.builder().withProjectDir(this.temp).build(); + project.getPlugins().apply(SpringBootPlugin.class); + Configuration bootArchives = project.getConfigurations() + .getByName(SpringBootPlugin.BOOT_ARCHIVES_CONFIGURATION_NAME); + assertThat(bootArchives.isCanBeResolved()).isFalse(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/WarPluginActionIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/WarPluginActionIntegrationTests.java index 6da554754d66..162b6a57128f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/WarPluginActionIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/WarPluginActionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,19 @@ package org.springframework.boot.gradle.plugin; +import java.io.BufferedReader; import java.io.File; +import java.io.IOException; +import java.io.StringReader; +import java.util.HashSet; +import java.util.Set; import org.gradle.testkit.runner.BuildResult; import org.gradle.testkit.runner.TaskOutcome; +import org.gradle.util.GradleVersion; import org.junit.jupiter.api.TestTemplate; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.boot.gradle.junit.GradleCompatibilityExtension; +import org.springframework.boot.gradle.junit.GradleCompatibility; import org.springframework.boot.gradle.testkit.GradleBuild; import static org.assertj.core.api.Assertions.assertThat; @@ -33,46 +38,59 @@ * * @author Andy Wilkinson */ -@ExtendWith(GradleCompatibilityExtension.class) -public class WarPluginActionIntegrationTests { +@GradleCompatibility +class WarPluginActionIntegrationTests { GradleBuild gradleBuild; @TestTemplate - public void noBootWarTaskWithoutWarPluginApplied() { + void noBootWarTaskWithoutWarPluginApplied() { assertThat(this.gradleBuild.build("taskExists", "-PtaskName=bootWar").getOutput()) .contains("bootWar exists = false"); } @TestTemplate - public void applyingWarPluginCreatesBootWarTask() { + void applyingWarPluginCreatesBootWarTask() { assertThat(this.gradleBuild.build("taskExists", "-PtaskName=bootWar", "-PapplyWarPlugin").getOutput()) .contains("bootWar exists = true"); } @TestTemplate - public void assembleRunsBootWarAndWarIsSkipped() { - BuildResult result = this.gradleBuild.build("assemble"); - assertThat(result.task(":bootWar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); - assertThat(result.task(":war").getOutcome()).isEqualTo(TaskOutcome.SKIPPED); - } - - @TestTemplate - public void warAndBootWarCanBothBeBuilt() { + void assembleRunsBootWarAndWar() { BuildResult result = this.gradleBuild.build("assemble"); assertThat(result.task(":bootWar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); assertThat(result.task(":war").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); File buildLibs = new File(this.gradleBuild.getProjectDir(), "build/libs"); assertThat(buildLibs.listFiles()).containsExactlyInAnyOrder( new File(buildLibs, this.gradleBuild.getProjectDir().getName() + ".war"), - new File(buildLibs, this.gradleBuild.getProjectDir().getName() + "-boot.war")); + new File(buildLibs, this.gradleBuild.getProjectDir().getName() + "-plain.war")); } @TestTemplate - public void errorMessageIsHelpfulWhenMainClassCannotBeResolved() { + void errorMessageIsHelpfulWhenMainClassCannotBeResolved() { BuildResult result = this.gradleBuild.buildAndFail("build", "-PapplyWarPlugin"); assertThat(result.task(":bootWar").getOutcome()).isEqualTo(TaskOutcome.FAILED); assertThat(result.getOutput()).contains("Main class name has not been configured and it could not be resolved"); } + @TestTemplate + void taskConfigurationIsAvoided() throws IOException { + BuildResult result = this.gradleBuild.build("help"); + String output = result.getOutput(); + BufferedReader reader = new BufferedReader(new StringReader(output)); + String line; + Set configured = new HashSet<>(); + while ((line = reader.readLine()) != null) { + if (line.startsWith("Configuring :")) { + configured.add(line.substring("Configuring :".length())); + } + } + if (GradleVersion.version(this.gradleBuild.getGradleVersion()).compareTo(GradleVersion.version("7.3.3")) < 0) { + assertThat(configured).containsExactly("help"); + } + else { + assertThat(configured).containsExactlyInAnyOrder("help", "clean"); + } + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests.java index 2e82719b9233..e18d9ecee625 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,16 +19,17 @@ import java.io.File; import java.io.FileReader; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.time.Instant; import java.util.Properties; -import org.gradle.testkit.runner.BuildResult; -import org.gradle.testkit.runner.InvalidRunnerConfigurationException; import org.gradle.testkit.runner.TaskOutcome; -import org.gradle.testkit.runner.UnexpectedBuildFailure; import org.junit.jupiter.api.TestTemplate; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.boot.gradle.junit.GradleCompatibilityExtension; +import org.springframework.boot.gradle.junit.GradleCompatibility; import org.springframework.boot.gradle.testkit.GradleBuild; import org.springframework.boot.loader.tools.FileUtils; @@ -39,13 +40,13 @@ * * @author Andy Wilkinson */ -@ExtendWith(GradleCompatibilityExtension.class) -public class BuildInfoIntegrationTests { +@GradleCompatibility(configurationCache = true) +class BuildInfoIntegrationTests { GradleBuild gradleBuild; @TestTemplate - public void defaultValues() { + void defaultValues() { assertThat(this.gradleBuild.build("buildInfo").task(":buildInfo").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); Properties buildInfoProperties = buildInfoProperties(); assertThat(buildInfoProperties).containsKey("build.time"); @@ -56,7 +57,7 @@ public void defaultValues() { } @TestTemplate - public void basicExecution() { + void basicExecution() { assertThat(this.gradleBuild.build("buildInfo").task(":buildInfo").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); Properties buildInfoProperties = buildInfoProperties(); assertThat(buildInfoProperties).containsKey("build.time"); @@ -64,17 +65,24 @@ public void basicExecution() { assertThat(buildInfoProperties).containsEntry("build.group", "foo"); assertThat(buildInfoProperties).containsEntry("build.additional", "foo"); assertThat(buildInfoProperties).containsEntry("build.name", "foo"); - assertThat(buildInfoProperties).containsEntry("build.version", "1.0"); + assertThat(buildInfoProperties).containsEntry("build.version", "0.1.0"); } @TestTemplate - public void notUpToDateWhenExecutedTwiceAsTimeChanges() { + void notUpToDateWhenExecutedTwiceAsTimeChanges() { assertThat(this.gradleBuild.build("buildInfo").task(":buildInfo").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + Properties first = buildInfoProperties(); + String firstBuildTime = first.getProperty("build.time"); + assertThat(firstBuildTime).isNotNull(); assertThat(this.gradleBuild.build("buildInfo").task(":buildInfo").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + Properties second = buildInfoProperties(); + String secondBuildTime = second.getProperty("build.time"); + assertThat(secondBuildTime).isNotNull(); + assertThat(Instant.parse(firstBuildTime)).isBefore(Instant.parse(secondBuildTime)); } @TestTemplate - public void upToDateWhenExecutedTwiceWithFixedTime() { + void upToDateWhenExecutedTwiceWithFixedTime() { assertThat(this.gradleBuild.build("buildInfo", "-PnullTime").task(":buildInfo").getOutcome()) .isEqualTo(TaskOutcome.SUCCESS); assertThat(this.gradleBuild.build("buildInfo", "-PnullTime").task(":buildInfo").getOutcome()) @@ -82,16 +90,26 @@ public void upToDateWhenExecutedTwiceWithFixedTime() { } @TestTemplate - public void notUpToDateWhenExecutedTwiceWithFixedTimeAndChangedProjectVersion() { - assertThat(this.gradleBuild.build("buildInfo", "-PnullTime").task(":buildInfo").getOutcome()) - .isEqualTo(TaskOutcome.SUCCESS); - BuildResult result = this.gradleBuild.build("buildInfo", "-PnullTime", "-PprojectVersion=0.2.0"); - assertThat(result.task(":buildInfo").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + void notUpToDateWhenExecutedTwiceWithFixedTimeAndChangedProjectVersion() { + assertThat(this.gradleBuild.scriptProperty("projectVersion", "0.1.0").build("buildInfo").task(":buildInfo") + .getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(this.gradleBuild.scriptProperty("projectVersion", "0.2.0").build("buildInfo").task(":buildInfo") + .getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + } + + @TestTemplate + void notUpToDateWhenExecutedTwiceWithFixedTimeAndChangedGradlePropertiesProjectVersion() throws IOException { + Path gradleProperties = new File(this.gradleBuild.getProjectDir(), "gradle.properties").toPath(); + Files.write(gradleProperties, "version=0.1.0".getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, + StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); + assertThat(this.gradleBuild.build("buildInfo").task(":buildInfo").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + Files.write(gradleProperties, "version=0.2.0".getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, + StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); + assertThat(this.gradleBuild.build("buildInfo").task(":buildInfo").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); } @TestTemplate - public void reproducibleOutputWithFixedTime() - throws InvalidRunnerConfigurationException, UnexpectedBuildFailure, IOException, InterruptedException { + void reproducibleOutputWithFixedTime() throws IOException, InterruptedException { assertThat(this.gradleBuild.build("buildInfo", "-PnullTime").task(":buildInfo").getOutcome()) .isEqualTo(TaskOutcome.SUCCESS); File buildInfoProperties = new File(this.gradleBuild.getProjectDir(), "build/build-info.properties"); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoTests.java index 861343c6f6cf..61d2629dfd22 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,10 +24,14 @@ import java.util.Properties; import org.gradle.api.Project; -import org.gradle.testfixtures.ProjectBuilder; +import org.gradle.api.internal.project.ProjectInternal; +import org.gradle.initialization.GradlePropertiesController; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.springframework.boot.gradle.junit.GradleProjectBuilder; +import org.springframework.boot.testsupport.classpath.ClassPathExclusions; + import static org.assertj.core.api.Assertions.assertThat; /** @@ -35,6 +39,7 @@ * * @author Andy Wilkinson */ +@ClassPathExclusions("kotlin-daemon-client-*") class BuildInfoTests { @TempDir @@ -111,7 +116,8 @@ void timeCanBeCustomizedInProperties() { Instant now = Instant.now(); BuildInfo task = createTask(createProject("test")); task.getProperties().setTime(now); - assertThat(buildInfoProperties(task)).containsEntry("build.time", DateTimeFormatter.ISO_INSTANT.format(now)); + assertThat(buildInfoProperties(task)).containsEntry("build.time", + DateTimeFormatter.ISO_INSTANT.format(Instant.ofEpochMilli(now.toEpochMilli()))); } @Test @@ -125,7 +131,10 @@ void additionalPropertiesAreReflectedInProperties() { private Project createProject(String projectName) { File projectDir = new File(this.temp, projectName); - return ProjectBuilder.builder().withProjectDir(projectDir).withName(projectName).build(); + Project project = GradleProjectBuilder.builder().withProjectDir(projectDir).withName(projectName).build(); + ((ProjectInternal) project).getServices().get(GradlePropertiesController.class) + .loadGradlePropertiesFrom(projectDir); + return project; } private BuildInfo createTask(Project project) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java index 6b99c6662239..d1c82787fb08 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,47 +16,88 @@ package org.springframework.boot.gradle.tasks.bundling; +import java.io.BufferedReader; import java.io.File; +import java.io.FileOutputStream; +import java.io.FileWriter; import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.io.StringReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.function.Consumer; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; -import org.gradle.testkit.runner.InvalidRunnerConfigurationException; +import org.gradle.testkit.runner.BuildResult; import org.gradle.testkit.runner.TaskOutcome; -import org.gradle.testkit.runner.UnexpectedBuildFailure; import org.junit.jupiter.api.TestTemplate; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.boot.gradle.junit.GradleCompatibilityExtension; import org.springframework.boot.gradle.testkit.GradleBuild; import org.springframework.boot.loader.tools.FileUtils; +import org.springframework.boot.loader.tools.JarModeLibrary; +import org.springframework.util.FileSystemUtils; +import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; /** - * Integration tests for {@link BootJar}. + * Integration tests for {@link BootJar} and {@link BootWar}. * * @author Andy Wilkinson + * @author Madhura Bhave */ -@ExtendWith(GradleCompatibilityExtension.class) -public abstract class AbstractBootArchiveIntegrationTests { - - GradleBuild gradleBuild; +abstract class AbstractBootArchiveIntegrationTests { private final String taskName; - protected AbstractBootArchiveIntegrationTests(String taskName) { + private final String libPath; + + private final String classesPath; + + private final String indexPath; + + GradleBuild gradleBuild; + + protected AbstractBootArchiveIntegrationTests(String taskName, String libPath, String classesPath, + String indexPath) { this.taskName = taskName; + this.libPath = libPath; + this.classesPath = classesPath; + this.indexPath = indexPath; } @TestTemplate - public void basicBuild() throws InvalidRunnerConfigurationException, UnexpectedBuildFailure, IOException { + void basicBuild() { assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome()) .isEqualTo(TaskOutcome.SUCCESS); } + @Deprecated @TestTemplate - public void reproducibleArchive() - throws InvalidRunnerConfigurationException, UnexpectedBuildFailure, IOException, InterruptedException { + void basicBuildUsingDeprecatedMainClassName() { + assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome()) + .isEqualTo(TaskOutcome.SUCCESS); + } + + @TestTemplate + void reproducibleArchive() throws IOException, InterruptedException { assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome()) .isEqualTo(TaskOutcome.SUCCESS); File jar = new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0]; @@ -69,8 +110,7 @@ public void reproducibleArchive() } @TestTemplate - public void upToDateWhenBuiltTwice() - throws InvalidRunnerConfigurationException, UnexpectedBuildFailure, IOException { + void upToDateWhenBuiltTwice() { assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome()) .isEqualTo(TaskOutcome.SUCCESS); assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome()) @@ -78,8 +118,7 @@ public void upToDateWhenBuiltTwice() } @TestTemplate - public void upToDateWhenBuiltTwiceWithLaunchScriptIncluded() - throws InvalidRunnerConfigurationException, UnexpectedBuildFailure, IOException { + void upToDateWhenBuiltTwiceWithLaunchScriptIncluded() { assertThat(this.gradleBuild.build("-PincludeLaunchScript=true", this.taskName).task(":" + this.taskName) .getOutcome()).isEqualTo(TaskOutcome.SUCCESS); assertThat(this.gradleBuild.build("-PincludeLaunchScript=true", this.taskName).task(":" + this.taskName) @@ -87,31 +126,31 @@ public void upToDateWhenBuiltTwiceWithLaunchScriptIncluded() } @TestTemplate - public void notUpToDateWhenLaunchScriptWasNotIncludedAndThenIsIncluded() { - assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome()) - .isEqualTo(TaskOutcome.SUCCESS); - assertThat(this.gradleBuild.build("-PincludeLaunchScript=true", this.taskName).task(":" + this.taskName) + void notUpToDateWhenLaunchScriptWasNotIncludedAndThenIsIncluded() { + assertThat(this.gradleBuild.scriptProperty("launchScript", "").build(this.taskName).task(":" + this.taskName) .getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(this.gradleBuild.scriptProperty("launchScript", "launchScript()").build(this.taskName) + .task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS); } @TestTemplate - public void notUpToDateWhenLaunchScriptWasIncludedAndThenIsNotIncluded() { - assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome()) - .isEqualTo(TaskOutcome.SUCCESS); - assertThat(this.gradleBuild.build("-PincludeLaunchScript=true", this.taskName).task(":" + this.taskName) + void notUpToDateWhenLaunchScriptWasIncludedAndThenIsNotIncluded() { + assertThat(this.gradleBuild.scriptProperty("launchScript", "launchScript()").build(this.taskName) + .task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(this.gradleBuild.scriptProperty("launchScript", "").build(this.taskName).task(":" + this.taskName) .getOutcome()).isEqualTo(TaskOutcome.SUCCESS); } @TestTemplate - public void notUpToDateWhenLaunchScriptPropertyChanges() { - assertThat(this.gradleBuild.build("-PincludeLaunchScript=true", "-PlaunchScriptProperty=foo", this.taskName) + void notUpToDateWhenLaunchScriptPropertyChanges() { + assertThat(this.gradleBuild.scriptProperty("launchScriptProperty", "alpha").build(this.taskName) .task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS); - assertThat(this.gradleBuild.build("-PincludeLaunchScript=true", "-PlaunchScriptProperty=bar", this.taskName) + assertThat(this.gradleBuild.scriptProperty("launchScriptProperty", "bravo").build(this.taskName) .task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS); } @TestTemplate - public void applicationPluginMainClassNameIsUsed() throws IOException { + void applicationPluginMainClassNameIsUsed() throws IOException { assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome()) .isEqualTo(TaskOutcome.SUCCESS); try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { @@ -121,7 +160,7 @@ public void applicationPluginMainClassNameIsUsed() throws IOException { } @TestTemplate - public void springBootExtensionMainClassNameIsUsed() throws IOException { + void springBootExtensionMainClassNameIsUsed() throws IOException { assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome()) .isEqualTo(TaskOutcome.SUCCESS); try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { @@ -131,9 +170,450 @@ public void springBootExtensionMainClassNameIsUsed() throws IOException { } @TestTemplate - public void duplicatesAreHandledGracefully() throws IOException { + void duplicatesAreHandledGracefully() { + assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome()) + .isEqualTo(TaskOutcome.SUCCESS); + } + + @TestTemplate + void developmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault() throws IOException { + File srcMainResources = new File(this.gradleBuild.getProjectDir(), "src/main/resources"); + srcMainResources.mkdirs(); + new File(srcMainResources, "resource").createNewFile(); + assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome()) + .isEqualTo(TaskOutcome.SUCCESS); + try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { + Stream libEntryNames = jarFile.stream().filter((entry) -> !entry.isDirectory()) + .map(JarEntry::getName).filter((name) -> name.startsWith(this.libPath)); + assertThat(libEntryNames).containsExactly(this.libPath + "commons-io-2.6.jar"); + Stream classesEntryNames = jarFile.stream().filter((entry) -> !entry.isDirectory()) + .map(JarEntry::getName).filter((name) -> name.startsWith(this.classesPath)); + assertThat(classesEntryNames).containsExactly(this.classesPath + "resource"); + } + } + + @TestTemplate + void developmentOnlyDependenciesCanBeIncludedInTheArchive() throws IOException { + assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome()) + .isEqualTo(TaskOutcome.SUCCESS); + try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { + Stream libEntryNames = jarFile.stream().filter((entry) -> !entry.isDirectory()) + .map(JarEntry::getName).filter((name) -> name.startsWith(this.libPath)); + assertThat(libEntryNames).containsExactly(this.libPath + "commons-io-2.6.jar", + this.libPath + "commons-lang3-3.9.jar"); + } + } + + @TestTemplate + void jarTypeFilteringIsApplied() throws IOException { + File flatDirRepository = new File(this.gradleBuild.getProjectDir(), "repository"); + createDependenciesStarterJar(new File(flatDirRepository, "starter.jar")); + createStandardJar(new File(flatDirRepository, "standard.jar")); + assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome()) + .isEqualTo(TaskOutcome.SUCCESS); + try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { + Stream libEntryNames = jarFile.stream().filter((entry) -> !entry.isDirectory()) + .map(JarEntry::getName).filter((name) -> name.startsWith(this.libPath)); + assertThat(libEntryNames).containsExactly(this.libPath + "standard.jar"); + } + } + + @TestTemplate + void startClassIsSetByResolvingTheMainClass() throws IOException { + copyMainClassApplication(); + assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome()) + .isEqualTo(TaskOutcome.SUCCESS); + try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { + Attributes mainAttributes = jarFile.getManifest().getMainAttributes(); + assertThat(mainAttributes.getValue("Start-Class")) + .isEqualTo("com.example." + this.taskName.toLowerCase(Locale.ENGLISH) + ".main.CustomMainClass"); + } + assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome()) + .isEqualTo(TaskOutcome.UP_TO_DATE); + } + + @TestTemplate + void upToDateWhenBuiltWithDefaultLayeredAndThenWithExplicitLayered() { + assertThat(this.gradleBuild.scriptProperty("layered", "").build("" + this.taskName).task(":" + this.taskName) + .getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(this.gradleBuild.scriptProperty("layered", "layered {}").build("" + this.taskName) + .task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.UP_TO_DATE); + } + + @TestTemplate + void notUpToDateWhenBuiltWithoutLayersAndThenWithLayers() { + assertThat(this.gradleBuild.scriptProperty("layerEnablement", "enabled = false").build(this.taskName) + .task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(this.gradleBuild.scriptProperty("layerEnablement", "enabled = true").build(this.taskName) + .task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + } + + @TestTemplate + void notUpToDateWhenBuiltWithLayerToolsAndThenWithoutLayerTools() { + assertThat(this.gradleBuild.scriptProperty("layerTools", "").build(this.taskName).task(":" + this.taskName) + .getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(this.gradleBuild.scriptProperty("layerTools", "includeLayerTools = false").build(this.taskName) + .task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + } + + @TestTemplate + void layersWithCustomSourceSet() { + assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome()) + .isEqualTo(TaskOutcome.SUCCESS); + } + + @TestTemplate + void implicitLayers() throws IOException { + writeMainClass(); + writeResource(); + assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome()) + .isEqualTo(TaskOutcome.SUCCESS); + Map> indexedLayers; + String layerToolsJar = this.libPath + JarModeLibrary.LAYER_TOOLS.getName(); + try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { + assertThat(jarFile.getEntry(layerToolsJar)).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(); + assertThat(jarFile.getEntry(this.libPath + "library-1.0-SNAPSHOT.jar")).isNotNull(); + assertThat(jarFile.getEntry(this.classesPath + "example/Main.class")).isNotNull(); + assertThat(jarFile.getEntry(this.classesPath + "static/file.txt")).isNotNull(); + indexedLayers = readLayerIndex(jarFile); + } + List layerNames = Arrays.asList("dependencies", "spring-boot-loader", "snapshot-dependencies", + "application"); + assertThat(indexedLayers.keySet()).containsExactlyElementsOf(layerNames); + Set expectedDependencies = new TreeSet<>(); + expectedDependencies.add(this.libPath + "commons-lang3-3.9.jar"); + expectedDependencies.add(this.libPath + "spring-core-5.2.5.RELEASE.jar"); + expectedDependencies.add(this.libPath + "spring-jcl-5.2.5.RELEASE.jar"); + expectedDependencies.add(this.libPath + "jul-to-slf4j-1.7.28.jar"); + expectedDependencies.add(this.libPath + "log4j-api-2.12.1.jar"); + expectedDependencies.add(this.libPath + "log4j-to-slf4j-2.12.1.jar"); + expectedDependencies.add(this.libPath + "logback-classic-1.2.3.jar"); + expectedDependencies.add(this.libPath + "logback-core-1.2.3.jar"); + expectedDependencies.add(this.libPath + "slf4j-api-1.7.28.jar"); + 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); + assertThat(indexedLayers.get("dependencies")).containsExactlyElementsOf(expectedDependencies); + assertThat(indexedLayers.get("spring-boot-loader")).containsExactly("org/"); + assertThat(indexedLayers.get("snapshot-dependencies")).containsExactlyElementsOf(expectedSnapshotDependencies); + assertThat(indexedLayers.get("application")) + .containsExactly(getExpectedApplicationLayerContents(this.classesPath)); + BuildResult listLayers = this.gradleBuild.build("listLayers"); + assertThat(listLayers.task(":listLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + String listLayersOutput = listLayers.getOutput(); + assertThat(new BufferedReader(new StringReader(listLayersOutput)).lines()).containsSequence(layerNames); + BuildResult extractLayers = this.gradleBuild.build("extractLayers"); + assertThat(extractLayers.task(":extractLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertExtractedLayers(layerNames, indexedLayers); + } + + abstract String[] getExpectedApplicationLayerContents(String... additionalFiles); + + @TestTemplate + void multiModuleImplicitLayers() throws IOException { + writeSettingsGradle(); + writeMainClass(); + writeResource(); assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome()) .isEqualTo(TaskOutcome.SUCCESS); + Map> indexedLayers; + String layerToolsJar = this.libPath + JarModeLibrary.LAYER_TOOLS.getName(); + try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { + assertThat(jarFile.getEntry(layerToolsJar)).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(); + 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(); + assertThat(jarFile.getEntry(this.libPath + "library-1.0-SNAPSHOT.jar")).isNotNull(); + assertThat(jarFile.getEntry(this.classesPath + "example/Main.class")).isNotNull(); + assertThat(jarFile.getEntry(this.classesPath + "static/file.txt")).isNotNull(); + indexedLayers = readLayerIndex(jarFile); + } + List layerNames = Arrays.asList("dependencies", "spring-boot-loader", "snapshot-dependencies", + "application"); + assertThat(indexedLayers.keySet()).containsExactlyElementsOf(layerNames); + Set expectedDependencies = new TreeSet<>(); + expectedDependencies.add(this.libPath + "commons-lang3-3.9.jar"); + expectedDependencies.add(this.libPath + "spring-core-5.2.5.RELEASE.jar"); + 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); + assertThat(indexedLayers.get("dependencies")).containsExactlyElementsOf(expectedDependencies); + assertThat(indexedLayers.get("spring-boot-loader")).containsExactly("org/"); + assertThat(indexedLayers.get("snapshot-dependencies")).containsExactlyElementsOf(expectedSnapshotDependencies); + assertThat(indexedLayers.get("application")) + .containsExactly(getExpectedApplicationLayerContents(this.classesPath, this.libPath + "alpha-1.2.3.jar", + this.libPath + "bravo-1.2.3.jar", this.libPath + "charlie-1.2.3.jar")); + BuildResult listLayers = this.gradleBuild.build("listLayers"); + assertThat(listLayers.task(":listLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + String listLayersOutput = listLayers.getOutput(); + assertThat(new BufferedReader(new StringReader(listLayersOutput)).lines()).containsSequence(layerNames); + BuildResult extractLayers = this.gradleBuild.build("extractLayers"); + assertThat(extractLayers.task(":extractLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertExtractedLayers(layerNames, indexedLayers); + } + + @TestTemplate + void customLayers() throws IOException { + writeMainClass(); + writeResource(); + BuildResult build = this.gradleBuild.build(this.taskName); + assertThat(build.task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + Map> indexedLayers; + String layerToolsJar = this.libPath + JarModeLibrary.LAYER_TOOLS.getName(); + try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { + assertThat(jarFile.getEntry(layerToolsJar)).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(); + assertThat(jarFile.getEntry(this.libPath + "library-1.0-SNAPSHOT.jar")).isNotNull(); + assertThat(jarFile.getEntry(this.classesPath + "example/Main.class")).isNotNull(); + assertThat(jarFile.getEntry(this.classesPath + "static/file.txt")).isNotNull(); + assertThat(jarFile.getEntry(this.indexPath + "layers.idx")).isNotNull(); + indexedLayers = readLayerIndex(jarFile); + } + List layerNames = Arrays.asList("dependencies", "commons-dependencies", "snapshot-dependencies", + "static", "app"); + assertThat(indexedLayers.keySet()).containsExactlyElementsOf(layerNames); + Set expectedDependencies = new TreeSet<>(); + expectedDependencies.add(this.libPath + "spring-core-5.2.5.RELEASE.jar"); + 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); + 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); + assertThat(indexedLayers.get("static")).containsExactly(this.classesPath + "static/"); + List appLayer = new ArrayList<>(indexedLayers.get("app")); + String[] appLayerContents = getExpectedApplicationLayerContents(this.classesPath + "example/"); + assertThat(appLayer).containsSubsequence(appLayerContents); + appLayer.removeAll(Arrays.asList(appLayerContents)); + assertThat(appLayer).containsExactly("org/"); + BuildResult listLayers = this.gradleBuild.build("listLayers"); + assertThat(listLayers.task(":listLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + String listLayersOutput = listLayers.getOutput(); + assertThat(new BufferedReader(new StringReader(listLayersOutput)).lines()).containsSequence(layerNames); + BuildResult extractLayers = this.gradleBuild.build("extractLayers"); + assertThat(extractLayers.task(":extractLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertExtractedLayers(layerNames, indexedLayers); + } + + @TestTemplate + void multiModuleCustomLayers() throws IOException { + writeSettingsGradle(); + writeMainClass(); + writeResource(); + BuildResult build = this.gradleBuild.build(this.taskName); + assertThat(build.task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + Map> indexedLayers; + String layerToolsJar = this.libPath + JarModeLibrary.LAYER_TOOLS.getName(); + try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { + assertThat(jarFile.getEntry(layerToolsJar)).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(); + 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(); + assertThat(jarFile.getEntry(this.libPath + "library-1.0-SNAPSHOT.jar")).isNotNull(); + assertThat(jarFile.getEntry(this.classesPath + "example/Main.class")).isNotNull(); + assertThat(jarFile.getEntry(this.classesPath + "static/file.txt")).isNotNull(); + assertThat(jarFile.getEntry(this.indexPath + "layers.idx")).isNotNull(); + indexedLayers = readLayerIndex(jarFile); + } + List layerNames = Arrays.asList("dependencies", "commons-dependencies", "snapshot-dependencies", + "subproject-dependencies", "static", "app"); + assertThat(indexedLayers.keySet()).containsExactlyElementsOf(layerNames); + Set expectedSubprojectDependencies = new TreeSet<>(); + expectedSubprojectDependencies.add(this.libPath + "alpha-1.2.3.jar"); + expectedSubprojectDependencies.add(this.libPath + "bravo-1.2.3.jar"); + expectedSubprojectDependencies.add(this.libPath + "charlie-1.2.3.jar"); + Set expectedDependencies = new TreeSet<>(); + expectedDependencies.add(this.libPath + "spring-core-5.2.5.RELEASE.jar"); + 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); + assertThat(indexedLayers.get("subproject-dependencies")) + .containsExactlyElementsOf(expectedSubprojectDependencies); + 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); + assertThat(indexedLayers.get("static")).containsExactly(this.classesPath + "static/"); + List appLayer = new ArrayList<>(indexedLayers.get("app")); + String[] appLayerContents = getExpectedApplicationLayerContents(this.classesPath + "example/"); + assertThat(appLayer).containsSubsequence(appLayerContents); + appLayer.removeAll(Arrays.asList(appLayerContents)); + assertThat(appLayer).containsExactly("org/"); + BuildResult listLayers = this.gradleBuild.build("listLayers"); + assertThat(listLayers.task(":listLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + String listLayersOutput = listLayers.getOutput(); + assertThat(new BufferedReader(new StringReader(listLayersOutput)).lines()).containsSequence(layerNames); + BuildResult extractLayers = this.gradleBuild.build("extractLayers"); + assertThat(extractLayers.task(":extractLayers").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertExtractedLayers(layerNames, indexedLayers); + } + + @TestTemplate + void classesFromASecondarySourceSetCanBeIncludedInTheArchive() throws IOException { + writeMainClass(); + File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/secondary/java/example"); + examplePackage.mkdirs(); + File main = new File(examplePackage, "Secondary.java"); + try (PrintWriter writer = new PrintWriter(new FileWriter(main))) { + writer.println("package example;"); + writer.println(); + writer.println("public class Secondary {}"); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + BuildResult build = this.gradleBuild.build(this.taskName); + assertThat(build.task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { + Stream classesEntryNames = jarFile.stream().filter((entry) -> !entry.isDirectory()) + .map(JarEntry::getName).filter((name) -> name.startsWith(this.classesPath)); + assertThat(classesEntryNames).containsExactly(this.classesPath + "example/Main.class", + this.classesPath + "example/Secondary.class"); + } + } + + private void copyMainClassApplication() throws IOException { + copyApplication("main"); + } + + protected void copyApplication(String name) throws IOException { + File output = new File(this.gradleBuild.getProjectDir(), + "src/main/java/com/example/" + this.taskName.toLowerCase() + "/" + name); + output.mkdirs(); + FileSystemUtils.copyRecursively( + new File("src/test/java/com/example/" + this.taskName.toLowerCase(Locale.ENGLISH) + "/" + name), + output); + } + + private void createStandardJar(File location) throws IOException { + createJar(location, (attributes) -> { + }); + } + + private void createDependenciesStarterJar(File location) throws IOException { + createJar(location, (attributes) -> attributes.putValue("Spring-Boot-Jar-Type", "dependencies-starter")); + } + + private void createJar(File location, Consumer attributesConfigurer) throws IOException { + location.getParentFile().mkdirs(); + Manifest manifest = new Manifest(); + Attributes attributes = manifest.getMainAttributes(); + attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0"); + attributesConfigurer.accept(attributes); + new JarOutputStream(new FileOutputStream(location), manifest).close(); + } + + private void writeSettingsGradle() { + try (PrintWriter writer = new PrintWriter( + new FileWriter(new File(this.gradleBuild.getProjectDir(), "settings.gradle")))) { + writer.println("include 'alpha', 'bravo', 'charlie'"); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + private void writeMainClass() { + File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/main/java/example"); + examplePackage.mkdirs(); + File main = new File(examplePackage, "Main.java"); + try (PrintWriter writer = new PrintWriter(new FileWriter(main))) { + writer.println("package example;"); + writer.println(); + writer.println("import java.io.IOException;"); + writer.println(); + writer.println("public class Main {"); + writer.println(); + writer.println(" public static void main(String[] args) {"); + writer.println(" }"); + writer.println(); + writer.println("}"); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + private void writeResource() { + try { + Path path = this.gradleBuild.getProjectDir().toPath() + .resolve(Paths.get("src", "main", "resources", "static", "file.txt")); + Files.createDirectories(path.getParent()); + Files.createFile(path); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + private Map> readLayerIndex(JarFile jarFile) throws IOException { + Map> index = new LinkedHashMap<>(); + ZipEntry indexEntry = jarFile.getEntry(this.indexPath + "layers.idx"); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(jarFile.getInputStream(indexEntry)))) { + String line = reader.readLine(); + String layer = null; + while (line != null) { + if (line.startsWith("- ")) { + layer = line.substring(3, line.length() - 2); + } + else if (line.startsWith(" - ")) { + index.computeIfAbsent(layer, (key) -> new ArrayList<>()).add(line.substring(5, line.length() - 1)); + } + line = reader.readLine(); + } + return index; + } + } + + private Map> readExtractedLayers(File root, List layerNames) throws IOException { + Map> extractedLayers = new LinkedHashMap<>(); + for (String layerName : layerNames) { + File layer = new File(root, layerName); + assertThat(layer).isDirectory(); + extractedLayers.put(layerName, + Files.walk(layer.toPath()).filter((path) -> path.toFile().isFile()).map(layer.toPath()::relativize) + .map(Path::toString).map(StringUtils::cleanPath).collect(Collectors.toList())); + } + return extractedLayers; + } + + private void assertExtractedLayers(List layerNames, Map> indexedLayers) + throws IOException { + Map> extractedLayers = readExtractedLayers(this.gradleBuild.getProjectDir(), layerNames); + assertThat(extractedLayers.keySet()).isEqualTo(indexedLayers.keySet()); + extractedLayers.forEach((name, contents) -> { + List index = indexedLayers.get(name); + List unexpected = new ArrayList<>(); + for (String file : contents) { + if (!isInIndex(index, file)) { + unexpected.add(name); + } + } + assertThat(unexpected).isEmpty(); + }); + } + + private boolean isInIndex(List index, String file) { + for (String candidate : index) { + if (file.equals(candidate) || candidate.endsWith("/") && file.startsWith(candidate)) { + return true; + } + } + return false; } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java index 9b2ecd048ccd..5a33a52e9ac9 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,12 @@ package org.springframework.boot.gradle.tasks.bundling; +import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStreamReader; import java.nio.file.Files; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.PosixFilePermission; @@ -27,35 +29,58 @@ import java.util.Arrays; import java.util.Enumeration; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeSet; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; +import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipFile; +import org.gradle.api.Action; +import org.gradle.api.DomainObjectSet; import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.DependencySet; +import org.gradle.api.artifacts.ModuleVersionIdentifier; +import org.gradle.api.artifacts.ProjectDependency; +import org.gradle.api.artifacts.ResolvableDependencies; +import org.gradle.api.artifacts.ResolvedArtifact; +import org.gradle.api.artifacts.ResolvedConfiguration; +import org.gradle.api.artifacts.ResolvedModuleVersion; +import org.gradle.api.artifacts.component.ComponentArtifactIdentifier; +import org.gradle.api.artifacts.component.ModuleComponentIdentifier; +import org.gradle.api.artifacts.component.ProjectComponentIdentifier; +import org.gradle.api.internal.file.archive.ZipCopyAction; import org.gradle.api.tasks.bundling.AbstractArchiveTask; import org.gradle.api.tasks.bundling.Jar; -import org.gradle.testfixtures.ProjectBuilder; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.springframework.boot.gradle.junit.GradleProjectBuilder; import org.springframework.boot.loader.tools.DefaultLaunchScript; +import org.springframework.boot.loader.tools.JarModeLibrary; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; /** * Abstract base class for testing {@link BootArchive} implementations. * * @param the type of the concrete BootArchive implementation * @author Andy Wilkinson + * @author Scott Frederick */ abstract class AbstractBootArchiveTests { @@ -70,15 +95,19 @@ abstract class AbstractBootArchiveTests { private final String classesPath; + private final String indexPath; + private Project project; private T task; - protected AbstractBootArchiveTests(Class taskClass, String launcherClass, String libPath, String classesPath) { + protected AbstractBootArchiveTests(Class taskClass, String launcherClass, String libPath, String classesPath, + String indexPath) { this.taskClass = taskClass; this.launcherClass = launcherClass; this.libPath = libPath; this.classesPath = classesPath; + this.indexPath = indexPath; } @BeforeEach @@ -86,7 +115,7 @@ void createTask() { try { File projectDir = new File(this.temp, "project"); projectDir.mkdirs(); - this.project = ProjectBuilder.builder().withProjectDir(projectDir).build(); + this.project = GradleProjectBuilder.builder().withProjectDir(projectDir).build(); this.project.setDescription("Test project for " + this.taskClass.getSimpleName()); this.task = configure(this.project.getTasks().create("testArchive", this.taskClass)); } @@ -97,10 +126,9 @@ void createTask() { @Test void basicArchiveCreation() throws IOException { - this.task.setMainClassName("com.example.Main"); + this.task.getMainClass().set("com.example.Main"); executeTask(); - assertThat(this.task.getArchivePath()).exists(); - try (JarFile jarFile = new JarFile(this.task.getArchivePath())) { + try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) { assertThat(jarFile.getManifest().getMainAttributes().getValue("Main-Class")).isEqualTo(this.launcherClass); assertThat(jarFile.getManifest().getMainAttributes().getValue("Start-Class")).isEqualTo("com.example.Main"); assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Classes")) @@ -111,43 +139,45 @@ void basicArchiveCreation() throws IOException { } @Test - void classpathJarsArePackagedBeneathLibPath() throws IOException { - this.task.setMainClassName("com.example.Main"); + void classpathJarsArePackagedBeneathLibPathAndAreStored() throws IOException { + this.task.getMainClass().set("com.example.Main"); this.task.classpath(jarFile("one.jar"), jarFile("two.jar")); executeTask(); - try (JarFile jarFile = new JarFile(this.task.getArchivePath())) { - assertThat(jarFile.getEntry(this.libPath + "one.jar")).isNotNull(); - assertThat(jarFile.getEntry(this.libPath + "two.jar")).isNotNull(); + try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) { + assertThat(jarFile.getEntry(this.libPath + "one.jar")).isNotNull().extracting(ZipEntry::getMethod) + .isEqualTo(ZipEntry.STORED); + assertThat(jarFile.getEntry(this.libPath + "two.jar")).isNotNull().extracting(ZipEntry::getMethod) + .isEqualTo(ZipEntry.STORED); } } @Test - void classpathFoldersArePackagedBeneathClassesPath() throws IOException { - this.task.setMainClassName("com.example.Main"); - File classpathFolder = new File(this.temp, "classes"); - File applicationClass = new File(classpathFolder, "com/example/Application.class"); + void classpathDirectoriesArePackagedBeneathClassesPath() throws IOException { + this.task.getMainClass().set("com.example.Main"); + File classpathDirectory = new File(this.temp, "classes"); + File applicationClass = new File(classpathDirectory, "com/example/Application.class"); applicationClass.getParentFile().mkdirs(); applicationClass.createNewFile(); - this.task.classpath(classpathFolder); + this.task.classpath(classpathDirectory); executeTask(); - try (JarFile jarFile = new JarFile(this.task.getArchivePath())) { + try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) { assertThat(jarFile.getEntry(this.classesPath + "com/example/Application.class")).isNotNull(); } } @Test void moduleInfoClassIsPackagedInTheRootOfTheArchive() throws IOException { - this.task.setMainClassName("com.example.Main"); - File classpathFolder = new File(this.temp, "classes"); - File moduleInfoClass = new File(classpathFolder, "module-info.class"); + this.task.getMainClass().set("com.example.Main"); + File classpathDirectory = new File(this.temp, "classes"); + File moduleInfoClass = new File(classpathDirectory, "module-info.class"); moduleInfoClass.getParentFile().mkdirs(); moduleInfoClass.createNewFile(); - File applicationClass = new File(classpathFolder, "com/example/Application.class"); + File applicationClass = new File(classpathDirectory, "com/example/Application.class"); applicationClass.getParentFile().mkdirs(); applicationClass.createNewFile(); - this.task.classpath(classpathFolder); - this.task.execute(); - try (JarFile jarFile = new JarFile(this.task.getArchivePath())) { + this.task.classpath(classpathDirectory); + executeTask(); + try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) { assertThat(jarFile.getEntry(this.classesPath + "com/example/Application.class")).isNotNull(); assertThat(jarFile.getEntry("com/example/Application.class")).isNull(); assertThat(jarFile.getEntry("module-info.class")).isNotNull(); @@ -157,11 +187,11 @@ void moduleInfoClassIsPackagedInTheRootOfTheArchive() throws IOException { @Test void classpathCanBeSetUsingAFileCollection() throws IOException { - this.task.setMainClassName("com.example.Main"); + this.task.getMainClass().set("com.example.Main"); this.task.classpath(jarFile("one.jar")); this.task.setClasspath(this.task.getProject().files(jarFile("two.jar"))); executeTask(); - try (JarFile jarFile = new JarFile(this.task.getArchivePath())) { + try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) { assertThat(jarFile.getEntry(this.libPath + "one.jar")).isNull(); assertThat(jarFile.getEntry(this.libPath + "two.jar")).isNotNull(); } @@ -169,11 +199,11 @@ void classpathCanBeSetUsingAFileCollection() throws IOException { @Test void classpathCanBeSetUsingAnObject() throws IOException { - this.task.setMainClassName("com.example.Main"); + this.task.getMainClass().set("com.example.Main"); this.task.classpath(jarFile("one.jar")); this.task.setClasspath(jarFile("two.jar")); executeTask(); - try (JarFile jarFile = new JarFile(this.task.getArchivePath())) { + try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) { assertThat(jarFile.getEntry(this.libPath + "one.jar")).isNull(); assertThat(jarFile.getEntry(this.libPath + "two.jar")).isNotNull(); } @@ -181,24 +211,25 @@ void classpathCanBeSetUsingAnObject() throws IOException { @Test void filesOnTheClasspathThatAreNotZipFilesAreSkipped() throws IOException { - this.task.setMainClassName("com.example.Main"); + this.task.getMainClass().set("com.example.Main"); this.task.classpath(new File("test.pom")); - this.task.execute(); - try (JarFile jarFile = new JarFile(this.task.getArchivePath())) { + executeTask(); + try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) { assertThat(jarFile.getEntry(this.libPath + "/test.pom")).isNull(); } } @Test void loaderIsWrittenToTheRootOfTheJarAfterManifest() throws IOException { - this.task.setMainClassName("com.example.Main"); + this.task.getMainClass().set("com.example.Main"); executeTask(); - try (JarFile jarFile = new JarFile(this.task.getArchivePath())) { + try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) { assertThat(jarFile.getEntry("org/springframework/boot/loader/LaunchedURLClassLoader.class")).isNotNull(); assertThat(jarFile.getEntry("org/springframework/boot/loader/")).isNotNull(); } // gh-16698 - try (ZipInputStream zipInputStream = new ZipInputStream(new FileInputStream(this.task.getArchivePath()))) { + try (ZipInputStream zipInputStream = new ZipInputStream( + new FileInputStream(this.task.getArchiveFile().get().getAsFile()))) { assertThat(zipInputStream.getNextEntry().getName()).isEqualTo("META-INF/"); assertThat(zipInputStream.getNextEntry().getName()).isEqualTo("META-INF/MANIFEST.MF"); } @@ -206,10 +237,10 @@ void loaderIsWrittenToTheRootOfTheJarAfterManifest() throws IOException { @Test void loaderIsWrittenToTheRootOfTheJarWhenUsingThePropertiesLauncher() throws IOException { - this.task.setMainClassName("com.example.Main"); + this.task.getMainClass().set("com.example.Main"); executeTask(); this.task.getManifest().getAttributes().put("Main-Class", "org.springframework.boot.loader.PropertiesLauncher"); - try (JarFile jarFile = new JarFile(this.task.getArchivePath())) { + try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) { assertThat(jarFile.getEntry("org/springframework/boot/loader/LaunchedURLClassLoader.class")).isNotNull(); assertThat(jarFile.getEntry("org/springframework/boot/loader/")).isNotNull(); } @@ -217,11 +248,11 @@ void loaderIsWrittenToTheRootOfTheJarWhenUsingThePropertiesLauncher() throws IOE @Test void unpackCommentIsAddedToEntryIdentifiedByAPattern() throws IOException { - this.task.setMainClassName("com.example.Main"); + this.task.getMainClass().set("com.example.Main"); this.task.classpath(jarFile("one.jar"), jarFile("two.jar")); this.task.requiresUnpack("**/one.jar"); executeTask(); - try (JarFile jarFile = new JarFile(this.task.getArchivePath())) { + try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) { assertThat(jarFile.getEntry(this.libPath + "one.jar").getComment()).startsWith("UNPACK:"); assertThat(jarFile.getEntry(this.libPath + "two.jar").getComment()).isNull(); } @@ -229,11 +260,11 @@ void unpackCommentIsAddedToEntryIdentifiedByAPattern() throws IOException { @Test void unpackCommentIsAddedToEntryIdentifiedByASpec() throws IOException { - this.task.setMainClassName("com.example.Main"); + this.task.getMainClass().set("com.example.Main"); this.task.classpath(jarFile("one.jar"), jarFile("two.jar")); this.task.requiresUnpack((element) -> element.getName().endsWith("two.jar")); executeTask(); - try (JarFile jarFile = new JarFile(this.task.getArchivePath())) { + try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) { assertThat(jarFile.getEntry(this.libPath + "two.jar").getComment()).startsWith("UNPACK:"); assertThat(jarFile.getEntry(this.libPath + "one.jar").getComment()).isNull(); } @@ -241,17 +272,18 @@ void unpackCommentIsAddedToEntryIdentifiedByASpec() throws IOException { @Test void launchScriptCanBePrepended() throws IOException { - this.task.setMainClassName("com.example.Main"); + this.task.getMainClass().set("com.example.Main"); this.task.launchScript(); executeTask(); Map properties = new HashMap<>(); - properties.put("initInfoProvides", this.task.getBaseName()); + properties.put("initInfoProvides", this.task.getArchiveBaseName().get()); properties.put("initInfoShortDescription", this.project.getDescription()); properties.put("initInfoDescription", this.project.getDescription()); - assertThat(Files.readAllBytes(this.task.getArchivePath().toPath())) + assertThat(Files.readAllBytes(this.task.getArchiveFile().get().getAsFile().toPath())) .startsWith(new DefaultLaunchScript(null, properties).toByteArray()); try { - Set permissions = Files.getPosixFilePermissions(this.task.getArchivePath().toPath()); + Set permissions = Files + .getPosixFilePermissions(this.task.getArchiveFile().get().getAsFile().toPath()); assertThat(permissions).contains(PosixFilePermission.OWNER_EXECUTE); } catch (UnsupportedOperationException ex) { @@ -261,24 +293,25 @@ void launchScriptCanBePrepended() throws IOException { @Test void customLaunchScriptCanBePrepended() throws IOException { - this.task.setMainClassName("com.example.Main"); + this.task.getMainClass().set("com.example.Main"); File customScript = new File(this.temp, "custom.script"); Files.write(customScript.toPath(), Arrays.asList("custom script"), StandardOpenOption.CREATE); this.task.launchScript((configuration) -> configuration.setScript(customScript)); executeTask(); - assertThat(Files.readAllBytes(this.task.getArchivePath().toPath())).startsWith("custom script".getBytes()); + assertThat(Files.readAllBytes(this.task.getArchiveFile().get().getAsFile().toPath())) + .startsWith("custom script".getBytes()); } @Test void launchScriptInitInfoPropertiesCanBeCustomized() throws IOException { - this.task.setMainClassName("com.example.Main"); + this.task.getMainClass().set("com.example.Main"); this.task.launchScript((configuration) -> { configuration.getProperties().put("initInfoProvides", "provides"); configuration.getProperties().put("initInfoShortDescription", "short description"); configuration.getProperties().put("initInfoDescription", "description"); }); executeTask(); - byte[] bytes = Files.readAllBytes(this.task.getArchivePath().toPath()); + byte[] bytes = Files.readAllBytes(this.task.getArchiveFile().get().getAsFile().toPath()); assertThat(bytes).containsSequence("Provides: provides".getBytes()); assertThat(bytes).containsSequence("Short-Description: short description".getBytes()); assertThat(bytes).containsSequence("Description: description".getBytes()); @@ -286,11 +319,11 @@ void launchScriptInitInfoPropertiesCanBeCustomized() throws IOException { @Test void customMainClassInTheManifestIsHonored() throws IOException { - this.task.setMainClassName("com.example.Main"); + this.task.getMainClass().set("com.example.Main"); this.task.getManifest().getAttributes().put("Main-Class", "com.example.CustomLauncher"); executeTask(); - assertThat(this.task.getArchivePath()).exists(); - try (JarFile jarFile = new JarFile(this.task.getArchivePath())) { + assertThat(this.task.getArchiveFile().get().getAsFile()).exists(); + try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) { assertThat(jarFile.getManifest().getMainAttributes().getValue("Main-Class")) .isEqualTo("com.example.CustomLauncher"); assertThat(jarFile.getManifest().getMainAttributes().getValue("Start-Class")).isEqualTo("com.example.Main"); @@ -300,11 +333,11 @@ void customMainClassInTheManifestIsHonored() throws IOException { @Test void customStartClassInTheManifestIsHonored() throws IOException { - this.task.setMainClassName("com.example.Main"); + this.task.getMainClass().set("com.example.Main"); this.task.getManifest().getAttributes().put("Start-Class", "com.example.CustomMain"); executeTask(); - assertThat(this.task.getArchivePath()).exists(); - try (JarFile jarFile = new JarFile(this.task.getArchivePath())) { + assertThat(this.task.getArchiveFile().get().getAsFile()).exists(); + try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) { assertThat(jarFile.getManifest().getMainAttributes().getValue("Main-Class")).isEqualTo(this.launcherClass); assertThat(jarFile.getManifest().getMainAttributes().getValue("Start-Class")) .isEqualTo("com.example.CustomMain"); @@ -313,11 +346,11 @@ void customStartClassInTheManifestIsHonored() throws IOException { @Test void fileTimestampPreservationCanBeDisabled() throws IOException { - this.task.setMainClassName("com.example.Main"); + this.task.getMainClass().set("com.example.Main"); this.task.setPreserveFileTimestamps(false); executeTask(); - assertThat(this.task.getArchivePath()).exists(); - try (JarFile jarFile = new JarFile(this.task.getArchivePath())) { + assertThat(this.task.getArchiveFile().get().getAsFile()).exists(); + try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) { Enumeration entries = jarFile.entries(); while (entries.hasMoreElements()) { JarEntry entry = entries.nextElement(); @@ -326,15 +359,21 @@ void fileTimestampPreservationCanBeDisabled() throws IOException { } } + @Test + void constantTimestampMatchesGradleInternalTimestamp() { + assertThat(BootZipCopyAction.CONSTANT_TIME_FOR_ZIP_ENTRIES) + .isEqualTo(ZipCopyAction.CONSTANT_TIME_FOR_ZIP_ENTRIES); + } + @Test void reproducibleOrderingCanBeEnabled() throws IOException { - this.task.setMainClassName("com.example.Main"); + this.task.getMainClass().set("com.example.Main"); this.task.from(newFile("bravo.txt"), newFile("alpha.txt"), newFile("charlie.txt")); this.task.setReproducibleFileOrder(true); executeTask(); - assertThat(this.task.getArchivePath()).exists(); + assertThat(this.task.getArchiveFile().get().getAsFile()).exists(); List textFiles = new ArrayList<>(); - try (JarFile jarFile = new JarFile(this.task.getArchivePath())) { + try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) { Enumeration entries = jarFile.entries(); while (entries.hasMoreElements()) { JarEntry entry = entries.nextElement(); @@ -348,38 +387,26 @@ void reproducibleOrderingCanBeEnabled() throws IOException { @Test void devtoolsJarIsExcludedByDefault() throws IOException { - this.task.setMainClassName("com.example.Main"); + this.task.getMainClass().set("com.example.Main"); this.task.classpath(newFile("spring-boot-devtools-0.1.2.jar")); executeTask(); - assertThat(this.task.getArchivePath()).exists(); - try (JarFile jarFile = new JarFile(this.task.getArchivePath())) { + assertThat(this.task.getArchiveFile().get().getAsFile()).exists(); + try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) { assertThat(jarFile.getEntry(this.libPath + "spring-boot-devtools-0.1.2.jar")).isNull(); } } - @Test - void devtoolsJarCanBeIncluded() throws IOException { - this.task.setMainClassName("com.example.Main"); - this.task.classpath(jarFile("spring-boot-devtools-0.1.2.jar")); - this.task.setExcludeDevtools(false); - executeTask(); - assertThat(this.task.getArchivePath()).exists(); - try (JarFile jarFile = new JarFile(this.task.getArchivePath())) { - assertThat(jarFile.getEntry(this.libPath + "spring-boot-devtools-0.1.2.jar")).isNotNull(); - } - } - @Test void allEntriesUseUnixPlatformAndUtf8NameEncoding() throws IOException { - this.task.setMainClassName("com.example.Main"); + this.task.getMainClass().set("com.example.Main"); this.task.setMetadataCharset("UTF-8"); - File classpathFolder = new File(this.temp, "classes"); - File resource = new File(classpathFolder, "some-resource.xml"); + File classpathDirectory = new File(this.temp, "classes"); + File resource = new File(classpathDirectory, "some-resource.xml"); resource.getParentFile().mkdirs(); resource.createNewFile(); - this.task.classpath(classpathFolder); + this.task.classpath(classpathDirectory); executeTask(); - File archivePath = this.task.getArchivePath(); + File archivePath = this.task.getArchiveFile().get().getAsFile(); try (ZipFile zip = new ZipFile(archivePath)) { Enumeration entries = zip.getEntries(); while (entries.hasMoreElements()) { @@ -392,18 +419,158 @@ void allEntriesUseUnixPlatformAndUtf8NameEncoding() throws IOException { @Test void loaderIsWrittenFirstThenApplicationClassesThenLibraries() throws IOException { - this.task.setMainClassName("com.example.Main"); - File classpathFolder = new File(this.temp, "classes"); - File applicationClass = new File(classpathFolder, "com/example/Application.class"); + this.task.getMainClass().set("com.example.Main"); + File classpathDirectory = new File(this.temp, "classes"); + File applicationClass = new File(classpathDirectory, "com/example/Application.class"); applicationClass.getParentFile().mkdirs(); applicationClass.createNewFile(); - this.task.classpath(classpathFolder, jarFile("first-library.jar"), jarFile("second-library.jar"), + this.task.classpath(classpathDirectory, jarFile("first-library.jar"), jarFile("second-library.jar"), jarFile("third-library.jar")); this.task.requiresUnpack("second-library.jar"); executeTask(); - assertThat(getEntryNames(this.task.getArchivePath())).containsSubsequence("org/springframework/boot/loader/", - this.classesPath + "com/example/Application.class", this.libPath + "first-library.jar", - this.libPath + "second-library.jar", this.libPath + "third-library.jar"); + assertThat(getEntryNames(this.task.getArchiveFile().get().getAsFile())).containsSubsequence( + "org/springframework/boot/loader/", this.classesPath + "com/example/Application.class", + this.libPath + "first-library.jar", this.libPath + "second-library.jar", + this.libPath + "third-library.jar"); + } + + @Test + void archiveShouldBeLayeredByDefault() throws IOException { + addContent(); + executeTask(); + try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) { + assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Classes")) + .isEqualTo(this.classesPath); + assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Lib")).isEqualTo(this.libPath); + assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Layers-Index")) + .isEqualTo(this.indexPath + "layers.idx"); + assertThat(getEntryNames(jarFile)).contains(this.libPath + JarModeLibrary.LAYER_TOOLS.getName()); + } + } + + @Test + void jarWhenLayersDisabledShouldNotContainLayersIndex() throws IOException { + List entryNames = getEntryNames(createLayeredJar((configuration) -> configuration.setEnabled(false))); + assertThat(entryNames).doesNotContain(this.indexPath + "layers.idx"); + } + + @Test + void whenJarIsLayeredThenManifestContainsEntryForLayersIndexInPlaceOfClassesAndLib() throws IOException { + try (JarFile jarFile = new JarFile(createLayeredJar())) { + assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Classes")) + .isEqualTo(this.classesPath); + assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Lib")).isEqualTo(this.libPath); + assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Layers-Index")) + .isEqualTo(this.indexPath + "layers.idx"); + } + } + + @Test + void whenJarIsLayeredThenLayersIndexIsPresentAndCorrect() throws IOException { + try (JarFile jarFile = new JarFile(createLayeredJar())) { + List entryNames = getEntryNames(jarFile); + assertThat(entryNames).contains(this.libPath + "first-library.jar", this.libPath + "second-library.jar", + this.libPath + "third-library-SNAPSHOT.jar", this.libPath + "first-project-library.jar", + this.libPath + "second-project-library-SNAPSHOT.jar", + this.classesPath + "com/example/Application.class", this.classesPath + "application.properties", + this.classesPath + "static/test.css"); + List index = entryLines(jarFile, this.indexPath + "layers.idx"); + assertThat(getLayerNames(index)).containsExactly("dependencies", "spring-boot-loader", + "snapshot-dependencies", "application"); + String layerToolsJar = this.libPath + JarModeLibrary.LAYER_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 + "second-library.jar\""); + if (!layerToolsJar.contains("SNAPSHOT")) { + expected.add(" - \"" + layerToolsJar + "\""); + } + 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 + "\""); + } + expected.add(" - \"" + this.libPath + "third-library-SNAPSHOT.jar\""); + expected.add("- \"application\":"); + Set applicationContents = new TreeSet<>(); + applicationContents.add(" - \"" + this.classesPath + "\""); + if (archiveHasClasspathIndex()) { + applicationContents.add(" - \"" + this.indexPath + "classpath.idx\""); + } + applicationContents.add(" - \"" + this.indexPath + "layers.idx\""); + applicationContents.add(" - \"META-INF/\""); + expected.addAll(applicationContents); + assertThat(index).containsExactlyElementsOf(expected); + } + } + + @Test + void whenJarIsLayeredWithCustomStrategiesThenLayersIndexIsPresentAndCorrect() throws IOException { + File jar = createLayeredJar((layered) -> { + layered.application((application) -> { + application.intoLayer("resources", (spec) -> spec.include("static/**")); + application.intoLayer("application"); + }); + layered.dependencies((dependencies) -> { + dependencies.intoLayer("my-snapshot-deps", (spec) -> spec.include("com.example:*:*.SNAPSHOT")); + dependencies.intoLayer("my-internal-deps", (spec) -> spec.include("com.example:*:*")); + dependencies.intoLayer("my-deps"); + }); + layered.setLayerOrder("my-deps", "my-internal-deps", "my-snapshot-deps", "resources", "application"); + }); + try (JarFile jarFile = new JarFile(jar)) { + List entryNames = getEntryNames(jar); + assertThat(entryNames).contains(this.libPath + "first-library.jar", this.libPath + "second-library.jar", + this.libPath + "third-library-SNAPSHOT.jar", this.libPath + "first-project-library.jar", + this.libPath + "second-project-library-SNAPSHOT.jar", + this.classesPath + "com/example/Application.class", this.classesPath + "application.properties", + this.classesPath + "static/test.css"); + 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.LAYER_TOOLS.getName(); + List expected = new ArrayList<>(); + expected.add("- \"my-deps\":"); + expected.add(" - \"" + layerToolsJar + "\""); + expected.add("- \"my-internal-deps\":"); + expected.add(" - \"" + this.libPath + "first-library.jar\""); + expected.add(" - \"" + this.libPath + "first-project-library.jar\""); + expected.add(" - \"" + this.libPath + "second-library.jar\""); + expected.add("- \"my-snapshot-deps\":"); + expected.add(" - \"" + this.libPath + "second-project-library-SNAPSHOT.jar\""); + expected.add(" - \"" + this.libPath + "third-library-SNAPSHOT.jar\""); + expected.add("- \"resources\":"); + expected.add(" - \"" + this.classesPath + "static/\""); + expected.add("- \"application\":"); + Set applicationContents = new TreeSet<>(); + applicationContents.add(" - \"" + this.classesPath + "application.properties\""); + applicationContents.add(" - \"" + this.classesPath + "com/\""); + if (archiveHasClasspathIndex()) { + applicationContents.add(" - \"" + this.indexPath + "classpath.idx\""); + } + applicationContents.add(" - \"" + this.indexPath + "layers.idx\""); + applicationContents.add(" - \"META-INF/\""); + applicationContents.add(" - \"org/\""); + expected.addAll(applicationContents); + assertThat(index).containsExactlyElementsOf(expected); + } + } + + @Test + void whenArchiveIsLayeredThenLayerToolsAreAddedToTheJar() throws IOException { + List entryNames = getEntryNames(createLayeredJar()); + assertThat(entryNames).contains(this.libPath + JarModeLibrary.LAYER_TOOLS.getName()); + } + + @Test + void whenArchiveIsLayeredAndIncludeLayerToolsIsFalseThenLayerToolsAreNotAddedToTheJar() throws IOException { + List entryNames = getEntryNames( + createLayeredJar((configuration) -> configuration.setIncludeLayerTools(false))); + assertThat(entryNames) + .doesNotContain(this.indexPath + "layers/dependencies/lib/spring-boot-jarmode-layertools.jar"); } protected File jarFile(String name) throws IOException { @@ -418,10 +585,10 @@ protected File jarFile(String name) throws IOException { private T configure(T task) throws IOException { AbstractArchiveTask archiveTask = task; - archiveTask.setBaseName("test"); + archiveTask.getArchiveBaseName().set("test"); File destination = new File(this.temp, "destination"); destination.mkdirs(); - archiveTask.setDestinationDir(destination); + archiveTask.getDestinationDirectory().set(destination); return task; } @@ -432,12 +599,16 @@ protected T getTask() { } protected List getEntryNames(File file) throws IOException { - List entryNames = new ArrayList<>(); try (JarFile jarFile = new JarFile(file)) { - Enumeration entries = jarFile.entries(); - while (entries.hasMoreElements()) { - entryNames.add(entries.nextElement().getName()); - } + return getEntryNames(jarFile); + } + } + + protected List getEntryNames(JarFile jarFile) { + List entryNames = new ArrayList<>(); + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + entryNames.add(entries.nextElement().getName()); } return entryNames; } @@ -448,4 +619,118 @@ protected File newFile(String name) throws IOException { return file; } + File createLayeredJar() throws IOException { + return createLayeredJar((spec) -> { + }); + } + + File createLayeredJar(Action action) throws IOException { + applyLayered(action); + addContent(); + executeTask(); + return getTask().getArchiveFile().get().getAsFile(); + } + + abstract void applyLayered(Action action); + + boolean archiveHasClasspathIndex() { + return true; + } + + @SuppressWarnings("unchecked") + void addContent() throws IOException { + this.task.getMainClass().set("com.example.Main"); + File classesJavaMain = new File(this.temp, "classes/java/main"); + File applicationClass = new File(classesJavaMain, "com/example/Application.class"); + applicationClass.getParentFile().mkdirs(); + applicationClass.createNewFile(); + File resourcesMain = new File(this.temp, "resources/main"); + File applicationProperties = new File(resourcesMain, "application.properties"); + applicationProperties.getParentFile().mkdirs(); + applicationProperties.createNewFile(); + File staticResources = new File(resourcesMain, "static"); + staticResources.mkdir(); + File css = new File(staticResources, "test.css"); + css.createNewFile(); + this.task.classpath(classesJavaMain, resourcesMain, jarFile("first-library.jar"), jarFile("second-library.jar"), + jarFile("third-library-SNAPSHOT.jar"), jarFile("first-project-library.jar"), + jarFile("second-project-library-SNAPSHOT.jar")); + Set artifacts = new LinkedHashSet<>(); + artifacts.add(mockLibraryArtifact("first-library.jar", "com.example", "first-library", "1.0.0")); + artifacts.add(mockLibraryArtifact("second-library.jar", "com.example", "second-library", "1.0.0")); + artifacts.add( + mockLibraryArtifact("third-library-SNAPSHOT.jar", "com.example", "third-library", "1.0.0.SNAPSHOT")); + artifacts + .add(mockProjectArtifact("first-project-library.jar", "com.example", "first-project-library", "1.0.0")); + artifacts.add(mockProjectArtifact("second-project-library-SNAPSHOT.jar", "com.example", + "second-project-library", "1.0.0.SNAPSHOT")); + ResolvedConfiguration resolvedConfiguration = mock(ResolvedConfiguration.class); + given(resolvedConfiguration.getResolvedArtifacts()).willReturn(artifacts); + Configuration configuration = mock(Configuration.class); + given(configuration.getResolvedConfiguration()).willReturn(resolvedConfiguration); + ResolvableDependencies resolvableDependencies = mock(ResolvableDependencies.class); + given(configuration.getIncoming()).willReturn(resolvableDependencies); + DependencySet dependencies = mock(DependencySet.class); + DomainObjectSet projectDependencies = mock(DomainObjectSet.class); + given(dependencies.withType(ProjectDependency.class)).willReturn(projectDependencies); + given(configuration.getAllDependencies()).willReturn(dependencies); + willAnswer((invocation) -> { + invocation.getArgument(0, Action.class).execute(resolvableDependencies); + return null; + }).given(resolvableDependencies).afterResolve(any(Action.class)); + given(configuration.getIncoming()).willReturn(resolvableDependencies); + populateResolvedDependencies(configuration); + } + + abstract void populateResolvedDependencies(Configuration configuration); + + private ResolvedArtifact mockLibraryArtifact(String fileName, String group, String module, String version) { + ModuleComponentIdentifier moduleComponentIdentifier = mock(ModuleComponentIdentifier.class); + ComponentArtifactIdentifier libraryArtifactId = mock(ComponentArtifactIdentifier.class); + given(libraryArtifactId.getComponentIdentifier()).willReturn(moduleComponentIdentifier); + ResolvedArtifact libraryArtifact = mockArtifact(fileName, group, module, version); + given(libraryArtifact.getId()).willReturn(libraryArtifactId); + return libraryArtifact; + } + + private ResolvedArtifact mockProjectArtifact(String fileName, String group, String module, String version) { + ProjectComponentIdentifier projectComponentIdentifier = mock(ProjectComponentIdentifier.class); + ComponentArtifactIdentifier projectArtifactId = mock(ComponentArtifactIdentifier.class); + given(projectArtifactId.getComponentIdentifier()).willReturn(projectComponentIdentifier); + ResolvedArtifact projectArtifact = mockArtifact(fileName, group, module, version); + given(projectArtifact.getId()).willReturn(projectArtifactId); + return projectArtifact; + } + + private ResolvedArtifact mockArtifact(String fileName, String group, String module, String version) { + ModuleVersionIdentifier moduleVersionIdentifier = mock(ModuleVersionIdentifier.class); + given(moduleVersionIdentifier.getGroup()).willReturn(group); + given(moduleVersionIdentifier.getName()).willReturn(module); + given(moduleVersionIdentifier.getVersion()).willReturn(version); + ResolvedModuleVersion moduleVersion = mock(ResolvedModuleVersion.class); + given(moduleVersion.getId()).willReturn(moduleVersionIdentifier); + ResolvedArtifact libraryArtifact = mock(ResolvedArtifact.class); + File file = new File(this.temp, fileName).getAbsoluteFile(); + given(libraryArtifact.getFile()).willReturn(file); + given(libraryArtifact.getModuleVersion()).willReturn(moduleVersion); + return libraryArtifact; + } + + List entryLines(JarFile jarFile, String entryName) throws IOException { + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(jarFile.getInputStream(jarFile.getEntry(entryName))))) { + return reader.lines().collect(Collectors.toList()); + } + } + + private Set getLayerNames(List index) { + Set layerNames = new LinkedHashSet<>(); + for (String line : index) { + if (line.startsWith("- ")) { + layerNames.add(line.substring(3, line.length() - 2)); + } + } + return layerNames; + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java new file mode 100644 index 000000000000..14742f09a77d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java @@ -0,0 +1,411 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.gradle.tasks.bundling; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.Random; +import java.util.Set; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream; +import org.apache.commons.compress.utils.IOUtils; +import org.gradle.testkit.runner.BuildResult; +import org.gradle.testkit.runner.TaskOutcome; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import org.springframework.boot.buildpack.platform.docker.DockerApi; +import org.springframework.boot.buildpack.platform.docker.type.ImageName; +import org.springframework.boot.buildpack.platform.docker.type.ImageReference; +import org.springframework.boot.buildpack.platform.io.FilePermissions; +import org.springframework.boot.gradle.junit.GradleCompatibility; +import org.springframework.boot.gradle.testkit.GradleBuild; +import org.springframework.boot.testsupport.testcontainers.DisabledIfDockerUnavailable; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link BootBuildImage}. + * + * @author Andy Wilkinson + * @author Scott Frederick + */ +@GradleCompatibility(configurationCache = true) +@DisabledIfDockerUnavailable +class BootBuildImageIntegrationTests { + + GradleBuild gradleBuild; + + @TestTemplate + void buildsImageWithDefaultBuilder() throws IOException { + writeMainClass(); + writeLongNameResource(); + BuildResult result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT"); + String projectName = this.gradleBuild.getProjectDir().getName(); + assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("docker.io/library/" + projectName); + assertThat(result.getOutput()).contains("---> Test Info buildpack building"); + assertThat(result.getOutput()).contains("env: BP_JVM_VERSION=8.*"); + assertThat(result.getOutput()).contains("---> Test Info buildpack done"); + removeImage(projectName); + } + + @TestTemplate + void buildsImageWithWarPackaging() throws IOException { + writeMainClass(); + writeLongNameResource(); + BuildResult result = this.gradleBuild.build("bootBuildImage", "-PapplyWarPlugin", + "--pullPolicy=IF_NOT_PRESENT"); + String projectName = this.gradleBuild.getProjectDir().getName(); + assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("docker.io/library/" + projectName); + assertThat(result.getOutput()).contains("---> Test Info buildpack building"); + assertThat(result.getOutput()).contains("env: BP_JVM_VERSION=8.*"); + assertThat(result.getOutput()).contains("---> Test Info buildpack done"); + File buildLibs = new File(this.gradleBuild.getProjectDir(), "build/libs"); + assertThat(buildLibs.listFiles()) + .containsExactly(new File(buildLibs, this.gradleBuild.getProjectDir().getName() + ".war")); + removeImage(projectName); + } + + @TestTemplate + void buildsImageWithWarPackagingAndJarConfiguration() throws IOException { + writeMainClass(); + writeLongNameResource(); + BuildResult result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT"); + String projectName = this.gradleBuild.getProjectDir().getName(); + assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("docker.io/library/" + projectName); + assertThat(result.getOutput()).contains("---> Test Info buildpack building"); + assertThat(result.getOutput()).contains("---> Test Info buildpack done"); + File buildLibs = new File(this.gradleBuild.getProjectDir(), "build/libs"); + assertThat(buildLibs.listFiles()) + .containsExactly(new File(buildLibs, this.gradleBuild.getProjectDir().getName() + ".war")); + removeImage(projectName); + } + + @TestTemplate + void buildsImageWithCustomName() throws IOException { + writeMainClass(); + writeLongNameResource(); + BuildResult result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT"); + assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("example/test-image-name"); + assertThat(result.getOutput()).contains("---> Test Info buildpack building"); + assertThat(result.getOutput()).contains("---> Test Info buildpack done"); + removeImage("example/test-image-name"); + } + + @TestTemplate + void buildsImageWithCustomBuilderAndRunImage() throws IOException { + writeMainClass(); + writeLongNameResource(); + BuildResult result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT"); + assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("example/test-image-custom"); + assertThat(result.getOutput()).contains("---> Test Info buildpack building"); + assertThat(result.getOutput()).contains("---> Test Info buildpack done"); + removeImage("example/test-image-custom"); + } + + @TestTemplate + void buildsImageWithCommandLineOptions() throws IOException { + writeMainClass(); + writeLongNameResource(); + BuildResult result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT", + "--imageName=example/test-image-cmd", + "--builder=projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1", + "--runImage=projects.registry.vmware.com/springboot/run:tiny-cnb"); + assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("example/test-image-cmd"); + assertThat(result.getOutput()).contains("---> Test Info buildpack building"); + assertThat(result.getOutput()).contains("---> Test Info buildpack done"); + removeImage("example/test-image-cmd"); + } + + @TestTemplate + void buildsImageWithPullPolicy() throws IOException { + writeMainClass(); + writeLongNameResource(); + String projectName = this.gradleBuild.getProjectDir().getName(); + BuildResult result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=ALWAYS"); + assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("Pulled builder image").contains("Pulled run image"); + result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT"); + assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).doesNotContain("Pulled builder image").doesNotContain("Pulled run image"); + removeImage(projectName); + } + + @TestTemplate + void buildsImageWithBuildpackFromBuilder() throws IOException { + writeMainClass(); + writeLongNameResource(); + BuildResult result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT"); + String projectName = this.gradleBuild.getProjectDir().getName(); + assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("docker.io/library/" + projectName); + assertThat(result.getOutput()).contains("---> Test Info buildpack building") + .contains("---> Test Info buildpack done"); + removeImage(projectName); + } + + @TestTemplate + @DisabledOnOs(OS.WINDOWS) + void buildsImageWithBuildpackFromDirectory() throws IOException { + writeMainClass(); + writeLongNameResource(); + writeBuildpackContent(); + BuildResult result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT"); + String projectName = this.gradleBuild.getProjectDir().getName(); + assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("docker.io/library/" + projectName); + assertThat(result.getOutput()).contains("---> Hello World buildpack"); + removeImage(projectName); + } + + @TestTemplate + @DisabledOnOs(OS.WINDOWS) + void buildsImageWithBuildpackFromTarGzip() throws IOException { + writeMainClass(); + writeLongNameResource(); + writeBuildpackContent(); + tarGzipBuildpackContent(); + BuildResult result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT"); + String projectName = this.gradleBuild.getProjectDir().getName(); + assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("docker.io/library/" + projectName); + assertThat(result.getOutput()).contains("---> Hello World buildpack"); + removeImage(projectName); + } + + @TestTemplate + void buildsImageWithBuildpacksFromImages() throws IOException { + writeMainClass(); + writeLongNameResource(); + BuildResult result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT"); + String projectName = this.gradleBuild.getProjectDir().getName(); + assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("docker.io/library/" + projectName); + assertThat(result.getOutput()).contains("---> Test Info buildpack building") + .contains("---> Test Info buildpack done"); + removeImage(projectName); + } + + @TestTemplate + void buildsImageWithBinding() throws IOException { + writeMainClass(); + writeLongNameResource(); + writeCertificateBindingFiles(); + BuildResult result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT"); + String projectName = this.gradleBuild.getProjectDir().getName(); + assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("docker.io/library/" + projectName); + assertThat(result.getOutput()).contains("---> Test Info buildpack building"); + assertThat(result.getOutput()).contains("binding: certificates/type=ca-certificates"); + assertThat(result.getOutput()).contains("binding: certificates/test1.crt=---certificate one---"); + assertThat(result.getOutput()).contains("binding: certificates/test2.crt=---certificate two---"); + assertThat(result.getOutput()).contains("---> Test Info buildpack done"); + removeImage(projectName); + } + + @TestTemplate + void failsWithLaunchScript() throws IOException { + writeMainClass(); + writeLongNameResource(); + BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage"); + assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.FAILED); + assertThat(result.getOutput()).contains("not compatible with buildpacks"); + } + + @TestTemplate + void failsWithBuilderError() throws IOException { + writeMainClass(); + writeLongNameResource(); + BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT"); + assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.FAILED); + assertThat(result.getOutput()).contains("Forced builder failure"); + assertThat(result.getOutput()).containsPattern("Builder lifecycle '.*' failed with status code"); + } + + @TestTemplate + void failsWithInvalidImageName() throws IOException { + writeMainClass(); + writeLongNameResource(); + BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage", "--imageName=example/Invalid-Image-Name"); + assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.FAILED); + assertThat(result.getOutput()).containsPattern("Unable to parse image reference") + .containsPattern("example/Invalid-Image-Name"); + } + + @TestTemplate + void failsWithPublishMissingPublishRegistry() throws IOException { + writeMainClass(); + writeLongNameResource(); + BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage", "--publishImage"); + assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.FAILED); + assertThat(result.getOutput()).contains("requires docker.publishRegistry"); + } + + @TestTemplate + void failsWithBuildpackNotInBuilder() throws IOException { + writeMainClass(); + writeLongNameResource(); + BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT"); + assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.FAILED); + assertThat(result.getOutput()).contains("'urn:cnb:builder:example/does-not-exist:0.0.1' not found in builder"); + } + + private void writeMainClass() throws IOException { + File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/main/java/example"); + examplePackage.mkdirs(); + File main = new File(examplePackage, "Main.java"); + try (PrintWriter writer = new PrintWriter(new FileWriter(main))) { + writer.println("package example;"); + writer.println(); + writer.println("import java.io.IOException;"); + writer.println(); + writer.println("public class Main {"); + writer.println(); + writer.println(" public static void main(String[] args) throws Exception {"); + writer.println(" System.out.println(\"Launched\");"); + writer.println(" synchronized(args) {"); + writer.println(" args.wait(); // Prevent exit"); + writer.println(" }"); + writer.println(" }"); + writer.println(); + writer.println("}"); + } + } + + private void writeLongNameResource() throws IOException { + StringBuilder name = new StringBuilder(); + new Random().ints('a', 'z' + 1).limit(128).forEach((i) -> name.append((char) i)); + Path path = this.gradleBuild.getProjectDir().toPath() + .resolve(Paths.get("src", "main", "resources", name.toString())); + Files.createDirectories(path.getParent()); + Files.createFile(path); + } + + private void writeBuildpackContent() throws IOException { + FileAttribute> dirAttribute = PosixFilePermissions + .asFileAttribute(PosixFilePermissions.fromString("rwxr-xr-x")); + FileAttribute> execFileAttribute = PosixFilePermissions + .asFileAttribute(PosixFilePermissions.fromString("rwxrwxrwx")); + File buildpackDir = new File(this.gradleBuild.getProjectDir(), "buildpack/hello-world"); + Files.createDirectories(buildpackDir.toPath(), dirAttribute); + File binDir = new File(buildpackDir, "bin"); + Files.createDirectories(binDir.toPath(), dirAttribute); + File descriptor = new File(buildpackDir, "buildpack.toml"); + try (PrintWriter writer = new PrintWriter(new FileWriter(descriptor))) { + writer.println("api = \"0.2\""); + writer.println("[buildpack]"); + writer.println("id = \"example/hello-world\""); + writer.println("version = \"0.0.1\""); + writer.println("name = \"Hello World Buildpack\""); + writer.println("homepage = \"https://github.com/buildpacks/samples/tree/main/buildpacks/hello-world\""); + writer.println("[[stacks]]\n"); + writer.println("id = \"io.buildpacks.stacks.bionic\""); + } + File detect = Files.createFile(Paths.get(binDir.getAbsolutePath(), "detect"), execFileAttribute).toFile(); + try (PrintWriter writer = new PrintWriter(new FileWriter(detect))) { + writer.println("#!/usr/bin/env bash"); + writer.println("set -eo pipefail"); + writer.println("exit 0"); + } + File build = Files.createFile(Paths.get(binDir.getAbsolutePath(), "build"), execFileAttribute).toFile(); + try (PrintWriter writer = new PrintWriter(new FileWriter(build))) { + writer.println("#!/usr/bin/env bash"); + writer.println("set -eo pipefail"); + writer.println("echo \"---> Hello World buildpack\""); + writer.println("echo \"---> done\""); + writer.println("exit 0"); + } + } + + private void tarGzipBuildpackContent() throws IOException { + Path tarGzipPath = Paths.get(this.gradleBuild.getProjectDir().getAbsolutePath(), "hello-world.tgz"); + try (TarArchiveOutputStream tar = new TarArchiveOutputStream( + new GzipCompressorOutputStream(Files.newOutputStream(Files.createFile(tarGzipPath))))) { + File buildpackDir = new File(this.gradleBuild.getProjectDir(), "buildpack/hello-world"); + writeDirectoryToTar(tar, buildpackDir, buildpackDir.getAbsolutePath()); + } + } + + private void writeDirectoryToTar(TarArchiveOutputStream tar, File dir, String baseDirPath) throws IOException { + for (File file : dir.listFiles()) { + String name = file.getAbsolutePath().replace(baseDirPath, ""); + int mode = FilePermissions.umaskForPath(file.toPath()); + if (file.isDirectory()) { + writeTarEntry(tar, name + "/", mode); + writeDirectoryToTar(tar, file, baseDirPath); + } + else { + writeTarEntry(tar, file, name, mode); + } + } + } + + private void writeTarEntry(TarArchiveOutputStream tar, String name, int mode) throws IOException { + TarArchiveEntry entry = new TarArchiveEntry(name); + entry.setMode(mode); + tar.putArchiveEntry(entry); + tar.closeArchiveEntry(); + } + + private void writeTarEntry(TarArchiveOutputStream tar, File file, String name, int mode) throws IOException { + TarArchiveEntry entry = new TarArchiveEntry(file, name); + entry.setMode(mode); + tar.putArchiveEntry(entry); + IOUtils.copy(Files.newInputStream(file.toPath()), tar); + tar.closeArchiveEntry(); + } + + private void writeCertificateBindingFiles() throws IOException { + File bindingDir = new File(this.gradleBuild.getProjectDir(), "bindings/ca-certificates"); + bindingDir.mkdirs(); + File type = new File(bindingDir, "type"); + try (PrintWriter writer = new PrintWriter(new FileWriter(type))) { + writer.print("ca-certificates"); + } + File cert1 = new File(bindingDir, "test1.crt"); + try (PrintWriter writer = new PrintWriter(new FileWriter(cert1))) { + writer.println("---certificate one---"); + } + File cert2 = new File(bindingDir, "test2.crt"); + try (PrintWriter writer = new PrintWriter(new FileWriter(cert2))) { + writer.println("---certificate two---"); + } + } + + private void removeImage(String name) throws IOException { + ImageReference imageReference = ImageReference.of(ImageName.of(name)); + new DockerApi().image().remove(imageReference, false); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageRegistryIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageRegistryIntegrationTests.java new file mode 100644 index 000000000000..db6519025553 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageRegistryIntegrationTests.java @@ -0,0 +1,115 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.gradle.tasks.bundling; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.time.Duration; + +import org.gradle.testkit.runner.BuildResult; +import org.gradle.testkit.runner.TaskOutcome; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.TestTemplate; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.boot.buildpack.platform.docker.DockerApi; +import org.springframework.boot.buildpack.platform.docker.UpdateListener; +import org.springframework.boot.buildpack.platform.docker.type.Image; +import org.springframework.boot.buildpack.platform.docker.type.ImageReference; +import org.springframework.boot.gradle.junit.GradleCompatibility; +import org.springframework.boot.gradle.testkit.GradleBuild; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link BootBuildImage} tasks requiring a Docker image registry. + * + * @author Scott Frederick + */ +@GradleCompatibility +@Testcontainers(disabledWithoutDocker = true) +@Disabled("Disabled until differences between running locally and in CI can be diagnosed") +class BootBuildImageRegistryIntegrationTests { + + @Container + static final RegistryContainer registry = new RegistryContainer().withStartupAttempts(5) + .withStartupTimeout(Duration.ofMinutes(3)); + + String registryAddress; + + GradleBuild gradleBuild; + + @BeforeEach + void setUp() { + assertThat(registry.isRunning()).isTrue(); + this.registryAddress = registry.getHost() + ":" + registry.getFirstMappedPort(); + } + + @TestTemplate + void buildsImageAndPublishesToRegistry() throws IOException { + writeMainClass(); + String repoName = "test-image"; + String imageName = this.registryAddress + "/" + repoName; + BuildResult result = this.gradleBuild.build("bootBuildImage", "--imageName=" + imageName); + assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("Building image").contains("Successfully built image") + .contains("Pushing image '" + imageName + ":latest" + "'") + .contains("Pushed image '" + imageName + ":latest" + "'"); + ImageReference imageReference = ImageReference.of(imageName); + Image pulledImage = new DockerApi().image().pull(imageReference, UpdateListener.none()); + assertThat(pulledImage).isNotNull(); + new DockerApi().image().remove(imageReference, false); + } + + private void writeMainClass() { + File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/main/java/example"); + examplePackage.mkdirs(); + File main = new File(examplePackage, "Main.java"); + try (PrintWriter writer = new PrintWriter(new FileWriter(main))) { + writer.println("package example;"); + writer.println(); + writer.println("import java.io.IOException;"); + writer.println(); + writer.println("public class Main {"); + writer.println(); + writer.println(" public static void main(String[] args) {"); + writer.println(" }"); + writer.println(); + writer.println("}"); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + private static class RegistryContainer extends GenericContainer { + + RegistryContainer() { + super(DockerImageNames.registry()); + addExposedPorts(5000); + addEnv("SERVER_NAME", "localhost"); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java new file mode 100644 index 000000000000..cf6ca504336c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java @@ -0,0 +1,280 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.gradle.tasks.bundling; + +import java.io.File; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import org.gradle.api.GradleException; +import org.gradle.api.JavaVersion; +import org.gradle.api.Project; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.buildpack.platform.build.BuildRequest; +import org.springframework.boot.buildpack.platform.build.BuildpackReference; +import org.springframework.boot.buildpack.platform.build.PullPolicy; +import org.springframework.boot.buildpack.platform.docker.type.Binding; +import org.springframework.boot.gradle.junit.GradleProjectBuilder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link BootBuildImage}. + * + * @author Andy Wilkinson + * @author Scott Frederick + * @author Andrey Shlykov + */ +class BootBuildImageTests { + + Project project; + + private BootBuildImage buildImage; + + @BeforeEach + void setUp(@TempDir File temp) { + File projectDir = new File(temp, "project"); + projectDir.mkdirs(); + this.project = GradleProjectBuilder.builder().withProjectDir(projectDir).withName("build-image-test").build(); + this.project.setDescription("Test project for BootBuildImage"); + this.buildImage = this.project.getTasks().create("buildImage", BootBuildImage.class); + } + + @Test + void whenProjectVersionIsUnspecifiedThenItIsIgnoredWhenDerivingImageName() { + BuildRequest request = this.buildImage.createRequest(); + assertThat(request.getName().getDomain()).isEqualTo("docker.io"); + assertThat(request.getName().getName()).isEqualTo("library/build-image-test"); + assertThat(request.getName().getTag()).isEqualTo("latest"); + assertThat(request.getName().getDigest()).isNull(); + } + + @Test + void whenProjectVersionIsSpecifiedThenItIsUsedInTagOfImageName() { + this.project.setVersion("1.2.3"); + BuildRequest request = this.buildImage.createRequest(); + assertThat(request.getName().getDomain()).isEqualTo("docker.io"); + assertThat(request.getName().getName()).isEqualTo("library/build-image-test"); + assertThat(request.getName().getTag()).isEqualTo("1.2.3"); + assertThat(request.getName().getDigest()).isNull(); + } + + @Test + void whenImageNameIsSpecifiedThenItIsUsedInRequest() { + this.project.setVersion("1.2.3"); + this.buildImage.setImageName("example.com/test/build-image:1.0"); + BuildRequest request = this.buildImage.createRequest(); + assertThat(request.getName().getDomain()).isEqualTo("example.com"); + assertThat(request.getName().getName()).isEqualTo("test/build-image"); + assertThat(request.getName().getTag()).isEqualTo("1.0"); + assertThat(request.getName().getDigest()).isNull(); + } + + @Test + void springBootVersionDefaultValueIsUsed() { + BuildRequest request = this.buildImage.createRequest(); + assertThat(request.getCreator().getName()).isEqualTo("Spring Boot"); + assertThat(request.getCreator().getVersion()).isEqualTo(""); + } + + @Test + void whenIndividualEntriesAreAddedToTheEnvironmentThenTheyAreIncludedInTheRequest() { + this.buildImage.environment("ALPHA", "a"); + this.buildImage.environment("BRAVO", "b"); + assertThat(this.buildImage.createRequest().getEnv()).containsEntry("ALPHA", "a").containsEntry("BRAVO", "b") + .hasSize(2); + } + + @Test + void whenEntriesAreAddedToTheEnvironmentThenTheyAreIncludedInTheRequest() { + Map environment = new HashMap<>(); + environment.put("ALPHA", "a"); + environment.put("BRAVO", "b"); + this.buildImage.environment(environment); + assertThat(this.buildImage.createRequest().getEnv()).containsEntry("ALPHA", "a").containsEntry("BRAVO", "b") + .hasSize(2); + } + + @Test + void whenTheEnvironmentIsSetItIsIncludedInTheRequest() { + Map environment = new HashMap<>(); + environment.put("ALPHA", "a"); + environment.put("BRAVO", "b"); + this.buildImage.setEnvironment(environment); + assertThat(this.buildImage.createRequest().getEnv()).containsEntry("ALPHA", "a").containsEntry("BRAVO", "b") + .hasSize(2); + } + + @Test + void whenTheEnvironmentIsSetItReplacesAnyExistingEntriesAndIsIncludedInTheRequest() { + Map environment = new HashMap<>(); + environment.put("ALPHA", "a"); + environment.put("BRAVO", "b"); + this.buildImage.environment("C", "Charlie"); + this.buildImage.setEnvironment(environment); + assertThat(this.buildImage.createRequest().getEnv()).containsEntry("ALPHA", "a").containsEntry("BRAVO", "b") + .hasSize(2); + } + + @Test + void whenJavaVersionIsSetInEnvironmentItIsIncludedInTheRequest() { + this.buildImage.environment("BP_JVM_VERSION", "from-env"); + this.buildImage.getTargetJavaVersion().set(JavaVersion.VERSION_1_8); + assertThat(this.buildImage.createRequest().getEnv()).containsEntry("BP_JVM_VERSION", "from-env").hasSize(1); + } + + @Test + void whenTargetCompatibilityIsSetThenJavaVersionIsIncludedInTheRequest() { + this.buildImage.getTargetJavaVersion().set(JavaVersion.VERSION_1_8); + assertThat(this.buildImage.createRequest().getEnv()).containsEntry("BP_JVM_VERSION", "8.*").hasSize(1); + } + + @Test + void whenTargetCompatibilityIsSetThenJavaVersionIsAddedToEnvironment() { + this.buildImage.environment("ALPHA", "a"); + this.buildImage.getTargetJavaVersion().set(JavaVersion.VERSION_11); + assertThat(this.buildImage.createRequest().getEnv()).containsEntry("ALPHA", "a") + .containsEntry("BP_JVM_VERSION", "11.*").hasSize(2); + } + + @Test + void whenUsingDefaultConfigurationThenRequestHasVerboseLoggingDisabled() { + assertThat(this.buildImage.createRequest().isVerboseLogging()).isFalse(); + } + + @Test + void whenVerboseLoggingIsEnabledThenRequestHasVerboseLoggingEnabled() { + this.buildImage.setVerboseLogging(true); + assertThat(this.buildImage.createRequest().isVerboseLogging()).isTrue(); + } + + @Test + void whenUsingDefaultConfigurationThenRequestHasCleanCacheDisabled() { + assertThat(this.buildImage.createRequest().isCleanCache()).isFalse(); + } + + @Test + void whenCleanCacheIsEnabledThenRequestHasCleanCacheEnabled() { + this.buildImage.setCleanCache(true); + assertThat(this.buildImage.createRequest().isCleanCache()).isTrue(); + } + + @Test + void whenUsingDefaultConfigurationThenRequestHasPublishDisabled() { + assertThat(this.buildImage.createRequest().isPublish()).isFalse(); + } + + @Test + void whenPublishIsEnabledWithoutPublishRegistryThenExceptionIsThrown() { + this.buildImage.setPublish(true); + assertThatExceptionOfType(GradleException.class).isThrownBy(this.buildImage::createRequest) + .withMessageContaining("Publishing an image requires docker.publishRegistry to be configured"); + } + + @Test + void whenNoBuilderIsConfiguredThenRequestHasDefaultBuilder() { + assertThat(this.buildImage.createRequest().getBuilder().getName()).isEqualTo("paketobuildpacks/builder"); + } + + @Test + void whenBuilderIsConfiguredThenRequestUsesSpecifiedBuilder() { + this.buildImage.setBuilder("example.com/test/builder:1.2"); + assertThat(this.buildImage.createRequest().getBuilder().getName()).isEqualTo("test/builder"); + } + + @Test + void whenNoRunImageIsConfiguredThenRequestUsesDefaultRunImage() { + assertThat(this.buildImage.createRequest().getRunImage()).isNull(); + } + + @Test + void whenRunImageIsConfiguredThenRequestUsesSpecifiedRunImage() { + this.buildImage.setRunImage("example.com/test/run:1.0"); + assertThat(this.buildImage.createRequest().getRunImage().getName()).isEqualTo("test/run"); + } + + @Test + void whenUsingDefaultConfigurationThenRequestHasAlwaysPullPolicy() { + assertThat(this.buildImage.createRequest().getPullPolicy()).isEqualTo(PullPolicy.ALWAYS); + } + + @Test + void whenPullPolicyIsConfiguredThenRequestHasPullPolicy() { + this.buildImage.setPullPolicy(PullPolicy.NEVER); + assertThat(this.buildImage.createRequest().getPullPolicy()).isEqualTo(PullPolicy.NEVER); + } + + @Test + void whenNoBuildpacksAreConfiguredThenRequestUsesDefaultBuildpacks() { + assertThat(this.buildImage.createRequest().getBuildpacks()).isEmpty(); + } + + @Test + void whenBuildpacksAreConfiguredThenRequestHasBuildpacks() { + this.buildImage.setBuildpacks(Arrays.asList("example/buildpack1", "example/buildpack2")); + assertThat(this.buildImage.createRequest().getBuildpacks()).containsExactly( + BuildpackReference.of("example/buildpack1"), BuildpackReference.of("example/buildpack2")); + } + + @Test + void whenEntriesAreAddedToBuildpacksThenRequestHasBuildpacks() { + this.buildImage.buildpacks(Arrays.asList("example/buildpack1", "example/buildpack2")); + assertThat(this.buildImage.createRequest().getBuildpacks()).containsExactly( + BuildpackReference.of("example/buildpack1"), BuildpackReference.of("example/buildpack2")); + } + + @Test + void whenIndividualEntriesAreAddedToBuildpacksThenRequestHasBuildpacks() { + this.buildImage.buildpack("example/buildpack1"); + this.buildImage.buildpack("example/buildpack2"); + assertThat(this.buildImage.createRequest().getBuildpacks()).containsExactly( + BuildpackReference.of("example/buildpack1"), BuildpackReference.of("example/buildpack2")); + } + + @Test + void whenNoBindingsAreConfiguredThenRequestHasNoBindings() { + assertThat(this.buildImage.createRequest().getBindings()).isEmpty(); + } + + @Test + void whenBindingsAreConfiguredThenRequestHasBindings() { + this.buildImage.setBindings(Arrays.asList("host-src:container-dest:ro", "volume-name:container-dest:rw")); + assertThat(this.buildImage.createRequest().getBindings()) + .containsExactly(Binding.of("host-src:container-dest:ro"), Binding.of("volume-name:container-dest:rw")); + } + + @Test + void whenEntriesAreAddedToBindingsThenRequestHasBindings() { + this.buildImage.bindings(Arrays.asList("host-src:container-dest:ro", "volume-name:container-dest:rw")); + assertThat(this.buildImage.createRequest().getBindings()) + .containsExactly(Binding.of("host-src:container-dest:ro"), Binding.of("volume-name:container-dest:rw")); + } + + @Test + void whenIndividualEntriesAreAddedToBindingsThenRequestHasBindings() { + this.buildImage.binding("host-src:container-dest:ro"); + this.buildImage.binding("volume-name:container-dest:rw"); + assertThat(this.buildImage.createRequest().getBindings()) + .containsExactly(Binding.of("host-src:container-dest:ro"), Binding.of("volume-name:container-dest:rw")); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.java index 98953ecd8f94..3400dd974cf3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,15 +16,73 @@ package org.springframework.boot.gradle.tasks.bundling; +import java.io.IOException; +import java.util.Arrays; +import java.util.Set; +import java.util.TreeSet; + +import org.gradle.testkit.runner.BuildResult; +import org.gradle.testkit.runner.TaskOutcome; +import org.junit.jupiter.api.TestTemplate; + +import org.springframework.boot.gradle.junit.GradleCompatibility; + +import static org.assertj.core.api.Assertions.assertThat; + /** * Integration tests for {@link BootJar}. * * @author Andy Wilkinson + * @author Madhura Bhave + * @author Paddy Drury */ -public class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests { +@GradleCompatibility(configurationCache = true) +class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests { + + BootJarIntegrationTests() { + super("bootJar", "BOOT-INF/lib/", "BOOT-INF/classes/", "BOOT-INF/"); + } + + @TestTemplate + void whenAResolvableCopyOfAnUnresolvableConfigurationIsResolvedThenResolutionSucceeds() { + BuildResult build = this.gradleBuild.build("resolveResolvableCopyOfUnresolvableConfiguration"); + assertThat(build.task(":resolveResolvableCopyOfUnresolvableConfiguration").getOutcome()) + .isEqualTo(TaskOutcome.SUCCESS); + } + + @TestTemplate + void packagedApplicationClasspath() throws IOException { + copyClasspathApplication(); + BuildResult result = this.gradleBuild.build("launch"); + String output = result.getOutput(); + assertThat(output).containsPattern("1\\. .*classes"); + assertThat(output).containsPattern("2\\. .*library-1.0-SNAPSHOT.jar"); + assertThat(output).containsPattern("3\\. .*commons-lang3-3.9.jar"); + assertThat(output).containsPattern("4\\. .*spring-boot-jarmode-layertools.*.jar"); + assertThat(output).doesNotContain("5. "); + } + + @TestTemplate + void explodedApplicationClasspath() throws IOException { + copyClasspathApplication(); + BuildResult result = this.gradleBuild.build("launch"); + String output = result.getOutput(); + assertThat(output).containsPattern("1\\. .*classes"); + assertThat(output).containsPattern("2\\. .*spring-boot-jarmode-layertools.*.jar"); + assertThat(output).containsPattern("3\\. .*library-1.0-SNAPSHOT.jar"); + assertThat(output).containsPattern("4\\. .*commons-lang3-3.9.jar"); + assertThat(output).doesNotContain("5. "); + } + + private void copyClasspathApplication() throws IOException { + copyApplication("classpath"); + } - public BootJarIntegrationTests() { - super("bootJar"); + @Override + String[] getExpectedApplicationLayerContents(String... additionalFiles) { + Set contents = new TreeSet<>(Arrays.asList(additionalFiles)); + contents.addAll(Arrays.asList("BOOT-INF/classpath.idx", "BOOT-INF/layers.idx", "META-INF/")); + return contents.toArray(new String[0]); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarTests.java index 5fae9e8d1336..8e3bf128b4a8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,29 +19,39 @@ import java.io.File; import java.io.IOException; import java.util.jar.JarFile; +import java.util.zip.ZipEntry; +import org.gradle.api.Action; +import org.gradle.api.artifacts.Configuration; import org.junit.jupiter.api.Test; +import org.springframework.boot.testsupport.classpath.ClassPathExclusions; + import static org.assertj.core.api.Assertions.assertThat; /** * Tests for {@link BootJar}. * * @author Andy Wilkinson + * @author Madhura Bhave + * @author Scott Frederick + * @author Paddy Drury */ +@ClassPathExclusions("kotlin-daemon-client-*") class BootJarTests extends AbstractBootArchiveTests { BootJarTests() { - super(BootJar.class, "org.springframework.boot.loader.JarLauncher", "BOOT-INF/lib/", "BOOT-INF/classes/"); + super(BootJar.class, "org.springframework.boot.loader.JarLauncher", "BOOT-INF/lib/", "BOOT-INF/classes/", + "BOOT-INF/"); } @Test void contentCanBeAddedToBootInfUsingCopySpecFromGetter() throws IOException { BootJar bootJar = getTask(); - bootJar.setMainClassName("com.example.Application"); + bootJar.getMainClass().set("com.example.Application"); bootJar.getBootInf().into("test").from(new File("build.gradle").getAbsolutePath()); bootJar.copy(); - try (JarFile jarFile = new JarFile(bootJar.getArchivePath())) { + try (JarFile jarFile = new JarFile(bootJar.getArchiveFile().get().getAsFile())) { assertThat(jarFile.getJarEntry("BOOT-INF/test/build.gradle")).isNotNull(); } } @@ -49,14 +59,126 @@ void contentCanBeAddedToBootInfUsingCopySpecFromGetter() throws IOException { @Test void contentCanBeAddedToBootInfUsingCopySpecAction() throws IOException { BootJar bootJar = getTask(); - bootJar.setMainClassName("com.example.Application"); + bootJar.getMainClass().set("com.example.Application"); bootJar.bootInf((copySpec) -> copySpec.into("test").from(new File("build.gradle").getAbsolutePath())); bootJar.copy(); - try (JarFile jarFile = new JarFile(bootJar.getArchivePath())) { + try (JarFile jarFile = new JarFile(bootJar.getArchiveFile().get().getAsFile())) { assertThat(jarFile.getJarEntry("BOOT-INF/test/build.gradle")).isNotNull(); } } + @Test + void jarsInLibAreStored() throws IOException { + try (JarFile jarFile = new JarFile(createLayeredJar())) { + assertThat(jarFile.getEntry("BOOT-INF/lib/first-library.jar").getMethod()).isEqualTo(ZipEntry.STORED); + assertThat(jarFile.getEntry("BOOT-INF/lib/second-library.jar").getMethod()).isEqualTo(ZipEntry.STORED); + assertThat(jarFile.getEntry("BOOT-INF/lib/third-library-SNAPSHOT.jar").getMethod()) + .isEqualTo(ZipEntry.STORED); + assertThat(jarFile.getEntry("BOOT-INF/lib/first-project-library.jar").getMethod()) + .isEqualTo(ZipEntry.STORED); + assertThat(jarFile.getEntry("BOOT-INF/lib/second-project-library-SNAPSHOT.jar").getMethod()) + .isEqualTo(ZipEntry.STORED); + } + } + + @Test + void whenJarIsLayeredClasspathIndexPointsToLayeredLibs() throws IOException { + try (JarFile jarFile = new JarFile(createLayeredJar())) { + assertThat(entryLines(jarFile, "BOOT-INF/classpath.idx")).containsExactly( + "- \"BOOT-INF/lib/first-library.jar\"", "- \"BOOT-INF/lib/second-library.jar\"", + "- \"BOOT-INF/lib/third-library-SNAPSHOT.jar\"", "- \"BOOT-INF/lib/first-project-library.jar\"", + "- \"BOOT-INF/lib/second-project-library-SNAPSHOT.jar\""); + } + } + + @Test + void classpathIndexPointsToBootInfLibs() throws IOException { + try (JarFile jarFile = new JarFile(createPopulatedJar())) { + assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Classpath-Index")) + .isEqualTo("BOOT-INF/classpath.idx"); + assertThat(entryLines(jarFile, "BOOT-INF/classpath.idx")).containsExactly( + "- \"BOOT-INF/lib/first-library.jar\"", "- \"BOOT-INF/lib/second-library.jar\"", + "- \"BOOT-INF/lib/third-library-SNAPSHOT.jar\"", "- \"BOOT-INF/lib/first-project-library.jar\"", + "- \"BOOT-INF/lib/second-project-library-SNAPSHOT.jar\""); + } + } + + @Test + void metaInfEntryIsPackagedInTheRootOfTheArchive() throws IOException { + getTask().getMainClass().set("com.example.Main"); + File classpathDirectory = new File(this.temp, "classes"); + File metaInfEntry = new File(classpathDirectory, "META-INF/test"); + metaInfEntry.getParentFile().mkdirs(); + metaInfEntry.createNewFile(); + File applicationClass = new File(classpathDirectory, "com/example/Application.class"); + applicationClass.getParentFile().mkdirs(); + applicationClass.createNewFile(); + getTask().classpath(classpathDirectory); + executeTask(); + try (JarFile jarFile = new JarFile(getTask().getArchiveFile().get().getAsFile())) { + assertThat(jarFile.getEntry("BOOT-INF/classes/com/example/Application.class")).isNotNull(); + assertThat(jarFile.getEntry("com/example/Application.class")).isNull(); + assertThat(jarFile.getEntry("BOOT-INF/classes/META-INF/test")).isNull(); + assertThat(jarFile.getEntry("META-INF/test")).isNotNull(); + } + } + + @Test + void aopXmlIsPackagedBeneathClassesDirectory() throws IOException { + getTask().getMainClass().set("com.example.Main"); + File classpathDirectory = new File(this.temp, "classes"); + File aopXml = new File(classpathDirectory, "META-INF/aop.xml"); + aopXml.getParentFile().mkdirs(); + aopXml.createNewFile(); + File applicationClass = new File(classpathDirectory, "com/example/Application.class"); + applicationClass.getParentFile().mkdirs(); + applicationClass.createNewFile(); + getTask().classpath(classpathDirectory); + executeTask(); + try (JarFile jarFile = new JarFile(getTask().getArchiveFile().get().getAsFile())) { + assertThat(jarFile.getEntry("BOOT-INF/classes/com/example/Application.class")).isNotNull(); + assertThat(jarFile.getEntry("com/example/Application.class")).isNull(); + assertThat(jarFile.getEntry("BOOT-INF/classes/META-INF/aop.xml")).isNotNull(); + assertThat(jarFile.getEntry("META-INF/aop.xml")).isNull(); + } + } + + @Test + void kotlinModuleIsPackagedBeneathClassesDirectory() throws IOException { + getTask().getMainClass().set("com.example.Main"); + File classpathDirectory = new File(this.temp, "classes"); + File kotlinModule = new File(classpathDirectory, "META-INF/example.kotlin_module"); + kotlinModule.getParentFile().mkdirs(); + kotlinModule.createNewFile(); + File applicationClass = new File(classpathDirectory, "com/example/Application.class"); + applicationClass.getParentFile().mkdirs(); + applicationClass.createNewFile(); + getTask().classpath(classpathDirectory); + executeTask(); + try (JarFile jarFile = new JarFile(getTask().getArchiveFile().get().getAsFile())) { + assertThat(jarFile.getEntry("BOOT-INF/classes/com/example/Application.class")).isNotNull(); + assertThat(jarFile.getEntry("com/example/Application.class")).isNull(); + assertThat(jarFile.getEntry("BOOT-INF/classes/META-INF/example.kotlin_module")).isNotNull(); + assertThat(jarFile.getEntry("META-INF/example.kotlin_module")).isNull(); + } + } + + private File createPopulatedJar() throws IOException { + addContent(); + executeTask(); + return getTask().getArchiveFile().get().getAsFile(); + } + + @Override + void applyLayered(Action action) { + getTask().layered(action); + } + + @Override + void populateResolvedDependencies(Configuration configuration) { + getTask().getResolvedDependencies().processConfiguration(getTask().getProject(), configuration); + } + @Override protected void executeTask() { getTask().copy(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests.java index 730a6c27b0f8..8c29f55e1521 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,15 +16,29 @@ package org.springframework.boot.gradle.tasks.bundling; +import java.util.Arrays; +import java.util.Set; +import java.util.TreeSet; + +import org.springframework.boot.gradle.junit.GradleCompatibility; + /** - * Integration tests for {@link BootJar}. + * Integration tests for {@link BootWar}. * * @author Andy Wilkinson */ -public class BootWarIntegrationTests extends AbstractBootArchiveIntegrationTests { +@GradleCompatibility(configurationCache = true) +class BootWarIntegrationTests extends AbstractBootArchiveIntegrationTests { + + BootWarIntegrationTests() { + super("bootWar", "WEB-INF/lib/", "WEB-INF/classes/", "WEB-INF/"); + } - public BootWarIntegrationTests() { - super("bootWar"); + @Override + String[] getExpectedApplicationLayerContents(String... additionalFiles) { + Set contents = new TreeSet<>(Arrays.asList(additionalFiles)); + contents.addAll(Arrays.asList("WEB-INF/layers.idx", "META-INF/")); + return contents.toArray(new String[0]); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootWarTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootWarTests.java index 107a8272837b..6ffff4a4360d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootWarTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootWarTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,8 @@ import java.io.IOException; import java.util.jar.JarFile; +import org.gradle.api.Action; +import org.gradle.api.artifacts.Configuration; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -32,15 +34,16 @@ class BootWarTests extends AbstractBootArchiveTests { BootWarTests() { - super(BootWar.class, "org.springframework.boot.loader.WarLauncher", "WEB-INF/lib/", "WEB-INF/classes/"); + super(BootWar.class, "org.springframework.boot.loader.WarLauncher", "WEB-INF/lib/", "WEB-INF/classes/", + "WEB-INF/"); } @Test void providedClasspathJarsArePackagedInWebInfLibProvided() throws IOException { - getTask().setMainClassName("com.example.Main"); + getTask().getMainClass().set("com.example.Main"); getTask().providedClasspath(jarFile("one.jar"), jarFile("two.jar")); executeTask(); - try (JarFile jarFile = new JarFile(getTask().getArchivePath())) { + try (JarFile jarFile = new JarFile(getTask().getArchiveFile().get().getAsFile())) { assertThat(jarFile.getEntry("WEB-INF/lib-provided/one.jar")).isNotNull(); assertThat(jarFile.getEntry("WEB-INF/lib-provided/two.jar")).isNotNull(); } @@ -48,11 +51,11 @@ void providedClasspathJarsArePackagedInWebInfLibProvided() throws IOException { @Test void providedClasspathCanBeSetUsingAFileCollection() throws IOException { - getTask().setMainClassName("com.example.Main"); + getTask().getMainClass().set("com.example.Main"); getTask().providedClasspath(jarFile("one.jar")); getTask().setProvidedClasspath(getTask().getProject().files(jarFile("two.jar"))); executeTask(); - try (JarFile jarFile = new JarFile(getTask().getArchivePath())) { + try (JarFile jarFile = new JarFile(getTask().getArchiveFile().get().getAsFile())) { assertThat(jarFile.getEntry("WEB-INF/lib-provided/one.jar")).isNull(); assertThat(jarFile.getEntry("WEB-INF/lib-provided/two.jar")).isNotNull(); } @@ -60,11 +63,11 @@ void providedClasspathCanBeSetUsingAFileCollection() throws IOException { @Test void providedClasspathCanBeSetUsingAnObject() throws IOException { - getTask().setMainClassName("com.example.Main"); + getTask().getMainClass().set("com.example.Main"); getTask().providedClasspath(jarFile("one.jar")); getTask().setProvidedClasspath(jarFile("two.jar")); executeTask(); - try (JarFile jarFile = new JarFile(getTask().getArchivePath())) { + try (JarFile jarFile = new JarFile(getTask().getArchiveFile().get().getAsFile())) { assertThat(jarFile.getEntry("WEB-INF/lib-provided/one.jar")).isNull(); assertThat(jarFile.getEntry("WEB-INF/lib-provided/two.jar")).isNotNull(); } @@ -72,39 +75,25 @@ void providedClasspathCanBeSetUsingAnObject() throws IOException { @Test void devtoolsJarIsExcludedByDefaultWhenItsOnTheProvidedClasspath() throws IOException { - getTask().setMainClassName("com.example.Main"); + getTask().getMainClass().set("com.example.Main"); getTask().providedClasspath(newFile("spring-boot-devtools-0.1.2.jar")); executeTask(); - assertThat(getTask().getArchivePath()).exists(); - try (JarFile jarFile = new JarFile(getTask().getArchivePath())) { + try (JarFile jarFile = new JarFile(getTask().getArchiveFile().get().getAsFile())) { assertThat(jarFile.getEntry("WEB-INF/lib-provided/spring-boot-devtools-0.1.2.jar")).isNull(); } } - @Test - void devtoolsJarCanBeIncludedWhenItsOnTheProvidedClasspath() throws IOException { - getTask().setMainClassName("com.example.Main"); - getTask().providedClasspath(jarFile("spring-boot-devtools-0.1.2.jar")); - getTask().setExcludeDevtools(false); - executeTask(); - assertThat(getTask().getArchivePath()).exists(); - try (JarFile jarFile = new JarFile(getTask().getArchivePath())) { - assertThat(jarFile.getEntry("WEB-INF/lib-provided/spring-boot-devtools-0.1.2.jar")).isNotNull(); - } - } - @Test void webappResourcesInDirectoriesThatOverlapWithLoaderCanBePackaged() throws IOException { - File webappFolder = new File(this.temp, "src/main/webapp"); - webappFolder.mkdirs(); - File orgFolder = new File(webappFolder, "org"); - orgFolder.mkdir(); - new File(orgFolder, "foo.txt").createNewFile(); - getTask().from(webappFolder); - getTask().setMainClassName("com.example.Main"); + File webappDirectory = new File(this.temp, "src/main/webapp"); + webappDirectory.mkdirs(); + File orgDirectory = new File(webappDirectory, "org"); + orgDirectory.mkdir(); + new File(orgDirectory, "foo.txt").createNewFile(); + getTask().from(webappDirectory); + getTask().getMainClass().set("com.example.Main"); executeTask(); - assertThat(getTask().getArchivePath()).exists(); - try (JarFile jarFile = new JarFile(getTask().getArchivePath())) { + try (JarFile jarFile = new JarFile(getTask().getArchiveFile().get().getAsFile())) { assertThat(jarFile.getEntry("org/")).isNotNull(); assertThat(jarFile.getEntry("org/foo.txt")).isNotNull(); } @@ -112,12 +101,12 @@ void webappResourcesInDirectoriesThatOverlapWithLoaderCanBePackaged() throws IOE @Test void libProvidedEntriesAreWrittenAfterLibEntries() throws IOException { - getTask().setMainClassName("com.example.Main"); + getTask().getMainClass().set("com.example.Main"); getTask().classpath(jarFile("library.jar")); getTask().providedClasspath(jarFile("provided-library.jar")); executeTask(); - assertThat(getEntryNames(getTask().getArchivePath())).containsSubsequence("WEB-INF/lib/library.jar", - "WEB-INF/lib-provided/provided-library.jar"); + assertThat(getEntryNames(getTask().getArchiveFile().get().getAsFile())) + .containsSubsequence("WEB-INF/lib/library.jar", "WEB-INF/lib-provided/provided-library.jar"); } @Override @@ -125,4 +114,19 @@ protected void executeTask() { getTask().copy(); } + @Override + void populateResolvedDependencies(Configuration configuration) { + getTask().getResolvedDependencies().processConfiguration(getTask().getProject(), configuration); + } + + @Override + void applyLayered(Action action) { + getTask().layered(action); + } + + @Override + boolean archiveHasClasspathIndex() { + return false; + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/DockerSpecTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/DockerSpecTests.java new file mode 100644 index 000000000000..846bc7d6841c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/DockerSpecTests.java @@ -0,0 +1,136 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.gradle.tasks.bundling; + +import org.gradle.api.GradleException; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerHost; +import org.springframework.util.Base64Utils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link DockerSpec}. + * + * @author Wei Jiang + * @author Scott Frederick + */ +class DockerSpecTests { + + @Test + void asDockerConfigurationWithDefaults() { + DockerSpec dockerSpec = new DockerSpec(); + assertThat(dockerSpec.asDockerConfiguration().getHost()).isNull(); + assertThat(dockerSpec.asDockerConfiguration().getBuilderRegistryAuthentication()).isNull(); + assertThat(dockerSpec.asDockerConfiguration().getPublishRegistryAuthentication()).isNull(); + } + + @Test + void asDockerConfigurationWithHostConfiguration() { + DockerSpec dockerSpec = new DockerSpec(); + dockerSpec.setHost("docker.example.com"); + dockerSpec.setTlsVerify(true); + dockerSpec.setCertPath("/tmp/ca-cert"); + DockerConfiguration dockerConfiguration = dockerSpec.asDockerConfiguration(); + DockerHost host = dockerConfiguration.getHost(); + assertThat(host.getAddress()).isEqualTo("docker.example.com"); + assertThat(host.isSecure()).isEqualTo(true); + assertThat(host.getCertificatePath()).isEqualTo("/tmp/ca-cert"); + assertThat(dockerSpec.asDockerConfiguration().getBuilderRegistryAuthentication()).isNull(); + assertThat(dockerSpec.asDockerConfiguration().getPublishRegistryAuthentication()).isNull(); + } + + @Test + void asDockerConfigurationWithHostConfigurationNoTlsVerify() { + DockerSpec dockerSpec = new DockerSpec(); + dockerSpec.setHost("docker.example.com"); + DockerConfiguration dockerConfiguration = dockerSpec.asDockerConfiguration(); + DockerHost host = dockerConfiguration.getHost(); + assertThat(host.getAddress()).isEqualTo("docker.example.com"); + assertThat(host.isSecure()).isEqualTo(false); + assertThat(host.getCertificatePath()).isNull(); + assertThat(dockerSpec.asDockerConfiguration().getBuilderRegistryAuthentication()).isNull(); + assertThat(dockerSpec.asDockerConfiguration().getPublishRegistryAuthentication()).isNull(); + } + + @Test + void asDockerConfigurationWithUserAuth() { + DockerSpec dockerSpec = new DockerSpec( + new DockerSpec.DockerRegistrySpec("user1", "secret1", "https://docker1.example.com", + "docker1@example.com"), + new DockerSpec.DockerRegistrySpec("user2", "secret2", "https://docker2.example.com", + "docker2@example.com")); + DockerConfiguration dockerConfiguration = dockerSpec.asDockerConfiguration(); + assertThat(decoded(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())) + .contains("\"username\" : \"user1\"").contains("\"password\" : \"secret1\"") + .contains("\"email\" : \"docker1@example.com\"") + .contains("\"serveraddress\" : \"https://docker1.example.com\""); + assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader())) + .contains("\"username\" : \"user2\"").contains("\"password\" : \"secret2\"") + .contains("\"email\" : \"docker2@example.com\"") + .contains("\"serveraddress\" : \"https://docker2.example.com\""); + assertThat(dockerSpec.asDockerConfiguration().getHost()).isNull(); + } + + @Test + void asDockerConfigurationWithIncompleteBuilderUserAuthFails() { + DockerSpec.DockerRegistrySpec builderRegistry = new DockerSpec.DockerRegistrySpec("user", null, + "https://docker.example.com", "docker@example.com"); + DockerSpec dockerSpec = new DockerSpec(builderRegistry, null); + assertThatExceptionOfType(GradleException.class).isThrownBy(dockerSpec::asDockerConfiguration) + .withMessageContaining("Invalid Docker builder registry configuration"); + } + + @Test + void asDockerConfigurationWithIncompletePublishUserAuthFails() { + DockerSpec.DockerRegistrySpec publishRegistry = new DockerSpec.DockerRegistrySpec("user2", null, + "https://docker2.example.com", "docker2@example.com"); + DockerSpec dockerSpec = new DockerSpec(null, publishRegistry); + assertThatExceptionOfType(GradleException.class).isThrownBy(dockerSpec::asDockerConfiguration) + .withMessageContaining("Invalid Docker publish registry configuration"); + } + + @Test + void asDockerConfigurationWithTokenAuth() { + DockerSpec dockerSpec = new DockerSpec(new DockerSpec.DockerRegistrySpec("token1"), + new DockerSpec.DockerRegistrySpec("token2")); + DockerConfiguration dockerConfiguration = dockerSpec.asDockerConfiguration(); + assertThat(decoded(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())) + .contains("\"identitytoken\" : \"token1\""); + assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader())) + .contains("\"identitytoken\" : \"token2\""); + } + + @Test + void asDockerConfigurationWithUserAndTokenAuthFails() { + DockerSpec.DockerRegistrySpec builderRegistry = new DockerSpec.DockerRegistrySpec(); + builderRegistry.setUsername("user"); + builderRegistry.setPassword("secret"); + builderRegistry.setToken("token"); + DockerSpec dockerSpec = new DockerSpec(builderRegistry, null); + assertThatExceptionOfType(GradleException.class).isThrownBy(dockerSpec::asDockerConfiguration) + .withMessageContaining("Invalid Docker builder registry configuration"); + } + + String decoded(String value) { + return new String(Base64Utils.decodeFromString(value)); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/LaunchScriptConfigurationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/LaunchScriptConfigurationTests.java index bbff7099e922..04d3134f356c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/LaunchScriptConfigurationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/LaunchScriptConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.boot.gradle.tasks.bundling; import org.gradle.api.Project; +import org.gradle.api.provider.Property; import org.gradle.api.tasks.bundling.AbstractArchiveTask; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -43,7 +44,8 @@ void setUp() { @Test void initInfoProvidesUsesArchiveBaseNameByDefault() { - given(this.task.getBaseName()).willReturn("base-name"); + Property baseName = stringProperty("base-name"); + given(this.task.getArchiveBaseName()).willReturn(baseName); assertThat(new LaunchScriptConfiguration(this.task).getProperties()).containsEntry("initInfoProvides", "base-name"); } @@ -51,13 +53,16 @@ void initInfoProvidesUsesArchiveBaseNameByDefault() { @Test void initInfoShortDescriptionUsesDescriptionByDefault() { given(this.project.getDescription()).willReturn("Project description"); + Property baseName = stringProperty("base-name"); + given(this.task.getArchiveBaseName()).willReturn(baseName); assertThat(new LaunchScriptConfiguration(this.task).getProperties()).containsEntry("initInfoShortDescription", "Project description"); } @Test void initInfoShortDescriptionUsesArchiveBaseNameWhenDescriptionIsNull() { - given(this.task.getBaseName()).willReturn("base-name"); + Property baseName = stringProperty("base-name"); + given(this.task.getArchiveBaseName()).willReturn(baseName); assertThat(new LaunchScriptConfiguration(this.task).getProperties()).containsEntry("initInfoShortDescription", "base-name"); } @@ -65,13 +70,16 @@ void initInfoShortDescriptionUsesArchiveBaseNameWhenDescriptionIsNull() { @Test void initInfoShortDescriptionUsesSingleLineVersionOfMultiLineProjectDescription() { given(this.project.getDescription()).willReturn("Project\ndescription"); + Property baseName = stringProperty("base-name"); + given(this.task.getArchiveBaseName()).willReturn(baseName); assertThat(new LaunchScriptConfiguration(this.task).getProperties()).containsEntry("initInfoShortDescription", "Project description"); } @Test void initInfoDescriptionUsesArchiveBaseNameWhenDescriptionIsNull() { - given(this.task.getBaseName()).willReturn("base-name"); + Property baseName = stringProperty("base-name"); + given(this.task.getArchiveBaseName()).willReturn(baseName); assertThat(new LaunchScriptConfiguration(this.task).getProperties()).containsEntry("initInfoDescription", "base-name"); } @@ -79,6 +87,8 @@ void initInfoDescriptionUsesArchiveBaseNameWhenDescriptionIsNull() { @Test void initInfoDescriptionUsesProjectDescriptionByDefault() { given(this.project.getDescription()).willReturn("Project description"); + Property baseName = stringProperty("base-name"); + given(this.task.getArchiveBaseName()).willReturn(baseName); assertThat(new LaunchScriptConfiguration(this.task).getProperties()).containsEntry("initInfoDescription", "Project description"); } @@ -86,8 +96,17 @@ void initInfoDescriptionUsesProjectDescriptionByDefault() { @Test void initInfoDescriptionUsesCorrectlyFormattedMultiLineProjectDescription() { given(this.project.getDescription()).willReturn("The\nproject\ndescription"); + Property baseName = stringProperty("base-name"); + given(this.task.getArchiveBaseName()).willReturn(baseName); assertThat(new LaunchScriptConfiguration(this.task).getProperties()).containsEntry("initInfoDescription", "The\n# project\n# description"); } + @SuppressWarnings("unchecked") + private Property stringProperty(String value) { + Property property = mock(Property.class); + given(property.get()).willReturn(value); + return property; + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/MavenIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/MavenIntegrationTests.java index a2d6e344c97b..975c63c8f4ca 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/MavenIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/MavenIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,15 +17,14 @@ package org.springframework.boot.gradle.tasks.bundling; import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; import org.gradle.testkit.runner.BuildResult; import org.gradle.testkit.runner.TaskOutcome; import org.junit.jupiter.api.TestTemplate; -import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.condition.DisabledForJreRange; +import org.junit.jupiter.api.condition.JRE; -import org.springframework.boot.gradle.junit.GradleCompatibilityExtension; +import org.springframework.boot.gradle.junit.GradleCompatibility; import org.springframework.boot.gradle.testkit.GradleBuild; import static org.assertj.core.api.Assertions.assertThat; @@ -35,14 +34,16 @@ * * @author Andy Wilkinson */ -@ExtendWith(GradleCompatibilityExtension.class) -public class MavenIntegrationTests { +@DisabledForJreRange(min = JRE.JAVA_16) +@GradleCompatibility(versionsLessThan = "7.0-milestone-1") +class MavenIntegrationTests { GradleBuild gradleBuild; @TestTemplate - public void bootJarCanBeUploaded() throws FileNotFoundException, IOException { - BuildResult result = this.gradleBuild.build("uploadBootArchives"); + void bootJarCanBeUploaded() { + BuildResult result = this.gradleBuild.expectDeprecationWarningsWithAtLeastVersion("6.0.0") + .build("uploadBootArchives"); assertThat(result.task(":uploadBootArchives").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); assertThat(artifactWithSuffix("jar")).isFile(); assertThat(artifactWithSuffix("pom")).is(pomWith().groupId("com.example") @@ -50,8 +51,9 @@ public void bootJarCanBeUploaded() throws FileNotFoundException, IOException { } @TestTemplate - public void bootWarCanBeUploaded() throws IOException { - BuildResult result = this.gradleBuild.build("uploadBootArchives"); + void bootWarCanBeUploaded() { + BuildResult result = this.gradleBuild.expectDeprecationWarningsWithAtLeastVersion("6.0.0") + .build("uploadBootArchives"); assertThat(result.task(":uploadBootArchives").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); assertThat(artifactWithSuffix("war")).isFile(); assertThat(artifactWithSuffix("pom")) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/MavenPublishingIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/MavenPublishingIntegrationTests.java index 0bfbe2513042..2571e1e25614 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/MavenPublishingIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/MavenPublishingIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,15 +17,12 @@ package org.springframework.boot.gradle.tasks.bundling; import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; import org.gradle.testkit.runner.BuildResult; import org.gradle.testkit.runner.TaskOutcome; import org.junit.jupiter.api.TestTemplate; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.boot.gradle.junit.GradleCompatibilityExtension; +import org.springframework.boot.gradle.junit.GradleCompatibility; import org.springframework.boot.gradle.testkit.GradleBuild; import static org.assertj.core.api.Assertions.assertThat; @@ -36,13 +33,13 @@ * * @author Andy Wilkinson */ -@ExtendWith(GradleCompatibilityExtension.class) -public class MavenPublishingIntegrationTests { +@GradleCompatibility +class MavenPublishingIntegrationTests { GradleBuild gradleBuild; @TestTemplate - public void bootJarCanBePublished() throws FileNotFoundException, IOException { + void bootJarCanBePublished() { BuildResult result = this.gradleBuild.build("publish"); assertThat(result.task(":publish").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); assertThat(artifactWithSuffix("jar")).isFile(); @@ -51,7 +48,7 @@ public void bootJarCanBePublished() throws FileNotFoundException, IOException { } @TestTemplate - public void bootWarCanBePublished() throws IOException { + void bootWarCanBePublished() { BuildResult result = this.gradleBuild.build("publish"); assertThat(result.task(":publish").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); assertThat(artifactWithSuffix("war")).isFile(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests.java index 4ba86a6d8ec9..ac753ed56b47 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,14 +17,19 @@ package org.springframework.boot.gradle.tasks.run; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; +import java.util.function.Consumer; +import java.util.jar.Attributes; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import org.gradle.api.JavaVersion; import org.gradle.testkit.runner.BuildResult; import org.gradle.testkit.runner.TaskOutcome; import org.junit.jupiter.api.TestTemplate; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.boot.gradle.junit.GradleCompatibilityExtension; +import org.springframework.boot.gradle.junit.GradleCompatibility; import org.springframework.boot.gradle.testkit.GradleBuild; import org.springframework.util.FileSystemUtils; @@ -35,13 +40,13 @@ * * @author Andy Wilkinson */ -@ExtendWith(GradleCompatibilityExtension.class) -public class BootRunIntegrationTests { +@GradleCompatibility(configurationCache = true) +class BootRunIntegrationTests { GradleBuild gradleBuild; @TestTemplate - public void basicExecution() throws IOException { + void basicExecution() throws IOException { copyClasspathApplication(); new File(this.gradleBuild.getProjectDir(), "src/main/resources").mkdirs(); BuildResult result = this.gradleBuild.build("bootRun"); @@ -52,7 +57,7 @@ public void basicExecution() throws IOException { } @TestTemplate - public void sourceResourcesCanBeUsed() throws IOException { + void sourceResourcesCanBeUsed() throws IOException { copyClasspathApplication(); BuildResult result = this.gradleBuild.build("bootRun"); assertThat(result.task(":bootRun").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); @@ -62,37 +67,45 @@ public void sourceResourcesCanBeUsed() throws IOException { } @TestTemplate - public void springBootExtensionMainClassNameIsUsed() throws IOException { - BuildResult result = this.gradleBuild.build("echoMainClassName"); - assertThat(result.task(":echoMainClassName").getOutcome()).isEqualTo(TaskOutcome.UP_TO_DATE); - assertThat(result.getOutput()).contains("Main class name = com.example.CustomMainClass"); + void springBootExtensionMainClassNameIsUsed() throws IOException { + copyMainClassApplication(); + BuildResult result = this.gradleBuild.build("bootRun"); + assertThat(result.task(":bootRun").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("com.example.bootrun.main.CustomMainClass"); } @TestTemplate - public void applicationPluginMainClassNameIsUsed() throws IOException { - BuildResult result = this.gradleBuild.build("echoMainClassName"); - assertThat(result.task(":echoMainClassName").getOutcome()).isEqualTo(TaskOutcome.UP_TO_DATE); - assertThat(result.getOutput()).contains("Main class name = com.example.CustomMainClass"); + void applicationPluginMainClassNameIsUsed() throws IOException { + copyMainClassApplication(); + BuildResult result = this.gradleBuild.build("bootRun"); + assertThat(result.task(":bootRun").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("com.example.bootrun.main.CustomMainClass"); } @TestTemplate - public void applicationPluginMainClassNameIsNotUsedWhenItIsNull() throws IOException { + void applicationPluginMainClassNameIsNotUsedWhenItIsNull() throws IOException { copyClasspathApplication(); - BuildResult result = this.gradleBuild.build("echoMainClassName"); - assertThat(result.task(":echoMainClassName").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); - assertThat(result.getOutput()).contains("Main class name = com.example.classpath.BootRunClasspathApplication"); + BuildResult result = this.gradleBuild.build("bootRun"); + assertThat(result.task(":bootRun").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()) + .contains("Main class name = com.example.bootrun.classpath.BootRunClasspathApplication"); } @TestTemplate - public void defaultJvmArgs() throws IOException { + void defaultJvmArgs() throws IOException { copyJvmArgsApplication(); BuildResult result = this.gradleBuild.build("bootRun"); assertThat(result.task(":bootRun").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); - assertThat(result.getOutput()).contains("1. -Xverify:none").contains("2. -XX:TieredStopAtLevel=1"); + if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_13)) { + assertThat(result.getOutput()).contains("1. -XX:TieredStopAtLevel=1"); + } + else { + assertThat(result.getOutput()).contains("1. -Xverify:none").contains("2. -XX:TieredStopAtLevel=1"); + } } @TestTemplate - public void optimizedLaunchDisabledJvmArgs() throws IOException { + void optimizedLaunchDisabledJvmArgs() throws IOException { copyJvmArgsApplication(); BuildResult result = this.gradleBuild.build("bootRun"); assertThat(result.task(":bootRun").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); @@ -100,12 +113,43 @@ public void optimizedLaunchDisabledJvmArgs() throws IOException { } @TestTemplate - public void applicationPluginJvmArgumentsAreUsed() throws IOException { + void applicationPluginJvmArgumentsAreUsed() throws IOException { copyJvmArgsApplication(); BuildResult result = this.gradleBuild.build("bootRun"); assertThat(result.task(":bootRun").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); - assertThat(result.getOutput()).contains("1. -Dcom.bar=baz").contains("2. -Dcom.foo=bar") - .contains("3. -Xverify:none").contains("4. -XX:TieredStopAtLevel=1"); + if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_13)) { + assertThat(result.getOutput()).contains("1. -Dcom.bar=baz").contains("2. -Dcom.foo=bar") + .contains("3. -XX:TieredStopAtLevel=1"); + } + else { + assertThat(result.getOutput()).contains("1. -Dcom.bar=baz").contains("2. -Dcom.foo=bar") + .contains("3. -Xverify:none").contains("4. -XX:TieredStopAtLevel=1"); + } + } + + @TestTemplate + void jarTypeFilteringIsAppliedToTheClasspath() throws IOException { + copyClasspathApplication(); + File flatDirRepository = new File(this.gradleBuild.getProjectDir(), "repository"); + createDependenciesStarterJar(new File(flatDirRepository, "starter.jar")); + createStandardJar(new File(flatDirRepository, "standard.jar")); + BuildResult result = this.gradleBuild.build("bootRun"); + assertThat(result.task(":bootRun").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("standard.jar").doesNotContain("starter.jar"); + } + + @TestTemplate + void classesFromASecondarySourceSetCanBeOnTheClasspath() throws IOException { + File output = new File(this.gradleBuild.getProjectDir(), "src/secondary/java/com/example/bootrun/main"); + output.mkdirs(); + FileSystemUtils.copyRecursively(new File("src/test/java/com/example/bootrun/main"), output); + BuildResult result = this.gradleBuild.build("bootRun"); + assertThat(result.task(":bootRun").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("com.example.bootrun.main.CustomMainClass"); + } + + private void copyMainClassApplication() throws IOException { + copyApplication("main"); } private void copyClasspathApplication() throws IOException { @@ -117,13 +161,31 @@ private void copyJvmArgsApplication() throws IOException { } private void copyApplication(String name) throws IOException { - File output = new File(this.gradleBuild.getProjectDir(), "src/main/java/com/example/" + name); + File output = new File(this.gradleBuild.getProjectDir(), "src/main/java/com/example/bootrun/" + name); output.mkdirs(); - FileSystemUtils.copyRecursively(new File("src/test/java/com/example/" + name), output); + FileSystemUtils.copyRecursively(new File("src/test/java/com/example/bootrun/" + name), output); } private String canonicalPathOf(String path) throws IOException { return new File(this.gradleBuild.getProjectDir(), path).getCanonicalPath(); } + private void createStandardJar(File location) throws IOException { + createJar(location, (attributes) -> { + }); + } + + private void createDependenciesStarterJar(File location) throws IOException { + createJar(location, (attributes) -> attributes.putValue("Spring-Boot-Jar-Type", "dependencies-starter")); + } + + private void createJar(File location, Consumer attributesConfigurer) throws IOException { + location.getParentFile().mkdirs(); + Manifest manifest = new Manifest(); + Attributes attributes = manifest.getMainAttributes(); + attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0"); + attributesConfigurer.accept(attributes); + new JarOutputStream(new FileOutputStream(location), manifest).close(); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/Dsl.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/Dsl.java index 28463b807772..ac352bf03196 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/Dsl.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/Dsl.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.boot.gradle.testkit; /** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/GradleBuild.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/GradleBuild.java index 6978bd825053..d73f5857d4de 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/GradleBuild.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/GradleBuild.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,26 +24,40 @@ import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import java.util.jar.JarFile; +import com.fasterxml.jackson.annotation.JsonView; +import com.fasterxml.jackson.core.Versioned; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; +import com.sun.jna.Platform; import io.spring.gradle.dependencymanagement.DependencyManagementPlugin; import io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension; +import org.antlr.v4.runtime.Lexer; import org.apache.commons.compress.archivers.ArchiveEntry; +import org.apache.http.HttpRequest; +import org.apache.http.conn.HttpClientConnectionManager; import org.gradle.testkit.runner.BuildResult; import org.gradle.testkit.runner.GradleRunner; -import org.jetbrains.kotlin.cli.common.PropertiesKt; -import org.jetbrains.kotlin.compilerRunner.KotlinLogger; -import org.jetbrains.kotlin.daemon.client.KotlinCompilerClient; +import org.gradle.util.GradleVersion; import org.jetbrains.kotlin.gradle.model.KotlinProject; -import org.jetbrains.kotlin.gradle.plugin.KotlinGradleSubplugin; +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilerPluginSupportPlugin; import org.jetbrains.kotlin.gradle.plugin.KotlinPlugin; +import org.jetbrains.kotlin.project.model.LanguageSettings; +import org.tomlj.Toml; import org.springframework.asm.ClassVisitor; +import org.springframework.boot.buildpack.platform.build.BuildRequest; import org.springframework.boot.loader.tools.LaunchScript; import org.springframework.util.FileCopyUtils; import org.springframework.util.FileSystemUtils; +import static org.assertj.core.api.Assertions.assertThat; + /** * A {@code GradleBuild} is used to run a Gradle build using {@link GradleRunner}. * @@ -59,8 +73,16 @@ public class GradleBuild { private String gradleVersion; + private GradleVersion expectDeprecationWarnings; + + private boolean configurationCache = false; + + private Map scriptProperties = new HashMap<>(); + public GradleBuild() { this(Dsl.GROOVY); + this.scriptProperties.put("bootVersion", getBootVersion()); + this.scriptProperties.put("dependencyManagementPluginVersion", getDependencyManagementPluginVersion()); } public GradleBuild(Dsl dsl) { @@ -76,19 +98,37 @@ void before() throws IOException { } void after() { - GradleBuild.this.script = null; + this.script = null; FileSystemUtils.deleteRecursively(this.projectDir); } private List pluginClasspath() { - return Arrays.asList(new File("bin"), new File("build/classes/java/main"), new File("build/resources/main"), - new File(pathOfJarContaining(LaunchScript.class)), new File(pathOfJarContaining(ClassVisitor.class)), + return Arrays.asList(new File("bin/main"), new File("build/classes/java/main"), + new File("build/resources/main"), new File(pathOfJarContaining(LaunchScript.class)), + new File(pathOfJarContaining(ClassVisitor.class)), new File(pathOfJarContaining(DependencyManagementPlugin.class)), - new File(pathOfJarContaining(PropertiesKt.class)), new File(pathOfJarContaining(KotlinLogger.class)), + new File(pathOfJarContaining("org.jetbrains.kotlin.cli.common.PropertiesKt")), + new File(pathOfJarContaining("org.jetbrains.kotlin.compilerRunner.KotlinLogger")), new File(pathOfJarContaining(KotlinPlugin.class)), new File(pathOfJarContaining(KotlinProject.class)), - new File(pathOfJarContaining(KotlinCompilerClient.class)), - new File(pathOfJarContaining(KotlinGradleSubplugin.class)), - new File(pathOfJarContaining(ArchiveEntry.class))); + new File(pathOfJarContaining("org.jetbrains.kotlin.daemon.client.KotlinCompilerClient")), + new File(pathOfJarContaining(KotlinCompilerPluginSupportPlugin.class)), + new File(pathOfJarContaining(LanguageSettings.class)), + new File(pathOfJarContaining(ArchiveEntry.class)), new File(pathOfJarContaining(BuildRequest.class)), + new File(pathOfJarContaining(HttpClientConnectionManager.class)), + new File(pathOfJarContaining(HttpRequest.class)), new File(pathOfJarContaining(Module.class)), + new File(pathOfJarContaining(Versioned.class)), + new File(pathOfJarContaining(ParameterNamesModule.class)), + new File(pathOfJarContaining(JsonView.class)), new File(pathOfJarContaining(Platform.class)), + new File(pathOfJarContaining(Toml.class)), new File(pathOfJarContaining(Lexer.class))); + } + + private String pathOfJarContaining(String className) { + try { + return pathOfJarContaining(Class.forName(className)); + } + catch (ClassNotFoundException ex) { + throw new IllegalArgumentException(ex); + } } private String pathOfJarContaining(Class type) { @@ -100,9 +140,33 @@ public GradleBuild script(String script) { return this; } + public GradleBuild expectDeprecationWarningsWithAtLeastVersion(String gradleVersion) { + this.expectDeprecationWarnings = GradleVersion.version(gradleVersion); + return this; + } + + public GradleBuild configurationCache() { + this.configurationCache = true; + return this; + } + + public boolean isConfigurationCache() { + return this.configurationCache; + } + + public GradleBuild scriptProperty(String key, String value) { + this.scriptProperties.put(key, value); + return this; + } + public BuildResult build(String... arguments) { try { - return prepareRunner(arguments).build(); + BuildResult result = prepareRunner(arguments).build(); + if (this.expectDeprecationWarnings == null || (this.gradleVersion != null + && this.expectDeprecationWarnings.compareTo(GradleVersion.version(this.gradleVersion)) > 0)) { + assertThat(result.getOutput()).doesNotContain("Deprecated").doesNotContain("deprecated"); + } + return result; } catch (Exception ex) { throw new RuntimeException(ex); @@ -119,31 +183,42 @@ public BuildResult buildAndFail(String... arguments) { } public GradleRunner prepareRunner(String... arguments) throws IOException { - String scriptContent = FileCopyUtils.copyToString(new FileReader(this.script)) - .replace("{version}", getBootVersion()) - .replace("{dependency-management-plugin-version}", getDependencyManagementPluginVersion()); + String scriptContent = FileCopyUtils.copyToString(new FileReader(this.script)); + for (Entry property : this.scriptProperties.entrySet()) { + scriptContent = scriptContent.replace("{" + property.getKey() + "}", property.getValue()); + } FileCopyUtils.copy(scriptContent, new FileWriter(new File(this.projectDir, "build" + this.dsl.getExtension()))); FileSystemUtils.copyRecursively(new File("src/test/resources/repository"), new File(this.projectDir, "repository")); GradleRunner gradleRunner = GradleRunner.create().withProjectDir(this.projectDir) .withPluginClasspath(pluginClasspath()); - if (this.dsl != Dsl.KOTLIN) { + if (this.dsl != Dsl.KOTLIN && !this.configurationCache) { // see https://github.com/gradle/gradle/issues/6862 gradleRunner.withDebug(true); } if (this.gradleVersion != null) { gradleRunner.withGradleVersion(this.gradleVersion); } - else if (this.dsl == Dsl.KOTLIN) { - gradleRunner.withGradleVersion("4.10.3"); - } + gradleRunner.withTestKitDir(getTestKitDir()); List allArguments = new ArrayList<>(); allArguments.add("-PbootVersion=" + getBootVersion()); allArguments.add("--stacktrace"); allArguments.addAll(Arrays.asList(arguments)); + allArguments.add("--warning-mode"); + allArguments.add("all"); + if (this.configurationCache) { + allArguments.add("--configuration-cache"); + } return gradleRunner.withArguments(allArguments); } + private File getTestKitDir() { + File temp = new File(System.getProperty("java.io.tmpdir")); + String username = System.getProperty("user.name"); + String gradleVersion = (this.gradleVersion != null) ? this.gradleVersion : "default"; + return new File(temp, ".gradle-test-kit-" + username + "-" + getBootVersion() + "-" + gradleVersion); + } + public File getProjectDir() { return this.projectDir; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests-jarWithCustomName.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests-jarWithCustomName.gradle index 5abe1e05195c..04953cce3f59 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests-jarWithCustomName.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests-jarWithCustomName.gradle @@ -1,3 +1,5 @@ +import org.gradle.util.GradleVersion + plugins { id 'java' id 'org.springframework.boot' version '{version}' @@ -7,7 +9,12 @@ group = 'com.example' version = '1.0' bootJar { - baseName = 'foo' + if (GradleVersion.current().compareTo(GradleVersion.version('6.0.0')) < 0) { + baseName = 'foo' + } + else { + archiveBaseName = 'foo' + } } springBoot { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests-warWithCustomName.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests-warWithCustomName.gradle index 9239a0a84f7e..611946af9121 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests-warWithCustomName.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests-warWithCustomName.gradle @@ -1,3 +1,5 @@ +import org.gradle.util.GradleVersion + plugins { id 'war' id 'org.springframework.boot' version '{version}' @@ -7,7 +9,12 @@ group = 'com.example' version = '1.0' bootWar { - baseName = 'foo' + if (GradleVersion.current().compareTo(GradleVersion.version('6.0.0')) < 0) { + baseName = 'foo' + } + else { + archiveBaseName = 'foo' + } } springBoot { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests-applicationNameCanBeUsedToCustomizeDistributionName.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests-applicationNameCanBeUsedToCustomizeDistributionName.gradle index 6120cf651201..53ef2f261e1c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests-applicationNameCanBeUsedToCustomizeDistributionName.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests-applicationNameCanBeUsedToCustomizeDistributionName.gradle @@ -7,5 +7,5 @@ plugins { applicationName = 'custom' bootJar { - mainClassName = 'com.example.ExampleApplication' + mainClass = 'com.example.ExampleApplication' } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests-scriptsHaveCorrectPermissions.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests-scriptsHaveCorrectPermissions.gradle index 74450beaa862..7446695620dd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests-scriptsHaveCorrectPermissions.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests-scriptsHaveCorrectPermissions.gradle @@ -5,5 +5,5 @@ plugins { } bootJar { - mainClassName = 'com.example.ExampleApplication' + mainClass = 'com.example.ExampleApplication' } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests-tarDistributionForJarCanBeBuilt.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests-tarDistributionForJarCanBeBuilt.gradle index 74450beaa862..7446695620dd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests-tarDistributionForJarCanBeBuilt.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests-tarDistributionForJarCanBeBuilt.gradle @@ -5,5 +5,5 @@ plugins { } bootJar { - mainClassName = 'com.example.ExampleApplication' + mainClass = 'com.example.ExampleApplication' } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests-tarDistributionForWarCanBeBuilt.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests-tarDistributionForWarCanBeBuilt.gradle index 71b35f403416..f62326968272 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests-tarDistributionForWarCanBeBuilt.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests-tarDistributionForWarCanBeBuilt.gradle @@ -5,5 +5,5 @@ plugins { } bootWar { - mainClassName = 'com.example.ExampleApplication' + mainClass = 'com.example.ExampleApplication' } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests-taskConfigurationIsAvoided.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests-taskConfigurationIsAvoided.gradle new file mode 100644 index 000000000000..5e38257d3e83 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests-taskConfigurationIsAvoided.gradle @@ -0,0 +1,9 @@ +plugins { + id 'org.springframework.boot' version '{version}' + id 'java' + id 'application' +} + +tasks.configureEach { + println "Configuring ${it.path}" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests-zipDistributionForJarCanBeBuilt.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests-zipDistributionForJarCanBeBuilt.gradle index 74450beaa862..7446695620dd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests-zipDistributionForJarCanBeBuilt.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests-zipDistributionForJarCanBeBuilt.gradle @@ -5,5 +5,5 @@ plugins { } bootJar { - mainClassName = 'com.example.ExampleApplication' + mainClass = 'com.example.ExampleApplication' } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests-zipDistributionForWarCanBeBuilt.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests-zipDistributionForWarCanBeBuilt.gradle index 71b35f403416..f62326968272 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests-zipDistributionForWarCanBeBuilt.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests-zipDistributionForWarCanBeBuilt.gradle @@ -5,5 +5,5 @@ plugins { } bootWar { - mainClassName = 'com.example.ExampleApplication' + mainClass = 'com.example.ExampleApplication' } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests.gradle index bf327cfdb7db..8a6084325fbf 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ApplicationPluginActionIntegrationTests.gradle @@ -9,7 +9,7 @@ if (project.hasProperty('applyApplicationPlugin')) { task('taskExists') { doFirst { - println "$taskName exists = ${tasks.findByName(taskName) != null}" + println "${taskName} exists = ${tasks.findByName(taskName) != null}" } } @@ -17,21 +17,21 @@ task('distributionExists') { doFirst { boolean found = project.hasProperty('distributions') && distributions.findByName(distributionName) != null - println "$distributionName exists = $found" + println "${distributionName} exists = ${found}" } } task('javaCompileEncoding') { doFirst { tasks.withType(JavaCompile) { - println "$name = ${options.encoding}" + println "${name} = ${options.encoding}" } } } task('startScriptsDefaultJvmOpts') { doFirst { - tasks.withType(org.springframework.boot.gradle.tasks.application.CreateBootStartScripts) { + tasks.getByName("bootStartScripts") { println "$name defaultJvmOpts = $defaultJvmOpts" } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/DependencyManagementPluginActionIntegrationTests-helpfulErrorWhenVersionlessDependencyFailsToResolve.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/DependencyManagementPluginActionIntegrationTests-helpfulErrorWhenVersionlessDependencyFailsToResolve.gradle index 2004d24e14ce..63ced9d0597c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/DependencyManagementPluginActionIntegrationTests-helpfulErrorWhenVersionlessDependencyFailsToResolve.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/DependencyManagementPluginActionIntegrationTests-helpfulErrorWhenVersionlessDependencyFailsToResolve.gradle @@ -4,5 +4,5 @@ plugins { } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-web' + implementation('org.springframework.boot:spring-boot-starter-web') } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-additionalMetadataLocationsConfiguredWhenProcessorIsPresent.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-additionalMetadataLocationsConfiguredWhenProcessorIsPresent.gradle index 1e8a4e27d9b5..3fd83366a983 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-additionalMetadataLocationsConfiguredWhenProcessorIsPresent.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-additionalMetadataLocationsConfiguredWhenProcessorIsPresent.gradle @@ -14,6 +14,6 @@ dependencies { compileJava { doLast { - println "$name compiler args: ${options.compilerArgs}" + println "${name} compiler args: ${options.compilerArgs}" } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-additionalMetadataLocationsNotConfiguredWhenProcessorIsAbsent.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-additionalMetadataLocationsNotConfiguredWhenProcessorIsAbsent.gradle index 5fd735724db9..903c34925d96 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-additionalMetadataLocationsNotConfiguredWhenProcessorIsAbsent.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-additionalMetadataLocationsNotConfiguredWhenProcessorIsAbsent.gradle @@ -5,6 +5,6 @@ plugins { compileJava { doLast { - println "$name compiler args: ${options.compilerArgs}" + println "${name} compiler args: ${options.compilerArgs}" } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-assembleRunsBootJarAndJar.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-assembleRunsBootJarAndJar.gradle new file mode 100644 index 000000000000..be0e82ef6d85 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-assembleRunsBootJarAndJar.gradle @@ -0,0 +1,8 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +bootJar { + mainClass = 'com.example.Application' +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-jarAndBootJarCanBothBeBuilt.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-jarAndBootJarCanBothBeBuilt.gradle deleted file mode 100644 index 1d3126a70ecd..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-jarAndBootJarCanBothBeBuilt.gradle +++ /dev/null @@ -1,13 +0,0 @@ -plugins { - id 'java' - id 'org.springframework.boot' version '{version}' -} - -bootJar { - mainClassName = 'com.example.Application' - classifier = 'boot' -} - -jar { - enabled = true -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-javaCompileTasksCanOverrideDefaultParametersCompilerFlag.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-javaCompileTasksCanOverrideDefaultParametersCompilerFlag.gradle index e60a89436c7c..0d4893bca274 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-javaCompileTasksCanOverrideDefaultParametersCompilerFlag.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-javaCompileTasksCanOverrideDefaultParametersCompilerFlag.gradle @@ -11,7 +11,7 @@ tasks.withType(JavaCompile) { task('javaCompileTasksCompilerArgs') { doFirst { tasks.withType(JavaCompile) { - println "$name compiler args: ${options.compilerArgs}" + println "${name} compiler args: ${options.compilerArgs}" } } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-javaCompileTasksUseParametersAndAdditionalCompilerFlags.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-javaCompileTasksUseParametersAndAdditionalCompilerFlags.gradle index b93b99bfa26b..f018b3a2a491 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-javaCompileTasksUseParametersAndAdditionalCompilerFlags.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-javaCompileTasksUseParametersAndAdditionalCompilerFlags.gradle @@ -10,7 +10,7 @@ tasks.withType(JavaCompile) { task('javaCompileTasksCompilerArgs') { doFirst { tasks.withType(JavaCompile) { - println "$name compiler args: ${options.compilerArgs}" + println "${name} compiler args: ${options.compilerArgs}" } } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-javaCompileTasksUseParametersCompilerFlagByDefault.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-javaCompileTasksUseParametersCompilerFlagByDefault.gradle index 5f177371c390..ed3eb61ff925 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-javaCompileTasksUseParametersCompilerFlagByDefault.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-javaCompileTasksUseParametersCompilerFlagByDefault.gradle @@ -6,7 +6,7 @@ plugins { task('javaCompileTasksCompilerArgs') { doFirst { tasks.withType(JavaCompile) { - println "$name compiler args: ${options.compilerArgs}" + println "${name} compiler args: ${options.compilerArgs}" } } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-taskConfigurationIsAvoided.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-taskConfigurationIsAvoided.gradle new file mode 100644 index 000000000000..152dcf13e1d3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-taskConfigurationIsAvoided.gradle @@ -0,0 +1,8 @@ +plugins { + id 'org.springframework.boot' version '{version}' + id 'java' +} + +tasks.configureEach { + println "Configuring ${it.path}" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests.gradle index 7c633087a5a7..e96ca9aa2819 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests.gradle @@ -8,14 +8,42 @@ if (project.hasProperty('applyJavaPlugin')) { task('taskExists') { doFirst { - println "$taskName exists = ${tasks.findByName(taskName) != null}" + println "${taskName} exists = ${tasks.findByName(taskName) != null}" } } task('javaCompileEncoding') { doFirst { tasks.withType(JavaCompile) { - println "$name = ${options.encoding}" + println "${name} = ${options.encoding}" } } } + +task('configurationExists') { + doFirst { + println "${configurationName} exists = ${configurations.findByName(configurationName) != null}" + } +} + +task('configurationAttributes') { + doFirst { + def attributes = configurations.findByName(configurationName).attributes + println "${attributes.keySet().size()} ${configurationName} attributes:" + attributes.keySet().each { attribute -> + println " ${attribute}: ${attributes.getAttribute(attribute)}" + } + } +} + +task('configurationResolvabilityAndConsumability') { + if (project.hasProperty("configurationName")) { + Configuration configuration = configurations.findByName(configurationName) + def canBeResolved = configuration.canBeResolved + def canBeConsumed = configuration.canBeConsumed + doFirst { + println "canBeResolved: ${canBeResolved}" + println "canBeConsumed: ${canBeConsumed}" + } + } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/KotlinPluginActionIntegrationTests-kotlinCompileTasksCanOverrideDefaultJavaParametersFlag.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/KotlinPluginActionIntegrationTests-kotlinCompileTasksCanOverrideDefaultJavaParametersFlag.gradle index d2c6c2dfbf76..625f97ccd43b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/KotlinPluginActionIntegrationTests-kotlinCompileTasksCanOverrideDefaultJavaParametersFlag.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/KotlinPluginActionIntegrationTests-kotlinCompileTasksCanOverrideDefaultJavaParametersFlag.gradle @@ -13,7 +13,7 @@ tasks.withType(KotlinCompile) { task('kotlinCompileTasksJavaParameters') { doFirst { tasks.withType(KotlinCompile) { - println "$name java parameters: ${kotlinOptions.javaParameters}" + println "${name} java parameters: ${kotlinOptions.javaParameters}" } } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/KotlinPluginActionIntegrationTests-kotlinCompileTasksUseJavaParametersFlagByDefault.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/KotlinPluginActionIntegrationTests-kotlinCompileTasksUseJavaParametersFlagByDefault.gradle index cb6e5f89672f..f5be02c186bd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/KotlinPluginActionIntegrationTests-kotlinCompileTasksUseJavaParametersFlagByDefault.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/KotlinPluginActionIntegrationTests-kotlinCompileTasksUseJavaParametersFlagByDefault.gradle @@ -9,7 +9,7 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinCompile task('kotlinCompileTasksJavaParameters') { doFirst { tasks.withType(KotlinCompile) { - println "$name java parameters: ${kotlinOptions.javaParameters}" + println "${name} java parameters: ${kotlinOptions.javaParameters}" } } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/KotlinPluginActionIntegrationTests-kotlinVersionPropertyIsSet.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/KotlinPluginActionIntegrationTests-kotlinVersionPropertyIsSet.gradle index 47ecadc6b87c..058a68d5c58c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/KotlinPluginActionIntegrationTests-kotlinVersionPropertyIsSet.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/KotlinPluginActionIntegrationTests-kotlinVersionPropertyIsSet.gradle @@ -21,7 +21,7 @@ repositories { } dependencies { - implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + implementation('org.jetbrains.kotlin:kotlin-stdlib-jdk8') } task kotlinVersion { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/KotlinPluginActionIntegrationTests-taskConfigurationIsAvoided.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/KotlinPluginActionIntegrationTests-taskConfigurationIsAvoided.gradle new file mode 100644 index 000000000000..57b997af8295 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/KotlinPluginActionIntegrationTests-taskConfigurationIsAvoided.gradle @@ -0,0 +1,9 @@ +plugins { + id 'org.springframework.boot' version '{version}' +} + +apply plugin: 'org.jetbrains.kotlin.jvm' + +tasks.configureEach { + println "Configuring ${it.path}" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/SpringBootPluginIntegrationTests-unresolvedDependenciesAreAnalyzedWhenDependencyResolutionFails.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/SpringBootPluginIntegrationTests-unresolvedDependenciesAreAnalyzedWhenDependencyResolutionFails.gradle index 8c187442cfb1..4600fe75b2b7 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/SpringBootPluginIntegrationTests-unresolvedDependenciesAreAnalyzedWhenDependencyResolutionFails.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/SpringBootPluginIntegrationTests-unresolvedDependenciesAreAnalyzedWhenDependencyResolutionFails.gradle @@ -8,5 +8,5 @@ repositories { } dependencies { - implementation 'org.springframework.boot:spring-boot-starter' + implementation('org.springframework.boot:spring-boot-starter') } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/WarPluginActionIntegrationTests-assembleRunsBootWarAndWar.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/WarPluginActionIntegrationTests-assembleRunsBootWarAndWar.gradle new file mode 100644 index 000000000000..a9db466bdfc5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/WarPluginActionIntegrationTests-assembleRunsBootWarAndWar.gradle @@ -0,0 +1,8 @@ +plugins { + id 'war' + id 'org.springframework.boot' version '{version}' +} + +bootWar { + mainClass = 'com.example.Application' +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/WarPluginActionIntegrationTests-taskConfigurationIsAvoided.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/WarPluginActionIntegrationTests-taskConfigurationIsAvoided.gradle new file mode 100644 index 000000000000..93d99ceb1148 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/WarPluginActionIntegrationTests-taskConfigurationIsAvoided.gradle @@ -0,0 +1,8 @@ +plugins { + id 'org.springframework.boot' version '{version}' + id 'war' +} + +tasks.configureEach { + println "Configuring ${it.path}" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/WarPluginActionIntegrationTests-warAndBootWarCanBothBeBuilt.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/WarPluginActionIntegrationTests-warAndBootWarCanBothBeBuilt.gradle deleted file mode 100644 index fc129cef9660..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/WarPluginActionIntegrationTests-warAndBootWarCanBothBeBuilt.gradle +++ /dev/null @@ -1,13 +0,0 @@ -plugins { - id 'war' - id 'org.springframework.boot' version '{version}' -} - -bootWar { - mainClassName = 'com.example.Application' - classifier = 'boot' -} - -war { - enabled = true -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/WarPluginActionIntegrationTests.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/WarPluginActionIntegrationTests.gradle index e1ed4062c704..139d713b58a5 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/WarPluginActionIntegrationTests.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/WarPluginActionIntegrationTests.gradle @@ -8,6 +8,6 @@ if (project.hasProperty('applyWarPlugin')) { task('taskExists') { doFirst { - println "$taskName exists = ${tasks.findByName(taskName) != null}" + println "${taskName} exists = ${tasks.findByName(taskName) != null}" } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests-basicExecution.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests-basicExecution.gradle new file mode 100644 index 000000000000..826d2e21e338 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests-basicExecution.gradle @@ -0,0 +1,15 @@ +plugins { + id 'org.springframework.boot' version '{version}' apply false +} + +version = '0.1.0' + +task buildInfo(type: org.springframework.boot.gradle.tasks.buildinfo.BuildInfo) { + destinationDir project.buildDir + properties { + artifact = 'foo' + group = 'foo' + name = 'foo' + additional = ['additional': 'foo'] + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests-defaultValues.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests-defaultValues.gradle index 62b7abef2bcf..cdf455fff260 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests-defaultValues.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests-defaultValues.gradle @@ -2,8 +2,4 @@ plugins { id 'org.springframework.boot' version '{version}' apply false } -def property(String name, Object defaultValue) { - project.hasProperty(name) ? project.getProperty(name) : defaultValue -} - task buildInfo(type: org.springframework.boot.gradle.tasks.buildinfo.BuildInfo) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests-notUpToDateWhenExecutedTwiceAsTimeChanges.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests-notUpToDateWhenExecutedTwiceAsTimeChanges.gradle new file mode 100644 index 000000000000..cdf455fff260 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests-notUpToDateWhenExecutedTwiceAsTimeChanges.gradle @@ -0,0 +1,5 @@ +plugins { + id 'org.springframework.boot' version '{version}' apply false +} + +task buildInfo(type: org.springframework.boot.gradle.tasks.buildinfo.BuildInfo) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests-notUpToDateWhenExecutedTwiceWithFixedTimeAndChangedGradlePropertiesProjectVersion.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests-notUpToDateWhenExecutedTwiceWithFixedTimeAndChangedGradlePropertiesProjectVersion.gradle new file mode 100644 index 000000000000..3193b71f3c0e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests-notUpToDateWhenExecutedTwiceWithFixedTimeAndChangedGradlePropertiesProjectVersion.gradle @@ -0,0 +1,13 @@ +plugins { + id 'org.springframework.boot' version '{version}' apply false +} + +task buildInfo(type: org.springframework.boot.gradle.tasks.buildinfo.BuildInfo) { + properties { + artifact = 'example' + group = 'com.example' + name = 'example' + additional = ['additional': 'alpha'] + time = null + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests-notUpToDateWhenExecutedTwiceWithFixedTimeAndChangedProjectVersion.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests-notUpToDateWhenExecutedTwiceWithFixedTimeAndChangedProjectVersion.gradle new file mode 100644 index 000000000000..60b6b3b2df9a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests-notUpToDateWhenExecutedTwiceWithFixedTimeAndChangedProjectVersion.gradle @@ -0,0 +1,15 @@ +plugins { + id 'org.springframework.boot' version '{version}' apply false +} + +version = '{projectVersion}' + +task buildInfo(type: org.springframework.boot.gradle.tasks.buildinfo.BuildInfo) { + properties { + artifact = 'example' + group = 'com.example' + name = 'example' + additional = ['additional': 'alpha'] + time = null + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests-reproducibleOutputWithFixedTime.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests-reproducibleOutputWithFixedTime.gradle new file mode 100644 index 000000000000..5d529839a10b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests-reproducibleOutputWithFixedTime.gradle @@ -0,0 +1,9 @@ +plugins { + id 'org.springframework.boot' version '{version}' apply false +} + +task buildInfo(type: org.springframework.boot.gradle.tasks.buildinfo.BuildInfo) { + properties { + time = null + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests-upToDateWhenExecutedTwiceWithFixedTime.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests-upToDateWhenExecutedTwiceWithFixedTime.gradle new file mode 100644 index 000000000000..5d529839a10b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests-upToDateWhenExecutedTwiceWithFixedTime.gradle @@ -0,0 +1,9 @@ +plugins { + id 'org.springframework.boot' version '{version}' apply false +} + +task buildInfo(type: org.springframework.boot.gradle.tasks.buildinfo.BuildInfo) { + properties { + time = null + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests.gradle deleted file mode 100644 index 16571fb1c2a8..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests.gradle +++ /dev/null @@ -1,25 +0,0 @@ -plugins { - id 'org.springframework.boot' version '{version}' apply false -} - -def property(String name, Object defaultValue) { - project.hasProperty(name) ? project.getProperty(name) : defaultValue -} - -version = property('projectVersion', '0.1.0') - -task buildInfo(type: org.springframework.boot.gradle.tasks.buildinfo.BuildInfo) { - destinationDir file(property('buildInfoDestinationDir', project.buildDir)) - properties { - artifact = property('buildInfoArtifact', 'foo') - group = property('buildInfoGroup', 'foo') - name = property('buildInfoName', 'foo') - if (!project.hasProperty('projectVersion')) { - version = property('buildInfoVersion', '1.0') - } - additional = ['additional': property('buildInfoAdditional', 'foo')] - if (project.hasProperty('nullTime')) { - time = null - } - } -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBinding.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBinding.gradle new file mode 100644 index 000000000000..d9cb8f4b43a6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBinding.gradle @@ -0,0 +1,12 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +sourceCompatibility = '1.8' +targetCompatibility = '1.8' + +bootBuildImage { + builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1" + bindings = [ "${projectDir}/bindings/ca-certificates:/platform/bindings/certificates" ] +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBuildpackFromBuilder.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBuildpackFromBuilder.gradle new file mode 100644 index 000000000000..148c37228b39 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBuildpackFromBuilder.gradle @@ -0,0 +1,12 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +sourceCompatibility = '1.8' +targetCompatibility = '1.8' + +bootBuildImage { + builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1" + buildpacks = [ "spring-boot/test-info" ] +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBuildpackFromDirectory.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBuildpackFromDirectory.gradle new file mode 100644 index 000000000000..b21045dce189 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBuildpackFromDirectory.gradle @@ -0,0 +1,12 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +sourceCompatibility = '1.8' +targetCompatibility = '1.8' + +bootBuildImage { + builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1" + buildpacks = [ "file://${projectDir}/buildpack/hello-world" ] +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBuildpackFromTarGzip.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBuildpackFromTarGzip.gradle new file mode 100644 index 000000000000..4f3a7b94748f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBuildpackFromTarGzip.gradle @@ -0,0 +1,12 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +sourceCompatibility = '1.8' +targetCompatibility = '1.8' + +bootBuildImage { + builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1" + buildpacks = [ "file://${projectDir}/hello-world.tgz" ] +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBuildpacksFromImages.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBuildpacksFromImages.gradle new file mode 100644 index 000000000000..73c6d37ab96a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBuildpacksFromImages.gradle @@ -0,0 +1,12 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +sourceCompatibility = '1.8' +targetCompatibility = '1.8' + +bootBuildImage { + builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1" + buildpacks = ["projects.registry.vmware.com/springboot/test-info:latest"] +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithCommandLineOptions.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithCommandLineOptions.gradle new file mode 100644 index 000000000000..8971742147d8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithCommandLineOptions.gradle @@ -0,0 +1,7 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +sourceCompatibility = '1.8' +targetCompatibility = '1.8' diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithCustomBuilderAndRunImage.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithCustomBuilderAndRunImage.gradle new file mode 100644 index 000000000000..b3fd2aa08384 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithCustomBuilderAndRunImage.gradle @@ -0,0 +1,13 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +sourceCompatibility = '1.8' +targetCompatibility = '1.8' + +bootBuildImage { + imageName = "example/test-image-custom" + builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1" + runImage = "projects.registry.vmware.com/springboot/run:tiny-cnb" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithCustomName.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithCustomName.gradle new file mode 100644 index 000000000000..73e1cc664c21 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithCustomName.gradle @@ -0,0 +1,12 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +sourceCompatibility = '1.8' +targetCompatibility = '1.8' + +bootBuildImage { + imageName = "example/test-image-name" + builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithWarPackagingAndJarConfiguration.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithWarPackagingAndJarConfiguration.gradle new file mode 100644 index 000000000000..5c74c2f829d9 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithWarPackagingAndJarConfiguration.gradle @@ -0,0 +1,15 @@ +plugins { + id 'war' + id 'org.springframework.boot' version '{version}' +} + +bootBuildImage { + jar = bootWar.archiveFile +} + +sourceCompatibility = '1.8' +targetCompatibility = '1.8' + +bootBuildImage { + builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1" +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWithBuilderError.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWithBuilderError.gradle new file mode 100644 index 000000000000..cb5cf9966f37 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWithBuilderError.gradle @@ -0,0 +1,12 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +sourceCompatibility = '1.8' +targetCompatibility = '1.8' + +bootBuildImage { + builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1" + environment = ["FORCE_FAILURE": "true"] +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWithBuildpackNotInBuilder.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWithBuildpackNotInBuilder.gradle new file mode 100644 index 000000000000..f9a98a434316 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWithBuildpackNotInBuilder.gradle @@ -0,0 +1,12 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +sourceCompatibility = '1.8' +targetCompatibility = '1.8' + +bootBuildImage { + builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1" + buildpacks = [ "urn:cnb:builder:example/does-not-exist:0.0.1" ] +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWithLaunchScript.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWithLaunchScript.gradle new file mode 100644 index 000000000000..c69f24eabc85 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWithLaunchScript.gradle @@ -0,0 +1,15 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +sourceCompatibility = '1.8' +targetCompatibility = '1.8' + +bootJar { + launchScript() +} + +bootBuildImage { + builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.gradle new file mode 100644 index 000000000000..cf75779cbb91 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.gradle @@ -0,0 +1,15 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +if (project.hasProperty('applyWarPlugin')) { + apply plugin: 'war' +} + +sourceCompatibility = '1.8' +targetCompatibility = '1.8' + +bootBuildImage { + builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageRegistryIntegrationTests.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageRegistryIntegrationTests.gradle new file mode 100644 index 000000000000..3badc8b261a4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageRegistryIntegrationTests.gradle @@ -0,0 +1,17 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +sourceCompatibility = '1.8' +targetCompatibility = '1.8' + +bootBuildImage { + publish = true + docker { + publishRegistry { + username = "user" + password = "secret" + } + } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-basicBuild.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-basicBuild.gradle new file mode 100644 index 000000000000..be0e82ef6d85 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-basicBuild.gradle @@ -0,0 +1,8 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +bootJar { + mainClass = 'com.example.Application' +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-assembleRunsBootJarAndJarIsSkipped.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-basicBuildUsingDeprecatedMainClassName.gradle similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-assembleRunsBootJarAndJarIsSkipped.gradle rename to spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-basicBuildUsingDeprecatedMainClassName.gradle diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-classesFromASecondarySourceSetCanBeIncludedInTheArchive.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-classesFromASecondarySourceSetCanBeIncludedInTheArchive.gradle new file mode 100644 index 000000000000..eeca788d36fe --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-classesFromASecondarySourceSetCanBeIncludedInTheArchive.gradle @@ -0,0 +1,15 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +sourceSets { + secondary + main { + runtimeClasspath += secondary.output + } +} + +bootJar { + mainClass = 'com.example.Application' +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-customLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-customLayers.gradle new file mode 100644 index 000000000000..3ba806e07e0b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-customLayers.gradle @@ -0,0 +1,49 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +bootJar { + mainClass = 'com.example.Application' + layered { + application { + intoLayer("static") { + include "META-INF/resources/**", "resources/**", "static/**", "public/**" + } + intoLayer("app") + } + dependencies { + intoLayer("snapshot-dependencies") { + include "*:*:*SNAPSHOT" + } + intoLayer("commons-dependencies") { + include "org.apache.commons:*" + } + intoLayer("dependencies") + } + layerOrder = ["dependencies", "commons-dependencies", "snapshot-dependencies", "static", "app"] + } +} + +repositories { + mavenCentral() + maven { url "file:repository" } +} + +dependencies { + implementation("com.example:library:1.0-SNAPSHOT") + implementation("org.apache.commons:commons-lang3:3.9") + implementation("org.springframework:spring-core:5.2.5.RELEASE") +} + +task listLayers(type: JavaExec) { + classpath = bootJar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "list" +} + +task extractLayers(type: JavaExec) { + classpath = bootJar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "extract" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchive.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchive.gradle new file mode 100644 index 000000000000..cdbb87315a6e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchive.gradle @@ -0,0 +1,23 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +bootJar { + mainClass = 'com.example.Application' +} + +repositories { + mavenCentral() +} + +dependencies { + developmentOnly("org.apache.commons:commons-lang3:3.9") + implementation("commons-io:commons-io:2.6") +} + +bootJar { + layered { + enabled = false + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle new file mode 100644 index 000000000000..941f20aa4c92 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle @@ -0,0 +1,24 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +bootJar { + mainClass = 'com.example.Application' +} + +repositories { + mavenCentral() +} + +dependencies { + developmentOnly("org.apache.commons:commons-lang3:3.9") + developmentOnly("commons-io:commons-io:2.6") + implementation("commons-io:commons-io:2.6") +} + +bootJar { + layered { + enabled = false + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesCanBeIncludedInTheArchive.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesCanBeIncludedInTheArchive.gradle new file mode 100644 index 000000000000..d035cf456ef0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesCanBeIncludedInTheArchive.gradle @@ -0,0 +1,27 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +bootJar { + mainClass = 'com.example.Application' +} + +repositories { + mavenCentral() +} + +dependencies { + developmentOnly("org.apache.commons:commons-lang3:3.9") + implementation("commons-io:commons-io:2.6") +} + +bootJar { + classpath configurations.developmentOnly +} + +bootJar { + layered { + enabled = false + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-duplicatesAreHandledGracefully.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-duplicatesAreHandledGracefully.gradle index 3c649f5cff40..4b6c9a46edef 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-duplicatesAreHandledGracefully.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-duplicatesAreHandledGracefully.gradle @@ -4,7 +4,7 @@ plugins { } bootJar { - mainClassName = 'com.example.CustomMain' + mainClass = 'com.example.CustomMain' duplicatesStrategy = "exclude" } @@ -22,6 +22,6 @@ repositories { } dependencies { - implementation "org.apache.commons:commons-lang3:3.6" + implementation("org.apache.commons:commons-lang3:3.6") provided "org.apache.commons:commons-lang3:3.6" } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-explodedApplicationClasspath.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-explodedApplicationClasspath.gradle new file mode 100644 index 000000000000..f97b7df5b6bd --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-explodedApplicationClasspath.gradle @@ -0,0 +1,25 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +repositories { + mavenCentral() + maven { url "file:repository" } +} + +dependencies { + implementation("com.example:library:1.0-SNAPSHOT") + implementation("org.apache.commons:commons-lang3:3.9") +} + +task explode(type: Sync) { + dependsOn(bootJar) + destinationDir = file("$buildDir/exploded") + from zipTree(files(bootJar).singleFile) +} + +task launch(type: JavaExec) { + classpath = files(explode) + mainClass = 'org.springframework.boot.loader.JarLauncher' +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-implicitLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-implicitLayers.gradle new file mode 100644 index 000000000000..cc3aa6f0e8b0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-implicitLayers.gradle @@ -0,0 +1,32 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +bootJar { + mainClass = 'com.example.Application' +} + +repositories { + mavenCentral() + maven { url "file:repository" } +} + +dependencies { + implementation("com.example:library:1.0-SNAPSHOT") + implementation("org.apache.commons:commons-lang3:3.9") + implementation("org.springframework:spring-core:5.2.5.RELEASE") + implementation("org.springframework.boot:spring-boot-starter-logging:2.2.0.RELEASE") +} + +task listLayers(type: JavaExec) { + classpath = bootJar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "list" +} + +task extractLayers(type: JavaExec) { + classpath = bootJar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "extract" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-jarTypeFilteringIsApplied.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-jarTypeFilteringIsApplied.gradle new file mode 100644 index 000000000000..970d90d116f9 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-jarTypeFilteringIsApplied.gradle @@ -0,0 +1,25 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +bootJar { + mainClass = 'com.example.Application' +} + +repositories { + flatDir { + dirs 'repository' + } +} + +dependencies { + implementation(name: "standard") + implementation(name: "starter") +} + +bootJar { + layered { + enabled = false + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-layersWithCustomSourceSet.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-layersWithCustomSourceSet.gradle new file mode 100644 index 000000000000..1f18bb3ebc27 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-layersWithCustomSourceSet.gradle @@ -0,0 +1,35 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +sourceSets { + custom +} + +bootJar { + mainClass = 'com.example.Application' +} + +repositories { + mavenCentral() + maven { url "file:repository" } +} + +dependencies { + implementation("com.example:library:1.0-SNAPSHOT") + implementation("org.apache.commons:commons-lang3:3.9") + implementation("org.springframework:spring-core:5.2.5.RELEASE") +} + +task listLayers(type: JavaExec) { + classpath = bootJar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "list" +} + +task extractLayers(type: JavaExec) { + classpath = bootJar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "extract" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-multiModuleCustomLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-multiModuleCustomLayers.gradle new file mode 100644 index 000000000000..f8127b2f3db5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-multiModuleCustomLayers.gradle @@ -0,0 +1,66 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +subprojects { + apply plugin: 'java' + group = 'org.example.projects' + version = '1.2.3' + if (it.name == 'bravo') { + dependencies { + implementation(project(':charlie')) + } + } +} + +bootJar { + mainClass = 'com.example.Application' + layered { + application { + intoLayer("static") { + include "META-INF/resources/**", "resources/**", "static/**", "public/**" + } + intoLayer("app") + } + dependencies { + intoLayer("snapshot-dependencies") { + include "*:*:*SNAPSHOT" + excludeProjectDependencies() + } + intoLayer("subproject-dependencies") { + includeProjectDependencies() + } + intoLayer("commons-dependencies") { + include "org.apache.commons:*" + } + intoLayer("dependencies") + } + layerOrder = ["dependencies", "commons-dependencies", "snapshot-dependencies", "subproject-dependencies", "static", "app"] + } +} + +repositories { + mavenCentral() + maven { url "file:repository" } +} + +dependencies { + implementation(project(':alpha')) + implementation(project(':bravo')) + implementation("com.example:library:1.0-SNAPSHOT") + implementation("org.apache.commons:commons-lang3:3.9") + implementation("org.springframework:spring-core:5.2.5.RELEASE") +} + +task listLayers(type: JavaExec) { + classpath = bootJar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "list" +} + +task extractLayers(type: JavaExec) { + classpath = bootJar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "extract" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-multiModuleImplicitLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-multiModuleImplicitLayers.gradle new file mode 100644 index 000000000000..ba34cf4d1d6c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-multiModuleImplicitLayers.gradle @@ -0,0 +1,44 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +subprojects { + apply plugin: 'java' + group = 'org.example.projects' + version = '1.2.3' + if (it.name == 'bravo') { + dependencies { + implementation(project(':charlie')) + } + } +} + +bootJar { + mainClass = 'com.example.Application' +} + +repositories { + mavenCentral() + maven { url "file:repository" } +} + +dependencies { + implementation(project(':alpha')) + implementation(project(':bravo')) + implementation("com.example:library:1.0-SNAPSHOT") + implementation("org.apache.commons:commons-lang3:3.9") + implementation("org.springframework:spring-core:5.2.5.RELEASE") +} + +task listLayers(type: JavaExec) { + classpath = bootJar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "list" +} + +task extractLayers(type: JavaExec) { + classpath = bootJar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "extract" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-notUpToDateWhenBuiltWithLayerToolsAndThenWithoutLayerTools.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-notUpToDateWhenBuiltWithLayerToolsAndThenWithoutLayerTools.gradle new file mode 100644 index 000000000000..7530a605b9e5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-notUpToDateWhenBuiltWithLayerToolsAndThenWithoutLayerTools.gradle @@ -0,0 +1,11 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +bootJar { + mainClass = 'com.example.Application' + layered { + {layerTools} + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-notUpToDateWhenBuiltWithoutLayersAndThenWithLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-notUpToDateWhenBuiltWithoutLayersAndThenWithLayers.gradle new file mode 100644 index 000000000000..b6b9c3940224 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-notUpToDateWhenBuiltWithoutLayersAndThenWithLayers.gradle @@ -0,0 +1,11 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +bootJar { + mainClass = 'com.example.Application' + layered { + {layerEnablement} + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-notUpToDateWhenLaunchScriptPropertyChanges.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-notUpToDateWhenLaunchScriptPropertyChanges.gradle new file mode 100644 index 000000000000..36e7a1e38bcd --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-notUpToDateWhenLaunchScriptPropertyChanges.gradle @@ -0,0 +1,11 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +bootJar { + mainClass = 'com.example.Application' + launchScript { + properties 'prop' : '{launchScriptProperty}' + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-notUpToDateWhenLaunchScriptWasIncludedAndThenIsNotIncluded.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-notUpToDateWhenLaunchScriptWasIncludedAndThenIsNotIncluded.gradle new file mode 100644 index 000000000000..ac286dabae9a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-notUpToDateWhenLaunchScriptWasIncludedAndThenIsNotIncluded.gradle @@ -0,0 +1,9 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +bootJar { + mainClass = 'com.example.Application' + {launchScript} +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-notUpToDateWhenLaunchScriptWasNotIncludedAndThenIsIncluded.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-notUpToDateWhenLaunchScriptWasNotIncludedAndThenIsIncluded.gradle new file mode 100644 index 000000000000..ac286dabae9a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-notUpToDateWhenLaunchScriptWasNotIncludedAndThenIsIncluded.gradle @@ -0,0 +1,9 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +bootJar { + mainClass = 'com.example.Application' + {launchScript} +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-packagedApplicationClasspath.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-packagedApplicationClasspath.gradle new file mode 100644 index 000000000000..72b2787a4332 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-packagedApplicationClasspath.gradle @@ -0,0 +1,19 @@ + +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +task launch(type: JavaExec) { + classpath = files(bootJar) +} + +repositories { + mavenCentral() + maven { url "file:repository" } +} + +dependencies { + implementation("com.example:library:1.0-SNAPSHOT") + implementation("org.apache.commons:commons-lang3:3.9") +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-reproducibleArchive.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-reproducibleArchive.gradle index 7fd1b1216fcd..4e69b4589880 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-reproducibleArchive.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-reproducibleArchive.gradle @@ -4,7 +4,7 @@ plugins { } bootJar { - mainClassName = 'com.example.Application' + mainClass = 'com.example.Application' preserveFileTimestamps = false reproducibleFileOrder = true } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-springBootExtensionMainClassNameIsUsed.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-springBootExtensionMainClassNameIsUsed.gradle index 38af6a38f65b..821cd40ac014 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-springBootExtensionMainClassNameIsUsed.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-springBootExtensionMainClassNameIsUsed.gradle @@ -5,5 +5,5 @@ plugins { } springBoot { - mainClassName = 'com.example.CustomMain' + mainClass = 'com.example.CustomMain' } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-startClassIsSetByResolvingTheMainClass.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-startClassIsSetByResolvingTheMainClass.gradle new file mode 100644 index 000000000000..617daecbe6bd --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-startClassIsSetByResolvingTheMainClass.gradle @@ -0,0 +1,5 @@ +plugins { + id 'java' + id 'application' + id 'org.springframework.boot' version '{version}' +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-upToDateWhenBuiltTwice.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-upToDateWhenBuiltTwice.gradle new file mode 100644 index 000000000000..be0e82ef6d85 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-upToDateWhenBuiltTwice.gradle @@ -0,0 +1,8 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +bootJar { + mainClass = 'com.example.Application' +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-upToDateWhenBuiltTwiceWithLaunchScriptIncluded.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-upToDateWhenBuiltTwiceWithLaunchScriptIncluded.gradle new file mode 100644 index 000000000000..d60d326ebba6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-upToDateWhenBuiltTwiceWithLaunchScriptIncluded.gradle @@ -0,0 +1,9 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +bootJar { + mainClass = 'com.example.Application' + launchScript() +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-upToDateWhenBuiltWithDefaultLayeredAndThenWithExplicitLayered.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-upToDateWhenBuiltWithDefaultLayeredAndThenWithExplicitLayered.gradle new file mode 100644 index 000000000000..30f703228b21 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-upToDateWhenBuiltWithDefaultLayeredAndThenWithExplicitLayered.gradle @@ -0,0 +1,9 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +bootJar { + mainClass = 'com.example.Application' + {layered} +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-whenAResolvableCopyOfAnUnresolvableConfigurationIsResolvedThenResolutionSucceeds.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-whenAResolvableCopyOfAnUnresolvableConfigurationIsResolvedThenResolutionSucceeds.gradle new file mode 100644 index 000000000000..2f5e2250a89c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-whenAResolvableCopyOfAnUnresolvableConfigurationIsResolvedThenResolutionSucceeds.gradle @@ -0,0 +1,16 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +bootJar { + mainClass = 'com.example.Application' +} + +task resolveResolvableCopyOfUnresolvableConfiguration { + doFirst { + def copy = configurations.implementation.copyRecursive() + copy.canBeResolved = true + copy.resolve() + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.gradle deleted file mode 100644 index 1befd0db0667..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.gradle +++ /dev/null @@ -1,13 +0,0 @@ -plugins { - id 'java' - id 'org.springframework.boot' version '{version}' -} - -bootJar { - mainClassName = 'com.example.Application' - if (project.hasProperty('includeLaunchScript') ? includeLaunchScript : false) { - launchScript { - properties 'prop' : project.hasProperty('launchScriptProperty') ? launchScriptProperty : 'default' - } - } -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-basicBuild.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-basicBuild.gradle new file mode 100644 index 000000000000..a9db466bdfc5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-basicBuild.gradle @@ -0,0 +1,8 @@ +plugins { + id 'war' + id 'org.springframework.boot' version '{version}' +} + +bootWar { + mainClass = 'com.example.Application' +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/WarPluginActionIntegrationTests-assembleRunsBootWarAndWarIsSkipped.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-basicBuildUsingDeprecatedMainClassName.gradle similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/WarPluginActionIntegrationTests-assembleRunsBootWarAndWarIsSkipped.gradle rename to spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-basicBuildUsingDeprecatedMainClassName.gradle diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-classesFromASecondarySourceSetCanBeIncludedInTheArchive.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-classesFromASecondarySourceSetCanBeIncludedInTheArchive.gradle new file mode 100644 index 000000000000..e107dfd956fb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-classesFromASecondarySourceSetCanBeIncludedInTheArchive.gradle @@ -0,0 +1,15 @@ +plugins { + id 'war' + id 'org.springframework.boot' version '{version}' +} + +sourceSets { + secondary + main { + runtimeClasspath += secondary.output + } +} + +bootWar { + mainClass = 'com.example.Application' +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-customLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-customLayers.gradle new file mode 100644 index 000000000000..0c4bcdcaf065 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-customLayers.gradle @@ -0,0 +1,50 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' + id 'war' +} + +bootWar { + mainClass = 'com.example.Application' + layered { + application { + intoLayer("static") { + include "META-INF/resources/**", "resources/**", "static/**", "public/**" + } + intoLayer("app") + } + dependencies { + intoLayer("snapshot-dependencies") { + include "*:*:*SNAPSHOT" + } + intoLayer("commons-dependencies") { + include "org.apache.commons:*" + } + intoLayer("dependencies") + } + layerOrder = ["dependencies", "commons-dependencies", "snapshot-dependencies", "static", "app"] + } +} + +repositories { + mavenCentral() + maven { url "file:repository" } +} + +dependencies { + implementation("com.example:library:1.0-SNAPSHOT") + implementation("org.apache.commons:commons-lang3:3.9") + implementation("org.springframework:spring-core:5.2.5.RELEASE") +} + +task listLayers(type: JavaExec) { + classpath = bootWar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "list" +} + +task extractLayers(type: JavaExec) { + classpath = bootWar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "extract" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle new file mode 100644 index 000000000000..85aea3ecce29 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle @@ -0,0 +1,24 @@ +plugins { + id 'war' + id 'org.springframework.boot' version '{version}' +} + +bootWar { + mainClass = 'com.example.Application' +} + +repositories { + mavenCentral() +} + +dependencies { + developmentOnly("org.apache.commons:commons-lang3:3.9") + developmentOnly("commons-io:commons-io:2.6") + implementation("commons-io:commons-io:2.6") +} + +bootWar { + layered { + enabled = false + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesCanBeIncludedInTheArchive.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesCanBeIncludedInTheArchive.gradle new file mode 100644 index 000000000000..184c97603e2b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesCanBeIncludedInTheArchive.gradle @@ -0,0 +1,27 @@ +plugins { + id 'war' + id 'org.springframework.boot' version '{version}' +} + +bootWar { + mainClass = 'com.example.Application' +} + +repositories { + mavenCentral() +} + +dependencies { + developmentOnly("org.apache.commons:commons-lang3:3.9") + implementation("commons-io:commons-io:2.6") +} + +bootWar { + classpath configurations.developmentOnly +} + +bootWar { + layered { + enabled = false + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-duplicatesAreHandledGracefully.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-duplicatesAreHandledGracefully.gradle new file mode 100644 index 000000000000..3b5c8323b0cb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-duplicatesAreHandledGracefully.gradle @@ -0,0 +1,27 @@ +plugins { + id 'war' + id 'org.springframework.boot' version '{version}' +} + +bootWar { + mainClass = 'com.example.CustomMain' + duplicatesStrategy = "exclude" +} + +configurations { + provided +} + +sourceSets.all { + compileClasspath += configurations.provided + runtimeClasspath += configurations.provided +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("org.apache.commons:commons-lang3:3.6") + provided "org.apache.commons:commons-lang3:3.6" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-implicitLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-implicitLayers.gradle new file mode 100644 index 000000000000..6fd9018c4552 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-implicitLayers.gradle @@ -0,0 +1,33 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' + id 'war' +} + +bootWar { + mainClass = 'com.example.Application' +} + +repositories { + mavenCentral() + maven { url "file:repository" } +} + +dependencies { + implementation("com.example:library:1.0-SNAPSHOT") + implementation("org.apache.commons:commons-lang3:3.9") + implementation("org.springframework:spring-core:5.2.5.RELEASE") + implementation("org.springframework.boot:spring-boot-starter-logging:2.2.0.RELEASE") +} + +task listLayers(type: JavaExec) { + classpath = bootWar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "list" +} + +task extractLayers(type: JavaExec) { + classpath = bootWar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "extract" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-jarTypeFilteringIsApplied.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-jarTypeFilteringIsApplied.gradle new file mode 100644 index 000000000000..60e32af928b3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-jarTypeFilteringIsApplied.gradle @@ -0,0 +1,25 @@ +plugins { + id 'war' + id 'org.springframework.boot' version '{version}' +} + +bootWar { + mainClass = 'com.example.Application' +} + +repositories { + flatDir { + dirs 'repository' + } +} + +dependencies { + implementation(name: "standard") + implementation(name: "starter") +} + +bootWar { + layered { + enabled = false + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-layersWithCustomSourceSet.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-layersWithCustomSourceSet.gradle new file mode 100644 index 000000000000..6892f814bbbb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-layersWithCustomSourceSet.gradle @@ -0,0 +1,36 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' + id 'war' +} + +sourceSets { + custom +} + +bootWar { + mainClass = 'com.example.Application' +} + +repositories { + mavenCentral() + maven { url "file:repository" } +} + +dependencies { + implementation("com.example:library:1.0-SNAPSHOT") + implementation("org.apache.commons:commons-lang3:3.9") + implementation("org.springframework:spring-core:5.2.5.RELEASE") +} + +task listLayers(type: JavaExec) { + classpath = bootWar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "list" +} + +task extractLayers(type: JavaExec) { + classpath = bootWar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "extract" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleCustomLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleCustomLayers.gradle new file mode 100644 index 000000000000..da574d0d1537 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleCustomLayers.gradle @@ -0,0 +1,67 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' + id 'war' +} + +subprojects { + apply plugin: 'java' + group = 'org.example.projects' + version = '1.2.3' + if (it.name == 'bravo') { + dependencies { + implementation(project(':charlie')) + } + } +} + +bootWar { + mainClass = 'com.example.Application' + layered { + application { + intoLayer("static") { + include "META-INF/resources/**", "resources/**", "static/**", "public/**" + } + intoLayer("app") + } + dependencies { + intoLayer("snapshot-dependencies") { + include "*:*:*SNAPSHOT" + excludeProjectDependencies() + } + intoLayer("subproject-dependencies") { + includeProjectDependencies() + } + intoLayer("commons-dependencies") { + include "org.apache.commons:*" + } + intoLayer("dependencies") + } + layerOrder = ["dependencies", "commons-dependencies", "snapshot-dependencies", "subproject-dependencies", "static", "app"] + } +} + +repositories { + mavenCentral() + maven { url "file:repository" } +} + +dependencies { + implementation(project(':alpha')) + implementation(project(':bravo')) + implementation("com.example:library:1.0-SNAPSHOT") + implementation("org.apache.commons:commons-lang3:3.9") + implementation("org.springframework:spring-core:5.2.5.RELEASE") +} + +task listLayers(type: JavaExec) { + classpath = bootWar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "list" +} + +task extractLayers(type: JavaExec) { + classpath = bootWar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "extract" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleImplicitLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleImplicitLayers.gradle new file mode 100644 index 000000000000..cba40d5c3d3d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleImplicitLayers.gradle @@ -0,0 +1,45 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' + id 'war' +} + +subprojects { + apply plugin: 'java' + group = 'org.example.projects' + version = '1.2.3' + if (it.name == 'bravo') { + dependencies { + implementation(project(':charlie')) + } + } +} + +bootWar { + mainClass = 'com.example.Application' +} + +repositories { + mavenCentral() + maven { url "file:repository" } +} + +dependencies { + implementation(project(':alpha')) + implementation(project(':bravo')) + implementation("com.example:library:1.0-SNAPSHOT") + implementation("org.apache.commons:commons-lang3:3.9") + implementation("org.springframework:spring-core:5.2.5.RELEASE") +} + +task listLayers(type: JavaExec) { + classpath = bootWar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "list" +} + +task extractLayers(type: JavaExec) { + classpath = bootWar.outputs.files + systemProperties = [ "jarmode": "layertools" ] + args "extract" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-notUpToDateWhenBuiltWithLayerToolsAndThenWithoutLayerTools.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-notUpToDateWhenBuiltWithLayerToolsAndThenWithoutLayerTools.gradle new file mode 100644 index 000000000000..aa8d6fa822f6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-notUpToDateWhenBuiltWithLayerToolsAndThenWithoutLayerTools.gradle @@ -0,0 +1,12 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' + id 'war' +} + +bootWar { + mainClass = 'com.example.Application' + layered { + {layerTools} + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-notUpToDateWhenBuiltWithoutLayersAndThenWithLayers.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-notUpToDateWhenBuiltWithoutLayersAndThenWithLayers.gradle new file mode 100644 index 000000000000..713e4c0dd01f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-notUpToDateWhenBuiltWithoutLayersAndThenWithLayers.gradle @@ -0,0 +1,12 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' + id 'war' +} + +bootWar { + mainClass = 'com.example.Application' + layered { + {layerEnablement} + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-notUpToDateWhenLaunchScriptPropertyChanges.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-notUpToDateWhenLaunchScriptPropertyChanges.gradle new file mode 100644 index 000000000000..c8c1bb54ed6f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-notUpToDateWhenLaunchScriptPropertyChanges.gradle @@ -0,0 +1,11 @@ +plugins { + id 'war' + id 'org.springframework.boot' version '{version}' +} + +bootWar { + mainClass = 'com.example.Application' + launchScript { + properties 'prop' : '{launchScriptProperty}' + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-notUpToDateWhenLaunchScriptWasIncludedAndThenIsNotIncluded.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-notUpToDateWhenLaunchScriptWasIncludedAndThenIsNotIncluded.gradle new file mode 100644 index 000000000000..66aeab59e580 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-notUpToDateWhenLaunchScriptWasIncludedAndThenIsNotIncluded.gradle @@ -0,0 +1,9 @@ +plugins { + id 'war' + id 'org.springframework.boot' version '{version}' +} + +bootWar { + mainClass = 'com.example.Application' + {launchScript} +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-notUpToDateWhenLaunchScriptWasNotIncludedAndThenIsIncluded.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-notUpToDateWhenLaunchScriptWasNotIncludedAndThenIsIncluded.gradle new file mode 100644 index 000000000000..66aeab59e580 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-notUpToDateWhenLaunchScriptWasNotIncludedAndThenIsIncluded.gradle @@ -0,0 +1,9 @@ +plugins { + id 'war' + id 'org.springframework.boot' version '{version}' +} + +bootWar { + mainClass = 'com.example.Application' + {launchScript} +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-reproducibleArchive.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-reproducibleArchive.gradle index ec17f4aa9225..86f7349a0eac 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-reproducibleArchive.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-reproducibleArchive.gradle @@ -4,7 +4,7 @@ plugins { } bootWar { - mainClassName = 'com.example.Application' + mainClass = 'com.example.Application' preserveFileTimestamps = false reproducibleFileOrder = true } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-springBootExtensionMainClassNameIsUsed.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-springBootExtensionMainClassNameIsUsed.gradle index 2ba6865c4030..43b612386a23 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-springBootExtensionMainClassNameIsUsed.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-springBootExtensionMainClassNameIsUsed.gradle @@ -5,5 +5,5 @@ plugins { } springBoot { - mainClassName = 'com.example.CustomMain' + mainClass = 'com.example.CustomMain' } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-startClassIsSetByResolvingTheMainClass.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-startClassIsSetByResolvingTheMainClass.gradle new file mode 100644 index 000000000000..0ee67eb10db8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-startClassIsSetByResolvingTheMainClass.gradle @@ -0,0 +1,5 @@ +plugins { + id 'war' + id 'application' + id 'org.springframework.boot' version '{version}' +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-upToDateWhenBuiltTwice.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-upToDateWhenBuiltTwice.gradle new file mode 100644 index 000000000000..a9db466bdfc5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-upToDateWhenBuiltTwice.gradle @@ -0,0 +1,8 @@ +plugins { + id 'war' + id 'org.springframework.boot' version '{version}' +} + +bootWar { + mainClass = 'com.example.Application' +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-upToDateWhenBuiltTwiceWithLaunchScriptIncluded.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-upToDateWhenBuiltTwiceWithLaunchScriptIncluded.gradle new file mode 100644 index 000000000000..2f4ccbf47419 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-upToDateWhenBuiltTwiceWithLaunchScriptIncluded.gradle @@ -0,0 +1,9 @@ +plugins { + id 'war' + id 'org.springframework.boot' version '{version}' +} + +bootWar { + mainClass = 'com.example.Application' + launchScript() +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-upToDateWhenBuiltWithDefaultLayeredAndThenWithExplicitLayered.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-upToDateWhenBuiltWithDefaultLayeredAndThenWithExplicitLayered.gradle new file mode 100644 index 000000000000..4c45b1c35fe8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-upToDateWhenBuiltWithDefaultLayeredAndThenWithExplicitLayered.gradle @@ -0,0 +1,10 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' + id 'war' +} + +bootWar { + mainClass = 'com.example.Application' + {layered} +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests.gradle deleted file mode 100644 index 79fbe5579721..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests.gradle +++ /dev/null @@ -1,13 +0,0 @@ -plugins { - id 'war' - id 'org.springframework.boot' version '{version}' -} - -bootWar { - mainClassName = 'com.example.Application' - if (project.hasProperty('includeLaunchScript') ? includeLaunchScript : false) { - launchScript { - properties 'prop' : project.hasProperty('launchScriptProperty') ? launchScriptProperty : 'default' - } - } -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/MavenIntegrationTests-bootJarCanBeUploaded.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/MavenIntegrationTests-bootJarCanBeUploaded.gradle index 8a679f9c1dea..06fddc9cf194 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/MavenIntegrationTests-bootJarCanBeUploaded.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/MavenIntegrationTests-bootJarCanBeUploaded.gradle @@ -5,7 +5,7 @@ plugins { } bootJar { - mainClassName = 'com.example.Application' + mainClass = 'com.example.Application' } group = 'com.example' @@ -14,7 +14,7 @@ version = '1.0' uploadBootArchives { repositories { mavenDeployer { - repository(url: "file:$buildDir/repo") + repository(url: "file:${buildDir}/repo") } } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/MavenIntegrationTests-bootWarCanBeUploaded.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/MavenIntegrationTests-bootWarCanBeUploaded.gradle index 5c34f47cfc4a..5a3b86ddff1c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/MavenIntegrationTests-bootWarCanBeUploaded.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/MavenIntegrationTests-bootWarCanBeUploaded.gradle @@ -5,7 +5,7 @@ plugins { } bootWar { - mainClassName = 'com.example.Application' + mainClass = 'com.example.Application' } group = 'com.example' @@ -14,7 +14,7 @@ version = '1.0' uploadBootArchives { repositories { mavenDeployer { - repository(url: "file:$buildDir/repo") + repository(url: "file:${buildDir}/repo") } } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/MavenPublishingIntegrationTests-bootJarCanBePublished.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/MavenPublishingIntegrationTests-bootJarCanBePublished.gradle index ad434d134358..4c0506b7dbb3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/MavenPublishingIntegrationTests-bootJarCanBePublished.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/MavenPublishingIntegrationTests-bootJarCanBePublished.gradle @@ -5,7 +5,7 @@ plugins { } bootJar { - mainClassName = 'com.example.Application' + mainClass = 'com.example.Application' } group = 'com.example' @@ -14,7 +14,7 @@ version = '1.0' publishing { repositories { maven { - url "$buildDir/repo" + url "${buildDir}/repo" } } publications { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/MavenPublishingIntegrationTests-bootWarCanBePublished.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/MavenPublishingIntegrationTests-bootWarCanBePublished.gradle index 432c5e1f6717..cf6d104d42ee 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/MavenPublishingIntegrationTests-bootWarCanBePublished.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/MavenPublishingIntegrationTests-bootWarCanBePublished.gradle @@ -5,7 +5,7 @@ plugins { } bootWar { - mainClassName = 'com.example.Application' + mainClass = 'com.example.Application' } group = 'com.example' @@ -14,7 +14,7 @@ version = '1.0' publishing { repositories { maven { - url "$buildDir/repo" + url "${buildDir}/repo" } } publications { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests-applicationPluginMainClassNameIsNotUsedWhenItIsNull.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests-applicationPluginMainClassNameIsNotUsedWhenItIsNull.gradle index 3187d8536f80..5e803a3fc5d5 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests-applicationPluginMainClassNameIsNotUsedWhenItIsNull.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests-applicationPluginMainClassNameIsNotUsedWhenItIsNull.gradle @@ -3,9 +3,8 @@ plugins { id 'org.springframework.boot' version '{version}' } -task echoMainClassName { - dependsOn compileJava - doLast { - println 'Main class name = ' + bootRun.main +bootRun { + doFirst { + println "Main class name = ${bootRun.mainClass.get()}" } -} +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests-applicationPluginMainClassNameIsUsed.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests-applicationPluginMainClassNameIsUsed.gradle index f1a0ca76dbb1..60588fdb3778 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests-applicationPluginMainClassNameIsUsed.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests-applicationPluginMainClassNameIsUsed.gradle @@ -3,8 +3,4 @@ plugins { id 'org.springframework.boot' version '{version}' } -mainClassName = 'com.example.CustomMainClass' - -task echoMainClassName { - println 'Main class name = ' + bootRun.main -} +mainClassName = 'com.example.bootrun.main.CustomMainClass' diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests-classesFromASecondarySourceSetCanBeOnTheClasspath.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests-classesFromASecondarySourceSetCanBeOnTheClasspath.gradle new file mode 100644 index 000000000000..315eb8a0b856 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests-classesFromASecondarySourceSetCanBeOnTheClasspath.gradle @@ -0,0 +1,15 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +sourceSets { + secondary + main { + runtimeClasspath += secondary.output + } +} + +springBoot { + mainClass = 'com.example.bootrun.main.CustomMainClass' +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests-jarTypeFilteringIsAppliedToTheClasspath.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests-jarTypeFilteringIsAppliedToTheClasspath.gradle new file mode 100644 index 000000000000..e21adffa8c13 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests-jarTypeFilteringIsAppliedToTheClasspath.gradle @@ -0,0 +1,15 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +repositories { + flatDir { + dirs 'repository' + } +} + +dependencies { + implementation(name: "standard") + implementation(name: "starter") +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests-springBootExtensionMainClassNameIsUsed.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests-springBootExtensionMainClassNameIsUsed.gradle index e2ce1837c038..082de3eb005d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests-springBootExtensionMainClassNameIsUsed.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests-springBootExtensionMainClassNameIsUsed.gradle @@ -1,12 +1,8 @@ plugins { - id 'application' + id 'java' id 'org.springframework.boot' version '{version}' } springBoot { - mainClassName = 'com.example.CustomMainClass' -} - -task echoMainClassName { - println 'Main class name = ' + bootRun.main + mainClass = 'com.example.bootrun.main.CustomMainClass' } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/repository/com/example/library/1.0-SNAPSHOT/library-1.0-SNAPSHOT.jar b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/repository/com/example/library/1.0-SNAPSHOT/library-1.0-SNAPSHOT.jar new file mode 100644 index 000000000000..772cfe2d5268 Binary files /dev/null and b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/repository/com/example/library/1.0-SNAPSHOT/library-1.0-SNAPSHOT.jar differ diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/repository/com/example/library/1.0-SNAPSHOT/library-1.0-SNAPSHOT.pom b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/repository/com/example/library/1.0-SNAPSHOT/library-1.0-SNAPSHOT.pom new file mode 100644 index 000000000000..61b2d25a54ad --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/repository/com/example/library/1.0-SNAPSHOT/library-1.0-SNAPSHOT.pom @@ -0,0 +1,8 @@ + + + 4.0.0 + com.example + library + 1.0-SNAPSHOT + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/build.gradle new file mode 100644 index 000000000000..bd774a035efa --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/build.gradle @@ -0,0 +1,17 @@ +plugins { + id "java-library" + id "org.springframework.boot.conventions" + id "org.springframework.boot.deployed" +} + +description = "Spring Boot Layers Tools" + +dependencies { + implementation(project(":spring-boot-project:spring-boot-tools:spring-boot-loader")) + implementation("org.springframework:spring-core") + + testImplementation("org.assertj:assertj-core") + testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.mockito:mockito-core") + testImplementation("org.mockito:mockito-junit-jupiter") +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/Command.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/Command.java new file mode 100644 index 000000000000..c6669b0d5419 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/Command.java @@ -0,0 +1,343 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jarmode.layertools; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +/** + * A command that can be launched from the layertools jarmode. + * + * @author Phillip Webb + * @author Scott Frederick + */ +abstract class Command { + + private final String name; + + private final String description; + + private final Options options; + + private final Parameters parameters; + + /** + * Create a new {@link Command} instance. + * @param name the name of the command + * @param description a description of the command + * @param options the command options + * @param parameters the command parameters + */ + Command(String name, String description, Options options, Parameters parameters) { + this.name = name; + this.description = description; + this.options = options; + this.parameters = parameters; + } + + /** + * Return the name of this command. + * @return the command name + */ + String getName() { + return this.name; + } + + /** + * Return the description of this command. + * @return the command description + */ + String getDescription() { + return this.description; + } + + /** + * Return options that this command accepts. + * @return the command options + */ + Options getOptions() { + return this.options; + } + + /** + * Return parameters that this command accepts. + * @return the command parameters + */ + Parameters getParameters() { + return this.parameters; + } + + /** + * Run the command by processing the remaining arguments. + * @param args a mutable deque of the remaining arguments + */ + final void run(Deque args) { + List parameters = new ArrayList<>(); + Map options = new HashMap<>(); + while (!args.isEmpty()) { + String arg = args.removeFirst(); + Option option = this.options.find(arg); + if (option != null) { + options.put(option, option.claimArg(args)); + } + else { + parameters.add(arg); + } + } + run(options, parameters); + } + + /** + * Run the actual command. + * @param options any options extracted from the arguments + * @param parameters any parameters extracted from the arguments + */ + protected abstract void run(Map options, List parameters); + + /** + * Static method that can be used to find a single command from a collection. + * @param commands the commands to search + * @param name the name of the command to find + * @return a {@link Command} instance or {@code null}. + */ + static Command find(Collection commands, String name) { + for (Command command : commands) { + if (command.getName().equals(name)) { + return command; + } + } + return null; + } + + /** + * Parameters that the command accepts. + */ + protected static final class Parameters { + + private final List descriptions; + + private Parameters(String[] descriptions) { + this.descriptions = Collections.unmodifiableList(Arrays.asList(descriptions)); + } + + /** + * Return the parameter descriptions. + * @return the descriptions + */ + List getDescriptions() { + return this.descriptions; + } + + @Override + public String toString() { + return this.descriptions.toString(); + } + + /** + * Factory method used if there are no expected parameters. + * @return a new {@link Parameters} instance + */ + protected static Parameters none() { + return of(); + } + + /** + * Factory method used to create a new {@link Parameters} instance with specific + * descriptions. + * @param descriptions the parameter descriptions + * @return a new {@link Parameters} instance with the given descriptions + */ + protected static Parameters of(String... descriptions) { + return new Parameters(descriptions); + } + + } + + /** + * Options that the command accepts. + */ + protected static final class Options { + + private final Option[] values; + + private Options(Option[] values) { + this.values = values; + } + + private Option find(String arg) { + if (arg.startsWith("--")) { + String name = arg.substring(2); + for (Option candidate : this.values) { + if (candidate.getName().equals(name)) { + return candidate; + } + } + throw new UnknownOptionException(name); + } + return null; + } + + /** + * Return if this options collection is empty. + * @return if there are no options + */ + boolean isEmpty() { + return this.values.length == 0; + } + + /** + * Return a stream of each option. + * @return a stream of the options + */ + Stream

    + * Index files are designed to be compatible with YAML and may be read into a list of + * {@code Map>} instances. + * + * @author Madhura Bhave + * @author Andy Wilkinson + * @author Phillip Webb + * @since 2.3.0 + */ +public class LayersIndex { + + private final Iterable layers; + + private final Node root = new Node(); + + /** + * Create a new {@link LayersIndex} backed by the given layers. + * @param layers the layers in the index + */ + public LayersIndex(Layer... layers) { + this(Arrays.asList(layers)); + } + + /** + * Create a new {@link LayersIndex} backed by the given layers. + * @param layers the layers in the index + */ + public LayersIndex(Iterable layers) { + this.layers = layers; + } + + /** + * Add an item to the index. + * @param layer the layer of the item + * @param name the name of the item + */ + public void add(Layer layer, String name) { + String[] segments = name.split("/"); + Node node = this.root; + for (int i = 0; i < segments.length; i++) { + boolean isDirectory = i < (segments.length - 1); + node = node.updateOrAddNode(segments[i], isDirectory, layer); + } + } + + /** + * Write the layer index to an output stream. + * @param out the destination stream + * @throws IOException on IO error + */ + public void writeTo(OutputStream out) throws IOException { + MultiValueMap index = new LinkedMultiValueMap<>(); + this.root.buildIndex("", index); + index.values().forEach(Collections::sort); + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8)); + for (Layer layer : this.layers) { + List names = index.get(layer); + writer.write("- \"" + layer + "\":\n"); + if (names != null) { + for (String name : names) { + writer.write(" - \"" + name + "\"\n"); + } + } + } + writer.flush(); + } + + /** + * A node within the index representing a single path segment. + */ + private static class Node { + + private final String name; + + private final Set layers; + + private final List children = new ArrayList<>(); + + Node() { + this.name = ""; + this.layers = new HashSet<>(); + } + + Node(String name, Layer layer) { + this.name = name; + this.layers = new HashSet<>(Collections.singleton(layer)); + } + + Node updateOrAddNode(String segment, boolean isDirectory, Layer layer) { + String name = segment + (isDirectory ? "/" : ""); + for (Node child : this.children) { + if (name.equals(child.name)) { + child.layers.add(layer); + return child; + } + } + Node child = new Node(name, layer); + this.children.add(child); + return child; + } + + void buildIndex(String path, MultiValueMap index) { + String name = path + this.name; + if (this.layers.size() == 1) { + index.add(this.layers.iterator().next(), name); + } + else { + for (Node child : this.children) { + child.buildIndex(name, index); + } + } + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layout.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layout.java index f7e7e938c4df..36d96624dc74 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layout.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layout.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,10 +39,10 @@ public interface Layout { * Returns the destination path for a given library. * @param libraryName the name of the library (excluding any path) * @param scope the scope of the library - * @return the destination relative to the root of the archive (should end with '/') - * or {@code null} if the library should not be included. + * @return the location of the library relative to the root of the archive (should end + * with '/') or {@code null} if the library should not be included. */ - String getLibraryDestination(String libraryName, LibraryScope scope); + String getLibraryLocation(String libraryName, LibraryScope scope); /** * Returns the location of classes within the archive. @@ -50,6 +50,28 @@ public interface Layout { */ String getClassesLocation(); + /** + * Returns the location of the classpath index file that should be written or + * {@code null} if not index is required. The result should include the filename and + * is relative to the root of the jar. + * @return the classpath index file location + * @since 2.5.0 + */ + default String getClasspathIndexFileLocation() { + return null; + } + + /** + * Returns the location of the layer index file that should be written or {@code null} + * if not index is required. The result should include the filename and is relative to + * the root of the jar. + * @return the layer index file location + * @since 2.5.0 + */ + default String getLayersIndexFileLocation() { + return null; + } + /** * Returns if loader classes should be included to make the archive executable. * @return if the layout is executable diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layouts.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layouts.java index fed158d289ac..87e61c60a979 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layouts.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layouts.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,8 @@ * @author Phillip Webb * @author Dave Syer * @author Andy Wilkinson + * @author Madhura Bhave + * @author Scott Frederick * @since 1.0.0 */ public final class Layouts { @@ -68,7 +70,7 @@ public String getLauncherClassName() { } @Override - public String getLibraryDestination(String libraryName, LibraryScope scope) { + public String getLibraryLocation(String libraryName, LibraryScope scope) { return "BOOT-INF/lib/"; } @@ -82,6 +84,16 @@ public String getRepackagedClassesLocation() { return "BOOT-INF/classes/"; } + @Override + public String getClasspathIndexFileLocation() { + return "BOOT-INF/classpath.idx"; + } + + @Override + public String getLayersIndexFileLocation() { + return "BOOT-INF/layers.idx"; + } + @Override public boolean isExecutable() { return true; @@ -123,15 +135,15 @@ public boolean isExecutable() { */ public static class War implements Layout { - private static final Map SCOPE_DESTINATIONS; + private static final Map SCOPE_LOCATION; static { - Map map = new HashMap<>(); - map.put(LibraryScope.COMPILE, "WEB-INF/lib/"); - map.put(LibraryScope.CUSTOM, "WEB-INF/lib/"); - map.put(LibraryScope.RUNTIME, "WEB-INF/lib/"); - map.put(LibraryScope.PROVIDED, "WEB-INF/lib-provided/"); - SCOPE_DESTINATIONS = Collections.unmodifiableMap(map); + Map locations = new HashMap<>(); + locations.put(LibraryScope.COMPILE, "WEB-INF/lib/"); + locations.put(LibraryScope.CUSTOM, "WEB-INF/lib/"); + locations.put(LibraryScope.RUNTIME, "WEB-INF/lib/"); + locations.put(LibraryScope.PROVIDED, "WEB-INF/lib-provided/"); + SCOPE_LOCATION = Collections.unmodifiableMap(locations); } @Override @@ -140,8 +152,8 @@ public String getLauncherClassName() { } @Override - public String getLibraryDestination(String libraryName, LibraryScope scope) { - return SCOPE_DESTINATIONS.get(scope); + public String getLibraryLocation(String libraryName, LibraryScope scope) { + return SCOPE_LOCATION.get(scope); } @Override @@ -149,6 +161,11 @@ public String getClassesLocation() { return "WEB-INF/classes/"; } + @Override + public String getLayersIndexFileLocation() { + return "WEB-INF/layers.idx"; + } + @Override public boolean isExecutable() { return true; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Library.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Library.java index b9ca6c75c5a9..20315ce7f4d0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Library.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Library.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,11 +17,15 @@ package org.springframework.boot.loader.tools; import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; /** * Encapsulates information about a single library that may be packed into the archive. * * @author Phillip Webb + * @author Scott Frederick * @since 1.1.2 * @see Libraries */ @@ -33,15 +37,21 @@ public class Library { private final LibraryScope scope; + private final LibraryCoordinates coordinates; + private final boolean unpackRequired; + private final boolean local; + + private final boolean included; + /** * Create a new {@link Library}. * @param file the source file * @param scope the scope of the library */ public Library(File file, LibraryScope scope) { - this(file, scope, false); + this(null, file, scope, null, false, false, true); } /** @@ -49,7 +59,10 @@ public Library(File file, LibraryScope scope) { * @param file the source file * @param scope the scope of the library * @param unpackRequired if the library needs to be unpacked before it can be used + * @deprecated since 2.4.8 for removal in 2.6.0 in favor of + * {@link #Library(String, File, LibraryScope, LibraryCoordinates, boolean, boolean, boolean)} */ + @Deprecated public Library(File file, LibraryScope scope, boolean unpackRequired) { this(null, file, scope, unpackRequired); } @@ -61,12 +74,72 @@ public Library(File file, LibraryScope scope, boolean unpackRequired) { * @param file the source file * @param scope the scope of the library * @param unpackRequired if the library needs to be unpacked before it can be used + * @deprecated since 2.4.8 for removal in 2.6.0 in favor of + * {@link #Library(String, File, LibraryScope, LibraryCoordinates, boolean, boolean, boolean)} */ + @Deprecated public Library(String name, File file, LibraryScope scope, boolean unpackRequired) { + this(name, file, scope, null, unpackRequired); + } + + /** + * Create a new {@link Library}. + * @param name the name of the library as it should be written or {@code null} to use + * the file name + * @param file the source file + * @param scope the scope of the library + * @param coordinates the library coordinates or {@code null} + * @param unpackRequired if the library needs to be unpacked before it can be used + * @deprecated since 2.4.8 for removal in 2.6.0 in favor of + * {@link #Library(String, File, LibraryScope, LibraryCoordinates, boolean, boolean, boolean)} + */ + @Deprecated + public Library(String name, File file, LibraryScope scope, LibraryCoordinates coordinates, boolean unpackRequired) { + this(name, file, scope, coordinates, unpackRequired, false); + } + + /** + * Create a new {@link Library}. + * @param name the name of the library as it should be written or {@code null} to use + * the file name + * @param file the source file + * @param scope the scope of the library + * @param coordinates the library coordinates or {@code null} + * @param unpackRequired if the library needs to be unpacked before it can be used + * @param local if the library is local (part of the same build) to the application + * that is being packaged + * @since 2.4.0 + * @deprecated since 2.4.8 for removal in 2.6.0 in favor of + * {@link #Library(String, File, LibraryScope, LibraryCoordinates, boolean, boolean, boolean)} + */ + @Deprecated + public Library(String name, File file, LibraryScope scope, LibraryCoordinates coordinates, boolean unpackRequired, + boolean local) { + this(name, file, scope, coordinates, unpackRequired, local, true); + } + + /** + * Create a new {@link Library}. + * @param name the name of the library as it should be written or {@code null} to use + * the file name + * @param file the source file + * @param scope the scope of the library + * @param coordinates the library coordinates or {@code null} + * @param unpackRequired if the library needs to be unpacked before it can be used + * @param local if the library is local (part of the same build) to the application + * that is being packaged + * @param included if the library is included in the fat jar + * @since 2.4.8 + */ + public Library(String name, File file, LibraryScope scope, LibraryCoordinates coordinates, boolean unpackRequired, + boolean local, boolean included) { this.name = (name != null) ? name : file.getName(); this.file = file; this.scope = scope; + this.coordinates = coordinates; this.unpackRequired = unpackRequired; + this.local = local; + this.included = included; } /** @@ -85,6 +158,15 @@ public File getFile() { return this.file; } + /** + * Open a stream that provides the content of the source file. + * @return the file content + * @throws IOException on error + */ + InputStream openStream() throws IOException { + return new FileInputStream(this.file); + } + /** * Return the scope of the library. * @return the scope @@ -93,6 +175,14 @@ public LibraryScope getScope() { return this.scope; } + /** + * Return the {@linkplain LibraryCoordinates coordinates} of the library. + * @return the coordinates + */ + public LibraryCoordinates getCoordinates() { + return this.coordinates; + } + /** * Return if the file cannot be used directly as a nested jar and needs to be * unpacked. @@ -102,4 +192,25 @@ public boolean isUnpackRequired() { return this.unpackRequired; } + long getLastModified() { + return this.file.lastModified(); + } + + /** + * Return if the library is local (part of the same build) to the application that is + * being packaged. + * @return if the library is local + */ + public boolean isLocal() { + return this.local; + } + + /** + * Return if the library is included in the fat jar. + * @return if the library is included + */ + public boolean isIncluded() { + return this.included; + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LibraryCoordinates.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LibraryCoordinates.java new file mode 100644 index 000000000000..5b77a3b17d8f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LibraryCoordinates.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.tools; + +/** + * Encapsulates information about the artifact coordinates of a library. + * + * @author Scott Frederick + * @author Phillip Webb + * @since 2.3.0 + */ +public interface LibraryCoordinates { + + /** + * Return the group ID of the coordinates. + * @return the group ID + */ + String getGroupId(); + + /** + * Return the artifact ID of the coordinates. + * @return the artifact ID + */ + String getArtifactId(); + + /** + * Return the version of the coordinates. + * @return the version + */ + String getVersion(); + + /** + * Factory method to create {@link LibraryCoordinates} with the specified values. + * @param groupId the group ID + * @param artifactId the artifact ID + * @param version the version + * @return a new {@link LibraryCoordinates} instance + */ + static LibraryCoordinates of(String groupId, String artifactId, String version) { + return new DefaultLibraryCoordinates(groupId, artifactId, version); + } + + /** + * Utility method that returns the given coordinates using the standard + * {@code group:artifact:version} form. + * @param coordinates the coordinates to convert (may be {@code null}) + * @return the standard notation form or {@code "::"} when the coordinates are null + */ + static String toStandardNotationString(LibraryCoordinates coordinates) { + if (coordinates == null) { + return "::"; + } + StringBuilder builder = new StringBuilder(); + builder.append((coordinates.getGroupId() != null) ? coordinates.getGroupId() : ""); + builder.append(":"); + builder.append((coordinates.getArtifactId() != null) ? coordinates.getArtifactId() : ""); + builder.append(":"); + builder.append((coordinates.getVersion() != null) ? coordinates.getVersion() : ""); + return builder.toString(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/MainClassFinder.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/MainClassFinder.java index a44888362012..9a4709df89bf 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/MainClassFinder.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/MainClassFinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -64,71 +64,71 @@ public abstract class MainClassFinder { private static final FileFilter CLASS_FILE_FILTER = MainClassFinder::isClassFile; - private static final FileFilter PACKAGE_FOLDER_FILTER = MainClassFinder::isPackageFolder; + private static final FileFilter PACKAGE_DIRECTORY_FILTER = MainClassFinder::isPackageDirectory; private static boolean isClassFile(File file) { return file.isFile() && file.getName().endsWith(DOT_CLASS); } - private static boolean isPackageFolder(File file) { + private static boolean isPackageDirectory(File file) { return file.isDirectory() && !file.getName().startsWith("."); } /** - * Find the main class from a given folder. - * @param rootFolder the root folder to search + * Find the main class from a given directory. + * @param rootDirectory the root directory to search * @return the main class or {@code null} - * @throws IOException if the folder cannot be read + * @throws IOException if the directory cannot be read */ - public static String findMainClass(File rootFolder) throws IOException { - return doWithMainClasses(rootFolder, MainClass::getName); + public static String findMainClass(File rootDirectory) throws IOException { + return doWithMainClasses(rootDirectory, MainClass::getName); } /** - * Find a single main class from the given {@code rootFolder}. - * @param rootFolder the root folder to search + * Find a single main class from the given {@code rootDirectory}. + * @param rootDirectory the root directory to search * @return the main class or {@code null} - * @throws IOException if the folder cannot be read + * @throws IOException if the directory cannot be read */ - public static String findSingleMainClass(File rootFolder) throws IOException { - return findSingleMainClass(rootFolder, null); + public static String findSingleMainClass(File rootDirectory) throws IOException { + return findSingleMainClass(rootDirectory, null); } /** - * Find a single main class from the given {@code rootFolder}. A main class annotated - * with an annotation with the given {@code annotationName} will be preferred over a - * main class with no such annotation. - * @param rootFolder the root folder to search + * Find a single main class from the given {@code rootDirectory}. A main class + * annotated with an annotation with the given {@code annotationName} will be + * preferred over a main class with no such annotation. + * @param rootDirectory the root directory to search * @param annotationName the name of the annotation that may be present on the main * class * @return the main class or {@code null} - * @throws IOException if the folder cannot be read + * @throws IOException if the directory cannot be read */ - public static String findSingleMainClass(File rootFolder, String annotationName) throws IOException { + public static String findSingleMainClass(File rootDirectory, String annotationName) throws IOException { SingleMainClassCallback callback = new SingleMainClassCallback(annotationName); - MainClassFinder.doWithMainClasses(rootFolder, callback); + MainClassFinder.doWithMainClasses(rootDirectory, callback); return callback.getMainClassName(); } /** * Perform the given callback operation on all main classes from the given root - * folder. + * directory. * @param the result type - * @param rootFolder the root folder + * @param rootDirectory the root directory * @param callback the callback * @return the first callback result or {@code null} * @throws IOException in case of I/O errors */ - static T doWithMainClasses(File rootFolder, MainClassCallback callback) throws IOException { - if (!rootFolder.exists()) { + static T doWithMainClasses(File rootDirectory, MainClassCallback callback) throws IOException { + if (!rootDirectory.exists()) { return null; // nothing to do } - if (!rootFolder.isDirectory()) { - throw new IllegalArgumentException("Invalid root folder '" + rootFolder + "'"); + if (!rootDirectory.isDirectory()) { + throw new IllegalArgumentException("Invalid root directory '" + rootDirectory + "'"); } - String prefix = rootFolder.getAbsolutePath() + "/"; + String prefix = rootDirectory.getAbsolutePath() + "/"; Deque stack = new ArrayDeque<>(); - stack.push(rootFolder); + stack.push(rootDirectory); while (!stack.isEmpty()) { File file = stack.pop(); if (file.isFile()) { @@ -144,7 +144,7 @@ static T doWithMainClasses(File rootFolder, MainClassCallback callback) t } } if (file.isDirectory()) { - pushAllSorted(stack, file.listFiles(PACKAGE_FOLDER_FILTER)); + pushAllSorted(stack, file.listFiles(PACKAGE_DIRECTORY_FILTER)); pushAllSorted(stack, file.listFiles(CLASS_FILE_FILTER)); } } @@ -381,10 +381,7 @@ public boolean equals(Object obj) { return false; } MainClass other = (MainClass) obj; - if (!this.name.equals(other.name)) { - return false; - } - return true; + return this.name.equals(other.name); } @Override diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java new file mode 100644 index 000000000000..5a1f5b68aece --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java @@ -0,0 +1,543 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.tools; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.stream.Collectors; + +import org.apache.commons.compress.archivers.jar.JarArchiveEntry; + +import org.springframework.boot.loader.tools.AbstractJarWriter.EntryTransformer; +import org.springframework.boot.loader.tools.AbstractJarWriter.UnpackHandler; +import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Abstract base class for packagers. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @author Stephane Nicoll + * @author Madhura Bhave + * @author Scott Frederick + * @since 2.3.0 + */ +public abstract class Packager { + + private static final String MAIN_CLASS_ATTRIBUTE = "Main-Class"; + + private static final String START_CLASS_ATTRIBUTE = "Start-Class"; + + private static final String BOOT_VERSION_ATTRIBUTE = "Spring-Boot-Version"; + + private static final String BOOT_CLASSES_ATTRIBUTE = "Spring-Boot-Classes"; + + private static final String BOOT_LIB_ATTRIBUTE = "Spring-Boot-Lib"; + + private static final String BOOT_CLASSPATH_INDEX_ATTRIBUTE = "Spring-Boot-Classpath-Index"; + + private static final String BOOT_LAYERS_INDEX_ATTRIBUTE = "Spring-Boot-Layers-Index"; + + private static final byte[] ZIP_FILE_HEADER = new byte[] { 'P', 'K', 3, 4 }; + + private static final long FIND_WARNING_TIMEOUT = TimeUnit.SECONDS.toMillis(10); + + private static final String SPRING_BOOT_APPLICATION_CLASS_NAME = "org.springframework.boot.autoconfigure.SpringBootApplication"; + + private final List mainClassTimeoutListeners = new ArrayList<>(); + + private String mainClass; + + private final File source; + + private File backupFile; + + private Layout layout; + + private LayoutFactory layoutFactory; + + private Layers layers; + + private LayersIndex layersIndex; + + private boolean includeRelevantJarModeJars = true; + + /** + * Create a new {@link Packager} instance. + * @param source the source archive file to package + */ + protected Packager(File source) { + this(source, null); + } + + /** + * Create a new {@link Packager} instance. + * @param source the source archive file to package + * @param layoutFactory the layout factory to use or {@code null} + * @deprecated since 2.3.10 for removal in 2.5 in favor of {@link #Packager(File)} and + * {@link #setLayoutFactory(LayoutFactory)} + */ + @Deprecated + protected Packager(File source, LayoutFactory layoutFactory) { + Assert.notNull(source, "Source file must not be null"); + Assert.isTrue(source.exists() && source.isFile(), + () -> "Source must refer to an existing file, got " + source.getAbsolutePath()); + this.source = source.getAbsoluteFile(); + this.layoutFactory = layoutFactory; + } + + /** + * Add a listener that will be triggered to display a warning if searching for the + * main class takes too long. + * @param listener the listener to add + */ + public void addMainClassTimeoutWarningListener(MainClassTimeoutWarningListener listener) { + this.mainClassTimeoutListeners.add(listener); + } + + /** + * Sets the main class that should be run. If not specified the value from the + * MANIFEST will be used, or if no manifest entry is found the archive will be + * searched for a suitable class. + * @param mainClass the main class name + */ + public void setMainClass(String mainClass) { + this.mainClass = mainClass; + } + + /** + * Sets the layout to use for the jar. Defaults to {@link Layouts#forFile(File)}. + * @param layout the layout + */ + public void setLayout(Layout layout) { + Assert.notNull(layout, "Layout must not be null"); + this.layout = layout; + } + + /** + * Sets the layout factory for the jar. The factory can be used when no specific + * layout is specified. + * @param layoutFactory the layout factory to set + */ + public void setLayoutFactory(LayoutFactory layoutFactory) { + this.layoutFactory = layoutFactory; + } + + /** + * Sets the layers that should be used in the jar. + * @param layers the jar layers + */ + public void setLayers(Layers layers) { + Assert.notNull(layers, "Layers must not be null"); + this.layers = layers; + this.layersIndex = new LayersIndex(layers); + } + + /** + * Sets the {@link File} to use to backup the original source. + * @param backupFile the file to use to backup the original source + */ + protected void setBackupFile(File backupFile) { + this.backupFile = backupFile; + } + + /** + * Sets if jarmode jars relevant for the packaging should be automatically included. + * @param includeRelevantJarModeJars if relevant jars are included + */ + public void setIncludeRelevantJarModeJars(boolean includeRelevantJarModeJars) { + this.includeRelevantJarModeJars = includeRelevantJarModeJars; + } + + protected final boolean isAlreadyPackaged() { + return isAlreadyPackaged(this.source); + } + + protected final boolean isAlreadyPackaged(File file) { + try (JarFile jarFile = new JarFile(file)) { + Manifest manifest = jarFile.getManifest(); + return (manifest != null && manifest.getMainAttributes().getValue(BOOT_VERSION_ATTRIBUTE) != null); + } + catch (IOException ex) { + throw new IllegalStateException("Error reading archive file", ex); + } + } + + protected final void write(JarFile sourceJar, Libraries libraries, AbstractJarWriter writer) throws IOException { + Assert.notNull(libraries, "Libraries must not be null"); + write(sourceJar, writer, new PackagedLibraries(libraries)); + } + + private void write(JarFile sourceJar, AbstractJarWriter writer, PackagedLibraries libraries) throws IOException { + if (isLayered()) { + writer.useLayers(this.layers, this.layersIndex); + } + writer.writeManifest(buildManifest(sourceJar)); + writeLoaderClasses(writer); + writer.writeEntries(sourceJar, getEntityTransformer(), libraries.getUnpackHandler(), + libraries.getLibraryLookup()); + libraries.write(writer); + if (isLayered()) { + writeLayerIndex(writer); + } + } + + private void writeLoaderClasses(AbstractJarWriter writer) throws IOException { + Layout layout = getLayout(); + if (layout instanceof CustomLoaderLayout) { + ((CustomLoaderLayout) getLayout()).writeLoadedClasses(writer); + } + else if (layout.isExecutable()) { + writer.writeLoaderClasses(); + } + } + + private void writeLayerIndex(AbstractJarWriter writer) throws IOException { + String name = this.layout.getLayersIndexFileLocation(); + if (StringUtils.hasLength(name)) { + Layer layer = this.layers.getLayer(name); + this.layersIndex.add(layer, name); + writer.writeEntry(name, this.layersIndex::writeTo); + } + } + + private EntryTransformer getEntityTransformer() { + if (getLayout() instanceof RepackagingLayout) { + return new RepackagingEntryTransformer((RepackagingLayout) getLayout()); + } + return EntryTransformer.NONE; + } + + private boolean isZip(InputStreamSupplier supplier) { + try { + try (InputStream inputStream = supplier.openStream()) { + return isZip(inputStream); + } + } + catch (IOException ex) { + return false; + } + } + + private boolean isZip(InputStream inputStream) throws IOException { + for (byte magicByte : ZIP_FILE_HEADER) { + if (inputStream.read() != magicByte) { + return false; + } + } + return true; + } + + private Manifest buildManifest(JarFile source) throws IOException { + Manifest manifest = createInitialManifest(source); + addMainAndStartAttributes(source, manifest); + addBootAttributes(manifest.getMainAttributes()); + return manifest; + } + + private Manifest createInitialManifest(JarFile source) throws IOException { + if (source.getManifest() != null) { + return new Manifest(source.getManifest()); + } + Manifest manifest = new Manifest(); + manifest.getMainAttributes().putValue("Manifest-Version", "1.0"); + return manifest; + } + + private void addMainAndStartAttributes(JarFile source, Manifest manifest) throws IOException { + String mainClass = getMainClass(source, manifest); + String launcherClass = getLayout().getLauncherClassName(); + if (launcherClass != null) { + Assert.state(mainClass != null, "Unable to find main class"); + manifest.getMainAttributes().putValue(MAIN_CLASS_ATTRIBUTE, launcherClass); + manifest.getMainAttributes().putValue(START_CLASS_ATTRIBUTE, mainClass); + } + else if (mainClass != null) { + manifest.getMainAttributes().putValue(MAIN_CLASS_ATTRIBUTE, mainClass); + } + } + + private String getMainClass(JarFile source, Manifest manifest) throws IOException { + if (this.mainClass != null) { + return this.mainClass; + } + String attributeValue = manifest.getMainAttributes().getValue(MAIN_CLASS_ATTRIBUTE); + if (attributeValue != null) { + return attributeValue; + } + return findMainMethodWithTimeoutWarning(source); + } + + private String findMainMethodWithTimeoutWarning(JarFile source) throws IOException { + long startTime = System.currentTimeMillis(); + String mainMethod = findMainMethod(source); + long duration = System.currentTimeMillis() - startTime; + if (duration > FIND_WARNING_TIMEOUT) { + for (MainClassTimeoutWarningListener listener : this.mainClassTimeoutListeners) { + listener.handleTimeoutWarning(duration, mainMethod); + } + } + return mainMethod; + } + + protected String findMainMethod(JarFile source) throws IOException { + return MainClassFinder.findSingleMainClass(source, getLayout().getClassesLocation(), + SPRING_BOOT_APPLICATION_CLASS_NAME); + } + + /** + * Return the {@link File} to use to backup the original source. + * @return the file to use to backup the original source + */ + public final File getBackupFile() { + if (this.backupFile != null) { + return this.backupFile; + } + return new File(this.source.getParentFile(), this.source.getName() + ".original"); + } + + protected final File getSource() { + return this.source; + } + + protected final Layout getLayout() { + if (this.layout == null) { + Layout createdLayout = getLayoutFactory().getLayout(this.source); + Assert.state(createdLayout != null, "Unable to detect layout"); + this.layout = createdLayout; + } + return this.layout; + } + + private LayoutFactory getLayoutFactory() { + if (this.layoutFactory != null) { + return this.layoutFactory; + } + List factories = SpringFactoriesLoader.loadFactories(LayoutFactory.class, null); + if (factories.isEmpty()) { + return new DefaultLayoutFactory(); + } + Assert.state(factories.size() == 1, "No unique LayoutFactory found"); + return factories.get(0); + } + + private void addBootAttributes(Attributes attributes) { + attributes.putValue(BOOT_VERSION_ATTRIBUTE, getClass().getPackage().getImplementationVersion()); + addBootAttributesForLayout(attributes); + } + + private void addBootAttributesForLayout(Attributes attributes) { + Layout layout = getLayout(); + if (layout instanceof RepackagingLayout) { + attributes.putValue(BOOT_CLASSES_ATTRIBUTE, ((RepackagingLayout) layout).getRepackagedClassesLocation()); + } + else { + attributes.putValue(BOOT_CLASSES_ATTRIBUTE, layout.getClassesLocation()); + } + putIfHasLength(attributes, BOOT_LIB_ATTRIBUTE, getLayout().getLibraryLocation("", LibraryScope.COMPILE)); + putIfHasLength(attributes, BOOT_CLASSPATH_INDEX_ATTRIBUTE, layout.getClasspathIndexFileLocation()); + if (isLayered()) { + putIfHasLength(attributes, BOOT_LAYERS_INDEX_ATTRIBUTE, layout.getLayersIndexFileLocation()); + } + } + + private void putIfHasLength(Attributes attributes, String name, String value) { + if (StringUtils.hasLength(value)) { + attributes.putValue(name, value); + } + } + + private boolean isLayered() { + return this.layers != null; + } + + /** + * Callback interface used to present a warning when finding the main class takes too + * long. + */ + @FunctionalInterface + public interface MainClassTimeoutWarningListener { + + /** + * Handle a timeout warning. + * @param duration the amount of time it took to find the main method + * @param mainMethod the main method that was actually found + */ + void handleTimeoutWarning(long duration, String mainMethod); + + } + + /** + * An {@code EntryTransformer} that renames entries by applying a prefix. + */ + private static final class RepackagingEntryTransformer implements EntryTransformer { + + private final RepackagingLayout layout; + + private RepackagingEntryTransformer(RepackagingLayout layout) { + this.layout = layout; + } + + @Override + public JarArchiveEntry transform(JarArchiveEntry entry) { + if (entry.getName().equals("META-INF/INDEX.LIST")) { + return null; + } + if (!isTransformable(entry)) { + return entry; + } + String transformedName = transformName(entry.getName()); + JarArchiveEntry transformedEntry = new JarArchiveEntry(transformedName); + transformedEntry.setTime(entry.getTime()); + transformedEntry.setSize(entry.getSize()); + transformedEntry.setMethod(entry.getMethod()); + if (entry.getComment() != null) { + transformedEntry.setComment(entry.getComment()); + } + transformedEntry.setCompressedSize(entry.getCompressedSize()); + transformedEntry.setCrc(entry.getCrc()); + if (entry.getCreationTime() != null) { + transformedEntry.setCreationTime(entry.getCreationTime()); + } + if (entry.getExtra() != null) { + transformedEntry.setExtra(entry.getExtra()); + } + if (entry.getLastAccessTime() != null) { + transformedEntry.setLastAccessTime(entry.getLastAccessTime()); + } + if (entry.getLastModifiedTime() != null) { + transformedEntry.setLastModifiedTime(entry.getLastModifiedTime()); + } + return transformedEntry; + } + + private String transformName(String name) { + return this.layout.getRepackagedClassesLocation() + name; + } + + private boolean isTransformable(JarArchiveEntry entry) { + String name = entry.getName(); + if (name.startsWith("META-INF/")) { + return name.equals("META-INF/aop.xml") || name.endsWith(".kotlin_module"); + } + return !name.startsWith("BOOT-INF/") && !name.equals("module-info.class"); + } + + } + + /** + * An {@link UnpackHandler} that determines that an entry needs to be unpacked if a + * library that requires unpacking has a matching entry name. + */ + private final class PackagedLibraries { + + private final Map libraries = new LinkedHashMap<>(); + + private final UnpackHandler unpackHandler; + + private final Function libraryLookup; + + PackagedLibraries(Libraries libraries) throws IOException { + libraries.doWithLibraries((library) -> { + if (isZip(library::openStream)) { + addLibrary(library); + } + }); + if (isLayered() && Packager.this.includeRelevantJarModeJars) { + addLibrary(JarModeLibrary.LAYER_TOOLS); + } + this.unpackHandler = new PackagedLibrariesUnpackHandler(); + this.libraryLookup = this::lookup; + } + + private void addLibrary(Library library) { + String location = getLayout().getLibraryLocation(library.getName(), library.getScope()); + if (location != null) { + String path = location + library.getName(); + Library existing = this.libraries.putIfAbsent(path, library); + Assert.state(existing == null, () -> "Duplicate library " + library.getName()); + } + } + + private Library lookup(JarEntry entry) { + return this.libraries.get(entry.getName()); + } + + UnpackHandler getUnpackHandler() { + return this.unpackHandler; + } + + Function getLibraryLookup() { + return this.libraryLookup; + } + + void write(AbstractJarWriter writer) throws IOException { + List writtenPaths = new ArrayList<>(); + for (Entry entry : this.libraries.entrySet()) { + String path = entry.getKey(); + Library library = entry.getValue(); + if (library.isIncluded()) { + String location = path.substring(0, path.lastIndexOf('/') + 1); + writer.writeNestedLibrary(location, library); + writtenPaths.add(path); + } + } + writeClasspathIndexIfNecessary(writtenPaths, getLayout(), writer); + } + + private void writeClasspathIndexIfNecessary(List paths, Layout layout, AbstractJarWriter writer) + throws IOException { + if (layout.getClasspathIndexFileLocation() != null) { + List names = paths.stream().map((path) -> "- \"" + path + "\"").collect(Collectors.toList()); + writer.writeIndexFile(layout.getClasspathIndexFileLocation(), names); + } + } + + private class PackagedLibrariesUnpackHandler implements UnpackHandler { + + @Override + public boolean requiresUnpack(String name) { + Library library = PackagedLibraries.this.libraries.get(name); + return library != null && library.isUnpackRequired(); + } + + @Override + public String sha1Hash(String name) throws IOException { + Library library = PackagedLibraries.this.libraries.get(name); + Assert.notNull(library, () -> "No library found for entry name '" + name + "'"); + return Digest.sha1(library::openStream); + } + + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java index ccf8426af65d..e5d66796ac7b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,25 +17,11 @@ package org.springframework.boot.loader.tools; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.concurrent.TimeUnit; +import java.nio.file.attribute.FileTime; import java.util.jar.JarFile; -import java.util.jar.Manifest; -import org.apache.commons.compress.archivers.jar.JarArchiveEntry; - -import org.springframework.boot.loader.tools.JarWriter.EntryTransformer; -import org.springframework.boot.loader.tools.JarWriter.UnpackHandler; -import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.util.Assert; -import org.springframework.util.StringUtils; /** * Utility class that can be used to repackage an archive so that it can be executed using @@ -44,71 +30,32 @@ * @author Phillip Webb * @author Andy Wilkinson * @author Stephane Nicoll + * @author Madhura Bhave + * @author Scott Frederick * @since 1.0.0 */ -public class Repackager { - - private static final String MAIN_CLASS_ATTRIBUTE = "Main-Class"; - - private static final String START_CLASS_ATTRIBUTE = "Start-Class"; - - private static final String BOOT_VERSION_ATTRIBUTE = "Spring-Boot-Version"; - - private static final String BOOT_LIB_ATTRIBUTE = "Spring-Boot-Lib"; - - private static final String BOOT_CLASSES_ATTRIBUTE = "Spring-Boot-Classes"; - - private static final byte[] ZIP_FILE_HEADER = new byte[] { 'P', 'K', 3, 4 }; - - private static final long FIND_WARNING_TIMEOUT = TimeUnit.SECONDS.toMillis(10); - - private static final String SPRING_BOOT_APPLICATION_CLASS_NAME = "org.springframework.boot.autoconfigure.SpringBootApplication"; - - private List mainClassTimeoutListeners = new ArrayList<>(); - - private String mainClass; +public class Repackager extends Packager { private boolean backupSource = true; - private final File source; - - private Layout layout; - - private LayoutFactory layoutFactory; - - public Repackager(File source) { - this(source, null); - } - - public Repackager(File source, LayoutFactory layoutFactory) { - if (source == null) { - throw new IllegalArgumentException("Source file must be provided"); - } - if (!source.exists() || !source.isFile()) { - throw new IllegalArgumentException( - "Source must refer to an existing file, got " + source.getAbsolutePath()); - } - this.source = source.getAbsoluteFile(); - this.layoutFactory = layoutFactory; - } - /** - * Add a listener that will be triggered to display a warning if searching for the - * main class takes too long. - * @param listener the listener to add + * Create a new {@link Repackager} instance. + * @param source the source archive file to package */ - public void addMainClassTimeoutWarningListener(MainClassTimeoutWarningListener listener) { - this.mainClassTimeoutListeners.add(listener); + public Repackager(File source) { + super(source); } /** - * Sets the main class that should be run. If not specified the value from the - * MANIFEST will be used, or if no manifest entry is found the archive will be - * searched for a suitable class. - * @param mainClass the main class name + * Create a new {@link Repackager} instance. + * @param source the source archive file to package + * @param layoutFactory the layout factory to use or {@code null} + * @deprecated since 2.3.10 for removal in 2.5 in favor of {@link #Repackager(File)} + * and {@link #setLayoutFactory(LayoutFactory)} */ - public void setMainClass(String mainClass) { - this.mainClass = mainClass; + @Deprecated + public Repackager(File source, LayoutFactory layoutFactory) { + super(source, layoutFactory); } /** @@ -119,33 +66,13 @@ public void setBackupSource(boolean backupSource) { this.backupSource = backupSource; } - /** - * Sets the layout to use for the jar. Defaults to {@link Layouts#forFile(File)}. - * @param layout the layout - */ - public void setLayout(Layout layout) { - if (layout == null) { - throw new IllegalArgumentException("Layout must not be null"); - } - this.layout = layout; - } - - /** - * Sets the layout factory for the jar. The factory can be used when no specific - * layout is specified. - * @param layoutFactory the layout factory to set - */ - public void setLayoutFactory(LayoutFactory layoutFactory) { - this.layoutFactory = layoutFactory; - } - /** * Repackage the source file so that it can be run using '{@literal java -jar}'. * @param libraries the libraries required to run the archive * @throws IOException if the file cannot be repackaged */ public void repackage(Libraries libraries) throws IOException { - repackage(this.source, libraries); + repackage(getSource(), libraries); } /** @@ -169,165 +96,58 @@ public void repackage(File destination, Libraries libraries) throws IOException * @since 1.3.0 */ public void repackage(File destination, Libraries libraries, LaunchScript launchScript) throws IOException { - if (destination == null || destination.isDirectory()) { - throw new IllegalArgumentException("Invalid destination"); - } - if (libraries == null) { - throw new IllegalArgumentException("Libraries must not be null"); - } - if (this.layout == null) { - this.layout = getLayoutFactory().getLayout(this.source); - } + repackage(destination, libraries, launchScript, null); + } + + /** + * Repackage to the given destination so that it can be launched using ' + * {@literal java -jar}'. + * @param destination the destination file (may be the same as the source) + * @param libraries the libraries required to run the archive + * @param launchScript an optional launch script prepended to the front of the jar + * @param lastModifiedTime an optional last modified time to apply to the archive and + * its contents + * @throws IOException if the file cannot be repackaged + * @since 2.3.0 + */ + public void repackage(File destination, Libraries libraries, LaunchScript launchScript, FileTime lastModifiedTime) + throws IOException { + Assert.isTrue(destination != null && !destination.isDirectory(), "Invalid destination"); + Layout layout = getLayout(); // get layout early destination = destination.getAbsoluteFile(); - File workingSource = this.source; - if (alreadyRepackaged() && this.source.equals(destination)) { + File source = getSource(); + if (isAlreadyPackaged() && source.equals(destination)) { return; } - if (this.source.equals(destination)) { + File workingSource = source; + if (source.equals(destination)) { workingSource = getBackupFile(); workingSource.delete(); - renameFile(this.source, workingSource); + renameFile(source, workingSource); } destination.delete(); try { - try (JarFile jarFileSource = new JarFile(workingSource)) { - repackage(jarFileSource, destination, libraries, launchScript); + try (JarFile sourceJar = new JarFile(workingSource)) { + repackage(sourceJar, destination, libraries, launchScript, lastModifiedTime); } } finally { - if (!this.backupSource && !this.source.equals(workingSource)) { + if (!this.backupSource && !source.equals(workingSource)) { deleteFile(workingSource); } } } - private LayoutFactory getLayoutFactory() { - if (this.layoutFactory != null) { - return this.layoutFactory; - } - List factories = SpringFactoriesLoader.loadFactories(LayoutFactory.class, null); - if (factories.isEmpty()) { - return new DefaultLayoutFactory(); - } - Assert.state(factories.size() == 1, "No unique LayoutFactory found"); - return factories.get(0); - } - - /** - * Return the {@link File} to use to backup the original source. - * @return the file to use to backup the original source - */ - public final File getBackupFile() { - return new File(this.source.getParentFile(), this.source.getName() + ".original"); - } - - private boolean alreadyRepackaged() throws IOException { - try (JarFile jarFile = new JarFile(this.source)) { - Manifest manifest = jarFile.getManifest(); - return (manifest != null && manifest.getMainAttributes().getValue(BOOT_VERSION_ATTRIBUTE) != null); - } - } - - private void repackage(JarFile sourceJar, File destination, Libraries libraries, LaunchScript launchScript) - throws IOException { - WritableLibraries writeableLibraries = new WritableLibraries(libraries); - try (JarWriter writer = new JarWriter(destination, launchScript)) { - writer.writeManifest(buildManifest(sourceJar)); - writeLoaderClasses(writer); - if (this.layout instanceof RepackagingLayout) { - writer.writeEntries(sourceJar, - new RenamingEntryTransformer(((RepackagingLayout) this.layout).getRepackagedClassesLocation()), - writeableLibraries); - } - else { - writer.writeEntries(sourceJar, writeableLibraries); - } - writeableLibraries.write(writer); - } - } - - private void writeLoaderClasses(JarWriter writer) throws IOException { - if (this.layout instanceof CustomLoaderLayout) { - ((CustomLoaderLayout) this.layout).writeLoadedClasses(writer); + private void repackage(JarFile sourceJar, File destination, Libraries libraries, LaunchScript launchScript, + FileTime lastModifiedTime) throws IOException { + try (JarWriter writer = new JarWriter(destination, launchScript, lastModifiedTime)) { + write(sourceJar, libraries, writer); } - else if (this.layout.isExecutable()) { - writer.writeLoaderClasses(); + if (lastModifiedTime != null) { + destination.setLastModified(lastModifiedTime.toMillis()); } } - private boolean isZip(File file) { - try { - try (FileInputStream fileInputStream = new FileInputStream(file)) { - return isZip(fileInputStream); - } - } - catch (IOException ex) { - return false; - } - } - - private boolean isZip(InputStream inputStream) throws IOException { - for (byte magicByte : ZIP_FILE_HEADER) { - if (inputStream.read() != magicByte) { - return false; - } - } - return true; - } - - private Manifest buildManifest(JarFile source) throws IOException { - Manifest manifest = source.getManifest(); - if (manifest == null) { - manifest = new Manifest(); - manifest.getMainAttributes().putValue("Manifest-Version", "1.0"); - } - manifest = new Manifest(manifest); - String startClass = this.mainClass; - if (startClass == null) { - startClass = manifest.getMainAttributes().getValue(MAIN_CLASS_ATTRIBUTE); - } - if (startClass == null) { - startClass = findMainMethodWithTimeoutWarning(source); - } - String launcherClassName = this.layout.getLauncherClassName(); - if (launcherClassName != null) { - manifest.getMainAttributes().putValue(MAIN_CLASS_ATTRIBUTE, launcherClassName); - if (startClass == null) { - throw new IllegalStateException("Unable to find main class"); - } - manifest.getMainAttributes().putValue(START_CLASS_ATTRIBUTE, startClass); - } - else if (startClass != null) { - manifest.getMainAttributes().putValue(MAIN_CLASS_ATTRIBUTE, startClass); - } - String bootVersion = getClass().getPackage().getImplementationVersion(); - manifest.getMainAttributes().putValue(BOOT_VERSION_ATTRIBUTE, bootVersion); - manifest.getMainAttributes().putValue(BOOT_CLASSES_ATTRIBUTE, (this.layout instanceof RepackagingLayout) - ? ((RepackagingLayout) this.layout).getRepackagedClassesLocation() : this.layout.getClassesLocation()); - String lib = this.layout.getLibraryDestination("", LibraryScope.COMPILE); - if (StringUtils.hasLength(lib)) { - manifest.getMainAttributes().putValue(BOOT_LIB_ATTRIBUTE, lib); - } - return manifest; - } - - private String findMainMethodWithTimeoutWarning(JarFile source) throws IOException { - long startTime = System.currentTimeMillis(); - String mainMethod = findMainMethod(source); - long duration = System.currentTimeMillis() - startTime; - if (duration > FIND_WARNING_TIMEOUT) { - for (MainClassTimeoutWarningListener listener : this.mainClassTimeoutListeners) { - listener.handleTimeoutWarning(duration, mainMethod); - } - } - return mainMethod; - } - - protected String findMainMethod(JarFile source) throws IOException { - return MainClassFinder.findSingleMainClass(source, this.layout.getClassesLocation(), - SPRING_BOOT_APPLICATION_CLASS_NAME); - } - private void renameFile(File file, File dest) { if (!file.renameTo(dest)) { throw new IllegalStateException("Unable to rename '" + file + "' to '" + dest + "'"); @@ -340,115 +160,4 @@ private void deleteFile(File file) { } } - /** - * Callback interface used to present a warning when finding the main class takes too - * long. - */ - @FunctionalInterface - public interface MainClassTimeoutWarningListener { - - /** - * Handle a timeout warning. - * @param duration the amount of time it took to find the main method - * @param mainMethod the main method that was actually found - */ - void handleTimeoutWarning(long duration, String mainMethod); - - } - - /** - * An {@code EntryTransformer} that renames entries by applying a prefix. - */ - private static final class RenamingEntryTransformer implements EntryTransformer { - - private final String namePrefix; - - private RenamingEntryTransformer(String namePrefix) { - this.namePrefix = namePrefix; - } - - @Override - public JarArchiveEntry transform(JarArchiveEntry entry) { - if (entry.getName().equals("META-INF/INDEX.LIST")) { - return null; - } - if ((entry.getName().startsWith("META-INF/") && !entry.getName().equals("META-INF/aop.xml") - && !entry.getName().endsWith(".kotlin_module")) || entry.getName().startsWith("BOOT-INF/") - || entry.getName().equals("module-info.class")) { - return entry; - } - JarArchiveEntry renamedEntry = new JarArchiveEntry(this.namePrefix + entry.getName()); - renamedEntry.setTime(entry.getTime()); - renamedEntry.setSize(entry.getSize()); - renamedEntry.setMethod(entry.getMethod()); - if (entry.getComment() != null) { - renamedEntry.setComment(entry.getComment()); - } - renamedEntry.setCompressedSize(entry.getCompressedSize()); - renamedEntry.setCrc(entry.getCrc()); - if (entry.getCreationTime() != null) { - renamedEntry.setCreationTime(entry.getCreationTime()); - } - if (entry.getExtra() != null) { - renamedEntry.setExtra(entry.getExtra()); - } - if (entry.getLastAccessTime() != null) { - renamedEntry.setLastAccessTime(entry.getLastAccessTime()); - } - if (entry.getLastModifiedTime() != null) { - renamedEntry.setLastModifiedTime(entry.getLastModifiedTime()); - } - return renamedEntry; - } - - } - - /** - * An {@link UnpackHandler} that determines that an entry needs to be unpacked if a - * library that requires unpacking has a matching entry name. - */ - private final class WritableLibraries implements UnpackHandler { - - private final Map libraryEntryNames = new LinkedHashMap<>(); - - private WritableLibraries(Libraries libraries) throws IOException { - libraries.doWithLibraries((library) -> { - if (isZip(library.getFile())) { - String libraryDestination = Repackager.this.layout.getLibraryDestination(library.getName(), - library.getScope()); - if (libraryDestination != null) { - Library existing = this.libraryEntryNames.putIfAbsent(libraryDestination + library.getName(), - library); - if (existing != null) { - throw new IllegalStateException("Duplicate library " + library.getName()); - } - } - } - }); - } - - @Override - public boolean requiresUnpack(String name) { - Library library = this.libraryEntryNames.get(name); - return library != null && library.isUnpackRequired(); - } - - @Override - public String sha1Hash(String name) throws IOException { - Library library = this.libraryEntryNames.get(name); - if (library == null) { - throw new IllegalArgumentException("No library found for entry name '" + name + "'"); - } - return FileUtils.sha1Hash(library.getFile()); - } - - private void write(JarWriter writer) throws IOException { - for (Entry entry : this.libraryEntryNames.entrySet()) { - writer.writeNestedLibrary(entry.getKey().substring(0, entry.getKey().lastIndexOf('/') + 1), - entry.getValue()); - } - } - - } - } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/RepackagingLayout.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/RepackagingLayout.java index 953e0864d2bc..992c04887c7e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/RepackagingLayout.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/RepackagingLayout.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/RunProcess.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/RunProcess.java index c4438a06f89e..1c35c7555926 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/RunProcess.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/RunProcess.java @@ -113,13 +113,33 @@ public Process getRunningProcess() { * @return {@code true} if stopped */ public boolean handleSigInt() { - // if the process has just ended, probably due to this SIGINT, consider handled. - if (hasJustEnded()) { + if (allowChildToHandleSigInt()) { return true; } return doKill(); } + private boolean allowChildToHandleSigInt() { + Process process = this.process; + if (process == null) { + return true; + } + long end = System.currentTimeMillis() + 5000; + while (System.currentTimeMillis() < end) { + if (!process.isAlive()) { + return true; + } + try { + Thread.sleep(500); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + return false; + } + } + return false; + } + /** * Kill this process. */ diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/SizeCalculatingEntryWriter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/SizeCalculatingEntryWriter.java new file mode 100644 index 000000000000..1fbb23b8c502 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/SizeCalculatingEntryWriter.java @@ -0,0 +1,150 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.tools; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.springframework.util.StreamUtils; + +/** + * {@link EntryWriter} that always provides size information. + * + * @author Phillip Webb + */ +final class SizeCalculatingEntryWriter implements EntryWriter { + + static final int THRESHOLD = 1024 * 20; + + private final Object content; + + private final int size; + + private SizeCalculatingEntryWriter(EntryWriter entryWriter) throws IOException { + SizeCalculatingOutputStream outputStream = new SizeCalculatingOutputStream(); + try { + entryWriter.write(outputStream); + } + finally { + outputStream.close(); + } + this.content = outputStream.getContent(); + this.size = outputStream.getSize(); + } + + @Override + public void write(OutputStream outputStream) throws IOException { + InputStream inputStream = getContentInputStream(); + copy(inputStream, outputStream); + } + + private InputStream getContentInputStream() throws FileNotFoundException { + if (this.content instanceof File) { + return new FileInputStream((File) this.content); + } + return new ByteArrayInputStream((byte[]) this.content); + } + + private void copy(InputStream inputStream, OutputStream outputStream) throws IOException { + try { + StreamUtils.copy(inputStream, outputStream); + } + finally { + inputStream.close(); + } + } + + @Override + public int size() { + return this.size; + } + + static EntryWriter get(EntryWriter entryWriter) throws IOException { + if (entryWriter == null || entryWriter.size() != -1) { + return entryWriter; + } + return new SizeCalculatingEntryWriter(entryWriter); + } + + /** + * {@link OutputStream} to calculate the size and allow content to be written again. + */ + private static class SizeCalculatingOutputStream extends OutputStream { + + private int size = 0; + + private File tempFile; + + private OutputStream outputStream; + + SizeCalculatingOutputStream() { + this.outputStream = new ByteArrayOutputStream(); + } + + @Override + public void write(int b) throws IOException { + write(new byte[] { (byte) b }, 0, 1); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + int updatedSize = this.size + len; + if (updatedSize > THRESHOLD && this.outputStream instanceof ByteArrayOutputStream) { + this.outputStream = convertToFileOutputStream((ByteArrayOutputStream) this.outputStream); + } + this.outputStream.write(b, off, len); + this.size = updatedSize; + } + + private OutputStream convertToFileOutputStream(ByteArrayOutputStream byteArrayOutputStream) throws IOException { + initializeTempFile(); + FileOutputStream fileOutputStream = new FileOutputStream(this.tempFile); + StreamUtils.copy(byteArrayOutputStream.toByteArray(), fileOutputStream); + return fileOutputStream; + } + + private void initializeTempFile() throws IOException { + if (this.tempFile == null) { + this.tempFile = File.createTempFile("springboot-", "-entrycontent"); + this.tempFile.deleteOnExit(); + } + } + + @Override + public void close() throws IOException { + this.outputStream.close(); + } + + Object getContent() { + return (this.outputStream instanceof ByteArrayOutputStream) + ? ((ByteArrayOutputStream) this.outputStream).toByteArray() : this.tempFile; + } + + int getSize() { + return this.size; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/StandardLayers.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/StandardLayers.java new file mode 100644 index 000000000000..544c213f3ad9 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/StandardLayers.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.tools; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Stream; + +/** + * Base class for the standard set of {@link Layers}. Defines the following layers: + *

      + *
    1. "dependencies" - For non snapshot dependencies
    2. + *
    3. "spring-boot-loader" - For classes from {@code spring-boot-loader} used to launch a + * fat jar
    4. + *
    5. "snapshot-dependencies" - For snapshot dependencies
    6. + *
    7. "application" - For application classes and resources
    8. + *
    + * + * @author Madhura Bhave + * @author Phillip Webb + * @since 2.3.0 + */ +public abstract class StandardLayers implements Layers { + + /** + * The dependencies layer. + */ + public static final Layer DEPENDENCIES = new Layer("dependencies"); + + /** + * The spring boot loader layer. + */ + public static final Layer SPRING_BOOT_LOADER = new Layer("spring-boot-loader"); + + /** + * The snapshot dependencies layer. + */ + public static final Layer SNAPSHOT_DEPENDENCIES = new Layer("snapshot-dependencies"); + + /** + * The application layer. + */ + public static final Layer APPLICATION = new Layer("application"); + + private static final List LAYERS; + static { + List layers = new ArrayList<>(); + layers.add(DEPENDENCIES); + layers.add(SPRING_BOOT_LOADER); + layers.add(SNAPSHOT_DEPENDENCIES); + layers.add(APPLICATION); + LAYERS = Collections.unmodifiableList(layers); + } + + @Override + public Iterator iterator() { + return LAYERS.iterator(); + } + + @Override + public Stream stream() { + return LAYERS.stream(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/ZipHeaderPeekInputStream.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/ZipHeaderPeekInputStream.java new file mode 100644 index 000000000000..8e2325d57955 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/ZipHeaderPeekInputStream.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.tools; + +import java.io.ByteArrayInputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; + +/** + * {@link InputStream} that can peek ahead at zip header bytes. + * + * @author Phillip Webb + */ +class ZipHeaderPeekInputStream extends FilterInputStream { + + private static final byte[] ZIP_HEADER = new byte[] { 0x50, 0x4b, 0x03, 0x04 }; + + private final byte[] header; + + private final int headerLength; + + private int position; + + private ByteArrayInputStream headerStream; + + protected ZipHeaderPeekInputStream(InputStream in) throws IOException { + super(in); + this.header = new byte[4]; + this.headerLength = in.read(this.header); + this.headerStream = new ByteArrayInputStream(this.header, 0, this.headerLength); + } + + @Override + public int read() throws IOException { + int read = (this.headerStream != null) ? this.headerStream.read() : -1; + if (read != -1) { + this.position++; + if (this.position >= this.headerLength) { + this.headerStream = null; + } + return read; + } + return super.read(); + } + + @Override + public int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int read = (this.headerStream != null) ? this.headerStream.read(b, off, len) : -1; + if (read <= 0) { + return readRemainder(b, off, len); + } + this.position += read; + if (read < len) { + int remainderRead = readRemainder(b, off + read, len - read); + if (remainderRead > 0) { + read += remainderRead; + } + } + if (this.position >= this.headerLength) { + this.headerStream = null; + } + return read; + } + + boolean hasZipHeader() { + return Arrays.equals(this.header, ZIP_HEADER); + } + + private int readRemainder(byte[] b, int off, int len) throws IOException { + int read = super.read(b, off, len); + if (read > 0) { + this.position += read; + } + return read; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/ApplicationContentFilter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/ApplicationContentFilter.java new file mode 100644 index 000000000000..734ea7e2605b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/ApplicationContentFilter.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.tools.layer; + +import org.springframework.util.AntPathMatcher; +import org.springframework.util.Assert; + +/** + * {@link ContentFilter} that matches application items based on an Ant-style path + * pattern. + * + * @author Madhura Bhave + * @author Phillip Webb + * @since 2.3.0 + */ +public class ApplicationContentFilter implements ContentFilter { + + private static final AntPathMatcher MATCHER = new AntPathMatcher(); + + private final String pattern; + + public ApplicationContentFilter(String pattern) { + Assert.hasText(pattern, "Pattern must not be empty"); + this.pattern = pattern; + } + + @Override + public boolean matches(String path) { + return MATCHER.match(this.pattern, path); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/ContentFilter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/ContentFilter.java new file mode 100644 index 000000000000..02ce50b193e7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/ContentFilter.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.tools.layer; + +/** + * Callback interface that can be used to filter layer contents. + * + * @param the content type + * @author Madhura Bhave + * @author Phillip Webb + * @since 2.3.0 + */ +@FunctionalInterface +public interface ContentFilter { + + /** + * Return if the filter matches the specified item. + * @param item the item to test + * @return if the filter matches + */ + boolean matches(T item); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/ContentSelector.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/ContentSelector.java new file mode 100644 index 000000000000..3f63bf79bb48 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/ContentSelector.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.tools.layer; + +import org.springframework.boot.loader.tools.Layer; + +/** + * Strategy used by {@link CustomLayers} to select the layer of an item. + * + * @param the content type + * @author Madhura Bhave + * @author Phillip Webb + * @since 2.3.0 + * @see IncludeExcludeContentSelector + */ +public interface ContentSelector { + + /** + * Return the {@link Layer} that the selector represents. + * @return the named layer + */ + Layer getLayer(); + + /** + * Returns {@code true} if the specified item is contained in this selection. + * @param item the item to test + * @return if the item is contained + */ + boolean contains(T item); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/CustomLayers.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/CustomLayers.java new file mode 100644 index 000000000000..c8334b5b1607 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/CustomLayers.java @@ -0,0 +1,99 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.tools.layer; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.springframework.boot.loader.tools.Layer; +import org.springframework.boot.loader.tools.Layers; +import org.springframework.boot.loader.tools.Library; +import org.springframework.util.Assert; + +/** + * Custom {@link Layers} implementation where layer content is selected by the user. + * + * @author Madhura Bhave + * @author Phillip Webb + * @since 2.3.0 + */ +public class CustomLayers implements Layers { + + private final List layers; + + private final List> applicationSelectors; + + private final List> librarySelectors; + + public CustomLayers(List layers, List> applicationSelectors, + List> librarySelectors) { + Assert.notNull(layers, "Layers must not be null"); + Assert.notNull(applicationSelectors, "ApplicationSelectors must not be null"); + validateSelectorLayers(applicationSelectors, layers); + Assert.notNull(librarySelectors, "LibrarySelectors must not be null"); + validateSelectorLayers(librarySelectors, layers); + this.layers = new ArrayList<>(layers); + this.applicationSelectors = new ArrayList<>(applicationSelectors); + this.librarySelectors = new ArrayList<>(librarySelectors); + } + + private static void validateSelectorLayers(List> selectors, List layers) { + for (ContentSelector selector : selectors) { + validateSelectorLayers(selector, layers); + } + } + + private static void validateSelectorLayers(ContentSelector selector, List layers) { + Layer layer = selector.getLayer(); + Assert.state(layer != null, "Missing content selector layer"); + Assert.state(layers.contains(layer), + () -> "Content selector layer '" + selector.getLayer() + "' not found in " + layers); + } + + @Override + public Iterator iterator() { + return this.layers.iterator(); + } + + @Override + public Stream stream() { + return this.layers.stream(); + } + + @Override + public Layer getLayer(String resourceName) { + return selectLayer(resourceName, this.applicationSelectors, () -> "Resource '" + resourceName + "'"); + } + + @Override + public Layer getLayer(Library library) { + return selectLayer(library, this.librarySelectors, () -> "Library '" + library.getName() + "'"); + } + + private Layer selectLayer(T item, List> selectors, Supplier name) { + for (ContentSelector selector : selectors) { + if (selector.contains(item)) { + return selector.getLayer(); + } + } + throw new IllegalStateException(name.get() + " did not match any layer"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/IncludeExcludeContentSelector.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/IncludeExcludeContentSelector.java new file mode 100644 index 000000000000..194a57a3e25b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/IncludeExcludeContentSelector.java @@ -0,0 +1,96 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.tools.layer; + +import java.util.Collections; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.springframework.boot.loader.tools.Layer; +import org.springframework.util.Assert; + +/** + * {@link ContentSelector} backed by {@code include}/{@code exclude} {@link ContentFilter + * filters}. + * + * @param the content type + * @author Madhura Bhave + * @author Phillip Webb + * @since 2.3.0 + */ +public class IncludeExcludeContentSelector implements ContentSelector { + + private final Layer layer; + + private final List> includes; + + private final List> excludes; + + public IncludeExcludeContentSelector(Layer layer, List> includes, + List> excludes) { + this(layer, includes, excludes, Function.identity()); + } + + public IncludeExcludeContentSelector(Layer layer, List includes, List excludes, + Function> filterFactory) { + Assert.notNull(layer, "Layer must not be null"); + Assert.notNull(filterFactory, "FilterFactory must not be null"); + this.layer = layer; + this.includes = (includes != null) ? adapt(includes, filterFactory) : Collections.emptyList(); + this.excludes = (excludes != null) ? adapt(excludes, filterFactory) : Collections.emptyList(); + } + + private List> adapt(List list, Function> mapper) { + return list.stream().map(mapper).collect(Collectors.toList()); + } + + @Override + public Layer getLayer() { + return this.layer; + } + + @Override + public boolean contains(T item) { + return isIncluded(item) && !isExcluded(item); + } + + private boolean isIncluded(T item) { + if (this.includes.isEmpty()) { + return true; + } + for (ContentFilter include : this.includes) { + if (include.matches(item)) { + return true; + } + } + return false; + } + + private boolean isExcluded(T item) { + if (this.excludes.isEmpty()) { + return false; + } + for (ContentFilter exclude : this.excludes) { + if (exclude.matches(item)) { + return true; + } + } + return false; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/LibraryContentFilter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/LibraryContentFilter.java new file mode 100644 index 000000000000..295994c6bd95 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/LibraryContentFilter.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.tools.layer; + +import java.util.regex.Pattern; + +import org.springframework.boot.loader.tools.Library; +import org.springframework.boot.loader.tools.LibraryCoordinates; +import org.springframework.util.Assert; + +/** + * {@link ContentFilter} that matches {@link Library} items based on a coordinates + * pattern. + * + * @author Madhura Bhave + * @author Scott Frederick + * @author Phillip Webb + * @since 2.3.0 + */ +public class LibraryContentFilter implements ContentFilter { + + private final Pattern pattern; + + public LibraryContentFilter(String coordinatesPattern) { + Assert.hasText(coordinatesPattern, "CoordinatesPattern must not be empty"); + StringBuilder regex = new StringBuilder(); + for (int i = 0; i < coordinatesPattern.length(); i++) { + char c = coordinatesPattern.charAt(i); + if (c == '.') { + regex.append("\\."); + } + else if (c == '*') { + regex.append(".*"); + } + else { + regex.append(c); + } + } + this.pattern = Pattern.compile(regex.toString()); + } + + @Override + public boolean matches(Library library) { + return this.pattern.matcher(LibraryCoordinates.toStandardNotationString(library.getCoordinates())).matches(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/package-info.java new file mode 100644 index 000000000000..8020397e62ef --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/layer/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Classes used to support layer customization. + * + */ +package org.springframework.boot.loader.tools.layer; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/resources/org/springframework/boot/loader/tools/launch.script b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/resources/org/springframework/boot/loader/tools/launch.script index a7bbd78b09d1..ce3115c43711 100755 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/resources/org/springframework/boot/loader/tools/launch.script +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/resources/org/springframework/boot/loader/tools/launch.script @@ -128,6 +128,9 @@ log_file="$LOG_FOLDER/$LOG_FILENAME" # shellcheck disable=SC2012 [[ $(id -u) == "0" ]] && run_user=$(ls -ld "$jarfile" | awk '{print $3}') +# Ensure the user actually exists +id -u "$run_user" &> /dev/null || unset run_user + # Run as user specified in RUN_AS_USER if [[ -n "$RUN_AS_USER" ]]; then if ! [[ "$action" =~ ^(status|run)$ ]]; then @@ -223,7 +226,7 @@ stop() { do_stop() { kill "$1" &> /dev/null || { echoRed "Unable to kill process $1"; return 1; } - for i in $(seq 1 $STOP_WAIT_TIME); do + for ((i = 1; i <= STOP_WAIT_TIME; i++)); do isRunning "$1" || { echoGreen "Stopped [$1]"; rm -f "$2"; return 0; } [[ $i -eq STOP_WAIT_TIME/2 ]] && kill "$1" &> /dev/null sleep 1 @@ -241,7 +244,7 @@ force_stop() { do_force_stop() { kill -9 "$1" &> /dev/null || { echoRed "Unable to kill process $1"; return 1; } - for i in $(seq 1 $STOP_WAIT_TIME); do + for ((i = 1; i <= STOP_WAIT_TIME; i++)); do isRunning "$1" || { echoGreen "Stopped [$1]"; rm -f "$2"; return 0; } [[ $i -eq STOP_WAIT_TIME/2 ]] && kill -9 "$1" &> /dev/null sleep 1 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java new file mode 100644 index 000000000000..83389a6e18a0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java @@ -0,0 +1,711 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.tools; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.jar.Attributes; +import java.util.jar.Manifest; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.zip.Deflater; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.zeroturnaround.zip.ZipUtil; + +import org.springframework.boot.loader.tools.sample.ClassWithMainMethod; +import org.springframework.boot.loader.tools.sample.ClassWithoutMainMethod; +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Abstract class for {@link Packager} based tests. + * + * @param

    The packager type + * @author Phillip Webb + * @author Andy Wilkinson + * @author Madhura Bhave + */ +abstract class AbstractPackagerTests

    { + + protected static final Libraries NO_LIBRARIES = (callback) -> { + }; + + private static final long JAN_1_1980; + static { + Calendar calendar = Calendar.getInstance(); + calendar.set(1980, 0, 1, 0, 0, 0); + calendar.set(Calendar.MILLISECOND, 0); + JAN_1_1980 = calendar.getTime().getTime(); + } + + private static final long JAN_1_1985; + static { + Calendar calendar = Calendar.getInstance(); + calendar.set(1985, 0, 1, 0, 0, 0); + calendar.set(Calendar.MILLISECOND, 0); + JAN_1_1985 = calendar.getTime().getTime(); + } + + @TempDir + File tempDir; + + protected TestJarFile testJarFile; + + @BeforeEach + void setup() throws IOException { + this.testJarFile = new TestJarFile(this.tempDir); + } + + @Test + void specificMainClass() throws Exception { + this.testJarFile.addClass("a/b/C.class", ClassWithoutMainMethod.class); + P packager = createPackager(); + packager.setMainClass("a.b.C"); + execute(packager, NO_LIBRARIES); + Manifest actualManifest = getPackagedManifest(); + assertThat(actualManifest.getMainAttributes().getValue("Main-Class")) + .isEqualTo("org.springframework.boot.loader.JarLauncher"); + assertThat(actualManifest.getMainAttributes().getValue("Start-Class")).isEqualTo("a.b.C"); + assertThat(hasPackagedLauncherClasses()).isTrue(); + } + + @Test + void mainClassFromManifest() throws Exception { + this.testJarFile.addClass("a/b/C.class", ClassWithoutMainMethod.class); + Manifest manifest = new Manifest(); + manifest.getMainAttributes().putValue("Manifest-Version", "1.0"); + manifest.getMainAttributes().putValue("Main-Class", "a.b.C"); + this.testJarFile.addManifest(manifest); + P packager = createPackager(); + execute(packager, NO_LIBRARIES); + Manifest actualManifest = getPackagedManifest(); + assertThat(actualManifest.getMainAttributes().getValue("Main-Class")) + .isEqualTo("org.springframework.boot.loader.JarLauncher"); + assertThat(actualManifest.getMainAttributes().getValue("Start-Class")).isEqualTo("a.b.C"); + assertThat(hasPackagedLauncherClasses()).isTrue(); + } + + @Test + void mainClassFound() throws Exception { + this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); + P packager = createPackager(); + execute(packager, NO_LIBRARIES); + Manifest actualManifest = getPackagedManifest(); + assertThat(actualManifest.getMainAttributes().getValue("Main-Class")) + .isEqualTo("org.springframework.boot.loader.JarLauncher"); + assertThat(actualManifest.getMainAttributes().getValue("Start-Class")).isEqualTo("a.b.C"); + assertThat(hasPackagedLauncherClasses()).isTrue(); + } + + @Test + void multipleMainClassFound() throws Exception { + this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); + this.testJarFile.addClass("a/b/D.class", ClassWithMainMethod.class); + P packager = createPackager(); + assertThatIllegalStateException().isThrownBy(() -> execute(packager, NO_LIBRARIES)).withMessageContaining( + "Unable to find a single main class from the following candidates [a.b.C, a.b.D]"); + } + + @Test + void noMainClass() throws Exception { + this.testJarFile.addClass("a/b/C.class", ClassWithoutMainMethod.class); + P packager = createPackager(this.testJarFile.getFile()); + assertThatIllegalStateException().isThrownBy(() -> execute(packager, NO_LIBRARIES)) + .withMessageContaining("Unable to find main class"); + } + + @Test + void noMainClassAndLayoutIsNone() throws Exception { + this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); + P packager = createPackager(); + packager.setLayout(new Layouts.None()); + execute(packager, NO_LIBRARIES); + Manifest actualManifest = getPackagedManifest(); + assertThat(actualManifest.getMainAttributes().getValue("Main-Class")).isEqualTo("a.b.C"); + assertThat(hasPackagedLauncherClasses()).isFalse(); + } + + @Test + void noMainClassAndLayoutIsNoneWithNoMain() throws Exception { + this.testJarFile.addClass("a/b/C.class", ClassWithoutMainMethod.class); + P packager = createPackager(); + packager.setLayout(new Layouts.None()); + execute(packager, NO_LIBRARIES); + Manifest actualManifest = getPackagedManifest(); + assertThat(actualManifest.getMainAttributes().getValue("Main-Class")).isNull(); + assertThat(hasPackagedLauncherClasses()).isFalse(); + } + + @Test + void nullLibraries() throws Exception { + this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); + P packager = createPackager(); + assertThatIllegalArgumentException().isThrownBy(() -> execute(packager, null)) + .withMessageContaining("Libraries must not be null"); + } + + @Test + void libraries() throws Exception { + TestJarFile libJar = new TestJarFile(this.tempDir); + libJar.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985); + File libJarFile = libJar.getFile(); + File libJarFileToUnpack = libJar.getFile(); + File libNonJarFile = new File(this.tempDir, "non-lib.jar"); + FileCopyUtils.copy(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 }, libNonJarFile); + this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); + this.testJarFile.addFile("BOOT-INF/lib/" + libJarFileToUnpack.getName(), libJarFileToUnpack); + libJarFile.setLastModified(JAN_1_1980); + P packager = createPackager(); + execute(packager, (callback) -> { + callback.library(newLibrary(libJarFile, LibraryScope.COMPILE, false)); + callback.library(newLibrary(libJarFileToUnpack, LibraryScope.COMPILE, true)); + callback.library(newLibrary(libNonJarFile, LibraryScope.COMPILE, false)); + }); + assertThat(hasPackagedEntry("BOOT-INF/lib/" + libJarFile.getName())).isTrue(); + assertThat(hasPackagedEntry("BOOT-INF/lib/" + libJarFileToUnpack.getName())).isTrue(); + assertThat(hasPackagedEntry("BOOT-INF/lib/" + libNonJarFile.getName())).isFalse(); + ZipEntry entry = getPackagedEntry("BOOT-INF/lib/" + libJarFile.getName()); + assertThat(entry.getTime()).isEqualTo(JAN_1_1985); + entry = getPackagedEntry("BOOT-INF/lib/" + libJarFileToUnpack.getName()); + assertThat(entry.getComment()).startsWith("UNPACK:"); + assertThat(entry.getComment()).hasSize(47); + } + + @Test + void classPathIndex() throws Exception { + TestJarFile libJar1 = new TestJarFile(this.tempDir); + libJar1.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985); + File libJarFile1 = libJar1.getFile(); + TestJarFile libJar2 = new TestJarFile(this.tempDir); + libJar2.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985); + File libJarFile2 = libJar2.getFile(); + TestJarFile libJar3 = new TestJarFile(this.tempDir); + libJar3.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985); + File libJarFile3 = libJar3.getFile(); + this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); + File file = this.testJarFile.getFile(); + P packager = createPackager(file); + execute(packager, (callback) -> { + callback.library(newLibrary(libJarFile1, LibraryScope.COMPILE, false)); + callback.library(newLibrary(libJarFile2, LibraryScope.COMPILE, false)); + callback.library(newLibrary(libJarFile3, LibraryScope.COMPILE, false)); + }); + assertThat(hasPackagedEntry("BOOT-INF/classpath.idx")).isTrue(); + String index = getPackagedEntryContent("BOOT-INF/classpath.idx"); + String[] libraries = index.split("\\r?\\n"); + List expected = Stream.of(libJarFile1, libJarFile2, libJarFile3) + .map((jar) -> "- \"BOOT-INF/lib/" + jar.getName() + "\"").collect(Collectors.toList()); + assertThat(Arrays.asList(libraries)).containsExactlyElementsOf(expected); + } + + @Test + void layersIndex() throws Exception { + TestJarFile libJar1 = new TestJarFile(this.tempDir); + libJar1.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985); + File libJarFile1 = libJar1.getFile(); + TestJarFile libJar2 = new TestJarFile(this.tempDir); + libJar2.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985); + File libJarFile2 = libJar2.getFile(); + TestJarFile libJar3 = new TestJarFile(this.tempDir); + libJar3.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985); + File libJarFile3 = libJar3.getFile(); + this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); + P packager = createPackager(); + TestLayers layers = new TestLayers(); + layers.addLibrary(libJarFile1, "0001"); + layers.addLibrary(libJarFile2, "0002"); + layers.addLibrary(libJarFile3, "0003"); + packager.setLayers(layers); + packager.setIncludeRelevantJarModeJars(false); + execute(packager, (callback) -> { + callback.library(newLibrary(libJarFile1, LibraryScope.COMPILE, false)); + callback.library(newLibrary(libJarFile2, LibraryScope.COMPILE, false)); + callback.library(newLibrary(libJarFile3, LibraryScope.COMPILE, false)); + }); + assertThat(hasPackagedEntry("BOOT-INF/classpath.idx")).isTrue(); + String classpathIndex = getPackagedEntryContent("BOOT-INF/classpath.idx"); + List expectedClasspathIndex = Stream.of(libJarFile1, libJarFile2, libJarFile3) + .map((file) -> "- \"BOOT-INF/lib/" + file.getName() + "\"").collect(Collectors.toList()); + assertThat(Arrays.asList(classpathIndex.split("\\n"))).containsExactlyElementsOf(expectedClasspathIndex); + assertThat(hasPackagedEntry("BOOT-INF/layers.idx")).isTrue(); + String layersIndex = getPackagedEntryContent("BOOT-INF/layers.idx"); + List expectedLayers = new ArrayList<>(); + expectedLayers.add("- 'default':"); + expectedLayers.add(" - 'BOOT-INF/classes/'"); + expectedLayers.add(" - 'BOOT-INF/classpath.idx'"); + expectedLayers.add(" - 'BOOT-INF/layers.idx'"); + expectedLayers.add(" - 'META-INF/'"); + expectedLayers.add(" - 'org/'"); + expectedLayers.add("- '0001':"); + expectedLayers.add(" - 'BOOT-INF/lib/" + libJarFile1.getName() + "'"); + expectedLayers.add("- '0002':"); + expectedLayers.add(" - 'BOOT-INF/lib/" + libJarFile2.getName() + "'"); + expectedLayers.add("- '0003':"); + expectedLayers.add(" - 'BOOT-INF/lib/" + libJarFile3.getName() + "'"); + assertThat(layersIndex.split("\\n")) + .containsExactly(expectedLayers.stream().map((s) -> s.replace('\'', '"')).toArray(String[]::new)); + } + + @Test + void layersEnabledAddJarModeJar() throws Exception { + this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); + P packager = createPackager(); + TestLayers layers = new TestLayers(); + packager.setLayers(layers); + execute(packager, Libraries.NONE); + assertThat(hasPackagedEntry("BOOT-INF/classpath.idx")).isTrue(); + String classpathIndex = getPackagedEntryContent("BOOT-INF/classpath.idx"); + assertThat(Arrays.asList(classpathIndex.split("\\n"))) + .containsExactly("- \"BOOT-INF/lib/spring-boot-jarmode-layertools.jar\""); + assertThat(hasPackagedEntry("BOOT-INF/layers.idx")).isTrue(); + String layersIndex = getPackagedEntryContent("BOOT-INF/layers.idx"); + List expectedLayers = new ArrayList<>(); + expectedLayers.add("- 'default':"); + expectedLayers.add(" - 'BOOT-INF/'"); + expectedLayers.add(" - 'META-INF/'"); + expectedLayers.add(" - 'org/'"); + assertThat(layersIndex.split("\\n")) + .containsExactly(expectedLayers.stream().map((s) -> s.replace('\'', '"')).toArray(String[]::new)); + } + + @Test + void duplicateLibraries() throws Exception { + TestJarFile libJar = new TestJarFile(this.tempDir); + libJar.addClass("a/b/C.class", ClassWithoutMainMethod.class); + File libJarFile = libJar.getFile(); + this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); + P packager = createPackager(); + assertThatIllegalStateException().isThrownBy(() -> execute(packager, (callback) -> { + callback.library(newLibrary(libJarFile, LibraryScope.COMPILE, false)); + callback.library(newLibrary(libJarFile, LibraryScope.COMPILE, false)); + })).withMessageContaining("Duplicate library"); + } + + @Test + void customLayout() throws Exception { + TestJarFile libJar = new TestJarFile(this.tempDir); + libJar.addClass("a/b/C.class", ClassWithoutMainMethod.class); + File libJarFile = libJar.getFile(); + this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); + P packager = createPackager(); + Layout layout = mock(Layout.class); + LibraryScope scope = mock(LibraryScope.class); + given(layout.getLauncherClassName()).willReturn("testLauncher"); + given(layout.getLibraryLocation(anyString(), eq(scope))).willReturn("test/"); + given(layout.getLibraryLocation(anyString(), eq(LibraryScope.COMPILE))).willReturn("test-lib/"); + packager.setLayout(layout); + execute(packager, (callback) -> callback.library(newLibrary(libJarFile, scope, false))); + assertThat(hasPackagedEntry("test/" + libJarFile.getName())).isTrue(); + assertThat(getPackagedManifest().getMainAttributes().getValue("Spring-Boot-Lib")).isEqualTo("test-lib/"); + assertThat(getPackagedManifest().getMainAttributes().getValue("Main-Class")).isEqualTo("testLauncher"); + } + + @Test + void customLayoutNoBootLib() throws Exception { + TestJarFile libJar = new TestJarFile(this.tempDir); + libJar.addClass("a/b/C.class", ClassWithoutMainMethod.class); + File libJarFile = libJar.getFile(); + this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); + P packager = createPackager(); + Layout layout = mock(Layout.class); + LibraryScope scope = mock(LibraryScope.class); + given(layout.getLauncherClassName()).willReturn("testLauncher"); + packager.setLayout(layout); + execute(packager, (callback) -> callback.library(newLibrary(libJarFile, scope, false))); + assertThat(getPackagedManifest().getMainAttributes().getValue("Spring-Boot-Lib")).isNull(); + assertThat(getPackagedManifest().getMainAttributes().getValue("Main-Class")).isEqualTo("testLauncher"); + } + + @Test + void springBootVersion() throws Exception { + this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); + P packager = createPackager(); + execute(packager, NO_LIBRARIES); + Manifest actualManifest = getPackagedManifest(); + assertThat(actualManifest.getMainAttributes()).containsKey(new Attributes.Name("Spring-Boot-Version")); + } + + @Test + void executableJarLayoutAttributes() throws Exception { + this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); + P packager = createPackager(); + execute(packager, NO_LIBRARIES); + Manifest actualManifest = getPackagedManifest(); + assertThat(actualManifest.getMainAttributes()).containsEntry(new Attributes.Name("Spring-Boot-Lib"), + "BOOT-INF/lib/"); + assertThat(actualManifest.getMainAttributes()).containsEntry(new Attributes.Name("Spring-Boot-Classes"), + "BOOT-INF/classes/"); + } + + @Test + void executableWarLayoutAttributes() throws Exception { + this.testJarFile.addClass("WEB-INF/classes/a/b/C.class", ClassWithMainMethod.class); + P packager = createPackager(this.testJarFile.getFile("war")); + execute(packager, NO_LIBRARIES); + Manifest actualManifest = getPackagedManifest(); + assertThat(actualManifest.getMainAttributes()).containsEntry(new Attributes.Name("Spring-Boot-Lib"), + "WEB-INF/lib/"); + assertThat(actualManifest.getMainAttributes()).containsEntry(new Attributes.Name("Spring-Boot-Classes"), + "WEB-INF/classes/"); + } + + @Test + void nullCustomLayout() throws Exception { + this.testJarFile.addClass("a/b/C.class", ClassWithoutMainMethod.class); + Packager packager = createPackager(); + assertThatIllegalArgumentException().isThrownBy(() -> packager.setLayout(null)) + .withMessageContaining("Layout must not be null"); + } + + @Test + void dontRecompressZips() throws Exception { + TestJarFile nested = new TestJarFile(this.tempDir); + nested.addClass("a/b/C.class", ClassWithoutMainMethod.class); + File nestedFile = nested.getFile(); + this.testJarFile.addFile("test/nested.jar", nestedFile); + this.testJarFile.addClass("A.class", ClassWithMainMethod.class); + P packager = createPackager(); + execute(packager, (callback) -> callback.library(newLibrary(nestedFile, LibraryScope.COMPILE, false))); + assertThat(getPackagedEntry("BOOT-INF/lib/" + nestedFile.getName()).getMethod()).isEqualTo(ZipEntry.STORED); + assertThat(getPackagedEntry("BOOT-INF/classes/test/nested.jar").getMethod()).isEqualTo(ZipEntry.STORED); + } + + @Test + void unpackLibrariesTakePrecedenceOverExistingSourceEntries() throws Exception { + TestJarFile nested = new TestJarFile(this.tempDir); + nested.addClass("a/b/C.class", ClassWithoutMainMethod.class); + File nestedFile = nested.getFile(); + String name = "BOOT-INF/lib/" + nestedFile.getName(); + this.testJarFile.addFile(name, nested.getFile()); + this.testJarFile.addClass("A.class", ClassWithMainMethod.class); + P packager = createPackager(); + execute(packager, (callback) -> callback.library(newLibrary(nestedFile, LibraryScope.COMPILE, true))); + assertThat(getPackagedEntry(name).getComment()).startsWith("UNPACK:"); + } + + @Test + void existingSourceEntriesTakePrecedenceOverStandardLibraries() throws Exception { + TestJarFile nested = new TestJarFile(this.tempDir); + nested.addClass("a/b/C.class", ClassWithoutMainMethod.class); + File nestedFile = nested.getFile(); + this.testJarFile.addFile("BOOT-INF/lib/" + nestedFile.getName(), nested.getFile()); + this.testJarFile.addClass("A.class", ClassWithMainMethod.class); + P packager = createPackager(); + long sourceLength = nestedFile.length(); + execute(packager, (callback) -> { + nestedFile.delete(); + File toZip = new File(this.tempDir, "to-zip"); + toZip.createNewFile(); + ZipUtil.packEntry(toZip, nestedFile); + callback.library(newLibrary(nestedFile, LibraryScope.COMPILE, false)); + }); + assertThat(getPackagedEntry("BOOT-INF/lib/" + nestedFile.getName()).getSize()).isEqualTo(sourceLength); + } + + @Test + void metaInfIndexListIsRemovedFromRepackagedJar() throws Exception { + this.testJarFile.addClass("A.class", ClassWithMainMethod.class); + File indexList = new File(this.tempDir, "INDEX.LIST"); + indexList.createNewFile(); + this.testJarFile.addFile("META-INF/INDEX.LIST", indexList); + P packager = createPackager(); + execute(packager, NO_LIBRARIES); + assertThat(getPackagedEntry("META-INF/INDEX.LIST")).isNull(); + } + + @Test + void customLayoutFactoryWithoutLayout() throws Exception { + this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); + P packager = createPackager(); + packager.setLayoutFactory(new TestLayoutFactory()); + execute(packager, NO_LIBRARIES); + assertThat(getPackagedEntry("test")).isNotNull(); + } + + @Test + void customLayoutFactoryWithLayout() throws Exception { + this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); + P packager = createPackager(); + packager.setLayoutFactory(new TestLayoutFactory()); + packager.setLayout(new Layouts.Jar()); + execute(packager, NO_LIBRARIES); + assertThat(getPackagedEntry("test")).isNull(); + } + + @Test + void metaInfAopXmlIsMovedBeneathBootInfClassesWhenRepackaged() throws Exception { + this.testJarFile.addClass("A.class", ClassWithMainMethod.class); + File aopXml = new File(this.tempDir, "aop.xml"); + aopXml.createNewFile(); + this.testJarFile.addFile("META-INF/aop.xml", aopXml); + P packager = createPackager(); + execute(packager, NO_LIBRARIES); + assertThat(getPackagedEntry("META-INF/aop.xml")).isNull(); + assertThat(getPackagedEntry("BOOT-INF/classes/META-INF/aop.xml")).isNotNull(); + } + + @Test + void allEntriesUseUnixPlatformAndUtf8NameEncoding() throws IOException { + this.testJarFile.addClass("A.class", ClassWithMainMethod.class); + P packager = createPackager(); + execute(packager, NO_LIBRARIES); + for (ZipArchiveEntry entry : getAllPackagedEntries()) { + assertThat(entry.getPlatform()).isEqualTo(ZipArchiveEntry.PLATFORM_UNIX); + assertThat(entry.getGeneralPurposeBit().usesUTF8ForNames()).isTrue(); + } + } + + @Test + void loaderIsWrittenFirstThenApplicationClassesThenLibraries() throws IOException { + this.testJarFile.addClass("com/example/Application.class", ClassWithMainMethod.class); + File libraryOne = createLibraryJar(); + File libraryTwo = createLibraryJar(); + File libraryThree = createLibraryJar(); + P packager = createPackager(); + execute(packager, (callback) -> { + callback.library(newLibrary(libraryOne, LibraryScope.COMPILE, false)); + callback.library(newLibrary(libraryTwo, LibraryScope.COMPILE, true)); + callback.library(newLibrary(libraryThree, LibraryScope.COMPILE, false)); + }); + assertThat(getPackagedEntryNames()).containsSubsequence("org/springframework/boot/loader/", + "BOOT-INF/classes/com/example/Application.class", "BOOT-INF/lib/" + libraryOne.getName(), + "BOOT-INF/lib/" + libraryTwo.getName(), "BOOT-INF/lib/" + libraryThree.getName()); + } + + @Test + void existingEntryThatMatchesUnpackLibraryIsMarkedForUnpack() throws IOException { + File library = createLibraryJar(); + this.testJarFile.addClass("WEB-INF/classes/com/example/Application.class", ClassWithMainMethod.class); + this.testJarFile.addFile("WEB-INF/lib/" + library.getName(), library); + P packager = createPackager(this.testJarFile.getFile("war")); + packager.setLayout(new Layouts.War()); + execute(packager, (callback) -> callback.library(newLibrary(library, LibraryScope.COMPILE, true))); + assertThat(getPackagedEntryNames()).containsSubsequence("org/springframework/boot/loader/", + "WEB-INF/classes/com/example/Application.class", "WEB-INF/lib/" + library.getName()); + ZipEntry unpackLibrary = getPackagedEntry("WEB-INF/lib/" + library.getName()); + assertThat(unpackLibrary.getComment()).startsWith("UNPACK:"); + } + + @Test + void layoutCanOmitLibraries() throws IOException { + TestJarFile libJar = new TestJarFile(this.tempDir); + libJar.addClass("a/b/C.class", ClassWithoutMainMethod.class); + File libJarFile = libJar.getFile(); + this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); + P packager = createPackager(); + Layout layout = mock(Layout.class); + LibraryScope scope = mock(LibraryScope.class); + packager.setLayout(layout); + execute(packager, (callback) -> callback.library(newLibrary(libJarFile, scope, false))); + assertThat(getPackagedEntryNames()).containsExactly("META-INF/", "META-INF/MANIFEST.MF", "a/", "a/b/", + "a/b/C.class"); + } + + @Test + void jarThatUsesCustomCompressionConfigurationCanBeRepackaged() throws IOException { + File source = new File(this.tempDir, "source.jar"); + ZipOutputStream output = new ZipOutputStream(new FileOutputStream(source)) { + { + this.def = new Deflater(Deflater.NO_COMPRESSION, true); + } + }; + byte[] data = new byte[1024 * 1024]; + new Random().nextBytes(data); + ZipEntry entry = new ZipEntry("entry.dat"); + output.putNextEntry(entry); + output.write(data); + output.closeEntry(); + output.close(); + P packager = createPackager(source); + packager.setMainClass("com.example.Main"); + execute(packager, NO_LIBRARIES); + } + + @Test + void moduleInfoClassRemainsInRootOfJarWhenRepackaged() throws Exception { + this.testJarFile.addClass("A.class", ClassWithMainMethod.class); + this.testJarFile.addClass("module-info.class", ClassWithoutMainMethod.class); + P packager = createPackager(); + execute(packager, NO_LIBRARIES); + assertThat(getPackagedEntry("module-info.class")).isNotNull(); + assertThat(getPackagedEntry("BOOT-INF/classes/module-info.class")).isNull(); + } + + @Test + void kotlinModuleMetadataMovesBeneathBootInfClassesWhenRepackaged() throws Exception { + this.testJarFile.addClass("A.class", ClassWithMainMethod.class); + File kotlinModule = new File(this.tempDir, "test.kotlin_module"); + kotlinModule.createNewFile(); + this.testJarFile.addFile("META-INF/test.kotlin_module", kotlinModule); + P packager = createPackager(); + execute(packager, NO_LIBRARIES); + assertThat(getPackagedEntry("META-INF/test.kotlin_module")).isNull(); + assertThat(getPackagedEntry("BOOT-INF/classes/META-INF/test.kotlin_module")).isNotNull(); + } + + @Test + void entryFiltering() throws Exception { + File webLibrary = createLibraryJar(); + File libraryOne = createLibraryJar(); + File libraryTwo = createLibraryJar(); + this.testJarFile.addClass("WEB-INF/classes/com/example/Application.class", ClassWithMainMethod.class); + this.testJarFile.addFile("WEB-INF/lib/" + webLibrary.getName(), webLibrary); + P packager = createPackager(this.testJarFile.getFile("war")); + packager.setLayout(new Layouts.War()); + execute(packager, (callback) -> { + callback.library(newLibrary(webLibrary, LibraryScope.COMPILE, false, false)); + callback.library(newLibrary(libraryOne, LibraryScope.COMPILE, false, false)); + callback.library(newLibrary(libraryTwo, LibraryScope.COMPILE, false, true)); + }); + Collection packagedEntryNames = getPackagedEntryNames(); + packagedEntryNames.removeIf((name) -> !name.endsWith(".jar")); + assertThat(packagedEntryNames).containsExactly("WEB-INF/lib/" + libraryTwo.getName()); + } + + private File createLibraryJar() throws IOException { + TestJarFile library = new TestJarFile(this.tempDir); + library.addClass("com/example/library/Library.class", ClassWithoutMainMethod.class); + return library.getFile(); + } + + private Library newLibrary(File file, LibraryScope scope, boolean unpackRequired) { + return new Library(null, file, scope, null, unpackRequired, false, true); + } + + private Library newLibrary(File file, LibraryScope scope, boolean unpackRequired, boolean included) { + return new Library(null, file, scope, null, unpackRequired, false, included); + } + + protected final P createPackager() throws IOException { + return createPackager(this.testJarFile.getFile()); + } + + protected abstract P createPackager(File source); + + protected abstract void execute(P packager, Libraries libraries) throws IOException; + + protected Collection getPackagedEntryNames() throws IOException { + return getAllPackagedEntries().stream().map(ZipArchiveEntry::getName).collect(Collectors.toList()); + } + + protected boolean hasPackagedLauncherClasses() throws IOException { + return hasPackagedEntry("org/springframework/boot/") + && hasPackagedEntry("org/springframework/boot/loader/JarLauncher.class"); + } + + private boolean hasPackagedEntry(String name) throws IOException { + return getPackagedEntry(name) != null; + } + + protected ZipEntry getPackagedEntry(String name) throws IOException { + return getAllPackagedEntries().stream().filter((entry) -> name.equals(entry.getName())).findFirst() + .orElse(null); + + } + + protected abstract Collection getAllPackagedEntries() throws IOException; + + protected abstract Manifest getPackagedManifest() throws IOException; + + protected abstract String getPackagedEntryContent(String name) throws IOException; + + static class TestLayoutFactory implements LayoutFactory { + + @Override + public Layout getLayout(File source) { + return new TestLayout(); + } + + } + + static class TestLayout extends Layouts.Jar implements CustomLoaderLayout { + + @Override + public void writeLoadedClasses(LoaderClassesWriter writer) throws IOException { + writer.writeEntry("test", new ByteArrayInputStream("test".getBytes())); + } + + } + + static class TestLayers implements Layers { + + private static final Layer DEFAULT_LAYER = new Layer("default"); + + private Set layers = new LinkedHashSet<>(); + + private Map libraries = new HashMap<>(); + + TestLayers() { + this.layers.add(DEFAULT_LAYER); + } + + void addLibrary(File jarFile, String layerName) { + Layer layer = new Layer(layerName); + this.layers.add(layer); + this.libraries.put(jarFile.getName(), layer); + } + + @Override + public Iterator iterator() { + return this.layers.iterator(); + } + + @Override + public Stream stream() { + return this.layers.stream(); + } + + @Override + public Layer getLayer(String name) { + return DEFAULT_LAYER; + } + + @Override + public Layer getLayer(Library library) { + String name = new File(library.getName()).getName(); + return this.libraries.getOrDefault(name, DEFAULT_LAYER); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/FileUtilsTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/FileUtilsTests.java index e24c84e159d8..1050e1ec5134 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/FileUtilsTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/FileUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,7 +43,7 @@ class FileUtilsTests { private File originDirectory; @BeforeEach - void init() throws IOException { + void init() { this.outputDirectory = new File(this.tempDir, "remove"); this.originDirectory = new File(this.tempDir, "keep"); this.outputDirectory.mkdirs(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/ImagePackagerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/ImagePackagerTests.java new file mode 100644 index 000000000000..f9760d04eff6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/ImagePackagerTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.tools; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; + +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; + +/** + * Tests for {@link ImagePackager} + * + * @author Phillip Webb + */ +class ImagePackagerTests extends AbstractPackagerTests { + + private Map entries; + + @Override + protected ImagePackager createPackager(File source) { + return new ImagePackager(source, null); + } + + @Override + protected void execute(ImagePackager packager, Libraries libraries) throws IOException { + this.entries = new LinkedHashMap<>(); + packager.packageImage(libraries, this::save); + } + + private void save(ZipEntry entry, EntryWriter writer) { + try { + this.entries.put((ZipArchiveEntry) entry, getContent(writer)); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + private byte[] getContent(EntryWriter writer) throws IOException { + if (writer == null) { + return null; + } + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + writer.write(outputStream); + return outputStream.toByteArray(); + } + + @Override + protected Collection getAllPackagedEntries() throws IOException { + return this.entries.keySet(); + } + + @Override + protected Manifest getPackagedManifest() throws IOException { + byte[] bytes = getEntryBytes("META-INF/MANIFEST.MF"); + return (bytes != null) ? new Manifest(new ByteArrayInputStream(bytes)) : null; + } + + @Override + protected String getPackagedEntryContent(String name) throws IOException { + byte[] bytes = getEntryBytes(name); + return (bytes != null) ? new String(bytes, StandardCharsets.UTF_8) : null; + } + + private byte[] getEntryBytes(String name) throws IOException { + ZipEntry entry = getPackagedEntry(name); + return (entry != null) ? this.entries.get(entry) : null; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/ImplicitLayerResolverTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/ImplicitLayerResolverTests.java new file mode 100644 index 000000000000..44350aafb42e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/ImplicitLayerResolverTests.java @@ -0,0 +1,94 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.tools; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ImplicitLayerResolver}. + * + * @author Madhura Bhave + * @author Phillip Webb + */ +class ImplicitLayerResolverTests { + + private Layers layers = Layers.IMPLICIT; + + @Test + void iteratorReturnsLayers() { + assertThat(this.layers).containsExactly(StandardLayers.DEPENDENCIES, StandardLayers.SPRING_BOOT_LOADER, + StandardLayers.SNAPSHOT_DEPENDENCIES, StandardLayers.APPLICATION); + } + + @Test + void getLayerWhenNameInResourceLocationReturnsApplicationLayer() { + assertThat(this.layers.getLayer("META-INF/resources/logo.gif")).isEqualTo(StandardLayers.APPLICATION); + assertThat(this.layers.getLayer("resources/logo.gif")).isEqualTo(StandardLayers.APPLICATION); + assertThat(this.layers.getLayer("static/logo.gif")).isEqualTo(StandardLayers.APPLICATION); + assertThat(this.layers.getLayer("public/logo.gif")).isEqualTo(StandardLayers.APPLICATION); + } + + @Test + void getLayerWhenNameIsClassInResourceLocationReturnsApplicationLayer() { + assertThat(this.layers.getLayer("META-INF/resources/Logo.class")).isEqualTo(StandardLayers.APPLICATION); + assertThat(this.layers.getLayer("resources/Logo.class")).isEqualTo(StandardLayers.APPLICATION); + assertThat(this.layers.getLayer("static/Logo.class")).isEqualTo(StandardLayers.APPLICATION); + assertThat(this.layers.getLayer("public/Logo.class")).isEqualTo(StandardLayers.APPLICATION); + } + + @Test + void getLayerWhenNameNotInResourceLocationReturnsApplicationLayer() { + assertThat(this.layers.getLayer("com/example/Application.class")).isEqualTo(StandardLayers.APPLICATION); + assertThat(this.layers.getLayer("com/example/application.properties")).isEqualTo(StandardLayers.APPLICATION); + } + + @Test + void getLayerWhenLoaderClassReturnsLoaderLayer() { + assertThat(this.layers.getLayer("org/springframework/boot/loader/Launcher.class")) + .isEqualTo(StandardLayers.SPRING_BOOT_LOADER); + assertThat(this.layers.getLayer("org/springframework/boot/loader/Utils.class")) + .isEqualTo(StandardLayers.SPRING_BOOT_LOADER); + } + + @Test + void getLayerWhenLibraryIsSnapshotReturnsSnapshotLayer() { + assertThat(this.layers.getLayer(mockLibrary("spring-boot.2.0.0.BUILD-SNAPSHOT.jar"))) + .isEqualTo(StandardLayers.SNAPSHOT_DEPENDENCIES); + assertThat(this.layers.getLayer(mockLibrary("spring-boot.2.0.0-SNAPSHOT.jar"))) + .isEqualTo(StandardLayers.SNAPSHOT_DEPENDENCIES); + assertThat(this.layers.getLayer(mockLibrary("spring-boot.2.0.0.SNAPSHOT.jar"))) + .isEqualTo(StandardLayers.SNAPSHOT_DEPENDENCIES); + } + + @Test + void getLayerWhenLibraryIsNotSnapshotReturnsDependenciesLayer() { + assertThat(this.layers.getLayer(mockLibrary("spring-boot.2.0.0.jar"))).isEqualTo(StandardLayers.DEPENDENCIES); + assertThat(this.layers.getLayer(mockLibrary("spring-boot.2.0.0-classified.jar"))) + .isEqualTo(StandardLayers.DEPENDENCIES); + } + + private Library mockLibrary(String name) { + Library library = mock(Library.class); + given(library.getName()).willReturn(name); + return library; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/LayerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/LayerTests.java new file mode 100644 index 000000000000..2dddf91b6c6d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/LayerTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.tools; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link Layer}. + * + * @author Madhura Bhave + * @author Phillip Webb + */ +class LayerTests { + + @Test + void createWhenNameIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new Layer(null)).withMessage("Name must not be empty"); + } + + @Test + void createWhenNameIsEmptyThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new Layer("")).withMessage("Name must not be empty"); + } + + @Test + void createWhenNameContainsBadCharsThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new Layer("bad!name")) + .withMessage("Malformed layer name 'bad!name'"); + } + + @Test + void equalsAndHashCode() { + Layer layer1 = new Layer("testa"); + Layer layer2 = new Layer("testa"); + Layer layer3 = new Layer("testb"); + assertThat(layer1.hashCode()).isEqualTo(layer2.hashCode()); + assertThat(layer1).isEqualTo(layer1).isEqualTo(layer2).isNotEqualTo(layer3); + } + + @Test + void toStringReturnsName() { + assertThat(new Layer("test")).hasToString("test"); + } + + @Test + void createWhenUsingReservedNameThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new Layer("ext")) + .withMessage("Layer name 'ext' is reserved"); + assertThatIllegalArgumentException().isThrownBy(() -> new Layer("ExT")) + .withMessage("Layer name 'ExT' is reserved"); + assertThatIllegalArgumentException().isThrownBy(() -> new Layer("springbootloader")) + .withMessage("Layer name 'springbootloader' is reserved"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/LayersIndexTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/LayersIndexTests.java new file mode 100644 index 000000000000..c1759b24022e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/LayersIndexTests.java @@ -0,0 +1,152 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.tools; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import org.assertj.core.api.AbstractObjectAssert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; + +import org.springframework.util.Assert; +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link LayersIndex}. + * + * @author Phillip Webb + */ +class LayersIndexTests { + + private static final Layer LAYER_A = new Layer("a"); + + private static final Layer LAYER_B = new Layer("b"); + + private static final Layer LAYER_C = new Layer("c"); + + private String testMethodName; + + @BeforeEach + void setup(TestInfo testInfo) { + this.testMethodName = testInfo.getTestMethod().get().getName(); + } + + @Test + void writeToWhenSimpleNamesSortsAlphabetically() { + LayersIndex index = new LayersIndex(LAYER_A); + index.add(LAYER_A, "cat"); + index.add(LAYER_A, "dog"); + index.add(LAYER_A, "aardvark"); + index.add(LAYER_A, "zerbra"); + index.add(LAYER_A, "hamster"); + assertThatIndex(index).writesExpectedContent(); + } + + @Test + void writeToWritesLayersInIteratorOrder() { + LayersIndex index = new LayersIndex(LAYER_B, LAYER_A, LAYER_C); + index.add(LAYER_A, "a1"); + index.add(LAYER_A, "a2"); + index.add(LAYER_B, "b1"); + index.add(LAYER_B, "b2"); + index.add(LAYER_C, "c1"); + index.add(LAYER_C, "c2"); + assertThatIndex(index).writesExpectedContent(); + } + + @Test + void writeToWhenLayerNotUsedDoesNotSkipLayer() { + LayersIndex index = new LayersIndex(LAYER_A, LAYER_B, LAYER_C); + index.add(LAYER_A, "a1"); + index.add(LAYER_A, "a2"); + index.add(LAYER_C, "c1"); + index.add(LAYER_C, "c2"); + assertThatIndex(index).writesExpectedContent(); + } + + @Test + void writeToWhenAllFilesInDirectoryAreInSameLayerUsesDirectory() { + LayersIndex index = new LayersIndex(LAYER_A, LAYER_B, LAYER_C); + index.add(LAYER_A, "a1/b1/c1"); + index.add(LAYER_A, "a1/b1/c2"); + index.add(LAYER_A, "a1/b2/c1"); + index.add(LAYER_B, "a2/b1"); + index.add(LAYER_B, "a2/b2"); + assertThatIndex(index).writesExpectedContent(); + } + + @Test + void writeToWhenAllFilesInDirectoryAreInNotInSameLayerUsesFiles() { + LayersIndex index = new LayersIndex(LAYER_A, LAYER_B, LAYER_C); + index.add(LAYER_A, "a1/b1/c1"); + index.add(LAYER_B, "a1/b1/c2"); + index.add(LAYER_C, "a1/b2/c1"); + index.add(LAYER_A, "a2/b1"); + index.add(LAYER_B, "a2/b2"); + assertThatIndex(index).writesExpectedContent(); + } + + @Test + void writeToWhenSpaceInFileName() { + LayersIndex index = new LayersIndex(LAYER_A); + index.add(LAYER_A, "a b"); + index.add(LAYER_A, "a b/c"); + index.add(LAYER_A, "a b/d"); + assertThatIndex(index).writesExpectedContent(); + } + + private LayersIndexAssert assertThatIndex(LayersIndex index) { + return new LayersIndexAssert(index); + } + + private class LayersIndexAssert extends AbstractObjectAssert { + + LayersIndexAssert(LayersIndex actual) { + super(actual, LayersIndexAssert.class); + } + + void writesExpectedContent() { + try { + String actualContent = getContent(); + String name = "LayersIndexTests-" + LayersIndexTests.this.testMethodName + ".txt"; + InputStream in = LayersIndexTests.class.getResourceAsStream(name); + Assert.state(in != null, () -> "Can't read " + name); + String expectedContent = new String(FileCopyUtils.copyToByteArray(in), StandardCharsets.UTF_8); + expectedContent = expectedContent.replace("\r", ""); + assertThat(actualContent).isEqualTo(expectedContent); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + + } + + private String getContent() throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + this.actual.writeTo(out); + return new String(out.toByteArray(), StandardCharsets.UTF_8); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/LayoutsTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/LayoutsTests.java index 6145db5e2215..52ef2e61441c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/LayoutsTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/LayoutsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -56,19 +56,19 @@ void unknownFile() { @Test void jarLayout() { Layout layout = new Layouts.Jar(); - assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.COMPILE)).isEqualTo("BOOT-INF/lib/"); - assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.CUSTOM)).isEqualTo("BOOT-INF/lib/"); - assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.PROVIDED)).isEqualTo("BOOT-INF/lib/"); - assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.RUNTIME)).isEqualTo("BOOT-INF/lib/"); + assertThat(layout.getLibraryLocation("lib.jar", LibraryScope.COMPILE)).isEqualTo("BOOT-INF/lib/"); + assertThat(layout.getLibraryLocation("lib.jar", LibraryScope.CUSTOM)).isEqualTo("BOOT-INF/lib/"); + assertThat(layout.getLibraryLocation("lib.jar", LibraryScope.PROVIDED)).isEqualTo("BOOT-INF/lib/"); + assertThat(layout.getLibraryLocation("lib.jar", LibraryScope.RUNTIME)).isEqualTo("BOOT-INF/lib/"); } @Test void warLayout() { Layout layout = new Layouts.War(); - assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.COMPILE)).isEqualTo("WEB-INF/lib/"); - assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.CUSTOM)).isEqualTo("WEB-INF/lib/"); - assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.PROVIDED)).isEqualTo("WEB-INF/lib-provided/"); - assertThat(layout.getLibraryDestination("lib.jar", LibraryScope.RUNTIME)).isEqualTo("WEB-INF/lib/"); + assertThat(layout.getLibraryLocation("lib.jar", LibraryScope.COMPILE)).isEqualTo("WEB-INF/lib/"); + assertThat(layout.getLibraryLocation("lib.jar", LibraryScope.CUSTOM)).isEqualTo("WEB-INF/lib/"); + assertThat(layout.getLibraryLocation("lib.jar", LibraryScope.PROVIDED)).isEqualTo("WEB-INF/lib-provided/"); + assertThat(layout.getLibraryLocation("lib.jar", LibraryScope.RUNTIME)).isEqualTo("WEB-INF/lib/"); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/LibraryCoordinatesTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/LibraryCoordinatesTests.java new file mode 100644 index 000000000000..daebaecbfda7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/LibraryCoordinatesTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.tools; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link LibraryCoordinates}. + * + * @author Phillip Webb + */ +class LibraryCoordinatesTests { + + @Test + void ofCreateLibraryCoordinates() { + LibraryCoordinates coordinates = LibraryCoordinates.of("g", "a", "v"); + assertThat(coordinates.getGroupId()).isEqualTo("g"); + assertThat(coordinates.getArtifactId()).isEqualTo("a"); + assertThat(coordinates.getVersion()).isEqualTo("v"); + assertThat(coordinates.toString()).isEqualTo("g:a:v"); + } + + @Test + void toStandardNotationStringWhenCoordinatesAreNull() { + assertThat(LibraryCoordinates.toStandardNotationString(null)).isEqualTo("::"); + } + + @Test + void toStandardNotationStringWhenCoordinatesElementsNull() { + assertThat(LibraryCoordinates.toStandardNotationString(mock(LibraryCoordinates.class))).isEqualTo("::"); + } + + @Test + void toStandardNotationString() { + LibraryCoordinates coordinates = mock(LibraryCoordinates.class); + given(coordinates.getGroupId()).willReturn("a"); + given(coordinates.getArtifactId()).willReturn("b"); + given(coordinates.getVersion()).willReturn("c"); + assertThat(LibraryCoordinates.toStandardNotationString(coordinates)).isEqualTo("a:b:c"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/MainClassFinderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/MainClassFinderTests.java index 3aadff9eeb4d..d5d3c93ae827 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/MainClassFinderTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/MainClassFinderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -60,7 +60,7 @@ void findMainClassInJar() throws Exception { } @Test - void findMainClassInJarSubFolder() throws Exception { + void findMainClassInJarSubDirectory() throws Exception { this.testJarFile.addClass("a/b/c/D.class", ClassWithMainMethod.class); this.testJarFile.addClass("a/b/c/E.class", ClassWithoutMainMethod.class); this.testJarFile.addClass("a/b/F.class", ClassWithoutMainMethod.class); @@ -114,7 +114,7 @@ void findMainClassInJarSubLocation() throws Exception { } @Test - void findMainClassInFolder() throws Exception { + void findMainClassInDirectory() throws Exception { this.testJarFile.addClass("B.class", ClassWithMainMethod.class); this.testJarFile.addClass("A.class", ClassWithoutMainMethod.class); String actual = MainClassFinder.findMainClass(this.testJarFile.getJarSource()); @@ -122,7 +122,7 @@ void findMainClassInFolder() throws Exception { } @Test - void findMainClassInSubFolder() throws Exception { + void findMainClassInSubDirectory() throws Exception { this.testJarFile.addClass("a/b/c/D.class", ClassWithMainMethod.class); this.testJarFile.addClass("a/b/c/E.class", ClassWithoutMainMethod.class); this.testJarFile.addClass("a/b/F.class", ClassWithoutMainMethod.class); @@ -131,7 +131,7 @@ void findMainClassInSubFolder() throws Exception { } @Test - void usesBreadthFirstFolderSearch() throws Exception { + void usesBreadthFirstDirectorySearch() throws Exception { this.testJarFile.addClass("a/B.class", ClassWithMainMethod.class); this.testJarFile.addClass("a/b/c/E.class", ClassWithMainMethod.class); String actual = MainClassFinder.findMainClass(this.testJarFile.getJarSource()); @@ -139,7 +139,7 @@ void usesBreadthFirstFolderSearch() throws Exception { } @Test - void findSingleFolderSearch() throws Exception { + void findSingleDirectorySearch() throws Exception { this.testJarFile.addClass("a/B.class", ClassWithMainMethod.class); this.testJarFile.addClass("a/b/c/E.class", ClassWithMainMethod.class); assertThatIllegalStateException() @@ -149,7 +149,7 @@ void findSingleFolderSearch() throws Exception { } @Test - void findSingleFolderSearchPrefersAnnotatedMainClass() throws Exception { + void findSingleDirectorySearchPrefersAnnotatedMainClass() throws Exception { this.testJarFile.addClass("a/B.class", ClassWithMainMethod.class); this.testJarFile.addClass("a/b/c/E.class", AnnotatedClassWithMainMethod.class); String mainClass = MainClassFinder.findSingleMainClass(this.testJarFile.getJarSource(), @@ -158,7 +158,7 @@ void findSingleFolderSearchPrefersAnnotatedMainClass() throws Exception { } @Test - void doWithFolderMainMethods() throws Exception { + void doWithDirectoryMainMethods() throws Exception { this.testJarFile.addClass("a/b/c/D.class", ClassWithMainMethod.class); this.testJarFile.addClass("a/b/c/E.class", ClassWithoutMainMethod.class); this.testJarFile.addClass("a/b/F.class", ClassWithoutMainMethod.class); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java index 11a8aab4b16d..9f8f186b3c00 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,77 +16,44 @@ package org.springframework.boot.loader.tools; -import java.io.ByteArrayInputStream; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.attribute.FileTime; import java.nio.file.attribute.PosixFilePermission; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; import java.util.ArrayList; -import java.util.Calendar; +import java.util.Collection; import java.util.Enumeration; import java.util.List; -import java.util.Random; -import java.util.jar.Attributes; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.Manifest; -import java.util.zip.Deflater; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipFile; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.zeroturnaround.zip.ZipUtil; import org.springframework.boot.loader.tools.sample.ClassWithMainMethod; -import org.springframework.boot.loader.tools.sample.ClassWithoutMainMethod; import org.springframework.util.FileCopyUtils; +import org.springframework.util.StopWatch; +import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; /** * Tests for {@link Repackager}. * * @author Phillip Webb * @author Andy Wilkinson + * @author Madhura Bhave */ -class RepackagerTests { +class RepackagerTests extends AbstractPackagerTests { - private static final Libraries NO_LIBRARIES = (callback) -> { - }; - - private static final long JAN_1_1980; - - private static final long JAN_1_1985; - - static { - Calendar calendar = Calendar.getInstance(); - calendar.set(1980, 0, 1, 0, 0, 0); - calendar.set(Calendar.MILLISECOND, 0); - JAN_1_1980 = calendar.getTime().getTime(); - calendar.set(Calendar.YEAR, 1985); - JAN_1_1985 = calendar.getTime().getTime(); - } - - @TempDir - File tempDir; - - private TestJarFile testJarFile; - - @BeforeEach - void setup() throws IOException { - this.testJarFile = new TestJarFile(this.tempDir); - } + private File destination; @Test void nullSource() { @@ -103,143 +70,55 @@ void directorySource() { assertThatIllegalArgumentException().isThrownBy(() -> new Repackager(this.tempDir)); } - @Test - void specificMainClass() throws Exception { - this.testJarFile.addClass("a/b/C.class", ClassWithoutMainMethod.class); - File file = this.testJarFile.getFile(); - Repackager repackager = new Repackager(file); - repackager.setMainClass("a.b.C"); - repackager.repackage(NO_LIBRARIES); - Manifest actualManifest = getManifest(file); - assertThat(actualManifest.getMainAttributes().getValue("Main-Class")) - .isEqualTo("org.springframework.boot.loader.JarLauncher"); - assertThat(actualManifest.getMainAttributes().getValue("Start-Class")).isEqualTo("a.b.C"); - assertThat(hasLauncherClasses(file)).isTrue(); - } - - @Test - void mainClassFromManifest() throws Exception { - this.testJarFile.addClass("a/b/C.class", ClassWithoutMainMethod.class); - Manifest manifest = new Manifest(); - manifest.getMainAttributes().putValue("Manifest-Version", "1.0"); - manifest.getMainAttributes().putValue("Main-Class", "a.b.C"); - this.testJarFile.addManifest(manifest); - File file = this.testJarFile.getFile(); - Repackager repackager = new Repackager(file); - repackager.repackage(NO_LIBRARIES); - Manifest actualManifest = getManifest(file); - assertThat(actualManifest.getMainAttributes().getValue("Main-Class")) - .isEqualTo("org.springframework.boot.loader.JarLauncher"); - assertThat(actualManifest.getMainAttributes().getValue("Start-Class")).isEqualTo("a.b.C"); - assertThat(hasLauncherClasses(file)).isTrue(); - } - - @Test - void mainClassFound() throws Exception { - this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); - File file = this.testJarFile.getFile(); - Repackager repackager = new Repackager(file); - repackager.repackage(NO_LIBRARIES); - Manifest actualManifest = getManifest(file); - assertThat(actualManifest.getMainAttributes().getValue("Main-Class")) - .isEqualTo("org.springframework.boot.loader.JarLauncher"); - assertThat(actualManifest.getMainAttributes().getValue("Start-Class")).isEqualTo("a.b.C"); - assertThat(hasLauncherClasses(file)).isTrue(); - } - @Test void jarIsOnlyRepackagedOnce() throws Exception { this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); - File file = this.testJarFile.getFile(); - Repackager repackager = new Repackager(file); + Repackager repackager = createRepackager(this.testJarFile.getFile(), false); repackager.repackage(NO_LIBRARIES); repackager.repackage(NO_LIBRARIES); - Manifest actualManifest = getManifest(file); + Manifest actualManifest = getPackagedManifest(); assertThat(actualManifest.getMainAttributes().getValue("Main-Class")) .isEqualTo("org.springframework.boot.loader.JarLauncher"); assertThat(actualManifest.getMainAttributes().getValue("Start-Class")).isEqualTo("a.b.C"); - assertThat(hasLauncherClasses(file)).isTrue(); - } - - @Test - void multipleMainClassFound() throws Exception { - this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); - this.testJarFile.addClass("a/b/D.class", ClassWithMainMethod.class); - File file = this.testJarFile.getFile(); - Repackager repackager = new Repackager(file); - assertThatIllegalStateException().isThrownBy(() -> repackager.repackage(NO_LIBRARIES)).withMessageContaining( - "Unable to find a single main class from the following candidates [a.b.C, a.b.D]"); - } - - @Test - void noMainClass() throws Exception { - this.testJarFile.addClass("a/b/C.class", ClassWithoutMainMethod.class); - assertThatIllegalStateException() - .isThrownBy(() -> new Repackager(this.testJarFile.getFile()).repackage(NO_LIBRARIES)) - .withMessageContaining("Unable to find main class"); + assertThat(hasPackagedLauncherClasses()).isTrue(); } @Test - void noMainClassAndLayoutIsNone() throws Exception { + void sameSourceAndDestinationWithoutBackup() throws Exception { this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); File file = this.testJarFile.getFile(); - Repackager repackager = new Repackager(file); - repackager.setLayout(new Layouts.None()); - repackager.repackage(file, NO_LIBRARIES); - Manifest actualManifest = getManifest(file); - assertThat(actualManifest.getMainAttributes().getValue("Main-Class")).isEqualTo("a.b.C"); - assertThat(hasLauncherClasses(file)).isFalse(); - } - - @Test - void noMainClassAndLayoutIsNoneWithNoMain() throws Exception { - this.testJarFile.addClass("a/b/C.class", ClassWithoutMainMethod.class); - File file = this.testJarFile.getFile(); - Repackager repackager = new Repackager(file); - repackager.setLayout(new Layouts.None()); - repackager.repackage(file, NO_LIBRARIES); - Manifest actualManifest = getManifest(file); - assertThat(actualManifest.getMainAttributes().getValue("Main-Class")).isNull(); - assertThat(hasLauncherClasses(file)).isFalse(); + Repackager repackager = createRepackager(file, false); + repackager.setBackupSource(false); + repackager.repackage(NO_LIBRARIES); + assertThat(new File(file.getParent(), file.getName() + ".original")).doesNotExist(); + assertThat(hasPackagedLauncherClasses()).isTrue(); } @Test void sameSourceAndDestinationWithBackup() throws Exception { this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); File file = this.testJarFile.getFile(); - Repackager repackager = new Repackager(file); + Repackager repackager = createRepackager(file, false); repackager.repackage(NO_LIBRARIES); assertThat(new File(file.getParent(), file.getName() + ".original")).exists(); - assertThat(hasLauncherClasses(file)).isTrue(); - } - - @Test - void sameSourceAndDestinationWithoutBackup() throws Exception { - this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); - File file = this.testJarFile.getFile(); - Repackager repackager = new Repackager(file); - repackager.setBackupSource(false); - repackager.repackage(NO_LIBRARIES); - assertThat(new File(file.getParent(), file.getName() + ".original")).doesNotExist(); - assertThat(hasLauncherClasses(file)).isTrue(); + assertThat(hasPackagedLauncherClasses()).isTrue(); } @Test void differentDestination() throws Exception { this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); File source = this.testJarFile.getFile(); - File dest = new File(this.tempDir, "different.jar"); - Repackager repackager = new Repackager(source); - repackager.repackage(dest, NO_LIBRARIES); + Repackager repackager = createRepackager(source, true); + execute(repackager, NO_LIBRARIES); assertThat(new File(source.getParent(), source.getName() + ".original")).doesNotExist(); assertThat(hasLauncherClasses(source)).isFalse(); - assertThat(hasLauncherClasses(dest)).isTrue(); + assertThat(hasPackagedLauncherClasses()).isTrue(); } @Test void nullDestination() throws Exception { this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); - Repackager repackager = new Repackager(this.testJarFile.getFile()); + Repackager repackager = createRepackager(this.testJarFile.getFile(), true); assertThatIllegalArgumentException().isThrownBy(() -> repackager.repackage(null, NO_LIBRARIES)) .withMessageContaining("Invalid destination"); } @@ -247,7 +126,7 @@ void nullDestination() throws Exception { @Test void destinationIsDirectory() throws Exception { this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); - Repackager repackager = new Repackager(this.testJarFile.getFile()); + Repackager repackager = createRepackager(this.testJarFile.getFile(), true); assertThatIllegalArgumentException().isThrownBy(() -> repackager.repackage(this.tempDir, NO_LIBRARIES)) .withMessageContaining("Invalid destination"); } @@ -255,176 +134,35 @@ void destinationIsDirectory() throws Exception { @Test void overwriteDestination() throws Exception { this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); - Repackager repackager = new Repackager(this.testJarFile.getFile()); - File dest = new File(this.tempDir, "dest.jar"); - dest.createNewFile(); - repackager.repackage(dest, NO_LIBRARIES); - assertThat(hasLauncherClasses(dest)).isTrue(); - } - - @Test - void nullLibraries() throws Exception { - this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); - File file = this.testJarFile.getFile(); - Repackager repackager = new Repackager(file); - assertThatIllegalArgumentException().isThrownBy(() -> repackager.repackage(file, null)) - .withMessageContaining("Libraries must not be null"); - } - - @Test - void libraries() throws Exception { - TestJarFile libJar = new TestJarFile(this.tempDir); - libJar.addClass("a/b/C.class", ClassWithoutMainMethod.class, JAN_1_1985); - File libJarFile = libJar.getFile(); - File libJarFileToUnpack = libJar.getFile(); - File libNonJarFile = new File(this.tempDir, "non-lib.jar"); - FileCopyUtils.copy(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 }, libNonJarFile); - this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); - this.testJarFile.addFile("BOOT-INF/lib/" + libJarFileToUnpack.getName(), libJarFileToUnpack); - File file = this.testJarFile.getFile(); - libJarFile.setLastModified(JAN_1_1980); - Repackager repackager = new Repackager(file); - repackager.repackage((callback) -> { - callback.library(new Library(libJarFile, LibraryScope.COMPILE)); - callback.library(new Library(libJarFileToUnpack, LibraryScope.COMPILE, true)); - callback.library(new Library(libNonJarFile, LibraryScope.COMPILE)); - }); - assertThat(hasEntry(file, "BOOT-INF/lib/" + libJarFile.getName())).isTrue(); - assertThat(hasEntry(file, "BOOT-INF/lib/" + libJarFileToUnpack.getName())).isTrue(); - assertThat(hasEntry(file, "BOOT-INF/lib/" + libNonJarFile.getName())).isFalse(); - JarEntry entry = getEntry(file, "BOOT-INF/lib/" + libJarFile.getName()); - assertThat(entry.getTime()).isEqualTo(JAN_1_1985); - entry = getEntry(file, "BOOT-INF/lib/" + libJarFileToUnpack.getName()); - assertThat(entry.getComment()).startsWith("UNPACK:"); - assertThat(entry.getComment()).hasSize(47); + Repackager repackager = createRepackager(this.testJarFile.getFile(), true); + this.destination.createNewFile(); + repackager.repackage(this.destination, NO_LIBRARIES); + assertThat(hasLauncherClasses(this.destination)).isTrue(); } @Test - void duplicateLibraries() throws Exception { - TestJarFile libJar = new TestJarFile(this.tempDir); - libJar.addClass("a/b/C.class", ClassWithoutMainMethod.class); - File libJarFile = libJar.getFile(); + void layoutFactoryGetsOriginalFile() throws Exception { this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); - File file = this.testJarFile.getFile(); - Repackager repackager = new Repackager(file); - assertThatIllegalStateException().isThrownBy(() -> repackager.repackage((callback) -> { - callback.library(new Library(libJarFile, LibraryScope.COMPILE, false)); - callback.library(new Library(libJarFile, LibraryScope.COMPILE, false)); - })).withMessageContaining("Duplicate library"); - } - - @Test - void customLayout() throws Exception { - TestJarFile libJar = new TestJarFile(this.tempDir); - libJar.addClass("a/b/C.class", ClassWithoutMainMethod.class); - File libJarFile = libJar.getFile(); - this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); - File file = this.testJarFile.getFile(); - Repackager repackager = new Repackager(file); - Layout layout = mock(Layout.class); - LibraryScope scope = mock(LibraryScope.class); - given(layout.getLauncherClassName()).willReturn("testLauncher"); - given(layout.getLibraryDestination(anyString(), eq(scope))).willReturn("test/"); - given(layout.getLibraryDestination(anyString(), eq(LibraryScope.COMPILE))).willReturn("test-lib/"); - repackager.setLayout(layout); - repackager.repackage((callback) -> callback.library(new Library(libJarFile, scope))); - assertThat(hasEntry(file, "test/" + libJarFile.getName())).isTrue(); - assertThat(getManifest(file).getMainAttributes().getValue("Spring-Boot-Lib")).isEqualTo("test-lib/"); - assertThat(getManifest(file).getMainAttributes().getValue("Main-Class")).isEqualTo("testLauncher"); - } - - @Test - void customLayoutNoBootLib() throws Exception { - TestJarFile libJar = new TestJarFile(this.tempDir); - libJar.addClass("a/b/C.class", ClassWithoutMainMethod.class); - File libJarFile = libJar.getFile(); - this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); - File file = this.testJarFile.getFile(); - Repackager repackager = new Repackager(file); - Layout layout = mock(Layout.class); - LibraryScope scope = mock(LibraryScope.class); - given(layout.getLauncherClassName()).willReturn("testLauncher"); - repackager.setLayout(layout); - repackager.repackage((callback) -> callback.library(new Library(libJarFile, scope))); - assertThat(getManifest(file).getMainAttributes().getValue("Spring-Boot-Lib")).isNull(); - assertThat(getManifest(file).getMainAttributes().getValue("Main-Class")).isEqualTo("testLauncher"); - } - - @Test - void springBootVersion() throws Exception { - this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); - File file = this.testJarFile.getFile(); - Repackager repackager = new Repackager(file); - repackager.repackage(NO_LIBRARIES); - Manifest actualManifest = getManifest(file); - assertThat(actualManifest.getMainAttributes()).containsKey(new Attributes.Name("Spring-Boot-Version")); - } - - @Test - void executableJarLayoutAttributes() throws Exception { - this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); - File file = this.testJarFile.getFile(); - Repackager repackager = new Repackager(file); - repackager.repackage(NO_LIBRARIES); - Manifest actualManifest = getManifest(file); - assertThat(actualManifest.getMainAttributes()).containsEntry(new Attributes.Name("Spring-Boot-Lib"), - "BOOT-INF/lib/"); - assertThat(actualManifest.getMainAttributes()).containsEntry(new Attributes.Name("Spring-Boot-Classes"), - "BOOT-INF/classes/"); - } - - @Test - void executableWarLayoutAttributes() throws Exception { - this.testJarFile.addClass("WEB-INF/classes/a/b/C.class", ClassWithMainMethod.class); - File file = this.testJarFile.getFile("war"); - Repackager repackager = new Repackager(file); - repackager.repackage(NO_LIBRARIES); - Manifest actualManifest = getManifest(file); - assertThat(actualManifest.getMainAttributes()).containsEntry(new Attributes.Name("Spring-Boot-Lib"), - "WEB-INF/lib/"); - assertThat(actualManifest.getMainAttributes()).containsEntry(new Attributes.Name("Spring-Boot-Classes"), - "WEB-INF/classes/"); - } - - @Test - void nullCustomLayout() throws Exception { - this.testJarFile.addClass("a/b/C.class", ClassWithoutMainMethod.class); - Repackager repackager = new Repackager(this.testJarFile.getFile()); - assertThatIllegalArgumentException().isThrownBy(() -> repackager.setLayout(null)) - .withMessageContaining("Layout must not be null"); - } - - @Test - void dontRecompressZips() throws Exception { - TestJarFile nested = new TestJarFile(this.tempDir); - nested.addClass("a/b/C.class", ClassWithoutMainMethod.class); - File nestedFile = nested.getFile(); - this.testJarFile.addFile("test/nested.jar", nestedFile); - this.testJarFile.addClass("A.class", ClassWithMainMethod.class); - File file = this.testJarFile.getFile(); - Repackager repackager = new Repackager(file); - repackager.repackage((callback) -> callback.library(new Library(nestedFile, LibraryScope.COMPILE))); - - try (JarFile jarFile = new JarFile(file)) { - assertThat(jarFile.getEntry("BOOT-INF/lib/" + nestedFile.getName()).getMethod()).isEqualTo(ZipEntry.STORED); - assertThat(jarFile.getEntry("BOOT-INF/classes/test/nested.jar").getMethod()).isEqualTo(ZipEntry.STORED); - } + Repackager repackager = createRepackager(this.testJarFile.getFile(), false); + repackager.setLayoutFactory(new TestLayoutFactory()); + repackager.repackage(this.destination, NO_LIBRARIES); + assertThat(hasLauncherClasses(this.destination)).isTrue(); } @Test void addLauncherScript() throws Exception { this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); File source = this.testJarFile.getFile(); - File dest = new File(this.tempDir, "dest.jar"); - Repackager repackager = new Repackager(source); + Repackager repackager = createRepackager(source, true); LaunchScript script = new MockLauncherScript("ABC"); - repackager.repackage(dest, NO_LIBRARIES, script); - byte[] bytes = FileCopyUtils.copyToByteArray(dest); + repackager.repackage(this.destination, NO_LIBRARIES, script); + byte[] bytes = FileCopyUtils.copyToByteArray(this.destination); assertThat(new String(bytes)).startsWith("ABC"); assertThat(hasLauncherClasses(source)).isFalse(); - assertThat(hasLauncherClasses(dest)).isTrue(); + assertThat(hasLauncherClasses(this.destination)).isTrue(); try { - assertThat(Files.getPosixFilePermissions(dest.toPath())).contains(PosixFilePermission.OWNER_EXECUTE); + assertThat(Files.getPosixFilePermissions(this.destination.toPath())) + .contains(PosixFilePermission.OWNER_EXECUTE); } catch (UnsupportedOperationException ex) { // Probably running the test on Windows @@ -432,252 +170,107 @@ void addLauncherScript() throws Exception { } @Test - void unpackLibrariesTakePrecedenceOverExistingSourceEntries() throws Exception { - TestJarFile nested = new TestJarFile(this.tempDir); - nested.addClass("a/b/C.class", ClassWithoutMainMethod.class); - File nestedFile = nested.getFile(); - String name = "BOOT-INF/lib/" + nestedFile.getName(); - this.testJarFile.addFile(name, nested.getFile()); + void allLoaderDirectoriesAndFilesUseSameTimestamp() throws IOException { this.testJarFile.addClass("A.class", ClassWithMainMethod.class); - File file = this.testJarFile.getFile(); - Repackager repackager = new Repackager(file); - repackager.repackage((callback) -> callback.library(new Library(nestedFile, LibraryScope.COMPILE, true))); - try (JarFile jarFile = new JarFile(file)) { - assertThat(jarFile.getEntry(name).getComment()).startsWith("UNPACK:"); + Repackager repackager = createRepackager(this.testJarFile.getFile(), true); + Long timestamp = null; + repackager.repackage(this.destination, NO_LIBRARIES); + for (ZipArchiveEntry entry : getAllPackagedEntries()) { + if (entry.getName().startsWith("org/springframework/boot/loader")) { + if (timestamp == null) { + timestamp = entry.getTime(); + } + else { + assertThat(entry.getTime()).withFailMessage("Expected time %d to be equal to %d for entry %s", + entry.getTime(), timestamp, entry.getName()).isEqualTo(timestamp); + } + } } } @Test - void existingSourceEntriesTakePrecedenceOverStandardLibraries() throws Exception { - TestJarFile nested = new TestJarFile(this.tempDir); - nested.addClass("a/b/C.class", ClassWithoutMainMethod.class); - File nestedFile = nested.getFile(); - this.testJarFile.addFile("BOOT-INF/lib/" + nestedFile.getName(), nested.getFile()); + void allEntriesUseProvidedTimestamp() throws IOException { this.testJarFile.addClass("A.class", ClassWithMainMethod.class); - File file = this.testJarFile.getFile(); - Repackager repackager = new Repackager(file); - long sourceLength = nestedFile.length(); - repackager.repackage((callback) -> { - nestedFile.delete(); - File toZip = new File(this.tempDir, "to-zip"); - toZip.createNewFile(); - ZipUtil.packEntry(toZip, nestedFile); - callback.library(new Library(nestedFile, LibraryScope.COMPILE)); - }); - try (JarFile jarFile = new JarFile(file)) { - assertThat(jarFile.getEntry("BOOT-INF/lib/" + nestedFile.getName()).getSize()).isEqualTo(sourceLength); + Repackager repackager = createRepackager(this.testJarFile.getFile(), true); + long timestamp = OffsetDateTime.of(2000, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC).toInstant().toEpochMilli(); + repackager.repackage(this.destination, NO_LIBRARIES, null, FileTime.fromMillis(timestamp)); + for (ZipArchiveEntry entry : getAllPackagedEntries()) { + assertThat(entry.getTime()).isEqualTo(timestamp); } } @Test - void metaInfIndexListIsRemovedFromRepackagedJar() throws Exception { - this.testJarFile.addClass("A.class", ClassWithMainMethod.class); - File indexList = new File(this.tempDir, "INDEX.LIST"); - indexList.createNewFile(); - this.testJarFile.addFile("META-INF/INDEX.LIST", indexList); - File source = this.testJarFile.getFile(); - File dest = new File(this.tempDir, "dest.jar"); - Repackager repackager = new Repackager(source); - repackager.repackage(dest, NO_LIBRARIES); - try (JarFile jarFile = new JarFile(dest)) { - assertThat(jarFile.getEntry("META-INF/INDEX.LIST")).isNull(); - } + void repackagingDeeplyNestedPackageIsNotProhibitivelySlow() throws IOException { + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + this.testJarFile.addClass("a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/Some.class", + ClassWithMainMethod.class); + Repackager repackager = createRepackager(this.testJarFile.getFile(), true); + repackager.repackage(this.destination, NO_LIBRARIES, null, null); + stopWatch.stop(); + assertThat(stopWatch.getTotalTimeMillis()).isLessThan(5000); } - @Test - void customLayoutFactoryWithoutLayout() throws Exception { - this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); - File source = this.testJarFile.getFile(); - Repackager repackager = new Repackager(source, new TestLayoutFactory()); - repackager.repackage(NO_LIBRARIES); - JarFile jarFile = new JarFile(source); - assertThat(jarFile.getEntry("test")).isNotNull(); - jarFile.close(); - } - - @Test - void customLayoutFactoryWithLayout() throws Exception { - this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); - File source = this.testJarFile.getFile(); - Repackager repackager = new Repackager(source, new TestLayoutFactory()); - repackager.setLayout(new Layouts.Jar()); - repackager.repackage(NO_LIBRARIES); - JarFile jarFile = new JarFile(source); - assertThat(jarFile.getEntry("test")).isNull(); - jarFile.close(); + private boolean hasLauncherClasses(File file) throws IOException { + return hasEntry(file, "org/springframework/boot/") + && hasEntry(file, "org/springframework/boot/loader/JarLauncher.class"); } - @Test - void metaInfAopXmlIsMovedBeneathBootInfClassesWhenRepackaged() throws Exception { - this.testJarFile.addClass("A.class", ClassWithMainMethod.class); - File aopXml = new File(this.tempDir, "aop.xml"); - aopXml.createNewFile(); - this.testJarFile.addFile("META-INF/aop.xml", aopXml); - File source = this.testJarFile.getFile(); - File dest = new File(this.tempDir, "dest.jar"); - Repackager repackager = new Repackager(source); - repackager.repackage(dest, NO_LIBRARIES); - try (JarFile jarFile = new JarFile(dest)) { - assertThat(jarFile.getEntry("META-INF/aop.xml")).isNull(); - assertThat(jarFile.getEntry("BOOT-INF/classes/META-INF/aop.xml")).isNotNull(); - } + private boolean hasEntry(File file, String name) throws IOException { + return getEntry(file, name) != null; } - @Test - void allEntriesUseUnixPlatformAndUtf8NameEncoding() throws IOException { - this.testJarFile.addClass("A.class", ClassWithMainMethod.class); - File source = this.testJarFile.getFile(); - File dest = new File(this.tempDir, "dest.jar"); - Repackager repackager = new Repackager(source); - repackager.repackage(dest, NO_LIBRARIES); - try (ZipFile zip = new ZipFile(dest)) { - Enumeration entries = zip.getEntries(); - while (entries.hasMoreElements()) { - ZipArchiveEntry entry = entries.nextElement(); - assertThat(entry.getPlatform()).isEqualTo(ZipArchiveEntry.PLATFORM_UNIX); - assertThat(entry.getGeneralPurposeBit().usesUTF8ForNames()).isTrue(); - } + private JarEntry getEntry(File file, String name) throws IOException { + try (JarFile jarFile = new JarFile(file)) { + return jarFile.getJarEntry(name); } } - @Test - void loaderIsWrittenFirstThenApplicationClassesThenLibraries() throws IOException { - this.testJarFile.addClass("com/example/Application.class", ClassWithMainMethod.class); - File source = this.testJarFile.getFile(); - File dest = new File(this.tempDir, "dest.jar"); - File libraryOne = createLibrary(); - File libraryTwo = createLibrary(); - File libraryThree = createLibrary(); - Repackager repackager = new Repackager(source); - repackager.repackage(dest, (callback) -> { - callback.library(new Library(libraryOne, LibraryScope.COMPILE, false)); - callback.library(new Library(libraryTwo, LibraryScope.COMPILE, true)); - callback.library(new Library(libraryThree, LibraryScope.COMPILE, false)); - }); - assertThat(getEntryNames(dest)).containsSubsequence("org/springframework/boot/loader/", - "BOOT-INF/classes/com/example/Application.class", "BOOT-INF/lib/" + libraryOne.getName(), - "BOOT-INF/lib/" + libraryTwo.getName(), "BOOT-INF/lib/" + libraryThree.getName()); + @Override + protected Repackager createPackager(File source) { + return createRepackager(source, true); } - @Test - void existingEntryThatMatchesUnpackLibraryIsMarkedForUnpack() throws IOException { - File library = createLibrary(); - this.testJarFile.addClass("WEB-INF/classes/com/example/Application.class", ClassWithMainMethod.class); - this.testJarFile.addFile("WEB-INF/lib/" + library.getName(), library); - File source = this.testJarFile.getFile("war"); - File dest = new File(this.tempDir, "dest.war"); - Repackager repackager = new Repackager(source); - repackager.setLayout(new Layouts.War()); - repackager.repackage(dest, (callback) -> callback.library(new Library(library, LibraryScope.COMPILE, true))); - assertThat(getEntryNames(dest)).containsSubsequence("org/springframework/boot/loader/", - "WEB-INF/classes/com/example/Application.class", "WEB-INF/lib/" + library.getName()); - JarEntry unpackLibrary = getEntry(dest, "WEB-INF/lib/" + library.getName()); - assertThat(unpackLibrary.getComment()).startsWith("UNPACK:"); + private Repackager createRepackager(File source, boolean differentDest) { + String ext = StringUtils.getFilenameExtension(source.getName()); + this.destination = differentDest ? new File(this.tempDir, "dest." + ext) : source; + return new Repackager(source); } - @Test - void layoutCanOmitLibraries() throws IOException { - TestJarFile libJar = new TestJarFile(this.tempDir); - libJar.addClass("a/b/C.class", ClassWithoutMainMethod.class); - File libJarFile = libJar.getFile(); - this.testJarFile.addClass("a/b/C.class", ClassWithMainMethod.class); - File file = this.testJarFile.getFile(); - Repackager repackager = new Repackager(file); - Layout layout = mock(Layout.class); - LibraryScope scope = mock(LibraryScope.class); - repackager.setLayout(layout); - repackager.repackage((callback) -> callback.library(new Library(libJarFile, scope))); - assertThat(getEntryNames(file)).containsExactly("META-INF/", "META-INF/MANIFEST.MF", "a/", "a/b/", - "a/b/C.class"); + @Override + protected void execute(Repackager packager, Libraries libraries) throws IOException { + packager.repackage(this.destination, libraries); } - @Test - void jarThatUsesCustomCompressionConfigurationCanBeRepackaged() throws IOException { - File source = new File(this.tempDir, "source.jar"); - ZipOutputStream output = new ZipOutputStream(new FileOutputStream(source)) { - { - this.def = new Deflater(Deflater.NO_COMPRESSION, true); + @Override + protected Collection getAllPackagedEntries() throws IOException { + List result = new ArrayList<>(); + try (ZipFile zip = new ZipFile(this.destination)) { + Enumeration entries = zip.getEntries(); + while (entries.hasMoreElements()) { + result.add(entries.nextElement()); } - }; - byte[] data = new byte[1024 * 1024]; - new Random().nextBytes(data); - ZipEntry entry = new ZipEntry("entry.dat"); - output.putNextEntry(entry); - output.write(data); - output.closeEntry(); - output.close(); - File dest = new File(this.tempDir, "dest.jar"); - Repackager repackager = new Repackager(source); - repackager.setMainClass("com.example.Main"); - repackager.repackage(dest, NO_LIBRARIES); - } - - @Test - void moduleInfoClassRemainsInRootOfJarWhenRepackaged() throws Exception { - this.testJarFile.addClass("A.class", ClassWithMainMethod.class); - this.testJarFile.addClass("module-info.class", ClassWithoutMainMethod.class); - File source = this.testJarFile.getFile(); - File dest = new File(this.tempDir, "dest.jar"); - Repackager repackager = new Repackager(source); - repackager.repackage(dest, NO_LIBRARIES); - try (JarFile jarFile = new JarFile(dest)) { - assertThat(jarFile.getEntry("module-info.class")).isNotNull(); - assertThat(jarFile.getEntry("BOOT-INF/classes/module-info.class")).isNull(); - } - } - - @Test - void kotlinModuleMetadataMovesBeneathBootInfClassesWhenRepackaged() throws Exception { - this.testJarFile.addClass("A.class", ClassWithMainMethod.class); - File kotlinModule = new File(this.tempDir, "test.kotlin_module"); - kotlinModule.createNewFile(); - this.testJarFile.addFile("META-INF/test.kotlin_module", kotlinModule); - File source = this.testJarFile.getFile(); - File dest = new File(this.tempDir, "dest.jar"); - Repackager repackager = new Repackager(source); - repackager.repackage(dest, NO_LIBRARIES); - try (JarFile jarFile = new JarFile(dest)) { - assertThat(jarFile.getEntry("META-INF/test.kotlin_module")).isNull(); - assertThat(jarFile.getEntry("BOOT-INF/classes/META-INF/test.kotlin_module")).isNotNull(); } + return result; } - private File createLibrary() throws IOException { - TestJarFile library = new TestJarFile(this.tempDir); - library.addClass("com/example/library/Library.class", ClassWithoutMainMethod.class); - return library.getFile(); - } - - private boolean hasLauncherClasses(File file) throws IOException { - return hasEntry(file, "org/springframework/boot/") - && hasEntry(file, "org/springframework/boot/loader/JarLauncher.class"); - } - - private boolean hasEntry(File file, String name) throws IOException { - return getEntry(file, name) != null; - } - - private JarEntry getEntry(File file, String name) throws IOException { - try (JarFile jarFile = new JarFile(file)) { - return jarFile.getJarEntry(name); - } - } - - private Manifest getManifest(File file) throws IOException { - try (JarFile jarFile = new JarFile(file)) { + @Override + protected Manifest getPackagedManifest() throws IOException { + try (JarFile jarFile = new JarFile(this.destination)) { return jarFile.getManifest(); } } - private List getEntryNames(File file) throws IOException { - List entryNames = new ArrayList<>(); - try (JarFile jarFile = new JarFile(file)) { - Enumeration entries = jarFile.entries(); - while (entries.hasMoreElements()) { - entryNames.add(entries.nextElement().getName()); + @Override + protected String getPackagedEntryContent(String name) throws IOException { + try (ZipFile zip = new ZipFile(this.destination)) { + ZipArchiveEntry entry = zip.getEntry(name); + if (entry == null) { + return null; } + byte[] bytes = FileCopyUtils.copyToByteArray(zip.getInputStream(entry)); + return new String(bytes, StandardCharsets.UTF_8); } - return entryNames; } static class MockLauncherScript implements LaunchScript { @@ -699,16 +292,8 @@ static class TestLayoutFactory implements LayoutFactory { @Override public Layout getLayout(File source) { - return new TestLayout(); - } - - } - - static class TestLayout extends Layouts.Jar implements CustomLoaderLayout { - - @Override - public void writeLoadedClasses(LoaderClassesWriter writer) throws IOException { - writer.writeEntry("test", new ByteArrayInputStream("test".getBytes())); + assertThat(source.length()).isGreaterThan(0); + return new DefaultLayoutFactory().getLayout(source); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/SizeCalculatingEntryWriterTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/SizeCalculatingEntryWriterTests.java new file mode 100644 index 000000000000..5207a3dbd24b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/SizeCalculatingEntryWriterTests.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.tools; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Random; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SizeCalculatingEntryWriter}. + * + * @author Phillip Webb + */ +class SizeCalculatingEntryWriterTests { + + @Test + void getWhenWithinThreshold() throws Exception { + TestEntryWriter original = new TestEntryWriter(SizeCalculatingEntryWriter.THRESHOLD - 1); + EntryWriter writer = SizeCalculatingEntryWriter.get(original); + assertThat(writer.size()).isEqualTo(original.getBytes().length); + assertThat(writeBytes(writer)).isEqualTo(original.getBytes()); + assertThat(writer).extracting("content").isNotInstanceOf(File.class); + } + + @Test + void getWhenExceedingThreshold() throws Exception { + TestEntryWriter original = new TestEntryWriter(SizeCalculatingEntryWriter.THRESHOLD + 1); + EntryWriter writer = SizeCalculatingEntryWriter.get(original); + assertThat(writer.size()).isEqualTo(original.getBytes().length); + assertThat(writeBytes(writer)).isEqualTo(original.getBytes()); + assertThat(writer).extracting("content").isInstanceOf(File.class); + } + + private byte[] writeBytes(EntryWriter writer) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + writer.write(outputStream); + outputStream.close(); + return outputStream.toByteArray(); + } + + private static class TestEntryWriter implements EntryWriter { + + private byte[] bytes; + + TestEntryWriter(int size) { + this.bytes = new byte[size]; + new Random().nextBytes(this.bytes); + } + + byte[] getBytes() { + return this.bytes; + } + + @Override + public void write(OutputStream outputStream) throws IOException { + outputStream.write(this.bytes); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/TestJarFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/TestJarFile.java index 8ac35d9d89c7..6bf63dc842c2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/TestJarFile.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/TestJarFile.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,15 +40,15 @@ public class TestJarFile { private final byte[] buffer = new byte[4096]; - private final File temporaryFolder; + private final File temporaryDirectory; private final File jarSource; private final List entries = new ArrayList<>(); - public TestJarFile(File temporaryFolder) throws IOException { - this.temporaryFolder = temporaryFolder; - this.jarSource = new File(temporaryFolder, "jar-source"); + public TestJarFile(File temporaryDirectory) throws IOException { + this.temporaryDirectory = temporaryDirectory; + this.jarSource = new File(temporaryDirectory, "jar-source"); } public void addClass(String filename, Class classToCopy) throws IOException { @@ -120,7 +120,7 @@ public File getFile() throws IOException { } public File getFile(String extension) throws IOException { - File file = new File(this.temporaryFolder, UUID.randomUUID() + "." + extension); + File file = new File(this.temporaryDirectory, UUID.randomUUID() + "." + extension); ZipUtil.pack(this.entries.toArray(new ZipEntrySource[0]), file); return file; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/ZipHeaderPeekInputStreamTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/ZipHeaderPeekInputStreamTests.java index 042fe763117b..514c1dc796ec 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/ZipHeaderPeekInputStreamTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/ZipHeaderPeekInputStreamTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,8 +21,6 @@ import org.junit.jupiter.api.Test; -import org.springframework.boot.loader.tools.JarWriter.ZipHeaderPeekInputStream; - import static org.assertj.core.api.Assertions.assertThat; /** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/ApplicationContentFilterTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/ApplicationContentFilterTests.java new file mode 100644 index 000000000000..36bbec30841d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/ApplicationContentFilterTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.tools.layer; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link ApplicationContentFilter}. + * + * @author Madhura Bhave + * @author Stephane Nicoll + * @author Phillip Webb + */ +class ApplicationContentFilterTests { + + @Test + void createWhenPatternIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new ApplicationContentFilter(null)) + .withMessage("Pattern must not be empty"); + } + + @Test + void createWhenPatternIsEmptyThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new ApplicationContentFilter("")) + .withMessage("Pattern must not be empty"); + } + + @Test + void matchesWhenWildcardPatternMatchesReturnsTrue() { + ApplicationContentFilter filter = new ApplicationContentFilter("META-INF/**"); + assertThat(filter.matches("META-INF/resources/application.yml")).isTrue(); + } + + @Test + void matchesWhenWildcardPatternDoesNotMatchReturnsFalse() { + ApplicationContentFilter filter = new ApplicationContentFilter("META-INF/**"); + assertThat(filter.matches("src/main/resources/application.yml")).isFalse(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/IncludeExcludeContentSelectorTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/IncludeExcludeContentSelectorTests.java new file mode 100644 index 000000000000..f5efbfe92449 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/IncludeExcludeContentSelectorTests.java @@ -0,0 +1,141 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.tools.layer; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.loader.tools.Layer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link IncludeExcludeContentSelector}. + * + * @author Madhura Bhave + * @author Phillip Webb + */ +class IncludeExcludeContentSelectorTests { + + private static final Layer LAYER = new Layer("test"); + + @Test + void createWhenLayerIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy( + () -> new IncludeExcludeContentSelector<>(null, Collections.emptyList(), Collections.emptyList())) + .withMessage("Layer must not be null"); + } + + @Test + void createWhenFactoryIsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new IncludeExcludeContentSelector<>(LAYER, null, null, null)) + .withMessage("FilterFactory must not be null"); + } + + @Test + void getLayerReturnsLayer() { + IncludeExcludeContentSelector selector = new IncludeExcludeContentSelector<>(LAYER, null, null); + assertThat(selector.getLayer()).isEqualTo(LAYER); + } + + @Test + void containsWhenEmptyIncludesAndEmptyExcludesReturnsTrue() { + List includes = Arrays.asList(); + List excludes = Arrays.asList(); + IncludeExcludeContentSelector selector = new IncludeExcludeContentSelector<>(LAYER, includes, excludes, + TestContentsFilter::new); + assertThat(selector.contains("A")).isTrue(); + } + + @Test + void containsWhenNullIncludesAndEmptyExcludesReturnsTrue() { + List includes = null; + List excludes = null; + IncludeExcludeContentSelector selector = new IncludeExcludeContentSelector<>(LAYER, includes, excludes, + TestContentsFilter::new); + assertThat(selector.contains("A")).isTrue(); + } + + @Test + void containsWhenEmptyIncludesAndNotExcludedReturnsTrue() { + List includes = Arrays.asList(); + List excludes = Arrays.asList("B"); + IncludeExcludeContentSelector selector = new IncludeExcludeContentSelector<>(LAYER, includes, excludes, + TestContentsFilter::new); + assertThat(selector.contains("A")).isTrue(); + } + + @Test + void containsWhenEmptyIncludesAndExcludedReturnsFalse() { + List includes = Arrays.asList(); + List excludes = Arrays.asList("A"); + IncludeExcludeContentSelector selector = new IncludeExcludeContentSelector<>(LAYER, includes, excludes, + TestContentsFilter::new); + assertThat(selector.contains("A")).isFalse(); + } + + @Test + void containsWhenIncludedAndEmptyExcludesReturnsTrue() { + List includes = Arrays.asList("A", "B"); + List excludes = Arrays.asList(); + IncludeExcludeContentSelector selector = new IncludeExcludeContentSelector<>(LAYER, includes, excludes, + TestContentsFilter::new); + assertThat(selector.contains("B")).isTrue(); + } + + @Test + void containsWhenIncludedAndNotExcludedReturnsTrue() { + List includes = Arrays.asList("A", "B"); + List excludes = Arrays.asList("C", "D"); + IncludeExcludeContentSelector selector = new IncludeExcludeContentSelector<>(LAYER, includes, excludes, + TestContentsFilter::new); + assertThat(selector.contains("B")).isTrue(); + } + + @Test + void containsWhenIncludedAndExcludedReturnsFalse() { + List includes = Arrays.asList("A", "B"); + List excludes = Arrays.asList("C", "D"); + IncludeExcludeContentSelector selector = new IncludeExcludeContentSelector<>(LAYER, includes, excludes, + TestContentsFilter::new); + assertThat(selector.contains("C")).isFalse(); + } + + /** + * {@link ContentFilter} used for testing. + */ + static class TestContentsFilter implements ContentFilter { + + private final String match; + + TestContentsFilter(String match) { + this.match = match; + } + + @Override + public boolean matches(String item) { + return this.match.equals(item); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/LibraryContentFilterTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/LibraryContentFilterTests.java new file mode 100644 index 000000000000..bcdaf986d238 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/layer/LibraryContentFilterTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.tools.layer; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.loader.tools.Library; +import org.springframework.boot.loader.tools.LibraryCoordinates; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link LibraryContentFilter}. + * + * @author Madhura Bhave + * @author Scott Frederick + * @author Phillip Webb + */ +class LibraryContentFilterTests { + + @Test + void createWhenCoordinatesPatternIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new LibraryContentFilter(null)) + .withMessage("CoordinatesPattern must not be empty"); + } + + @Test + void createWhenCoordinatesPatternIsEmptyThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new LibraryContentFilter("")) + .withMessage("CoordinatesPattern must not be empty"); + } + + @Test + void matchesWhenGroupIdIsNullAndToMatchHasWildcardReturnsTrue() { + LibraryContentFilter filter = new LibraryContentFilter("*:*"); + assertThat(filter.matches(mockLibrary(null, null, null))).isTrue(); + } + + @Test + void matchesWhenArtifactIdIsNullAndToMatchHasWildcardReturnsTrue() { + LibraryContentFilter filter = new LibraryContentFilter("org.acme:*"); + assertThat(filter.matches(mockLibrary("org.acme", null, null))).isTrue(); + } + + @Test + void matchesWhenVersionIsNullAndToMatchHasWildcardReturnsTrue() { + LibraryContentFilter filter = new LibraryContentFilter("org.acme:something:*"); + assertThat(filter.matches(mockLibrary("org.acme", "something", null))).isTrue(); + } + + @Test + void matchesWhenGroupIdDoesNotMatchReturnsFalse() { + LibraryContentFilter filter = new LibraryContentFilter("org.acme:*"); + assertThat(filter.matches(mockLibrary("other.foo", null, null))).isFalse(); + } + + @Test + void matchesWhenWhenArtifactIdDoesNotMatchReturnsFalse() { + LibraryContentFilter filter = new LibraryContentFilter("org.acme:test:*"); + assertThat(filter.matches(mockLibrary("org.acme", "other", null))).isFalse(); + } + + @Test + void matchesWhenArtifactIdMatchesReturnsTrue() { + LibraryContentFilter filter = new LibraryContentFilter("org.acme:test:*"); + assertThat(filter.matches(mockLibrary("org.acme", "test", null))).isTrue(); + } + + @Test + void matchesWhenVersionDoesNotMatchReturnsFalse() { + LibraryContentFilter filter = new LibraryContentFilter("org.acme:test:*SNAPSHOT"); + assertThat(filter.matches(mockLibrary("org.acme", "test", "1.0.0"))).isFalse(); + } + + @Test + void matchesWhenVersionMatchesReturnsTrue() { + LibraryContentFilter filter = new LibraryContentFilter("org.acme:test:*SNAPSHOT"); + assertThat(filter.matches(mockLibrary("org.acme", "test", "1.0.0-SNAPSHOT"))).isTrue(); + } + + private Library mockLibrary(String groupId, String artifactId, String version) { + return mockLibrary(LibraryCoordinates.of(groupId, artifactId, version)); + } + + private Library mockLibrary(LibraryCoordinates coordinates) { + Library library = mock(Library.class); + given(library.getCoordinates()).willReturn(coordinates); + return library; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-.txt b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-.txt new file mode 100644 index 000000000000..ed75eeb15ba1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-.txt @@ -0,0 +1,6 @@ +- "a" + - "aardvark" + - "cat" + - "dog" + - "hamster" + - "zerbra" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWhenAllFilesInDirectoryAreInNotInSameLayerUsesFiles.txt b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWhenAllFilesInDirectoryAreInNotInSameLayerUsesFiles.txt new file mode 100644 index 000000000000..dedad372e84d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWhenAllFilesInDirectoryAreInNotInSameLayerUsesFiles.txt @@ -0,0 +1,8 @@ +- "a": + - "a1/b1/c1" + - "a2/b1" +- "b": + - "a1/b1/c2" + - "a2/b2" +- "c": + - "a1/b2/" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWhenAllFilesInDirectoryAreInSameLayerUsesDirectory.txt b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWhenAllFilesInDirectoryAreInSameLayerUsesDirectory.txt new file mode 100644 index 000000000000..c463bda16b56 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWhenAllFilesInDirectoryAreInSameLayerUsesDirectory.txt @@ -0,0 +1,5 @@ +- "a": + - "a1/" +- "b": + - "a2/" +- "c": diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWhenLayerNotUsedDoesNotSkipLayer.txt b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWhenLayerNotUsedDoesNotSkipLayer.txt new file mode 100644 index 000000000000..c895ed2aadee --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWhenLayerNotUsedDoesNotSkipLayer.txt @@ -0,0 +1,7 @@ +- "a": + - "a1" + - "a2" +- "b": +- "c": + - "c1" + - "c2" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWhenSimpleNamesSortsAlphabetically.txt b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWhenSimpleNamesSortsAlphabetically.txt new file mode 100644 index 000000000000..420b66baa700 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWhenSimpleNamesSortsAlphabetically.txt @@ -0,0 +1,6 @@ +- "a": + - "aardvark" + - "cat" + - "dog" + - "hamster" + - "zerbra" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWhenSpaceInFileName.txt b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWhenSpaceInFileName.txt new file mode 100644 index 000000000000..4b3e278897fa --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWhenSpaceInFileName.txt @@ -0,0 +1,3 @@ +- "a": + - "a b" + - "a b/" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWritesLayersInIteratorOrder.txt b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWritesLayersInIteratorOrder.txt new file mode 100644 index 000000000000..d7d29b10764c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/LayersIndexTests-writeToWritesLayersInIteratorOrder.txt @@ -0,0 +1,9 @@ +- "b": + - "b1" + - "b2" +- "a": + - "a1" + - "a2" +- "c": + - "c1" + - "c2" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-loader/build.gradle new file mode 100644 index 000000000000..1a03bb4eda0a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/build.gradle @@ -0,0 +1,22 @@ +plugins { + id "java-library" + id "org.springframework.boot.conventions" + id "org.springframework.boot.deployed" +} + +description = "Spring Boot Loader" + +dependencies { + compileOnly("org.springframework:spring-core") + + testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) + testImplementation("org.assertj:assertj-core") + testImplementation("org.awaitility:awaitility") + testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.mockito:mockito-core") + testImplementation("org.springframework:spring-test") + + testRuntimeOnly("ch.qos.logback:logback-classic") + testRuntimeOnly("org.bouncycastle:bcprov-jdk16:1.46") + testRuntimeOnly("org.springframework:spring-webmvc") +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-loader/pom.xml deleted file mode 100644 index 09f459c6ef8b..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/pom.xml +++ /dev/null @@ -1,122 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-tools - ${revision} - - spring-boot-loader - Spring Boot Loader - Spring Boot Loader - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - - org.springframework - spring-core - true - - - - org.springframework.boot - spring-boot-test-support - test - - - ch.qos.logback - logback-classic - test - - - org.awaitility - awaitility - test - - - - org.bouncycastle - bcprov-jdk16 - 1.46 - test - - - org.springframework - spring-webmvc - test - - - - - integration - - true - - - - - org.apache.maven.plugins - maven-invoker-plugin - - ${project.build.directory}/local-repo - - - - prepare-integration-test - pre-integration-test - - install - - - - integration-test - - run - - - ${project.build.directory}/it - src/it/settings.xml - verify - true - ${skipTests} - true - - - - - - org.apache.maven.plugins - maven-antrun-plugin - - - cleanup-local-integration-repo - pre-integration-test - - run - - - - - - - - - - - - - - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-dir/application.properties b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-dir/application.properties deleted file mode 100644 index 42acafa985f1..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-dir/application.properties +++ /dev/null @@ -1,2 +0,0 @@ -loader.path: target,target/lib,. -loader.main: org.springframework.boot.load.it.jar.EmbeddedJarStarter \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-dir/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-dir/pom.xml deleted file mode 100644 index 62ccd436bd42..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-dir/pom.xml +++ /dev/null @@ -1,97 +0,0 @@ - - - 4.0.0 - org.springframework.boot.launcher.it - executable-dir - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - - - org.apache.maven.plugins - maven-compiler-plugin - @maven-compiler-plugin.version@ - - - org.apache.maven.plugins - maven-dependency-plugin - @maven-dependency-plugin.version@ - - - unpack - prepare-package - - copy - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - jar - - - ${project.build.directory}/lib - - - - copy - prepare-package - - copy-dependencies - - - ${project.build.directory}/lib - - - - - - org.apache.maven.plugins - maven-jar-plugin - @maven-jar-plugin.version@ - - - org.apache.maven.plugins - maven-surefire-plugin - @maven-surefire-plugin.version@ - - - org.codehaus.mojo - exec-maven-plugin - @exec-maven-plugin.version@ - - java - - -cp - ${project.build.directory}/lib/@project.artifactId@-@project.version@.jar - org.springframework.boot.loader.PropertiesLauncher - - - - - - - - org.eclipse.jetty - jetty-webapp - @jetty.version@ - - - org.eclipse.jetty - jetty-annotations - @jetty.version@ - - - org.springframework - spring-webmvc - @spring-framework.version@ - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-dir/src/main/java/org/springframework/launcher/it/jar/EmbeddedJarStarter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-dir/src/main/java/org/springframework/launcher/it/jar/EmbeddedJarStarter.java deleted file mode 100644 index 86afc3a61ec5..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-dir/src/main/java/org/springframework/launcher/it/jar/EmbeddedJarStarter.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.load.it.jar; - -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; -import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; -import org.springframework.web.servlet.DispatcherServlet; - -/** - * Main class to start the embedded server. - * - * @author Phillip Webb - */ -public final class EmbeddedJarStarter { - - public static void main(String[] args) throws Exception { - Server server = new Server(8080); - - ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); - context.setContextPath("/"); - server.setHandler(context); - - AnnotationConfigWebApplicationContext webApplicationContext = new AnnotationConfigWebApplicationContext(); - webApplicationContext.register(SpringConfiguration.class); - DispatcherServlet dispatcherServlet = new DispatcherServlet(webApplicationContext); - context.addServlet(new ServletHolder(dispatcherServlet), "/*"); - - server.start(); - server.join(); - } -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-dir/src/main/java/org/springframework/launcher/it/jar/ExampleController.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-dir/src/main/java/org/springframework/launcher/it/jar/ExampleController.java deleted file mode 100644 index 41df3759ee6e..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-dir/src/main/java/org/springframework/launcher/it/jar/ExampleController.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.load.it.jar; - -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseBody; - -/** - * Simple example Spring MVC Controller. - * - * @author Phillip Webb - */ -@Controller -public class ExampleController { - - @RequestMapping("/") - @ResponseBody - public String helloWorld() { - return "Hello Embedded Jar World!"; - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-dir/src/main/java/org/springframework/launcher/it/jar/SpringConfiguration.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-dir/src/main/java/org/springframework/launcher/it/jar/SpringConfiguration.java deleted file mode 100644 index c7c0d0bf2019..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-dir/src/main/java/org/springframework/launcher/it/jar/SpringConfiguration.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.load.it.jar; - -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; - -/** - * Spring configuration. - * - * @author Phillip Webb - */ -@Configuration(proxyBeanMethods = false) -@EnableWebMvc -@ComponentScan -public class SpringConfiguration { - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-jar/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-jar/pom.xml deleted file mode 100644 index 7111e1f2178f..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-jar/pom.xml +++ /dev/null @@ -1,111 +0,0 @@ - - - 4.0.0 - org.springframework.boot.launcher.it - executable-jar - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - - - maven-assembly-plugin - @maven-assembly-plugin.version@ - - - src/main/assembly/jar-with-dependencies.xml - - false - - - org.springframework.boot.loader.JarLauncher - - - org.springframework.boot.load.it.jar.EmbeddedJarStarter - - - - - - jar-with-dependencies - package - - single - - - - - - org.apache.maven.plugins - maven-compiler-plugin - @maven-compiler-plugin.version@ - - - org.apache.maven.plugins - maven-dependency-plugin - @maven-dependency-plugin.version@ - - - unpack - prepare-package - - unpack - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - jar - - - ${project.build.directory}/assembly - - - - copy - prepare-package - - copy-dependencies - - - ${project.build.directory}/assembly/lib - - - - - - org.apache.maven.plugins - maven-jar-plugin - @maven-jar-plugin.version@ - - - org.apache.maven.plugins - maven-surefire-plugin - @maven-surefire-plugin.version@ - - - - - - org.eclipse.jetty - jetty-webapp - @jetty.version@ - - - org.eclipse.jetty - jetty-annotations - @jetty.version@ - - - org.springframework - spring-webmvc - @spring-framework.version@ - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-jar/src/main/assembly/jar-with-dependencies.xml b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-jar/src/main/assembly/jar-with-dependencies.xml deleted file mode 100644 index 1f7cf8ec13a4..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-jar/src/main/assembly/jar-with-dependencies.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - full - - jar - - false - - - - - ${project.groupId}:${project.artifactId} - - true - - - - - ${project.build.directory}/assembly - / - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-jar/src/main/java/org/springframework/launcher/it/jar/EmbeddedJarStarter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-jar/src/main/java/org/springframework/launcher/it/jar/EmbeddedJarStarter.java deleted file mode 100644 index 86afc3a61ec5..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-jar/src/main/java/org/springframework/launcher/it/jar/EmbeddedJarStarter.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.load.it.jar; - -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; -import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; -import org.springframework.web.servlet.DispatcherServlet; - -/** - * Main class to start the embedded server. - * - * @author Phillip Webb - */ -public final class EmbeddedJarStarter { - - public static void main(String[] args) throws Exception { - Server server = new Server(8080); - - ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); - context.setContextPath("/"); - server.setHandler(context); - - AnnotationConfigWebApplicationContext webApplicationContext = new AnnotationConfigWebApplicationContext(); - webApplicationContext.register(SpringConfiguration.class); - DispatcherServlet dispatcherServlet = new DispatcherServlet(webApplicationContext); - context.addServlet(new ServletHolder(dispatcherServlet), "/*"); - - server.start(); - server.join(); - } -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-jar/src/main/java/org/springframework/launcher/it/jar/ExampleController.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-jar/src/main/java/org/springframework/launcher/it/jar/ExampleController.java deleted file mode 100644 index 41df3759ee6e..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-jar/src/main/java/org/springframework/launcher/it/jar/ExampleController.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.load.it.jar; - -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseBody; - -/** - * Simple example Spring MVC Controller. - * - * @author Phillip Webb - */ -@Controller -public class ExampleController { - - @RequestMapping("/") - @ResponseBody - public String helloWorld() { - return "Hello Embedded Jar World!"; - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-jar/src/main/java/org/springframework/launcher/it/jar/SpringConfiguration.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-jar/src/main/java/org/springframework/launcher/it/jar/SpringConfiguration.java deleted file mode 100644 index c7c0d0bf2019..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-jar/src/main/java/org/springframework/launcher/it/jar/SpringConfiguration.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.load.it.jar; - -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; - -/** - * Spring configuration. - * - * @author Phillip Webb - */ -@Configuration(proxyBeanMethods = false) -@EnableWebMvc -@ComponentScan -public class SpringConfiguration { - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props-lib/loader.properties b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props-lib/loader.properties deleted file mode 100644 index 48a576ba043d..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props-lib/loader.properties +++ /dev/null @@ -1 +0,0 @@ -loader.path=jar:file:target/executable-props-lib-0.0.1.BUILD-SNAPSHOT-dependencies.jar/!BOOT-INF/lib \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props-lib/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props-lib/pom.xml deleted file mode 100644 index c466d86f44d9..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props-lib/pom.xml +++ /dev/null @@ -1,114 +0,0 @@ - - - 4.0.0 - org.springframework.boot.launcher.it - executable-props-lib - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - - - maven-assembly-plugin - @maven-assembly-plugin.version@ - - - app - package - - single - - - - src/main/assembly/app.xml - - false - - - org.springframework.boot.loader.PropertiesLauncher - - - org.springframework.boot.launcher.it.props.EmbeddedJarStarter - - - - - - depedendencies - package - - single - - - - src/main/assembly/dependencies.xml - - false - - - - - - org.apache.maven.plugins - maven-compiler-plugin - @maven-compiler-plugin.version@ - - - org.apache.maven.plugins - maven-dependency-plugin - @maven-dependency-plugin.version@ - - - unpack - prepare-package - - unpack - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - jar - - - ${project.build.directory}/app-assembly - - - - copy - prepare-package - - copy-dependencies - - - ${project.build.directory}/dependencies-assembly/BOOT-INF/lib - - - - - - org.apache.maven.plugins - maven-jar-plugin - @maven-jar-plugin.version@ - - - org.apache.maven.plugins - maven-surefire-plugin - @maven-surefire-plugin.version@ - - - - - - org.springframework - spring-context - @spring-framework.version@ - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props-lib/src/main/assembly/app.xml b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props-lib/src/main/assembly/app.xml deleted file mode 100644 index 83a4f927e243..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props-lib/src/main/assembly/app.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - app - - jar - - false - - - - - ${project.groupId}:${project.artifactId} - - BOOT-INF/classes - true - - - - - ${project.build.directory}/app-assembly - / - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props-lib/src/main/assembly/dependencies.xml b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props-lib/src/main/assembly/dependencies.xml deleted file mode 100644 index 306b55e4e9c6..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props-lib/src/main/assembly/dependencies.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - dependencies - - jar - - false - - - ${project.build.directory}/dependencies-assembly - / - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props-lib/src/main/java/org/springframework/boot/launcher/it/props/EmbeddedJarStarter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props-lib/src/main/java/org/springframework/boot/launcher/it/props/EmbeddedJarStarter.java deleted file mode 100644 index 23a4a38fd061..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props-lib/src/main/java/org/springframework/boot/launcher/it/props/EmbeddedJarStarter.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.launcher.it.props; - -import org.springframework.context.annotation.AnnotationConfigApplicationContext; - -/** - * Main class to start the embedded server. - * - * @author Dave Syer - */ -public final class EmbeddedJarStarter { - - public static void main(String[] args) throws Exception { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfiguration.class); - context.getBean(SpringConfiguration.class).run(args); - context.close(); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props-lib/src/main/java/org/springframework/boot/launcher/it/props/SpringConfiguration.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props-lib/src/main/java/org/springframework/boot/launcher/it/props/SpringConfiguration.java deleted file mode 100644 index 9bdd06d44764..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props-lib/src/main/java/org/springframework/boot/launcher/it/props/SpringConfiguration.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.launcher.it.props; - -import java.io.IOException; -import java.util.Properties; - -import org.springframework.beans.factory.InitializingBean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.io.ClassPathResource; - -/** - * Spring configuration. - * - * @author Dave Syer - */ -@Configuration(proxyBeanMethods = false) -@ComponentScan -public class SpringConfiguration implements InitializingBean { - - private String message = "Jar"; - - @Override - public void afterPropertiesSet() throws IOException { - Properties props = new Properties(); - props.load(new ClassPathResource("application.properties").getInputStream()); - String value = props.getProperty("message"); - if (value!=null) { - this.message = value; - } - - } - - public void run(String... args) { - System.err.println("Hello Embedded " + this.message + "!"); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props-lib/src/main/resources/application.properties b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props-lib/src/main/resources/application.properties deleted file mode 100644 index c11051e34771..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props-lib/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -message: World \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props-lib/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props-lib/verify.groovy deleted file mode 100644 index b855fbe7c7a7..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props-lib/verify.groovy +++ /dev/null @@ -1,17 +0,0 @@ -def jarfile = './target/executable-props-lib-0.0.1.BUILD-SNAPSHOT-app.jar' - -new File("${basedir}/application.properties").delete() - -String exec(String command) { - def proc = command.execute([], basedir) - proc.waitFor() - proc.err.text -} - -String out = exec("java -jar ${jarfile}") -assert out.contains('Hello Embedded World!'), - 'Using -jar my.jar should load dependencies from separate jar and use the application.properties from the jar\n' + out - -out = exec("java -cp ${jarfile} org.springframework.boot.loader.PropertiesLauncher") -assert out.contains('Hello Embedded World!'), - 'Using -cp my.jar with PropertiesLauncher should load dependencies from separate jar and use the application.properties from the jar\n' + out diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props/pom.xml deleted file mode 100644 index 3e2a75482ca1..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props/pom.xml +++ /dev/null @@ -1,101 +0,0 @@ - - - 4.0.0 - org.springframework.boot.launcher.it - executable-props - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - - - maven-assembly-plugin - @maven-assembly-plugin.version@ - - - src/main/assembly/jar-with-dependencies.xml - - false - - - org.springframework.boot.loader.PropertiesLauncher - - - org.springframework.boot.load.it.props.EmbeddedJarStarter - - - - - - jar-with-dependencies - package - - single - - - - - - org.apache.maven.plugins - maven-compiler-plugin - @maven-compiler-plugin.version@ - - - org.apache.maven.plugins - maven-dependency-plugin - @maven-dependency-plugin.version@ - - - unpack - prepare-package - - unpack - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - jar - - - ${project.build.directory}/assembly - - - - copy - prepare-package - - copy-dependencies - - - ${project.build.directory}/assembly/BOOT-INF/lib - - - - - - org.apache.maven.plugins - maven-jar-plugin - @maven-jar-plugin.version@ - - - org.apache.maven.plugins - maven-surefire-plugin - @maven-surefire-plugin.version@ - - - - - - org.springframework - spring-context - @spring-framework.version@ - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props/src/main/assembly/jar-with-dependencies.xml b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props/src/main/assembly/jar-with-dependencies.xml deleted file mode 100644 index 2dbf0f3a5021..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props/src/main/assembly/jar-with-dependencies.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - full - - jar - - false - - - - - ${project.groupId}:${project.artifactId} - - BOOT-INF/classes - true - - - - - ${project.build.directory}/assembly - / - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props/src/main/java/org/springframework/launcher/it/props/EmbeddedJarStarter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props/src/main/java/org/springframework/launcher/it/props/EmbeddedJarStarter.java deleted file mode 100644 index 1837293b5223..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props/src/main/java/org/springframework/launcher/it/props/EmbeddedJarStarter.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.load.it.props; - -import org.springframework.context.annotation.AnnotationConfigApplicationContext; - -/** - * Main class to start the embedded server. - * - * @author Dave Syer - */ -public final class EmbeddedJarStarter { - - public static void main(String[] args) throws Exception { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfiguration.class); - context.getBean(SpringConfiguration.class).run(args); - context.close(); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props/src/main/java/org/springframework/launcher/it/props/SpringConfiguration.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props/src/main/java/org/springframework/launcher/it/props/SpringConfiguration.java deleted file mode 100644 index eb5bc598b11c..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props/src/main/java/org/springframework/launcher/it/props/SpringConfiguration.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.load.it.props; - -import java.io.IOException; -import java.util.Properties; - -import org.springframework.beans.factory.InitializingBean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.io.ClassPathResource; - -/** - * Spring configuration. - * - * @author Dave Syer - */ -@Configuration(proxyBeanMethods = false) -@ComponentScan -public class SpringConfiguration implements InitializingBean { - - private String message = "Jar"; - - @Override - public void afterPropertiesSet() throws IOException { - Properties props = new Properties(); - props.load(new ClassPathResource("application.properties").getInputStream()); - String value = props.getProperty("message"); - if (value!=null) { - this.message = value; - } - - } - - public void run(String... args) { - System.err.println("Hello Embedded " + this.message + "!"); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props/src/main/resources/application.properties b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props/src/main/resources/application.properties deleted file mode 100644 index c11051e34771..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -message: World \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props/verify.groovy deleted file mode 100644 index 0a0c034267a9..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-props/verify.groovy +++ /dev/null @@ -1,31 +0,0 @@ -def jarfile = './target/executable-props-0.0.1.BUILD-SNAPSHOT-full.jar' - -new File("${basedir}/application.properties").delete() - -String exec(String command) { - def proc = command.execute([], basedir) - proc.waitFor() - proc.err.text -} - -String out = exec("java -jar ${jarfile}") -assert out.contains('Hello Embedded World!'), - 'Using -jar my.jar should use the application.properties from the jar\n' + out - -out = exec("java -cp ${jarfile} org.springframework.boot.loader.PropertiesLauncher") -assert out.contains('Hello Embedded World!'), - 'Using -cp my.jar with PropertiesLauncher should use the application.properties from the jar\n' + out - -new File("${basedir}/application.properties").withWriter { it -> it << "message: Foo" } -out = exec("java -jar ${jarfile}") -assert out.contains('Hello Embedded World!'), - 'Should use the application.properties from the jar in preference to local filesystem\n' + out - -out = exec("java -Dloader.path=.,lib -jar ${jarfile}") -assert out.contains('Hello Embedded Foo!'), - 'With loader.path=.,lib should use the application.properties from the local filesystem\n' + out - -new File("${basedir}/target/application.properties").withWriter { it -> it << "message: Spam" } -out = exec("java -Dloader.path=target,.,lib -jar ${jarfile}") -assert out.contains('Hello Embedded Spam!'), - 'With loader.path=target,.,lib should use the application.properties from the target directory\n' + out \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-war/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-war/pom.xml deleted file mode 100644 index af6d71231e52..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-war/pom.xml +++ /dev/null @@ -1,90 +0,0 @@ - - - 4.0.0 - org.springframework.boot.launcher.it - executable-war - 0.0.1.BUILD-SNAPSHOT - war - - UTF-8 - @java.version@ - @java.version@ - - - - - org.apache.maven.plugins - maven-compiler-plugin - @maven-compiler-plugin.version@ - - - org.apache.maven.plugins - maven-dependency-plugin - @maven-dependency-plugin.version@ - - - unpack - prepare-package - - unpack - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - jar - - - ${project.build.directory}/${project.artifactId}-${project.version} - - - - - - org.apache.maven.plugins - maven-surefire-plugin - @maven-surefire-plugin.version@ - - - org.apache.maven.plugins - maven-war-plugin - @maven-war-plugin.version@ - - - - org.springframework.boot.loader.WarLauncher - - - org.springframework.boot.load.it.war.embedded.EmbeddedWarStarter - - - - - - - - - org.eclipse.jetty - jetty-webapp - @jetty.version@ - - - org.eclipse.jetty - jetty-plus - @jetty.version@ - - - org.eclipse.jetty - jetty-annotations - @jetty.version@ - - - org.springframework - spring-webmvc - @spring-framework.version@ - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-war/src/main/java/org/springframework/launcher/it/war/ExampleController.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-war/src/main/java/org/springframework/launcher/it/war/ExampleController.java deleted file mode 100644 index 6c0cbe3ca0b4..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-war/src/main/java/org/springframework/launcher/it/war/ExampleController.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.load.it.war; - -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseBody; - -/** - * Simple example Spring MVC Controller. - * - * @author Phillip Webb - */ -@Controller -public class ExampleController { - - @RequestMapping("/") - @ResponseBody - public String helloWorld() { - return "Hello Embedded WAR World!"; - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-war/src/main/java/org/springframework/launcher/it/war/SpringConfiguration.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-war/src/main/java/org/springframework/launcher/it/war/SpringConfiguration.java deleted file mode 100644 index 1bc22a2efd5b..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-war/src/main/java/org/springframework/launcher/it/war/SpringConfiguration.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.load.it.war; - -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; - -/** - * Spring configuration. - * - * @author Phillip Webb - */ -@Configuration(proxyBeanMethods = false) -@EnableWebMvc -@ComponentScan -public class SpringConfiguration { - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-war/src/main/java/org/springframework/launcher/it/war/SpringInitializer.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-war/src/main/java/org/springframework/launcher/it/war/SpringInitializer.java deleted file mode 100644 index ee0cf7f8824c..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-war/src/main/java/org/springframework/launcher/it/war/SpringInitializer.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.load.it.war; - -import javax.servlet.ServletContext; -import javax.servlet.ServletException; - -import org.springframework.web.WebApplicationInitializer; -import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; -import org.springframework.web.servlet.DispatcherServlet; - -/** - * Spring {@link WebApplicationInitializer} for classic WAR deployment. - * - * @author Phillip Webb - */ -public class SpringInitializer implements WebApplicationInitializer { - - @Override - public void onStartup(ServletContext servletContext) throws ServletException { - AnnotationConfigWebApplicationContext webApplicationContext = new AnnotationConfigWebApplicationContext(); - webApplicationContext.register(SpringConfiguration.class); - servletContext.addServlet("dispatcherServlet", - new DispatcherServlet(webApplicationContext)).addMapping("/*"); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-war/src/main/java/org/springframework/launcher/it/war/embedded/EmbeddedWarStarter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-war/src/main/java/org/springframework/launcher/it/war/embedded/EmbeddedWarStarter.java deleted file mode 100644 index 4aac0a7dc692..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-war/src/main/java/org/springframework/launcher/it/war/embedded/EmbeddedWarStarter.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.load.it.war.embedded; - -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.webapp.Configuration; -import org.eclipse.jetty.webapp.WebAppContext; -import org.springframework.boot.load.it.war.SpringInitializer; - -/** - * Starter to launch the embedded server. NOTE: Jetty annotation scanning is not - * compatible with executable WARs so we must specify the {@link SpringInitializer}. - * - * @author Phillip Webb - */ -public final class EmbeddedWarStarter { - - public static void main(String[] args) throws Exception { - Server server = new Server(8080); - - WebAppContext webAppContext = new WebAppContext(); - webAppContext.setContextPath("/"); - webAppContext.setConfigurations(new Configuration[] { - new WebApplicationInitializersConfiguration(SpringInitializer.class) }); - - webAppContext.setParentLoaderPriority(true); - server.setHandler(webAppContext); - server.start(); - - server.join(); - } -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-war/src/main/java/org/springframework/launcher/it/war/embedded/WebApplicationInitializersConfiguration.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-war/src/main/java/org/springframework/launcher/it/war/embedded/WebApplicationInitializersConfiguration.java deleted file mode 100644 index 0d544e30763e..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/executable-war/src/main/java/org/springframework/launcher/it/war/embedded/WebApplicationInitializersConfiguration.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.load.it.war.embedded; - -import javax.servlet.ServletContextEvent; -import javax.servlet.ServletContextListener; - -import org.eclipse.jetty.webapp.AbstractConfiguration; -import org.eclipse.jetty.webapp.Configuration; -import org.eclipse.jetty.webapp.WebAppContext; -import org.springframework.util.Assert; -import org.springframework.web.WebApplicationInitializer; - -/** - * Jetty {@link Configuration} that allows Spring {@link WebApplicationInitializer} to be - * started. This is required because Jetty annotation scanning does not work with packaged - * WARs. - * - * @author Phillip Webb - */ -public class WebApplicationInitializersConfiguration extends AbstractConfiguration { - - private Class[] webApplicationInitializers; - - public WebApplicationInitializersConfiguration(Class webApplicationInitializer, - Class... webApplicationInitializers) { - this.webApplicationInitializers = new Class[webApplicationInitializers.length + 1]; - this.webApplicationInitializers[0] = webApplicationInitializer; - System.arraycopy(webApplicationInitializers, 0, this.webApplicationInitializers, - 1, webApplicationInitializers.length); - for (Class i : webApplicationInitializers) { - Assert.notNull(i, "WebApplicationInitializer must not be null"); - Assert.isAssignable(WebApplicationInitializer.class, i); - } - } - - @Override - public void configure(WebAppContext context) throws Exception { - context.getServletContext().addListener(new ServletContextListener() { - - @Override - public void contextInitialized(ServletContextEvent sce) { - try { - for (Class webApplicationInitializer : webApplicationInitializers) { - WebApplicationInitializer initializer = (WebApplicationInitializer) webApplicationInitializer.newInstance(); - initializer.onStartup(sce.getServletContext()); - } - } - catch (Exception ex) { - throw new RuntimeException(ex); - } - } - - @Override - public void contextDestroyed(ServletContextEvent sce) { - } - }); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/settings.xml b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/settings.xml deleted file mode 100644 index e1e0ace341b9..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/it/settings.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - it-repo - - true - - - - local.central - @localRepositoryUrl@ - - true - - - true - - - - - - local.central - @localRepositoryUrl@ - - true - - - true - - - - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ClassPathIndexFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ClassPathIndexFile.java new file mode 100644 index 000000000000..f3deb7b0d7d5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ClassPathIndexFile.java @@ -0,0 +1,124 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * A class path index file that provides ordering information for JARs. + * + * @author Madhura Bhave + * @author Phillip Webb + */ +final class ClassPathIndexFile { + + private final File root; + + private final List lines; + + private ClassPathIndexFile(File root, List lines) { + this.root = root; + this.lines = lines.stream().map(this::extractName).collect(Collectors.toList()); + } + + private String extractName(String line) { + if (line.startsWith("- \"") && line.endsWith("\"")) { + return line.substring(3, line.length() - 1); + } + throw new IllegalStateException("Malformed classpath index line [" + line + "]"); + } + + int size() { + return this.lines.size(); + } + + boolean containsEntry(String name) { + if (name == null || name.isEmpty()) { + return false; + } + return this.lines.contains(name); + } + + List getUrls() { + return Collections.unmodifiableList(this.lines.stream().map(this::asUrl).collect(Collectors.toList())); + } + + private URL asUrl(String line) { + try { + return new File(this.root, line).toURI().toURL(); + } + catch (MalformedURLException ex) { + throw new IllegalStateException(ex); + } + } + + static ClassPathIndexFile loadIfPossible(URL root, String location) throws IOException { + return loadIfPossible(asFile(root), location); + } + + private static ClassPathIndexFile loadIfPossible(File root, String location) throws IOException { + return loadIfPossible(root, new File(root, location)); + } + + private static ClassPathIndexFile loadIfPossible(File root, File indexFile) throws IOException { + if (indexFile.exists() && indexFile.isFile()) { + try (InputStream inputStream = new FileInputStream(indexFile)) { + return new ClassPathIndexFile(root, loadLines(inputStream)); + } + } + return null; + } + + private static List loadLines(InputStream inputStream) throws IOException { + List lines = new ArrayList<>(); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + String line = reader.readLine(); + while (line != null) { + if (!line.trim().isEmpty()) { + lines.add(line); + } + line = reader.readLine(); + } + return Collections.unmodifiableList(lines); + } + + private static File asFile(URL url) { + if (!"file".equals(url.getProtocol())) { + throw new IllegalArgumentException("URL does not reference a file"); + } + try { + return new File(url.toURI()); + } + catch (URISyntaxException ex) { + return new File(url.getPath()); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java index 19ace6d9bd1f..07fad13e0b89 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,11 @@ package org.springframework.boot.loader; +import java.io.IOException; +import java.net.URL; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; -import java.util.jar.JarEntry; import java.util.jar.Manifest; import org.springframework.boot.loader.archive.Archive; @@ -28,15 +30,23 @@ * * @author Phillip Webb * @author Andy Wilkinson + * @author Madhura Bhave * @since 1.0.0 */ public abstract class ExecutableArchiveLauncher extends Launcher { + private static final String START_CLASS_ATTRIBUTE = "Start-Class"; + + protected static final String BOOT_CLASSPATH_INDEX_ATTRIBUTE = "Spring-Boot-Classpath-Index"; + private final Archive archive; + private final ClassPathIndexFile classPathIndex; + public ExecutableArchiveLauncher() { try { this.archive = createArchive(); + this.classPathIndex = getClassPathIndex(this.archive); } catch (Exception ex) { throw new IllegalStateException(ex); @@ -44,11 +54,17 @@ public ExecutableArchiveLauncher() { } protected ExecutableArchiveLauncher(Archive archive) { - this.archive = archive; + try { + this.archive = archive; + this.classPathIndex = getClassPathIndex(this.archive); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } } - protected final Archive getArchive() { - return this.archive; + protected ClassPathIndexFile getClassPathIndex(Archive archive) throws IOException { + return null; } @Override @@ -56,7 +72,7 @@ protected String getMainClass() throws Exception { Manifest manifest = this.archive.getManifest(); String mainClass = null; if (manifest != null) { - mainClass = manifest.getMainAttributes().getValue("Start-Class"); + mainClass = manifest.getMainAttributes().getValue(START_CLASS_ATTRIBUTE); } if (mainClass == null) { throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this); @@ -65,27 +81,99 @@ protected String getMainClass() throws Exception { } @Override - protected List getClassPathArchives() throws Exception { - List archives = new ArrayList<>(this.archive.getNestedArchives(this::isNestedArchive)); - postProcessClassPathArchives(archives); + protected ClassLoader createClassLoader(Iterator archives) throws Exception { + List urls = new ArrayList<>(guessClassPathSize()); + while (archives.hasNext()) { + urls.add(archives.next().getUrl()); + } + if (this.classPathIndex != null) { + urls.addAll(this.classPathIndex.getUrls()); + } + return createClassLoader(urls.toArray(new URL[0])); + } + + private int guessClassPathSize() { + if (this.classPathIndex != null) { + return this.classPathIndex.size() + 10; + } + return 50; + } + + @Override + protected Iterator getClassPathArchivesIterator() throws Exception { + Archive.EntryFilter searchFilter = this::isSearchCandidate; + Iterator archives = this.archive.getNestedArchives(searchFilter, + (entry) -> isNestedArchive(entry) && !isEntryIndexed(entry)); + if (isPostProcessingClassPathArchives()) { + archives = applyClassPathArchivePostProcessing(archives); + } return archives; } + private boolean isEntryIndexed(Archive.Entry entry) { + if (this.classPathIndex != null) { + return this.classPathIndex.containsEntry(entry.getName()); + } + return false; + } + + private Iterator applyClassPathArchivePostProcessing(Iterator archives) throws Exception { + List list = new ArrayList<>(); + while (archives.hasNext()) { + list.add(archives.next()); + } + postProcessClassPathArchives(list); + return list.iterator(); + } + + /** + * Determine if the specified entry is a candidate for further searching. + * @param entry the entry to check + * @return {@code true} if the entry is a candidate for further searching + * @since 2.3.0 + */ + protected boolean isSearchCandidate(Archive.Entry entry) { + return true; + } + /** - * Determine if the specified {@link JarEntry} is a nested item that should be added - * to the classpath. The method is called once for each entry. - * @param entry the jar entry - * @return {@code true} if the entry is a nested item (jar or folder) + * Determine if the specified entry is a nested item that should be added to the + * classpath. + * @param entry the entry to check + * @return {@code true} if the entry is a nested item (jar or directory) */ protected abstract boolean isNestedArchive(Archive.Entry entry); + /** + * Return if post processing needs to be applied to the archives. For back + * compatibility this method returns {@code true}, but subclasses that don't override + * {@link #postProcessClassPathArchives(List)} should provide an implementation that + * returns {@code false}. + * @return if the {@link #postProcessClassPathArchives(List)} method is implemented + * @since 2.3.0 + */ + protected boolean isPostProcessingClassPathArchives() { + return true; + } + /** * Called to post-process archive entries before they are used. Implementations can * add and remove entries. * @param archives the archives * @throws Exception if the post processing fails + * @see #isPostProcessingClassPathArchives() */ protected void postProcessClassPathArchives(List archives) throws Exception { } + @Override + protected boolean isExploded() { + return this.archive.isExploded(); + } + + @Override + protected final Archive getArchive() { + return this.archive; + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java index d2f12c9e8934..c10b28e6ea5d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,13 @@ package org.springframework.boot.loader; +import java.io.IOException; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + import org.springframework.boot.loader.archive.Archive; +import org.springframework.boot.loader.archive.Archive.EntryFilter; +import org.springframework.boot.loader.archive.ExplodedArchive; /** * {@link Launcher} for JAR based archives. This launcher assumes that dependency jars are @@ -25,13 +31,19 @@ * * @author Phillip Webb * @author Andy Wilkinson + * @author Madhura Bhave * @since 1.0.0 */ public class JarLauncher extends ExecutableArchiveLauncher { - static final String BOOT_INF_CLASSES = "BOOT-INF/classes/"; + private static final String DEFAULT_CLASSPATH_INDEX_LOCATION = "BOOT-INF/classpath.idx"; - static final String BOOT_INF_LIB = "BOOT-INF/lib/"; + static final EntryFilter NESTED_ARCHIVE_ENTRY_FILTER = (entry) -> { + if (entry.isDirectory()) { + return entry.getName().equals("BOOT-INF/classes/"); + } + return entry.getName().startsWith("BOOT-INF/lib/"); + }; public JarLauncher() { } @@ -41,11 +53,35 @@ protected JarLauncher(Archive archive) { } @Override - protected boolean isNestedArchive(Archive.Entry entry) { - if (entry.isDirectory()) { - return entry.getName().equals(BOOT_INF_CLASSES); + protected ClassPathIndexFile getClassPathIndex(Archive archive) throws IOException { + // Only needed for exploded archives, regular ones already have a defined order + if (archive instanceof ExplodedArchive) { + String location = getClassPathIndexFileLocation(archive); + return ClassPathIndexFile.loadIfPossible(archive.getUrl(), location); } - return entry.getName().startsWith(BOOT_INF_LIB); + return super.getClassPathIndex(archive); + } + + private String getClassPathIndexFileLocation(Archive archive) throws IOException { + Manifest manifest = archive.getManifest(); + Attributes attributes = (manifest != null) ? manifest.getMainAttributes() : null; + String location = (attributes != null) ? attributes.getValue(BOOT_CLASSPATH_INDEX_ATTRIBUTE) : null; + return (location != null) ? location : DEFAULT_CLASSPATH_INDEX_LOCATION; + } + + @Override + protected boolean isPostProcessingClassPathArchives() { + return false; + } + + @Override + protected boolean isSearchCandidate(Archive.Entry entry) { + return entry.getName().startsWith("BOOT-INF/"); + } + + @Override + protected boolean isNestedArchive(Archive.Entry entry) { + return NESTED_ARCHIVE_ENTRY_FILTER.matches(entry); } public static void main(String[] args) throws Exception { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java index 9a6ca13269b8..75ac50815094 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,9 @@ package org.springframework.boot.loader; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; import java.net.JarURLConnection; import java.net.URL; import java.net.URLClassLoader; @@ -24,8 +26,11 @@ import java.security.AccessController; import java.security.PrivilegedExceptionAction; import java.util.Enumeration; +import java.util.function.Supplier; import java.util.jar.JarFile; +import java.util.jar.Manifest; +import org.springframework.boot.loader.archive.Archive; import org.springframework.boot.loader.jar.Handler; /** @@ -38,21 +43,58 @@ */ public class LaunchedURLClassLoader extends URLClassLoader { + private static final int BUFFER_SIZE = 4096; + static { ClassLoader.registerAsParallelCapable(); } + private final boolean exploded; + + private final Archive rootArchive; + + private final Object packageLock = new Object(); + + private volatile DefinePackageCallType definePackageCallType; + /** * Create a new {@link LaunchedURLClassLoader} instance. * @param urls the URLs from which to load classes and resources * @param parent the parent class loader for delegation */ public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) { + this(false, urls, parent); + } + + /** + * Create a new {@link LaunchedURLClassLoader} instance. + * @param exploded if the underlying archive is exploded + * @param urls the URLs from which to load classes and resources + * @param parent the parent class loader for delegation + */ + public LaunchedURLClassLoader(boolean exploded, URL[] urls, ClassLoader parent) { + this(exploded, null, urls, parent); + } + + /** + * Create a new {@link LaunchedURLClassLoader} instance. + * @param exploded if the underlying archive is exploded + * @param rootArchive the root archive or {@code null} + * @param urls the URLs from which to load classes and resources + * @param parent the parent class loader for delegation + * @since 2.3.1 + */ + public LaunchedURLClassLoader(boolean exploded, Archive rootArchive, URL[] urls, ClassLoader parent) { super(urls, parent); + this.exploded = exploded; + this.rootArchive = rootArchive; } @Override public URL findResource(String name) { + if (this.exploded) { + return super.findResource(name); + } Handler.setUseFastConnectionExceptions(true); try { return super.findResource(name); @@ -64,6 +106,9 @@ public URL findResource(String name) { @Override public Enumeration findResources(String name) throws IOException { + if (this.exploded) { + return super.findResources(name); + } Handler.setUseFastConnectionExceptions(true); try { return new UseFastConnectionExceptionsEnumeration(super.findResources(name)); @@ -75,6 +120,20 @@ public Enumeration findResources(String name) throws IOException { @Override protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (name.startsWith("org.springframework.boot.loader.jarmode.")) { + try { + Class result = loadClassInLaunchedClassLoader(name); + if (resolve) { + resolveClass(result); + } + return result; + } + catch (ClassNotFoundException ex) { + } + } + if (this.exploded) { + return super.loadClass(name, resolve); + } Handler.setUseFastConnectionExceptions(true); try { try { @@ -96,6 +155,35 @@ protected Class loadClass(String name, boolean resolve) throws ClassNotFoundE } } + private Class loadClassInLaunchedClassLoader(String name) throws ClassNotFoundException { + String internalName = name.replace('.', '/') + ".class"; + InputStream inputStream = getParent().getResourceAsStream(internalName); + if (inputStream == null) { + throw new ClassNotFoundException(name); + } + try { + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + byte[] buffer = new byte[BUFFER_SIZE]; + int bytesRead = -1; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + inputStream.close(); + byte[] bytes = outputStream.toByteArray(); + Class definedClass = defineClass(name, bytes, 0, bytes.length); + definePackageIfNecessary(name); + return definedClass; + } + finally { + inputStream.close(); + } + } + catch (IOException ex) { + throw new ClassNotFoundException("Cannot load resource for class [" + name + "]", ex); + } + } + /** * Define a package before a {@code findClass} call is made. This is necessary to * ensure that the appropriate manifest for nested JARs is associated with the @@ -153,10 +241,65 @@ private void definePackage(String className, String packageName) { } } + @Override + protected Package definePackage(String name, Manifest man, URL url) throws IllegalArgumentException { + if (!this.exploded) { + return super.definePackage(name, man, url); + } + synchronized (this.packageLock) { + return doDefinePackage(DefinePackageCallType.MANIFEST, () -> super.definePackage(name, man, url)); + } + } + + @Override + protected Package definePackage(String name, String specTitle, String specVersion, String specVendor, + String implTitle, String implVersion, String implVendor, URL sealBase) throws IllegalArgumentException { + if (!this.exploded) { + return super.definePackage(name, specTitle, specVersion, specVendor, implTitle, implVersion, implVendor, + sealBase); + } + synchronized (this.packageLock) { + if (this.definePackageCallType == null) { + // We're not part of a call chain which means that the URLClassLoader + // is trying to define a package for our exploded JAR. We use the + // manifest version to ensure package attributes are set + Manifest manifest = getManifest(this.rootArchive); + if (manifest != null) { + return definePackage(name, manifest, sealBase); + } + } + return doDefinePackage(DefinePackageCallType.ATTRIBUTES, () -> super.definePackage(name, specTitle, + specVersion, specVendor, implTitle, implVersion, implVendor, sealBase)); + } + } + + private Manifest getManifest(Archive archive) { + try { + return (archive != null) ? archive.getManifest() : null; + } + catch (IOException ex) { + return null; + } + } + + private T doDefinePackage(DefinePackageCallType type, Supplier call) { + DefinePackageCallType existingType = this.definePackageCallType; + try { + this.definePackageCallType = type; + return call.get(); + } + finally { + this.definePackageCallType = existingType; + } + } + /** * Clear URL caches. */ public void clearCache() { + if (this.exploded) { + return; + } for (URL url : getURLs()) { try { URLConnection connection = url.openConnection(); @@ -211,4 +354,22 @@ public URL nextElement() { } + /** + * The different types of call made to define a package. We track these for exploded + * jars so that we can detect packages that should have manifest attributes applied. + */ + private enum DefinePackageCallType { + + /** + * A define package call from a resource that has a manifest. + */ + MANIFEST, + + /** + * A define package call with a direct set of attributes. + */ + ATTRIBUTES + + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/Launcher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/Launcher.java index d67c0c7048f5..a2e8c249b8eb 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/Launcher.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/Launcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import java.security.CodeSource; import java.security.ProtectionDomain; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import org.springframework.boot.loader.archive.Archive; @@ -39,6 +40,8 @@ */ public abstract class Launcher { + private static final String JAR_MODE_LAUNCHER = "org.springframework.boot.loader.jarmode.JarModeLauncher"; + /** * Launch the application. This method is the initial entry point that should be * called by a subclass {@code public static void main(String[] args)} method. @@ -46,9 +49,13 @@ public abstract class Launcher { * @throws Exception if the application fails to launch */ protected void launch(String[] args) throws Exception { - JarFile.registerUrlProtocolHandler(); - ClassLoader classLoader = createClassLoader(getClassPathArchives()); - launch(args, getMainClass(), classLoader); + if (!isExploded()) { + JarFile.registerUrlProtocolHandler(); + } + ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator()); + String jarMode = System.getProperty("jarmode"); + String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass(); + launch(args, launchClass, classLoader); } /** @@ -56,11 +63,25 @@ protected void launch(String[] args) throws Exception { * @param archives the archives * @return the classloader * @throws Exception if the classloader cannot be created + * @deprecated since 2.3.0 for removal in 2.5.0 in favor of + * {@link #createClassLoader(Iterator)} */ + @Deprecated protected ClassLoader createClassLoader(List archives) throws Exception { - List urls = new ArrayList<>(archives.size()); - for (Archive archive : archives) { - urls.add(archive.getUrl()); + return createClassLoader(archives.iterator()); + } + + /** + * Create a classloader for the specified archives. + * @param archives the archives + * @return the classloader + * @throws Exception if the classloader cannot be created + * @since 2.3.0 + */ + protected ClassLoader createClassLoader(Iterator archives) throws Exception { + List urls = new ArrayList<>(50); + while (archives.hasNext()) { + urls.add(archives.next().getUrl()); } return createClassLoader(urls.toArray(new URL[0])); } @@ -72,19 +93,19 @@ protected ClassLoader createClassLoader(List archives) throws Exception * @throws Exception if the classloader cannot be created */ protected ClassLoader createClassLoader(URL[] urls) throws Exception { - return new LaunchedURLClassLoader(urls, getClass().getClassLoader()); + return new LaunchedURLClassLoader(isExploded(), getArchive(), urls, getClass().getClassLoader()); } /** * Launch the application given the archive file and a fully configured classloader. * @param args the incoming arguments - * @param mainClass the main class to run + * @param launchClass the launch class to run * @param classLoader the classloader * @throws Exception if the launch fails */ - protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception { + protected void launch(String[] args, String launchClass, ClassLoader classLoader) throws Exception { Thread.currentThread().setContextClassLoader(classLoader); - createMainMethodRunner(mainClass, args, classLoader).run(); + createMainMethodRunner(launchClass, args, classLoader).run(); } /** @@ -109,8 +130,23 @@ protected MainMethodRunner createMainMethodRunner(String mainClass, String[] arg * Returns the archives that will be used to construct the class path. * @return the class path archives * @throws Exception if the class path archives cannot be obtained + * @since 2.3.0 + */ + protected Iterator getClassPathArchivesIterator() throws Exception { + return getClassPathArchives().iterator(); + } + + /** + * Returns the archives that will be used to construct the class path. + * @return the class path archives + * @throws Exception if the class path archives cannot be obtained + * @deprecated since 2.3.0 for removal in 2.5.0 in favor of implementing + * {@link #getClassPathArchivesIterator()}. */ - protected abstract List getClassPathArchives() throws Exception; + @Deprecated + protected List getClassPathArchives() throws Exception { + throw new IllegalStateException("Unexpected call to getClassPathArchives()"); + } protected final Archive createArchive() throws Exception { ProtectionDomain protectionDomain = getClass().getProtectionDomain(); @@ -127,4 +163,24 @@ protected final Archive createArchive() throws Exception { return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root)); } + /** + * Returns if the launcher is running in an exploded mode. If this method returns + * {@code true} then only regular JARs are supported and the additional URL and + * ClassLoader support infrastructure can be optimized. + * @return if the jar is exploded. + * @since 2.3.0 + */ + protected boolean isExploded() { + return false; + } + + /** + * Return the root archive. + * @return the root archive + * @since 2.3.1 + */ + protected Archive getArchive() { + return null; + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/MainMethodRunner.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/MainMethodRunner.java index 23edb4e72615..9b7a551a8b6d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/MainMethodRunner.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/MainMethodRunner.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,8 +43,9 @@ public MainMethodRunner(String mainClass, String[] args) { } public void run() throws Exception { - Class mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName); + Class mainClass = Class.forName(this.mainClassName, false, Thread.currentThread().getContextClassLoader()); Method mainMethod = mainClass.getDeclaredMethod("main", String[].class); + mainMethod.setAccessible(true); mainMethod.invoke(null, new Object[] { this.args }); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java index 75e48d507eb4..a84c01139ebb 100755 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ import java.net.URLDecoder; import java.util.ArrayList; import java.util.Collections; +import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; @@ -43,7 +44,6 @@ import org.springframework.boot.loader.archive.ExplodedArchive; import org.springframework.boot.loader.archive.JarFileArchive; import org.springframework.boot.loader.util.SystemPropertyUtils; -import org.springframework.util.Assert; /** * {@link Launcher} for archives with user-configured classpath and main class via a @@ -143,7 +143,9 @@ public class PropertiesLauncher extends Launcher { private final Properties properties = new Properties(); - private Archive parent; + private final Archive parent; + + private volatile ClassPathArchives classPathArchives; public PropertiesLauncher() { try { @@ -166,7 +168,7 @@ protected File getHomeDirectory() { } } - private void initializeProperties() throws Exception, IOException { + private void initializeProperties() throws Exception { List configs = new ArrayList<>(); if (getProperty(CONFIG_LOCATION) != null) { configs.add(getProperty(CONFIG_LOCATION)); @@ -194,7 +196,7 @@ private void initializeProperties() throws Exception, IOException { } } - private void loadResource(InputStream resource) throws IOException, Exception { + private void loadResource(InputStream resource) throws Exception { this.properties.load(resource); for (Object key : Collections.list(this.properties.propertyNames())) { String text = this.properties.getProperty((String) key); @@ -314,7 +316,7 @@ private List parsePathsProperty(String commaSeparatedPaths) { for (String path : commaSeparatedPaths.split(",")) { path = cleanupPath(path); // "" means the user wants root of archive but not current directory - path = "".equals(path) ? "/" : path; + path = (path == null || path.isEmpty()) ? "/" : path; paths.add(path); } if (paths.isEmpty()) { @@ -345,18 +347,19 @@ protected String getMainClass() throws Exception { } @Override - protected ClassLoader createClassLoader(List archives) throws Exception { - Set urls = new LinkedHashSet<>(archives.size()); - for (Archive archive : archives) { - urls.add(archive.getUrl()); - } - ClassLoader loader = new LaunchedURLClassLoader(urls.toArray(NO_URLS), getClass().getClassLoader()); - debug("Classpath: " + urls); + protected ClassLoader createClassLoader(Iterator archives) throws Exception { String customLoaderClassName = getProperty("loader.classLoader"); - if (customLoaderClassName != null) { - loader = wrapWithCustomClassLoader(loader, customLoaderClassName); - debug("Using custom class loader: " + customLoaderClassName); + if (customLoaderClassName == null) { + return super.createClassLoader(archives); } + Set urls = new LinkedHashSet<>(); + while (archives.hasNext()) { + urls.add(archives.next().getUrl()); + } + ClassLoader loader = new LaunchedURLClassLoader(urls.toArray(NO_URLS), getClass().getClassLoader()); + debug("Classpath for custom loader: " + urls); + loader = wrapWithCustomClassLoader(loader, customLoaderClassName); + debug("Using custom class loader: " + customLoaderClassName); return loader; } @@ -370,7 +373,9 @@ private ClassLoader wrapWithCustomClassLoader(ClassLoader parent, String classNa if (classLoader == null) { classLoader = newClassLoader(type, NO_PARAMS); } - Assert.notNull(classLoader, "Unable to create class loader for " + className); + if (classLoader == null) { + throw new IllegalArgumentException("Unable to create class loader for " + className); + } return classLoader; } @@ -447,125 +452,43 @@ private String getProperty(String propertyKey, String manifestKey, String defaul } @Override - protected List getClassPathArchives() throws Exception { - List lib = new ArrayList<>(); - for (String path : this.paths) { - for (Archive archive : getClassPathArchives(path)) { - if (archive instanceof ExplodedArchive) { - List nested = new ArrayList<>(archive.getNestedArchives(new ArchiveEntryFilter())); - nested.add(0, archive); - lib.addAll(nested); - } - else { - lib.add(archive); - } - } + protected Iterator getClassPathArchivesIterator() throws Exception { + ClassPathArchives classPathArchives = this.classPathArchives; + if (classPathArchives == null) { + classPathArchives = new ClassPathArchives(); + this.classPathArchives = classPathArchives; } - addNestedEntries(lib); - return lib; + return classPathArchives.iterator(); } - private List getClassPathArchives(String path) throws Exception { - String root = cleanupPath(handleUrl(path)); - List lib = new ArrayList<>(); - File file = new File(root); - if (!"/".equals(root)) { - if (!isAbsolutePath(root)) { - file = new File(this.home, root); - } - if (file.isDirectory()) { - debug("Adding classpath entries from " + file); - Archive archive = new ExplodedArchive(file, false); - lib.add(archive); - } - } - Archive archive = getArchive(file); - if (archive != null) { - debug("Adding classpath entries from archive " + archive.getUrl() + root); - lib.add(archive); - } - List nestedArchives = getNestedArchives(root); - if (nestedArchives != null) { - debug("Adding classpath entries from nested " + root); - lib.addAll(nestedArchives); - } - return lib; - } - - private boolean isAbsolutePath(String root) { - // Windows contains ":" others start with "/" - return root.contains(":") || root.startsWith("/"); + public static void main(String[] args) throws Exception { + PropertiesLauncher launcher = new PropertiesLauncher(); + args = launcher.getArgs(args); + launcher.launch(args); } - private Archive getArchive(File file) throws IOException { - if (isNestedArchivePath(file)) { + public static String toCamelCase(CharSequence string) { + if (string == null) { return null; } - String name = file.getName().toLowerCase(Locale.ENGLISH); - if (name.endsWith(".jar") || name.endsWith(".zip")) { - return new JarFileArchive(file); + StringBuilder builder = new StringBuilder(); + Matcher matcher = WORD_SEPARATOR.matcher(string); + int pos = 0; + while (matcher.find()) { + builder.append(capitalize(string.subSequence(pos, matcher.end()).toString())); + pos = matcher.end(); } - return null; - } - - private boolean isNestedArchivePath(File file) { - return file.getPath().contains(NESTED_ARCHIVE_SEPARATOR); + builder.append(capitalize(string.subSequence(pos, string.length()).toString())); + return builder.toString(); } - private List getNestedArchives(String path) throws Exception { - Archive parent = this.parent; - String root = path; - if (!root.equals("/") && root.startsWith("/") || parent.getUrl().equals(this.home.toURI().toURL())) { - // If home dir is same as parent archive, no need to add it twice. - return null; - } - int index = root.indexOf('!'); - if (index != -1) { - File file = new File(this.home, root.substring(0, index)); - if (root.startsWith("jar:file:")) { - file = new File(root.substring("jar:file:".length(), index)); - } - parent = new JarFileArchive(file); - root = root.substring(index + 1); - while (root.startsWith("/")) { - root = root.substring(1); - } - } - if (root.endsWith(".jar")) { - File file = new File(this.home, root); - if (file.exists()) { - parent = new JarFileArchive(file); - root = ""; - } - } - if (root.equals("/") || root.equals("./") || root.equals(".")) { - // The prefix for nested jars is actually empty if it's at the root - root = ""; - } - EntryFilter filter = new PrefixMatchingArchiveFilter(root); - List archives = new ArrayList<>(parent.getNestedArchives(filter)); - if (("".equals(root) || ".".equals(root)) && !path.endsWith(".jar") && parent != this.parent) { - // You can't find the root with an entry filter so it has to be added - // explicitly. But don't add the root of the parent archive. - archives.add(parent); - } - return archives; + private static String capitalize(String str) { + return Character.toUpperCase(str.charAt(0)) + str.substring(1); } - private void addNestedEntries(List lib) { - // The parent archive might have "BOOT-INF/lib/" and "BOOT-INF/classes/" - // directories, meaning we are running from an executable JAR. We add nested - // entries from there with low priority (i.e. at end). - try { - lib.addAll(this.parent.getNestedArchives((entry) -> { - if (entry.isDirectory()) { - return entry.getName().equals(JarLauncher.BOOT_INF_CLASSES); - } - return entry.getName().startsWith(JarLauncher.BOOT_INF_LIB); - })); - } - catch (IOException ex) { - // Ignore + private void debug(String message) { + if (Boolean.getBoolean(DEBUG)) { + System.out.println(message); } } @@ -591,35 +514,173 @@ private String cleanupPath(String path) { return path; } - public static void main(String[] args) throws Exception { - PropertiesLauncher launcher = new PropertiesLauncher(); - args = launcher.getArgs(args); - launcher.launch(args); + void close() throws Exception { + if (this.classPathArchives != null) { + this.classPathArchives.close(); + } + if (this.parent != null) { + this.parent.close(); + } } - public static String toCamelCase(CharSequence string) { - if (string == null) { + /** + * An iterable collection of the classpath archives. + */ + private class ClassPathArchives implements Iterable { + + private final List classPathArchives; + + private final List jarFileArchives = new ArrayList<>(); + + ClassPathArchives() throws Exception { + this.classPathArchives = new ArrayList<>(); + for (String path : PropertiesLauncher.this.paths) { + for (Archive archive : getClassPathArchives(path)) { + addClassPathArchive(archive); + } + } + addNestedEntries(); + } + + private void addClassPathArchive(Archive archive) throws IOException { + if (!(archive instanceof ExplodedArchive)) { + this.classPathArchives.add(archive); + return; + } + this.classPathArchives.add(archive); + this.classPathArchives.addAll(asList(archive.getNestedArchives(null, new ArchiveEntryFilter()))); + } + + private List getClassPathArchives(String path) throws Exception { + String root = cleanupPath(handleUrl(path)); + List lib = new ArrayList<>(); + File file = new File(root); + if (!"/".equals(root)) { + if (!isAbsolutePath(root)) { + file = new File(PropertiesLauncher.this.home, root); + } + if (file.isDirectory()) { + debug("Adding classpath entries from " + file); + Archive archive = new ExplodedArchive(file, false); + lib.add(archive); + } + } + Archive archive = getArchive(file); + if (archive != null) { + debug("Adding classpath entries from archive " + archive.getUrl() + root); + lib.add(archive); + } + List nestedArchives = getNestedArchives(root); + if (nestedArchives != null) { + debug("Adding classpath entries from nested " + root); + lib.addAll(nestedArchives); + } + return lib; + } + + private boolean isAbsolutePath(String root) { + // Windows contains ":" others start with "/" + return root.contains(":") || root.startsWith("/"); + } + + private Archive getArchive(File file) throws IOException { + if (isNestedArchivePath(file)) { + return null; + } + String name = file.getName().toLowerCase(Locale.ENGLISH); + if (name.endsWith(".jar") || name.endsWith(".zip")) { + return getJarFileArchive(file); + } return null; } - StringBuilder builder = new StringBuilder(); - Matcher matcher = WORD_SEPARATOR.matcher(string); - int pos = 0; - while (matcher.find()) { - builder.append(capitalize(string.subSequence(pos, matcher.end()).toString())); - pos = matcher.end(); + + private boolean isNestedArchivePath(File file) { + return file.getPath().contains(NESTED_ARCHIVE_SEPARATOR); } - builder.append(capitalize(string.subSequence(pos, string.length()).toString())); - return builder.toString(); - } - private static String capitalize(String str) { - return Character.toUpperCase(str.charAt(0)) + str.substring(1); - } + private List getNestedArchives(String path) throws Exception { + Archive parent = PropertiesLauncher.this.parent; + String root = path; + if (!root.equals("/") && root.startsWith("/") + || parent.getUrl().toURI().equals(PropertiesLauncher.this.home.toURI())) { + // If home dir is same as parent archive, no need to add it twice. + return null; + } + int index = root.indexOf('!'); + if (index != -1) { + File file = new File(PropertiesLauncher.this.home, root.substring(0, index)); + if (root.startsWith("jar:file:")) { + file = new File(root.substring("jar:file:".length(), index)); + } + parent = getJarFileArchive(file); + root = root.substring(index + 1); + while (root.startsWith("/")) { + root = root.substring(1); + } + } + if (root.endsWith(".jar")) { + File file = new File(PropertiesLauncher.this.home, root); + if (file.exists()) { + parent = getJarFileArchive(file); + root = ""; + } + } + if (root.equals("/") || root.equals("./") || root.equals(".")) { + // The prefix for nested jars is actually empty if it's at the root + root = ""; + } + EntryFilter filter = new PrefixMatchingArchiveFilter(root); + List archives = asList(parent.getNestedArchives(null, filter)); + if ((root == null || root.isEmpty() || ".".equals(root)) && !path.endsWith(".jar") + && parent != PropertiesLauncher.this.parent) { + // You can't find the root with an entry filter so it has to be added + // explicitly. But don't add the root of the parent archive. + archives.add(parent); + } + return archives; + } - private void debug(String message) { - if (Boolean.getBoolean(DEBUG)) { - System.out.println(message); + private void addNestedEntries() { + // The parent archive might have "BOOT-INF/lib/" and "BOOT-INF/classes/" + // directories, meaning we are running from an executable JAR. We add nested + // entries from there with low priority (i.e. at end). + try { + Iterator archives = PropertiesLauncher.this.parent.getNestedArchives(null, + JarLauncher.NESTED_ARCHIVE_ENTRY_FILTER); + while (archives.hasNext()) { + this.classPathArchives.add(archives.next()); + } + } + catch (IOException ex) { + // Ignore + } + } + + private List asList(Iterator iterator) { + List list = new ArrayList<>(); + while (iterator.hasNext()) { + list.add(iterator.next()); + } + return list; + } + + private JarFileArchive getJarFileArchive(File file) throws IOException { + JarFileArchive archive = new JarFileArchive(file); + this.jarFileArchives.add(archive); + return archive; + } + + @Override + public Iterator iterator() { + return this.classPathArchives.iterator(); } + + void close() throws IOException { + for (JarFileArchive archive : this.jarFileArchives) { + archive.close(); + } + } + } /** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java index 09af7e418291..6f7d18ff5de3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java @@ -17,6 +17,7 @@ package org.springframework.boot.loader; import org.springframework.boot.loader.archive.Archive; +import org.springframework.boot.loader.archive.Archive.Entry; /** * {@link Launcher} for WAR based archives. This launcher for standard WAR archives. @@ -29,14 +30,6 @@ */ public class WarLauncher extends ExecutableArchiveLauncher { - private static final String WEB_INF = "WEB-INF/"; - - private static final String WEB_INF_CLASSES = WEB_INF + "classes/"; - - private static final String WEB_INF_LIB = WEB_INF + "lib/"; - - private static final String WEB_INF_LIB_PROVIDED = WEB_INF + "lib-provided/"; - public WarLauncher() { } @@ -44,14 +37,22 @@ protected WarLauncher(Archive archive) { super(archive); } + @Override + protected boolean isPostProcessingClassPathArchives() { + return false; + } + + @Override + protected boolean isSearchCandidate(Entry entry) { + return entry.getName().startsWith("WEB-INF/"); + } + @Override public boolean isNestedArchive(Archive.Entry entry) { if (entry.isDirectory()) { - return entry.getName().equals(WEB_INF_CLASSES); - } - else { - return entry.getName().startsWith(WEB_INF_LIB) || entry.getName().startsWith(WEB_INF_LIB_PROVIDED); + return entry.getName().equals("WEB-INF/classes/"); } + return entry.getName().startsWith("WEB-INF/lib/") || entry.getName().startsWith("WEB-INF/lib-provided/"); } public static void main(String[] args) throws Exception { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java index 9646e9499a41..7b8d53e6bab1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,12 @@ import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.util.Iterator; import java.util.List; +import java.util.Objects; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.Consumer; import java.util.jar.Manifest; import org.springframework.boot.loader.Launcher; @@ -47,13 +52,86 @@ public interface Archive extends Iterable, AutoCloseable { */ Manifest getManifest() throws IOException; + /** + * Returns nested {@link Archive}s for entries that match the specified filters. + * @param searchFilter filter used to limit when additional sub-entry searching is + * required or {@code null} if all entries should be considered. + * @param includeFilter filter used to determine which entries should be included in + * the result or {@code null} if all entries should be included + * @return the nested archives + * @throws IOException on IO error + * @since 2.3.0 + */ + default Iterator getNestedArchives(EntryFilter searchFilter, EntryFilter includeFilter) + throws IOException { + EntryFilter combinedFilter = (entry) -> (searchFilter == null || searchFilter.matches(entry)) + && (includeFilter == null || includeFilter.matches(entry)); + List nestedArchives = getNestedArchives(combinedFilter); + return nestedArchives.iterator(); + } + /** * Returns nested {@link Archive}s for entries that match the specified filter. * @param filter the filter used to limit entries * @return nested archives * @throws IOException if nested archives cannot be read + * @deprecated since 2.3.0 for removal in 2.5.0 in favor of + * {@link #getNestedArchives(EntryFilter, EntryFilter)} + */ + @Deprecated + default List getNestedArchives(EntryFilter filter) throws IOException { + throw new IllegalStateException("Unexpected call to getNestedArchives(filter)"); + } + + /** + * Return a new iterator for the archive entries. + * @deprecated since 2.3.0 for removal in 2.5.0 in favor of using + * {@link org.springframework.boot.loader.jar.JarFile} to access entries and + * {@link #getNestedArchives(EntryFilter, EntryFilter)} for accessing nested archives. + * @see java.lang.Iterable#iterator() + */ + @Deprecated + @Override + Iterator iterator(); + + /** + * Performs the given action for each element of the {@code Iterable} until all + * elements have been processed or the action throws an exception. + * @deprecated since 2.3.0 for removal in 2.5.0 in favor of using + * {@link org.springframework.boot.loader.jar.JarFile} to access entries and + * {@link #getNestedArchives(EntryFilter, EntryFilter)} for accessing nested archives. + * @see Iterable#forEach */ - List getNestedArchives(EntryFilter filter) throws IOException; + @Deprecated + @Override + default void forEach(Consumer action) { + Objects.requireNonNull(action); + for (Entry entry : this) { + action.accept(entry); + } + } + + /** + * Creates a {@link Spliterator} over the elements described by this {@code Iterable}. + * @deprecated since 2.3.0 for removal in 2.5.0 in favor of using + * {@link org.springframework.boot.loader.jar.JarFile} to access entries and + * {@link #getNestedArchives(EntryFilter, EntryFilter)} for accessing nested archives. + * @see Iterable#spliterator + */ + @Deprecated + @Override + default Spliterator spliterator() { + return Spliterators.spliteratorUnknownSize(iterator(), 0); + } + + /** + * Return if the archive is exploded (already unpacked). + * @return if the archive is exploded + * @since 2.3.0 + */ + default boolean isExploded() { + return false; + } /** * Closes the {@code Archive}, releasing any open resources. @@ -87,6 +165,7 @@ interface Entry { /** * Strategy interface to filter {@link Entry Entries}. */ + @FunctionalInterface interface EntryFilter { /** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java index e1c860d0c231..170948b5d1b8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,8 +20,8 @@ import java.io.FileInputStream; import java.io.IOException; import java.net.MalformedURLException; +import java.net.URI; import java.net.URL; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; @@ -29,7 +29,6 @@ import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; -import java.util.List; import java.util.NoSuchElementException; import java.util.Set; import java.util.jar.Manifest; @@ -39,6 +38,7 @@ * * @author Phillip Webb * @author Andy Wilkinson + * @author Madhura Bhave * @since 1.0.0 */ public class ExplodedArchive implements Archive { @@ -55,7 +55,7 @@ public class ExplodedArchive implements Archive { /** * Create a new {@link ExplodedArchive} instance. - * @param root the root folder + * @param root the root directory */ public ExplodedArchive(File root) { this(root, true); @@ -63,15 +63,14 @@ public ExplodedArchive(File root) { /** * Create a new {@link ExplodedArchive} instance. - * @param root the root folder + * @param root the root directory * @param recursive if recursive searching should be used to locate the manifest. - * Defaults to {@code true}, folders with a large tree might want to set this to - * {@code - * false}. + * Defaults to {@code true}, directories with a large tree might want to set this to + * {@code false}. */ public ExplodedArchive(File root, boolean recursive) { if (!root.exists() || !root.isDirectory()) { - throw new IllegalArgumentException("Invalid source folder " + root); + throw new IllegalArgumentException("Invalid source directory " + root); } this.root = root; this.recursive = recursive; @@ -99,24 +98,24 @@ public Manifest getManifest() throws IOException { } @Override - public List getNestedArchives(EntryFilter filter) throws IOException { - List nestedArchives = new ArrayList<>(); - for (Entry entry : this) { - if (filter.matches(entry)) { - nestedArchives.add(getNestedArchive(entry)); - } - } - return Collections.unmodifiableList(nestedArchives); + public Iterator getNestedArchives(EntryFilter searchFilter, EntryFilter includeFilter) throws IOException { + return new ArchiveIterator(this.root, this.recursive, searchFilter, includeFilter); } @Override + @Deprecated public Iterator iterator() { - return new FileEntryIterator(this.root, this.recursive); + return new EntryIterator(this.root, this.recursive, null, null); } protected Archive getNestedArchive(Entry entry) throws IOException { File file = ((FileEntry) entry).getFile(); - return (file.isDirectory() ? new ExplodedArchive(file) : new JarFileArchive(file)); + return (file.isDirectory() ? new ExplodedArchive(file) : new SimpleJarFileArchive((FileEntry) entry)); + } + + @Override + public boolean isExploded() { + return true; } @Override @@ -132,21 +131,30 @@ public String toString() { /** * File based {@link Entry} {@link Iterator}. */ - private static class FileEntryIterator implements Iterator { + private abstract static class AbstractIterator implements Iterator { - private final Comparator entryComparator = new EntryComparator(); + private static final Comparator entryComparator = Comparator.comparing(File::getAbsolutePath); private final File root; private final boolean recursive; + private final EntryFilter searchFilter; + + private final EntryFilter includeFilter; + private final Deque> stack = new LinkedList<>(); - private File current; + private FileEntry current; + + private String rootUrl; - FileEntryIterator(File root, boolean recursive) { + AbstractIterator(File root, boolean recursive, EntryFilter searchFilter, EntryFilter includeFilter) { this.root = root; + this.rootUrl = this.root.toURI().getPath(); this.recursive = recursive; + this.searchFilter = searchFilter; + this.includeFilter = includeFilter; this.stack.add(listFiles(root)); this.current = poll(); } @@ -157,34 +165,28 @@ public boolean hasNext() { } @Override - public Entry next() { - if (this.current == null) { + public T next() { + FileEntry entry = this.current; + if (entry == null) { throw new NoSuchElementException(); } - File file = this.current; - if (file.isDirectory() && (this.recursive || file.getParentFile().equals(this.root))) { - this.stack.addFirst(listFiles(file)); - } this.current = poll(); - String name = file.toURI().getPath().substring(this.root.toURI().getPath().length()); - return new FileEntry(name, file); + return adapt(entry); } - private Iterator listFiles(File file) { - File[] files = file.listFiles(); - if (files == null) { - return Collections.emptyIterator(); - } - Arrays.sort(files, this.entryComparator); - return Arrays.asList(files).iterator(); - } - - private File poll() { + private FileEntry poll() { while (!this.stack.isEmpty()) { while (this.stack.peek().hasNext()) { File file = this.stack.peek().next(); - if (!SKIPPED_NAMES.contains(file.getName())) { - return file; + if (SKIPPED_NAMES.contains(file.getName())) { + continue; + } + FileEntry entry = getFileEntry(file); + if (isListable(entry)) { + this.stack.addFirst(listFiles(file)); + } + if (this.includeFilter == null || this.includeFilter.matches(entry)) { + return entry; } } this.stack.poll(); @@ -192,21 +194,64 @@ private File poll() { return null; } + private FileEntry getFileEntry(File file) { + URI uri = file.toURI(); + String name = uri.getPath().substring(this.rootUrl.length()); + try { + return new FileEntry(name, file, uri.toURL()); + } + catch (MalformedURLException ex) { + throw new IllegalStateException(ex); + } + } + + private boolean isListable(FileEntry entry) { + return entry.isDirectory() && (this.recursive || entry.getFile().getParentFile().equals(this.root)) + && (this.searchFilter == null || this.searchFilter.matches(entry)) + && (this.includeFilter == null || !this.includeFilter.matches(entry)); + } + + private Iterator listFiles(File file) { + File[] files = file.listFiles(); + if (files == null) { + return Collections.emptyIterator(); + } + Arrays.sort(files, entryComparator); + return Arrays.asList(files).iterator(); + } + @Override public void remove() { throw new UnsupportedOperationException("remove"); } - /** - * {@link Comparator} that orders {@link File} entries by their absolute paths. - */ - private static class EntryComparator implements Comparator { + protected abstract T adapt(FileEntry entry); - @Override - public int compare(File o1, File o2) { - return o1.getAbsolutePath().compareTo(o2.getAbsolutePath()); - } + } + + private static class EntryIterator extends AbstractIterator { + + EntryIterator(File root, boolean recursive, EntryFilter searchFilter, EntryFilter includeFilter) { + super(root, recursive, searchFilter, includeFilter); + } + @Override + protected Entry adapt(FileEntry entry) { + return entry; + } + + } + + private static class ArchiveIterator extends AbstractIterator { + + ArchiveIterator(File root, boolean recursive, EntryFilter searchFilter, EntryFilter includeFilter) { + super(root, recursive, searchFilter, includeFilter); + } + + @Override + protected Archive adapt(FileEntry entry) { + File file = entry.getFile(); + return (file.isDirectory() ? new ExplodedArchive(file) : new SimpleJarFileArchive(entry)); } } @@ -220,9 +265,12 @@ private static class FileEntry implements Entry { private final File file; - FileEntry(String name, File file) { + private final URL url; + + FileEntry(String name, File file, URL url) { this.name = name; this.file = file; + this.url = url; } File getFile() { @@ -239,6 +287,56 @@ public String getName() { return this.name; } + URL getUrl() { + return this.url; + } + + } + + /** + * {@link Archive} implementation backed by a simple JAR file that doesn't itself + * contain nested archives. + */ + private static class SimpleJarFileArchive implements Archive { + + private final URL url; + + SimpleJarFileArchive(FileEntry file) { + this.url = file.getUrl(); + } + + @Override + public URL getUrl() throws MalformedURLException { + return this.url; + } + + @Override + public Manifest getManifest() throws IOException { + return null; + } + + @Override + public Iterator getNestedArchives(EntryFilter searchFilter, EntryFilter includeFilter) + throws IOException { + return Collections.emptyIterator(); + } + + @Override + @Deprecated + public Iterator iterator() { + return Collections.emptyIterator(); + } + + @Override + public String toString() { + try { + return getUrl().toString(); + } + catch (Exception ex) { + return "jar archive"; + } + } + } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java index 998c0b76f41f..bab4125f5c31 100755 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,17 +17,21 @@ package org.springframework.boot.loader.archive; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.MalformedURLException; import java.net.URL; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Enumeration; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.EnumSet; import java.util.Iterator; -import java.util.List; import java.util.UUID; import java.util.jar.JarEntry; import java.util.jar.Manifest; @@ -47,11 +51,19 @@ public class JarFileArchive implements Archive { private static final int BUFFER_SIZE = 32 * 1024; + private static final FileAttribute[] NO_FILE_ATTRIBUTES = {}; + + private static final EnumSet DIRECTORY_PERMISSIONS = EnumSet.of(PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE); + + private static final EnumSet FILE_PERMISSIONS = EnumSet.of(PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE); + private final JarFile jarFile; private URL url; - private File tempUnpackFolder; + private Path tempUnpackDirectory; public JarFileArchive(File file) throws IOException { this(file, file.toURI().toURL()); @@ -80,19 +92,14 @@ public Manifest getManifest() throws IOException { } @Override - public List getNestedArchives(EntryFilter filter) throws IOException { - List nestedArchives = new ArrayList<>(); - for (Entry entry : this) { - if (filter.matches(entry)) { - nestedArchives.add(getNestedArchive(entry)); - } - } - return Collections.unmodifiableList(nestedArchives); + public Iterator getNestedArchives(EntryFilter searchFilter, EntryFilter includeFilter) throws IOException { + return new NestedArchiveIterator(this.jarFile.iterator(), searchFilter, includeFilter); } @Override + @Deprecated public Iterator iterator() { - return new EntryIterator(this.jarFile.entries()); + return new EntryIterator(this.jarFile.iterator(), null, null); } @Override @@ -119,36 +126,42 @@ private Archive getUnpackedNestedArchive(JarEntry jarEntry) throws IOException { if (name.lastIndexOf('/') != -1) { name = name.substring(name.lastIndexOf('/') + 1); } - File file = new File(getTempUnpackFolder(), name); - if (!file.exists() || file.length() != jarEntry.getSize()) { - unpack(jarEntry, file); + Path path = getTempUnpackDirectory().resolve(name); + if (!Files.exists(path) || Files.size(path) != jarEntry.getSize()) { + unpack(jarEntry, path); } - return new JarFileArchive(file, file.toURI().toURL()); + return new JarFileArchive(path.toFile(), path.toUri().toURL()); } - private File getTempUnpackFolder() { - if (this.tempUnpackFolder == null) { - File tempFolder = new File(System.getProperty("java.io.tmpdir")); - this.tempUnpackFolder = createUnpackFolder(tempFolder); + private Path getTempUnpackDirectory() { + if (this.tempUnpackDirectory == null) { + Path tempDirectory = Paths.get(System.getProperty("java.io.tmpdir")); + this.tempUnpackDirectory = createUnpackDirectory(tempDirectory); } - return this.tempUnpackFolder; + return this.tempUnpackDirectory; } - private File createUnpackFolder(File parent) { + private Path createUnpackDirectory(Path parent) { int attempts = 0; while (attempts++ < 1000) { - String fileName = new File(this.jarFile.getName()).getName(); - File unpackFolder = new File(parent, fileName + "-spring-boot-libs-" + UUID.randomUUID()); - if (unpackFolder.mkdirs()) { - return unpackFolder; + String fileName = Paths.get(this.jarFile.getName()).getFileName().toString(); + Path unpackDirectory = parent.resolve(fileName + "-spring-boot-libs-" + UUID.randomUUID()); + try { + createDirectory(unpackDirectory); + return unpackDirectory; + } + catch (IOException ex) { } } - throw new IllegalStateException("Failed to create unpack folder in directory '" + parent + "'"); + throw new IllegalStateException("Failed to create unpack directory in directory '" + parent + "'"); } - private void unpack(JarEntry entry, File file) throws IOException { + private void unpack(JarEntry entry, Path path) throws IOException { + createFile(path); + path.toFile().deleteOnExit(); try (InputStream inputStream = this.jarFile.getInputStream(entry); - OutputStream outputStream = new FileOutputStream(file)) { + OutputStream outputStream = Files.newOutputStream(path, StandardOpenOption.WRITE, + StandardOpenOption.TRUNCATE_EXISTING)) { byte[] buffer = new byte[BUFFER_SIZE]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { @@ -158,6 +171,21 @@ private void unpack(JarEntry entry, File file) throws IOException { } } + private void createDirectory(Path path) throws IOException { + Files.createDirectory(path, getFileAttributes(path.getFileSystem(), DIRECTORY_PERMISSIONS)); + } + + private void createFile(Path path) throws IOException { + Files.createFile(path, getFileAttributes(path.getFileSystem(), FILE_PERMISSIONS)); + } + + private FileAttribute[] getFileAttributes(FileSystem fileSystem, EnumSet ownerReadWrite) { + if (!fileSystem.supportedFileAttributeViews().contains("posix")) { + return NO_FILE_ATTRIBUTES; + } + return new FileAttribute[] { PosixFilePermissions.asFileAttribute(ownerReadWrite) }; + } + @Override public String toString() { try { @@ -169,29 +197,85 @@ public String toString() { } /** - * {@link Archive.Entry} iterator implementation backed by {@link JarEntry}. + * Abstract base class for iterator implementations. */ - private static class EntryIterator implements Iterator { + private abstract static class AbstractIterator implements Iterator { + + private final Iterator iterator; + + private final EntryFilter searchFilter; - private final Enumeration enumeration; + private final EntryFilter includeFilter; - EntryIterator(Enumeration enumeration) { - this.enumeration = enumeration; + private Entry current; + + AbstractIterator(Iterator iterator, EntryFilter searchFilter, EntryFilter includeFilter) { + this.iterator = iterator; + this.searchFilter = searchFilter; + this.includeFilter = includeFilter; + this.current = poll(); } @Override public boolean hasNext() { - return this.enumeration.hasMoreElements(); + return this.current != null; } @Override - public Entry next() { - return new JarFileEntry(this.enumeration.nextElement()); + public T next() { + T result = adapt(this.current); + this.current = poll(); + return result; + } + + private Entry poll() { + while (this.iterator.hasNext()) { + JarFileEntry candidate = new JarFileEntry(this.iterator.next()); + if ((this.searchFilter == null || this.searchFilter.matches(candidate)) + && (this.includeFilter == null || this.includeFilter.matches(candidate))) { + return candidate; + } + } + return null; + } + + protected abstract T adapt(Entry entry); + + } + + /** + * {@link Archive.Entry} iterator implementation backed by {@link JarEntry}. + */ + private static class EntryIterator extends AbstractIterator { + + EntryIterator(Iterator iterator, EntryFilter searchFilter, EntryFilter includeFilter) { + super(iterator, searchFilter, includeFilter); + } + + @Override + protected Entry adapt(Entry entry) { + return entry; + } + + } + + /** + * Nested {@link Archive} iterator implementation backed by {@link JarEntry}. + */ + private class NestedArchiveIterator extends AbstractIterator { + + NestedArchiveIterator(Iterator iterator, EntryFilter searchFilter, EntryFilter includeFilter) { + super(iterator, searchFilter, includeFilter); } @Override - public void remove() { - throw new UnsupportedOperationException("remove"); + protected Archive adapt(Entry entry) { + try { + return getNestedArchive(entry); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/package-info.java index 189fbb7204e8..ebaca84bb95d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/package-info.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ /** * Abstraction over logical Archives be they backed by a JAR file or unpacked into a - * folder. + * directory. * * @see org.springframework.boot.loader.archive.Archive */ diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AbstractJarFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AbstractJarFile.java new file mode 100644 index 000000000000..88726e373754 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AbstractJarFile.java @@ -0,0 +1,78 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.jar; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.Permission; + +/** + * Base class for extended variants of {@link java.util.jar.JarFile}. + * + * @author Phillip Webb + */ +abstract class AbstractJarFile extends java.util.jar.JarFile { + + /** + * Create a new {@link AbstractJarFile}. + * @param file the root jar file. + * @throws IOException on IO error + */ + AbstractJarFile(File file) throws IOException { + super(file); + } + + /** + * Return a URL that can be used to access this JAR file. NOTE: the specified URL + * cannot be serialized and or cloned. + * @return the URL + * @throws MalformedURLException if the URL is malformed + */ + abstract URL getUrl() throws MalformedURLException; + + /** + * Return the {@link JarFileType} of this instance. + * @return the jar file type + */ + abstract JarFileType getType(); + + /** + * Return the security permission for this JAR. + * @return the security permission. + */ + abstract Permission getPermission(); + + /** + * Return an {@link InputStream} for the entire jar contents. + * @return the contents input stream + * @throws IOException on IO error + */ + abstract InputStream getInputStream() throws IOException; + + /** + * The type of a {@link JarFile}. + */ + enum JarFileType { + + DIRECT, NESTED_DIRECTORY, NESTED_JAR + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java index 1d12c00eb066..32c274ba17ac 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,8 +34,6 @@ class CentralDirectoryEndRecord { private static final int MAXIMUM_COMMENT_LENGTH = 0xFFFF; - private static final int ZIP64_MAGICCOUNT = 0xFFFF; - private static final int MAXIMUM_SIZE = MINIMUM_SIZE + MAXIMUM_COMMENT_LENGTH; private static final int SIGNATURE = 0x06054b50; @@ -74,8 +72,9 @@ class CentralDirectoryEndRecord { } this.offset = this.block.length - this.size; } - int startOfCentralDirectoryEndRecord = (int) (data.getSize() - this.size); - this.zip64End = isZip64() ? new Zip64End(data, startOfCentralDirectoryEndRecord) : null; + long startOfCentralDirectoryEndRecord = data.getSize() - this.size; + Zip64Locator zip64Locator = Zip64Locator.find(data, startOfCentralDirectoryEndRecord); + this.zip64End = (zip64Locator != null) ? new Zip64End(data, zip64Locator) : null; } private byte[] createBlockFromEndOfData(RandomAccessData data, int size) throws IOException { @@ -92,10 +91,6 @@ private boolean isValid() { return this.size == MINIMUM_SIZE + commentLength; } - private boolean isZip64() { - return (int) Bytes.littleEndianValue(this.block, this.offset + 10, 2) == ZIP64_MAGICCOUNT; - } - /** * Returns the location in the data that the archive actually starts. For most files * the archive data will start at 0, however, it is possible to have prefixed bytes @@ -105,7 +100,8 @@ private boolean isZip64() { */ long getStartOfArchive(RandomAccessData data) { long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4); - long specifiedOffset = Bytes.littleEndianValue(this.block, this.offset + 16, 4); + long specifiedOffset = (this.zip64End != null) ? this.zip64End.centralDirectoryOffset + : Bytes.littleEndianValue(this.block, this.offset + 16, 4); long zip64EndSize = (this.zip64End != null) ? this.zip64End.getSize() : 0L; int zip64LocSize = (this.zip64End != null) ? Zip64Locator.ZIP64_LOCSIZE : 0; long actualOffset = data.getSize() - this.size - length - zip64EndSize - zip64LocSize; @@ -145,6 +141,10 @@ String getComment() { return comment.toString(); } + boolean isZip64() { + return this.zip64End != null; + } + /** * A Zip64 end of central directory record. * @@ -165,11 +165,7 @@ private static final class Zip64End { private final long centralDirectoryLength; - private int numberOfRecords; - - private Zip64End(RandomAccessData data, int centratDirectoryEndOffset) throws IOException { - this(data, new Zip64Locator(data, centratDirectoryEndOffset)); - } + private final int numberOfRecords; private Zip64End(RandomAccessData data, Zip64Locator locator) throws IOException { this.locator = locator; @@ -215,16 +211,18 @@ private int getNumberOfRecords() { */ private static final class Zip64Locator { + static final int SIGNATURE = 0x07064b50; + static final int ZIP64_LOCSIZE = 20; // locator size + static final int ZIP64_LOCOFF = 8; // offset of zip64 end private final long zip64EndOffset; - private final int offset; + private final long offset; - private Zip64Locator(RandomAccessData data, int centralDirectoryEndOffset) throws IOException { - this.offset = centralDirectoryEndOffset - ZIP64_LOCSIZE; - byte[] block = data.read(this.offset, ZIP64_LOCSIZE); + private Zip64Locator(long offset, byte[] block) throws IOException { + this.offset = offset; this.zip64EndOffset = Bytes.littleEndianValue(block, ZIP64_LOCOFF, 8); } @@ -244,6 +242,17 @@ private long getZip64EndOffset() { return this.zip64EndOffset; } + private static Zip64Locator find(RandomAccessData data, long centralDirectoryEndOffset) throws IOException { + long offset = centralDirectoryEndOffset - ZIP64_LOCSIZE; + if (offset >= 0) { + byte[] block = data.read(offset, ZIP64_LOCSIZE); + if (Bytes.littleEndianValue(block, 0, 4) == SIGNATURE) { + return new Zip64Locator(offset, block); + } + } + return null; + } + } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java index 4a3aa0b28c05..acc05a439f63 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,11 @@ package org.springframework.boot.loader.jar; import java.io.IOException; -import java.time.LocalDateTime; import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoField; +import java.time.temporal.ChronoUnit; +import java.time.temporal.ValueRange; import org.springframework.boot.loader.data.RandomAccessData; @@ -27,6 +30,7 @@ * * @author Phillip Webb * @author Andy Wilkinson + * @author Dmytro Nosan * @see Zip File Format */ @@ -63,15 +67,17 @@ final class CentralDirectoryFileHeader implements FileHeader { this.localHeaderOffset = localHeaderOffset; } - void load(byte[] data, int dataOffset, RandomAccessData variableData, int variableOffset, JarEntryFilter filter) + void load(byte[] data, int dataOffset, RandomAccessData variableData, long variableOffset, JarEntryFilter filter) throws IOException { // Load fixed part this.header = data; this.headerOffset = dataOffset; + long compressedSize = Bytes.littleEndianValue(data, dataOffset + 20, 4); + long uncompressedSize = Bytes.littleEndianValue(data, dataOffset + 24, 4); long nameLength = Bytes.littleEndianValue(data, dataOffset + 28, 2); long extraLength = Bytes.littleEndianValue(data, dataOffset + 30, 2); long commentLength = Bytes.littleEndianValue(data, dataOffset + 32, 2); - this.localHeaderOffset = Bytes.littleEndianValue(data, dataOffset + 42, 4); + long localHeaderOffset = Bytes.littleEndianValue(data, dataOffset + 42, 4); // Load variable part dataOffset += 46; if (variableData != null) { @@ -88,11 +94,37 @@ void load(byte[] data, int dataOffset, RandomAccessData variableData, int variab this.extra = new byte[(int) extraLength]; System.arraycopy(data, (int) (dataOffset + nameLength), this.extra, 0, this.extra.length); } + this.localHeaderOffset = getLocalHeaderOffset(compressedSize, uncompressedSize, localHeaderOffset, this.extra); if (commentLength > 0) { this.comment = new AsciiBytes(data, (int) (dataOffset + nameLength + extraLength), (int) commentLength); } } + private long getLocalHeaderOffset(long compressedSize, long uncompressedSize, long localHeaderOffset, byte[] extra) + throws IOException { + if (localHeaderOffset != 0xFFFFFFFFL) { + return localHeaderOffset; + } + int extraOffset = 0; + while (extraOffset < extra.length - 2) { + int id = (int) Bytes.littleEndianValue(extra, extraOffset, 2); + int length = (int) Bytes.littleEndianValue(extra, extraOffset, 2); + extraOffset += 4; + if (id == 1) { + int localHeaderExtraOffset = 0; + if (compressedSize == 0xFFFFFFFFL) { + localHeaderExtraOffset += 4; + } + if (uncompressedSize == 0xFFFFFFFFL) { + localHeaderExtraOffset += 4; + } + return Bytes.littleEndianValue(extra, extraOffset + localHeaderExtraOffset, 8); + } + extraOffset += length; + } + throw new IOException("Zip64 Extended Information Extra Field not found"); + } + AsciiBytes getName() { return this.name; } @@ -124,10 +156,14 @@ long getTime() { * @return the date and time as milliseconds since the epoch */ private long decodeMsDosFormatDateTime(long datetime) { - LocalDateTime localDateTime = LocalDateTime.of((int) (((datetime >> 25) & 0x7f) + 1980), - (int) ((datetime >> 21) & 0x0f), (int) ((datetime >> 16) & 0x1f), (int) ((datetime >> 11) & 0x1f), - (int) ((datetime >> 5) & 0x3f), (int) ((datetime << 1) & 0x3e)); - return localDateTime.toEpochSecond(ZoneId.systemDefault().getRules().getOffset(localDateTime)) * 1000; + int year = getChronoValue(((datetime >> 25) & 0x7f) + 1980, ChronoField.YEAR); + int month = getChronoValue((datetime >> 21) & 0x0f, ChronoField.MONTH_OF_YEAR); + int day = getChronoValue((datetime >> 16) & 0x1f, ChronoField.DAY_OF_MONTH); + int hour = getChronoValue((datetime >> 11) & 0x1f, ChronoField.HOUR_OF_DAY); + int minute = getChronoValue((datetime >> 5) & 0x3f, ChronoField.MINUTE_OF_HOUR); + int second = getChronoValue((datetime << 1) & 0x3e, ChronoField.SECOND_OF_MINUTE); + return ZonedDateTime.of(year, month, day, hour, minute, second, 0, ZoneId.systemDefault()).toInstant() + .truncatedTo(ChronoUnit.SECONDS).toEpochMilli(); } long getCrc() { @@ -168,7 +204,7 @@ public CentralDirectoryFileHeader clone() { return new CentralDirectoryFileHeader(header, 0, this.name, header, this.comment, this.localHeaderOffset); } - static CentralDirectoryFileHeader fromRandomAccessData(RandomAccessData data, int offset, JarEntryFilter filter) + static CentralDirectoryFileHeader fromRandomAccessData(RandomAccessData data, long offset, JarEntryFilter filter) throws IOException { CentralDirectoryFileHeader fileHeader = new CentralDirectoryFileHeader(); byte[] bytes = data.read(offset, 46); @@ -176,4 +212,9 @@ static CentralDirectoryFileHeader fromRandomAccessData(RandomAccessData data, in return fileHeader; } + private static int getChronoValue(long value, ChronoField field) { + ValueRange range = field.range(); + return Math.toIntExact(Math.min(Math.max(value, range.getMinimum()), range.getMaximum())); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java index 941302d99e5b..71a767853561 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -86,7 +86,7 @@ private void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData ce } } - private void visitFileHeader(int dataOffset, CentralDirectoryFileHeader fileHeader) { + private void visitFileHeader(long dataOffset, CentralDirectoryFileHeader fileHeader) { for (CentralDirectoryVisitor visitor : this.visitors) { visitor.visitFileHeader(fileHeader, dataOffset); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryVisitor.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryVisitor.java index 993986f742f0..d160cbf84772 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryVisitor.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,7 @@ interface CentralDirectoryVisitor { void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData); - void visitFileHeader(CentralDirectoryFileHeader fileHeader, int dataOffset); + void visitFileHeader(CentralDirectoryFileHeader fileHeader, long dataOffset); void visitEnd(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java index 6829eca65c16..fad95b607e44 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,16 +47,24 @@ public class Handler extends URLStreamHandler { private static final String FILE_PROTOCOL = "file:"; + private static final String TOMCAT_WARFILE_PROTOCOL = "war:file:"; + private static final String SEPARATOR = "!/"; + private static final Pattern SEPARATOR_PATTERN = Pattern.compile(SEPARATOR, Pattern.LITERAL); + private static final String CURRENT_DIR = "/./"; private static final Pattern CURRENT_DIR_PATTERN = Pattern.compile(CURRENT_DIR, Pattern.LITERAL); private static final String PARENT_DIR = "/../"; + private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs"; + private static final String[] FALLBACK_HANDLERS = { "sun.net.www.protocol.jar.Handler" }; + private static URL jarContextUrl; + private static SoftReference> rootFileCache; static { @@ -96,7 +104,9 @@ private boolean isUrlInJarFile(URL url, JarFile jarFile) throws MalformedURLExce private URLConnection openFallbackConnection(URL url, Exception reason) throws IOException { try { - return openConnection(getFallbackHandler(), url); + URLConnection connection = openFallbackTomcatConnection(url); + connection = (connection != null) ? connection : openFallbackContextConnection(url); + return (connection != null) ? connection : openFallbackHandlerConnection(url); } catch (Exception ex) { if (reason instanceof IOException) { @@ -111,16 +121,73 @@ private URLConnection openFallbackConnection(URL url, Exception reason) throws I } } - private void log(boolean warning, String message, Exception cause) { + /** + * Attempt to open a Tomcat formatted 'jar:war:file:...' URL. This method allows us to + * use our own nested JAR support to open the content rather than the logic in + * {@code sun.net.www.protocol.jar.URLJarFile} which will extract the nested jar to + * the temp folder to that its content can be accessed. + * @param url the URL to open + * @return a {@link URLConnection} or {@code null} + */ + private URLConnection openFallbackTomcatConnection(URL url) { + String file = url.getFile(); + if (isTomcatWarUrl(file)) { + file = file.substring(TOMCAT_WARFILE_PROTOCOL.length()); + file = file.replaceFirst("\\*/", "!/"); + try { + URLConnection connection = openConnection(new URL("jar:file:" + file)); + connection.getInputStream().close(); + return connection; + } + catch (IOException ex) { + } + } + return null; + } + + private boolean isTomcatWarUrl(String file) { + if (file.startsWith(TOMCAT_WARFILE_PROTOCOL) || !file.contains("*/")) { + try { + URLConnection connection = new URL(file).openConnection(); + if (connection.getClass().getName().startsWith("org.apache.catalina")) { + return true; + } + } + catch (Exception ex) { + } + } + return false; + } + + /** + * Attempt to open a fallback connection by using a context URL captured before the + * jar handler was replaced with our own version. Since this method doesn't use + * reflection it won't trigger "illegal reflective access operation has occurred" + * warnings on Java 13+. + * @param url the URL to open + * @return a {@link URLConnection} or {@code null} + */ + private URLConnection openFallbackContextConnection(URL url) { try { - Level level = warning ? Level.WARNING : Level.FINEST; - Logger.getLogger(getClass().getName()).log(level, message, cause); + if (jarContextUrl != null) { + return new URL(jarContextUrl, url.toExternalForm()).openConnection(); + } } catch (Exception ex) { - if (warning) { - System.err.println("WARNING: " + message); - } } + return null; + } + + /** + * Attempt to open a fallback connection by using reflection to access Java's default + * jar {@link URLStreamHandler}. + * @param url the URL to open + * @return the {@link URLConnection} + * @throws Exception if not connection could be opened + */ + private URLConnection openFallbackHandlerConnection(URL url) throws Exception { + URLStreamHandler fallbackHandler = getFallbackHandler(); + return new URL(null, url.toExternalForm(), fallbackHandler).openConnection(); } private URLStreamHandler getFallbackHandler() { @@ -130,7 +197,7 @@ private URLStreamHandler getFallbackHandler() { for (String handlerClassName : FALLBACK_HANDLERS) { try { Class handlerClass = Class.forName(handlerClassName); - this.fallbackHandler = (URLStreamHandler) handlerClass.newInstance(); + this.fallbackHandler = (URLStreamHandler) handlerClass.getDeclaredConstructor().newInstance(); return this.fallbackHandler; } catch (Exception ex) { @@ -140,8 +207,16 @@ private URLStreamHandler getFallbackHandler() { throw new IllegalStateException("Unable to find fallback handler"); } - private URLConnection openConnection(URLStreamHandler handler, URL url) throws Exception { - return new URL(null, url.toExternalForm(), handler).openConnection(); + private void log(boolean warning, String message, Exception cause) { + try { + Level level = warning ? Level.WARNING : Level.FINEST; + Logger.getLogger(getClass().getName()).log(level, message, cause); + } + catch (Exception ex) { + if (warning) { + System.err.println("WARNING: " + message); + } + } } @Override @@ -285,7 +360,7 @@ protected boolean sameFile(URL u1, URL u2) { } private String canonicalize(String path) { - return path.replace(SEPARATOR, "/"); + return SEPARATOR_PATTERN.matcher(path).replaceAll("/"); } public JarFile getRootJarFileFromUrl(URL url) throws IOException { @@ -331,6 +406,53 @@ static void addToRootFileCache(File sourceFile, JarFile jarFile) { cache.put(sourceFile, jarFile); } + /** + * If possible, capture a URL that is configured with the original jar handler so that + * we can use it as a fallback context later. We can only do this if we know that we + * can reset the handlers after. + */ + static void captureJarContextUrl() { + if (canResetCachedUrlHandlers()) { + String handlers = System.getProperty(PROTOCOL_HANDLER, ""); + try { + System.clearProperty(PROTOCOL_HANDLER); + try { + resetCachedUrlHandlers(); + jarContextUrl = new URL("jar:file:context.jar!/"); + URLConnection connection = jarContextUrl.openConnection(); + if (connection instanceof JarURLConnection) { + jarContextUrl = null; + } + } + catch (Exception ex) { + } + } + finally { + if (handlers == null) { + System.clearProperty(PROTOCOL_HANDLER); + } + else { + System.setProperty(PROTOCOL_HANDLER, handlers); + } + } + resetCachedUrlHandlers(); + } + } + + private static boolean canResetCachedUrlHandlers() { + try { + resetCachedUrlHandlers(); + return true; + } + catch (Error ex) { + return false; + } + } + + private static void resetCachedUrlHandlers() { + URL.setURLStreamHandlerFactory(null); + } + /** * Set if a generic static exception can be thrown when a URL cannot be connected. * This optimization is used during class loading to save creating lots of exceptions diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java index 1c69dbaa1e8b..f272e64d137c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,20 +32,21 @@ */ class JarEntry extends java.util.jar.JarEntry implements FileHeader { + private final int index; + private final AsciiBytes name; private final AsciiBytes headerName; - private Certificate[] certificates; - - private CodeSigner[] codeSigners; - private final JarFile jarFile; private long localHeaderOffset; - JarEntry(JarFile jarFile, CentralDirectoryFileHeader header, AsciiBytes nameAlias) { + private volatile JarEntryCertification certification; + + JarEntry(JarFile jarFile, int index, CentralDirectoryFileHeader header, AsciiBytes nameAlias) { super((nameAlias != null) ? nameAlias.toString() : header.getName().toString()); + this.index = index; this.name = (nameAlias != null) ? nameAlias : header.getName(); this.headerName = header.getName(); this.jarFile = jarFile; @@ -61,6 +62,10 @@ class JarEntry extends java.util.jar.JarEntry implements FileHeader { } } + int getIndex() { + return this.index; + } + AsciiBytes getAsciiBytesName() { return this.name; } @@ -87,23 +92,24 @@ public Attributes getAttributes() throws IOException { @Override public Certificate[] getCertificates() { - if (this.jarFile.isSigned() && this.certificates == null) { - this.jarFile.setupEntryCertificates(this); - } - return this.certificates; + return getCertification().getCertificates(); } @Override public CodeSigner[] getCodeSigners() { - if (this.jarFile.isSigned() && this.codeSigners == null) { - this.jarFile.setupEntryCertificates(this); - } - return this.codeSigners; + return getCertification().getCodeSigners(); } - void setCertificates(java.util.jar.JarEntry entry) { - this.certificates = entry.getCertificates(); - this.codeSigners = entry.getCodeSigners(); + private JarEntryCertification getCertification() { + if (!this.jarFile.isSigned()) { + return JarEntryCertification.NONE; + } + JarEntryCertification certification = this.certification; + if (certification == null) { + certification = this.jarFile.getCertification(this); + this.certification = certification; + } + return certification; } @Override diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryCertification.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryCertification.java new file mode 100644 index 000000000000..cbf66412e215 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryCertification.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.jar; + +import java.security.CodeSigner; +import java.security.cert.Certificate; + +/** + * {@link Certificate} and {@link CodeSigner} details for a {@link JarEntry} from a signed + * {@link JarFile}. + * + * @author Phillip Webb + */ +class JarEntryCertification { + + static final JarEntryCertification NONE = new JarEntryCertification(null, null); + + private final Certificate[] certificates; + + private final CodeSigner[] codeSigners; + + JarEntryCertification(Certificate[] certificates, CodeSigner[] codeSigners) { + this.certificates = certificates; + this.codeSigners = codeSigners; + } + + Certificate[] getCertificates() { + return (this.certificates != null) ? this.certificates.clone() : null; + } + + CodeSigner[] getCodeSigners() { + return (this.codeSigners != null) ? this.codeSigners.clone() : null; + } + + static JarEntryCertification from(java.util.jar.JarEntry certifiedEntry) { + Certificate[] certificates = (certifiedEntry != null) ? certifiedEntry.getCertificates() : null; + CodeSigner[] codeSigners = (certifiedEntry != null) ? certifiedEntry.getCodeSigners() : null; + if (certificates == null && codeSigners == null) { + return NONE; + } + return new JarEntryCertification(certificates, codeSigners); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java index 26d62978eb4c..81386386f931 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.boot.loader.jar; import java.io.File; +import java.io.FilePermission; import java.io.IOException; import java.io.InputStream; import java.lang.ref.SoftReference; @@ -24,11 +25,15 @@ import java.net.URL; import java.net.URLStreamHandler; import java.net.URLStreamHandlerFactory; +import java.security.Permission; import java.util.Enumeration; import java.util.Iterator; +import java.util.Spliterator; +import java.util.Spliterators; import java.util.function.Supplier; -import java.util.jar.JarInputStream; import java.util.jar.Manifest; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; import java.util.zip.ZipEntry; import org.springframework.boot.loader.data.RandomAccessData; @@ -48,7 +53,7 @@ * @author Andy Wilkinson * @since 1.0.0 */ -public class JarFile extends java.util.jar.JarFile { +public class JarFile extends AbstractJarFile implements Iterable { private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF"; @@ -60,6 +65,8 @@ public class JarFile extends java.util.jar.JarFile { private static final AsciiBytes SIGNATURE_FILE_EXTENSION = new AsciiBytes(".SF"); + private static final String READ_ACTION = "read"; + private final RandomAccessDataFile rootFile; private final String pathFromRoot; @@ -82,6 +89,10 @@ public class JarFile extends java.util.jar.JarFile { private String comment; + private volatile boolean closed; + + private volatile JarFileWrapper wrapper; + /** * Create a new {@link JarFile} backed by the specified file. * @param file the root jar file @@ -117,6 +128,9 @@ private JarFile(RandomAccessDataFile rootFile, String pathFromRoot, RandomAccess private JarFile(RandomAccessDataFile rootFile, String pathFromRoot, RandomAccessData data, JarEntryFilter filter, JarFileType type, Supplier manifestSupplier) throws IOException { super(rootFile.getFile()); + if (System.getSecurityManager() == null) { + super.close(); + } this.rootFile = rootFile; this.pathFromRoot = pathFromRoot; CentralDirectoryParser parser = new CentralDirectoryParser(); @@ -127,7 +141,12 @@ private JarFile(RandomAccessDataFile rootFile, String pathFromRoot, RandomAccess this.data = parser.parse(data, filter == null); } catch (RuntimeException ex) { - close(); + try { + this.rootFile.close(); + super.close(); + } + catch (IOException ioex) { + } throw ex; } this.manifestSupplier = (manifestSupplier != null) ? manifestSupplier : () -> { @@ -152,7 +171,7 @@ public void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData cen } @Override - public void visitFileHeader(CentralDirectoryFileHeader fileHeader, int dataOffset) { + public void visitFileHeader(CentralDirectoryFileHeader fileHeader, long dataOffset) { AsciiBytes name = fileHeader.getName(); if (name.startsWith(META_INF) && name.endsWith(SIGNATURE_FILE_EXTENSION)) { JarFile.this.signed = true; @@ -166,6 +185,20 @@ public void visitEnd() { }; } + JarFileWrapper getWrapper() throws IOException { + JarFileWrapper wrapper = this.wrapper; + if (wrapper == null) { + wrapper = new JarFileWrapper(this); + this.wrapper = wrapper; + } + return wrapper; + } + + @Override + Permission getPermission() { + return new FilePermission(this.rootFile.getFile().getPath(), READ_ACTION); + } + protected final RandomAccessDataFile getRootJarFile() { return this.rootFile; } @@ -191,20 +224,25 @@ public Manifest getManifest() throws IOException { @Override public Enumeration entries() { - final Iterator iterator = this.entries.iterator(); - return new Enumeration() { - - @Override - public boolean hasMoreElements() { - return iterator.hasNext(); - } + return new JarEntryEnumeration(this.entries.iterator()); + } - @Override - public java.util.jar.JarEntry nextElement() { - return iterator.next(); - } + @Override + public Stream stream() { + Spliterator spliterator = Spliterators.spliterator(iterator(), size(), + Spliterator.ORDERED | Spliterator.DISTINCT | Spliterator.IMMUTABLE | Spliterator.NONNULL); + return StreamSupport.stream(spliterator, false); + } - }; + /** + * Return an iterator for the contained entries. + * @since 2.3.0 + * @see java.lang.Iterable#iterator() + */ + @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) + public Iterator iterator() { + return (Iterator) this.entries.iterator(this::ensureOpen); } public JarEntry getJarEntry(CharSequence name) { @@ -222,11 +260,18 @@ public boolean containsEntry(String name) { @Override public ZipEntry getEntry(String name) { + ensureOpen(); return this.entries.getEntry(name); } + @Override + InputStream getInputStream() throws IOException { + return this.data.getInputStream(); + } + @Override public synchronized InputStream getInputStream(ZipEntry entry) throws IOException { + ensureOpen(); if (entry instanceof JarEntry) { return this.entries.getInputStream((JarEntry) entry); } @@ -295,20 +340,36 @@ private JarFile createJarFileFromFileEntry(JarEntry entry) throws IOException { @Override public String getComment() { + ensureOpen(); return this.comment; } @Override public int size() { + ensureOpen(); return this.entries.getSize(); } @Override public void close() throws IOException { + if (this.closed) { + return; + } super.close(); if (this.type == JarFileType.DIRECT) { this.rootFile.close(); } + this.closed = true; + } + + private void ensureOpen() { + if (this.closed) { + throw new IllegalStateException("zip file closed"); + } + } + + boolean isClosed() { + return this.closed; } String getUrlString() throws MalformedURLException { @@ -318,18 +379,12 @@ String getUrlString() throws MalformedURLException { return this.urlString; } - /** - * Return a URL that can be used to access this JAR file. NOTE: the specified URL - * cannot be serialized and or cloned. - * @return the URL - * @throws MalformedURLException if the URL is malformed - */ + @Override public URL getUrl() throws MalformedURLException { if (this.url == null) { - Handler handler = new Handler(this); String file = this.rootFile.getFile().toURI() + this.pathFromRoot + "!/"; file = file.replace("file:////", "file://"); // Fix UNC paths - this.url = new URL("jar", "", -1, file, handler); + this.url = new URL("jar", "", -1, file, new Handler(this)); } return this.url; } @@ -348,33 +403,15 @@ boolean isSigned() { return this.signed; } - void setupEntryCertificates(JarEntry entry) { - // Fallback to JarInputStream to obtain certificates, not fast but hopefully not - // happening that often. + JarEntryCertification getCertification(JarEntry entry) { try { - try (JarInputStream inputStream = new JarInputStream(getData().getInputStream())) { - java.util.jar.JarEntry certEntry = inputStream.getNextJarEntry(); - while (certEntry != null) { - inputStream.closeEntry(); - if (entry.getName().equals(certEntry.getName())) { - setCertificates(entry, certEntry); - } - setCertificates(getJarEntry(certEntry.getName()), certEntry); - certEntry = inputStream.getNextJarEntry(); - } - } + return this.entries.getCertification(entry); } catch (IOException ex) { throw new IllegalStateException(ex); } } - private void setCertificates(JarEntry entry, java.util.jar.JarEntry certEntry) { - if (entry != null) { - entry.setCertificates(certEntry); - } - } - public void clearCache() { this.entries.clearCache(); } @@ -383,6 +420,7 @@ protected String getPathFromRoot() { return this.pathFromRoot; } + @Override JarFileType getType() { return this.type; } @@ -392,9 +430,10 @@ JarFileType getType() { * {@link URLStreamHandler} will be located to deal with jar URLs. */ public static void registerUrlProtocolHandler() { + Handler.captureJarContextUrl(); String handlers = System.getProperty(PROTOCOL_HANDLER, ""); System.setProperty(PROTOCOL_HANDLER, - ("".equals(handlers) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE)); + ((handlers == null || handlers.isEmpty()) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE)); resetCachedUrlHandlers(); } @@ -413,11 +452,25 @@ private static void resetCachedUrlHandlers() { } /** - * The type of a {@link JarFile}. + * An {@link Enumeration} on {@linkplain java.util.jar.JarEntry jar entries}. */ - enum JarFileType { + private static class JarEntryEnumeration implements Enumeration { - DIRECT, NESTED_DIRECTORY, NESTED_JAR + private final Iterator iterator; + + JarEntryEnumeration(Iterator iterator) { + this.iterator = iterator; + } + + @Override + public boolean hasMoreElements() { + return this.iterator.hasNext(); + } + + @Override + public java.util.jar.JarEntry nextElement() { + return this.iterator.next(); + } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java index c69b7f1dd324..31c65fe76d2a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import java.util.NoSuchElementException; import java.util.jar.Attributes; import java.util.jar.Attributes.Name; +import java.util.jar.JarInputStream; import java.util.jar.Manifest; import java.util.zip.ZipEntry; @@ -33,9 +34,9 @@ /** * Provides access to entries from a {@link JarFile}. In order to reduce memory - * consumption entry details are stored using int arrays. The {@code hashCodes} array - * stores the hash code of the entry name, the {@code centralDirectoryOffsets} provides - * the offset to the central directory record and {@code positions} provides the original + * consumption entry details are stored using arrays. The {@code hashCodes} array stores + * the hash code of the entry name, the {@code centralDirectoryOffsets} provides the + * offset to the central directory record and {@code positions} provides the original * order position of the entry. The arrays are stored in hashCode order so that a binary * search can be used to find a name. *

    @@ -47,6 +48,9 @@ */ class JarFileEntries implements CentralDirectoryVisitor, Iterable { + private static final Runnable NO_VALIDATION = () -> { + }; + private static final String META_INF_PREFIX = "META-INF/"; private static final Name MULTI_RELEASE = new Name("Multi-Release"); @@ -85,20 +89,19 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable { private int[] hashCodes; - private int[] centralDirectoryOffsets; + private Offsets centralDirectoryOffsets; private int[] positions; private Boolean multiReleaseJar; + private JarEntryCertification[] certifications; + private final Map entriesCache = Collections .synchronizedMap(new LinkedHashMap(16, 0.75f, true) { @Override protected boolean removeEldestEntry(Map.Entry eldest) { - if (JarFileEntries.this.jarFile.isSigned()) { - return false; - } return size() >= ENTRY_CACHE_SIZE; } @@ -117,21 +120,21 @@ public void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData cen int maxSize = endRecord.getNumberOfRecords(); this.centralDirectoryData = centralDirectoryData; this.hashCodes = new int[maxSize]; - this.centralDirectoryOffsets = new int[maxSize]; + this.centralDirectoryOffsets = Offsets.from(endRecord); this.positions = new int[maxSize]; } @Override - public void visitFileHeader(CentralDirectoryFileHeader fileHeader, int dataOffset) { + public void visitFileHeader(CentralDirectoryFileHeader fileHeader, long dataOffset) { AsciiBytes name = applyFilter(fileHeader.getName()); if (name != null) { add(name, dataOffset); } } - private void add(AsciiBytes name, int dataOffset) { + private void add(AsciiBytes name, long dataOffset) { this.hashCodes[this.size] = name.hashCode(); - this.centralDirectoryOffsets[this.size] = dataOffset; + this.centralDirectoryOffsets.set(this.size, dataOffset); this.positions[this.size] = this.size; this.size++; } @@ -180,19 +183,17 @@ private void sort(int left, int right) { private void swap(int i, int j) { swap(this.hashCodes, i, j); - swap(this.centralDirectoryOffsets, i, j); + this.centralDirectoryOffsets.swap(i, j); swap(this.positions, i, j); } - private void swap(int[] array, int i, int j) { - int temp = array[i]; - array[i] = array[j]; - array[j] = temp; - } - @Override public Iterator iterator() { - return new EntryIterator(); + return new EntryIterator(NO_VALIDATION); + } + + Iterator iterator(Runnable validator) { + return new EntryIterator(validator); } boolean containsEntry(CharSequence name) { @@ -309,11 +310,12 @@ private T getEntry(int hashCode, CharSequence name, char @SuppressWarnings("unchecked") private T getEntry(int index, Class type, boolean cacheEntry, AsciiBytes nameAlias) { try { + long offset = this.centralDirectoryOffsets.get(index); FileHeader cached = this.entriesCache.get(index); - FileHeader entry = (cached != null) ? cached : CentralDirectoryFileHeader - .fromRandomAccessData(this.centralDirectoryData, this.centralDirectoryOffsets[index], this.filter); + FileHeader entry = (cached != null) ? cached + : CentralDirectoryFileHeader.fromRandomAccessData(this.centralDirectoryData, offset, this.filter); if (CentralDirectoryFileHeader.class.equals(entry.getClass()) && type.equals(JarEntry.class)) { - entry = new JarEntry(this.jarFile, (CentralDirectoryFileHeader) entry, nameAlias); + entry = new JarEntry(this.jarFile, index, (CentralDirectoryFileHeader) entry, nameAlias); } if (cacheEntry && cached != entry) { this.entriesCache.put(index, entry); @@ -344,20 +346,77 @@ private AsciiBytes applyFilter(AsciiBytes name) { return (this.filter != null) ? this.filter.apply(name) : name; } + JarEntryCertification getCertification(JarEntry entry) throws IOException { + JarEntryCertification[] certifications = this.certifications; + if (certifications == null) { + certifications = new JarEntryCertification[this.size]; + // We fallback to use JarInputStream to obtain the certs. This isn't that + // fast, but hopefully doesn't happen too often. + try (JarInputStream certifiedJarStream = new JarInputStream(this.jarFile.getData().getInputStream())) { + java.util.jar.JarEntry certifiedEntry = null; + while ((certifiedEntry = certifiedJarStream.getNextJarEntry()) != null) { + // Entry must be closed to trigger a read and set entry certificates + certifiedJarStream.closeEntry(); + int index = getEntryIndex(certifiedEntry.getName()); + if (index != -1) { + certifications[index] = JarEntryCertification.from(certifiedEntry); + } + } + } + this.certifications = certifications; + } + JarEntryCertification certification = certifications[entry.getIndex()]; + return (certification != null) ? certification : JarEntryCertification.NONE; + } + + private int getEntryIndex(CharSequence name) { + int hashCode = AsciiBytes.hashCode(name); + int index = getFirstIndex(hashCode); + while (index >= 0 && index < this.size && this.hashCodes[index] == hashCode) { + FileHeader candidate = getEntry(index, FileHeader.class, false, null); + if (candidate.hasName(name, NO_SUFFIX)) { + return index; + } + index++; + } + return -1; + } + + private static void swap(int[] array, int i, int j) { + int temp = array[i]; + array[i] = array[j]; + array[j] = temp; + } + + private static void swap(long[] array, int i, int j) { + long temp = array[i]; + array[i] = array[j]; + array[j] = temp; + } + /** * Iterator for contained entries. */ - private class EntryIterator implements Iterator { + private final class EntryIterator implements Iterator { + + private final Runnable validator; private int index = 0; + private EntryIterator(Runnable validator) { + this.validator = validator; + validator.run(); + } + @Override public boolean hasNext() { + this.validator.run(); return this.index < JarFileEntries.this.size; } @Override public JarEntry next() { + this.validator.run(); if (!hasNext()) { throw new NoSuchElementException(); } @@ -368,4 +427,80 @@ public JarEntry next() { } + /** + * Interface to manage offsets to central directory records. Regular zip files are + * backed by an {@code int[]} based implementation, Zip64 files are backed by a + * {@code long[]} and will consume more memory. + */ + private interface Offsets { + + void set(int index, long value); + + long get(int index); + + void swap(int i, int j); + + static Offsets from(CentralDirectoryEndRecord endRecord) { + int size = endRecord.getNumberOfRecords(); + return endRecord.isZip64() ? new Zip64Offsets(size) : new ZipOffsets(size); + } + + } + + /** + * {@link Offsets} implementation for regular zip files. + */ + private static final class ZipOffsets implements Offsets { + + private final int[] offsets; + + private ZipOffsets(int size) { + this.offsets = new int[size]; + } + + @Override + public void swap(int i, int j) { + JarFileEntries.swap(this.offsets, i, j); + } + + @Override + public void set(int index, long value) { + this.offsets[index] = (int) value; + } + + @Override + public long get(int index) { + return this.offsets[index]; + } + + } + + /** + * {@link Offsets} implementation for zip64 files. + */ + private static final class Zip64Offsets implements Offsets { + + private final long[] offsets; + + private Zip64Offsets(int size) { + this.offsets = new long[size]; + } + + @Override + public void swap(int i, int j) { + JarFileEntries.swap(this.offsets, i, j); + } + + @Override + public void set(int index, long value) { + this.offsets[index] = value; + } + + @Override + public long get(int index) { + return this.offsets[index]; + } + + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileWrapper.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileWrapper.java new file mode 100644 index 000000000000..ebc897985553 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileWrapper.java @@ -0,0 +1,128 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.jar; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.Permission; +import java.util.Enumeration; +import java.util.jar.JarEntry; +import java.util.jar.Manifest; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; + +/** + * A wrapper used to create a copy of a {@link JarFile} so that it can be safely closed + * without closing the original. + * + * @author Phillip Webb + */ +class JarFileWrapper extends AbstractJarFile { + + private final JarFile parent; + + JarFileWrapper(JarFile parent) throws IOException { + super(parent.getRootJarFile().getFile()); + this.parent = parent; + if (System.getSecurityManager() == null) { + super.close(); + } + } + + @Override + URL getUrl() throws MalformedURLException { + return this.parent.getUrl(); + } + + @Override + JarFileType getType() { + return this.parent.getType(); + } + + @Override + Permission getPermission() { + return this.parent.getPermission(); + } + + @Override + public Manifest getManifest() throws IOException { + return this.parent.getManifest(); + } + + @Override + public Enumeration entries() { + return this.parent.entries(); + } + + @Override + public Stream stream() { + return this.parent.stream(); + } + + @Override + public JarEntry getJarEntry(String name) { + return this.parent.getJarEntry(name); + } + + @Override + public ZipEntry getEntry(String name) { + return this.parent.getEntry(name); + } + + @Override + InputStream getInputStream() throws IOException { + return this.parent.getInputStream(); + } + + @Override + public synchronized InputStream getInputStream(ZipEntry ze) throws IOException { + return this.parent.getInputStream(ze); + } + + @Override + public String getComment() { + return this.parent.getComment(); + } + + @Override + public int size() { + return this.parent.size(); + } + + @Override + public String toString() { + return this.parent.toString(); + } + + @Override + public String getName() { + return this.parent.getName(); + } + + static JarFile unwrap(java.util.jar.JarFile jarFile) { + if (jarFile instanceof JarFile) { + return (JarFile) jarFile; + } + if (jarFile instanceof JarFileWrapper) { + return unwrap(((JarFileWrapper) jarFile).parent); + } + throw new IllegalStateException("Not a JarFile or Wrapper"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java index a995697d4eb9..5e3a4f7a80c8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,8 +18,6 @@ import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; -import java.io.FilePermission; -import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; @@ -69,11 +67,9 @@ protected URLConnection openConnection(URL u) throws IOException { private static final JarEntryName EMPTY_JAR_ENTRY_NAME = new JarEntryName(new StringSequence("")); - private static final String READ_ACTION = "read"; - private static final JarURLConnection NOT_FOUND_CONNECTION = JarURLConnection.notFound(); - private final JarFile jarFile; + private final AbstractJarFile jarFile; private Permission permission; @@ -81,18 +77,14 @@ protected URLConnection openConnection(URL u) throws IOException { private final JarEntryName jarEntryName; - private final CloseAction closeAction; - - private JarEntry jarEntry; + private java.util.jar.JarEntry jarEntry; - private JarURLConnection(URL url, JarFile jarFile, JarEntryName jarEntryName, CloseAction closeAction) - throws IOException { + private JarURLConnection(URL url, AbstractJarFile jarFile, JarEntryName jarEntryName) throws IOException { // What we pass to super is ultimately ignored super(EMPTY_JAR_URL); this.url = url; this.jarFile = jarFile; this.jarEntryName = jarEntryName; - this.closeAction = closeAction; } @Override @@ -110,7 +102,7 @@ public void connect() throws IOException { } @Override - public JarFile getJarFile() throws IOException { + public java.util.jar.JarFile getJarFile() throws IOException { connect(); return this.jarFile; } @@ -143,7 +135,7 @@ private URL buildJarFileUrl() { } @Override - public JarEntry getJarEntry() throws IOException { + public java.util.jar.JarEntry getJarEntry() throws IOException { if (this.jarEntryName == null || this.jarEntryName.isEmpty()) { return null; } @@ -168,25 +160,15 @@ public InputStream getInputStream() throws IOException { throw new IOException("no entry name specified"); } connect(); - InputStream inputStream = (this.jarEntryName.isEmpty() ? this.jarFile.getData().getInputStream() + InputStream inputStream = (this.jarEntryName.isEmpty() ? this.jarFile.getInputStream() : this.jarFile.getInputStream(this.jarEntry)); if (inputStream == null) { throwFileNotFound(this.jarEntryName, this.jarFile); } - return new FilterInputStream(inputStream) { - - @Override - public void close() throws IOException { - super.close(); - if (JarURLConnection.this.closeAction != null) { - JarURLConnection.this.closeAction.perform(); - } - } - - }; + return inputStream; } - private void throwFileNotFound(Object entry, JarFile jarFile) throws FileNotFoundException { + private void throwFileNotFound(Object entry, AbstractJarFile jarFile) throws FileNotFoundException { if (Boolean.TRUE.equals(useFastExceptions.get())) { throw FILE_NOT_FOUND_EXCEPTION; } @@ -211,7 +193,7 @@ public long getContentLengthLong() { if (this.jarEntryName.isEmpty()) { return this.jarFile.size(); } - JarEntry entry = getJarEntry(); + java.util.jar.JarEntry entry = getJarEntry(); return (entry != null) ? (int) entry.getSize() : -1; } catch (IOException ex) { @@ -236,7 +218,7 @@ public Permission getPermission() throws IOException { throw FILE_NOT_FOUND_EXCEPTION; } if (this.permission == null) { - this.permission = new FilePermission(this.jarFile.getRootJarFile().getFile().getPath(), READ_ACTION); + this.permission = this.jarFile.getPermission(); } return this.permission; } @@ -247,7 +229,7 @@ public long getLastModified() { return 0; } try { - JarEntry entry = getJarEntry(); + java.util.jar.JarEntry entry = getJarEntry(); return (entry != null) ? entry.getTime() : 0; } catch (IOException ex) { @@ -264,30 +246,24 @@ static JarURLConnection get(URL url, JarFile jarFile) throws IOException { int index = indexOfRootSpec(spec, jarFile.getPathFromRoot()); if (index == -1) { return (Boolean.TRUE.equals(useFastExceptions.get()) ? NOT_FOUND_CONNECTION - : new JarURLConnection(url, null, EMPTY_JAR_ENTRY_NAME, null)); + : new JarURLConnection(url, null, EMPTY_JAR_ENTRY_NAME)); } int separator; - JarFile connectionJarFile = jarFile; while ((separator = spec.indexOf(SEPARATOR, index)) > 0) { JarEntryName entryName = JarEntryName.get(spec.subSequence(index, separator)); JarEntry jarEntry = jarFile.getJarEntry(entryName.toCharSequence()); if (jarEntry == null) { - return JarURLConnection.notFound(connectionJarFile, entryName, - (connectionJarFile != jarFile) ? connectionJarFile::close : null); + return JarURLConnection.notFound(jarFile, entryName); } - connectionJarFile = connectionJarFile.getNestedJarFile(jarEntry); + jarFile = jarFile.getNestedJarFile(jarEntry); index = separator + SEPARATOR.length(); } JarEntryName jarEntryName = JarEntryName.get(spec, index); if (Boolean.TRUE.equals(useFastExceptions.get()) && !jarEntryName.isEmpty() - && !connectionJarFile.containsEntry(jarEntryName.toString())) { - if (connectionJarFile != jarFile) { - connectionJarFile.close(); - } + && !jarFile.containsEntry(jarEntryName.toString())) { return NOT_FOUND_CONNECTION; } - return new JarURLConnection(url, connectionJarFile, jarEntryName, - (connectionJarFile != jarFile) ? connectionJarFile::close : null); + return new JarURLConnection(url, jarFile.getWrapper(), jarEntryName); } private static int indexOfRootSpec(StringSequence file, String pathFromRoot) { @@ -300,22 +276,18 @@ private static int indexOfRootSpec(StringSequence file, String pathFromRoot) { private static JarURLConnection notFound() { try { - return notFound(null, null, null); + return notFound(null, null); } catch (IOException ex) { throw new IllegalStateException(ex); } } - private static JarURLConnection notFound(JarFile jarFile, JarEntryName jarEntryName, CloseAction closeAction) - throws IOException { + private static JarURLConnection notFound(JarFile jarFile, JarEntryName jarEntryName) throws IOException { if (Boolean.TRUE.equals(useFastExceptions.get())) { - if (closeAction != null) { - closeAction.perform(); - } return NOT_FOUND_CONNECTION; } - return new JarURLConnection(null, jarFile, jarEntryName, closeAction); + return new JarURLConnection(null, jarFile, jarEntryName); } /** @@ -418,15 +390,4 @@ static JarEntryName get(StringSequence spec, int beginIndex) { } - /** - * An action to be taken when the connection is being "closed" and its underlying - * resources are no longer needed. - */ - @FunctionalInterface - private interface CloseAction { - - void perform() throws IOException; - - } - } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/StringSequence.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/StringSequence.java index a1129e7173d1..b77492e562da 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/StringSequence.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/StringSequence.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -72,7 +72,11 @@ public StringSequence subSequence(int start, int end) { return new StringSequence(this.source, subSequenceStart, subSequenceEnd); } - boolean isEmpty() { + /** + * Returns {@code true} if the sequence is empty. Public to be compatible with JDK 15. + * @return {@code true} if {@link #length()} is {@code 0}, otherwise {@code false} + */ + public boolean isEmpty() { return length() == 0; } @@ -98,23 +102,17 @@ int indexOf(String str, int fromIndex) { return this.source.indexOf(str, this.start + fromIndex) - this.start; } - boolean startsWith(CharSequence prefix) { + boolean startsWith(String prefix) { return startsWith(prefix, 0); } - boolean startsWith(CharSequence prefix, int offset) { + boolean startsWith(String prefix, int offset) { int prefixLength = prefix.length(); - if (length() - prefixLength - offset < 0) { + int length = length(); + if (length - prefixLength - offset < 0) { return false; } - int prefixOffset = 0; - int sourceOffset = offset; - while (prefixLength-- != 0) { - if (charAt(sourceOffset++) != prefix.charAt(prefixOffset++)) { - return false; - } - } - return true; + return this.source.startsWith(prefix, this.start + offset); } @Override diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java new file mode 100644 index 000000000000..c711e206f5da --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.jarmode; + +/** + * Interface registered in {@code spring.factories} to provides extended 'jarmode' + * support. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public interface JarMode { + + /** + * Returns if this accepts and can run the given mode. + * @param mode the mode to check + * @return if this instance accepts the mode + */ + boolean accepts(String mode); + + /** + * Run the jar in the given mode. + * @param mode the mode to use + * @param args any program arguments + */ + void run(String mode, String[] args); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java new file mode 100644 index 000000000000..42a89a50a35b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.jarmode; + +import java.util.List; + +import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.util.ClassUtils; + +/** + * Delegate class used to launch the fat jar in a specific mode. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public final class JarModeLauncher { + + static final String DISABLE_SYSTEM_EXIT = JarModeLauncher.class.getName() + ".DISABLE_SYSTEM_EXIT"; + + private JarModeLauncher() { + } + + public static void main(String[] args) { + String mode = System.getProperty("jarmode"); + List candidates = SpringFactoriesLoader.loadFactories(JarMode.class, + ClassUtils.getDefaultClassLoader()); + for (JarMode candidate : candidates) { + if (candidate.accepts(mode)) { + candidate.run(mode, args); + return; + } + } + System.err.println("Unsupported jarmode '" + mode + "'"); + if (!Boolean.getBoolean(DISABLE_SYSTEM_EXIT)) { + System.exit(1); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java new file mode 100644 index 000000000000..6a6e83ff23c4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.jarmode; + +import java.util.Arrays; + +/** + * {@link JarMode} for testing. + * + * @author Phillip Webb + */ +class TestJarMode implements JarMode { + + @Override + public boolean accepts(String mode) { + return "test".equals(mode); + } + + @Override + public void run(String mode, String[] args) { + System.out.println("running in " + mode + " jar mode " + Arrays.asList(args)); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/package-info.java new file mode 100644 index 000000000000..315cb5696b83 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Support for launching the JAR using jarmode. + * + * @see org.springframework.boot.loader.jarmode.JarModeLauncher + */ +package org.springframework.boot.loader.jarmode; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/AbstractExecutableArchiveLauncherTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/AbstractExecutableArchiveLauncherTests.java index 49449cf24380..a461ba1f1dde 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/AbstractExecutableArchiveLauncherTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/AbstractExecutableArchiveLauncherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,15 +20,20 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; import java.net.MalformedURLException; import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Collections; import java.util.Enumeration; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; import java.util.zip.CRC32; import java.util.zip.ZipEntry; @@ -41,6 +46,7 @@ * Base class for testing {@link ExecutableArchiveLauncher} implementations. * * @author Andy Wilkinson + * @author Madhura Bhave */ public abstract class AbstractExecutableArchiveLauncherTests { @@ -48,12 +54,50 @@ public abstract class AbstractExecutableArchiveLauncherTests { File tempDir; protected File createJarArchive(String name, String entryPrefix) throws IOException { + return createJarArchive(name, entryPrefix, false, Collections.emptyList()); + } + + @SuppressWarnings("resource") + protected File createJarArchive(String name, String entryPrefix, boolean indexed, List extraLibs) + throws IOException { + return createJarArchive(name, null, entryPrefix, indexed, extraLibs); + } + + @SuppressWarnings("resource") + protected File createJarArchive(String name, Manifest manifest, String entryPrefix, boolean indexed, + List extraLibs) throws IOException { File archive = new File(this.tempDir, name); JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(archive)); + if (manifest != null) { + jarOutputStream.putNextEntry(new JarEntry("META-INF/")); + jarOutputStream.putNextEntry(new JarEntry("META-INF/MANIFEST.MF")); + manifest.write(jarOutputStream); + jarOutputStream.closeEntry(); + } jarOutputStream.putNextEntry(new JarEntry(entryPrefix + "/")); jarOutputStream.putNextEntry(new JarEntry(entryPrefix + "/classes/")); jarOutputStream.putNextEntry(new JarEntry(entryPrefix + "/lib/")); - JarEntry libFoo = new JarEntry(entryPrefix + "/lib/foo.jar"); + if (indexed) { + jarOutputStream.putNextEntry(new JarEntry(entryPrefix + "/classpath.idx")); + Writer writer = new OutputStreamWriter(jarOutputStream, StandardCharsets.UTF_8); + writer.write("- \"BOOT-INF/lib/foo.jar\"\n"); + writer.write("- \"BOOT-INF/lib/bar.jar\"\n"); + writer.write("- \"BOOT-INF/lib/baz.jar\"\n"); + writer.flush(); + jarOutputStream.closeEntry(); + } + addNestedJars(entryPrefix, "/lib/foo.jar", jarOutputStream); + addNestedJars(entryPrefix, "/lib/bar.jar", jarOutputStream); + addNestedJars(entryPrefix, "/lib/baz.jar", jarOutputStream); + for (String lib : extraLibs) { + addNestedJars(entryPrefix, "/lib/" + lib, jarOutputStream); + } + jarOutputStream.close(); + return archive; + } + + private void addNestedJars(String entryPrefix, String lib, JarOutputStream jarOutputStream) throws IOException { + JarEntry libFoo = new JarEntry(entryPrefix + lib); libFoo.setMethod(ZipEntry.STORED); ByteArrayOutputStream fooJarStream = new ByteArrayOutputStream(); new JarOutputStream(fooJarStream).close(); @@ -63,8 +107,6 @@ protected File createJarArchive(String name, String entryPrefix) throws IOExcept libFoo.setCrc(crc32.getValue()); jarOutputStream.putNextEntry(libFoo); jarOutputStream.write(fooJarStream.toByteArray()); - jarOutputStream.close(); - return archive; } protected File explode(File archive) throws IOException { @@ -87,11 +129,20 @@ protected File explode(File archive) throws IOException { } protected Set getUrls(List archives) throws MalformedURLException { - Set urls = new HashSet<>(archives.size()); + Set urls = new LinkedHashSet<>(archives.size()); for (Archive archive : archives) { urls.add(archive.getUrl()); } return urls; } + protected final URL toUrl(File file) { + try { + return file.toURI().toURL(); + } + catch (MalformedURLException ex) { + throw new IllegalStateException(ex); + } + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/ClassPathIndexFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/ClassPathIndexFileTests.java new file mode 100644 index 000000000000..a7e74a6fa56a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/ClassPathIndexFileTests.java @@ -0,0 +1,109 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link ClassPathIndexFile}. + * + * @author Madhura Bhave + * @author Phillip Webb + */ +class ClassPathIndexFileTests { + + @TempDir + File temp; + + @Test + void loadIfPossibleWhenRootIsNotFileReturnsNull() { + assertThatIllegalArgumentException() + .isThrownBy(() -> ClassPathIndexFile.loadIfPossible(new URL("https://example.com/file"), "test.idx")) + .withMessage("URL does not reference a file"); + } + + @Test + void loadIfPossibleWhenRootDoesNotExistReturnsNull() throws Exception { + File root = new File(this.temp, "missing"); + assertThat(ClassPathIndexFile.loadIfPossible(root.toURI().toURL(), "test.idx")).isNull(); + } + + @Test + void loadIfPossibleWhenRootIsDirectoryThrowsException() throws Exception { + File root = new File(this.temp, "directory"); + root.mkdirs(); + assertThat(ClassPathIndexFile.loadIfPossible(root.toURI().toURL(), "test.idx")).isNull(); + } + + @Test + void loadIfPossibleReturnsInstance() throws Exception { + ClassPathIndexFile indexFile = copyAndLoadTestIndexFile(); + assertThat(indexFile).isNotNull(); + } + + @Test + void sizeReturnsNumberOfLines() throws Exception { + ClassPathIndexFile indexFile = copyAndLoadTestIndexFile(); + assertThat(indexFile.size()).isEqualTo(5); + } + + @Test + void getUrlsReturnsUrls() throws Exception { + ClassPathIndexFile indexFile = copyAndLoadTestIndexFile(); + List urls = indexFile.getUrls(); + List expected = new ArrayList<>(); + expected.add(new File(this.temp, "BOOT-INF/layers/one/lib/a.jar")); + expected.add(new File(this.temp, "BOOT-INF/layers/one/lib/b.jar")); + expected.add(new File(this.temp, "BOOT-INF/layers/one/lib/c.jar")); + expected.add(new File(this.temp, "BOOT-INF/layers/two/lib/d.jar")); + expected.add(new File(this.temp, "BOOT-INF/layers/two/lib/e.jar")); + assertThat(urls).containsExactly(expected.stream().map(this::toUrl).toArray(URL[]::new)); + } + + private URL toUrl(File file) { + try { + return file.toURI().toURL(); + } + catch (MalformedURLException ex) { + throw new IllegalStateException(ex); + } + } + + private ClassPathIndexFile copyAndLoadTestIndexFile() throws IOException { + copyTestIndexFile(); + ClassPathIndexFile indexFile = ClassPathIndexFile.loadIfPossible(this.temp.toURI().toURL(), "test.idx"); + return indexFile; + } + + private void copyTestIndexFile() throws IOException { + Files.copy(getClass().getResourceAsStream("classpath-index-file.idx"), + new File(this.temp, "test.idx").toPath()); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/JarLauncherTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/JarLauncherTests.java index fa171150f5de..59cc53d2a25d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/JarLauncherTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/JarLauncherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,13 +18,23 @@ import java.io.File; import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; import java.util.List; +import java.util.jar.Attributes; +import java.util.jar.Attributes.Name; +import java.util.jar.Manifest; import org.junit.jupiter.api.Test; import org.springframework.boot.loader.archive.Archive; import org.springframework.boot.loader.archive.ExplodedArchive; import org.springframework.boot.loader.archive.JarFileArchive; +import org.springframework.boot.testsupport.compiler.TestCompiler; +import org.springframework.util.FileCopyUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -32,6 +42,7 @@ * Tests for {@link JarLauncher}. * * @author Andy Wilkinson + * @author Madhura Bhave */ class JarLauncherTests extends AbstractExecutableArchiveLauncherTests { @@ -39,10 +50,9 @@ class JarLauncherTests extends AbstractExecutableArchiveLauncherTests { void explodedJarHasOnlyBootInfClassesAndContentsOfBootInfLibOnClasspath() throws Exception { File explodedRoot = explode(createJarArchive("archive.jar", "BOOT-INF")); JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot, true)); - List archives = launcher.getClassPathArchives(); - assertThat(archives).hasSize(2); - assertThat(getUrls(archives)).containsOnly(new File(explodedRoot, "BOOT-INF/classes").toURI().toURL(), - new File(explodedRoot, "BOOT-INF/lib/foo.jar").toURI().toURL()); + List archives = new ArrayList<>(); + launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); + assertThat(getUrls(archives)).containsExactlyInAnyOrder(getExpectedFileUrls(explodedRoot)); for (Archive archive : archives) { archive.close(); } @@ -53,15 +63,85 @@ void archivedJarHasOnlyBootInfClassesAndContentsOfBootInfLibOnClasspath() throws File jarRoot = createJarArchive("archive.jar", "BOOT-INF"); try (JarFileArchive archive = new JarFileArchive(jarRoot)) { JarLauncher launcher = new JarLauncher(archive); - List classPathArchives = launcher.getClassPathArchives(); - assertThat(classPathArchives).hasSize(2); + List classPathArchives = new ArrayList<>(); + launcher.getClassPathArchivesIterator().forEachRemaining(classPathArchives::add); + assertThat(classPathArchives).hasSize(4); assertThat(getUrls(classPathArchives)).containsOnly( new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/classes!/"), - new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/lib/foo.jar!/")); + new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/lib/foo.jar!/"), + new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/lib/bar.jar!/"), + new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/lib/baz.jar!/")); for (Archive classPathArchive : classPathArchives) { classPathArchive.close(); } } } + @Test + void explodedJarShouldPreserveClasspathOrderWhenIndexPresent() throws Exception { + File explodedRoot = explode(createJarArchive("archive.jar", "BOOT-INF", true, Collections.emptyList())); + JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot, true)); + Iterator archives = launcher.getClassPathArchivesIterator(); + URLClassLoader classLoader = (URLClassLoader) launcher.createClassLoader(archives); + URL[] urls = classLoader.getURLs(); + assertThat(urls).containsExactly(getExpectedFileUrls(explodedRoot)); + } + + @Test + void jarFilesPresentInBootInfLibsAndNotInClasspathIndexShouldBeAddedAfterBootInfClasses() throws Exception { + ArrayList extraLibs = new ArrayList<>(Arrays.asList("extra-1.jar", "extra-2.jar")); + File explodedRoot = explode(createJarArchive("archive.jar", "BOOT-INF", true, extraLibs)); + JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot, true)); + Iterator archives = launcher.getClassPathArchivesIterator(); + URLClassLoader classLoader = (URLClassLoader) launcher.createClassLoader(archives); + URL[] urls = classLoader.getURLs(); + List expectedFiles = getExpectedFilesWithExtraLibs(explodedRoot); + URL[] expectedFileUrls = expectedFiles.stream().map(this::toUrl).toArray(URL[]::new); + assertThat(urls).containsExactly(expectedFileUrls); + } + + @Test + void explodedJarDefinedPackagesIncludeManifestAttributes() throws Exception { + Manifest manifest = new Manifest(); + Attributes attributes = manifest.getMainAttributes(); + attributes.put(Name.MANIFEST_VERSION, "1.0"); + attributes.put(Name.IMPLEMENTATION_TITLE, "test"); + File explodedRoot = explode( + createJarArchive("archive.jar", manifest, "BOOT-INF", true, Collections.emptyList())); + TestCompiler compiler = new TestCompiler(new File(explodedRoot, "BOOT-INF/classes")); + File source = new File(this.tempDir, "explodedsample/ExampleClass.java"); + source.getParentFile().mkdirs(); + FileCopyUtils.copy(new File("src/test/resources/explodedsample/ExampleClass.txt"), source); + compiler.getTask(Collections.singleton(source)).call(); + JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot, true)); + Iterator archives = launcher.getClassPathArchivesIterator(); + URLClassLoader classLoader = (URLClassLoader) launcher.createClassLoader(archives); + Class loaded = classLoader.loadClass("explodedsample.ExampleClass"); + assertThat(loaded.getPackage().getImplementationTitle()).isEqualTo("test"); + } + + protected final URL[] getExpectedFileUrls(File explodedRoot) { + return getExpectedFiles(explodedRoot).stream().map(this::toUrl).toArray(URL[]::new); + } + + protected final List getExpectedFiles(File parent) { + List expected = new ArrayList<>(); + expected.add(new File(parent, "BOOT-INF/classes")); + expected.add(new File(parent, "BOOT-INF/lib/foo.jar")); + expected.add(new File(parent, "BOOT-INF/lib/bar.jar")); + expected.add(new File(parent, "BOOT-INF/lib/baz.jar")); + return expected; + } + + protected final List getExpectedFilesWithExtraLibs(File parent) { + List expected = new ArrayList<>(); + expected.add(new File(parent, "BOOT-INF/classes")); + expected.add(new File(parent, "BOOT-INF/lib/extra-1.jar")); + expected.add(new File(parent, "BOOT-INF/lib/extra-2.jar")); + expected.add(new File(parent, "BOOT-INF/lib/foo.jar")); + expected.add(new File(parent, "BOOT-INF/lib/bar.jar")); + expected.add(new File(parent, "BOOT-INF/lib/baz.jar")); + return expected; + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java index 26c1b006f3c6..d36269c3a414 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,12 +19,15 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.lang.ref.SoftReference; import java.net.URL; import java.net.URLClassLoader; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; +import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.jar.Attributes; import java.util.jar.Manifest; @@ -39,10 +42,13 @@ import org.springframework.boot.loader.archive.Archive; import org.springframework.boot.loader.archive.ExplodedArchive; import org.springframework.boot.loader.archive.JarFileArchive; +import org.springframework.boot.loader.jar.Handler; +import org.springframework.boot.loader.jar.JarFile; import org.springframework.boot.testsupport.system.CapturedOutput; import org.springframework.boot.testsupport.system.OutputCaptureExtension; import org.springframework.core.io.FileSystemResource; import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.util.FileCopyUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; @@ -60,19 +66,22 @@ class PropertiesLauncherTests { @TempDir File tempDir; + private PropertiesLauncher launcher; + private ClassLoader contextClassLoader; private CapturedOutput output; @BeforeEach - void setup(CapturedOutput capturedOutput) { + void setup(CapturedOutput capturedOutput) throws Exception { this.contextClassLoader = Thread.currentThread().getContextClassLoader(); + clearHandlerCache(); System.setProperty("loader.home", new File("src/test/resources").getAbsolutePath()); this.output = capturedOutput; } @AfterEach - void close() { + void close() throws Exception { Thread.currentThread().setContextClassLoader(this.contextClassLoader); System.clearProperty("loader.home"); System.clearProperty("loader.path"); @@ -81,65 +90,82 @@ void close() { System.clearProperty("loader.config.location"); System.clearProperty("loader.system"); System.clearProperty("loader.classLoader"); + clearHandlerCache(); + if (this.launcher != null) { + this.launcher.close(); + } + } + + @SuppressWarnings("unchecked") + private void clearHandlerCache() throws Exception { + Map rootFileCache = ((SoftReference>) ReflectionTestUtils + .getField(Handler.class, "rootFileCache")).get(); + if (rootFileCache != null) { + for (JarFile rootJarFile : rootFileCache.values()) { + rootJarFile.close(); + } + rootFileCache.clear(); + } } @Test void testDefaultHome() { System.clearProperty("loader.home"); - PropertiesLauncher launcher = new PropertiesLauncher(); - assertThat(launcher.getHomeDirectory()).isEqualTo(new File(System.getProperty("user.dir"))); + this.launcher = new PropertiesLauncher(); + assertThat(this.launcher.getHomeDirectory()).isEqualTo(new File(System.getProperty("user.dir"))); } @Test void testAlternateHome() throws Exception { System.setProperty("loader.home", "src/test/resources/home"); - PropertiesLauncher launcher = new PropertiesLauncher(); - assertThat(launcher.getHomeDirectory()).isEqualTo(new File(System.getProperty("loader.home"))); - assertThat(launcher.getMainClass()).isEqualTo("demo.HomeApplication"); + this.launcher = new PropertiesLauncher(); + assertThat(this.launcher.getHomeDirectory()).isEqualTo(new File(System.getProperty("loader.home"))); + assertThat(this.launcher.getMainClass()).isEqualTo("demo.HomeApplication"); } @Test void testNonExistentHome() { System.setProperty("loader.home", "src/test/resources/nonexistent"); assertThatIllegalStateException().isThrownBy(PropertiesLauncher::new) - .withMessageContaining("Invalid source folder").withCauseInstanceOf(IllegalArgumentException.class); + .withMessageContaining("Invalid source directory").withCauseInstanceOf(IllegalArgumentException.class); } @Test void testUserSpecifiedMain() throws Exception { - PropertiesLauncher launcher = new PropertiesLauncher(); - assertThat(launcher.getMainClass()).isEqualTo("demo.Application"); + this.launcher = new PropertiesLauncher(); + assertThat(this.launcher.getMainClass()).isEqualTo("demo.Application"); assertThat(System.getProperty("loader.main")).isNull(); } @Test void testUserSpecifiedConfigName() throws Exception { System.setProperty("loader.config.name", "foo"); - PropertiesLauncher launcher = new PropertiesLauncher(); - assertThat(launcher.getMainClass()).isEqualTo("my.Application"); - assertThat(ReflectionTestUtils.getField(launcher, "paths").toString()).isEqualTo("[etc/]"); + this.launcher = new PropertiesLauncher(); + assertThat(this.launcher.getMainClass()).isEqualTo("my.Application"); + assertThat(ReflectionTestUtils.getField(this.launcher, "paths").toString()).isEqualTo("[etc/]"); } @Test void testRootOfClasspathFirst() throws Exception { System.setProperty("loader.config.name", "bar"); - PropertiesLauncher launcher = new PropertiesLauncher(); - assertThat(launcher.getMainClass()).isEqualTo("my.BarApplication"); + this.launcher = new PropertiesLauncher(); + assertThat(this.launcher.getMainClass()).isEqualTo("my.BarApplication"); } @Test void testUserSpecifiedDotPath() { System.setProperty("loader.path", "."); - PropertiesLauncher launcher = new PropertiesLauncher(); - assertThat(ReflectionTestUtils.getField(launcher, "paths").toString()).isEqualTo("[.]"); + this.launcher = new PropertiesLauncher(); + assertThat(ReflectionTestUtils.getField(this.launcher, "paths").toString()).isEqualTo("[.]"); } @Test void testUserSpecifiedSlashPath() throws Exception { System.setProperty("loader.path", "jars/"); - PropertiesLauncher launcher = new PropertiesLauncher(); - assertThat(ReflectionTestUtils.getField(launcher, "paths").toString()).isEqualTo("[jars/]"); - List archives = launcher.getClassPathArchives(); + this.launcher = new PropertiesLauncher(); + assertThat(ReflectionTestUtils.getField(this.launcher, "paths").toString()).isEqualTo("[jars/]"); + List archives = new ArrayList<>(); + this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); assertThat(archives).areExactly(1, endingWith("app.jar")); } @@ -147,9 +173,9 @@ void testUserSpecifiedSlashPath() throws Exception { void testUserSpecifiedWildcardPath() throws Exception { System.setProperty("loader.path", "jars/*"); System.setProperty("loader.main", "demo.Application"); - PropertiesLauncher launcher = new PropertiesLauncher(); - assertThat(ReflectionTestUtils.getField(launcher, "paths").toString()).isEqualTo("[jars/]"); - launcher.launch(new String[0]); + this.launcher = new PropertiesLauncher(); + assertThat(ReflectionTestUtils.getField(this.launcher, "paths").toString()).isEqualTo("[jars/]"); + this.launcher.launch(new String[0]); waitFor("Hello World"); } @@ -157,19 +183,20 @@ void testUserSpecifiedWildcardPath() throws Exception { void testUserSpecifiedJarPath() throws Exception { System.setProperty("loader.path", "jars/app.jar"); System.setProperty("loader.main", "demo.Application"); - PropertiesLauncher launcher = new PropertiesLauncher(); - assertThat(ReflectionTestUtils.getField(launcher, "paths").toString()).isEqualTo("[jars/app.jar]"); - launcher.launch(new String[0]); + this.launcher = new PropertiesLauncher(); + assertThat(ReflectionTestUtils.getField(this.launcher, "paths").toString()).isEqualTo("[jars/app.jar]"); + this.launcher.launch(new String[0]); waitFor("Hello World"); } @Test void testUserSpecifiedRootOfJarPath() throws Exception { System.setProperty("loader.path", "jar:file:./src/test/resources/nested-jars/app.jar!/"); - PropertiesLauncher launcher = new PropertiesLauncher(); - assertThat(ReflectionTestUtils.getField(launcher, "paths").toString()) + this.launcher = new PropertiesLauncher(); + assertThat(ReflectionTestUtils.getField(this.launcher, "paths").toString()) .isEqualTo("[jar:file:./src/test/resources/nested-jars/app.jar!/]"); - List archives = launcher.getClassPathArchives(); + List archives = new ArrayList<>(); + this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); assertThat(archives).areExactly(1, endingWith("foo.jar!/")); assertThat(archives).areExactly(1, endingWith("app.jar")); } @@ -177,8 +204,9 @@ void testUserSpecifiedRootOfJarPath() throws Exception { @Test void testUserSpecifiedRootOfJarPathWithDot() throws Exception { System.setProperty("loader.path", "nested-jars/app.jar!/./"); - PropertiesLauncher launcher = new PropertiesLauncher(); - List archives = launcher.getClassPathArchives(); + this.launcher = new PropertiesLauncher(); + List archives = new ArrayList<>(); + this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); assertThat(archives).areExactly(1, endingWith("foo.jar!/")); assertThat(archives).areExactly(1, endingWith("app.jar")); } @@ -186,8 +214,9 @@ void testUserSpecifiedRootOfJarPathWithDot() throws Exception { @Test void testUserSpecifiedRootOfJarPathWithDotAndJarPrefix() throws Exception { System.setProperty("loader.path", "jar:file:./src/test/resources/nested-jars/app.jar!/./"); - PropertiesLauncher launcher = new PropertiesLauncher(); - List archives = launcher.getClassPathArchives(); + this.launcher = new PropertiesLauncher(); + List archives = new ArrayList<>(); + this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); assertThat(archives).areExactly(1, endingWith("foo.jar!/")); } @@ -195,27 +224,30 @@ void testUserSpecifiedRootOfJarPathWithDotAndJarPrefix() throws Exception { void testUserSpecifiedJarFileWithNestedArchives() throws Exception { System.setProperty("loader.path", "nested-jars/app.jar"); System.setProperty("loader.main", "demo.Application"); - PropertiesLauncher launcher = new PropertiesLauncher(); - List archives = launcher.getClassPathArchives(); + this.launcher = new PropertiesLauncher(); + List archives = new ArrayList<>(); + this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); assertThat(archives).areExactly(1, endingWith("foo.jar!/")); assertThat(archives).areExactly(1, endingWith("app.jar")); } @Test void testUserSpecifiedNestedJarPath() throws Exception { - System.setProperty("loader.path", "nested-jars/app.jar!/foo.jar"); + System.setProperty("loader.path", "nested-jars/nested-jar-app.jar!/BOOT-INF/classes/"); System.setProperty("loader.main", "demo.Application"); - PropertiesLauncher launcher = new PropertiesLauncher(); - List archives = launcher.getClassPathArchives(); - assertThat(archives).hasSize(1).areExactly(1, endingWith("foo.jar!/")); + this.launcher = new PropertiesLauncher(); + assertThat(ReflectionTestUtils.getField(this.launcher, "paths").toString()) + .isEqualTo("[nested-jars/nested-jar-app.jar!/BOOT-INF/classes/]"); + this.launcher.launch(new String[0]); + waitFor("Hello World"); } @Test void testUserSpecifiedDirectoryContainingJarFileWithNestedArchives() throws Exception { System.setProperty("loader.path", "nested-jars"); System.setProperty("loader.main", "demo.Application"); - PropertiesLauncher launcher = new PropertiesLauncher(); - launcher.launch(new String[0]); + this.launcher = new PropertiesLauncher(); + this.launcher.launch(new String[0]); waitFor("Hello World"); } @@ -223,9 +255,9 @@ void testUserSpecifiedDirectoryContainingJarFileWithNestedArchives() throws Exce void testUserSpecifiedJarPathWithDot() throws Exception { System.setProperty("loader.path", "./jars/app.jar"); System.setProperty("loader.main", "demo.Application"); - PropertiesLauncher launcher = new PropertiesLauncher(); - assertThat(ReflectionTestUtils.getField(launcher, "paths").toString()).isEqualTo("[jars/app.jar]"); - launcher.launch(new String[0]); + this.launcher = new PropertiesLauncher(); + assertThat(ReflectionTestUtils.getField(this.launcher, "paths").toString()).isEqualTo("[jars/app.jar]"); + this.launcher.launch(new String[0]); waitFor("Hello World"); } @@ -233,9 +265,9 @@ void testUserSpecifiedJarPathWithDot() throws Exception { void testUserSpecifiedClassLoader() throws Exception { System.setProperty("loader.path", "jars/app.jar"); System.setProperty("loader.classLoader", URLClassLoader.class.getName()); - PropertiesLauncher launcher = new PropertiesLauncher(); - assertThat(ReflectionTestUtils.getField(launcher, "paths").toString()).isEqualTo("[jars/app.jar]"); - launcher.launch(new String[0]); + this.launcher = new PropertiesLauncher(); + assertThat(ReflectionTestUtils.getField(this.launcher, "paths").toString()).isEqualTo("[jars/app.jar]"); + this.launcher.launch(new String[0]); waitFor("Hello World"); } @@ -243,33 +275,39 @@ void testUserSpecifiedClassLoader() throws Exception { void testUserSpecifiedClassPathOrder() throws Exception { System.setProperty("loader.path", "more-jars/app.jar,jars/app.jar"); System.setProperty("loader.classLoader", URLClassLoader.class.getName()); - PropertiesLauncher launcher = new PropertiesLauncher(); - assertThat(ReflectionTestUtils.getField(launcher, "paths").toString()) + this.launcher = new PropertiesLauncher(); + assertThat(ReflectionTestUtils.getField(this.launcher, "paths").toString()) .isEqualTo("[more-jars/app.jar, jars/app.jar]"); - launcher.launch(new String[0]); + this.launcher.launch(new String[0]); waitFor("Hello Other World"); } @Test void testCustomClassLoaderCreation() throws Exception { System.setProperty("loader.classLoader", TestLoader.class.getName()); - PropertiesLauncher launcher = new PropertiesLauncher(); - ClassLoader loader = launcher.createClassLoader(archives()); + this.launcher = new PropertiesLauncher(); + ClassLoader loader = this.launcher.createClassLoader(archives()); assertThat(loader).isNotNull(); assertThat(loader.getClass().getName()).isEqualTo(TestLoader.class.getName()); } - private List archives() throws Exception { + private Iterator archives() throws Exception { List archives = new ArrayList<>(); String path = System.getProperty("java.class.path"); for (String url : path.split(File.pathSeparator)) { - archives.add(archive(url)); + Archive archive = archive(url); + if (archive != null) { + archives.add(archive); + } } - return archives; + return archives.iterator(); } private Archive archive(String url) throws IOException { File file = new FileSystemResource(url).getFile(); + if (!file.exists()) { + return null; + } if (url.endsWith(".jar")) { return new JarFileArchive(file); } @@ -278,18 +316,17 @@ private Archive archive(String url) throws IOException { @Test void testUserSpecifiedConfigPathWins() throws Exception { - System.setProperty("loader.config.name", "foo"); System.setProperty("loader.config.location", "classpath:bar.properties"); - PropertiesLauncher launcher = new PropertiesLauncher(); - assertThat(launcher.getMainClass()).isEqualTo("my.BarApplication"); + this.launcher = new PropertiesLauncher(); + assertThat(this.launcher.getMainClass()).isEqualTo("my.BarApplication"); } @Test void testSystemPropertySpecifiedMain() throws Exception { System.setProperty("loader.main", "foo.Bar"); - PropertiesLauncher launcher = new PropertiesLauncher(); - assertThat(launcher.getMainClass()).isEqualTo("foo.Bar"); + this.launcher = new PropertiesLauncher(); + assertThat(this.launcher.getMainClass()).isEqualTo("foo.Bar"); } @Test @@ -302,8 +339,8 @@ void testSystemPropertiesSet() { @Test void testArgsEnhanced() throws Exception { System.setProperty("loader.args", "foo"); - PropertiesLauncher launcher = new PropertiesLauncher(); - assertThat(Arrays.asList(launcher.getArgs("bar")).toString()).isEqualTo("[foo, bar]"); + this.launcher = new PropertiesLauncher(); + assertThat(Arrays.asList(this.launcher.getArgs("bar")).toString()).isEqualTo("[foo, bar]"); } @SuppressWarnings("unchecked") @@ -318,15 +355,16 @@ void testLoadPathCustomizedUsingManifest() throws Exception { try (FileOutputStream manifestStream = new FileOutputStream(manifestFile)) { manifest.write(manifestStream); } - PropertiesLauncher launcher = new PropertiesLauncher(); - assertThat((List) ReflectionTestUtils.getField(launcher, "paths")).containsExactly("/foo.jar", "/bar/"); + this.launcher = new PropertiesLauncher(); + assertThat((List) ReflectionTestUtils.getField(this.launcher, "paths")).containsExactly("/foo.jar", + "/bar/"); } @Test void testManifestWithPlaceholders() throws Exception { System.setProperty("loader.home", "src/test/resources/placeholders"); - PropertiesLauncher launcher = new PropertiesLauncher(); - assertThat(launcher.getMainClass()).isEqualTo("demo.FooApplication"); + this.launcher = new PropertiesLauncher(); + assertThat(this.launcher.getMainClass()).isEqualTo("demo.FooApplication"); } @Test @@ -334,14 +372,36 @@ void encodedFileUrlLoaderPathIsHandledCorrectly() throws Exception { File loaderPath = new File(this.tempDir, "loader path"); loaderPath.mkdir(); System.setProperty("loader.path", loaderPath.toURI().toURL().toString()); - PropertiesLauncher launcher = new PropertiesLauncher(); - List archives = launcher.getClassPathArchives(); + this.launcher = new PropertiesLauncher(); + List archives = new ArrayList<>(); + this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); assertThat(archives.size()).isEqualTo(1); File archiveRoot = (File) ReflectionTestUtils.getField(archives.get(0), "root"); assertThat(archiveRoot).isEqualTo(loaderPath); } - private void waitFor(String value) throws Exception { + @Test // gh-21575 + void loadResourceFromJarFile() throws Exception { + File jarFile = new File(this.tempDir, "app.jar"); + TestJarCreator.createTestJar(jarFile); + System.setProperty("loader.home", this.tempDir.getAbsolutePath()); + System.setProperty("loader.path", "app.jar"); + this.launcher = new PropertiesLauncher(); + try { + this.launcher.launch(new String[0]); + } + catch (Exception ex) { + // Expected ClassNotFoundException + LaunchedURLClassLoader classLoader = (LaunchedURLClassLoader) Thread.currentThread() + .getContextClassLoader(); + classLoader.close(); + } + URL resource = new URL("jar:" + jarFile.toURI() + "!/nested.jar!/3.dat"); + byte[] bytes = FileCopyUtils.copyToByteArray(resource.openStream()); + assertThat(bytes).isNotEmpty(); + } + + private void waitFor(String value) { Awaitility.waitAtMost(Duration.ofSeconds(5)).until(this.output::toString, containsString(value)); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java index 6303e8808c91..100e2c757e37 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,22 @@ */ public abstract class TestJarCreator { + private static final int BASE_VERSION = 8; + + private static final int RUNTIME_VERSION; + + static { + int version; + try { + Object runtimeVersion = Runtime.class.getMethod("version").invoke(null); + version = (int) runtimeVersion.getClass().getMethod("major").invoke(runtimeVersion); + } + catch (Throwable ex) { + version = BASE_VERSION; + } + RUNTIME_VERSION = version; + } + public static void createTestJar(File file) throws Exception { createTestJar(file, false); } @@ -49,7 +65,6 @@ public static void createTestJar(File file, boolean unpackNested) throws Excepti writeEntry(jarOutputStream, "d/9.dat", 9); writeDirEntry(jarOutputStream, "special/"); writeEntry(jarOutputStream, "special/\u00EB.dat", '\u00EB'); - writeNestedEntry("nested.jar", unpackNested, jarOutputStream); writeNestedEntry("another-nested.jar", unpackNested, jarOutputStream); writeNestedEntry("space nested.jar", unpackNested, jarOutputStream); @@ -79,7 +94,6 @@ private static void writeNestedEntry(String name, boolean unpackNested, JarOutpu CRC32 crc32 = new CRC32(); crc32.update(nestedJarData); nestedEntry.setCrc(crc32.getValue()); - nestedEntry.setMethod(ZipEntry.STORED); jarOutputStream.putNextEntry(nestedEntry); jarOutputStream.write(nestedJarData); @@ -92,12 +106,9 @@ private static byte[] getNestedJarData(boolean multiRelease) throws Exception { jarOutputStream.setComment("nested"); writeManifest(jarOutputStream, "j2", multiRelease); if (multiRelease) { - writeEntry(jarOutputStream, "multi-release.dat", 8); - writeEntry(jarOutputStream, "META-INF/versions/9/multi-release.dat", 9); - writeEntry(jarOutputStream, "META-INF/versions/10/multi-release.dat", 10); - writeEntry(jarOutputStream, "META-INF/versions/11/multi-release.dat", 11); - writeEntry(jarOutputStream, "META-INF/versions/12/multi-release.dat", 12); - writeEntry(jarOutputStream, "META-INF/versions/13/multi-release.dat", 13); + writeEntry(jarOutputStream, "multi-release.dat", BASE_VERSION); + writeEntry(jarOutputStream, String.format("META-INF/versions/%d/multi-release.dat", RUNTIME_VERSION), + RUNTIME_VERSION); } else { writeEntry(jarOutputStream, "3.dat", 3); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/WarLauncherTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/WarLauncherTests.java index 7b68a50d96d8..b0d5539ddfec 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/WarLauncherTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/WarLauncherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.io.File; import java.net.URL; +import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Test; @@ -39,29 +40,43 @@ class WarLauncherTests extends AbstractExecutableArchiveLauncherTests { void explodedWarHasOnlyWebInfClassesAndContentsOfWebInfLibOnClasspath() throws Exception { File explodedRoot = explode(createJarArchive("archive.war", "WEB-INF")); WarLauncher launcher = new WarLauncher(new ExplodedArchive(explodedRoot, true)); - List archives = launcher.getClassPathArchives(); - assertThat(archives).hasSize(2); - assertThat(getUrls(archives)).containsOnly(new File(explodedRoot, "WEB-INF/classes").toURI().toURL(), - new File(explodedRoot, "WEB-INF/lib/foo.jar").toURI().toURL()); + List archives = new ArrayList<>(); + launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); + assertThat(getUrls(archives)).containsExactlyInAnyOrder(getExpectedFileUrls(explodedRoot)); for (Archive archive : archives) { archive.close(); } } @Test - void archivedWarHasOnlyWebInfClassesAndContentsOWebInfLibOnClasspath() throws Exception { + void archivedWarHasOnlyWebInfClassesAndContentsOfWebInfLibOnClasspath() throws Exception { File jarRoot = createJarArchive("archive.war", "WEB-INF"); try (JarFileArchive archive = new JarFileArchive(jarRoot)) { WarLauncher launcher = new WarLauncher(archive); - List classPathArchives = launcher.getClassPathArchives(); - assertThat(classPathArchives).hasSize(2); + List classPathArchives = new ArrayList<>(); + launcher.getClassPathArchivesIterator().forEachRemaining(classPathArchives::add); assertThat(getUrls(classPathArchives)).containsOnly( new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/classes!/"), - new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/lib/foo.jar!/")); + new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/lib/foo.jar!/"), + new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/lib/bar.jar!/"), + new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/lib/baz.jar!/")); for (Archive classPathArchive : classPathArchives) { classPathArchive.close(); } } } + protected final URL[] getExpectedFileUrls(File explodedRoot) { + return getExpectedFiles(explodedRoot).stream().map(this::toUrl).toArray(URL[]::new); + } + + protected final List getExpectedFiles(File parent) { + List expected = new ArrayList<>(); + expected.add(new File(parent, "WEB-INF/classes")); + expected.add(new File(parent, "WEB-INF/lib/foo.jar")); + expected.add(new File(parent, "WEB-INF/lib/bar.jar")); + expected.add(new File(parent, "WEB-INF/lib/baz.jar")); + return expected; + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java index 23aead134d4d..080781be933f 100755 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,7 +51,7 @@ class ExplodedArchiveTests { @TempDir File tempDir; - private File rootFolder; + private File rootDirectory; private ExplodedArchive archive; @@ -71,17 +71,16 @@ private void createArchive() throws Exception { createArchive(null); } - private void createArchive(String folderName) throws Exception { + private void createArchive(String directoryName) throws Exception { File file = new File(this.tempDir, "test.jar"); TestJarCreator.createTestJar(file); - - this.rootFolder = (StringUtils.hasText(folderName) ? new File(this.tempDir, folderName) + this.rootDirectory = (StringUtils.hasText(directoryName) ? new File(this.tempDir, directoryName) : new File(this.tempDir, UUID.randomUUID().toString())); JarFile jarFile = new JarFile(file); Enumeration entries = jarFile.entries(); while (entries.hasMoreElements()) { JarEntry entry = entries.nextElement(); - File destination = new File(this.rootFolder.getAbsolutePath() + File.separator + entry.getName()); + File destination = new File(this.rootDirectory.getAbsolutePath() + File.separator + entry.getName()); destination.getParentFile().mkdirs(); if (entry.isDirectory()) { destination.mkdir(); @@ -90,7 +89,7 @@ private void createArchive(String folderName) throws Exception { FileCopyUtils.copy(jarFile.getInputStream(entry), new FileOutputStream(destination)); } } - this.archive = new ExplodedArchive(this.rootFolder); + this.archive = new ExplodedArchive(this.rootDirectory); jarFile.close(); } @@ -102,26 +101,26 @@ void getManifest() throws Exception { @Test void getEntries() { Map entries = getEntriesMap(this.archive); - assertThat(entries.size()).isEqualTo(12); + assertThat(entries).hasSize(12); } @Test void getUrl() throws Exception { - assertThat(this.archive.getUrl()).isEqualTo(this.rootFolder.toURI().toURL()); + assertThat(this.archive.getUrl()).isEqualTo(this.rootDirectory.toURI().toURL()); } @Test void getUrlWithSpaceInPath() throws Exception { createArchive("spaces in the name"); - assertThat(this.archive.getUrl()).isEqualTo(this.rootFolder.toURI().toURL()); + assertThat(this.archive.getUrl()).isEqualTo(this.rootDirectory.toURI().toURL()); } @Test void getNestedArchive() throws Exception { Entry entry = getEntriesMap(this.archive).get("nested.jar"); Archive nested = this.archive.getNestedArchive(entry); - assertThat(nested.getUrl().toString()).isEqualTo(this.rootFolder.toURI() + "nested.jar"); - ((JarFileArchive) nested).close(); + assertThat(nested.getUrl().toString()).isEqualTo(this.rootDirectory.toURI() + "nested.jar"); + nested.close(); } @Test @@ -130,7 +129,7 @@ void nestedDirArchive() throws Exception { Archive nested = this.archive.getNestedArchive(entry); Map nestedEntries = getEntriesMap(nested); assertThat(nestedEntries.size()).isEqualTo(1); - assertThat(nested.getUrl().toString()).isEqualTo("file:" + this.rootFolder.toURI().getPath() + "d/"); + assertThat(nested.getUrl().toString()).isEqualTo("file:" + this.rootDirectory.toURI().getPath() + "d/"); } @Test diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java index 3cc4d24b4091..1394af60155f 100755 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,7 @@ import org.springframework.boot.loader.TestJarCreator; import org.springframework.boot.loader.archive.Archive.Entry; +import org.springframework.boot.loader.jar.JarFile; import org.springframework.util.FileCopyUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -147,6 +148,7 @@ void filesInZip64ArchivesAreAllListed() throws IOException { File file = new File(this.tempDir, "test.jar"); FileCopyUtils.copy(writeZip64Jar(), file); try (JarFileArchive zip64Archive = new JarFileArchive(file)) { + @SuppressWarnings("deprecation") Iterator entries = zip64Archive.iterator(); for (int i = 0; i < 65537; i++) { assertThat(entries.hasNext()).as(i + "nth file is present").isTrue(); @@ -171,13 +173,14 @@ void nestedZip64ArchivesAreHandledGracefully() throws Exception { output.write(zip64JarData); output.closeEntry(); } - try (JarFileArchive jarFileArchive = new JarFileArchive(file); - Archive nestedArchive = jarFileArchive - .getNestedArchive(getEntriesMap(jarFileArchive).get("nested/zip64.jar"))) { - Iterator it = nestedArchive.iterator(); - for (int i = 0; i < 65537; i++) { - assertThat(it.hasNext()).as(i + "nth file is present").isTrue(); - it.next(); + try (JarFile jarFile = new JarFile(file)) { + ZipEntry nestedEntry = jarFile.getEntry("nested/zip64.jar"); + try (JarFile nestedJarFile = jarFile.getNestedJarFile(nestedEntry)) { + Iterator iterator = nestedJarFile.iterator(); + for (int i = 0; i < 65537; i++) { + assertThat(iterator.hasNext()).as(i + "nth file is present").isTrue(); + iterator.next(); + } } } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/data/RandomAccessDataFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/data/RandomAccessDataFileTests.java index 0e42e0fb12c6..fd567134f4c2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/data/RandomAccessDataFileTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/data/RandomAccessDataFileTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -96,23 +96,23 @@ void readWithOffsetAndLengthShouldRead() throws Exception { } @Test - void readWhenOffsetIsBeyondEOFShouldThrowException() throws Exception { + void readWhenOffsetIsBeyondEOFShouldThrowException() { assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> this.file.read(257, 0)); } @Test - void readWhenOffsetIsBeyondEndOfSubsectionShouldThrowException() throws Exception { + void readWhenOffsetIsBeyondEndOfSubsectionShouldThrowException() { RandomAccessData subsection = this.file.getSubsection(0, 10); assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> subsection.read(11, 0)); } @Test - void readWhenOffsetPlusLengthGreaterThanEOFShouldThrowException() throws Exception { + void readWhenOffsetPlusLengthGreaterThanEOFShouldThrowException() { assertThatExceptionOfType(EOFException.class).isThrownBy(() -> this.file.read(256, 1)); } @Test - void readWhenOffsetPlusLengthGreaterThanEndOfSubsectionShouldThrowException() throws Exception { + void readWhenOffsetPlusLengthGreaterThanEndOfSubsectionShouldThrowException() { RandomAccessData subsection = this.file.getSubsection(0, 10); assertThatExceptionOfType(EOFException.class).isThrownBy(() -> subsection.read(10, 1)); } @@ -125,13 +125,13 @@ void inputStreamRead() throws Exception { } @Test - void inputStreamReadNullBytes() throws Exception { + void inputStreamReadNullBytes() { assertThatNullPointerException().isThrownBy(() -> this.inputStream.read(null)) .withMessage("Bytes must not be null"); } @Test - void inputStreamReadNullBytesWithOffset() throws Exception { + void inputStreamReadNullBytesWithOffset() { assertThatNullPointerException().isThrownBy(() -> this.inputStream.read(null, 0, 1)) .withMessage("Bytes must not be null"); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java index 13bfe6f83730..62f35f00102c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -97,7 +97,7 @@ public void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData cen } @Override - public void visitFileHeader(CentralDirectoryFileHeader fileHeader, int dataOffset) { + public void visitFileHeader(CentralDirectoryFileHeader fileHeader, long dataOffset) { this.headers.add(fileHeader.clone()); } @@ -121,7 +121,7 @@ public void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData cen } @Override - public void visitFileHeader(CentralDirectoryFileHeader fileHeader, int dataOffset) { + public void visitFileHeader(CentralDirectoryFileHeader fileHeader, long dataOffset) { this.invocations.add("visitFileHeader"); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/HandlerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/HandlerTests.java index 5c1f3fcd18f2..13950475bc9a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/HandlerTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/HandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -126,20 +126,20 @@ void hashCodesAreEqualForUrlsThatReferenceSameFileViaNestedArchiveAndFromRootOfJ @Test void urlWithSpecReferencingParentDirectory() throws MalformedURLException { - assertStandardAndCustomHandlerUrlsAreEqual("file:/test.jar!/BOOT-INF/classes!/xsd/folderA/a.xsd", - "../folderB/c/d/e.xsd"); + assertStandardAndCustomHandlerUrlsAreEqual("file:/test.jar!/BOOT-INF/classes!/xsd/directoryA/a.xsd", + "../directoryB/c/d/e.xsd"); } @Test void urlWithSpecReferencingAncestorDirectoryOutsideJarStopsAtJarRoot() throws MalformedURLException { - assertStandardAndCustomHandlerUrlsAreEqual("file:/test.jar!/BOOT-INF/classes!/xsd/folderA/a.xsd", - "../../../../../../folderB/b.xsd"); + assertStandardAndCustomHandlerUrlsAreEqual("file:/test.jar!/BOOT-INF/classes!/xsd/directoryA/a.xsd", + "../../../../../../directoryB/b.xsd"); } @Test void urlWithSpecReferencingCurrentDirectory() throws MalformedURLException { - assertStandardAndCustomHandlerUrlsAreEqual("file:/test.jar!/BOOT-INF/classes!/xsd/folderA/a.xsd", - "./folderB/c/d/e.xsd"); + assertStandardAndCustomHandlerUrlsAreEqual("file:/test.jar!/BOOT-INF/classes!/xsd/directoryA/a.xsd", + "./directoryB/c/d/e.xsd"); } @Test @@ -163,6 +163,7 @@ void fallbackToJdksJarUrlStreamHandler(@TempDir File tempDir) throws Exception { URLConnection jdkConnection = new URL(null, "jar:file:" + testJar.toURI().toURL() + "!/nested.jar!/", this.handler).openConnection(); assertThat(jdkConnection).isNotInstanceOf(JarURLConnection.class); + assertThat(jdkConnection.getClass().getName()).endsWith(".JarURLConnection"); } @Test @@ -171,11 +172,8 @@ void whenJarHasAPlusInItsPathConnectionJarFileMatchesOriginalJarFile(@TempDir Fi TestJarCreator.createTestJar(testJar); URL url = new URL(null, "jar:" + testJar.toURI().toURL() + "!/nested.jar!/3.dat", this.handler); JarURLConnection connection = (JarURLConnection) url.openConnection(); - try { - assertThat(connection.getJarFile().getRootJarFile().getFile()).isEqualTo(testJar); - } - finally { - connection.getJarFile().close(); + try (JarFile jarFile = JarFileWrapper.unwrap(connection.getJarFile())) { + assertThat(jarFile.getRootJarFile().getFile()).isEqualTo(testJar); } } @@ -185,11 +183,8 @@ void whenJarHasASpaceInItsPathConnectionJarFileMatchesOriginalJarFile(@TempDir F TestJarCreator.createTestJar(testJar); URL url = new URL(null, "jar:" + testJar.toURI().toURL() + "!/nested.jar!/3.dat", this.handler); JarURLConnection connection = (JarURLConnection) url.openConnection(); - try { - assertThat(connection.getJarFile().getRootJarFile().getFile()).isEqualTo(testJar); - } - finally { - connection.getJarFile().close(); + try (JarFile jarFile = JarFileWrapper.unwrap(connection.getJarFile())) { + assertThat(jarFile.getRootJarFile().getFile()).isEqualTo(testJar); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java index 9ec200da7efe..4363d5abc587 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,18 +28,27 @@ import java.net.URLClassLoader; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.attribute.FileTime; +import java.time.Instant; +import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; +import java.util.Iterator; import java.util.List; +import java.util.Random; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; +import java.util.stream.Stream; import java.util.zip.CRC32; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; +import org.assertj.core.api.ThrowableAssert.ThrowingCallable; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -48,13 +57,15 @@ import org.springframework.boot.loader.TestJarCreator; import org.springframework.boot.loader.data.RandomAccessDataFile; import org.springframework.util.FileCopyUtils; +import org.springframework.util.StopWatch; import org.springframework.util.StreamUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIOException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; /** * Tests for {@link JarFile}. @@ -62,6 +73,7 @@ * @author Phillip Webb * @author Martin Lau * @author Andy Wilkinson + * @author Madhura Bhave */ @ExtendWith(JarUrlProtocolHandler.class) class JarFileTests { @@ -167,6 +179,12 @@ void getJarEntry() { assertThat(entry.getName()).isEqualTo("1.dat"); } + @Test + void getJarEntryWhenClosed() throws Exception { + this.jarFile.close(); + assertThatZipFileClosedIsThrownBy(() -> this.jarFile.getJarEntry("1.dat")); + } + @Test void getInputStream() throws Exception { InputStream inputStream = this.jarFile.getInputStream(this.jarFile.getEntry("1.dat")); @@ -176,23 +194,42 @@ void getInputStream() throws Exception { assertThat(inputStream.read()).isEqualTo(-1); } + @Test + void getInputStreamWhenClosed() throws Exception { + ZipEntry entry = this.jarFile.getEntry("1.dat"); + this.jarFile.close(); + assertThatZipFileClosedIsThrownBy(() -> this.jarFile.getInputStream(entry)); + } + @Test void getComment() { assertThat(this.jarFile.getComment()).isEqualTo("outer"); } + @Test + void getCommentWhenClosed() throws Exception { + this.jarFile.close(); + assertThatZipFileClosedIsThrownBy(() -> this.jarFile.getComment()); + } + @Test void getName() { assertThat(this.jarFile.getName()).isEqualTo(this.rootJarFile.getPath()); } @Test - void getSize() throws Exception { + void size() throws Exception { try (ZipFile zip = new ZipFile(this.rootJarFile)) { assertThat(this.jarFile.size()).isEqualTo(zip.size()); } } + @Test + void sizeWhenClosed() throws Exception { + this.jarFile.close(); + assertThatZipFileClosedIsThrownBy(() -> this.jarFile.size()); + } + @Test void getEntryTime() throws Exception { java.util.jar.JarFile jdkJarFile = new java.util.jar.JarFile(this.rootJarFile); @@ -206,7 +243,7 @@ void close() throws Exception { RandomAccessDataFile randomAccessDataFile = spy(new RandomAccessDataFile(this.rootJarFile)); JarFile jarFile = new JarFile(randomAccessDataFile); jarFile.close(); - verify(randomAccessDataFile).close(); + then(randomAccessDataFile).should().close(); } @Test @@ -214,10 +251,10 @@ void getUrl() throws Exception { URL url = this.jarFile.getUrl(); assertThat(url.toString()).isEqualTo("jar:" + this.rootJarFile.toURI() + "!/"); JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection(); - assertThat(jarURLConnection.getJarFile()).isSameAs(this.jarFile); + assertThat(JarFileWrapper.unwrap(jarURLConnection.getJarFile())).isSameAs(this.jarFile); assertThat(jarURLConnection.getJarEntry()).isNull(); assertThat(jarURLConnection.getContentLength()).isGreaterThan(1); - assertThat(jarURLConnection.getContent()).isSameAs(this.jarFile); + assertThat(JarFileWrapper.unwrap((java.util.jar.JarFile) jarURLConnection.getContent())).isSameAs(this.jarFile); assertThat(jarURLConnection.getContentType()).isEqualTo("x-java/jar"); assertThat(jarURLConnection.getJarFileURL().toURI()).isEqualTo(this.rootJarFile.toURI()); } @@ -227,7 +264,7 @@ void createEntryUrl() throws Exception { URL url = new URL(this.jarFile.getUrl(), "1.dat"); assertThat(url.toString()).isEqualTo("jar:" + this.rootJarFile.toURI() + "!/1.dat"); JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection(); - assertThat(jarURLConnection.getJarFile()).isSameAs(this.jarFile); + assertThat(JarFileWrapper.unwrap(jarURLConnection.getJarFile())).isSameAs(this.jarFile); assertThat(jarURLConnection.getJarEntry()).isSameAs(this.jarFile.getJarEntry("1.dat")); assertThat(jarURLConnection.getContentLength()).isEqualTo(1); assertThat(jarURLConnection.getContent()).isInstanceOf(InputStream.class); @@ -281,7 +318,7 @@ void getNestedJarFile() throws Exception { URL url = nestedJarFile.getUrl(); assertThat(url.toString()).isEqualTo("jar:" + this.rootJarFile.toURI() + "!/nested.jar!/"); JarURLConnection conn = (JarURLConnection) url.openConnection(); - assertThat(conn.getJarFile()).isSameAs(nestedJarFile); + assertThat(JarFileWrapper.unwrap(conn.getJarFile())).isSameAs(nestedJarFile); assertThat(conn.getJarFileURL().toString()).isEqualTo("jar:" + this.rootJarFile.toURI() + "!/nested.jar"); assertThat(conn.getInputStream()).isNotNull(); JarInputStream jarInputStream = new JarInputStream(conn.getInputStream()); @@ -310,7 +347,8 @@ void getNestedJarDirectory() throws Exception { URL url = nestedJarFile.getUrl(); assertThat(url.toString()).isEqualTo("jar:" + this.rootJarFile.toURI() + "!/d!/"); - assertThat(((JarURLConnection) url.openConnection()).getJarFile()).isSameAs(nestedJarFile); + JarURLConnection connection = (JarURLConnection) url.openConnection(); + assertThat(JarFileWrapper.unwrap(connection.getJarFile())).isSameAs(nestedJarFile); } } @@ -392,29 +430,36 @@ void sensibleToString() throws Exception { @Test void verifySignedJar() throws Exception { - String classpath = System.getProperty("java.class.path"); - String[] entries = classpath.split(System.getProperty("path.separator")); - String signedJarFile = null; - for (String entry : entries) { - if (entry.contains("bcprov")) { - signedJarFile = entry; + File signedJarFile = getSignedJarFile(); + assertThat(signedJarFile).exists(); + try (java.util.jar.JarFile expected = new java.util.jar.JarFile(signedJarFile)) { + try (JarFile actual = new JarFile(signedJarFile)) { + StopWatch stopWatch = new StopWatch(); + Enumeration actualEntries = actual.entries(); + while (actualEntries.hasMoreElements()) { + JarEntry actualEntry = actualEntries.nextElement(); + java.util.jar.JarEntry expectedEntry = expected.getJarEntry(actualEntry.getName()); + StreamUtils.drain(expected.getInputStream(expectedEntry)); + if (!actualEntry.getName().equals("META-INF/MANIFEST.MF")) { + assertThat(actualEntry.getCertificates()).as(actualEntry.getName()) + .isEqualTo(expectedEntry.getCertificates()); + assertThat(actualEntry.getCodeSigners()).as(actualEntry.getName()) + .isEqualTo(expectedEntry.getCodeSigners()); + } + } + assertThat(stopWatch.getTotalTimeSeconds()).isLessThan(3.0); } } - assertThat(signedJarFile).isNotNull(); - java.util.jar.JarFile jarFile = new JarFile(new File(signedJarFile)); - jarFile.getManifest(); - Enumeration jarEntries = jarFile.entries(); - while (jarEntries.hasMoreElements()) { - JarEntry jarEntry = jarEntries.nextElement(); - InputStream inputStream = jarFile.getInputStream(jarEntry); - inputStream.skip(Long.MAX_VALUE); - inputStream.close(); - if (!jarEntry.getName().startsWith("META-INF") && !jarEntry.isDirectory() - && !jarEntry.getName().endsWith("TigerDigest.class")) { - assertThat(jarEntry.getCertificates()).isNotNull(); + } + + private File getSignedJarFile() { + String[] entries = System.getProperty("java.class.path").split(System.getProperty("path.separator")); + for (String entry : entries) { + if (entry.contains("bcprov")) { + return new File(entry); } } - jarFile.close(); + return null; } @Test @@ -520,7 +565,7 @@ void multiReleaseEntry() throws Exception { } @Test - void zip64JarCanBeRead() throws Exception { + void zip64JarThatExceedsZipEntryLimitCanBeRead() throws Exception { File zip64Jar = new File(this.tempDir, "zip64.jar"); FileCopyUtils.copy(zip64Jar(), zip64Jar); try (JarFile zip64JarFile = new JarFile(zip64Jar)) { @@ -529,10 +574,42 @@ void zip64JarCanBeRead() throws Exception { for (int i = 0; i < entries.size(); i++) { JarEntry entry = entries.get(i); InputStream entryInput = zip64JarFile.getInputStream(entry); - String contents = StreamUtils.copyToString(entryInput, StandardCharsets.UTF_8); - assertThat(contents).isEqualTo("Entry " + (i + 1)); + assertThat(entryInput).hasContent("Entry " + (i + 1)); + } + } + } + + @Test + void zip64JarThatExceedsZipSizeLimitCanBeRead() throws Exception { + Assumptions.assumeTrue(this.tempDir.getFreeSpace() > 6 * 1024 * 1024 * 1024, "Insufficient disk space"); + File zip64Jar = new File(this.tempDir, "zip64.jar"); + File entry = new File(this.tempDir, "entry.dat"); + CRC32 crc32 = new CRC32(); + try (FileOutputStream entryOut = new FileOutputStream(entry)) { + byte[] data = new byte[1024 * 1024]; + new Random().nextBytes(data); + for (int i = 0; i < 1024; i++) { + entryOut.write(data); + crc32.update(data); + } + } + try (JarOutputStream jarOutput = new JarOutputStream(new FileOutputStream(zip64Jar))) { + for (int i = 0; i < 6; i++) { + JarEntry storedEntry = new JarEntry("huge-" + i); + storedEntry.setSize(entry.length()); + storedEntry.setCompressedSize(entry.length()); + storedEntry.setCrc(crc32.getValue()); + storedEntry.setMethod(ZipEntry.STORED); + jarOutput.putNextEntry(storedEntry); + try (FileInputStream entryIn = new FileInputStream(entry)) { + StreamUtils.copy(entryIn, jarOutput); + } + jarOutput.closeEntry(); } } + try (JarFile zip64JarFile = new JarFile(zip64Jar)) { + assertThat(Collections.list(zip64JarFile.entries())).hasSize(6); + } } @Test @@ -559,8 +636,7 @@ void nestedZip64JarCanBeRead() throws Exception { for (int i = 0; i < entries.size(); i++) { JarEntry entry = entries.get(i); InputStream entryInput = nestedZip64JarFile.getInputStream(entry); - String contents = StreamUtils.copyToString(entryInput, StandardCharsets.UTF_8); - assertThat(contents).isEqualTo("Entry " + (i + 1)); + assertThat(entryInput).hasContent("Entry " + (i + 1)); } } } @@ -578,6 +654,84 @@ private byte[] zip64Jar() throws IOException { return bytes.toByteArray(); } + @Test + void jarFileEntryWithEpochTimeOfZeroShouldNotFail() throws Exception { + File file = createJarFileWithEpochTimeOfZero(); + try (JarFile jar = new JarFile(file)) { + Enumeration entries = jar.entries(); + JarEntry entry = entries.nextElement(); + assertThat(entry.getLastModifiedTime().toInstant()).isEqualTo(Instant.EPOCH); + assertThat(entry.getName()).isEqualTo("1.dat"); + } + } + + private File createJarFileWithEpochTimeOfZero() throws Exception { + File jarFile = new File(this.tempDir, "temp.jar"); + FileOutputStream fileOutputStream = new FileOutputStream(jarFile); + String comment = "outer"; + try (JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream)) { + jarOutputStream.setComment(comment); + JarEntry entry = new JarEntry("1.dat"); + entry.setLastModifiedTime(FileTime.from(Instant.EPOCH)); + jarOutputStream.putNextEntry(entry); + jarOutputStream.write(new byte[] { (byte) 1 }); + jarOutputStream.closeEntry(); + } + + byte[] data = Files.readAllBytes(jarFile.toPath()); + int headerPosition = data.length - ZipFile.ENDHDR - comment.getBytes().length; + int centralHeaderPosition = (int) Bytes.littleEndianValue(data, headerPosition + ZipFile.ENDOFF, 1); + int localHeaderPosition = (int) Bytes.littleEndianValue(data, centralHeaderPosition + ZipFile.CENOFF, 1); + writeTimeBlock(data, centralHeaderPosition + ZipFile.CENTIM, 0); + writeTimeBlock(data, localHeaderPosition + ZipFile.LOCTIM, 0); + + File jar = new File(this.tempDir, "zerotimed.jar"); + Files.write(jar.toPath(), data); + return jar; + } + + private static void writeTimeBlock(byte[] data, int pos, int value) { + data[pos] = (byte) (value & 0xff); + data[pos + 1] = (byte) ((value >> 8) & 0xff); + data[pos + 2] = (byte) ((value >> 16) & 0xff); + data[pos + 3] = (byte) ((value >> 24) & 0xff); + } + + @Test + void iterator() { + Iterator iterator = this.jarFile.iterator(); + List names = new ArrayList<>(); + while (iterator.hasNext()) { + names.add(iterator.next().getName()); + } + assertThat(names).hasSize(12).contains("1.dat"); + } + + @Test + void iteratorWhenClosed() throws IOException { + this.jarFile.close(); + assertThatZipFileClosedIsThrownBy(() -> this.jarFile.iterator()); + } + + @Test + void iteratorWhenClosedLater() throws IOException { + Iterator iterator = this.jarFile.iterator(); + iterator.next(); + this.jarFile.close(); + assertThatZipFileClosedIsThrownBy(() -> iterator.hasNext()); + } + + @Test + void stream() { + Stream stream = this.jarFile.stream().map(JarEntry::getName); + assertThat(stream).hasSize(12).contains("1.dat"); + + } + + private void assertThatZipFileClosedIsThrownBy(ThrowingCallable throwingCallable) { + assertThatIllegalStateException().isThrownBy(throwingCallable).withMessage("zip file closed"); + } + private int getJavaVersion() { try { Object runtimeVersion = Runtime.class.getMethod("version").invoke(null); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileWrapperTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileWrapperTests.java new file mode 100644 index 000000000000..7da6716b00a9 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileWrapperTests.java @@ -0,0 +1,281 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.jar; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.Permission; +import java.util.EnumSet; +import java.util.Enumeration; +import java.util.Set; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.jar.JarFileWrapperTests.SpyJarFile.Call; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link JarFileWrapper}. + * + * @author Phillip Webb + */ +class JarFileWrapperTests { + + private SpyJarFile parent; + + private JarFileWrapper wrapper; + + @BeforeEach + void setup(@TempDir File temp) throws Exception { + this.parent = new SpyJarFile(createTempJar(temp)); + this.wrapper = new JarFileWrapper(this.parent); + } + + @AfterEach + void cleanup() throws Exception { + this.parent.close(); + } + + private File createTempJar(File temp) throws IOException { + File file = new File(temp, "temp.jar"); + new JarOutputStream(new FileOutputStream(file)).close(); + return file; + } + + @Test + void getUrlDelegatesToParent() throws MalformedURLException { + this.wrapper.getUrl(); + this.parent.verify(Call.GET_URL); + } + + @Test + void getTypeDelegatesToParent() { + this.wrapper.getType(); + this.parent.verify(Call.GET_TYPE); + } + + @Test + void getPermissionDelegatesToParent() { + this.wrapper.getPermission(); + this.parent.verify(Call.GET_PERMISSION); + } + + @Test + void getManifestDelegatesToParent() throws IOException { + this.wrapper.getManifest(); + this.parent.verify(Call.GET_MANIFEST); + } + + @Test + void entriesDelegatesToParent() { + this.wrapper.entries(); + this.parent.verify(Call.ENTRIES); + } + + @Test + void getJarEntryDelegatesToParent() { + this.wrapper.getJarEntry("test"); + this.parent.verify(Call.GET_JAR_ENTRY); + } + + @Test + void getEntryDelegatesToParent() { + this.wrapper.getEntry("test"); + this.parent.verify(Call.GET_ENTRY); + } + + @Test + void getInputStreamDelegatesToParent() throws IOException { + this.wrapper.getInputStream(); + this.parent.verify(Call.GET_INPUT_STREAM); + } + + @Test + void getEntryInputStreamDelegatesToParent() throws IOException { + ZipEntry entry = new ZipEntry("test"); + this.wrapper.getInputStream(entry); + this.parent.verify(Call.GET_ENTRY_INPUT_STREAM); + } + + @Test + void getCommentDelegatesToParent() { + this.wrapper.getComment(); + this.parent.verify(Call.GET_COMMENT); + } + + @Test + void sizeDelegatesToParent() { + this.wrapper.size(); + this.parent.verify(Call.SIZE); + } + + @Test + void toStringDelegatesToParent() { + assertThat(this.wrapper.toString()).endsWith("temp.jar"); + } + + @Test // gh-22991 + void wrapperMustNotImplementClose() { + // If the wrapper overrides close then on Java 11 a FinalizableResource + // instance will be used to perform cleanup. This can result in a lot + // of additional memory being used since cleanup only occurs when the + // finalizer thread runs. See gh-22991 + assertThatExceptionOfType(NoSuchMethodException.class) + .isThrownBy(() -> JarFileWrapper.class.getDeclaredMethod("close")); + } + + @Test + void streamDelegatesToParent() { + this.wrapper.stream(); + this.parent.verify(Call.STREAM); + } + + /** + * {@link JarFile} that we can spy (even on Java 11+) + */ + static class SpyJarFile extends JarFile { + + private final Set calls = EnumSet.noneOf(Call.class); + + SpyJarFile(File file) throws IOException { + super(file); + } + + @Override + Permission getPermission() { + mark(Call.GET_PERMISSION); + return super.getPermission(); + } + + @Override + public Manifest getManifest() throws IOException { + mark(Call.GET_MANIFEST); + return super.getManifest(); + } + + @Override + public Enumeration entries() { + mark(Call.ENTRIES); + return super.entries(); + } + + @Override + public Stream stream() { + mark(Call.STREAM); + return super.stream(); + } + + @Override + public JarEntry getJarEntry(String name) { + mark(Call.GET_JAR_ENTRY); + return super.getJarEntry(name); + } + + @Override + public ZipEntry getEntry(String name) { + mark(Call.GET_ENTRY); + return super.getEntry(name); + } + + @Override + InputStream getInputStream() throws IOException { + mark(Call.GET_INPUT_STREAM); + return super.getInputStream(); + } + + @Override + InputStream getInputStream(String name) throws IOException { + mark(Call.GET_ENTRY_INPUT_STREAM); + return super.getInputStream(name); + } + + @Override + public String getComment() { + mark(Call.GET_COMMENT); + return super.getComment(); + } + + @Override + public int size() { + mark(Call.SIZE); + return super.size(); + } + + @Override + public URL getUrl() throws MalformedURLException { + mark(Call.GET_URL); + return super.getUrl(); + } + + @Override + JarFileType getType() { + mark(Call.GET_TYPE); + return super.getType(); + } + + private void mark(Call call) { + this.calls.add(call); + } + + void verify(Call call) { + assertThat(call).matches(this.calls::contains); + } + + enum Call { + + GET_URL, + + GET_TYPE, + + GET_PERMISSION, + + GET_MANIFEST, + + ENTRIES, + + GET_JAR_ENTRY, + + GET_ENTRY, + + GET_INPUT_STREAM, + + GET_ENTRY_INPUT_STREAM, + + GET_COMMENT, + + SIZE, + + STREAM + + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java index e6267ede61b1..c01259c8ba28 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,13 @@ package org.springframework.boot.loader.jar; -import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.InputStream; import java.net.URL; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.stream.Collectors; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -61,20 +63,22 @@ void tearDown() throws Exception { @Test void connectionToRootUsingAbsoluteUrl() throws Exception { URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/"); - assertThat(JarURLConnection.get(url, this.jarFile).getContent()).isSameAs(this.jarFile); + Object content = JarURLConnection.get(url, this.jarFile).getContent(); + assertThat(JarFileWrapper.unwrap((java.util.jar.JarFile) content)).isSameAs(this.jarFile); } @Test void connectionToRootUsingRelativeUrl() throws Exception { URL url = new URL("jar:file:" + getRelativePath() + "!/"); - assertThat(JarURLConnection.get(url, this.jarFile).getContent()).isSameAs(this.jarFile); + Object content = JarURLConnection.get(url, this.jarFile).getContent(); + assertThat(JarFileWrapper.unwrap((java.util.jar.JarFile) content)).isSameAs(this.jarFile); } @Test void connectionToEntryUsingAbsoluteUrl() throws Exception { URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/1.dat"); try (InputStream input = JarURLConnection.get(url, this.jarFile).getInputStream()) { - assertThat(input).hasSameContentAs(new ByteArrayInputStream(new byte[] { 1 })); + assertThat(input).hasBinaryContent(new byte[] { 1 }); } } @@ -82,7 +86,7 @@ void connectionToEntryUsingAbsoluteUrl() throws Exception { void connectionToEntryUsingRelativeUrl() throws Exception { URL url = new URL("jar:file:" + getRelativePath() + "!/1.dat"); try (InputStream input = JarURLConnection.get(url, this.jarFile).getInputStream()) { - assertThat(input).hasSameContentAs(new ByteArrayInputStream(new byte[] { 1 })); + assertThat(input).hasBinaryContent(new byte[] { 1 }); } } @@ -90,7 +94,7 @@ void connectionToEntryUsingRelativeUrl() throws Exception { void connectionToEntryUsingAbsoluteUrlWithFileColonSlashSlashPrefix() throws Exception { URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/1.dat"); try (InputStream input = JarURLConnection.get(url, this.jarFile).getInputStream()) { - assertThat(input).hasSameContentAs(new ByteArrayInputStream(new byte[] { 1 })); + assertThat(input).hasBinaryContent(new byte[] { 1 }); } } @@ -99,7 +103,7 @@ void connectionToEntryUsingAbsoluteUrlForNestedEntry() throws Exception { URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/nested.jar!/3.dat"); JarURLConnection connection = JarURLConnection.get(url, this.jarFile); try (InputStream input = connection.getInputStream()) { - assertThat(input).hasSameContentAs(new ByteArrayInputStream(new byte[] { 3 })); + assertThat(input).hasBinaryContent(new byte[] { 3 }); } connection.getJarFile().close(); } @@ -109,7 +113,7 @@ void connectionToEntryUsingRelativeUrlForNestedEntry() throws Exception { URL url = new URL("jar:file:" + getRelativePath() + "!/nested.jar!/3.dat"); JarURLConnection connection = JarURLConnection.get(url, this.jarFile); try (InputStream input = connection.getInputStream()) { - assertThat(input).hasSameContentAs(new ByteArrayInputStream(new byte[] { 3 })); + assertThat(input).hasBinaryContent(new byte[] { 3 }); } connection.getJarFile().close(); } @@ -119,7 +123,7 @@ void connectionToEntryUsingAbsoluteUrlForEntryFromNestedJarFile() throws Excepti URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/nested.jar!/3.dat"); try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { try (InputStream input = JarURLConnection.get(url, nested).getInputStream()) { - assertThat(input).hasSameContentAs(new ByteArrayInputStream(new byte[] { 3 })); + assertThat(input).hasBinaryContent(new byte[] { 3 }); } } } @@ -129,7 +133,7 @@ void connectionToEntryUsingRelativeUrlForEntryFromNestedJarFile() throws Excepti URL url = new URL("jar:file:" + getRelativePath() + "!/nested.jar!/3.dat"); try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { try (InputStream input = JarURLConnection.get(url, nested).getInputStream()) { - assertThat(input).hasSameContentAs(new ByteArrayInputStream(new byte[] { 3 })); + assertThat(input).hasBinaryContent(new byte[] { 3 }); } } } @@ -140,7 +144,7 @@ void connectionToEntryInNestedJarFromUrlThatUsesExistingUrlAsContext() throws Ex "/3.dat"); try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { try (InputStream input = JarURLConnection.get(url, nested).getInputStream()) { - assertThat(input).hasSameContentAs(new ByteArrayInputStream(new byte[] { 3 })); + assertThat(input).hasBinaryContent(new byte[] { 3 }); } } } @@ -150,7 +154,7 @@ void connectionToEntryWithSpaceNestedEntry() throws Exception { URL url = new URL("jar:file:" + getRelativePath() + "!/space nested.jar!/3.dat"); JarURLConnection connection = JarURLConnection.get(url, this.jarFile); try (InputStream input = connection.getInputStream()) { - assertThat(input).hasSameContentAs(new ByteArrayInputStream(new byte[] { 3 })); + assertThat(input).hasBinaryContent(new byte[] { 3 }); } connection.getJarFile().close(); } @@ -160,7 +164,7 @@ void connectionToEntryWithEncodedSpaceNestedEntry() throws Exception { URL url = new URL("jar:file:" + getRelativePath() + "!/space%20nested.jar!/3.dat"); JarURLConnection connection = JarURLConnection.get(url, this.jarFile); try (InputStream input = connection.getInputStream()) { - assertThat(input).hasSameContentAs(new ByteArrayInputStream(new byte[] { 3 })); + assertThat(input).hasBinaryContent(new byte[] { 3 }); } connection.getJarFile().close(); } @@ -199,6 +203,14 @@ void getLastModifiedReturnsLastModifiedTimeOfJarEntry() throws Exception { assertThat(connection.getLastModified()).isEqualTo(connection.getJarEntry().getTime()); } + @Test + void entriesCanBeStreamedFromJarFileOfConnection() throws Exception { + URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/"); + JarURLConnection connection = JarURLConnection.get(url, this.jarFile); + List entryNames = connection.getJarFile().stream().map(JarEntry::getName).collect(Collectors.toList()); + assertThat(entryNames).hasSize(12); + } + @Test void jarEntryBasicName() { assertThat(new JarEntryName(new StringSequence("a/b/C.class")).toString()).isEqualTo("a/b/C.class"); @@ -220,6 +232,15 @@ void jarEntryNameWithMixtureOfEncodedAndUnencodedDoubleByteCharacters() { .isEqualTo("\u00e1/b/\u00c7.class"); } + @Test + void openConnectionCanBeClosedWithoutClosingSourceJar() throws Exception { + URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/"); + JarURLConnection connection = JarURLConnection.get(url, this.jarFile); + java.util.jar.JarFile connectionJarFile = connection.getJarFile(); + connectionJarFile.close(); + assertThat(this.jarFile.isClosed()).isFalse(); + } + private String getRelativePath() { return this.rootJarFile.getPath().replace('\\', '/'); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/StringSequenceTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/StringSequenceTests.java index fddd5ee72a2f..9226613c2d59 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/StringSequenceTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/StringSequenceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -203,4 +203,18 @@ void startsWithOffsetWhenShorterAndDoesNotStartWith() { assertThat(new StringSequence("xab").startsWith("c", 1)).isFalse(); } + @Test + void startsWithOnSubstringTailWhenMatch() { + StringSequence subSequence = new StringSequence("xabc").subSequence(1); + assertThat(subSequence.startsWith("abc")).isTrue(); + assertThat(subSequence.startsWith("abcd")).isFalse(); + } + + @Test + void startsWithOnSubstringMiddleWhenMatch() { + StringSequence subSequence = new StringSequence("xabc").subSequence(1, 3); + assertThat(subSequence.startsWith("ab")).isTrue(); + assertThat(subSequence.startsWith("abc")).isFalse(); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java new file mode 100644 index 000000000000..93179bad6fe2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.jarmode; + +import java.util.Collections; +import java.util.Iterator; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.loader.Launcher; +import org.springframework.boot.loader.archive.Archive; +import org.springframework.boot.testsupport.system.CapturedOutput; +import org.springframework.boot.testsupport.system.OutputCaptureExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Launcher} with jar mode support. + * + * @author Phillip Webb + */ +@ExtendWith(OutputCaptureExtension.class) +class LauncherJarModeTests { + + @BeforeEach + void setup() { + System.setProperty(JarModeLauncher.DISABLE_SYSTEM_EXIT, "true"); + } + + @AfterEach + void cleanup() { + System.clearProperty("jarmode"); + System.clearProperty(JarModeLauncher.DISABLE_SYSTEM_EXIT); + } + + @Test + void launchWhenJarModePropertyIsSetLaunchesJarMode(CapturedOutput out) throws Exception { + System.setProperty("jarmode", "test"); + new TestLauncher().launch(new String[] { "boot" }); + assertThat(out).contains("running in test jar mode [boot]"); + } + + @Test + void launchWhenJarModePropertyIsNotAcceptedThrowsException(CapturedOutput out) throws Exception { + System.setProperty("jarmode", "idontexist"); + new TestLauncher().launch(new String[] { "boot" }); + assertThat(out).contains("Unsupported jarmode 'idontexist'"); + } + + private static class TestLauncher extends Launcher { + + @Override + protected String getMainClass() throws Exception { + throw new IllegalStateException("Should not be called"); + } + + @Override + protected Iterator getClassPathArchivesIterator() throws Exception { + return Collections.emptyIterator(); + } + + @Override + protected void launch(String[] args) throws Exception { + super.launch(args); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/META-INF/spring.factories new file mode 100644 index 000000000000..a140aabf1739 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.loader.jarmode.JarMode=\ +org.springframework.boot.loader.jarmode.TestJarMode \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/explodedsample/ExampleClass.txt b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/explodedsample/ExampleClass.txt new file mode 100644 index 000000000000..c53100f90fa1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/explodedsample/ExampleClass.txt @@ -0,0 +1,26 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package explodedsample; + +/** + * Example class used to test class loading. + * + * @author Phillip Webb + */ +public class ExampleClass { + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/nested-jars/nested-jar-app.jar b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/nested-jars/nested-jar-app.jar new file mode 100644 index 000000000000..4c2254f6352b Binary files /dev/null and b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/nested-jars/nested-jar-app.jar differ diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/org/springframework/boot/loader/classpath-index-file.idx b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/org/springframework/boot/loader/classpath-index-file.idx new file mode 100644 index 000000000000..b84b99a6b47e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/org/springframework/boot/loader/classpath-index-file.idx @@ -0,0 +1,5 @@ +- "BOOT-INF/layers/one/lib/a.jar" +- "BOOT-INF/layers/one/lib/b.jar" +- "BOOT-INF/layers/one/lib/c.jar" +- "BOOT-INF/layers/two/lib/d.jar" +- "BOOT-INF/layers/two/lib/e.jar" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/build.gradle new file mode 100644 index 000000000000..a8e7b881fb10 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/build.gradle @@ -0,0 +1,138 @@ +plugins { + id "org.asciidoctor.jvm.convert" + id "org.springframework.boot.conventions" + id "org.springframework.boot.maven-plugin" + id "org.springframework.boot.optional-dependencies" +} + +description = "Spring Boot Maven Plugin" + +configurations { + dependenciesBom + documentation +} + +repositories { + maven { + url "https://repo.spring.io/release" + mavenContent { + includeGroup "io.spring.asciidoctor" + includeGroup "io.spring.asciidoctor.backends" + } + } +} + +dependencies { + compileOnly("org.apache.maven.plugin-tools:maven-plugin-annotations") + compileOnly("org.sonatype.plexus:plexus-build-api") + + implementation(project(":spring-boot-project:spring-boot-tools:spring-boot-buildpack-platform")) + implementation(project(":spring-boot-project:spring-boot-tools:spring-boot-loader-tools")) + implementation("org.apache.maven.shared:maven-common-artifact-filters") { + exclude(group: "javax.enterprise", module: "cdi-api") + exclude(group: "javax.inject", module: "javax.inject") + } + implementation("org.apache.maven:maven-plugin-api") { + exclude(group: "javax.enterprise", module: "cdi-api") + exclude(group: "javax.inject", module: "javax.inject") + } + + intTestImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-buildpack-platform")) + intTestImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-loader-tools")) + intTestImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) + intTestImplementation("org.apache.maven.shared:maven-invoker") { + exclude(group: "javax.inject", module: "javax.inject") + } + intTestImplementation("org.assertj:assertj-core") + intTestImplementation("org.junit.jupiter:junit-jupiter") + intTestImplementation("org.testcontainers:testcontainers") + intTestImplementation("org.testcontainers:junit-jupiter") + + mavenOptionalImplementation("org.apache.maven.plugins:maven-shade-plugin") { + exclude(group: "javax.enterprise", module: "cdi-api") + exclude(group: "javax.inject", module: "javax.inject") + } + + runtimeOnly("org.sonatype.plexus:plexus-build-api") + + testImplementation("org.assertj:assertj-core") + testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.mockito:mockito-core") + testImplementation("org.mockito:mockito-junit-jupiter") + testImplementation("org.springframework:spring-core") + + versionProperties(project(path: ":spring-boot-project:spring-boot-dependencies", configuration: "effectiveBom")) +} + +syncDocumentationSourceForAsciidoctor { + from(documentPluginGoals) { + into "asciidoc/goals" + } +} + +sourceSets { + intTest { + output.dir("${buildDir}/generated-resources", builtBy: "extractVersionProperties") + } +} + +tasks.withType(org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask) { + doFirst { + def versionEl = version.split("\\.") + attributes "spring-boot-xsd-version": versionEl[0] + '.' + versionEl[1] + } +} + +asciidoctor { + sources { + include "index.adoc" + } +} + +task asciidoctorPdf(type: org.asciidoctor.gradle.jvm.AsciidoctorTask) { + sources { + include "index.adoc" + } +} + +syncDocumentationSourceForAsciidoctorPdf { + from(documentPluginGoals) { + into "asciidoc/goals" + } +} + +javadoc { + options { + author = true + docTitle = "Spring Boot Maven Plugin ${project.version} API" + encoding = "UTF-8" + memberLevel = "protected" + outputLevel = "quiet" + splitIndex = true + use = true + windowTitle = "Spring Boot Maven Plugin ${project.version} API" + } +} + +task zip(type: Zip) { + dependsOn asciidoctor, asciidoctorPdf + duplicatesStrategy "fail" + from(asciidoctorPdf.outputDir) { + into "reference/pdf" + rename "index.pdf", "${project.name}-reference.pdf" + } + from(asciidoctor.outputDir) { + into "reference/htmlsingle" + } + from(javadoc) { + into "api" + } +} + +prepareMavenBinaries { + versions "3.8.1", "3.6.3", "3.5.4" +} + +artifacts { + "documentation" zip +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/pom.xml deleted file mode 100644 index d762ca4d46c9..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/pom.xml +++ /dev/null @@ -1,303 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-tools - ${revision} - - spring-boot-maven-plugin - maven-plugin - Spring Boot Maven Plugin - Spring Boot Maven Plugin - - ${basedir}/../../.. - 3.3.9 - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - - org.springframework.boot - spring-boot-loader-tools - - - org.apache.maven - maven-archiver - - - org.apache.maven - maven-artifact - - - org.apache.maven - maven-core - - - javax.enterprise - cdi-api - - - - - org.apache.maven - maven-model - - - org.apache.maven - maven-plugin-api - - - org.apache.maven - maven-settings - - - org.apache.maven.shared - maven-common-artifact-filters - - - javax.enterprise - cdi-api - - - - - org.codehaus.plexus - plexus-archiver - - - org.codehaus.plexus - plexus-container-default - - - org.codehaus.plexus - plexus-component-api - - - - - org.codehaus.plexus - plexus-utils - - - org.sonatype.plexus - plexus-build-api - - - - org.apache.maven.plugins - maven-shade-plugin - true - - - - org.apache.maven.plugin-tools - maven-plugin-annotations - provided - - - - - - org.apache.maven.plugins - maven-jar-plugin - - - org.apache.maven.plugins - maven-plugin-plugin - - spring-boot - true - - - - generate-descriptor - - descriptor - - - - generated-helpmojo - - helpmojo - - - - - - - - - integration - - true - - - - - org.apache.maven.plugins - maven-invoker-plugin - - ${project.build.directory}/local-repo - - - - prepare-integration-test - pre-integration-test - - install - - - - integration-test - - run - - - ${project.build.directory}/it - src/it/settings.xml - verify - true - ${skipTests} - true - - - - - - org.apache.maven.plugins - maven-antrun-plugin - - - cleanup-local-integration-repo - pre-integration-test - - run - - - - - - - - - - - - - - - - full - - - full - - - - - - org.apache.maven.plugins - maven-site-plugin - - - generate-site - - jar - - - - - - org.apache.maven.plugins - maven-invoker-plugin - - ${project.build.directory}/local-repo - - - - prepare-integration-test - pre-integration-test - - install - - - - integration-test - - run - - - ${project.build.directory}/it - src/it/settings.xml - verify - true - ${skipTests} - true - - - - - - org.apache.maven.plugins - maven-antrun-plugin - - - cleanup-local-integration-repo - pre-integration-test - - run - - - - - - - - - - - - - - - - - org.apache.maven.plugins - maven-plugin-plugin - - - org.apache.maven.plugins - maven-project-info-reports-plugin - 2.9 - - - - index - cim - issue-tracking - license - scm - - - - - true - - - - - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/anchor-rewrite.properties b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/anchor-rewrite.properties new file mode 100644 index 000000000000..af45d43d6838 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/anchor-rewrite.properties @@ -0,0 +1,46 @@ + +build-info=? +getting-started=? +goals=? +help=? +spring-boot-maven-plugin-documentation=? +introduction=?.? +integration-tests=integration-tests +integration-tests-no-starter-parent=integration-tests.no-starter-parent +integration-tests-example=integration-tests.examples +integration-tests-example-random-port=integration-tests.examples.random-port +integration-tests-example-jmx-port=integration-tests.examples.jmx-port +integration-tests-example-skip=integration-tests.examples.skip +build-image=build-image +build-image-docker-daemon=build-image.docker-daemon +build-image-docker-registry=build-image.docker-registry +build-image-customization=build-image.customization +build-image-examples=build-image.examples +build-image-example-custom-image-builder=build-image.examples.custom-image-builder +build-image-example-builder-configuration=build-image.examples.builder-configuration +build-image-example-runtime-jvm-configuration=build-image.examples.runtime-jvm-configuration +build-image-example-custom-image-name=build-image.examples.custom-image-name +build-image-example-buildpacks=build-image.examples.buildpacks +build-image-example-publish=build-image.examples.publish +build-image-example-docker=build-image.examples.docker +repackage=packaging +repackage-layers=packaging.layers +repackage-layers-configuration=packaging.layers.configuration=repackage-examples=packaging.examples +repackage-example-custom-classifier=packaging.examples.custom-classifier +repackage-example-custom-name=packaging.examples.custom-name +repackage-example-local-artifact=packaging.examples.local-artifact +repackage-example-custom-layout=packaging.examples.custom-layout +repackage-example-exclude-dependency=packaging.examples.exclude-dependency +repackage-layered-archive-tools=packaging.examples.layered-archive-tools +repackage-layered-archive-additional-layers=packaging.examples.custom-layers-configuration +run=run +run-examples=run.examples +run-example-debug=run.examples.debug +run-example-system-properties=run.examples.system-properties +run-example-environment-variables=run.examples.environment-variables +run-example-application-arguments=run.examples.using-application-arguments +run-example-active-profiles=run.examples.specify-active-profiles +using=using +using-parent-pom=using.parent-pom +using-import=using.import +using-overriding-command-line=using.overriding-command-line diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/build-info.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/build-info.adoc new file mode 100644 index 000000000000..57eb95a810e2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/build-info.adoc @@ -0,0 +1,17 @@ +[[build-info]] += Integrating with Actuator +Spring Boot Actuator displays build-related information if a `META-INF/build-info.properties` file is present. +The `build-info` goal generates such file with the coordinates of the project and the build time. +It also allows you to add an arbitrary number of additional properties, as shown in the following example: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/build-info/pom.xml[tags=build-info] +---- + +This configuration will generate a `build-info.properties` at the expected location with four additional keys. + +NOTE: `maven.compiler.source` and `maven.compiler.target` are expected to be regular properties available in the project. +They will be interpolated as you would expect. + +include::goals/build-info.adoc[leveloffset=+1] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/getting-started.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/getting-started.adoc new file mode 100644 index 000000000000..f43ef23c0ef4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/getting-started.adoc @@ -0,0 +1,15 @@ +[[getting-started]] += Getting Started +To use the Spring Boot Maven Plugin, include the appropriate XML in the `plugins` section of your `pom.xml`, as shown in the following example: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/getting-started/pom.xml[tags=getting-started] +---- + +If you use a milestone or snapshot release, you also need to add the appropriate `pluginRepository` elements, as shown in the following listing: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/getting-started/plugin-repositories-pom.xml[tags=plugin-repositories] +---- diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/goals.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/goals.adoc new file mode 100644 index 000000000000..2ce66a85ddb6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/goals.adoc @@ -0,0 +1,5 @@ +[[goals]] += Goals +The Spring Boot Plugin has the following goals: + +include::goals/overview.adoc[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/help.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/help.adoc new file mode 100644 index 000000000000..a3ec8df55c51 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/help.adoc @@ -0,0 +1,5 @@ +[[help]] += Help Information +The `help` goal is a standard goal that displays information on the capabilities of the plugin. + +include::goals/help.adoc[leveloffset=+1] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/index.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/index.adoc new file mode 100644 index 000000000000..3e291430a2ef --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/index.adoc @@ -0,0 +1,43 @@ +[[spring-boot-maven-plugin-documentation]] += Spring Boot Maven Plugin Documentation +Stephane Nicoll; Andy Wilkinson; Scott Frederick +v{gradle-project-version} +:!version-label: +:doctype: book +:toc: left +:toclevels: 4 +:numbered: +:sectanchors: +:icons: font +:hide-uri-scheme: +:docinfo: shared,private +:attribute-missing: warn +:buildpacks-reference: https://buildpacks.io/docs +:spring-boot-docs: https://docs.spring.io/spring-boot/docs/{gradle-project-version} +:spring-boot-api: {spring-boot-docs}/api/org/springframework/boot +:spring-boot-reference: {spring-boot-docs}/reference/htmlsingle +:version-properties-appendix: {spring-boot-reference}/#dependency-versions-properties +:paketo-reference: https://paketo.io/docs +:paketo-java-reference: {paketo-reference}/buildpacks/language-family-buildpacks/java + + + +include::introduction.adoc[leveloffset=+1] + +include::getting-started.adoc[leveloffset=+1] + +include::using.adoc[leveloffset=+1] + +include::goals.adoc[leveloffset=+1] + +include::packaging.adoc[leveloffset=+1] + +include::packaging-oci-image.adoc[leveloffset=+1] + +include::running.adoc[leveloffset=+1] + +include::integration-tests.adoc[leveloffset=+1] + +include::build-info.adoc[leveloffset=+1] + +include::help.adoc[leveloffset=+1] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/integration-tests.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/integration-tests.adoc new file mode 100644 index 000000000000..8dd219a1aec8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/integration-tests.adoc @@ -0,0 +1,89 @@ +[[integration-tests]] += Running Integration Tests +While you may start your Spring Boot application very easily from your test (or test suite) itself, it may be desirable to handle that in the build itself. +To make sure that the lifecycle of your Spring Boot application is properly managed around your integration tests, you can use the `start` and `stop` goals, as shown in the following example: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/integration-tests/pom.xml[tags=integration-tests] +---- + +Such setup can now use the https://maven.apache.org/surefire/maven-failsafe-plugin[failsafe-plugin] to run your integration tests as you would expect. + +NOTE: By default, the application is started in a separate process and JMX is used to communicate with the application. +If you need to configure the JMX port, see <>. + +You could also configure a more advanced setup to skip the integration tests when a specific property has been set, see <>. + + + +[[integration-tests.no-starter-parent]] +== Using Failsafe Without Spring Boot's Parent POM +Spring Boot's Parent POM, `spring-boot-starter-parent`, configures Failsafe's `` to be `${project.build.outputDirectory}`. +Without this configuration, which causes Failsafe to use the compiled classes rather than the repackaged jar, Failsafe cannot load your application's classes. +If you are not using the parent POM, you should configure Failsafe in the same way, as shown in the following example: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/integration-tests/failsafe-pom.xml[tags=failsafe] +---- + +include::goals/start.adoc[leveloffset=+1] + +include::goals/stop.adoc[leveloffset=+1] + + + +[[integration-tests.examples]] +== Examples + + + +[[integration-tests.examples.random-port]] +=== Random Port for Integration Tests +One nice feature of the Spring Boot test integration is that it can allocate a free port for the web application. +When the `start` goal of the plugin is used, the Spring Boot application is started separately, making it difficult to pass the actual port to the integration test itself. + +The example below showcases how you could achieve the same feature using the https://www.mojohaus.org/build-helper-maven-plugin[Build Helper Maven Plugin]: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/integration-tests/random-port-pom.xml[tags=random-port] +---- + +You can now retrieve the `test.server.port` system property in any of your integration test to create a proper `URL` to the server. + + + +[[integration-tests.examples.jmx-port]] +=== Customize JMX port +The `jmxPort` property allows to customize the port the plugin uses to communicate with the Spring Boot application. + +This example shows how you can customize the port in case `9001` is already used: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/integration-tests/customize-jmx-port-pom.xml[tags=customize-jmx-port] +---- + +TIP: If you need to configure the JMX port, make sure to do so in the global configuration as shown above so that it is shared by both goals. + + + +[[integration-tests.examples.skip]] +=== Skip Integration Tests +The `skip` property allows to skip the execution of the Spring Boot maven plugin altogether. + +This example shows how you can skip integration tests with a command-line property and still make sure that the `repackage` goal runs: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/integration-tests/skip-integration-tests-pom.xml[tags=skip-integration-tests] +---- + +By default, the integration tests will run but this setup allows you to easily disable them on the command-line as follows: + +[indent=0] +---- + $ mvn verify -Dskip.it=true +---- diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/introduction.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/introduction.adoc new file mode 100644 index 000000000000..db6c0234a3e1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/introduction.adoc @@ -0,0 +1,4 @@ +[[introduction]] += Introduction +The Spring Boot Maven Plugin provides Spring Boot support in https://maven.org[Apache Maven]. +It allows you to package executable jar or war archives, run Spring Boot applications, generate build information and start your Spring Boot application prior to running integration tests. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc new file mode 100644 index 000000000000..24de20aaa1a6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc @@ -0,0 +1,355 @@ +[[build-image]] += Packaging OCI Images +The plugin can create an https://github.com/opencontainers/image-spec[OCI image] from a jar or war file using https://buildpacks.io/[Cloud Native Buildpacks] (CNB). +Images can be built using the `build-image` goal. + +NOTE: For security reasons, images build and run as non-root users. +See the {buildpacks-reference}/reference/spec/platform-api/#users[CNB specification] for more details. + +The easiest way to get started is to invoke `mvn spring-boot:build-image` on a project. +It is possible to automate the creation of an image whenever the `package` phase is invoked, as shown in the following example: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/packaging-oci-image/pom.xml[tags=packaging-oci-image] +---- + +TIP: While the buildpack runs from an <>, it is not necessary to execute the `repackage` goal first as the executable archive is created automatically if necessary. +When the `build-image` repackages the application, it applies the same settings as the `repackage` goal would, i.e. dependencies can be excluded using one of the exclude options, and Devtools is automatically excluded by default (you can control that using the `excludeDevtools` property). + + + +[[build-image.docker-daemon]] +== Docker Daemon +The `build-image` goal requires access to a Docker daemon. +By default, it will communicate with a Docker daemon over a local connection. +This works with https://docs.docker.com/install/[Docker Engine] on all supported platforms without configuration. + +Environment variables can be set to configure the `build-image` goal to use the https://minikube.sigs.k8s.io/docs/tasks/docker_daemon/[Docker daemon provided by minikube]. +The following table shows the environment variables and their values: + +|=== +| Environment variable | Description + +| DOCKER_HOST +| URL containing the host and port for the Docker daemon - e.g. `tcp://192.168.99.100:2376` + +| DOCKER_TLS_VERIFY +| Enable secure HTTPS protocol when set to `1` (optional) + +| DOCKER_CERT_PATH +| Path to certificate and key files for HTTPS (required if `DOCKER_TLS_VERIFY=1`, ignored otherwise) +|=== + +On Linux and macOS, these environment variables can be set using the command `eval $(minikube docker-env)` after minikube has been started. + +Docker daemon connection information can also be provided using `docker` parameters in the plugin configuration. +The following table summarizes the available parameters: + +|=== +| Parameter | Description + +| `host` +| URL containing the host and port for the Docker daemon - e.g. `tcp://192.168.99.100:2376` + +| `tlsVerify` +| Enable secure HTTPS protocol when set to `true` (optional) + +| `certPath` +| Path to certificate and key files for HTTPS (required if `tlsVerify` is `true`, ignored otherwise) +|=== + +For more details, see also <>. + + + +[[build-image.docker-registry]] +== Docker Registry +If the Docker images specified by the `builder` or `runImage` parameters are stored in a private Docker image registry that requires authentication, the authentication credentials can be provided using `docker.builderRegistry` parameters. + +If the generated Docker image is to be published to a Docker image registry, the authentication credentials can be provided using `docker.publishRegistry` parameters. + +Parameters are provided for user authentication or identity token authentication. +Consult the documentation for the Docker registry being used to store images for further information on supported authentication methods. + +The following table summarizes the available parameters for `docker.builderRegistry` and `docker.publishRegistry`: + +|=== +| Parameter | Description + +| `username` +| Username for the Docker image registry user. Required for user authentication. + +| `password` +| Password for the Docker image registry user. Required for user authentication. + +| `url` +| Address of the Docker image registry. Optional for user authentication. + +| `email` +| E-mail address for the Docker image registry user. Optional for user authentication. + +| `token` +| Identity token for the Docker image registry user. Required for token authentication. +|=== + +For more details, see also <>. + + + +[[build-image.customization]] +== Image Customizations +The plugin invokes a {buildpacks-reference}/concepts/components/builder/[builder] to orchestrate the generation of an image. +The builder includes multiple {buildpacks-reference}/concepts/components/buildpack[buildpacks] that can inspect the application to influence the generated image. +By default, the plugin chooses a builder image. +The name of the generated image is deduced from project properties. + +The `image` parameter allows configuration of the builder and how it should operate on the project. +The following table summarizes the available parameters and their default values: + +[cols="1,4,1"] +|=== +| Parameter / (User Property)| Description | Default value + +| `builder` + +(`spring-boot.build-image.builder`) +| Name of the Builder image to use. +| `paketobuildpacks/builder:base` + +| `runImage` + +(`spring-boot.build-image.runImage`) +| Name of the run image to use. +| No default value, indicating the run image specified in Builder metadata should be used. + +| `name` + +(`spring-boot.build-image.imageName`) +| {spring-boot-api}/buildpack/platform/docker/type/ImageReference.html#of-java.lang.String-[Image name] for the generated image. +| `docker.io/library/` + +`${project.artifactId}:${project.version}` + +| `pullPolicy` + +(`spring-boot.build-image.pullPolicy`) +| {spring-boot-api}/buildpack/platform/build/PullPolicy.html[Policy] used to determine when to pull the builder and run images from the registry. +Acceptable values are `ALWAYS`, `NEVER`, and `IF_NOT_PRESENT`. +| `ALWAYS` + +| `env` +| Environment variables that should be passed to the builder. +| + +| `buildpacks` +a|Buildpacks that the builder should use when building the image. +Only the specified buildpacks will be used, overriding the default buildpacks included in the builder. +Buildpack references must be in one of the following forms: + +* Buildpack in the builder - `[urn:cnb:builder:][@]` +* Buildpack in a directory on the file system - `[file://]` +* Buildpack in a gzipped tar (.tgz) file on the file system - `[file://]/` +* Buildpack in an OCI image - `[docker://]/[:][@]` +| None, indicating the builder should use the buildpacks included in it. + +| `bindings` +a|https://docs.docker.com/storage/bind-mounts/[Volume bind mounts] that should be mounted to the builder container when building the image. +The bindings will be passed unparsed and unvalidated to Docker when creating the builder container. +Bindings must be in one of the following forms: + +* `:[:]` +* `:[:]` + +Where `` can contain: + +* `ro` to mount the volume as read-only in the container +* `rw` to mount the volume as readable and writable in the container +* `volume-opt=key=value` to specify key-value pairs consisting of an option name and its value +| + +| `cleanCache` + +(`spring-boot.build-image.cleanCache`) +| Whether to clean the cache before building. +| `false` + +| `verboseLogging` +| Enables verbose logging of builder operations. +| `false` + +| `publish` + +(`spring-boot.build-image.publish`) +| Whether to publish the generated image to a Docker registry. +| `false` +|=== + +NOTE: The plugin detects the target Java compatibility of the project using the compiler's plugin configuration or the `maven.compiler.target` property. +When using the default Paketo builder and buildpacks, the plugin instructs the buildpacks to install the same Java version. +You can override this behaviour as shown in the <> examples. + +For more details, see also <>. + +include::goals/build-image.adoc[leveloffset=+1] + + + +[[build-image.examples]] +== Examples + + + +[[build-image.examples.custom-image-builder]] +=== Custom Image Builder +If you need to customize the builder used to create the image or the run image used to launch the built image, configure the plugin as shown in the following example: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/packaging-oci-image/custom-image-builder-pom.xml[tags=custom-image-builder] +---- + +This configuration will use a builder image with the name `mine/java-cnb-builder` and the tag `latest`, and the run image named `mine/java-cnb-run` and the tag `latest`. + +The builder and run image can be specified on the command line as well, as shown in this example: + +[indent=0] +---- + $ mvn spring-boot:build-image -Dspring-boot.build-image.builder=mine/java-cnb-builder -Dspring-boot.build-image.runImage=mine/java-cnb-run +---- + + + +[[build-image.examples.builder-configuration]] +=== Builder Configuration +If the builder exposes configuration options using environment variables, those can be set using the `env` attributes. + +The following is an example of {paketo-java-reference}/#configuring-the-jvm-version[configuring the JVM version] used by the Paketo Java buildpacks at build time: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/packaging-oci-image/build-image-example-builder-configuration-pom.xml[tags=build-image-example-builder-configuration] +---- + +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: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/packaging-oci-image/paketo-pom.xml[tags=paketo] +---- + + + +[[build-image.examples.runtime-jvm-configuration]] +=== Runtime JVM Configuration +Paketo Java buildpacks {paketo-java-reference}/#runtime-jvm-configuration[configure the JVM runtime environment] by setting the `JAVA_TOOL_OPTIONS` environment variable. +The buildpack-provided `JAVA_TOOL_OPTIONS` value can be modified to customize JVM runtime behavior when the application image is launched in a container. + +Environment variable modifications that should be stored in the image and applied to every deployment can be set as described in the {paketo-reference}/buildpacks/configuration/#environment-variables[Paketo documentation] and shown in the following example: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/packaging-oci-image/runtime-jvm-configuration-pom.xml[tags=runtime-jvm-configuration] +---- + + + +[[build-image.examples.custom-image-name]] +=== Custom Image Name +By default, the image name is inferred from the `artifactId` and the `version` of the project, something like `docker.io/library/${project.artifactId}:${project.version}`. +You can take control over the name, as shown in the following example: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/packaging-oci-image/custom-image-name-pom.xml[tags=custom-image-name] +---- + +NOTE: This configuration does not provide an explicit tag so `latest` is used. +It is possible to specify a tag as well, either using `${project.version}`, any property available in the build or a hardcoded version. + +The image name can be specified on the command line as well, as shown in this example: + +[indent=0] +---- + $ mvn spring-boot:build-image -Dspring-boot.build-image.imageName=example.com/library/my-app:v1 +---- + + + +[[build-image.examples.buildpacks]] +=== Buildpacks +By default, the builder will use buildpacks included in the builder image and apply them in a pre-defined order. +An alternative set of buildpacks can be provided to apply buildpacks that are not included in the builder, or to change the order of included buildpacks. +When one or more buildpacks are provided, only the specified buildpacks will be applied. + +The following example instructs the builder to use a custom buildpack packaged in a `.tgz` file, followed by a buildpack included in the builder. + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/packaging-oci-image/buildpacks-pom.xml[tags=buildpacks] +---- + +Buildpacks can be specified in any of the forms shown below. + +A buildpack located in a CNB Builder (version may be omitted if there is only one buildpack in the builder matching the `buildpack-id`): + +* `urn:cnb:builder:buildpack-id` +* `urn:cnb:builder:buildpack-id@0.0.1` +* `buildpack-id` +* `buildpack-id@0.0.1` + +A path to a directory containing buildpack content (not supported on Windows): + +* `\file:///path/to/buildpack/` +* `/path/to/buildpack/` + +A path to a gzipped tar file containing buildpack content: + +* `\file:///path/to/buildpack.tgz` +* `/path/to/buildpack.tgz` + +An OCI image containing a https://buildpacks.io/docs/buildpack-author-guide/package-a-buildpack/[packaged buildpack]: + +* `docker://example/buildpack` +* `docker:///example/buildpack:latest` +* `docker:///example/buildpack@sha256:45b23dee08...` +* `example/buildpack` +* `example/buildpack:latest` +* `example/buildpack@sha256:45b23dee08...` + + + +[[build-image.examples.publish]] +=== Image Publishing +The generated image can be published to a Docker registry by enabling a `publish` option and configuring authentication for the registry using `docker.publishRegistry` parameters. + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/packaging-oci-image/docker-pom.xml[tags=docker] +---- + +The `publish` option can be specified on the command line as well, as shown in this example: + +[indent=0] +---- + $ mvn spring-boot:build-image -Dspring-boot.build-image.imageName=docker.example.com/library/my-app:v1 -Dspring-boot.build-image.publish=true +---- + + + +[[build-image.examples.docker]] +=== Docker Configuration +If you need the plugin to communicate with the Docker daemon using a remote connection instead of the default local connection, the connection details can be provided using `docker` parameters as shown in the following example: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/packaging-oci-image/docker-remote-pom.xml[tags=docker-remote] +---- + +If the builder or run image are stored in a private Docker registry that supports user authentication, authentication details can be provided using `docker.builderRegistry` parameters as shown in the following example: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/packaging-oci-image/docker-registry-authentication-pom.xml[tags=docker-registry-authentication] +---- + +If the builder or run image is stored in a private Docker registry that supports token authentication, the token value can be provided using `docker.builderRegistry` parameters as shown in the following example: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/packaging-oci-image/docker-token-authentication-pom.xml[tags=docker-token-authentication] +---- diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging.adoc new file mode 100644 index 000000000000..ebb39fef90f0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging.adoc @@ -0,0 +1,270 @@ +[[packaging]] += Packaging Executable Archives +The plugin can create executable archives (jar files and war files) that contain all of an application's dependencies and can then be run with `java -jar`. + +Packaging an executable archive is performed by the `repackage` goal, as shown in the following example: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/packaging/repackage-pom.xml[tags=repackage] +---- + +TIP: If you are using `spring-boot-starter-parent`, such execution is already pre-configured with a `repackage` execution ID so that only the plugin definition should be added. + +The example above repackages a `jar` or `war` archive that is built during the package phase of the Maven lifecycle, including any `provided` dependencies that are defined in the project. +If some of these dependencies need to be excluded, you can use one of the `exclude` options; see the <> for more details. + +The original (i.e. non-executable) artifact is renamed to `.original` by default but it is also possible to keep the original artifact using a custom classifier. + +NOTE: The `outputFileNameMapping` feature of the `maven-war-plugin` is currently not supported. + +Devtools is automatically excluded by default (you can control that using the `excludeDevtools` property). +In order to make that work with `war` packaging, the `spring-boot-devtools` dependency must be set as `optional` or with the `provided` scope. + +The plugin rewrites your manifest, and in particular it manages the `Main-Class` and `Start-Class` entries. +If the defaults don't work you have to configure the values in the Spring Boot plugin, not in the jar plugin. +The `Main-Class` in the manifest is controlled by the `layout` property of the Spring Boot plugin, as shown in the following example: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/packaging/non-default-pom.xml[tags=non-default] +---- + +The `layout` property defaults to a value determined by the archive type (`jar` or `war`). The following layouts are available: + +* `JAR`: regular executable JAR layout. +* `WAR`: executable WAR layout. `provided` dependencies are placed in `WEB-INF/lib-provided` to avoid any clash when the `war` is deployed in a servlet container. +* `ZIP` (alias to `DIR`): similar to the `JAR` layout using `PropertiesLauncher`. +* `NONE`: Bundle all dependencies and project resources. Does not bundle a bootstrap loader. + + + +[[packaging.layers]] +== Layered Jar or War +A repackaged jar contains the application's classes and dependencies in `BOOT-INF/classes` and `BOOT-INF/lib` respectively. +Similarly, an executable war contains the application's classes in `WEB-INF/classes` and dependencies in `WEB-INF/lib` and `WEB-INF/lib-provided`. +For cases where a docker image needs to be built from the contents of a jar or war, it's useful to be able to separate these directories further so that they can be written into distinct layers. + +Layered archives use the same layout as a regular repackaged jar or war, but include an additional meta-data file that describes each layer. + +By default, the following layers are defined: + +* `dependencies` for any dependency whose version does not contain `SNAPSHOT`. +* `spring-boot-loader` for the loader classes. +* `snapshot-dependencies` for any dependency whose version contains `SNAPSHOT`. +* `application` for local module dependencies, application classes, and resources. + +Module dependencies are identified by looking at all of the modules that are part of the current build. +If a module dependency can only be resolved because it has been installed into Maven's local cache and it is not part of the current build, it will be identified as regular dependency. + +The layers order is important as it determines how likely previous layers can be cached when part of the application changes. +The default order is `dependencies`, `spring-boot-loader`, `snapshot-dependencies`, `application`. +Content that is least likely to change should be added first, followed by layers that are more likely to change. + +The repackaged archive includes the `layers.idx` file by default. +To disable this feature, you can do so in the following manner: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/packaging/disable-layers-pom.xml[tags=disable-layers] +---- + + + +[[packaging.layers.configuration=]] +=== 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: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/packaging/custom-layers-pom.xml[tags=custom-layers] +---- + +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: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/packaging/layers.xml[tags=layers] +---- + +The `layers` XML format is defined in three sections: + +* The `` block defines how the application classes and resources should be layered. +* The `` block defines how dependencies should be layered. +* The `` block defines the order that the layers should be written. + +Nested `` blocks are used within `` and `` sections to claim content for a layer. +The blocks are evaluated in the order that they are defined, from top to bottom. +Any content not claimed by an earlier block remains available for subsequent blocks to consider. + +The `` block claims content using nested `` and `` elements. +The `` section uses Ant-style path matching for include/exclude expressions. +The `` section uses `group:artifact[:version]` patterns. +It also provides `` and `` elements that can be used to include or exclude local module dependencies. + +If no `` is defined, then all content (not claimed by an earlier block) is considered. + +If no `` is defined, then no exclusions are applied. + +Looking at the `` example above, we can see that the first `` will claim all module dependencies for the `application.layer`. +The next `` will claim all SNAPSHOT dependencies for the `snapshot-dependencies` layer. +The final `` will claim anything left (in this case, any dependency that is not a SNAPSHOT) for the `dependencies` layer. + +The `` block has similar rules. +First claiming `org/springframework/boot/loader/**` content for the `spring-boot-loader` layer. +Then claiming any remaining classes and resources for the `application` layer. + +NOTE: The order that `` blocks are defined is often different from the order that the layers are written. +For this reason the `` element must always be included and _must_ cover all layers referenced by the `` blocks. + +include::goals/repackage.adoc[leveloffset=+1] + + + +[[packaging.examples]] +== Examples + + + +[[packaging.examples.custom-classifier]] +=== Custom Classifier +By default, the `repackage` goal replaces the original artifact with the repackaged one. +That is a sane behavior for modules that represent an application but if your module is used as a dependency of another module, you need to provide a classifier for the repackaged one. +The reason for that is that application classes are packaged in `BOOT-INF/classes` so that the dependent module cannot load a repackaged jar's classes. + +If that is the case or if you prefer to keep the original artifact and attach the repackaged one with a different classifier, configure the plugin as shown in the following example: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/packaging/different-classifier-pom.xml[tags=different-classifier] +---- + +If you are using `spring-boot-starter-parent`, the `repackage` goal is executed automatically in an execution with id `repackage`. +In that setup, only the configuration should be specified, as shown in the following example: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/packaging/repackage-configuration-pom.xml[tags=repackage-configuration] +---- + +This configuration will generate two artifacts: the original one and the repackaged counter part produced by the repackage goal. +Both will be installed/deployed transparently. + +You can also use the same configuration if you want to repackage a secondary artifact the same way the main artifact is replaced. +The following configuration installs/deploys a single `task` classified artifact with the repackaged application: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/packaging/classified-artifact-pom.xml[tags=classified-artifact] +---- + +As both the `maven-jar-plugin` and the `spring-boot-maven-plugin` runs at the same phase, it is important that the jar plugin is defined first (so that it runs before the repackage goal). +Again, if you are using `spring-boot-starter-parent`, this can be simplified as follows: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/packaging/jar-plugin-first-pom.xml[tags=jar-plugin-first] +---- + + + +[[packaging.examples.custom-name]] +=== Custom Name +If you need the repackaged jar to have a different local name than the one defined by the `artifactId` attribute of the project, use the standard `finalName`, as shown in the following example: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/packaging/custom-name-pom.xml[tags=custom-name] +---- + +This configuration will generate the repackaged artifact in `target/my-app.jar`. + + + +[[packaging.examples.local-artifact]] +=== Local Repackaged Artifact +By default, the `repackage` goal replaces the original artifact with the executable one. +If you need to only deploy the original jar and yet be able to run your app with the regular file name, configure the plugin as follows: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/packaging/local-repackaged-artifact-pom.xml[tags=local-repackaged-artifact] +---- + +This configuration generates two artifacts: the original one and the executable counter part produced by the `repackage` goal. +Only the original one will be installed/deployed. + + + +[[packaging.examples.custom-layout]] +=== Custom Layout +Spring Boot repackages the jar file for this project using a custom layout factory defined in the additional jar file, provided as a dependency to the build plugin: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/packaging/custom-layout-pom.xml[tags=custom-layout] +---- + +The layout factory is provided as an implementation of `LayoutFactory` (from `spring-boot-loader-tools`) explicitly specified in the pom. +If there is only one custom `LayoutFactory` on the plugin classpath and it is listed in `META-INF/spring.factories` then it is unnecessary to explicitly set it in the plugin configuration. + +Layout factories are always ignored if an explicit <> is set. + + + +[[packaging.examples.exclude-dependency]] +=== Dependency Exclusion +By default, both the `repackage` and the `run` goals will include any `provided` dependencies that are defined in the project. +A Spring Boot project should consider `provided` dependencies as "container" dependencies that are required to run the application. + +Some of these dependencies may not be required at all and should be excluded from the executable jar. +For consistency, they should not be present either when running the application. + +There are two ways one can exclude a dependency from being packaged/used at runtime: + +* Exclude a specific artifact identified by `groupId` and `artifactId`, optionally with a `classifier` if needed. +* Exclude any artifact belonging to a given `groupId`. + +The following example excludes `com.example:module1`, and only that artifact: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/packaging/exclude-artifact-pom.xml[tags=exclude-artifact] +---- + +This example excludes any artifact belonging to the `com.example` group: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/packaging/exclude-artifact-group-pom.xml[tags=exclude-artifact-group] +---- + + + +[[packaging.examples.layered-archive-tools]] +=== Layered Archive Tools +When a layered jar or war is created, the `spring-boot-jarmode-layertools` jar will be added as a dependency to your archive. +With this jar on the classpath, you can launch your application in a special mode which allows the bootstrap code to run something entirely different from your application, for example, something that extracts the layers. +If you wish to exclude this dependency, you can do so in the following manner: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/packaging/exclude-dependency-pom.xml[tags=exclude-dependency] +---- + + + +[[packaging.examples.custom-layers-configuration]] +=== Custom Layers Configuration +The default setup splits dependencies into snapshot and non-snapshot, however, you may have more complex rules. +For example, you may want to isolate company-specific dependencies of your project in a dedicated layer. +The following `layers.xml` configuration shown one such setup: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/packaging/layers-configuration.xml[tags=layers-configuration] +---- + +The configuration above creates an additional `company-dependencies` layer with all libraries with the `com.acme` groupId. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/running.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/running.adoc new file mode 100644 index 000000000000..0d85babe3018 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/running.adoc @@ -0,0 +1,177 @@ +[[run]] += Running your Application with Maven +The plugin includes a run goal which can be used to launch your application from the command line, as shown in the following example: + +[indent=0] +---- + $ mvn spring-boot:run +---- + +Application arguments can be specified using the `arguments` parameter, see <> for more details. + +By default the application is executed in a forked process and setting properties on the command-line will not affect the application. +If you need to specify some JVM arguments (i.e. for debugging purposes), you can use the `jvmArguments` parameter, see <> for more details. +There is also explicit support for <> and <>. + +As enabling a profile is quite common, there is dedicated `profiles` property that offers a shortcut for `-Dspring-boot.run.jvmArguments="-Dspring.profiles.active=dev"`, see <>. + +Although this is not recommended, it is possible to execute the application directly from the Maven JVM by disabling the `fork` property. +Doing so means that the `jvmArguments`, `systemPropertyVariables`, `environmentVariables` and `agents` options are ignored. + +Spring Boot `devtools` is a module to improve the development-time experience when working on Spring Boot applications. +To enable it, just add the following dependency to your project: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/running/devtools-pom.xml[tags=devtools] +---- + +When `devtools` is running, it detects change 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: + +[source,properties,indent=0] +---- + spring.devtools.remote.restart.enabled=false +---- + +Prior to `devtools`, the plugin supported hot refreshing of resources by default which has now be disabled in favour of the solution described above. +You can restore it at any time by configuring your project: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/running/hot-refresh-pom.xml[tags=hot-refresh] +---- + +When `addResources` is enabled, any `src/main/resources` directory will be added to the application classpath when you run the application and any duplicate found in `target/classes` will be removed. +This allows hot refreshing of resources which can be very useful when developing web applications. +For example, you can work on HTML, CSS or JavaScript files and see your changes immediately without recompiling your application. +It is also a helpful way of allowing your front end developers to work without needing to download and install a Java IDE. + +NOTE: A side effect of using this feature is that filtering of resources at build time will not work. + +In order to be consistent with the `repackage` goal, the `run` goal builds the classpath in such a way that any dependency that is excluded in the plugin's configuration gets excluded from the classpath as well. +For more details, see <>. + +Sometimes it is useful to include test dependencies when running the application. +For example, if you want to run your application in a test mode that uses stub classes. +If you wish to do this, you can set the `useTestClasspath` parameter to true. + +NOTE: This is only applied when you run an application: the `repackage` goal will not add test dependencies to the resulting JAR/WAR. + +include::goals/run.adoc[leveloffset=+1] + + + +[[run.examples]] +== Examples + + + +[[run.examples.debug]] +=== Debug the Application +By default, the `run` goal runs your application in a forked process. +If you need to debug it, you should add the necessary JVM arguments to enable remote debugging. +The following configuration suspend the process until a debugger has joined on port 5005: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/running/debug-pom.xml[tags=debug] +---- + +These arguments can be specified on the command line as well, make sure to wrap that properly, that is: + +[indent=0] +---- + $ mvn spring-boot:run -Dspring-boot.run.jvmArguments="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005" +---- + + + +[[run.examples.system-properties]] +=== Using System Properties +System properties can be specified using the `systemPropertyVariables` attribute. +The following example sets `property1` to `test` and `property2` to 42: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/running/system-properties-pom.xml[tags=system-properties] +---- + +If the value is empty or not defined (i.e. `), the system property is set with an empty String as the value. +Maven trims values specified in the pom so it is not possible to specify a System property which needs to start or end with a space via this mechanism: consider using `jvmArguments` instead. + +Any String typed Maven variable can be passed as system properties. +Any attempt to pass any other Maven variable type (e.g. a `List` or a `URL` variable) will cause the variable expression to be passed literally (unevaluated). + +The `jvmArguments` parameter takes precedence over system properties defined with the mechanism above. +In the following example, the value for `property1` is `overridden`: + +[indent=0] +---- + $ mvn spring-boot:run -Dspring-boot.run.jvmArguments="-Dproperty1=overridden" +---- + + + +[[run.examples.environment-variables]] +=== Using Environment Variables +Environment variables can be specified using the `environmentVariables` attribute. +The following example sets the 'ENV1', 'ENV2', 'ENV3', 'ENV4' env variables: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/running/environment-variables-pom.xml[tags=environment-variables] +---- + +If the value is empty or not defined (i.e. `), the env variable is set with an empty String as the value. +Maven trims values specified in the pom so it is not possible to specify an env variable which needs to start or end with a space. + +Any String typed Maven variable can be passed as system properties. +Any attempt to pass any other Maven variable type (e.g. a `List` or a `URL` variable) will cause the variable expression to be passed literally (unevaluated). + +Environment variables defined this way take precedence over existing values. + + + +[[run.examples.using-application-arguments]] +=== Using Application Arguments +Application arguments can be specified using the `arguments` attribute. +The following example sets two arguments: `property1` and `property2=42`: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/running/application-arguments-pom.xml[tags=application-arguments] +---- + +On the command-line, arguments are separated by a space the same way `jvmArguments` are. +If an argument contains a space, make sure to quote it. +In the following example, two arguments are available: `property1` and `property2=Hello World`: + +[indent=0] +---- + $ mvn spring-boot:run -Dspring-boot.run.arguments="property1 'property2=Hello World'" +---- + + + +[[run.examples.specify-active-profiles]] +=== Specify Active Profiles +The active profiles to use for a particular application can be specified using the `profiles` argument. + +The following configuration enables the `local` and `dev` profiles: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/running/active-profiles-pom.xml[tags=active-profiles] +---- + +The profiles to enable can be specified on the command line as well, make sure to separate them with a comma, as shown in the following example: + +[indent=0] +---- + $ mvn spring-boot:run -Dspring-boot.run.profiles=local,dev +---- diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/using.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/using.adoc new file mode 100644 index 000000000000..58d13bae7bfb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/using.adoc @@ -0,0 +1,97 @@ +[[using]] += Using the Plugin +Maven users can inherit from the `spring-boot-starter-parent` project to obtain sensible defaults. +The parent project provides the following features: + +* Java 1.8 as the default compiler level. +* UTF-8 source encoding. +* Compilation with `-parameters`. +* A dependency management section, inherited from the `spring-boot-dependencies` POM, that manages the versions of common dependencies. +This dependency management lets you omit `` tags for those dependencies when used in your own POM. +* An execution of the <> with a `repackage` execution id. +* Sensible https://maven.apache.org/plugins/maven-resources-plugin/examples/filter.html[resource filtering]. +* Sensible plugin configuration (https://github.com/ktoso/maven-git-commit-id-plugin[Git commit ID], and https://maven.apache.org/plugins/maven-shade-plugin/[shade]). +* Sensible resource filtering for `application.properties` and `application.yml` including profile-specific files (for example, `application-dev.properties` and `application-dev.yml`) + +NOTE: Since the `application.properties` and `application.yml` files accept Spring style placeholders (`${...}`), the Maven filtering is changed to use `@..@` placeholders. +(You can override that by setting a Maven property called `resource.delimiter`.) + + + +[[using.parent-pom]] +== Inheriting the Starter Parent POM +To configure your project to inherit from the `spring-boot-starter-parent`, set the `parent` as follows: + +[source,xml,indent=0,subs="verbatim,quotes,attributes"] +---- + + + org.springframework.boot + spring-boot-starter-parent + {gradle-project-version} + +---- + +NOTE: You should need to specify only the Spring Boot version number on this dependency. +If you import additional starters, you can safely omit the version number. + +With that setup, you can also override individual dependencies by overriding a property in your own project. +For instance, to use a different version of the SLF4J library and the Spring Data release train, you would add the following to your `pom.xml`: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/using/different-versions-pom.xml[tags=different-versions] +---- + +Browse the {version-properties-appendix}[`Dependency versions Appendix`] in the Spring Boot reference for a complete list of dependency version properties. + + + +[[using.import]] +== Using Spring Boot without the Parent POM +There may be reasons for you not to inherit from the `spring-boot-starter-parent` POM. +You may have your own corporate standard parent that you need to use or you may prefer to explicitly declare all your Maven configuration. + +If you do not want to use the `spring-boot-starter-parent`, you can still keep the benefit of the dependency management (but not the plugin management) by using an `import` scoped dependency, as follows: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/using/no-starter-parent-pom.xml[tags=no-starter-parent] +---- + +The preceding sample setup does not let you override individual dependencies by using properties, as explained above. +To achieve the same result, you need to add entries in the `dependencyManagement` section of your project **before** the `spring-boot-dependencies` entry. +For instance, to use a different version of the SLF4J library and the Spring Data release train, you could add the following elements to your `pom.xml`: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/using/no-starter-parent-override-dependencies-pom.xml[tags=no-starter-parent-override-dependencies] +---- + + + +[[using.overriding-command-line]] +== Overriding settings on the command-line +The plugin offers a number of user properties, starting with `spring-boot`, to let you customize the configuration from the command-line. + +For instance, you could tune the profiles to enable when running the application as follows: + +[indent=0] +---- + $ mvn spring-boot:run -Dspring-boot.run.profiles=dev,local +---- + +If you want to both have a default while allowing it to be overridden on the command-line, you should use a combination of a user-provided project property and MOJO configuration. + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/using/default-and-override-pom.xml[tags=default-and-override] +---- + +The above makes sure that `local` and `dev` are enabled by default. +Now a dedicated property has been exposed, this can be overridden on the command-line as well: + +[indent=0] +---- + $ mvn spring-boot:run -Dapp.profiles=test +---- diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/build-info/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/build-info/pom.xml new file mode 100644 index 000000000000..976cd10e77c3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/build-info/pom.xml @@ -0,0 +1,31 @@ + + + + 4.0.0 + build-info + + + + org.springframework.boot + spring-boot-maven-plugin + + + + build-info + + + + UTF-8 + UTF-8 + ${maven.compiler.source} + ${maven.compiler.target} + + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/getting-started/plugin-repositories-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/getting-started/plugin-repositories-pom.xml new file mode 100644 index 000000000000..ff2203b1efc7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/getting-started/plugin-repositories-pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + getting-started + + + + org.springframework.boot + spring-boot-maven-plugin + + + + build-info + + + + UTF-8 + UTF-8 + ${maven.compiler.source} + ${maven.compiler.target} + + + + + + + + + + + + spring-snapshots + https://repo.spring.io/snapshot + + + spring-milestones + https://repo.spring.io/milestone + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/getting-started/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/getting-started/pom.xml new file mode 100644 index 000000000000..6c2d900e60be --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/getting-started/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + getting-started + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/integration-tests/customize-jmx-port-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/integration-tests/customize-jmx-port-pom.xml new file mode 100644 index 000000000000..4c57c01bea43 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/integration-tests/customize-jmx-port-pom.xml @@ -0,0 +1,33 @@ + + + 4.0.0 + integration-tests + + + + + org.springframework.boot + spring-boot-maven-plugin + + 9009 + + + + pre-integration-test + + start + + + + post-integration-test + + stop + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/integration-tests/failsafe-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/integration-tests/failsafe-pom.xml new file mode 100644 index 000000000000..d6e8993581e0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/integration-tests/failsafe-pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + integration-tests + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + ${project.build.outputDirectory} + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/integration-tests/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/integration-tests/pom.xml new file mode 100644 index 000000000000..fdbbe349660d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/integration-tests/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + integration-tests + + + + + org.springframework.boot + spring-boot-maven-plugin + + + pre-integration-test + + start + + + + post-integration-test + + stop + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/integration-tests/random-port-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/integration-tests/random-port-pom.xml new file mode 100644 index 000000000000..202b30600e01 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/integration-tests/random-port-pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + integration-tests + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + reserve-tomcat-port + + reserve-network-port + + process-resources + + + tomcat.http.port + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + pre-integration-test + + start + + + + --server.port=${tomcat.http.port} + + + + + post-integration-test + + stop + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + ${tomcat.http.port} + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/integration-tests/skip-integration-tests-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/integration-tests/skip-integration-tests-pom.xml new file mode 100644 index 000000000000..ef2d20d0236e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/integration-tests/skip-integration-tests-pom.xml @@ -0,0 +1,44 @@ + + + + + false + + + + + org.springframework.boot + spring-boot-maven-plugin + + + pre-integration-test + + start + + + ${skip.it} + + + + post-integration-test + + stop + + + ${skip.it} + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + ${skip.it} + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/build-image-example-builder-configuration-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/build-image-example-builder-configuration-pom.xml new file mode 100644 index 000000000000..5e6dde3e797d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/build-image-example-builder-configuration-pom.xml @@ -0,0 +1,21 @@ + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + 8.* + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/buildpacks-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/buildpacks-pom.xml new file mode 100644 index 000000000000..fed32dda8887 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/buildpacks-pom.xml @@ -0,0 +1,22 @@ + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + file:///path/to/example-buildpack.tgz + urn:cnb:builder:paketo-buildpacks/java + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/custom-image-builder-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/custom-image-builder-pom.xml new file mode 100644 index 000000000000..f93c97fa6261 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/custom-image-builder-pom.xml @@ -0,0 +1,20 @@ + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + mine/java-cnb-builder + mine/java-cnb-run + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/custom-image-name-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/custom-image-name-pom.xml new file mode 100644 index 000000000000..7a89363fe0eb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/custom-image-name-pom.xml @@ -0,0 +1,19 @@ + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + example.com/library/${project.artifactId} + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/docker-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/docker-pom.xml new file mode 100644 index 000000000000..377b0c717274 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/docker-pom.xml @@ -0,0 +1,28 @@ + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + docker.example.com/library/${project.artifactId} + true + + + + user + secret + https://docker.example.com/v1/ + user@example.com + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/docker-registry-authentication-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/docker-registry-authentication-pom.xml new file mode 100644 index 000000000000..478aaf01945f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/docker-registry-authentication-pom.xml @@ -0,0 +1,24 @@ + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + user + secret + https://docker.example.com/v1/ + user@example.com + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/docker-remote-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/docker-remote-pom.xml new file mode 100644 index 000000000000..942e3cdebdb6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/docker-remote-pom.xml @@ -0,0 +1,21 @@ + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + tcp://192.168.99.100:2376 + true + /home/user/.minikube/certs + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/docker-token-authentication-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/docker-token-authentication-pom.xml new file mode 100644 index 000000000000..6ac77fab6b96 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/docker-token-authentication-pom.xml @@ -0,0 +1,21 @@ + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + 9cbaf023786cd7... + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/paketo-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/paketo-pom.xml new file mode 100644 index 000000000000..ce818d71fcc2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/paketo-pom.xml @@ -0,0 +1,22 @@ + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + http://proxy.example.com + https://proxy.example.com + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/pom.xml new file mode 100644 index 000000000000..7445b14ad62d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + packaging-oci-image + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + build-image + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/runtime-jvm-configuration-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/runtime-jvm-configuration-pom.xml new file mode 100644 index 000000000000..cb246cae29c1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/runtime-jvm-configuration-pom.xml @@ -0,0 +1,22 @@ + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + -XX:+HeapDumpOnOutOfMemoryError + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/classified-artifact-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/classified-artifact-pom.xml new file mode 100644 index 000000000000..a6cfd32304da --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/classified-artifact-pom.xml @@ -0,0 +1,40 @@ + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + jar + + package + + task + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + repackage + + repackage + + + task + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/custom-layers-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/custom-layers-pom.xml new file mode 100644 index 000000000000..3a1af1c8d0d6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/custom-layers-pom.xml @@ -0,0 +1,20 @@ + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + true + ${project.basedir}/src/layers.xml + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/custom-layout-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/custom-layout-pom.xml new file mode 100644 index 000000000000..3e751124f05a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/custom-layout-pom.xml @@ -0,0 +1,34 @@ + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + repackage + + repackage + + + + value + + + + + + + com.example + custom-layout + 0.0.1.BUILD-SNAPSHOT + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/custom-name-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/custom-name-pom.xml new file mode 100644 index 000000000000..d37ba27d3b88 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/custom-name-pom.xml @@ -0,0 +1,23 @@ + + + + + my-app + + + org.springframework.boot + spring-boot-maven-plugin + + + repackage + + repackage + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/different-classifier-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/different-classifier-pom.xml new file mode 100644 index 000000000000..ec685d964d85 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/different-classifier-pom.xml @@ -0,0 +1,25 @@ + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + repackage + + repackage + + + exec + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/disable-layers-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/disable-layers-pom.xml new file mode 100644 index 000000000000..22d47df36b39 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/disable-layers-pom.xml @@ -0,0 +1,19 @@ + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + false + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/exclude-artifact-group-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/exclude-artifact-group-pom.xml new file mode 100644 index 000000000000..aef38a7cf536 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/exclude-artifact-group-pom.xml @@ -0,0 +1,17 @@ + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + com.example + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/exclude-artifact-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/exclude-artifact-pom.xml new file mode 100644 index 000000000000..1a969d14dcad --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/exclude-artifact-pom.xml @@ -0,0 +1,22 @@ + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + com.example + module1 + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/exclude-dependency-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/exclude-dependency-pom.xml new file mode 100644 index 000000000000..511cc0086024 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/exclude-dependency-pom.xml @@ -0,0 +1,19 @@ + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + false + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/jar-plugin-first-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/jar-plugin-first-pom.xml new file mode 100644 index 000000000000..c39b02d3cfd4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/jar-plugin-first-pom.xml @@ -0,0 +1,34 @@ + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + default-jar + + task + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + repackage + + task + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/layers-configuration.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/layers-configuration.xml new file mode 100644 index 000000000000..d2cc497dc88e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/layers-configuration.xml @@ -0,0 +1,29 @@ + + + + + org/springframework/boot/loader/** + + + + + + *:*:*SNAPSHOT + + + com.acme:* + + + + + dependencies + spring-boot-loader + snapshot-dependencies + company-dependencies + application + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/layers.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/layers.xml new file mode 100644 index 000000000000..93cbee9272b8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/layers.xml @@ -0,0 +1,28 @@ + + + + + org/springframework/boot/loader/** + + + + + + + + + *:*:*SNAPSHOT + + + + + dependencies + spring-boot-loader + snapshot-dependencies + application + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/local-repackaged-artifact-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/local-repackaged-artifact-pom.xml new file mode 100644 index 000000000000..137e455e245d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/local-repackaged-artifact-pom.xml @@ -0,0 +1,25 @@ + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + repackage + + repackage + + + false + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/non-default-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/non-default-pom.xml new file mode 100644 index 000000000000..ded8bc10bdd4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/non-default-pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + packaging + + + + + + org.springframework.boot + spring-boot-maven-plugin + + ${start.class} + ZIP + + + + + repackage + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/repackage-configuration-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/repackage-configuration-pom.xml new file mode 100644 index 000000000000..5878dbd5cab2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/repackage-configuration-pom.xml @@ -0,0 +1,22 @@ + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + repackage + + exec + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/repackage-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/repackage-pom.xml new file mode 100644 index 000000000000..ee7d5584e04b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging/repackage-pom.xml @@ -0,0 +1,23 @@ + + + 4.0.0 + packaging + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/running/active-profiles-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/running/active-profiles-pom.xml new file mode 100644 index 000000000000..9eb58adb6917 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/running/active-profiles-pom.xml @@ -0,0 +1,19 @@ + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + local + dev + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/running/application-arguments-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/running/application-arguments-pom.xml new file mode 100644 index 000000000000..cf0a1848a2ba --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/running/application-arguments-pom.xml @@ -0,0 +1,21 @@ + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + property1 + property2=${my.value} + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/running/debug-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/running/debug-pom.xml new file mode 100644 index 000000000000..08db158eb8bf --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/running/debug-pom.xml @@ -0,0 +1,20 @@ + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005 + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/running/devtools-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/running/devtools-pom.xml new file mode 100644 index 000000000000..222b96c2bdeb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/running/devtools-pom.xml @@ -0,0 +1,26 @@ + + + 4.0.0 + running + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + + org.springframework.boot + spring-boot-devtools + true + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/running/environment-variables-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/running/environment-variables-pom.xml new file mode 100644 index 000000000000..ebc1741b1065 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/running/environment-variables-pom.xml @@ -0,0 +1,23 @@ + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + 5000 + Some Text + + + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/running/hot-refresh-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/running/hot-refresh-pom.xml new file mode 100644 index 000000000000..190bd2c5613b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/running/hot-refresh-pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + getting-started + + + + + + org.springframework.boot + spring-boot-maven-plugin + + true + + + + + + + + + org.springframework.boot + spring-boot-devtools + true + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/running/system-properties-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/running/system-properties-pom.xml new file mode 100644 index 000000000000..5923b0dd3d4f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/running/system-properties-pom.xml @@ -0,0 +1,24 @@ + + + + + + 42 + + + + org.springframework.boot + spring-boot-maven-plugin + + + test + ${my.value} + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/using/default-and-override-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/using/default-and-override-pom.xml new file mode 100644 index 000000000000..a317af82da22 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/using/default-and-override-pom.xml @@ -0,0 +1,20 @@ + + + + + local,dev + + + + + org.springframework.boot + spring-boot-maven-plugin + + ${app.profiles} + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/using/different-versions-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/using/different-versions-pom.xml new file mode 100644 index 000000000000..ec86e1c0048f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/using/different-versions-pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + getting-started + + + + 1.7.30 + Moore-SR6 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/using/no-starter-parent-override-dependencies-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/using/no-starter-parent-override-dependencies-pom.xml new file mode 100644 index 000000000000..577be603cadf --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/using/no-starter-parent-override-dependencies-pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + getting-started + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + + + + org.slf4j + slf4j-api + 1.7.30 + + + + org.springframework.data + spring-data-releasetrain + 2020.0.0-SR1 + pom + import + + + org.springframework.boot + spring-boot-dependencies + {gradle-project-version} + pom + import + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/using/no-starter-parent-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/using/no-starter-parent-pom.xml new file mode 100644 index 000000000000..b5aaf8815d9d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/using/no-starter-parent-pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + using + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + + + + org.springframework.boot + spring-boot-dependencies + {gradle-project-version} + pom + import + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/AbstractArchiveIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/AbstractArchiveIntegrationTests.java new file mode 100644 index 000000000000..9ece3542565e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/AbstractArchiveIntegrationTests.java @@ -0,0 +1,214 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.maven; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; + +import org.assertj.core.api.AbstractAssert; +import org.assertj.core.api.AssertProvider; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.contentOf; + +/** + * Base class for archive (jar or war) related Maven plugin integration tests. + * + * @author Andy Wilkinson + * @author Scott Frederick + */ +abstract class AbstractArchiveIntegrationTests { + + protected String buildLog(File project) { + return contentOf(new File(project, "target/build.log")); + } + + protected String launchScript(File jar) { + String content = contentOf(jar); + return content.substring(0, content.indexOf(new String(new byte[] { 0x50, 0x4b, 0x03, 0x04 }))); + } + + protected AssertProvider jar(File file) { + return new AssertProvider() { + + @Override + @Deprecated + public JarAssert assertThat() { + return new JarAssert(file); + } + + }; + } + + protected Map> readLayerIndex(JarFile jarFile) throws IOException { + if (getLayersIndexLocation() == null) { + return Collections.emptyMap(); + } + Map> index = new LinkedHashMap<>(); + ZipEntry indexEntry = jarFile.getEntry(getLayersIndexLocation()); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(jarFile.getInputStream(indexEntry)))) { + String line = reader.readLine(); + String layer = null; + while (line != null) { + if (line.startsWith("- ")) { + layer = line.substring(3, line.length() - 2); + index.put(layer, new ArrayList<>()); + } + else if (line.startsWith(" - ")) { + index.computeIfAbsent(layer, (key) -> new ArrayList<>()).add(line.substring(5, line.length() - 1)); + } + line = reader.readLine(); + } + return index; + } + } + + protected String getLayersIndexLocation() { + return null; + } + + static final class JarAssert extends AbstractAssert { + + private JarAssert(File actual) { + super(actual, JarAssert.class); + assertThat(actual).exists(); + } + + JarAssert doesNotHaveEntryWithName(String name) { + withJarFile((jarFile) -> { + withEntries(jarFile, (entries) -> { + Optional match = entries.filter((entry) -> entry.getName().equals(name)).findFirst(); + assertThat(match).isNotPresent(); + }); + }); + return this; + } + + JarAssert hasEntryWithName(String name) { + withJarFile((jarFile) -> { + withEntries(jarFile, (entries) -> { + Optional match = entries.filter((entry) -> entry.getName().equals(name)).findFirst(); + assertThat(match).hasValueSatisfying((entry) -> assertThat(entry.getComment()).isNull()); + }); + }); + return this; + } + + JarAssert hasEntryWithNameStartingWith(String prefix) { + withJarFile((jarFile) -> { + withEntries(jarFile, (entries) -> { + Optional match = entries.filter((entry) -> entry.getName().startsWith(prefix)) + .findFirst(); + assertThat(match).hasValueSatisfying((entry) -> assertThat(entry.getComment()).isNull()); + }); + }); + return this; + } + + JarAssert hasUnpackEntryWithNameStartingWith(String prefix) { + withJarFile((jarFile) -> { + withEntries(jarFile, (entries) -> { + Optional match = entries.filter((entry) -> entry.getName().startsWith(prefix)) + .findFirst(); + assertThat(match).as("Name starting with %s", prefix) + .hasValueSatisfying((entry) -> assertThat(entry.getComment()).startsWith("UNPACK:")); + }); + }); + return this; + } + + JarAssert doesNotHaveEntryWithNameStartingWith(String prefix) { + withJarFile((jarFile) -> { + withEntries(jarFile, (entries) -> { + Optional match = entries.filter((entry) -> entry.getName().startsWith(prefix)) + .findFirst(); + assertThat(match).isNotPresent(); + }); + }); + return this; + } + + JarAssert manifest(Consumer consumer) { + withJarFile((jarFile) -> { + try { + consumer.accept(new ManifestAssert(jarFile.getManifest())); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + }); + return this; + } + + void withJarFile(Consumer consumer) { + try (JarFile jarFile = new JarFile(this.actual)) { + consumer.accept(jarFile); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + void withEntries(JarFile jarFile, Consumer> entries) { + entries.accept(Collections.list(jarFile.entries()).stream()); + } + + static final class ManifestAssert extends AbstractAssert { + + private ManifestAssert(Manifest actual) { + super(actual, ManifestAssert.class); + } + + ManifestAssert hasStartClass(String expected) { + assertThat(this.actual.getMainAttributes().getValue("Start-Class")).isEqualTo(expected); + return this; + } + + ManifestAssert hasMainClass(String expected) { + assertThat(this.actual.getMainAttributes().getValue("Main-Class")).isEqualTo(expected); + return this; + } + + ManifestAssert hasAttribute(String name, String value) { + assertThat(this.actual.getMainAttributes().getValue(name)).isEqualTo(value); + return this; + } + + ManifestAssert doesNotHaveAttribute(String name) { + assertThat(this.actual.getMainAttributes().getValue(name)).isNull(); + return this; + } + + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageRegistryIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageRegistryIntegrationTests.java new file mode 100644 index 000000000000..3a8de6021fbc --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageRegistryIntegrationTests.java @@ -0,0 +1,90 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.maven; + +import java.time.Duration; + +import com.github.dockerjava.api.DockerClient; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.boot.buildpack.platform.docker.DockerApi; +import org.springframework.boot.buildpack.platform.docker.UpdateListener; +import org.springframework.boot.buildpack.platform.docker.type.Image; +import org.springframework.boot.buildpack.platform.docker.type.ImageReference; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for the Maven plugin's image support using a Docker image registry. + * + * @author Scott Frederick + */ +@ExtendWith(MavenBuildExtension.class) +@Testcontainers(disabledWithoutDocker = true) +@Disabled("Disabled until differences between running locally and in CI can be diagnosed") +class BuildImageRegistryIntegrationTests extends AbstractArchiveIntegrationTests { + + @Container + static final RegistryContainer registry = new RegistryContainer().withStartupAttempts(5) + .withStartupTimeout(Duration.ofMinutes(3)); + + DockerClient dockerClient; + + String registryAddress; + + @BeforeEach + void setUp() { + assertThat(registry.isRunning()).isTrue(); + this.dockerClient = registry.getDockerClient(); + this.registryAddress = registry.getHost() + ":" + registry.getFirstMappedPort(); + } + + @TestTemplate + void whenBuildImageIsInvokedWithPublish(MavenBuild mavenBuild) { + String repoName = "test-image"; + String imageName = this.registryAddress + "/" + repoName; + mavenBuild.project("build-image-publish").goals("package") + .systemProperty("spring-boot.build-image.imageName", imageName).execute((project) -> { + assertThat(buildLog(project)).contains("Building image").contains("Successfully built image") + .contains("Pushing image '" + imageName + ":latest" + "'") + .contains("Pushed image '" + imageName + ":latest" + "'"); + ImageReference imageReference = ImageReference.of(imageName); + DockerApi.ImageApi imageApi = new DockerApi().image(); + Image pulledImage = imageApi.pull(imageReference, UpdateListener.none()); + assertThat(pulledImage).isNotNull(); + imageApi.remove(imageReference, false); + }); + } + + private static class RegistryContainer extends GenericContainer { + + RegistryContainer() { + super(DockerImageNames.registry()); + addExposedPorts(5000); + addEnv("SERVER_NAME", "localhost"); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java new file mode 100644 index 000000000000..b5c618b5bcc1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java @@ -0,0 +1,344 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.maven; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Random; + +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.buildpack.platform.docker.DockerApi; +import org.springframework.boot.buildpack.platform.docker.type.ImageName; +import org.springframework.boot.buildpack.platform.docker.type.ImageReference; +import org.springframework.boot.testsupport.testcontainers.DisabledIfDockerUnavailable; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for the Maven plugin's image support. + * + * @author Stephane Nicoll + * @author Scott Frederick + */ +@ExtendWith(MavenBuildExtension.class) +@DisabledIfDockerUnavailable +class BuildImageTests extends AbstractArchiveIntegrationTests { + + @TestTemplate + void whenBuildImageIsInvokedWithoutRepackageTheArchiveIsRepackagedOnTheFly(MavenBuild mavenBuild) { + mavenBuild.project("build-image").goals("package") + .systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT") + .prepare(this::writeLongNameResource).execute((project) -> { + File jar = new File(project, "target/build-image-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(jar).isFile(); + File original = new File(project, "target/build-image-0.0.1.BUILD-SNAPSHOT.jar.original"); + assertThat(original).doesNotExist(); + assertThat(buildLog(project)).contains("Building image") + .contains("docker.io/library/build-image:0.0.1.BUILD-SNAPSHOT") + .contains("---> Test Info buildpack building").contains("env: BP_JVM_VERSION=8.*") + .contains("---> Test Info buildpack done").contains("Successfully built image"); + removeImage("build-image", "0.0.1.BUILD-SNAPSHOT"); + }); + } + + @TestTemplate + void whenBuildImageIsInvokedWithClassifierWithoutRepackageTheArchiveIsRepackagedOnTheFly(MavenBuild mavenBuild) { + mavenBuild.project("build-image-classifier").goals("package") + .systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT") + .prepare(this::writeLongNameResource).execute((project) -> { + File jar = new File(project, "target/build-image-classifier-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(jar).isFile(); + File classifier = new File(project, "target/build-image-classifier-0.0.1.BUILD-SNAPSHOT-test.jar"); + assertThat(classifier).doesNotExist(); + assertThat(buildLog(project)).contains("Building image") + .contains("docker.io/library/build-image-classifier:0.0.1.BUILD-SNAPSHOT") + .contains("---> Test Info buildpack building").contains("env: BP_JVM_VERSION=8.*") + .contains("---> Test Info buildpack done").contains("Successfully built image"); + removeImage("build-image-classifier", "0.0.1.BUILD-SNAPSHOT"); + }); + } + + @TestTemplate + void whenBuildImageIsInvokedWithClassifierSourceWithoutRepackageTheArchiveIsRepackagedOnTheFly( + MavenBuild mavenBuild) { + mavenBuild.project("build-image-classifier-source").goals("package") + .systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT") + .prepare(this::writeLongNameResource).execute((project) -> { + File jar = new File(project, "target/build-image-classifier-source-0.0.1.BUILD-SNAPSHOT-test.jar"); + assertThat(jar).isFile(); + File original = new File(project, + "target/build-image-classifier-source-0.0.1.BUILD-SNAPSHOT-test.jar.original"); + assertThat(original).doesNotExist(); + assertThat(buildLog(project)).contains("Building image") + .contains("docker.io/library/build-image-classifier-source:0.0.1.BUILD-SNAPSHOT") + .contains("---> Test Info buildpack building").contains("---> Test Info buildpack done") + .contains("Successfully built image"); + removeImage("build-image-classifier-source", "0.0.1.BUILD-SNAPSHOT"); + }); + } + + @TestTemplate + void whenBuildImageIsInvokedWithRepackageTheExistingArchiveIsUsed(MavenBuild mavenBuild) { + mavenBuild.project("build-image-with-repackage").goals("package") + .systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT") + .prepare(this::writeLongNameResource).execute((project) -> { + File jar = new File(project, "target/build-image-with-repackage-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(jar).isFile(); + File original = new File(project, + "target/build-image-with-repackage-0.0.1.BUILD-SNAPSHOT.jar.original"); + assertThat(original).isFile(); + assertThat(buildLog(project)).contains("Building image") + .contains("docker.io/library/build-image-with-repackage:0.0.1.BUILD-SNAPSHOT") + .contains("---> Test Info buildpack building").contains("---> Test Info buildpack done") + .contains("Successfully built image"); + removeImage("build-image-with-repackage", "0.0.1.BUILD-SNAPSHOT"); + }); + } + + @TestTemplate + void whenBuildImageIsInvokedWithClassifierAndRepackageTheExistingArchiveIsUsed(MavenBuild mavenBuild) { + mavenBuild.project("build-image-classifier-with-repackage").goals("package") + .systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT") + .prepare(this::writeLongNameResource).execute((project) -> { + File jar = new File(project, + "target/build-image-classifier-with-repackage-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(jar).isFile(); + File original = new File(project, + "target/build-image-classifier-with-repackage-0.0.1.BUILD-SNAPSHOT-test.jar"); + assertThat(original).isFile(); + assertThat(buildLog(project)).contains("Building image") + .contains("docker.io/library/build-image-classifier-with-repackage:0.0.1.BUILD-SNAPSHOT") + .contains("---> Test Info buildpack building").contains("---> Test Info buildpack done") + .contains("Successfully built image"); + removeImage("build-image-classifier-with-repackage", "0.0.1.BUILD-SNAPSHOT"); + }); + } + + @TestTemplate + void whenBuildImageIsInvokedWithClassifierSourceAndRepackageTheExistingArchiveIsUsed(MavenBuild mavenBuild) { + mavenBuild.project("build-image-classifier-source-with-repackage").goals("package") + .systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT") + .prepare(this::writeLongNameResource).execute((project) -> { + File jar = new File(project, + "target/build-image-classifier-source-with-repackage-0.0.1.BUILD-SNAPSHOT-test.jar"); + assertThat(jar).isFile(); + File original = new File(project, + "target/build-image-classifier-source-with-repackage-0.0.1.BUILD-SNAPSHOT-test.jar.original"); + assertThat(original).isFile(); + assertThat(buildLog(project)).contains("Building image").contains( + "docker.io/library/build-image-classifier-source-with-repackage:0.0.1.BUILD-SNAPSHOT") + .contains("---> Test Info buildpack building").contains("---> Test Info buildpack done") + .contains("Successfully built image"); + removeImage("build-image-classifier-source-with-repackage", "0.0.1.BUILD-SNAPSHOT"); + }); + } + + @TestTemplate + void whenBuildImageIsInvokedWithWarPackaging(MavenBuild mavenBuild) { + mavenBuild.project("build-image-war-packaging").goals("package") + .systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT") + .prepare(this::writeLongNameResource).execute((project) -> { + File war = new File(project, "target/build-image-war-packaging-0.0.1.BUILD-SNAPSHOT.war"); + assertThat(war).isFile(); + File original = new File(project, + "target/build-image-war-packaging-0.0.1.BUILD-SNAPSHOT.war.original"); + assertThat(original).doesNotExist(); + assertThat(buildLog(project)).contains("Building image") + .contains("docker.io/library/build-image-war-packaging:0.0.1.BUILD-SNAPSHOT") + .contains("---> Test Info buildpack building").contains("---> Test Info buildpack done") + .contains("Successfully built image"); + removeImage("build-image-war-packaging", "0.0.1.BUILD-SNAPSHOT"); + }); + } + + @TestTemplate + void whenBuildImageIsInvokedWithCustomImageName(MavenBuild mavenBuild) { + mavenBuild.project("build-image-custom-name").goals("package") + .systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT") + .systemProperty("spring-boot.build-image.imageName", "example.com/test/property-ignored:pom-preferred") + .execute((project) -> { + File jar = new File(project, "target/build-image-custom-name-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(jar).isFile(); + File original = new File(project, + "target/build-image-custom-name-0.0.1.BUILD-SNAPSHOT.jar.original"); + assertThat(original).doesNotExist(); + assertThat(buildLog(project)).contains("Building image") + .contains("example.com/test/build-image:0.0.1.BUILD-SNAPSHOT") + .contains("---> Test Info buildpack building").contains("---> Test Info buildpack done") + .contains("Successfully built image"); + removeImage("example.com/test/build-image", "0.0.1.BUILD-SNAPSHOT"); + }); + } + + @TestTemplate + void whenBuildImageIsInvokedWithCommandLineParameters(MavenBuild mavenBuild) { + mavenBuild.project("build-image").goals("package") + .systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT") + .systemProperty("spring-boot.build-image.imageName", "example.com/test/cmd-property-name:v1") + .systemProperty("spring-boot.build-image.builder", + "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1") + .systemProperty("spring-boot.build-image.runImage", + "projects.registry.vmware.com/springboot/run:tiny-cnb") + .execute((project) -> { + assertThat(buildLog(project)).contains("Building image") + .contains("example.com/test/cmd-property-name:v1") + .contains("---> Test Info buildpack building").contains("---> Test Info buildpack done") + .contains("Successfully built image"); + removeImage("example.com/test/cmd-property-name", "v1"); + }); + } + + @TestTemplate + void whenBuildImageIsInvokedWithCustomBuilderImageAndRunImage(MavenBuild mavenBuild) { + mavenBuild.project("build-image-custom-builder").goals("package") + .systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT").execute((project) -> { + assertThat(buildLog(project)).contains("Building image") + .contains("docker.io/library/build-image-v2-builder:0.0.1.BUILD-SNAPSHOT") + .contains("---> Test Info buildpack building").contains("---> Test Info buildpack done") + .contains("Successfully built image"); + removeImage("docker.io/library/build-image-v2-builder", "0.0.1.BUILD-SNAPSHOT"); + }); + } + + @TestTemplate + void whenBuildImageIsInvokedWithEmptyEnvEntry(MavenBuild mavenBuild) { + mavenBuild.project("build-image-empty-env-entry").goals("package") + .systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT") + .prepare(this::writeLongNameResource).execute((project) -> { + assertThat(buildLog(project)).contains("Building image") + .contains("docker.io/library/build-image-empty-env-entry:0.0.1.BUILD-SNAPSHOT") + .contains("---> Test Info buildpack building").contains("---> Test Info buildpack done") + .contains("Successfully built image"); + removeImage("build-image-empty-env-entry", "0.0.1.BUILD-SNAPSHOT"); + }); + } + + @TestTemplate + void whenBuildImageIsInvokedWithZipPackaging(MavenBuild mavenBuild) { + mavenBuild.project("build-image-zip-packaging").goals("package").prepare(this::writeLongNameResource) + .systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT").execute((project) -> { + File jar = new File(project, "target/build-image-zip-packaging-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(jar).isFile(); + assertThat(buildLog(project)).contains("Building image") + .contains("docker.io/library/build-image-zip-packaging:0.0.1.BUILD-SNAPSHOT") + .contains("Main-Class: org.springframework.boot.loader.PropertiesLauncher") + .contains("Successfully built image"); + removeImage("build-image-zip-packaging", "0.0.1.BUILD-SNAPSHOT"); + }); + } + + @TestTemplate + void whenBuildImageIsInvokedWithBuildpacks(MavenBuild mavenBuild) { + mavenBuild.project("build-image-custom-buildpacks").goals("package") + .systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT").execute((project) -> { + assertThat(buildLog(project)).contains("Building image") + .contains("docker.io/library/build-image-custom-buildpacks:0.0.1.BUILD-SNAPSHOT") + .contains("Successfully built image"); + removeImage("build-image-custom-buildpacks", "0.0.1.BUILD-SNAPSHOT"); + }); + } + + @TestTemplate + void whenBuildImageIsInvokedWithBinding(MavenBuild mavenBuild) { + mavenBuild.project("build-image-bindings").goals("package") + .systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT").execute((project) -> { + assertThat(buildLog(project)).contains("Building image") + .contains("docker.io/library/build-image-bindings:0.0.1.BUILD-SNAPSHOT") + .contains("binding: ca-certificates/type=ca-certificates") + .contains("binding: ca-certificates/test.crt=---certificate one---") + .contains("Successfully built image"); + removeImage("build-image-bindings", "0.0.1.BUILD-SNAPSHOT"); + }); + } + + @TestTemplate + void whenBuildImageIsInvokedOnMultiModuleProjectWithPackageGoal(MavenBuild mavenBuild) { + mavenBuild.project("build-image-multi-module").goals("package") + .systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT").execute((project) -> { + assertThat(buildLog(project)).contains("Building image") + .contains("docker.io/library/build-image-multi-module-app:0.0.1.BUILD-SNAPSHOT") + .contains("Successfully built image"); + removeImage("build-image-multi-module-app", "0.0.1.BUILD-SNAPSHOT"); + }); + } + + @TestTemplate + void failsWhenBuildImageIsInvokedOnMultiModuleProjectWithBuildImageGoal(MavenBuild mavenBuild) { + mavenBuild.project("build-image-multi-module").goals("spring-boot:build-image") + .systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT").executeAndFail( + (project) -> assertThat(buildLog(project)).contains("Error packaging archive for image")); + } + + @TestTemplate + void failsWhenPublishWithoutPublishRegistryConfigured(MavenBuild mavenBuild) { + mavenBuild.project("build-image").goals("package").systemProperty("spring-boot.build-image.publish", "true") + .executeAndFail((project) -> assertThat(buildLog(project)).contains("requires docker.publishRegistry")); + } + + @TestTemplate + void failsWhenBuilderFails(MavenBuild mavenBuild) { + mavenBuild.project("build-image-builder-error").goals("package") + .systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT") + .executeAndFail((project) -> assertThat(buildLog(project)).contains("Building image") + .contains("---> Test Info buildpack building").contains("Forced builder failure") + .containsPattern("Builder lifecycle '.*' failed with status code")); + } + + @TestTemplate + void failsWithBuildpackNotInBuilder(MavenBuild mavenBuild) { + mavenBuild.project("build-image-bad-buildpack").goals("package") + .systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT") + .executeAndFail((project) -> assertThat(buildLog(project)) + .contains("'urn:cnb:builder:example/does-not-exist:0.0.1' not found in builder")); + } + + @TestTemplate + void failsWhenFinalNameIsMisconfigured(MavenBuild mavenBuild) { + mavenBuild.project("build-image-final-name").goals("package") + .executeAndFail((project) -> assertThat(buildLog(project)).contains("final-name.jar.original") + .contains("is required for building an image")); + } + + private void writeLongNameResource(File project) { + StringBuilder name = new StringBuilder(); + new Random().ints('a', 'z' + 1).limit(128).forEach((i) -> name.append((char) i)); + try { + Path path = project.toPath().resolve(Paths.get("src", "main", "resources", name.toString())); + Files.createDirectories(path.getParent()); + Files.createFile(path); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + private void removeImage(String name, String version) { + ImageReference imageReference = ImageReference.of(ImageName.of(name), version); + try { + new DockerApi().image().remove(imageReference, false); + } + catch (IOException ex) { + throw new IllegalStateException("Failed to remove docker image " + imageReference, ex); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildInfoIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildInfoIntegrationTests.java new file mode 100644 index 000000000000..6cdcfcc3a320 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildInfoIntegrationTests.java @@ -0,0 +1,150 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.maven; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.Properties; +import java.util.function.Consumer; + +import org.assertj.core.api.AbstractMapAssert; +import org.assertj.core.api.AssertProvider; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.maven.MavenBuild.ProjectCallback; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for the Maven plugin's build info support. + * + * @author Andy Wilkinson + */ +@ExtendWith(MavenBuildExtension.class) +class BuildInfoIntegrationTests { + + @TestTemplate + void buildInfoPropertiesAreGenerated(MavenBuild mavenBuild) { + mavenBuild.project("build-info").execute(buildInfo((buildInfo) -> assertThat(buildInfo) + .hasBuildGroup("org.springframework.boot.maven.it").hasBuildArtifact("build-info") + .hasBuildName("Generate build info").hasBuildVersion("0.0.1.BUILD-SNAPSHOT").containsBuildTime())); + } + + @TestTemplate + void generatedBuildInfoIncludesAdditionalProperties(MavenBuild mavenBuild) { + mavenBuild.project("build-info-additional-properties").execute(buildInfo((buildInfo) -> assertThat(buildInfo) + .hasBuildGroup("org.springframework.boot.maven.it").hasBuildArtifact("build-info-additional-properties") + .hasBuildName("Generate build info with additional properties").hasBuildVersion("0.0.1.BUILD-SNAPSHOT") + .containsBuildTime().containsEntry("build.foo", "bar").containsEntry("build.encoding", "UTF-8") + .containsEntry("build.java.source", "1.8"))); + } + + @TestTemplate + void generatedBuildInfoUsesCustomBuildTime(MavenBuild mavenBuild) { + mavenBuild.project("build-info-custom-build-time").execute(buildInfo((buildInfo) -> assertThat(buildInfo) + .hasBuildGroup("org.springframework.boot.maven.it").hasBuildArtifact("build-info-custom-build-time") + .hasBuildName("Generate build info with custom build time").hasBuildVersion("0.0.1.BUILD-SNAPSHOT") + .hasBuildTime("2019-07-08T08:00:00Z"))); + } + + @TestTemplate + void buildInfoPropertiesAreGeneratedToCustomOutputLocation(MavenBuild mavenBuild) { + mavenBuild.project("build-info-custom-file") + .execute(buildInfo("target/build.info", + (buildInfo) -> assertThat(buildInfo).hasBuildGroup("org.springframework.boot.maven.it") + .hasBuildArtifact("build-info-custom-file").hasBuildName("Generate custom build info") + .hasBuildVersion("0.0.1.BUILD-SNAPSHOT").containsBuildTime())); + } + + @TestTemplate + void whenBuildTimeIsDisabledIfDoesNotAppearInGeneratedBuildInfo(MavenBuild mavenBuild) { + mavenBuild.project("build-info-disable-build-time").execute(buildInfo((buildInfo) -> assertThat(buildInfo) + .hasBuildGroup("org.springframework.boot.maven.it").hasBuildArtifact("build-info-disable-build-time") + .hasBuildName("Generate build info with disabled build time").hasBuildVersion("0.0.1.BUILD-SNAPSHOT") + .doesNotContainBuildTime())); + } + + private ProjectCallback buildInfo(Consumer> buildInfo) { + return buildInfo("target/classes/META-INF/build-info.properties", buildInfo); + } + + private ProjectCallback buildInfo(String location, Consumer> buildInfo) { + return (project) -> buildInfo.accept((buildInfo(project, location))); + } + + private AssertProvider buildInfo(File project, String buildInfo) { + return new AssertProvider() { + + @Override + @Deprecated + public BuildInfoAssert assertThat() { + return new BuildInfoAssert(new File(project, buildInfo)); + } + + }; + } + + private static final class BuildInfoAssert extends AbstractMapAssert { + + private BuildInfoAssert(File actual) { + super(loadProperties(actual), BuildInfoAssert.class); + } + + private static Properties loadProperties(File file) { + try (FileReader reader = new FileReader(file)) { + Properties properties = new Properties(); + properties.load(reader); + return properties; + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + BuildInfoAssert hasBuildGroup(String expected) { + return containsEntry("build.group", expected); + } + + BuildInfoAssert hasBuildArtifact(String expected) { + return containsEntry("build.artifact", expected); + } + + BuildInfoAssert hasBuildName(String expected) { + return containsEntry("build.name", expected); + } + + BuildInfoAssert hasBuildVersion(String expected) { + return containsEntry("build.version", expected); + } + + BuildInfoAssert containsBuildTime() { + return containsKey("build.time"); + } + + BuildInfoAssert doesNotContainBuildTime() { + return doesNotContainKey("build.time"); + } + + BuildInfoAssert hasBuildTime(String expected) { + return containsEntry("build.time", expected); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/EclipseM2eIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/EclipseM2eIntegrationTests.java new file mode 100644 index 000000000000..255127377e7e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/EclipseM2eIntegrationTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.maven; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Comparator; + +import org.junit.jupiter.api.Test; + +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests to check that our plugin works well with Eclipse m2e. + * + * @author Phillip Webb + */ +class EclipseM2eIntegrationTests { + + @Test // gh-21992 + void pluginPomIncludesOptionalShadeDependency() throws Exception { + String version = new Versions().get("project.version"); + File repository = new File("build/int-test-maven-repository"); + File pluginDirectory = new File(repository, "org/springframework/boot/spring-boot-maven-plugin/" + version); + File[] pomFiles = pluginDirectory.listFiles(this::isPomFile); + Arrays.sort(pomFiles, Comparator.comparing(File::getName)); + File pomFile = pomFiles[pomFiles.length - 1]; + String pomContent = new String(FileCopyUtils.copyToByteArray(pomFile), StandardCharsets.UTF_8); + assertThat(pomContent).contains("maven-shade-plugin"); + } + + private boolean isPomFile(File file) { + return file.getName().endsWith(".pom"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java new file mode 100644 index 000000000000..e08548d388da --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java @@ -0,0 +1,408 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.maven; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.jar.JarFile; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.loader.tools.FileUtils; +import org.springframework.boot.loader.tools.JarModeLibrary; +import org.springframework.util.FileSystemUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for the Maven plugin's jar support. + * + * @author Andy Wilkinson + * @author Madhura Bhave + * @author Scott Frederick + */ +@ExtendWith(MavenBuildExtension.class) +class JarIntegrationTests extends AbstractArchiveIntegrationTests { + + @Override + protected String getLayersIndexLocation() { + return "BOOT-INF/layers.idx"; + } + + @TestTemplate + void whenJarIsRepackagedInPlaceOnlyRepackagedJarIsInstalled(MavenBuild mavenBuild) { + mavenBuild.project("jar").goals("install").execute((project) -> { + File original = new File(project, "target/jar-0.0.1.BUILD-SNAPSHOT.jar.original"); + assertThat(original).isFile(); + File repackaged = new File(project, "target/jar-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(launchScript(repackaged)).isEmpty(); + assertThat(jar(repackaged)).manifest((manifest) -> { + manifest.hasMainClass("org.springframework.boot.loader.JarLauncher"); + manifest.hasStartClass("some.random.Main"); + manifest.hasAttribute("Not-Used", "Foo"); + }).hasEntryWithNameStartingWith("BOOT-INF/lib/spring-context") + .hasEntryWithNameStartingWith("BOOT-INF/lib/spring-core") + .hasEntryWithNameStartingWith("BOOT-INF/lib/spring-jcl") + .hasEntryWithNameStartingWith("BOOT-INF/lib/jakarta.servlet-api-4") + .hasEntryWithName("BOOT-INF/classes/org/test/SampleApplication.class") + .hasEntryWithName("org/springframework/boot/loader/JarLauncher.class"); + assertThat(buildLog(project)).contains("Replacing main artifact with repackaged archive") + .contains("Installing " + repackaged + " to").doesNotContain("Installing " + original + " to"); + }); + } + + @TestTemplate + void whenAttachIsDisabledOnlyTheOriginalJarIsInstalled(MavenBuild mavenBuild) { + mavenBuild.project("jar-attach-disabled").goals("install").execute((project) -> { + File original = new File(project, "target/jar-attach-disabled-0.0.1.BUILD-SNAPSHOT.jar.original"); + assertThat(original).isFile(); + File main = new File(project, "target/jar-attach-disabled-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(main).isFile(); + assertThat(buildLog(project)).contains("Updating main artifact " + main + " to " + original) + .contains("Installing " + original + " to").doesNotContain("Installing " + main + " to"); + }); + } + + @TestTemplate + void whenAClassifierIsConfiguredTheRepackagedJarHasAClassifierAndBothItAndTheOriginalAreInstalled( + MavenBuild mavenBuild) { + mavenBuild.project("jar-classifier-main").goals("install").execute((project) -> { + assertThat(new File(project, "target/jar-classifier-main-0.0.1.BUILD-SNAPSHOT.jar.original")) + .doesNotExist(); + File main = new File(project, "target/jar-classifier-main-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(main).isFile(); + File repackaged = new File(project, "target/jar-classifier-main-0.0.1.BUILD-SNAPSHOT-test.jar"); + assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/classes/"); + assertThat(buildLog(project)) + .contains("Attaching repackaged archive " + repackaged + " with classifier test") + .doesNotContain("Creating repackaged archive " + repackaged + " with classifier test") + .contains("Installing " + main + " to").contains("Installing " + repackaged + " to"); + }); + } + + @TestTemplate + void whenBothJarsHaveTheSameClassifierRepackagingIsDoneInPlaceAndOnlyRepackagedJarIsInstalled( + MavenBuild mavenBuild) { + mavenBuild.project("jar-classifier-source").goals("install").execute((project) -> { + File original = new File(project, "target/jar-classifier-source-0.0.1.BUILD-SNAPSHOT-test.jar.original"); + assertThat(original).isFile(); + File repackaged = new File(project, "target/jar-classifier-source-0.0.1.BUILD-SNAPSHOT-test.jar"); + assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/classes/"); + assertThat(buildLog(project)).contains("Replacing artifact with classifier test with repackaged archive") + .doesNotContain("Installing " + original + " to").contains("Installing " + repackaged + " to"); + }); + } + + @TestTemplate + void whenBothJarsHaveTheSameClassifierAndAttachIsDisabledOnlyTheOriginalJarIsInstalled(MavenBuild mavenBuild) { + mavenBuild.project("jar-classifier-source-attach-disabled").goals("install").execute((project) -> { + File original = new File(project, + "target/jar-classifier-source-attach-disabled-0.0.1.BUILD-SNAPSHOT-test.jar.original"); + assertThat(original).isFile(); + File repackaged = new File(project, + "target/jar-classifier-source-attach-disabled-0.0.1.BUILD-SNAPSHOT-test.jar"); + assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/classes/"); + assertThat(buildLog(project)) + .doesNotContain("Attaching repackaged archive " + repackaged + " with classifier test") + .contains("Updating artifact with classifier test " + repackaged + " to " + original) + .contains("Installing " + original + " to").doesNotContain("Installing " + repackaged + " to"); + }); + } + + @TestTemplate + void whenAClassifierAndAnOutputDirectoryAreConfiguredTheRepackagedJarHasAClassifierAndIsWrittenToTheOutputDirectory( + MavenBuild mavenBuild) { + mavenBuild.project("jar-create-dir").goals("install").execute((project) -> { + File repackaged = new File(project, "target/foo/jar-create-dir-0.0.1.BUILD-SNAPSHOT-foo.jar"); + assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/classes/"); + assertThat(buildLog(project)).contains("Installing " + repackaged + " to"); + }); + } + + @TestTemplate + void whenAnOutputDirectoryIsConfiguredTheRepackagedJarIsWrittenToIt(MavenBuild mavenBuild) { + mavenBuild.project("jar-custom-dir").goals("install").execute((project) -> { + File repackaged = new File(project, "target/foo/jar-custom-dir-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/classes/"); + assertThat(buildLog(project)).contains("Installing " + repackaged + " to"); + }); + } + + @TestTemplate + void whenACustomLaunchScriptIsConfiguredItAppearsInTheRepackagedJar(MavenBuild mavenBuild) { + mavenBuild.project("jar-custom-launcher").goals("install").execute((project) -> { + File repackaged = new File(project, "target/jar-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/classes/"); + assertThat(launchScript(repackaged)).contains("Hello world"); + }); + } + + @TestTemplate + void whenAnEntryIsExcludedItDoesNotAppearInTheRepackagedJar(MavenBuild mavenBuild) { + mavenBuild.project("jar-exclude-entry").goals("install").execute((project) -> { + File repackaged = new File(project, "target/jar-exclude-entry-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/classes/") + .hasEntryWithNameStartingWith("BOOT-INF/lib/spring-context") + .hasEntryWithNameStartingWith("BOOT-INF/lib/spring-core") + .hasEntryWithNameStartingWith("BOOT-INF/lib/spring-jcl") + .doesNotHaveEntryWithName("BOOT-INF/lib/servlet-api-2.5.jar"); + }); + } + + @TestTemplate + void whenAGroupIsExcludedNoEntriesInThatGroupAppearInTheRepackagedJar(MavenBuild mavenBuild) { + mavenBuild.project("jar-exclude-group").goals("install").execute((project) -> { + File repackaged = new File(project, "target/jar-exclude-group-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/classes/") + .hasEntryWithNameStartingWith("BOOT-INF/lib/spring-context") + .hasEntryWithNameStartingWith("BOOT-INF/lib/spring-core") + .hasEntryWithNameStartingWith("BOOT-INF/lib/spring-jcl") + .doesNotHaveEntryWithName("BOOT-INF/lib/log4j-api-2.4.1.jar"); + }); + } + + @TestTemplate + void whenAJarIsExecutableItBeginsWithTheDefaultLaunchScript(MavenBuild mavenBuild) { + mavenBuild.project("jar-executable").execute((project) -> { + File repackaged = new File(project, "target/jar-executable-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/classes/"); + assertThat(launchScript(repackaged)).contains("Spring Boot Startup Script") + .contains("MyFullyExecutableJarName").contains("MyFullyExecutableJarDesc"); + }); + } + + @TestTemplate + void whenAJarIsBuiltWithLibrariesWithConflictingNamesTheyAreMadeUniqueUsingTheirGroupIds(MavenBuild mavenBuild) { + mavenBuild.project("jar-lib-name-conflict").execute((project) -> { + File repackaged = new File(project, "test-project/target/test-project-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/classes/") + .hasEntryWithName( + "BOOT-INF/lib/org.springframework.boot.maven.it-acme-lib-0.0.1.BUILD-SNAPSHOT.jar") + .hasEntryWithName( + "BOOT-INF/lib/org.springframework.boot.maven.it.another-acme-lib-0.0.1.BUILD-SNAPSHOT.jar"); + }); + } + + @TestTemplate + void whenAProjectUsesPomPackagingRepackagingIsSkipped(MavenBuild mavenBuild) { + mavenBuild.project("jar-pom").execute((project) -> { + File target = new File(project, "target"); + assertThat(target.listFiles()).containsExactly(new File(target, "build.log")); + }); + } + + @TestTemplate + void whenRepackagingIsSkippedTheJarIsNotRepackaged(MavenBuild mavenBuild) { + mavenBuild.project("jar-skip").execute((project) -> { + File main = new File(project, "target/jar-skip-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(jar(main)).doesNotHaveEntryWithNameStartingWith("org/springframework/boot"); + assertThat(new File(project, "target/jar-skip-0.0.1.BUILD-SNAPSHOT.jar.original")).doesNotExist(); + + }); + } + + @TestTemplate + void whenADependencyHasSystemScopeAndInclusionOfSystemScopeDependenciesIsEnabledItIsIncludedInTheRepackagedJar( + MavenBuild mavenBuild) { + mavenBuild.project("jar-system-scope").execute((project) -> { + File main = new File(project, "target/jar-system-scope-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(jar(main)).hasEntryWithName("BOOT-INF/lib/sample-1.0.0.jar"); + + }); + } + + @TestTemplate + void whenADependencyHasSystemScopeItIsNotIncludedInTheRepackagedJar(MavenBuild mavenBuild) { + mavenBuild.project("jar-system-scope-default").execute((project) -> { + File main = new File(project, "target/jar-system-scope-default-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(jar(main)).doesNotHaveEntryWithName("BOOT-INF/lib/sample-1.0.0.jar"); + + }); + } + + @TestTemplate + void whenADependencyHasTestScopeItIsNotIncludedInTheRepackagedJar(MavenBuild mavenBuild) { + mavenBuild.project("jar-test-scope").execute((project) -> { + File main = new File(project, "target/jar-test-scope-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(jar(main)).doesNotHaveEntryWithNameStartingWith("BOOT-INF/lib/log4j") + .hasEntryWithNameStartingWith("BOOT-INF/lib/spring-"); + }); + } + + @TestTemplate + void whenAProjectUsesKotlinItsModuleMetadataIsRepackagedIntoBootInfClasses(MavenBuild mavenBuild) { + mavenBuild.project("jar-with-kotlin-module").execute((project) -> { + File main = new File(project, "target/jar-with-kotlin-module-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(jar(main)).hasEntryWithName("BOOT-INF/classes/META-INF/jar-with-kotlin-module.kotlin_module"); + }); + } + + @TestTemplate + void whenAProjectIsBuiltWithALayoutPropertyTheSpecifiedLayoutIsUsed(MavenBuild mavenBuild) { + mavenBuild.project("jar-with-layout-property").goals("package", "-Dspring-boot.repackage.layout=ZIP") + .execute((project) -> { + File main = new File(project, "target/jar-with-layout-property-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(jar(main)).manifest( + (manifest) -> manifest.hasMainClass("org.springframework.boot.loader.PropertiesLauncher") + .hasStartClass("org.test.SampleApplication")); + assertThat(buildLog(project)).contains("Layout: ZIP"); + }); + } + + @TestTemplate + void whenALayoutIsConfiguredTheSpecifiedLayoutIsUsed(MavenBuild mavenBuild) { + mavenBuild.project("jar-with-zip-layout").execute((project) -> { + File main = new File(project, "target/jar-with-zip-layout-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(jar(main)) + .manifest((manifest) -> manifest.hasMainClass("org.springframework.boot.loader.PropertiesLauncher") + .hasStartClass("org.test.SampleApplication")); + assertThat(buildLog(project)).contains("Layout: ZIP"); + }); + } + + @TestTemplate + void whenRequiresUnpackConfigurationIsProvidedItIsReflectedInTheRepackagedJar(MavenBuild mavenBuild) { + mavenBuild.project("jar-with-unpack").execute((project) -> { + File main = new File(project, "target/jar-with-unpack-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(jar(main)).hasUnpackEntryWithNameStartingWith("BOOT-INF/lib/spring-core-") + .hasEntryWithNameStartingWith("BOOT-INF/lib/spring-context-"); + }); + } + + @TestTemplate + void whenJarIsRepackagedWithACustomLayoutTheJarUsesTheLayout(MavenBuild mavenBuild) { + mavenBuild.project("jar-custom-layout").execute((project) -> { + assertThat(jar(new File(project, "custom/target/custom-0.0.1.BUILD-SNAPSHOT.jar"))) + .hasEntryWithName("custom"); + assertThat(jar(new File(project, "default/target/default-0.0.1.BUILD-SNAPSHOT.jar"))) + .hasEntryWithName("sample"); + }); + } + + @TestTemplate + void repackagedJarContainsTheLayersIndexByDefault(MavenBuild mavenBuild) { + mavenBuild.project("jar-layered").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").hasEntryWithNameStartingWith( + "BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getCoordinates().getArtifactId()); + try (JarFile jarFile = new JarFile(repackaged)) { + Map> layerIndex = readLayerIndex(jarFile); + assertThat(layerIndex.keySet()).containsExactly("dependencies", "spring-boot-loader", + "snapshot-dependencies", "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"); + assertThat(layerIndex.get("dependencies")) + .anyMatch((dependency) -> dependency.startsWith("BOOT-INF/lib/log4j-api-2")); + } + catch (IOException ex) { + } + }); + } + + @TestTemplate + void whenJarIsRepackagedWithTheLayersDisabledDoesNotContainLayersIndex(MavenBuild mavenBuild) { + mavenBuild.project("jar-layered-disabled").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") + .doesNotHaveEntryWithName("BOOT-INF/layers.idx") + .doesNotHaveEntryWithNameStartingWith("BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName()); + }); + } + + @TestTemplate + void whenJarIsRepackagedWithTheLayersEnabledAndLayerToolsExcluded(MavenBuild mavenBuild) { + mavenBuild.project("jar-layered-no-layer-tools").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") + .hasEntryWithNameStartingWith("BOOT-INF/layers.idx") + .doesNotHaveEntryWithNameStartingWith("BOOT-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName()); + }); + } + + @TestTemplate + void whenJarIsRepackagedWithTheCustomLayers(MavenBuild mavenBuild) { + mavenBuild.project("jar-layered-custom").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"); + } + }); + } + + @TestTemplate + void repackagedJarContainsClasspathIndex(MavenBuild mavenBuild) { + mavenBuild.project("jar").execute((project) -> { + File repackaged = new File(project, "target/jar-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(jar(repackaged)).manifest( + (manifest) -> manifest.hasAttribute("Spring-Boot-Classpath-Index", "BOOT-INF/classpath.idx")); + assertThat(jar(repackaged)).hasEntryWithName("BOOT-INF/classpath.idx"); + }); + } + + @TestTemplate + void whenJarIsRepackagedWithOutputTimestampConfiguredThenJarIsReproducible(MavenBuild mavenBuild) + throws InterruptedException { + String firstHash = buildJarWithOutputTimestamp(mavenBuild); + Thread.sleep(1500); + String secondHash = buildJarWithOutputTimestamp(mavenBuild); + assertThat(firstHash).isEqualTo(secondHash); + } + + private String buildJarWithOutputTimestamp(MavenBuild mavenBuild) { + AtomicReference jarHash = new AtomicReference<>(); + mavenBuild.project("jar-output-timestamp").execute((project) -> { + File repackaged = new File(project, "target/jar-output-timestamp-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(repackaged).isFile(); + assertThat(repackaged.lastModified()).isEqualTo(1584352800000L); + try (JarFile jar = new JarFile(repackaged)) { + List unreproducibleEntries = jar.stream() + .filter((entry) -> entry.getLastModifiedTime().toMillis() != 1584352800000L) + .map((entry) -> entry.getName() + ": " + entry.getLastModifiedTime()) + .collect(Collectors.toList()); + assertThat(unreproducibleEntries).isEmpty(); + jarHash.set(FileUtils.sha1Hash(repackaged)); + FileSystemUtils.deleteRecursively(project); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + }); + return jarHash.get(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/MavenBuild.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/MavenBuild.java new file mode 100644 index 000000000000..93ab76e2aca1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/MavenBuild.java @@ -0,0 +1,215 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.maven; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; + +import org.apache.maven.shared.invoker.DefaultInvocationRequest; +import org.apache.maven.shared.invoker.DefaultInvoker; +import org.apache.maven.shared.invoker.InvocationRequest; +import org.apache.maven.shared.invoker.InvocationResult; +import org.apache.maven.shared.invoker.Invoker; +import org.apache.maven.shared.invoker.MavenInvocationException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.contentOf; + +/** + * Helper class for executing a Maven build. + * + * @author Andy Wilkinson + * @author Scott Frederick + */ +class MavenBuild { + + private final File home; + + private final File temp; + + private final Map pomReplacements; + + private final List goals = new ArrayList<>(); + + private final Properties properties = new Properties(); + + private ProjectCallback preparation; + + private File projectDir; + + MavenBuild(File home) { + this.home = home; + this.temp = createTempDirectory(); + this.pomReplacements = getPomReplacements(); + } + + private File createTempDirectory() { + try { + return Files.createTempDirectory("maven-build").toFile().getCanonicalFile(); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + private Map getPomReplacements() { + Map replacements = new HashMap<>(); + replacements.put("java.version", "1.8"); + replacements.put("project.groupId", "org.springframework.boot"); + replacements.put("project.artifactId", "spring-boot-maven-plugin"); + replacements.putAll(new Versions().asMap()); + return Collections.unmodifiableMap(replacements); + } + + MavenBuild project(String project) { + this.projectDir = new File("src/intTest/projects/" + project); + return this; + } + + MavenBuild goals(String... goals) { + this.goals.addAll(Arrays.asList(goals)); + return this; + } + + MavenBuild systemProperty(String name, String value) { + this.properties.setProperty(name, value); + return this; + } + + MavenBuild prepare(ProjectCallback callback) { + this.preparation = callback; + return this; + } + + void execute(ProjectCallback callback) { + execute(callback, 0); + } + + void executeAndFail(ProjectCallback callback) { + execute(callback, 1); + } + + private void execute(ProjectCallback callback, int expectedExitCode) { + Invoker invoker = new DefaultInvoker(); + invoker.setMavenHome(this.home); + InvocationRequest request = new DefaultInvocationRequest(); + try { + Path destination = this.temp.toPath(); + Path source = this.projectDir.toPath(); + Files.walkFileTree(source, new SimpleFileVisitor() { + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + Files.createDirectories(destination.resolve(source.relativize(dir))); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if (file.toFile().getName().equals("pom.xml")) { + String pomXml = new String(Files.readAllBytes(file), StandardCharsets.UTF_8); + for (Entry replacement : MavenBuild.this.pomReplacements.entrySet()) { + pomXml = pomXml.replace("@" + replacement.getKey() + "@", replacement.getValue()); + } + Files.write(destination.resolve(source.relativize(file)), + pomXml.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE_NEW); + } + else { + Files.copy(file, destination.resolve(source.relativize(file)), + StandardCopyOption.REPLACE_EXISTING); + } + return FileVisitResult.CONTINUE; + } + + }); + String settingsXml = new String(Files.readAllBytes(Paths.get("src", "intTest", "projects", "settings.xml")), + StandardCharsets.UTF_8) + .replace("@localCentralUrl@", + new File("build/int-test-maven-repository").toURI().toURL().toString()) + .replace("@localRepositoryPath@", + new File("build/local-maven-repository").getAbsolutePath()); + Files.write(destination.resolve("settings.xml"), settingsXml.getBytes(StandardCharsets.UTF_8), + StandardOpenOption.CREATE_NEW); + request.setBaseDirectory(this.temp); + request.setJavaHome(new File(System.getProperty("java.home"))); + request.setProperties(this.properties); + request.setGoals(this.goals.isEmpty() ? Collections.singletonList("package") : this.goals); + request.setUserSettingsFile(new File(this.temp, "settings.xml")); + request.setUpdateSnapshots(true); + request.setBatchMode(true); + // request.setMavenOpts("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=8000"); + File target = new File(this.temp, "target"); + target.mkdirs(); + if (this.preparation != null) { + this.preparation.doWith(this.temp); + } + File buildLogFile = new File(target, "build.log"); + try (PrintWriter buildLog = new PrintWriter(new FileWriter(buildLogFile))) { + request.setOutputHandler((line) -> { + buildLog.println(line); + buildLog.flush(); + }); + try { + InvocationResult result = invoker.execute(request); + assertThat(result.getExitCode()).as(contentOf(buildLogFile)).isEqualTo(expectedExitCode); + } + catch (MavenInvocationException ex) { + throw new RuntimeException(ex); + } + } + callback.doWith(this.temp); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + /** + * Action to take on a maven project directory. + */ + @FunctionalInterface + public interface ProjectCallback { + + /** + * Take the action on the given project. + * @param project the project directory + * @throws Exception on error + */ + void doWith(File project) throws Exception; + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/MavenBuildExtension.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/MavenBuildExtension.java new file mode 100644 index 000000000000..2e2bde0e8f7b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/MavenBuildExtension.java @@ -0,0 +1,97 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.maven; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.jupiter.api.extension.TestTemplateInvocationContext; +import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider; + +/** + * An {@link Extension} for templated tests that use {@link MavenBuild}. Each templated + * test is run against multiple versions of Maven. + * + * @author Andy Wilkinson + */ +class MavenBuildExtension implements TestTemplateInvocationContextProvider { + + @Override + public boolean supportsTestTemplate(ExtensionContext context) { + return true; + } + + @Override + public Stream provideTestTemplateInvocationContexts(ExtensionContext context) { + try { + return Files.list(Paths.get("build/maven-binaries")).map(MavenVersionTestTemplateInvocationContext::new); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + private static final class MavenVersionTestTemplateInvocationContext implements TestTemplateInvocationContext { + + private final Path mavenHome; + + private MavenVersionTestTemplateInvocationContext(Path mavenHome) { + this.mavenHome = mavenHome; + } + + @Override + public String getDisplayName(int invocationIndex) { + return this.mavenHome.getFileName().toString(); + } + + @Override + public List getAdditionalExtensions() { + return Arrays.asList(new MavenBuildParameterResolver(this.mavenHome)); + } + + } + + private static final class MavenBuildParameterResolver implements ParameterResolver { + + private final Path mavenHome; + + private MavenBuildParameterResolver(Path mavenHome) { + this.mavenHome = mavenHome; + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { + return parameterContext.getParameter().getType().equals(MavenBuild.class); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { + return new MavenBuild(this.mavenHome.toFile()); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/RunIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/RunIntegrationTests.java new file mode 100644 index 000000000000..33719a7bc382 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/RunIntegrationTests.java @@ -0,0 +1,168 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.maven; + +import java.io.File; +import java.lang.reflect.Method; + +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.contentOf; + +/** + * Integration tests for the Maven plugin's run goal. + * + * @author Andy Wilkinson + */ +@ExtendWith(MavenBuildExtension.class) +class RunIntegrationTests { + + @TestTemplate + void whenTheRunGoalIsExecutedTheApplicationIsForkedWithOptimizedJvmArguments(MavenBuild mavenBuild) { + mavenBuild.project("run").goals("spring-boot:run", "-X").execute((project) -> { + String jvmArguments = isJava13OrLater() ? "JVM argument(s): -XX:TieredStopAtLevel=1" + : "JVM argument(s): -Xverify:none -XX:TieredStopAtLevel=1"; + assertThat(buildLog(project)).contains("I haz been run").contains(jvmArguments); + }); + } + + @TestTemplate + void whenForkingIsDisabledAndDevToolsIsPresentDevToolsIsDisabled(MavenBuild mavenBuild) { + mavenBuild.project("run-devtools").goals("spring-boot:run").execute((project) -> assertThat(buildLog(project)) + .contains("I haz been run").contains("Fork mode disabled, devtools will be disabled")); + } + + @TestTemplate + void whenForkingIsDisabledJvmArgumentsAndWorkingDirectoryAreIgnored(MavenBuild mavenBuild) { + mavenBuild.project("run-disable-fork").goals("spring-boot:run") + .execute((project) -> assertThat(buildLog(project)).contains("I haz been run").contains( + "Fork mode disabled, ignoring JVM argument(s) [-Dproperty1=value1 -Dproperty2 -Dfoo=bar]") + .contains("Fork mode disabled, ignoring working directory configuration")); + } + + @TestTemplate + void whenEnvironmentVariablesAreConfiguredTheyAreAvailableToTheApplication(MavenBuild mavenBuild) { + mavenBuild.project("run-envargs").goals("spring-boot:run") + .execute((project) -> assertThat(buildLog(project)).contains("I haz been run")); + } + + @TestTemplate + void whenExclusionsAreConfiguredExcludedDependenciesDoNotAppearOnTheClasspath(MavenBuild mavenBuild) { + mavenBuild.project("run-exclude").goals("spring-boot:run") + .execute((project) -> assertThat(buildLog(project)).contains("I haz been run")); + } + + @TestTemplate + void whenSystemPropertiesAndJvmArgumentsAreConfiguredTheyAreAvailableToTheApplication(MavenBuild mavenBuild) { + mavenBuild.project("run-jvm-system-props").goals("spring-boot:run") + .execute((project) -> assertThat(buildLog(project)).contains("I haz been run")); + } + + @TestTemplate + void whenJvmArgumentsAreConfiguredTheyAreAvailableToTheApplication(MavenBuild mavenBuild) { + mavenBuild.project("run-jvmargs").goals("spring-boot:run") + .execute((project) -> assertThat(buildLog(project)).contains("I haz been run")); + } + + @TestTemplate + void whenCommandLineSpecifiesJvmArgumentsTheyAreAvailableToTheApplication(MavenBuild mavenBuild) { + mavenBuild.project("run-jvmargs-commandline").goals("spring-boot:run") + .systemProperty("spring-boot.run.jvmArguments", "-Dfoo=value-from-cmd") + .execute((project) -> assertThat(buildLog(project)).contains("I haz been run")); + } + + @TestTemplate + void whenPomAndCommandLineSpecifyJvmArgumentsThenPomOverrides(MavenBuild mavenBuild) { + mavenBuild.project("run-jvmargs").goals("spring-boot:run") + .systemProperty("spring-boot.run.jvmArguments", "-Dfoo=value-from-cmd") + .execute((project) -> assertThat(buildLog(project)).contains("I haz been run")); + } + + @TestTemplate + void whenProfilesAreConfiguredTheyArePassedToTheApplication(MavenBuild mavenBuild) { + mavenBuild.project("run-profiles").goals("spring-boot:run", "-X").execute( + (project) -> assertThat(buildLog(project)).contains("I haz been run with profile(s) 'foo,bar'")); + } + + @TestTemplate + void whenProfilesAreConfiguredAndForkingIsDisabledTheyArePassedToTheApplication(MavenBuild mavenBuild) { + mavenBuild.project("run-profiles-fork-disabled").goals("spring-boot:run").execute( + (project) -> assertThat(buildLog(project)).contains("I haz been run with profile(s) 'foo,bar'")); + } + + @TestTemplate + void whenUseTestClasspathIsEnabledTheApplicationHasTestDependenciesOnItsClasspath(MavenBuild mavenBuild) { + mavenBuild.project("run-use-test-classpath").goals("spring-boot:run") + .execute((project) -> assertThat(buildLog(project)).contains("I haz been run")); + } + + @TestTemplate + void whenAWorkingDirectoryIsConfiguredTheApplicationIsRunFromThatDirectory(MavenBuild mavenBuild) { + mavenBuild.project("run-working-directory").goals("spring-boot:run").execute( + (project) -> assertThat(buildLog(project)).containsPattern("I haz been run from.*src.main.java")); + } + + @TestTemplate + @DisabledOnOs(OS.WINDOWS) + void whenAToolchainIsConfiguredItIsUsedToRunTheApplication(MavenBuild mavenBuild) { + mavenBuild.project("run-toolchains").goals("verify", "-t", "toolchains.xml") + .execute((project) -> assertThat(buildLog(project)).contains("The Maven Toolchains is awesome!")); + } + + @TestTemplate + void whenPomSpecifiesRunArgumentsContainingCommasTheyArePassedToTheApplicationCorrectly(MavenBuild mavenBuild) { + mavenBuild.project("run-arguments").goals("spring-boot:run").execute((project) -> assertThat(buildLog(project)) + .contains("I haz been run with profile(s) 'foo,bar' and endpoint(s) 'prometheus,info'")); + } + + @TestTemplate + void whenCommandLineSpecifiesRunArgumentsContainingCommasTheyArePassedToTheApplicationCorrectly( + MavenBuild mavenBuild) { + mavenBuild.project("run-arguments-commandline").goals("spring-boot:run").systemProperty( + "spring-boot.run.arguments", + "--management.endpoints.web.exposure.include=prometheus,info,health,metrics --spring.profiles.active=foo,bar") + .execute((project) -> assertThat(buildLog(project)).contains( + "I haz been run with profile(s) 'foo,bar' and endpoint(s) 'prometheus,info,health,metrics'")); + } + + @TestTemplate + void whenPomAndCommandLineSpecifyRunArgumentsThenPomOverrides(MavenBuild mavenBuild) { + mavenBuild.project("run-arguments").goals("spring-boot:run") + .systemProperty("spring-boot.run.arguments", + "--management.endpoints.web.exposure.include=one,two,three --spring.profiles.active=test") + .execute((project) -> assertThat(buildLog(project)) + .contains("I haz been run with profile(s) 'foo,bar' and endpoint(s) 'prometheus,info'")); + } + + private String buildLog(File project) { + return contentOf(new File(project, "target/build.log")); + } + + private boolean isJava13OrLater() { + for (Method method : String.class.getMethods()) { + if (method.getName().equals("stripIndent")) { + return true; + } + } + return false; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/StartStopIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/StartStopIntegrationTests.java new file mode 100644 index 000000000000..34736018c3fe --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/StartStopIntegrationTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.maven; + +import java.io.File; + +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.contentOf; + +/** + * Integration tests for the Maven plugin's war support. + * + * @author Andy Wilkinson + */ +@ExtendWith(MavenBuildExtension.class) +class StartStopIntegrationTests { + + @TestTemplate + void startStopWithForkDisabledWaitsForApplicationToBeReadyAndThenRequestsShutdown(MavenBuild mavenBuild) { + mavenBuild.project("start-stop-fork-disabled").goals("verify").execute( + (project) -> assertThat(buildLog(project)).contains("isReady: true").contains("Shutdown requested")); + } + + @TestTemplate + void startStopWaitsForApplicationToBeReadyAndThenRequestsShutdown(MavenBuild mavenBuild) { + mavenBuild.project("start-stop").goals("verify").execute( + (project) -> assertThat(buildLog(project)).contains("isReady: true").contains("Shutdown requested")); + } + + @TestTemplate + void whenSkipIsTrueStartAndStopAreSkipped(MavenBuild mavenBuild) { + mavenBuild.project("start-stop-skip").goals("verify").execute((project) -> assertThat(buildLog(project)) + .doesNotContain("Ooops, I haz been run").doesNotContain("Stopping application")); + } + + private String buildLog(File project) { + return contentOf(new File(project, "target/build.log")); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/Versions.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/Versions.java new file mode 100644 index 000000000000..40f9a08a6b45 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/Versions.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.maven; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +/** + * Provides access to various versions. + * + * @author Andy Wilkinson + */ +class Versions { + + private final Map versions; + + Versions() { + this.versions = loadVersions(); + } + + private static Map loadVersions() { + try (InputStream input = Versions.class.getClassLoader().getResourceAsStream("extracted-versions.properties")) { + Properties properties = new Properties(); + properties.load(input); + Map versions = new HashMap<>(); + properties.forEach((key, value) -> versions.put((String) key, (String) value)); + return versions; + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + String get(String name) { + return this.versions.get(name); + } + + Map asMap() { + return Collections.unmodifiableMap(this.versions); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/WarIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/WarIntegrationTests.java new file mode 100644 index 000000000000..653fb321bbba --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/WarIntegrationTests.java @@ -0,0 +1,213 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.maven; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.jar.JarFile; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.loader.tools.FileUtils; +import org.springframework.boot.loader.tools.JarModeLibrary; +import org.springframework.util.FileSystemUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for the Maven plugin's war support. + * + * @author Andy Wilkinson + * @author Scott Frederick + */ +@ExtendWith(MavenBuildExtension.class) +class WarIntegrationTests extends AbstractArchiveIntegrationTests { + + @Override + protected String getLayersIndexLocation() { + return "WEB-INF/layers.idx"; + } + + @TestTemplate + void warRepackaging(MavenBuild mavenBuild) { + mavenBuild.project("war") + .execute((project) -> assertThat(jar(new File(project, "target/war-0.0.1.BUILD-SNAPSHOT.war"))) + .hasEntryWithNameStartingWith("WEB-INF/lib/spring-context") + .hasEntryWithNameStartingWith("WEB-INF/lib/spring-core") + .hasEntryWithNameStartingWith("WEB-INF/lib/spring-jcl") + .hasEntryWithNameStartingWith("WEB-INF/lib-provided/jakarta.servlet-api-4") + .hasEntryWithName("org/springframework/boot/loader/WarLauncher.class") + .hasEntryWithName("WEB-INF/classes/org/test/SampleApplication.class") + .hasEntryWithName("index.html") + .manifest((manifest) -> manifest.hasMainClass("org.springframework.boot.loader.WarLauncher") + .hasStartClass("org.test.SampleApplication").hasAttribute("Not-Used", "Foo"))); + } + + @TestTemplate + void jarDependencyWithCustomFinalNameBuiltInSameReactorIsPackagedUsingArtifactIdAndVersion(MavenBuild mavenBuild) { + mavenBuild.project("war-reactor") + .execute(((project) -> assertThat(jar(new File(project, "war/target/war-0.0.1.BUILD-SNAPSHOT.war"))) + .hasEntryWithName("WEB-INF/lib/jar-0.0.1.BUILD-SNAPSHOT.jar") + .doesNotHaveEntryWithName("WEB-INF/lib/jar.jar"))); + } + + @TestTemplate + void whenRequiresUnpackConfigurationIsProvidedItIsReflectedInTheRepackagedWar(MavenBuild mavenBuild) { + mavenBuild.project("war-with-unpack").execute( + (project) -> assertThat(jar(new File(project, "target/war-with-unpack-0.0.1.BUILD-SNAPSHOT.war"))) + .hasUnpackEntryWithNameStartingWith("WEB-INF/lib/spring-core-") + .hasEntryWithNameStartingWith("WEB-INF/lib/spring-context-") + .hasEntryWithNameStartingWith("WEB-INF/lib/spring-jcl-")); + } + + @TestTemplate + void whenWarIsRepackagedWithOutputTimestampConfiguredThenWarIsReproducible(MavenBuild mavenBuild) + throws InterruptedException { + String firstHash = buildWarWithOutputTimestamp(mavenBuild); + Thread.sleep(1500); + String secondHash = buildWarWithOutputTimestamp(mavenBuild); + assertThat(firstHash).isEqualTo(secondHash); + } + + private String buildWarWithOutputTimestamp(MavenBuild mavenBuild) { + AtomicReference warHash = new AtomicReference<>(); + mavenBuild.project("war-output-timestamp").execute((project) -> { + File repackaged = new File(project, "target/war-output-timestamp-0.0.1.BUILD-SNAPSHOT.war"); + assertThat(repackaged).isFile(); + assertThat(repackaged.lastModified()).isEqualTo(1584352800000L); + try (JarFile jar = new JarFile(repackaged)) { + List unreproducibleEntries = jar.stream() + .filter((entry) -> entry.getLastModifiedTime().toMillis() != 1584352800000L) + .map((entry) -> entry.getName() + ": " + entry.getLastModifiedTime()) + .collect(Collectors.toList()); + assertThat(unreproducibleEntries).isEmpty(); + warHash.set(FileUtils.sha1Hash(repackaged)); + FileSystemUtils.deleteRecursively(project); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + }); + return warHash.get(); + } + + @TestTemplate + void whenADependencyHasSystemScopeAndInclusionOfSystemScopeDependenciesIsEnabledItIsIncludedInTheRepackagedJar( + MavenBuild mavenBuild) { + mavenBuild.project("war-system-scope").execute((project) -> { + File main = new File(project, "target/war-system-scope-0.0.1.BUILD-SNAPSHOT.war"); + assertThat(jar(main)).hasEntryWithName("WEB-INF/lib-provided/sample-1.0.0.jar"); + }); + } + + @TestTemplate + void repackagedWarContainsTheLayersIndexByDefault(MavenBuild mavenBuild) { + mavenBuild.project("war-layered").execute((project) -> { + File repackaged = new File(project, "war/target/war-layered-0.0.1.BUILD-SNAPSHOT.war"); + assertThat(jar(repackaged)).hasEntryWithNameStartingWith("WEB-INF/classes/") + .hasEntryWithNameStartingWith("WEB-INF/lib/jar-release") + .hasEntryWithNameStartingWith("WEB-INF/lib/jar-snapshot").hasEntryWithNameStartingWith( + "WEB-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getCoordinates().getArtifactId()); + try (JarFile jarFile = new JarFile(repackaged)) { + Map> layerIndex = readLayerIndex(jarFile); + assertThat(layerIndex.keySet()).containsExactly("dependencies", "spring-boot-loader", + "snapshot-dependencies", "application"); + List dependenciesAndSnapshotDependencies = new ArrayList<>(); + dependenciesAndSnapshotDependencies.addAll(layerIndex.get("dependencies")); + dependenciesAndSnapshotDependencies.addAll(layerIndex.get("snapshot-dependencies")); + assertThat(layerIndex.get("application")).contains("WEB-INF/lib/jar-release-0.0.1.RELEASE.jar", + "WEB-INF/lib/jar-snapshot-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(dependenciesAndSnapshotDependencies) + .anyMatch((dependency) -> dependency.startsWith("WEB-INF/lib/spring-context")); + assertThat(layerIndex.get("dependencies")) + .anyMatch((dependency) -> dependency.startsWith("WEB-INF/lib-provided/")); + } + catch (IOException ex) { + } + }); + } + + @TestTemplate + void whenWarIsRepackagedWithTheLayersDisabledDoesNotContainLayersIndex(MavenBuild mavenBuild) { + mavenBuild.project("war-layered-disabled").execute((project) -> { + File repackaged = new File(project, "war/target/war-layered-0.0.1.BUILD-SNAPSHOT.war"); + assertThat(jar(repackaged)).hasEntryWithNameStartingWith("WEB-INF/classes/") + .hasEntryWithNameStartingWith("WEB-INF/lib/jar-release") + .hasEntryWithNameStartingWith("WEB-INF/lib/jar-snapshot") + .doesNotHaveEntryWithName("WEB-INF/layers.idx") + .doesNotHaveEntryWithNameStartingWith("WEB-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName()); + }); + } + + @TestTemplate + void whenWarIsRepackagedWithTheLayersEnabledAndLayerToolsExcluded(MavenBuild mavenBuild) { + mavenBuild.project("war-layered-no-layer-tools").execute((project) -> { + File repackaged = new File(project, "war/target/war-layered-0.0.1.BUILD-SNAPSHOT.war"); + assertThat(jar(repackaged)).hasEntryWithNameStartingWith("WEB-INF/classes/") + .hasEntryWithNameStartingWith("WEB-INF/lib/jar-release") + .hasEntryWithNameStartingWith("WEB-INF/lib/jar-snapshot") + .hasEntryWithNameStartingWith("WEB-INF/layers.idx") + .doesNotHaveEntryWithNameStartingWith("WEB-INF/lib/" + JarModeLibrary.LAYER_TOOLS.getName()); + }); + } + + @TestTemplate + void whenWarIsRepackagedWithTheCustomLayers(MavenBuild mavenBuild) { + mavenBuild.project("war-layered-custom").execute((project) -> { + File repackaged = new File(project, "war/target/war-layered-0.0.1.BUILD-SNAPSHOT.war"); + assertThat(jar(repackaged)).hasEntryWithNameStartingWith("WEB-INF/classes/") + .hasEntryWithNameStartingWith("WEB-INF/lib/jar-release") + .hasEntryWithNameStartingWith("WEB-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("WEB-INF/lib/jar-release-0.0.1.RELEASE.jar", + "WEB-INF/lib/jar-snapshot-0.0.1.BUILD-SNAPSHOT.jar", + "WEB-INF/lib/jar-classifier-0.0.1-bravo.jar") + .doesNotContain("WEB-INF/lib/jar-classifier-0.0.1-alpha.jar"); + } + }); + } + + @TestTemplate + void repackagedWarDoesNotContainClasspathIndex(MavenBuild mavenBuild) { + mavenBuild.project("war").execute((project) -> { + File repackaged = new File(project, "target/war-0.0.1.BUILD-SNAPSHOT.war"); + assertThat(jar(repackaged)) + .manifest((manifest) -> manifest.doesNotHaveAttribute("Spring-Boot-Classpath-Index")); + assertThat(jar(repackaged)).doesNotHaveEntryWithName("BOOT-INF/classpath.idx"); + }); + } + + @TestTemplate + void whenEntryIsExcludedItShouldNotBePresentInTheRepackagedWar(MavenBuild mavenBuild) { + mavenBuild.project("war-exclude-entry").execute((project) -> { + File war = new File(project, "target/war-exclude-entry-0.0.1.BUILD-SNAPSHOT.war"); + assertThat(jar(war)).hasEntryWithNameStartingWith("WEB-INF/lib/spring-context") + .doesNotHaveEntryWithNameStartingWith("WEB-INF/lib/spring-core"); + }); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bad-buildpack/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bad-buildpack/pom.xml new file mode 100644 index 000000000000..ead5de9ee1d6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bad-buildpack/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + org.springframework.boot.maven.it + build-image-custom-buildpacks + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + build-image + + + + projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1 + + urn:cnb:builder:example/does-not-exist:0.0.1 + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bad-buildpack/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bad-buildpack/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..e964724deacd --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bad-buildpack/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) throws Exception { + System.out.println("Launched"); + synchronized(args) { + args.wait(); // Prevent exit + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bindings/bindings/ca-certificates/test.crt b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bindings/bindings/ca-certificates/test.crt new file mode 100644 index 000000000000..55229e911fc9 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bindings/bindings/ca-certificates/test.crt @@ -0,0 +1 @@ +---certificate one--- \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bindings/bindings/ca-certificates/type b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bindings/bindings/ca-certificates/type new file mode 100644 index 000000000000..54619edd1661 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bindings/bindings/ca-certificates/type @@ -0,0 +1 @@ +ca-certificates \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bindings/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bindings/pom.xml new file mode 100644 index 000000000000..dc85da540833 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bindings/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + org.springframework.boot.maven.it + build-image-bindings + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + build-image + + + + projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1 + + ${basedir}/bindings/ca-certificates:/platform/bindings/ca-certificates + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bindings/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bindings/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..e964724deacd --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bindings/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) throws Exception { + System.out.println("Launched"); + synchronized(args) { + args.wait(); // Prevent exit + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-builder-error/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-builder-error/pom.xml new file mode 100644 index 000000000000..f4c651275654 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-builder-error/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + org.springframework.boot.maven.it + build-image-builder-error + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + build-image + + + + projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1 + + true + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-builder-error/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-builder-error/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..6825e1a694b1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-builder-error/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) throws Exception { + System.out.println("Launched"); + synchronized(args) { + args.wait(); // Prevent exit + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-classifier-source-with-repackage/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-classifier-source-with-repackage/pom.xml new file mode 100644 index 000000000000..07102b08fd9f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-classifier-source-with-repackage/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + org.springframework.boot.maven.it + build-image-classifier-source-with-repackage + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.2.0 + + + package + + jar + + + test + + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + repackage + + repackage + + + + + build-image + + + + projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1 + + + + + + test + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-classifier-source-with-repackage/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-classifier-source-with-repackage/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..5053809ef1fb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-classifier-source-with-repackage/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) throws Exception { + System.out.println("Launched"); + synchronized(args) { + args.wait(); // Prevent exit" + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-classifier-source/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-classifier-source/pom.xml new file mode 100644 index 000000000000..a46cf9d96b8d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-classifier-source/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + org.springframework.boot.maven.it + build-image-classifier-source + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.2.0 + + + package + + jar + + + test + + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + build-image + + + + projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1 + + + + + + test + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-classifier-source/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-classifier-source/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..5053809ef1fb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-classifier-source/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) throws Exception { + System.out.println("Launched"); + synchronized(args) { + args.wait(); // Prevent exit" + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-classifier-with-repackage/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-classifier-with-repackage/pom.xml new file mode 100644 index 000000000000..29abc0f7e841 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-classifier-with-repackage/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + org.springframework.boot.maven.it + build-image-classifier-with-repackage + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + repackage + + repackage + + + + + build-image + + + + projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1 + + + + + + test + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-classifier-with-repackage/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-classifier-with-repackage/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..5053809ef1fb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-classifier-with-repackage/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) throws Exception { + System.out.println("Launched"); + synchronized(args) { + args.wait(); // Prevent exit" + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-classifier/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-classifier/pom.xml new file mode 100644 index 000000000000..fd54fc672c05 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-classifier/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + org.springframework.boot.maven.it + build-image-classifier + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + build-image + + + + projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1 + + + + + + test + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-classifier/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-classifier/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..5053809ef1fb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-classifier/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) throws Exception { + System.out.println("Launched"); + synchronized(args) { + args.wait(); // Prevent exit" + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-cmd-line/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-cmd-line/pom.xml new file mode 100644 index 000000000000..0451a8426e89 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-cmd-line/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + org.springframework.boot.maven.it + build-image + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + build-image + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-cmd-line/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-cmd-line/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..5053809ef1fb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-cmd-line/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) throws Exception { + System.out.println("Launched"); + synchronized(args) { + args.wait(); // Prevent exit" + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-custom-builder/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-custom-builder/pom.xml new file mode 100644 index 000000000000..4ad4b944e4cb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-custom-builder/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + org.springframework.boot.maven.it + build-image-v2-builder + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + build-image + + + + projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1 + projects.registry.vmware.com/springboot/run:tiny-cnb + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-custom-builder/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-custom-builder/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..6825e1a694b1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-custom-builder/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) throws Exception { + System.out.println("Launched"); + synchronized(args) { + args.wait(); // Prevent exit + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-custom-buildpacks/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-custom-buildpacks/pom.xml new file mode 100644 index 000000000000..e308d28ed5d2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-custom-buildpacks/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + org.springframework.boot.maven.it + build-image-custom-buildpacks + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + build-image + + + + projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1 + + urn:cnb:builder:spring-boot/test-info + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-custom-buildpacks/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-custom-buildpacks/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..e964724deacd --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-custom-buildpacks/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) throws Exception { + System.out.println("Launched"); + synchronized(args) { + args.wait(); // Prevent exit + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-custom-name/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-custom-name/pom.xml new file mode 100644 index 000000000000..fac451852930 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-custom-name/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + org.springframework.boot.maven.it + build-image-custom-name + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + build-image + + + + projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1 + example.com/test/build-image:${project.version} + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-custom-name/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-custom-name/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..27259ff01ad0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-custom-name/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) throws Exception { + System.out.println("Launched"); + synchronized(args) { + args.wait(); // Prevent exit" + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-empty-env-entry/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-empty-env-entry/pom.xml new file mode 100644 index 000000000000..7143fda8e5ca --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-empty-env-entry/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + org.springframework.boot.maven.it + build-image-empty-env-entry + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + build-image + + + + projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1 + + + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-empty-env-entry/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-empty-env-entry/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..27259ff01ad0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-empty-env-entry/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) throws Exception { + System.out.println("Launched"); + synchronized(args) { + args.wait(); // Prevent exit" + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-final-name/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-final-name/pom.xml new file mode 100644 index 000000000000..0acb2e142c0d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-final-name/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + org.springframework.boot.maven.it + build-image-final-name + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + build-image + + + final-name + + projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1 + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-final-name/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-final-name/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..5053809ef1fb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-final-name/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) throws Exception { + System.out.println("Launched"); + synchronized(args) { + args.wait(); // Prevent exit" + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-multi-module/app/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-multi-module/app/pom.xml new file mode 100644 index 000000000000..b62508b86cc7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-multi-module/app/pom.xml @@ -0,0 +1,42 @@ + + + 4.0.0 + + org.springframework.boot.maven.it + build-image-multi-module + 0.0.1.BUILD-SNAPSHOT + + build-image-multi-module-app + app + + + + org.springframework.boot.maven.it + build-image-multi-module-library + 0.0.1.BUILD-SNAPSHOT + + + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + build-image + + + + projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1 + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-multi-module/app/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-multi-module/app/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..a09b075b1c26 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-multi-module/app/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +import org.test.SampleLibrary; + +public class SampleApplication { + + public static void main(String[] args) throws Exception { + System.out.println(SampleLibrary.getMessage()); + synchronized (args) { + args.wait(); // Prevent exit" + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-multi-module/library/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-multi-module/library/pom.xml new file mode 100644 index 000000000000..85907f446000 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-multi-module/library/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + + org.springframework.boot.maven.it + build-image-multi-module + 0.0.1.BUILD-SNAPSHOT + + build-image-multi-module-library + library + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-multi-module/library/src/main/java/org/test/SampleLibrary.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-multi-module/library/src/main/java/org/test/SampleLibrary.java new file mode 100644 index 000000000000..e70a97eca11f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-multi-module/library/src/main/java/org/test/SampleLibrary.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleLibrary { + public static String getMessage() { + return "Launched"; + } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-multi-module/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-multi-module/pom.xml new file mode 100644 index 000000000000..43f482f9629e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-multi-module/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + org.springframework.boot.maven.it + build-image-multi-module + 0.0.1.BUILD-SNAPSHOT + pom + + UTF-8 + @java.version@ + @java.version@ + + + + library + app + + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-publish/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-publish/pom.xml new file mode 100644 index 000000000000..ee4fe387ed05 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-publish/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + org.springframework.boot.maven.it + build-image + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + build-image + + + + true + + + + user + secret + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-publish/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-publish/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..27259ff01ad0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-publish/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) throws Exception { + System.out.println("Launched"); + synchronized(args) { + args.wait(); // Prevent exit" + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-war-packaging/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-war-packaging/pom.xml new file mode 100644 index 000000000000..fbb5204d6f9a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-war-packaging/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + org.springframework.boot.maven.it + build-image-war-packaging + 0.0.1.BUILD-SNAPSHOT + war + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + build-image + + + + projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1 + + + + + + + org.apache.maven.plugins + maven-war-plugin + @maven-war-plugin.version@ + + + + Foo + + + + + + + + + org.springframework + spring-context + @spring-framework.version@ + + + jakarta.servlet + jakarta.servlet-api + @jakarta-servlet.version@ + provided + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-war-packaging/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-war-packaging/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..5053809ef1fb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-war-packaging/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) throws Exception { + System.out.println("Launched"); + synchronized(args) { + args.wait(); // Prevent exit" + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-with-repackage/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-with-repackage/pom.xml new file mode 100644 index 000000000000..f7ba2ef2531e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-with-repackage/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + org.springframework.boot.maven.it + build-image-with-repackage + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + repackage + + repackage + + + + + build-image + + + + projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1 + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-with-repackage/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-with-repackage/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..5053809ef1fb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-with-repackage/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) throws Exception { + System.out.println("Launched"); + synchronized(args) { + args.wait(); // Prevent exit" + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-zip-packaging/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-zip-packaging/pom.xml new file mode 100644 index 000000000000..534c654beda2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-zip-packaging/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + org.springframework.boot.maven.it + build-image-zip-packaging + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + build-image + + + + + ZIP + + projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1 + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-zip-packaging/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-zip-packaging/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..5053809ef1fb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-zip-packaging/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) throws Exception { + System.out.println("Launched"); + synchronized(args) { + args.wait(); // Prevent exit" + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image/pom.xml new file mode 100644 index 000000000000..1ede9fbe0395 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + org.springframework.boot.maven.it + build-image + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + build-image + + + + projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1 + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..27259ff01ad0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) throws Exception { + System.out.println("Launched"); + synchronized(args) { + args.wait(); // Prevent exit" + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-info-additional-properties/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-info-additional-properties/pom.xml new file mode 100644 index 000000000000..e6ba60b03a49 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-info-additional-properties/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + org.springframework.boot.maven.it + build-info-additional-properties + 0.0.1.BUILD-SNAPSHOT + Generate build info with additional properties + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + build-info + + + + bar + ${project.build.sourceEncoding} + 1.8 + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-info-additional-properties/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-info-additional-properties/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-info-additional-properties/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-info-custom-build-time/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-info-custom-build-time/pom.xml new file mode 100644 index 000000000000..c5668a93c92a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-info-custom-build-time/pom.xml @@ -0,0 +1,33 @@ + + + 4.0.0 + org.springframework.boot.maven.it + build-info-custom-build-time + 0.0.1.BUILD-SNAPSHOT + Generate build info with custom build time + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + + + + build-info + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-info-custom-build-time/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-info-custom-build-time/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-info-custom-build-time/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-info-custom-file/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-info-custom-file/pom.xml new file mode 100644 index 000000000000..3e20c5c9c16b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-info-custom-file/pom.xml @@ -0,0 +1,33 @@ + + + 4.0.0 + org.springframework.boot.maven.it + build-info-custom-file + 0.0.1.BUILD-SNAPSHOT + Generate custom build info + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + build-info + + + ${project.build.directory}/build.info + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-info-custom-file/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-info-custom-file/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-info-custom-file/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-disable-build-time/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-info-disable-build-time/pom.xml similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-disable-build-time/pom.xml rename to spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-info-disable-build-time/pom.xml diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-info-disable-build-time/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-info-disable-build-time/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-info-disable-build-time/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-info/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-info/pom.xml new file mode 100644 index 000000000000..9f0e5a1503dc --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-info/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + org.springframework.boot.maven.it + build-info + 0.0.1.BUILD-SNAPSHOT + Generate build info + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + build-info + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-info/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-info/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-info/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-attach-disabled/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-attach-disabled/pom.xml new file mode 100644 index 000000000000..d6fa5cf6afa7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-attach-disabled/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-attach-disabled + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + false + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-attach-disabled/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-attach-disabled/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-attach-disabled/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-classifier-main-attach-disabled/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-classifier-main-attach-disabled/pom.xml new file mode 100644 index 000000000000..3fd4d7b8df8c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-classifier-main-attach-disabled/pom.xml @@ -0,0 +1,33 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-classifier-main-attach-disabled + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + test + false + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-classifier-main-attach-disabled/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-classifier-main-attach-disabled/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-classifier-main-attach-disabled/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-classifier-main/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-classifier-main/pom.xml new file mode 100644 index 000000000000..d05d5c4488c4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-classifier-main/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-classifier-main + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + test + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-classifier-main/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-classifier-main/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-classifier-main/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-classifier-source-attach-disabled/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-classifier-source-attach-disabled/pom.xml new file mode 100644 index 000000000000..357399d589e2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-classifier-source-attach-disabled/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-classifier-source-attach-disabled + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + org.apache.maven.plugins + maven-jar-plugin + @maven-jar-plugin.version@ + + + + jar + + package + + test + + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + test + false + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-classifier-source-attach-disabled/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-classifier-source-attach-disabled/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-classifier-source-attach-disabled/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-classifier-source/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-classifier-source/pom.xml new file mode 100644 index 000000000000..98d744d15118 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-classifier-source/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-classifier-source + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + org.apache.maven.plugins + maven-jar-plugin + @maven-jar-plugin.version@ + + + + jar + + package + + test + + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + test + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-classifier-source/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-classifier-source/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-classifier-source/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-create-dir/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-create-dir/pom.xml new file mode 100644 index 000000000000..0920206fd4fa --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-create-dir/pom.xml @@ -0,0 +1,33 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-create-dir + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + ${project.build.directory}/foo + foo + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-create-dir/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-create-dir/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-create-dir/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-dir/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-dir/pom.xml new file mode 100644 index 000000000000..f6a0ab30dbee --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-dir/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-custom-dir + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + ${project.build.directory}/foo + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-dir/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-dir/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-dir/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-launcher/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-launcher/pom.xml new file mode 100644 index 000000000000..6e78c3ea63a1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-launcher/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + ${basedir}/src/launcher/custom.script + + world + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-launcher/src/launcher/custom.script b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-launcher/src/launcher/custom.script similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-launcher/src/launcher/custom.script rename to spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-launcher/src/launcher/custom.script diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-launcher/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-launcher/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-launcher/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-layout/custom/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-layout/custom/pom.xml new file mode 100644 index 000000000000..e64b294529fa --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-layout/custom/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + org.springframework.boot.maven.it + custom + 0.0.1.BUILD-SNAPSHOT + + 1.8 + 1.8 + UTF-8 + + + + + org.springframework.boot + spring-boot-maven-plugin + @project.version@ + + + + repackage + + + + custom + + + + + + + org.springframework.boot.maven.it + layout + 0.0.1.BUILD-SNAPSHOT + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-layout/custom/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-layout/custom/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-layout/custom/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-layout/default/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-layout/default/pom.xml new file mode 100644 index 000000000000..f568c759e31b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-layout/default/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + org.springframework.boot.maven.it + default + 0.0.1.BUILD-SNAPSHOT + + 1.8 + 1.8 + UTF-8 + + + + + org.springframework.boot + spring-boot-maven-plugin + @project.version@ + + + + repackage + + + + + + org.springframework.boot.maven.it + layout + 0.0.1.BUILD-SNAPSHOT + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-layout/default/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-layout/default/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-layout/default/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-layout/layout/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-layout/layout/pom.xml new file mode 100644 index 000000000000..965a8b8d9dc0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-layout/layout/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + + org.springframework.boot.maven.it + jar-custom-layout + 0.0.1.BUILD-SNAPSHOT + + jar + layout + + + org.springframework.boot + spring-boot-loader-tools + @project.version@ + + + diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-custom-layout/src/main/java/smoketest/layout/SampleLayout.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-layout/layout/src/main/java/smoketest/layout/SampleLayout.java similarity index 95% rename from spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-custom-layout/src/main/java/smoketest/layout/SampleLayout.java rename to spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-layout/layout/src/main/java/smoketest/layout/SampleLayout.java index b00963e1cd04..3d38f80dfe09 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-custom-layout/src/main/java/smoketest/layout/SampleLayout.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-layout/layout/src/main/java/smoketest/layout/SampleLayout.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-custom-layout/src/main/java/smoketest/layout/SampleLayoutFactory.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-layout/layout/src/main/java/smoketest/layout/SampleLayoutFactory.java similarity index 95% rename from spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-custom-layout/src/main/java/smoketest/layout/SampleLayoutFactory.java rename to spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-layout/layout/src/main/java/smoketest/layout/SampleLayoutFactory.java index 475bb915adf8..d9f827dd867a 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-custom-layout/src/main/java/smoketest/layout/SampleLayoutFactory.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-layout/layout/src/main/java/smoketest/layout/SampleLayoutFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-custom-layout/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-layout/layout/src/main/resources/META-INF/spring.factories similarity index 100% rename from spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-custom-layout/src/main/resources/META-INF/spring.factories rename to spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-layout/layout/src/main/resources/META-INF/spring.factories diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-layout/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-layout/pom.xml new file mode 100644 index 000000000000..7022544d5735 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-custom-layout/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-custom-layout + 0.0.1.BUILD-SNAPSHOT + pom + + UTF-8 + @java.version@ + @java.version@ + + + layout + custom + default + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-exclude-entry/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-exclude-entry/pom.xml new file mode 100644 index 000000000000..d37e11e1528c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-exclude-entry/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-exclude-entry + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + + + javax.servlet + servlet-api + + + + + + + + + + + org.springframework + spring-context + @spring-framework.version@ + + + javax.servlet + servlet-api + 2.5 + provided + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-exclude-entry/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-exclude-entry/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-exclude-entry/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-exclude-group/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-exclude-group/pom.xml new file mode 100644 index 000000000000..4da81b2f08a6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-exclude-group/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-exclude-group + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + org.apache.logging.log4j + + + + + + org.apache.maven.plugins + maven-jar-plugin + @maven-jar-plugin.version@ + + + + Foo + + + + + + + + + org.springframework + spring-context + @spring-framework.version@ + + + org.apache.logging.log4j + log4j-api + @log4j2.version@ + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-exclude-group/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-exclude-group/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-exclude-group/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-executable/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-executable/pom.xml new file mode 100644 index 000000000000..802d166a273f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-executable/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-executable + MyFullyExecutableJarName + MyFullyExecutableJarDesc + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + true + + + + + + org.apache.maven.plugins + maven-jar-plugin + @maven-jar-plugin.version@ + + + + some.random.Main + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-executable/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-executable/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-executable/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar-classifier/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar-classifier/pom.xml new file mode 100644 index 000000000000..509a9fec7782 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/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/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar-release/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar-release/pom.xml new file mode 100644 index 000000000000..a06fe545f187 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/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/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar-snapshot/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar-snapshot/pom.xml new file mode 100644 index 000000000000..ab31e719baf5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/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/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/pom.xml new file mode 100644 index 000000000000..03d6b66d4d24 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/pom.xml @@ -0,0 +1,58 @@ + + + 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 + ${project.basedir}/src/layers.xml + + + + + + + + + + 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/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/layers.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/layers.xml new file mode 100644 index 000000000000..fdf6d7ed8d55 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/layers.xml @@ -0,0 +1,26 @@ + + + + **/application*.* + + + + + + + + + *:*:*-SNAPSHOT + + + + + my-dependencies-name + snapshot-dependencies + configuration + application + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/main/resources/application.yml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/main/resources/application.yml new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/pom.xml new file mode 100644 index 000000000000..6ee622cf58d7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + org.springframework.boot.maven.it + aggregator + 0.0.1.BUILD-SNAPSHOT + pom + + UTF-8 + @java.version@ + @java.version@ + + + jar-classifier + jar-release + jar-snapshot + jar + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-disabled/jar-release/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-disabled/jar-release/pom.xml new file mode 100644 index 000000000000..a06fe545f187 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-disabled/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/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-disabled/jar-snapshot/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-disabled/jar-snapshot/pom.xml new file mode 100644 index 000000000000..ab31e719baf5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-disabled/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/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-disabled/jar/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-disabled/jar/pom.xml new file mode 100644 index 000000000000..3bc2d63626be --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-disabled/jar/pom.xml @@ -0,0 +1,46 @@ + + + 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 + + + + false + + + + + + + + + + org.springframework.boot.maven.it + jar-snapshot + 0.0.1.BUILD-SNAPSHOT + + + org.springframework.boot.maven.it + jar-release + 0.0.1.RELEASE + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-disabled/jar/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-disabled/jar/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-disabled/jar/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-disabled/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-disabled/pom.xml new file mode 100644 index 000000000000..fdd98953811a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-disabled/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + org.springframework.boot.maven.it + aggregator + 0.0.1.BUILD-SNAPSHOT + pom + + UTF-8 + @java.version@ + @java.version@ + + + jar-snapshot + jar-release + jar + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-no-layer-tools/jar-release/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-no-layer-tools/jar-release/pom.xml new file mode 100644 index 000000000000..a06fe545f187 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-no-layer-tools/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/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-no-layer-tools/jar-snapshot/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-no-layer-tools/jar-snapshot/pom.xml new file mode 100644 index 000000000000..ab31e719baf5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-no-layer-tools/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/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-no-layer-tools/jar/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-no-layer-tools/jar/pom.xml new file mode 100644 index 000000000000..b3db8941df92 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-no-layer-tools/jar/pom.xml @@ -0,0 +1,46 @@ + + + 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 + + + + false + + + + + + + + + + org.springframework.boot.maven.it + jar-snapshot + 0.0.1.BUILD-SNAPSHOT + + + org.springframework.boot.maven.it + jar-release + 0.0.1.RELEASE + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-no-layer-tools/jar/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-no-layer-tools/jar/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-no-layer-tools/jar/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-no-layer-tools/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-no-layer-tools/pom.xml new file mode 100644 index 000000000000..fdd98953811a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered-no-layer-tools/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + org.springframework.boot.maven.it + aggregator + 0.0.1.BUILD-SNAPSHOT + pom + + UTF-8 + @java.version@ + @java.version@ + + + jar-snapshot + jar-release + jar + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered/jar-release/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered/jar-release/pom.xml new file mode 100644 index 000000000000..a06fe545f187 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered/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/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered/jar-snapshot/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered/jar-snapshot/pom.xml new file mode 100644 index 000000000000..ab31e719baf5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered/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/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered/jar/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered/jar/pom.xml new file mode 100644 index 000000000000..e63261909be9 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered/jar/pom.xml @@ -0,0 +1,46 @@ + + + 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 + + + + + + + + + org.apache.logging.log4j + log4j-api + @log4j2.version@ + + + org.springframework.boot.maven.it + jar-snapshot + 0.0.1.BUILD-SNAPSHOT + + + org.springframework.boot.maven.it + jar-release + 0.0.1.RELEASE + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered/jar/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered/jar/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered/jar/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered/pom.xml new file mode 100644 index 000000000000..fdd98953811a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-layered/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + org.springframework.boot.maven.it + aggregator + 0.0.1.BUILD-SNAPSHOT + pom + + UTF-8 + @java.version@ + @java.version@ + + + jar-snapshot + jar-release + jar + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-lib-name-conflict/acme-lib/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-lib-name-conflict/acme-lib/pom.xml new file mode 100644 index 000000000000..f0c7626623e8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-lib-name-conflict/acme-lib/pom.xml @@ -0,0 +1,13 @@ + + + 4.0.0 + acme-lib + + + org.springframework.boot.maven.it + jar-lib-name-conflict + 0.0.1.BUILD-SNAPSHOT + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-lib-name-conflict/another-acme-lib/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-lib-name-conflict/another-acme-lib/pom.xml similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-lib-name-conflict/another-acme-lib/pom.xml rename to spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-lib-name-conflict/another-acme-lib/pom.xml diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-lib-name-conflict/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-lib-name-conflict/pom.xml similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-lib-name-conflict/pom.xml rename to spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-lib-name-conflict/pom.xml diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-lib-name-conflict/test-project/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-lib-name-conflict/test-project/pom.xml new file mode 100644 index 000000000000..535fedc387ac --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-lib-name-conflict/test-project/pom.xml @@ -0,0 +1,42 @@ + + + 4.0.0 + org.springframework.boot.maven.it + test-project + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + + + + + + + + org.springframework.boot.maven.it + acme-lib + 0.0.1.BUILD-SNAPSHOT + + + org.springframework.boot.maven.it.another + acme-lib + 0.0.1.BUILD-SNAPSHOT + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-lib-name-conflict/test-project/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-lib-name-conflict/test-project/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-lib-name-conflict/test-project/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-output-timestamp/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-output-timestamp/pom.xml new file mode 100644 index 000000000000..5e1fcf629f99 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-output-timestamp/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-output-timestamp + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + 2020-03-16T02:00:00-08:00 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + + + + org.apache.maven.plugins + maven-jar-plugin + @maven-jar-plugin.version@ + + + + some.random.Main + + + Foo + + + + + + + + + org.springframework + spring-context + @spring-framework.version@ + + + jakarta.servlet + jakarta.servlet-api + @jakarta-servlet.version@ + provided + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-output-timestamp/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-output-timestamp/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-output-timestamp/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-pom/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-pom/pom.xml new file mode 100644 index 000000000000..f8eb3dae581e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-pom/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-pom + pom + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-skip/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-skip/pom.xml new file mode 100644 index 000000000000..15eea089cbf4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-skip/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-skip + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + true + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-system-scope-default/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-system-scope-default/pom.xml new file mode 100644 index 000000000000..f455d43879dc --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-system-scope-default/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-system-scope-default + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + + + + + + + com.example + sample + 1.0.0 + system + ${project.basedir}/sample-1.0.0.jar + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-system-scope-default/sample-1.0.0.jar b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-system-scope-default/sample-1.0.0.jar similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-system-scope-default/sample-1.0.0.jar rename to spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-system-scope-default/sample-1.0.0.jar diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-system-scope-default/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-system-scope-default/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-system-scope-default/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-system-scope/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-system-scope/pom.xml new file mode 100644 index 000000000000..2a5de56091f6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-system-scope/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-system-scope + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + true + + + + + + + + + com.example + sample + 1.0.0 + system + ${project.basedir}/sample-1.0.0.jar + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-system-scope/sample-1.0.0.jar b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-system-scope/sample-1.0.0.jar similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-system-scope/sample-1.0.0.jar rename to spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-system-scope/sample-1.0.0.jar diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-system-scope/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-system-scope/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-system-scope/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-test-scope/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-test-scope/pom.xml new file mode 100644 index 000000000000..5ea032cdd7d2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-test-scope/pom.xml @@ -0,0 +1,42 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-test-scope + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + + + + + + + org.springframework + spring-context + @spring-framework.version@ + + + org.apache.logging.log4j + log4j-api + @log4j2.version@ + test + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-test-scope/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-test-scope/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-test-scope/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-kotlin-module/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-kotlin-module/pom.xml new file mode 100644 index 000000000000..eea70adbfdd3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-kotlin-module/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-with-kotlin-module + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/test/kotlin + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + @kotlin.version@ + + + compile + process-resources + + compile + + + + test-compile + process-test-resources + + compile + + + + + + + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + @kotlin.version@ + + + org.jetbrains.kotlin + kotlin-reflect + @kotlin.version@ + + + org.jetbrains.kotlin + kotlin-compiler + @kotlin.version@ + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-kotlin-module/src/main/kotlin/org/test/SampleApplication.kt b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-kotlin-module/src/main/kotlin/org/test/SampleApplication.kt similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-kotlin-module/src/main/kotlin/org/test/SampleApplication.kt rename to spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-kotlin-module/src/main/kotlin/org/test/SampleApplication.kt diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-layout-property/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-layout-property/pom.xml similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-layout-property/pom.xml rename to spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-layout-property/pom.xml diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-layout-property/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-layout-property/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..85460ade61a2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-layout-property/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-unpack/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-unpack/pom.xml similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-unpack/pom.xml rename to spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-unpack/pom.xml diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-unpack/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-unpack/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-unpack/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-zip-layout/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-zip-layout/pom.xml new file mode 100644 index 000000000000..e4171d27cbd1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-zip-layout/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-with-zip-layout + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + ZIP + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-zip-layout/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-zip-layout/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-zip-layout/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar/pom.xml similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar/pom.xml rename to spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar/pom.xml diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-arguments-commandline/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-arguments-commandline/pom.xml new file mode 100644 index 000000000000..041d9b2ff79a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-arguments-commandline/pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + org.springframework.boot.maven.it + run-arguments-commandline + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-arguments-commandline/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-arguments-commandline/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..c208612ecce3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-arguments-commandline/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +import java.util.Arrays; + +public class SampleApplication { + + public static void main(String[] args) { + if (args.length < 2) { + throw new IllegalArgumentException("Missing arguments " + Arrays.toString(args)); + } + if (!args[0].startsWith("--management.endpoints.web.exposure.include=")) { + throw new IllegalArgumentException("Invalid argument " + args[0]); + } + if (!args[1].startsWith("--spring.profiles.active=")) { + throw new IllegalArgumentException("Invalid argument " + args[1]); + } + String endpoints = args[0].split("=")[1]; + String profile = args[1].split("=")[1]; + System.out.println("I haz been run with profile(s) '" + profile + "' and endpoint(s) '" + endpoints + "'"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-arguments/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-arguments/pom.xml new file mode 100644 index 000000000000..84563c8b9748 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-arguments/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + org.springframework.boot.maven.it + run-arguments + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + --management.endpoints.web.exposure.include=prometheus,info + --spring.profiles.active=foo,bar + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-arguments/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-arguments/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..c208612ecce3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-arguments/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +import java.util.Arrays; + +public class SampleApplication { + + public static void main(String[] args) { + if (args.length < 2) { + throw new IllegalArgumentException("Missing arguments " + Arrays.toString(args)); + } + if (!args[0].startsWith("--management.endpoints.web.exposure.include=")) { + throw new IllegalArgumentException("Invalid argument " + args[0]); + } + if (!args[1].startsWith("--spring.profiles.active=")) { + throw new IllegalArgumentException("Invalid argument " + args[1]); + } + String endpoints = args[0].split("=")[1]; + String profile = args[1].split("=")[1]; + System.out.println("I haz been run with profile(s) '" + profile + "' and endpoint(s) '" + endpoints + "'"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-devtools/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-devtools/pom.xml new file mode 100644 index 000000000000..4f6bcd76fabd --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-devtools/pom.xml @@ -0,0 +1,27 @@ + + + 4.0.0 + org.springframework.boot.maven.it + run-devtools + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + false + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java similarity index 93% rename from spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java rename to spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java index 37dea179d7cf..1b9c7813b325 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-devtools/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-devtools/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..94e5520be82e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-devtools/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,25 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + System.out.println("I haz been run"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-disable-fork/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-disable-fork/pom.xml new file mode 100644 index 000000000000..fa5832c24ef0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-disable-fork/pom.xml @@ -0,0 +1,31 @@ + + + 4.0.0 + org.springframework.boot.maven.it + run-disable-fork + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + false + -Dfoo=bar + ${project.build.sourceDirectory} + + value1 + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-disable-fork/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-disable-fork/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..afabfee12b0e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-disable-fork/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + String foo = System.getProperty("foo"); + if ("bar".equals(foo)) { + throw new IllegalStateException("System property foo should not be available. Fork disabled"); + } + System.out.println("I haz been run"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-envargs/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-envargs/pom.xml new file mode 100644 index 000000000000..836037851747 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-envargs/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + org.springframework.boot.maven.it + run-envargs + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + 5000 + Some Text + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-envargs/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-envargs/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..47b7c14b2535 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-envargs/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + assertEnvValue("ENV1", "5000"); + assertEnvValue("ENV2", "Some Text"); + assertEnvValue("ENV3", ""); + assertEnvValue("ENV4", ""); + + System.out.println("I haz been run"); + } + + private static void assertEnvValue(String envKey, String expectedValue) { + String actual = System.getenv(envKey); + if (!expectedValue.equals(actual)) { + throw new IllegalStateException("env property [" + envKey + "] mismatch " + + "(got [" + actual + "], expected [" + expectedValue + "]"); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-exclude/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-exclude/pom.xml new file mode 100644 index 000000000000..f92c9521153b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-exclude/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + org.springframework.boot.maven.it + run-exclude + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + org.apache.logging.log4j + log4j-api + + + jakarta.servlet,javax.servlet + + + + + + + org.apache.logging.log4j + log4j-api + @log4j2.version@ + + + jakarta.servlet + jakarta.servlet-api + @jakarta-servlet.version@ + provided + + + javax.servlet + servlet-api + 2.5 + provided + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-exclude/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-exclude/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..56892c15b942 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-exclude/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + if (isClassPresent("org.apache.log4j.Logger")) { + throw new IllegalStateException("Log4j was present and should not"); + } + if (isClassPresent("javax.servlet.Servlet")) { + throw new IllegalStateException("servlet-api was present and should not"); + } + System.out.println("I haz been run"); + } + + private static boolean isClassPresent(String className) { + + try { + ClassLoader classLoader = SampleApplication.class.getClassLoader(); + Class.forName(className, false, classLoader); + return true; + } + catch (ClassNotFoundException e) { + return false; + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-fork/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-fork/pom.xml similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-fork/pom.xml rename to spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-fork/pom.xml diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-fork/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-fork/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..8a0121d606b0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-fork/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,27 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +import java.io.File; + +public class SampleApplication { + + public static void main(String[] args) { + System.out.println("I haz been run from '" + new File("").getAbsolutePath() + "'"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-jvm-system-props/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-jvm-system-props/pom.xml new file mode 100644 index 000000000000..6e76f7526b66 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-jvm-system-props/pom.xml @@ -0,0 +1,31 @@ + + + 4.0.0 + org.springframework.boot.maven.it + run-jvmargs + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + -Dfoo="value 1" -Dbar=value2 + + value1 + + ${project.artifactId} + should-be-ignored + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-jvm-system-props/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-jvm-system-props/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..906fcd0ba8e1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-jvm-system-props/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + String foo = System.getProperty("foo"); + if (!"value 1".equals(foo)) { + throw new IllegalStateException("foo system property mismatch (got [" + foo + "]"); + } + String bar = System.getProperty("bar"); + if (!"value2".equals(bar)) { + throw new IllegalStateException("bar system property mismatch (got [" + bar + "]"); + } + String property1 = System.getProperty("property1"); + if (!"value1".equals(property1)) { + throw new IllegalStateException("property1 system property mismatch (got [" + property1 + "]"); + } + String property2 = System.getProperty("property2"); + if (!"".equals(property2)) { + throw new IllegalStateException("property2 system property mismatch (got [" + property2 + "]"); + } + String property3 = System.getProperty("property3"); + if (!"run-jvmargs".equals(property3)) { + throw new IllegalStateException("property3 system property mismatch (got [" + property3 + "]"); + } + System.out.println("I haz been run"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-jvmargs-commandline/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-jvmargs-commandline/pom.xml new file mode 100644 index 000000000000..bd5f47bec371 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-jvmargs-commandline/pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + org.springframework.boot.maven.it + run-jvmargs-commandline + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-jvmargs-commandline/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-jvmargs-commandline/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..4a22cbb28912 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-jvmargs-commandline/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + String foo = System.getProperty("foo"); + if (!"value-from-cmd".equals(foo)) { + throw new IllegalStateException("foo system property mismatch (got [" + foo + "]"); + } + System.out.println("I haz been run"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-jvmargs/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-jvmargs/pom.xml new file mode 100644 index 000000000000..849ed35c67bf --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-jvmargs/pom.xml @@ -0,0 +1,25 @@ + + + 4.0.0 + org.springframework.boot.maven.it + run-jvmargs + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + -Dfoo="value 1" -Dbar=value2 + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-jvmargs/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-jvmargs/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..0e6ae7170d81 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-jvmargs/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + String foo = System.getProperty("foo"); + if (!"value 1".equals(foo)) { + throw new IllegalStateException("foo system property mismatch (got [" + foo + "]"); + } + String bar = System.getProperty("bar"); + if (!"value2".equals(bar)) { + throw new IllegalStateException("bar system property mismatch (got [" + bar + "]"); + } + System.out.println("I haz been run"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-profiles-fork-disabled/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-profiles-fork-disabled/pom.xml new file mode 100644 index 000000000000..1a222c88974e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-profiles-fork-disabled/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + org.springframework.boot.maven.it + run-profiles + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + false + + foo + bar + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-profiles-fork-disabled/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-profiles-fork-disabled/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..74cf04b499f7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-profiles-fork-disabled/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +import java.util.Arrays; + +public class SampleApplication { + + public static void main(String[] args) { + if (args.length < 1) { + throw new IllegalArgumentException("Missing active profile argument " + Arrays.toString(args)); + } + String argument = args[0]; + if (!argument.startsWith("--spring.profiles.active=")) { + throw new IllegalArgumentException("Invalid argument " + argument); + } + int index = args[0].indexOf('='); + String profile = argument.substring(index + 1); + System.out.println("I haz been run with profile(s) '" + profile + "'"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-profiles/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-profiles/pom.xml new file mode 100644 index 000000000000..b46f8996088e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-profiles/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + org.springframework.boot.maven.it + run-profiles + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + foo + bar + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-profiles/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-profiles/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..74cf04b499f7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-profiles/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +import java.util.Arrays; + +public class SampleApplication { + + public static void main(String[] args) { + if (args.length < 1) { + throw new IllegalArgumentException("Missing active profile argument " + Arrays.toString(args)); + } + String argument = args[0]; + if (!argument.startsWith("--spring.profiles.active=")) { + throw new IllegalArgumentException("Invalid argument " + argument); + } + int index = args[0].indexOf('='); + String profile = argument.substring(index + 1); + System.out.println("I haz been run with profile(s) '" + profile + "'"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-toolchains/jdkHome/bin/java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-toolchains/jdkHome/bin/java new file mode 100755 index 000000000000..41f7d6efb02f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-toolchains/jdkHome/bin/java @@ -0,0 +1,2 @@ +#!/bin/bash +echo 'The Maven Toolchains is awesome!' diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-toolchains/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-toolchains/pom.xml new file mode 100644 index 000000000000..73800467e8cd --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-toolchains/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + org.springframework.boot.maven.it + run-toolchains + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + org.apache.maven.plugins + maven-toolchains-plugin + 3.0.0 + + + + toolchain + + + + + + + 42 + test + + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + package + + run + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-toolchains/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-toolchains/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..bc485014365c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-toolchains/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,25 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + throw new IllegalStateException("Should not be called!"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-toolchains/toolchains.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-toolchains/toolchains.xml new file mode 100644 index 000000000000..e6a9c5cd27aa --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-toolchains/toolchains.xml @@ -0,0 +1,12 @@ + + + jdk + + 42 + test + + + jdkHome + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-use-test-classpath/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-use-test-classpath/pom.xml new file mode 100644 index 000000000000..a652f2ca53e0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-use-test-classpath/pom.xml @@ -0,0 +1,33 @@ + + + 4.0.0 + org.springframework.boot.maven.it + run-use-test-classpath + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + true + + + + + + + org.springframework + spring-context + @spring-framework.version@ + test + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-use-test-classpath/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-use-test-classpath/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..615a64756da9 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-use-test-classpath/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.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/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-working-directory/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-working-directory/pom.xml new file mode 100644 index 000000000000..1dd1d52817f3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-working-directory/pom.xml @@ -0,0 +1,25 @@ + + + 4.0.0 + org.springframework.boot.maven.it + run-working-directory + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + ${project.build.sourceDirectory} + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-working-directory/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-working-directory/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..916e5969e5b6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-working-directory/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,26 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + String workingDirectory = System.getProperty("user.dir"); + System.out.println(String.format("I haz been run from %s", workingDirectory)); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run/pom.xml new file mode 100644 index 000000000000..5202b61be3df --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run/pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + org.springframework.boot.maven.it + run + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..94e5520be82e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,25 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + System.out.println("I haz been run"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/settings.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/settings.xml new file mode 100644 index 000000000000..d63e2d6b8d06 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/settings.xml @@ -0,0 +1,49 @@ + + + @localRepositoryPath@ + + + it-repo + + true + + + + local.central + @localCentralUrl@ + + true + + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + + + local.central + @localCentralUrl@ + + true + + + true + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/start-stop-fork-disabled/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/start-stop-fork-disabled/pom.xml new file mode 100644 index 000000000000..8a1a8ad6714c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/start-stop-fork-disabled/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + org.springframework.boot.maven.it + start-stop + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + pre-integration-test + + start + + + + post-integration-test + + stop + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/start-stop-fork-disabled/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/start-stop-fork-disabled/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..b876d616a0c7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/start-stop-fork-disabled/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.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 > 10) { + throw new IllegalStateException( + "Shutdown should have been invoked by now"); + } + synchronized (lock) { + lock.wait(250); + } + waitAttempts++; + } + } + + public interface SpringApplicationAdminMXBean { + + boolean isReady(); + + void shutdown(); + + } + + static class SpringApplicationAdmin implements SpringApplicationAdminMXBean { + + private boolean ready; + + private 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"); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-skip/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/start-stop-skip/pom.xml similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-skip/pom.xml rename to spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/start-stop-skip/pom.xml diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/start-stop-skip/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/start-stop-skip/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..42433c3dfe88 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/start-stop-skip/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +/** + * This sample should not run at all + */ +public class SampleApplication { + + public static void main(String[] args) throws Exception { + System.out.println("Ooops, I haz been run"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-fork/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/start-stop/pom.xml similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-fork/pom.xml rename to spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/start-stop/pom.xml diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/start-stop/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/start-stop/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..19b91bac5262 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/start-stop/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,82 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.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++; + } + } + + public interface SpringApplicationAdminMXBean { + + boolean isReady(); + + void shutdown(); + + } + + static class SpringApplicationAdmin implements SpringApplicationAdminMXBean { + + private boolean ready; + + private 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"); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-exclude-entry/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-exclude-entry/pom.xml new file mode 100644 index 000000000000..6657f2c89829 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-exclude-entry/pom.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + org.springframework.boot.maven.it + war-exclude-entry + 0.0.1.BUILD-SNAPSHOT + war + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + + + org.springframework + spring-core + + + + + + + + org.apache.maven.plugins + maven-war-plugin + @maven-war-plugin.version@ + + + + Foo + + + + + + + + + org.springframework + spring-context + @spring-framework.version@ + + + jakarta.servlet + jakarta.servlet-api + @jakarta-servlet.version@ + provided + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-exclude-entry/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-exclude-entry/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..16c76e92c504 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-exclude-entry/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war-reactor/war/src/main/webapp/index.html b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-exclude-entry/src/main/webapp/index.html similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war-reactor/war/src/main/webapp/index.html rename to spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-exclude-entry/src/main/webapp/index.html diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/jar-classifier/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/jar-classifier/pom.xml new file mode 100644 index 000000000000..509a9fec7782 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/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/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/jar-release/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/jar-release/pom.xml new file mode 100644 index 000000000000..a06fe545f187 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/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/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/jar-snapshot/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/jar-snapshot/pom.xml new file mode 100644 index 000000000000..ab31e719baf5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/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/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/pom.xml new file mode 100644 index 000000000000..fe15e8d88780 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + org.springframework.boot.maven.it + aggregator + 0.0.1.BUILD-SNAPSHOT + pom + + UTF-8 + @java.version@ + @java.version@ + + + jar-classifier + jar-release + jar-snapshot + war + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/war/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/war/pom.xml new file mode 100644 index 000000000000..1dc04b92c311 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/war/pom.xml @@ -0,0 +1,76 @@ + + + 4.0.0 + + org.springframework.boot.maven.it + aggregator + 0.0.1.BUILD-SNAPSHOT + + war-layered + war + war + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + + true + ${project.basedir}/src/layers.xml + + + + + + + org.apache.maven.plugins + maven-war-plugin + @maven-war-plugin.version@ + + + + Foo + + + + + + + + + org.springframework + spring-context + @spring-framework.version@ + + + jakarta.servlet + jakarta.servlet-api + @jakarta-servlet.version@ + provided + + + org.springframework.boot.maven.it + jar-snapshot + 0.0.1.BUILD-SNAPSHOT + + + org.springframework.boot.maven.it + jar-classifier + 0.0.1 + bravo + + + org.springframework.boot.maven.it + jar-release + 0.0.1.RELEASE + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/war/src/layers.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/war/src/layers.xml new file mode 100644 index 000000000000..fdf6d7ed8d55 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/war/src/layers.xml @@ -0,0 +1,26 @@ + + + + **/application*.* + + + + + + + + + *:*:*-SNAPSHOT + + + + + my-dependencies-name + snapshot-dependencies + configuration + application + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/war/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/war/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..16c76e92c504 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/war/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war-with-unpack/src/main/webapp/index.html b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/war/src/main/webapp/index.html similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war-with-unpack/src/main/webapp/index.html rename to spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/war/src/main/webapp/index.html diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-disabled/jar-release/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-disabled/jar-release/pom.xml new file mode 100644 index 000000000000..a06fe545f187 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-disabled/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/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-disabled/jar-snapshot/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-disabled/jar-snapshot/pom.xml new file mode 100644 index 000000000000..ab31e719baf5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-disabled/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/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-disabled/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-disabled/pom.xml new file mode 100644 index 000000000000..60503bdf68f8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-disabled/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + org.springframework.boot.maven.it + aggregator + 0.0.1.BUILD-SNAPSHOT + pom + + UTF-8 + @java.version@ + @java.version@ + + + jar-snapshot + jar-release + war + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-disabled/war/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-disabled/war/pom.xml new file mode 100644 index 000000000000..eb3041ccc3a4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-disabled/war/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + + org.springframework.boot.maven.it + aggregator + 0.0.1.BUILD-SNAPSHOT + + war-layered + war + war + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + + false + + + + + + + org.apache.maven.plugins + maven-war-plugin + @maven-war-plugin.version@ + + + + Foo + + + + + + + + + org.springframework + spring-context + @spring-framework.version@ + + + jakarta.servlet + jakarta.servlet-api + @jakarta-servlet.version@ + provided + + + org.springframework.boot.maven.it + jar-snapshot + 0.0.1.BUILD-SNAPSHOT + + + org.springframework.boot.maven.it + jar-release + 0.0.1.RELEASE + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-disabled/war/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-disabled/war/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..16c76e92c504 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-disabled/war/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war/src/main/webapp/index.html b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-disabled/war/src/main/webapp/index.html similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war/src/main/webapp/index.html rename to spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-disabled/war/src/main/webapp/index.html diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-no-layer-tools/jar-release/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-no-layer-tools/jar-release/pom.xml new file mode 100644 index 000000000000..a06fe545f187 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-no-layer-tools/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/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-no-layer-tools/jar-snapshot/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-no-layer-tools/jar-snapshot/pom.xml new file mode 100644 index 000000000000..ab31e719baf5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-no-layer-tools/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/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-no-layer-tools/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-no-layer-tools/pom.xml new file mode 100644 index 000000000000..60503bdf68f8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-no-layer-tools/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + org.springframework.boot.maven.it + aggregator + 0.0.1.BUILD-SNAPSHOT + pom + + UTF-8 + @java.version@ + @java.version@ + + + jar-snapshot + jar-release + war + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-no-layer-tools/war/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-no-layer-tools/war/pom.xml new file mode 100644 index 000000000000..bc367f75f113 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-no-layer-tools/war/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + + org.springframework.boot.maven.it + aggregator + 0.0.1.BUILD-SNAPSHOT + + war-layered + war + war + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + + false + + + + + + + org.apache.maven.plugins + maven-war-plugin + @maven-war-plugin.version@ + + + + Foo + + + + + + + + + org.springframework + spring-context + @spring-framework.version@ + + + jakarta.servlet + jakarta.servlet-api + @jakarta-servlet.version@ + provided + + + org.springframework.boot.maven.it + jar-snapshot + 0.0.1.BUILD-SNAPSHOT + + + org.springframework.boot.maven.it + jar-release + 0.0.1.RELEASE + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-no-layer-tools/war/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-no-layer-tools/war/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..16c76e92c504 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-no-layer-tools/war/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-no-layer-tools/war/src/main/webapp/index.html b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-no-layer-tools/war/src/main/webapp/index.html new file mode 100644 index 000000000000..18ecdcb795c3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-no-layer-tools/war/src/main/webapp/index.html @@ -0,0 +1 @@ + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered/jar-release/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered/jar-release/pom.xml new file mode 100644 index 000000000000..a06fe545f187 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered/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/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered/jar-snapshot/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered/jar-snapshot/pom.xml new file mode 100644 index 000000000000..ab31e719baf5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered/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/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered/pom.xml new file mode 100644 index 000000000000..60503bdf68f8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + org.springframework.boot.maven.it + aggregator + 0.0.1.BUILD-SNAPSHOT + pom + + UTF-8 + @java.version@ + @java.version@ + + + jar-snapshot + jar-release + war + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered/war/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered/war/pom.xml new file mode 100644 index 000000000000..f511ce0ced92 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered/war/pom.xml @@ -0,0 +1,64 @@ + + + 4.0.0 + + org.springframework.boot.maven.it + aggregator + 0.0.1.BUILD-SNAPSHOT + + war-layered + war + war + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + + + + org.apache.maven.plugins + maven-war-plugin + @maven-war-plugin.version@ + + + + Foo + + + + + + + + + org.springframework + spring-context + @spring-framework.version@ + + + jakarta.servlet + jakarta.servlet-api + @jakarta-servlet.version@ + provided + + + org.springframework.boot.maven.it + jar-snapshot + 0.0.1.BUILD-SNAPSHOT + + + org.springframework.boot.maven.it + jar-release + 0.0.1.RELEASE + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered/war/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered/war/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..16c76e92c504 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered/war/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered/war/src/main/webapp/index.html b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered/war/src/main/webapp/index.html new file mode 100644 index 000000000000..18ecdcb795c3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered/war/src/main/webapp/index.html @@ -0,0 +1 @@ + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-output-timestamp/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-output-timestamp/pom.xml new file mode 100644 index 000000000000..ea6a9c9f54bd --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-output-timestamp/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + org.springframework.boot.maven.it + war-output-timestamp + 0.0.1.BUILD-SNAPSHOT + war + + UTF-8 + 2020-03-16T02:00:00-08:00 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + + + + org.apache.maven.plugins + maven-war-plugin + @maven-war-plugin.version@ + + + + Foo + + + + + + + + + org.springframework + spring-context + @spring-framework.version@ + + + jakarta.servlet + jakarta.servlet-api + @jakarta-servlet.version@ + provided + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-output-timestamp/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-output-timestamp/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-output-timestamp/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-output-timestamp/src/main/webapp/index.html b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-output-timestamp/src/main/webapp/index.html new file mode 100644 index 000000000000..18ecdcb795c3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-output-timestamp/src/main/webapp/index.html @@ -0,0 +1 @@ + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war-reactor/jar/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-reactor/jar/pom.xml similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war-reactor/jar/pom.xml rename to spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-reactor/jar/pom.xml diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war-reactor/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-reactor/pom.xml similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war-reactor/pom.xml rename to spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-reactor/pom.xml diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war-reactor/war/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-reactor/war/pom.xml similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war-reactor/war/pom.xml rename to spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-reactor/war/pom.xml diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-reactor/war/src/main/java/com/example/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-reactor/war/src/main/java/com/example/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-reactor/war/src/main/java/com/example/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-reactor/war/src/main/webapp/index.html b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-reactor/war/src/main/webapp/index.html new file mode 100644 index 000000000000..18ecdcb795c3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-reactor/war/src/main/webapp/index.html @@ -0,0 +1 @@ + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-system-scope/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-system-scope/pom.xml new file mode 100644 index 000000000000..442bad317df3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-system-scope/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + org.springframework.boot.maven.it + war-system-scope + 0.0.1.BUILD-SNAPSHOT + war + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + true + + + + + + org.apache.maven.plugins + maven-war-plugin + @maven-war-plugin.version@ + + + + Foo + + + + + + + + + org.springframework + spring-context + @spring-framework.version@ + + + jakarta.servlet + jakarta.servlet-api + @jakarta-servlet.version@ + provided + + + com.example + sample + 1.0.0 + system + ${project.basedir}/sample-1.0.0.jar + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-system-scope/sample-1.0.0.jar b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-system-scope/sample-1.0.0.jar new file mode 100644 index 000000000000..99ae50b87eb3 Binary files /dev/null and b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-system-scope/sample-1.0.0.jar differ diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-system-scope/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-system-scope/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-system-scope/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-system-scope/src/main/webapp/index.html b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-system-scope/src/main/webapp/index.html new file mode 100644 index 000000000000..18ecdcb795c3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-system-scope/src/main/webapp/index.html @@ -0,0 +1 @@ + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-with-unpack/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-with-unpack/pom.xml new file mode 100644 index 000000000000..bb13ba1acde7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-with-unpack/pom.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + org.springframework.boot.maven.it + war-with-unpack + 0.0.1.BUILD-SNAPSHOT + war + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + + + org.springframework + spring-core + + + + + + + + org.apache.maven.plugins + maven-war-plugin + @maven-war-plugin.version@ + + + + Foo + + + + + + + + + org.springframework + spring-context + @spring-framework.version@ + + + jakarta.servlet + jakarta.servlet-api + @jakarta-servlet.version@ + provided + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-with-unpack/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-with-unpack/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-with-unpack/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-with-unpack/src/main/webapp/index.html b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-with-unpack/src/main/webapp/index.html new file mode 100644 index 000000000000..18ecdcb795c3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-with-unpack/src/main/webapp/index.html @@ -0,0 +1 @@ + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war/pom.xml similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war/pom.xml rename to spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war/pom.xml diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..ca2b9a2f0e50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war/src/main/webapp/index.html b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war/src/main/webapp/index.html new file mode 100644 index 000000000000..18ecdcb795c3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war/src/main/webapp/index.html @@ -0,0 +1 @@ + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-additional-properties/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-additional-properties/pom.xml deleted file mode 100644 index 1b5cd9fcb9b4..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-additional-properties/pom.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - build-info-additional-properties - 0.0.1.BUILD-SNAPSHOT - Generate build info - - UTF-8 - @java.version@ - @java.version@ - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - - build-info - - - - bar - ${project.build.sourceEncoding} - 1.8 - - - - - - - - - - org.springframework - spring-context - @spring-framework.version@ - - - jakarta.servlet - jakarta.servlet-api - @jakarta-servlet.version@ - provided - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-additional-properties/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-additional-properties/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index 672d96a2a604..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-additional-properties/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-additional-properties/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-additional-properties/verify.groovy deleted file mode 100644 index 10f6bfa4f77f..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-additional-properties/verify.groovy +++ /dev/null @@ -1,13 +0,0 @@ -import org.springframework.boot.maven.Verify - -import static org.junit.Assert.assertEquals -import static org.junit.Assert.assertTrue - -def file = new File(basedir, "target/classes/META-INF/build-info.properties") -Properties properties = Verify.verifyBuildInfo(file, - 'org.springframework.boot.maven.it', 'build-info-additional-properties', - 'Generate build info', '0.0.1.BUILD-SNAPSHOT') -assertTrue properties.containsKey('build.time') -assertEquals 'bar', properties.get('build.foo') -assertEquals 'UTF-8', properties.get('build.encoding') -assertEquals '1.8', properties.get('build.java.source') \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-custom-build-time/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-custom-build-time/pom.xml deleted file mode 100644 index bfa9a51de3cc..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-custom-build-time/pom.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - build-info-custom-build-time - 0.0.1.BUILD-SNAPSHOT - Generate build info with custom build time - - UTF-8 - @java.version@ - @java.version@ - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - - - - - build-info - - - - - - - - - org.springframework - spring-context - @spring-framework.version@ - - - jakarta.servlet - jakarta.servlet-api - @jakarta-servlet.version@ - provided - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-custom-build-time/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-custom-build-time/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index 672d96a2a604..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-custom-build-time/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-custom-build-time/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-custom-build-time/verify.groovy deleted file mode 100644 index e6ec4d3e1dab..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-custom-build-time/verify.groovy +++ /dev/null @@ -1,9 +0,0 @@ -import org.springframework.boot.maven.Verify - -import static org.junit.Assert.assertEquals - -def file = new File(basedir, "target/classes/META-INF/build-info.properties") -Properties properties = Verify.verifyBuildInfo(file, - 'org.springframework.boot.maven.it', 'build-info-custom-build-time', - 'Generate build info with custom build time', '0.0.1.BUILD-SNAPSHOT') -assertEquals(properties.get('build.time'), '2019-07-08T08:00:00Z') diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-custom-file/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-custom-file/pom.xml deleted file mode 100644 index 65bf4155f34f..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-custom-file/pom.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - build-info-custom-file - 0.0.1.BUILD-SNAPSHOT - Generate custom build info - - UTF-8 - @java.version@ - @java.version@ - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - - build-info - - - ${project.build.directory}/build.info - - - - - - - - - org.springframework - spring-context - @spring-framework.version@ - - - jakarta.servlet - jakarta.servlet-api - @jakarta-servlet.version@ - provided - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-custom-file/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-custom-file/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index 672d96a2a604..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-custom-file/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-custom-file/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-custom-file/verify.groovy deleted file mode 100644 index eecd9e10d89b..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-custom-file/verify.groovy +++ /dev/null @@ -1,9 +0,0 @@ -import org.springframework.boot.maven.Verify - -import static org.junit.Assert.assertTrue - -def file = new File(basedir, "target/build.info") -Properties properties = Verify.verifyBuildInfo(file, - 'org.springframework.boot.maven.it', 'build-info-custom-file', - 'Generate custom build info', '0.0.1.BUILD-SNAPSHOT') -assertTrue properties.containsKey('build.time') \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-disable-build-time/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-disable-build-time/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index 672d96a2a604..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-disable-build-time/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-disable-build-time/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-disable-build-time/verify.groovy deleted file mode 100644 index 7b5971224bb5..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info-disable-build-time/verify.groovy +++ /dev/null @@ -1,9 +0,0 @@ -import org.springframework.boot.maven.Verify - -import static org.junit.Assert.assertFalse - -def file = new File(basedir, "target/classes/META-INF/build-info.properties") -Properties properties = Verify.verifyBuildInfo(file, - 'org.springframework.boot.maven.it', 'build-info-disable-build-time', - 'Generate build info with disabled build time', '0.0.1.BUILD-SNAPSHOT') -assertFalse 'build time must not be present', properties.containsKey('build.time') diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info/pom.xml deleted file mode 100644 index 1f83522c8f8e..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info/pom.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - build-info - 0.0.1.BUILD-SNAPSHOT - Generate build info - - UTF-8 - @java.version@ - @java.version@ - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - - build-info - - - - - - - - - org.springframework - spring-context - @spring-framework.version@ - - - jakarta.servlet - jakarta.servlet-api - @jakarta-servlet.version@ - provided - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index 672d96a2a604..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info/verify.groovy deleted file mode 100644 index e266446a8ad2..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/build-info/verify.groovy +++ /dev/null @@ -1,9 +0,0 @@ -import org.springframework.boot.maven.Verify - -import static org.junit.Assert.assertTrue - -def file = new File(basedir, "target/classes/META-INF/build-info.properties") -Properties properties = Verify.verifyBuildInfo(file, - 'org.springframework.boot.maven.it', 'build-info', - 'Generate build info', '0.0.1.BUILD-SNAPSHOT') -assertTrue properties.containsKey('build.time') \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-attach-disabled/invoker.properties b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-attach-disabled/invoker.properties deleted file mode 100644 index c0c3f7cc079e..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-attach-disabled/invoker.properties +++ /dev/null @@ -1 +0,0 @@ -invoker.goals=clean install \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-attach-disabled/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-attach-disabled/pom.xml deleted file mode 100644 index 5c8f7f2ef19e..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-attach-disabled/pom.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - jar-attach-disabled - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - - repackage - - - false - - - - - - org.apache.maven.plugins - maven-jar-plugin - @maven-jar-plugin.version@ - - - - some.random.Main - - - Foo - - - - - - - - - org.springframework - spring-context - @spring-framework.version@ - - - jakarta.servlet - jakarta.servlet-api - @jakarta-servlet.version@ - provided - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-attach-disabled/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-attach-disabled/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index 672d96a2a604..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-attach-disabled/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-attach-disabled/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-attach-disabled/verify.groovy deleted file mode 100644 index 7cfb59170592..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-attach-disabled/verify.groovy +++ /dev/null @@ -1,18 +0,0 @@ -import org.springframework.boot.maven.* - -import static org.junit.Assert.assertTrue -import static org.junit.Assert.assertFalse - -File main = new File(basedir, "target/jar-attach-disabled-0.0.1.BUILD-SNAPSHOT.jar") -File backup = new File(basedir, "target/jar-attach-disabled-0.0.1.BUILD-SNAPSHOT.jar.original") -Verify.verifyJar(main, "some.random.Main") -assertTrue 'backup file should exist', backup.exists() - -def file = new File(basedir, "build.log") -assertTrue 'main artifact should have been updated', - file.text.contains("Updating main artifact " + main + " to " + backup) -assertTrue 'main artifact should have been installed', - file.text.contains ("Installing " + backup + " to") -assertFalse 'repackaged artifact should not have been installed', - file.text.contains ("Installing " + main + "to") - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-main-attach-disabled/invoker.properties b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-main-attach-disabled/invoker.properties deleted file mode 100644 index c0c3f7cc079e..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-main-attach-disabled/invoker.properties +++ /dev/null @@ -1 +0,0 @@ -invoker.goals=clean install \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-main-attach-disabled/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-main-attach-disabled/pom.xml deleted file mode 100644 index ba65f7d5ece9..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-main-attach-disabled/pom.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - jar-classifier-main-attach-disabled - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - - repackage - - - test - false - - - - - - org.apache.maven.plugins - maven-jar-plugin - @maven-jar-plugin.version@ - - - - Foo - - - - - - - - - org.springframework - spring-context - @spring-framework.version@ - - - jakarta.servlet - jakarta.servlet-api - @jakarta-servlet.version@ - provided - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-main-attach-disabled/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-main-attach-disabled/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index 672d96a2a604..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-main-attach-disabled/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-main-attach-disabled/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-main-attach-disabled/verify.groovy deleted file mode 100644 index 8a40399d7e2e..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-main-attach-disabled/verify.groovy +++ /dev/null @@ -1,24 +0,0 @@ -import org.springframework.boot.maven.* - -import static org.junit.Assert.assertTrue -import static org.junit.Assert.assertFalse - -File repackaged = new File(basedir, "target/jar-classifier-main-attach-disabled-0.0.1.BUILD-SNAPSHOT-test.jar") -File main = new File(basedir, "target/jar-classifier-main-attach-disabled-0.0.1.BUILD-SNAPSHOT.jar") -File backup = new File(basedir, "target/jar-classifier-main-attach-disabled-0.0.1.BUILD-SNAPSHOT.jar.original") - -new Verify.JarArchiveVerification(repackaged, Verify.SAMPLE_APP).verify(); -assertTrue 'main artifact should exist', main.exists() -assertFalse 'backup artifact should not exist', backup.exists() - -def file = new File(basedir, "build.log") -assertFalse 'repackaged artifact should not have been attached', - file.text.contains("Attaching repackaged archive " + repackaged + " with classifier test") -assertTrue 'repackaged artifact should have been created', - file.text.contains("Creating repackaged archive " + repackaged + " with classifier test") -assertTrue 'main artifact should have been installed', - file.text.contains ("Installing " + main + " to") -assertFalse 'repackaged artifact should not have been installed', - file.text.contains ("Installing " + repackaged + " to") -assertFalse 'backup artifact should not have been installed', - file.text.contains ("Installing " + backup + "to") diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-main/invoker.properties b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-main/invoker.properties deleted file mode 100644 index c0c3f7cc079e..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-main/invoker.properties +++ /dev/null @@ -1 +0,0 @@ -invoker.goals=clean install \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-main/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-main/pom.xml deleted file mode 100644 index b754ff6d29d9..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-main/pom.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - jar-classifier-main - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - - repackage - - - test - - - - - - org.apache.maven.plugins - maven-jar-plugin - @maven-jar-plugin.version@ - - - - Foo - - - - - - - - - org.springframework - spring-context - @spring-framework.version@ - - - jakarta.servlet - jakarta.servlet-api - @jakarta-servlet.version@ - provided - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-main/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-main/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index 672d96a2a604..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-main/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-main/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-main/verify.groovy deleted file mode 100644 index a575beda0a6c..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-main/verify.groovy +++ /dev/null @@ -1,22 +0,0 @@ -import org.springframework.boot.maven.* - -import static org.junit.Assert.assertTrue -import static org.junit.Assert.assertFalse - -File repackaged = new File(basedir, "target/jar-classifier-main-0.0.1.BUILD-SNAPSHOT-test.jar") -File main = new File(basedir, "target/jar-classifier-main-0.0.1.BUILD-SNAPSHOT.jar") -File backup = new File(basedir, "target/jar-classifier-main-0.0.1.BUILD-SNAPSHOT.jar.original") - -new Verify.JarArchiveVerification(repackaged, Verify.SAMPLE_APP).verify(); -assertTrue 'main artifact should exist', main.exists() -assertFalse 'backup artifact should not exist', backup.exists() - -def file = new File(basedir, "build.log") -assertTrue 'repackaged artifact should have been attached', - file.text.contains("Attaching repackaged archive " + repackaged + " with classifier test") -assertFalse 'repackaged artifact should have been created', - file.text.contains("Creating repackaged archive " + repackaged + " with classifier test") -assertTrue 'main artifact should have been installed', - file.text.contains ("Installing " + main + " to") -assertTrue 'repackaged artifact should have been installed', - file.text.contains ("Installing " + repackaged + " to") diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-source-attach-disabled/invoker.properties b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-source-attach-disabled/invoker.properties deleted file mode 100644 index c0c3f7cc079e..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-source-attach-disabled/invoker.properties +++ /dev/null @@ -1 +0,0 @@ -invoker.goals=clean install \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-source-attach-disabled/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-source-attach-disabled/pom.xml deleted file mode 100644 index 33b21884ec9a..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-source-attach-disabled/pom.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - jar-classifier-source-attach-disabled - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - - - org.apache.maven.plugins - maven-jar-plugin - @maven-jar-plugin.version@ - - - - jar - - package - - test - - - Foo - - - - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - - repackage - - - test - false - - - - - - - - - org.springframework - spring-context - @spring-framework.version@ - - - jakarta.servlet - jakarta.servlet-api - @jakarta-servlet.version@ - provided - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-source-attach-disabled/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-source-attach-disabled/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index 672d96a2a604..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-source-attach-disabled/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-source-attach-disabled/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-source-attach-disabled/verify.groovy deleted file mode 100644 index dae20efad32e..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-source-attach-disabled/verify.groovy +++ /dev/null @@ -1,20 +0,0 @@ -import org.springframework.boot.maven.* - -import static org.junit.Assert.assertTrue -import static org.junit.Assert.assertFalse - -File main = new File(basedir, "target/jar-classifier-source-attach-disabled-0.0.1.BUILD-SNAPSHOT-test.jar") -File backup = new File(basedir, "target/jar-classifier-source-attach-disabled-0.0.1.BUILD-SNAPSHOT-test.jar.original") - -new Verify.JarArchiveVerification(main, Verify.SAMPLE_APP).verify(); -assertTrue 'backup artifact should exist', backup.exists() - -def file = new File(basedir, "build.log") -assertFalse 'repackaged artifact should not have been attached', - file.text.contains("Attaching repackaged archive " + main + " with classifier test") -assertTrue 'test artifact should have been updated', - file.text.contains("Updating artifact with classifier test " + main + " to " + backup) -assertTrue 'backup artifact should have been installed', - file.text.contains ("Installing " + backup + " to") -assertFalse 'repackaged artifact should not have been installed', - file.text.contains ("Installing " + main + " to") diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-source/invoker.properties b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-source/invoker.properties deleted file mode 100644 index c0c3f7cc079e..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-source/invoker.properties +++ /dev/null @@ -1 +0,0 @@ -invoker.goals=clean install \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-source/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-source/pom.xml deleted file mode 100644 index a7b24d9cda77..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-source/pom.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - jar-classifier-source - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - - - org.apache.maven.plugins - maven-jar-plugin - @maven-jar-plugin.version@ - - - - jar - - package - - test - - - Foo - - - - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - - repackage - - - test - - - - - - - - - org.springframework - spring-context - @spring-framework.version@ - - - jakarta.servlet - jakarta.servlet-api - @jakarta-servlet.version@ - provided - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-source/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-source/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index 672d96a2a604..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-source/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-source/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-source/verify.groovy deleted file mode 100644 index d85fe4126222..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-classifier-source/verify.groovy +++ /dev/null @@ -1,16 +0,0 @@ -import org.springframework.boot.maven.* - -import static org.junit.Assert.assertFalse -import static org.junit.Assert.assertTrue - -File repackaged = new File(basedir, "target/jar-classifier-source-0.0.1.BUILD-SNAPSHOT-test.jar") -File backup = new File(basedir, "target/jar-classifier-source-0.0.1.BUILD-SNAPSHOT-test.jar.original") - -new Verify.JarArchiveVerification(repackaged, Verify.SAMPLE_APP).verify(); -assertTrue 'backup artifact should exist', backup.exists() - -def file = new File(basedir, "build.log") -assertTrue 'repackaged artifact should have been replaced', - file.text.contains("Replacing artifact with classifier test with repackaged archive") -assertFalse 'backup artifact should not have been installed', file.text.contains ("Installing "+backup) -assertTrue 'repackaged artifact should have been installed', file.text.contains ("Installing "+repackaged) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-create-dir/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-create-dir/pom.xml deleted file mode 100644 index 33e076fcfb32..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-create-dir/pom.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - jar-create-dir - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - - repackage - - - ${project.build.directory}/foo - foo - - - - - - org.apache.maven.plugins - maven-jar-plugin - @maven-jar-plugin.version@ - - - - some.random.Main - - - Foo - - - - - - - - - org.springframework - spring-context - @spring-framework.version@ - - - jakarta.servlet - jakarta.servlet-api - @jakarta-servlet.version@ - provided - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-create-dir/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-create-dir/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index 672d96a2a604..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-create-dir/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-create-dir/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-create-dir/verify.groovy deleted file mode 100644 index 9a3e44547440..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-create-dir/verify.groovy +++ /dev/null @@ -1,6 +0,0 @@ -import java.io.*; -import org.springframework.boot.maven.*; - -Verify.verifyJar( - new File(basedir, "target/foo/jar-create-dir-0.0.1.BUILD-SNAPSHOT-foo.jar"), "some.random.Main" -) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-dir/invoker.properties b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-dir/invoker.properties deleted file mode 100644 index c0c3f7cc079e..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-dir/invoker.properties +++ /dev/null @@ -1 +0,0 @@ -invoker.goals=clean install \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-dir/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-dir/pom.xml deleted file mode 100644 index fa75c04236ae..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-dir/pom.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - jar-custom-dir - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - - repackage - - - ${project.build.directory}/foo - - - - - - org.apache.maven.plugins - maven-jar-plugin - @maven-jar-plugin.version@ - - - - some.random.Main - - - Foo - - - - - - - - - org.springframework - spring-context - @spring-framework.version@ - - - jakarta.servlet - jakarta.servlet-api - @jakarta-servlet.version@ - provided - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-dir/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-dir/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index 672d96a2a604..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-dir/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-dir/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-dir/verify.groovy deleted file mode 100644 index 0960d4ee3e62..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-dir/verify.groovy +++ /dev/null @@ -1,10 +0,0 @@ -import java.io.*; -import org.springframework.boot.maven.*; - -Verify.verifyJar( - new File(basedir, "target/foo/jar-custom-dir-0.0.1.BUILD-SNAPSHOT.jar"), "some.random.Main" -) - -Verify.verifyJar( - new File(localRepositoryPath, "org/springframework/boot/maven/it/jar-custom-dir/0.0.1.BUILD-SNAPSHOT/jar-custom-dir-0.0.1.BUILD-SNAPSHOT.jar"), "some.random.Main" -) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-launcher/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-launcher/pom.xml deleted file mode 100644 index f91261e24565..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-launcher/pom.xml +++ /dev/null @@ -1,63 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - jar - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - - repackage - - - ${basedir}/src/launcher/custom.script - - world - - - - - - - org.apache.maven.plugins - maven-jar-plugin - @maven-jar-plugin.version@ - - - - some.random.Main - - - Foo - - - - - - - - - org.springframework - spring-context - @spring-framework.version@ - - - jakarta.servlet - jakarta.servlet-api - @jakarta-servlet.version@ - provided - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-launcher/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-launcher/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index 672d96a2a604..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-launcher/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-launcher/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-launcher/verify.groovy deleted file mode 100644 index 738a8ad14273..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-custom-launcher/verify.groovy +++ /dev/null @@ -1,6 +0,0 @@ -import java.io.*; -import org.springframework.boot.maven.*; - -Verify.verifyJar( - new File(basedir, "target/jar-0.0.1.BUILD-SNAPSHOT.jar"), "some.random.Main", "Hello world" -) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-exclude-entry/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-exclude-entry/pom.xml deleted file mode 100644 index ba0c43d46315..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-exclude-entry/pom.xml +++ /dev/null @@ -1,68 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - jar-exclude-entry - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - - repackage - - - - - javax.servlet - servlet-api - - - - - - - - org.apache.maven.plugins - maven-jar-plugin - @maven-jar-plugin.version@ - - - - Foo - - - - - - - - - org.springframework - spring-context - @spring-framework.version@ - - - jakarta.servlet - jakarta.servlet-api - @jakarta-servlet.version@ - provided - - - javax.servlet - servlet-api - 2.5 - provided - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-exclude-entry/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-exclude-entry/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index 672d96a2a604..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-exclude-entry/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-exclude-entry/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-exclude-entry/verify.groovy deleted file mode 100644 index 61011b7aa505..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-exclude-entry/verify.groovy +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import java.io.*; -import org.springframework.boot.maven.*; - -File f = new File(basedir, "target/jar-exclude-entry-0.0.1.BUILD-SNAPSHOT.jar") -new Verify.JarArchiveVerification(f, Verify.SAMPLE_APP) { - @Override - protected void verifyZipEntries(Verify.ArchiveVerifier verifier) throws Exception { - super.verifyZipEntries(verifier) - verifier.assertHasNoEntryNameStartingWith("BOOT-INF/lib/servlet-api-2.5.jar") - } -}.verify() diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-exclude-group/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-exclude-group/pom.xml deleted file mode 100644 index 19ae153fdfe8..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-exclude-group/pom.xml +++ /dev/null @@ -1,62 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - jar-exclude-group - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - - repackage - - - org.apache.logging.log4j - - - - - - org.apache.maven.plugins - maven-jar-plugin - @maven-jar-plugin.version@ - - - - Foo - - - - - - - - - org.springframework - spring-context - @spring-framework.version@ - - - jakarta.servlet - jakarta.servlet-api - @jakarta-servlet.version@ - provided - - - org.apache.logging.log4j - log4j-api - @log4j2.version@ - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-exclude-group/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-exclude-group/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index 672d96a2a604..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-exclude-group/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-exclude-group/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-exclude-group/verify.groovy deleted file mode 100644 index bf712e6801f3..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-exclude-group/verify.groovy +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import java.io.*; -import org.springframework.boot.maven.*; - -File f = new File(basedir, "target/jar-exclude-group-0.0.1.BUILD-SNAPSHOT.jar") -new Verify.JarArchiveVerification(f, Verify.SAMPLE_APP) { - @Override - protected void verifyZipEntries(Verify.ArchiveVerifier verifier) throws Exception { - super.verifyZipEntries(verifier) - verifier.assertHasNoEntryNameStartingWith("BOOT-INF/lib/log4j-api-2.4.1.jar") - } -}.verify() diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-executable/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-executable/pom.xml deleted file mode 100644 index f0147318109b..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-executable/pom.xml +++ /dev/null @@ -1,62 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - jar-executable - MyFullyExecutableJarName - MyFullyExecutableJarDesc - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - - repackage - - - true - - - - - - org.apache.maven.plugins - maven-jar-plugin - @maven-jar-plugin.version@ - - - - some.random.Main - - - Foo - - - - - - - - - org.springframework - spring-context - @spring-framework.version@ - - - jakarta.servlet - jakarta.servlet-api - @jakarta-servlet.version@ - provided - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-executable/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-executable/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index 672d96a2a604..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-executable/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-executable/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-executable/verify.groovy deleted file mode 100644 index 8c2522ba6845..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-executable/verify.groovy +++ /dev/null @@ -1,7 +0,0 @@ -import java.io.*; -import org.springframework.boot.maven.*; - -Verify.verifyJar( - new File(basedir, "target/jar-executable-0.0.1.BUILD-SNAPSHOT.jar"), - "some.random.Main", "Spring Boot Startup Script", "MyFullyExecutableJarName", - "MyFullyExecutableJarDesc") diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-lib-name-conflict/acme-lib/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-lib-name-conflict/acme-lib/pom.xml deleted file mode 100644 index 25d2c815b5fb..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-lib-name-conflict/acme-lib/pom.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - 4.0.0 - acme-lib - - - org.springframework.boot.maven.it - jar-lib-name-conflict - 0.0.1.BUILD-SNAPSHOT - - - - - org.springframework - spring-context - @spring-framework.version@ - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-lib-name-conflict/test-project/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-lib-name-conflict/test-project/pom.xml deleted file mode 100644 index 65d9663c2422..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-lib-name-conflict/test-project/pom.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - test-project - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - - repackage - - - - - - org.apache.maven.plugins - maven-jar-plugin - @maven-jar-plugin.version@ - - - - Foo - - - - - - - - - - org.springframework.boot.maven.it - acme-lib - 0.0.1.BUILD-SNAPSHOT - - - org.springframework.boot.maven.it.another - acme-lib - 0.0.1.BUILD-SNAPSHOT - - - jakarta.servlet - jakarta.servlet-api - @jakarta-servlet.version@ - provided - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-lib-name-conflict/test-project/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-lib-name-conflict/test-project/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index 672d96a2a604..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-lib-name-conflict/test-project/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-lib-name-conflict/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-lib-name-conflict/verify.groovy deleted file mode 100644 index 1294e4aeb7e9..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-lib-name-conflict/verify.groovy +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2012-2016 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. - */ - -import java.io.*; -import org.springframework.boot.maven.*; - -File f = new File(basedir, "test-project/target/test-project-0.0.1.BUILD-SNAPSHOT.jar") -new Verify.JarArchiveVerification(f, Verify.SAMPLE_APP) { - @Override - protected void verifyZipEntries(Verify.ArchiveVerifier verifier) throws Exception { - super.verifyZipEntries(verifier) - verifier.assertHasEntryNameStartingWith("BOOT-INF/lib/org.springframework.boot.maven.it-acme-lib-0.0.1.BUILD-SNAPSHOT.jar") - verifier.assertHasEntryNameStartingWith("BOOT-INF/lib/org.springframework.boot.maven.it.another-acme-lib-0.0.1.BUILD-SNAPSHOT.jar") - } -}.verify(); - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-non-executable/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-non-executable/pom.xml deleted file mode 100644 index 587f8a8695af..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-non-executable/pom.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - jar - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - - repackage - - - false - - - - - - org.apache.maven.plugins - maven-jar-plugin - @maven-jar-plugin.version@ - - - - some.random.Main - - - Foo - - - - - - - - - org.springframework - spring-context - @spring-framework.version@ - - - jakarta.servlet - jakarta.servlet-api - @jakarta-servlet.version@ - provided - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-non-executable/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-non-executable/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index 672d96a2a604..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-non-executable/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-non-executable/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-non-executable/verify.groovy deleted file mode 100644 index 5d0f840e1732..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-non-executable/verify.groovy +++ /dev/null @@ -1,6 +0,0 @@ -import java.io.*; -import org.springframework.boot.maven.*; - -Verify.verifyJar( - new File(basedir, "target/jar-0.0.1.BUILD-SNAPSHOT.jar"), "some.random.Main", false -) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-pom/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-pom/pom.xml deleted file mode 100644 index bbf54cebee22..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-pom/pom.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - jar-pom - pom - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - - repackage - - - - - - - - - org.springframework - spring-context - @spring-framework.version@ - - - jakarta.servlet - jakarta.servlet-api - @jakarta-servlet.version@ - provided - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-skip/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-skip/pom.xml deleted file mode 100644 index 23021a8af882..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-skip/pom.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - jar-skip - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - - repackage - - - true - - - - - - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-skip/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-skip/verify.groovy deleted file mode 100644 index cff568c8cbc9..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-skip/verify.groovy +++ /dev/null @@ -1,7 +0,0 @@ -import static org.junit.Assert.assertTrue -import static org.junit.Assert.assertFalse - -File f = new File(basedir, "target/jar-skip-0.0.1.BUILD-SNAPSHOT.jar") -assertTrue 'output file should have been generated', f.exists() -File shouldNotExist = new File(basedir, "target/jar-skip-0.0.1.BUILD-SNAPSHOT.jar.original") -assertFalse 'repackage goal should not have run. .original should not exist', shouldNotExist.exists() diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-system-scope-default/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-system-scope-default/pom.xml deleted file mode 100644 index 15c4b16a57b3..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-system-scope-default/pom.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - jar-system-scope-default - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - - repackage - - - - - - org.apache.maven.plugins - maven-jar-plugin - @maven-jar-plugin.version@ - - - - Foo - - - - - - - - - org.springframework - spring-context - @spring-framework.version@ - - - jakarta.servlet - jakarta.servlet-api - @jakarta-servlet.version@ - provided - - - com.example - sample - 1.0.0 - system - ${project.basedir}/sample-1.0.0.jar - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-system-scope-default/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-system-scope-default/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index 672d96a2a604..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-system-scope-default/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-system-scope-default/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-system-scope-default/verify.groovy deleted file mode 100644 index 0a1ef4fc8f50..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-system-scope-default/verify.groovy +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2012-2014 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. - */ - -import java.io.*; -import org.springframework.boot.maven.*; - -File f = new File(basedir, "target/jar-system-scope-default-0.0.1.BUILD-SNAPSHOT.jar") -new Verify.JarArchiveVerification(f, Verify.SAMPLE_APP) { - @Override - protected void verifyZipEntries(Verify.ArchiveVerifier verifier) throws Exception { - super.verifyZipEntries(verifier) - verifier.assertHasNoEntryNameStartingWith("BOOT-INF/lib/sample-1.0.0.jar") - } -}.verify() diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-system-scope/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-system-scope/pom.xml deleted file mode 100644 index 22c2be938762..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-system-scope/pom.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - jar-system-scope - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - - repackage - - - true - - - - - - org.apache.maven.plugins - maven-jar-plugin - @maven-jar-plugin.version@ - - - - Foo - - - - - - - - - org.springframework - spring-context - @spring-framework.version@ - - - jakarta.servlet - jakarta.servlet-api - @jakarta-servlet.version@ - provided - - - com.example - sample - 1.0.0 - system - ${project.basedir}/sample-1.0.0.jar - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-system-scope/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-system-scope/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index 672d96a2a604..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-system-scope/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-system-scope/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-system-scope/verify.groovy deleted file mode 100644 index 20097b2b78b1..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-system-scope/verify.groovy +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2012-2014 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. - */ - -import java.io.*; -import org.springframework.boot.maven.*; - -File f = new File(basedir, "target/jar-system-scope-0.0.1.BUILD-SNAPSHOT.jar") -new Verify.JarArchiveVerification(f, Verify.SAMPLE_APP) { - @Override - protected void verifyZipEntries(Verify.ArchiveVerifier verifier) throws Exception { - super.verifyZipEntries(verifier) - verifier.assertHasEntryNameStartingWith("BOOT-INF/lib/sample-1.0.0.jar") - } -}.verify() diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-test-scope/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-test-scope/pom.xml deleted file mode 100644 index 11985a8b8ae9..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-test-scope/pom.xml +++ /dev/null @@ -1,63 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - jar-test-scope - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - - repackage - - - servlet-api - - - - - - org.apache.maven.plugins - maven-jar-plugin - @maven-jar-plugin.version@ - - - - Foo - - - - - - - - - org.springframework - spring-context - @spring-framework.version@ - - - jakarta.servlet - jakarta.servlet-api - @jakarta-servlet.version@ - provided - - - org.apache.logging.log4j - log4j-api - @log4j2.version@ - test - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-test-scope/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-test-scope/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index 672d96a2a604..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-test-scope/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-test-scope/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-test-scope/verify.groovy deleted file mode 100644 index 24a032bfc2c4..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-test-scope/verify.groovy +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import java.io.*; -import org.springframework.boot.maven.*; - -File f = new File(basedir, "target/jar-test-scope-0.0.1.BUILD-SNAPSHOT.jar") -new Verify.JarArchiveVerification(f, Verify.SAMPLE_APP) { - @Override - protected void verifyZipEntries(Verify.ArchiveVerifier verifier) throws Exception { - super.verifyZipEntries(verifier) - verifier.assertHasNoEntryNameStartingWith("BOOT-INF/lib/log4j-api-2.4.1.jar") - } -}.verify() diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-kotlin-module/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-kotlin-module/pom.xml deleted file mode 100644 index 770402c9d6f2..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-kotlin-module/pom.xml +++ /dev/null @@ -1,106 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - jar-with-kotlin-module - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - ${project.basedir}/src/main/kotlin - ${project.basedir}/src/test/kotlin - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - - repackage - - - - - org.jetbrains.kotlin - kotlin-compiler - - - - - - - - org.apache.maven.plugins - maven-jar-plugin - @maven-jar-plugin.version@ - - - - Foo - - - - - - org.jetbrains.kotlin - kotlin-maven-plugin - @kotlin.version@ - - - compile - process-resources - - compile - - - - test-compile - process-test-resources - - compile - - - - - - - - - org.springframework - spring-context - @spring-framework.version@ - - - jakarta.servlet - jakarta.servlet-api - @jakarta-servlet.version@ - provided - - - org.apache.logging.log4j - log4j-api - @log4j2.version@ - - - - org.jetbrains.kotlin - kotlin-stdlib-jdk8 - @kotlin.version@ - - - org.jetbrains.kotlin - kotlin-reflect - @kotlin.version@ - - - org.jetbrains.kotlin - kotlin-compiler - @kotlin.version@ - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-kotlin-module/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-kotlin-module/verify.groovy deleted file mode 100644 index 6000b59e5c87..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-kotlin-module/verify.groovy +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import java.io.*; -import org.springframework.boot.maven.*; - -File f = new File(basedir, "target/jar-with-kotlin-module-0.0.1.BUILD-SNAPSHOT.jar") -new Verify.JarArchiveVerification(f, Verify.SAMPLE_APP) { - @Override - protected void verifyZipEntries(Verify.ArchiveVerifier verifier) throws Exception { - super.verifyZipEntries(verifier) - verifier.assertHasEntryNameStartingWith("BOOT-INF/classes/META-INF/jar-with-kotlin-module.kotlin_module") - verifier.assertHasUnpackEntry("BOOT-INF/lib/kotlin-compiler-") - verifier.assertHasNonUnpackEntry("BOOT-INF/lib/kotlin-stdlib-") - verifier.assertHasNonUnpackEntry("BOOT-INF/lib/kotlin-reflect-") - } -}.verify() diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-layout-property/invoker.properties b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-layout-property/invoker.properties deleted file mode 100644 index 034be1275494..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-layout-property/invoker.properties +++ /dev/null @@ -1 +0,0 @@ -invoker.goals=package -Dspring-boot.repackage.layout=ZIP \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-layout-property/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-layout-property/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index bdb43231edd5..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-layout-property/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - } -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-layout-property/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-layout-property/verify.groovy deleted file mode 100644 index 74a5ff22db55..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-layout-property/verify.groovy +++ /dev/null @@ -1,4 +0,0 @@ -import static org.junit.Assert.assertTrue - -def file = new File(basedir, "build.log") -assertTrue file.text.contains("Layout: ZIP") diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-unpack/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-unpack/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index 672d96a2a604..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-unpack/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-unpack/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-unpack/verify.groovy deleted file mode 100644 index ea7eee6d1285..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar-with-unpack/verify.groovy +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2012-2016 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. - */ - -import java.io.*; -import org.springframework.boot.maven.*; - -File f = new File(basedir, "target/jar-with-unpack-0.0.1.BUILD-SNAPSHOT.jar") -new Verify.JarArchiveVerification(f, Verify.SAMPLE_APP) { - @Override - protected void verifyZipEntries(Verify.ArchiveVerifier verifier) throws Exception { - super.verifyZipEntries(verifier) - verifier.assertHasUnpackEntry("BOOT-INF/lib/spring-core-") - verifier.assertHasNonUnpackEntry("BOOT-INF/lib/spring-context-") - } -}.verify() diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar/invoker.properties b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar/invoker.properties deleted file mode 100644 index c0c3f7cc079e..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar/invoker.properties +++ /dev/null @@ -1 +0,0 @@ -invoker.goals=clean install \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index 672d96a2a604..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar/verify.groovy deleted file mode 100644 index 248bf1acf3b6..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/jar/verify.groovy +++ /dev/null @@ -1,19 +0,0 @@ -import org.springframework.boot.maven.* - -import static org.junit.Assert.assertTrue -import static org.junit.Assert.assertFalse - -File main = new File(basedir, "target/jar-0.0.1.BUILD-SNAPSHOT.jar") -File backup = new File(basedir, "target/jar-0.0.1.BUILD-SNAPSHOT.jar.original") -Verify.verifyJar(main, "some.random.Main") -assertTrue 'backup file should exist', backup.exists() - -def file = new File(basedir, "build.log") -assertTrue 'main artifact should have been replaced by repackaged archive', - file.text.contains("Replacing main artifact with repackaged archive") -assertTrue 'main artifact should have been installed', - file.text.contains ("Installing " + main + " to") -assertFalse 'backup artifact should not have been installed', - file.text.contains ("Installing " + backup + "to") -assertFalse file.text.contains("Layout:") - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/prop/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/prop/pom.xml deleted file mode 100644 index 572465bd44f1..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/prop/pom.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - jar - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - - repackage - - - ZIP - - - - - - org.apache.maven.plugins - maven-jar-plugin - @maven-jar-plugin.version@ - - - - Foo - - - - - - - - - org.springframework - spring-context - @spring-framework.version@ - - - jakarta.servlet - jakarta.servlet-api - @jakarta-servlet.version@ - provided - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/prop/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/prop/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index 672d96a2a604..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/prop/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/prop/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/prop/verify.groovy deleted file mode 100644 index 39e9cd8496db..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/prop/verify.groovy +++ /dev/null @@ -1,6 +0,0 @@ -import java.io.*; -import org.springframework.boot.maven.*; - -Verify.verifyZip( - new File(basedir, "target/jar-0.0.1.BUILD-SNAPSHOT.jar") -) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-devtools/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-devtools/pom.xml deleted file mode 100644 index eeac82763d24..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-devtools/pom.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - run-devtools - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - package - - run - - - false - - - - - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-devtools/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-devtools/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index 1596b39a6478..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-devtools/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - System.out.println("I haz been run"); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-devtools/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-devtools/verify.groovy deleted file mode 100644 index ac9957acf2d4..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-devtools/verify.groovy +++ /dev/null @@ -1,6 +0,0 @@ -import static org.junit.Assert.assertTrue - -def file = new File(basedir, "build.log") -assertTrue 'Devtools should have been detected', file.text.contains('Fork mode disabled, devtools will be disabled') -assertTrue 'Application should have run', file.text.contains("I haz been run") - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-disable-fork/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-disable-fork/pom.xml deleted file mode 100644 index 4d7d9a226d27..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-disable-fork/pom.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - run-disable-fork - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - package - - run - - - false - -Dfoo=bar - ${project.build.sourceDirectory} - - value1 - - - - - - - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-disable-fork/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-disable-fork/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index fdc615695876..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-disable-fork/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - String foo = System.getProperty("foo"); - if ("bar".equals(foo)) { - throw new IllegalStateException("System property foo should not be available. Fork disabled"); - } - System.out.println("I haz been run"); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-disable-fork/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-disable-fork/verify.groovy deleted file mode 100644 index 752e0988f003..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-disable-fork/verify.groovy +++ /dev/null @@ -1,7 +0,0 @@ -import static org.junit.Assert.assertTrue - -def file = new File(basedir, "build.log") -assertTrue file.text.contains("I haz been run") -assertTrue file.text.contains("Fork mode disabled, ignoring JVM argument(s) [-Dproperty1=value1 -Dproperty2 -Dfoo=bar]") -assertTrue file.text.contains("Fork mode disabled, ignoring working directory configuration") - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-envargs/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-envargs/pom.xml deleted file mode 100644 index 5d40d970c37e..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-envargs/pom.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - run-envargs - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - package - - run - - - - 5000 - Some Text - - - - - - - - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-envargs/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-envargs/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index b799adfd889c..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-envargs/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - assertEnvValue("ENV1", "5000"); - assertEnvValue("ENV2", "Some Text"); - assertEnvValue("ENV3", ""); - assertEnvValue("ENV4", ""); - - System.out.println("I haz been run"); - } - - private static void assertEnvValue(String envKey, String expectedValue) { - String actual = System.getenv(envKey); - if (!expectedValue.equals(actual)) { - throw new IllegalStateException("env property [" + envKey + "] mismatch " - + "(got [" + actual + "], expected [" + expectedValue + "]"); - } - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-envargs/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-envargs/verify.groovy deleted file mode 100644 index 841c4a97de58..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-envargs/verify.groovy +++ /dev/null @@ -1,3 +0,0 @@ -def file = new File(basedir, "build.log") -return file.text.contains("I haz been run") - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-exclude/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-exclude/pom.xml deleted file mode 100644 index 2f02afbabd21..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-exclude/pom.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - run-exclude - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - package - - run - - - - - org.apache.logging.log4j - log4j-api - - - jakarta.servlet,javax.servlet - - - - - - - - - org.apache.logging.log4j - log4j-api - @log4j2.version@ - - - jakarta.servlet - jakarta.servlet-api - @jakarta-servlet.version@ - provided - - - javax.servlet - servlet-api - 2.5 - provided - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-exclude/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-exclude/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index ae1f6377da3e..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-exclude/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - if (isClassPresent("org.apache.log4j.Logger")) { - throw new IllegalStateException("Log4j was present and should not"); - } - if (isClassPresent("javax.servlet.Servlet")) { - throw new IllegalStateException("servlet-api was present and should not"); - } - System.out.println("I haz been run"); - } - - private static boolean isClassPresent(String className) { - - try { - ClassLoader classLoader = SampleApplication.class.getClassLoader(); - classLoader.loadClass(className); - return true; - } - catch (ClassNotFoundException e) { - return false; - } - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-exclude/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-exclude/verify.groovy deleted file mode 100644 index 841c4a97de58..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-exclude/verify.groovy +++ /dev/null @@ -1,3 +0,0 @@ -def file = new File(basedir, "build.log") -return file.text.contains("I haz been run") - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-fork/invoker.properties b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-fork/invoker.properties deleted file mode 100644 index 8056f772dae6..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-fork/invoker.properties +++ /dev/null @@ -1 +0,0 @@ -invoker.goals=clean verify -X \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-fork/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-fork/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index e2178b4942e8..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-fork/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -import java.io.File; - -public class SampleApplication { - - public static void main(String[] args) { - System.out.println("I haz been run from '" + new File("").getAbsolutePath() + "'"); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-fork/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-fork/verify.groovy deleted file mode 100644 index f675a4fa6121..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-fork/verify.groovy +++ /dev/null @@ -1,21 +0,0 @@ -import java.lang.reflect.Method; - -import static org.junit.Assert.assertTrue - -def boolean isJava13OrLater() { - for (Method method : String.class.getMethods()) { - if (method.getName().equals("stripIndent")) { - return true; - } - } - return false; -} - -def file = new File(basedir, "build.log") -assertTrue file.text.contains("I haz been run from '$basedir'") -if (isJava13OrLater()) { - assertTrue file.text.contains("JVM argument(s): -XX:TieredStopAtLevel=1") -} -else { - assertTrue file.text.contains("JVM argument(s): -Xverify:none -XX:TieredStopAtLevel=1") -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-jvm-system-props/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-jvm-system-props/pom.xml deleted file mode 100644 index 4828ceec96c6..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-jvm-system-props/pom.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - run-jvmargs - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - package - - run - - - -Dfoo="value 1" -Dbar=value2 - - value1 - - ${project.artifactId} - should-be-ignored - - - - - - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-jvm-system-props/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-jvm-system-props/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index 5b480c11a2ca..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-jvm-system-props/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - String foo = System.getProperty("foo"); - if (!"value 1".equals(foo)) { - throw new IllegalStateException("foo system property mismatch (got [" + foo + "]"); - } - String bar = System.getProperty("bar"); - if (!"value2".equals(bar)) { - throw new IllegalStateException("bar system property mismatch (got [" + bar + "]"); - } - String property1 = System.getProperty("property1"); - if (!"value1".equals(property1)) { - throw new IllegalStateException("property1 system property mismatch (got [" + property1 + "]"); - } - String property2 = System.getProperty("property2"); - if (!"".equals(property2)) { - throw new IllegalStateException("property2 system property mismatch (got [" + property2 + "]"); - } - String property3 = System.getProperty("property3"); - if (!"run-jvmargs".equals(property3)) { - throw new IllegalStateException("property3 system property mismatch (got [" + property3 + "]"); - } - System.out.println("I haz been run"); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-jvm-system-props/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-jvm-system-props/verify.groovy deleted file mode 100644 index 841c4a97de58..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-jvm-system-props/verify.groovy +++ /dev/null @@ -1,3 +0,0 @@ -def file = new File(basedir, "build.log") -return file.text.contains("I haz been run") - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-jvmargs/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-jvmargs/pom.xml deleted file mode 100644 index d7f82c8cd44c..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-jvmargs/pom.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - run-jvmargs - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - package - - run - - - -Dfoo="value 1" -Dbar=value2 - - - - - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-jvmargs/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-jvmargs/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index 74d79e9a69f9..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-jvmargs/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - String foo = System.getProperty("foo"); - if (!"value 1".equals(foo)) { - throw new IllegalStateException("foo system property mismatch (got [" + foo + "]"); - } - String bar = System.getProperty("bar"); - if (!"value2".equals(bar)) { - throw new IllegalStateException("bar system property mismatch (got [" + bar + "]"); - } - System.out.println("I haz been run"); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-jvmargs/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-jvmargs/verify.groovy deleted file mode 100644 index 841c4a97de58..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-jvmargs/verify.groovy +++ /dev/null @@ -1,3 +0,0 @@ -def file = new File(basedir, "build.log") -return file.text.contains("I haz been run") - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-profiles-fork/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-profiles-fork/pom.xml deleted file mode 100644 index 05049787147f..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-profiles-fork/pom.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - run-profiles - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - package - - run - - - - foo - bar - - - - - - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-profiles-fork/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-profiles-fork/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index e2dd8264196b..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-profiles-fork/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -import java.util.Arrays; - -public class SampleApplication { - - public static void main(String[] args) { - if (args.length < 1) { - throw new IllegalArgumentException("Missing active profile argument " + Arrays.toString(args) + ""); - } - String argument = args[0]; - if (!argument.startsWith("--spring.profiles.active=")) { - throw new IllegalArgumentException("Invalid argument " + argument); - } - int index = args[0].indexOf('='); - String profile = argument.substring(index + 1); - System.out.println("I haz been run with profile(s) '" + profile + "'"); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-profiles-fork/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-profiles-fork/verify.groovy deleted file mode 100644 index c9cb44bb059f..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-profiles-fork/verify.groovy +++ /dev/null @@ -1,2 +0,0 @@ -def file = new File(basedir, "build.log") -return file.text.contains("I haz been run with profile(s) 'foo,bar'") \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-profiles/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-profiles/pom.xml deleted file mode 100644 index 05049787147f..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-profiles/pom.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - run-profiles - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - package - - run - - - - foo - bar - - - - - - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-profiles/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-profiles/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index e2dd8264196b..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-profiles/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -import java.util.Arrays; - -public class SampleApplication { - - public static void main(String[] args) { - if (args.length < 1) { - throw new IllegalArgumentException("Missing active profile argument " + Arrays.toString(args) + ""); - } - String argument = args[0]; - if (!argument.startsWith("--spring.profiles.active=")) { - throw new IllegalArgumentException("Invalid argument " + argument); - } - int index = args[0].indexOf('='); - String profile = argument.substring(index + 1); - System.out.println("I haz been run with profile(s) '" + profile + "'"); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-profiles/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-profiles/verify.groovy deleted file mode 100644 index c9cb44bb059f..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-profiles/verify.groovy +++ /dev/null @@ -1,2 +0,0 @@ -def file = new File(basedir, "build.log") -return file.text.contains("I haz been run with profile(s) 'foo,bar'") \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-use-test-classpath/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-use-test-classpath/pom.xml deleted file mode 100644 index 419080b62dbb..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-use-test-classpath/pom.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - run-use-test-classpath - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - package - - run - - - true - - - - - - - - - org.springframework - spring-context - @spring-framework.version@ - test - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-use-test-classpath/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-use-test-classpath/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index d5787e2f4888..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-use-test-classpath/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.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/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-use-test-classpath/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-use-test-classpath/verify.groovy deleted file mode 100644 index 841c4a97de58..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-use-test-classpath/verify.groovy +++ /dev/null @@ -1,3 +0,0 @@ -def file = new File(basedir, "build.log") -return file.text.contains("I haz been run") - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-working-directory/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-working-directory/pom.xml deleted file mode 100644 index c82a150c74a1..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-working-directory/pom.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - run-working-directory - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - package - - run - - - ${project.build.sourceDirectory} - - - - - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-working-directory/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-working-directory/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index 6574969229b3..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-working-directory/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - String workingDirectory = System.getProperty("user.dir"); - System.out.println(String.format("I haz been run from %s", workingDirectory)); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-working-directory/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-working-directory/verify.groovy deleted file mode 100644 index 4f1b8234fc59..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run-working-directory/verify.groovy +++ /dev/null @@ -1,6 +0,0 @@ -import static org.junit.Assert.assertTrue - -def file = new File(basedir, "build.log") -def workDir = new File(basedir, "src/main/java").getAbsolutePath() -assertTrue file.text.contains("I haz been run from ${workDir}") - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run/pom.xml deleted file mode 100644 index 19743c2c37a1..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run/pom.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - run - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - package - - run - - - false - - - - - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index 1596b39a6478..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - System.out.println("I haz been run"); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run/verify.groovy deleted file mode 100644 index 841c4a97de58..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/run/verify.groovy +++ /dev/null @@ -1,3 +0,0 @@ -def file = new File(basedir, "build.log") -return file.text.contains("I haz been run") - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/settings.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/settings.xml deleted file mode 100644 index e1e0ace341b9..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/settings.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - it-repo - - true - - - - local.central - @localRepositoryUrl@ - - true - - - true - - - - - - local.central - @localRepositoryUrl@ - - true - - - true - - - - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-fork/invoker.properties b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-fork/invoker.properties deleted file mode 100644 index 793d89fcd371..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-fork/invoker.properties +++ /dev/null @@ -1 +0,0 @@ -invoker.goals=clean verify \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-fork/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-fork/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index c567de45bb10..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-fork/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.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++; - } - } - - public interface SpringApplicationAdminMXBean { - - boolean isReady(); - - void shutdown(); - - } - - static class SpringApplicationAdmin implements SpringApplicationAdminMXBean { - - private boolean ready; - - private 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"); - } - - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-fork/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-fork/verify.groovy deleted file mode 100644 index 5aa87af6830b..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-fork/verify.groovy +++ /dev/null @@ -1,5 +0,0 @@ -import static org.junit.Assert.assertTrue - -def file = new File(basedir, "build.log") -assertTrue 'Start should have waited for application to be ready', file.text.contains("isReady: true") -assertTrue 'Shutdown should have been invoked', file.text.contains("Shutdown requested") diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-skip/invoker.properties b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-skip/invoker.properties deleted file mode 100644 index 793d89fcd371..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-skip/invoker.properties +++ /dev/null @@ -1 +0,0 @@ -invoker.goals=clean verify \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-skip/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-skip/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index 7b6372fcc0b3..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-skip/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -/** - * This sample should not run at all - */ -public class SampleApplication { - - public static void main(String[] args) throws Exception { - System.out.println("Ooops, I haz been run"); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-skip/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-skip/verify.groovy deleted file mode 100644 index 0d3ec538c9ce..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-skip/verify.groovy +++ /dev/null @@ -1,5 +0,0 @@ -import static org.junit.Assert.assertFalse - -def file = new File(basedir, "build.log") -assertFalse 'Application should not have run', file.text.contains("Ooops, I haz been run") -assertFalse 'Should not attempt to stop the app', file.text.contains('Stopping application') diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop/invoker.properties b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop/invoker.properties deleted file mode 100644 index 793d89fcd371..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop/invoker.properties +++ /dev/null @@ -1 +0,0 @@ -invoker.goals=clean verify \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop/pom.xml deleted file mode 100644 index c0791845470d..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop/pom.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - start-stop - 0.0.1.BUILD-SNAPSHOT - - UTF-8 - @java.version@ - @java.version@ - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - pre-integration-test - - start - - - - post-integration-test - - stop - - - - - false - - - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index b02cd4969277..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.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 > 10) { - throw new IllegalStateException( - "Shutdown should have been invoked by now"); - } - synchronized (lock) { - lock.wait(250); - } - waitAttempts++; - } - } - - public interface SpringApplicationAdminMXBean { - - boolean isReady(); - - void shutdown(); - - } - - static class SpringApplicationAdmin implements SpringApplicationAdminMXBean { - - private boolean ready; - - private 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"); - } - - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop/verify.groovy deleted file mode 100644 index 5aa87af6830b..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop/verify.groovy +++ /dev/null @@ -1,5 +0,0 @@ -import static org.junit.Assert.assertTrue - -def file = new File(basedir, "build.log") -assertTrue 'Start should have waited for application to be ready', file.text.contains("isReady: true") -assertTrue 'Shutdown should have been invoked', file.text.contains("Shutdown requested") diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war-reactor/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war-reactor/verify.groovy deleted file mode 100644 index 48d805aaac69..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war-reactor/verify.groovy +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2012-2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import java.io.*; -import org.springframework.boot.maven.*; - -File f = new File(basedir, "war/target/war-0.0.1.BUILD-SNAPSHOT.war") -new Verify.WarArchiveVerification(f) { - @Override - protected void verifyZipEntries(Verify.ArchiveVerifier verifier) throws Exception { - super.verifyZipEntries(verifier) - verifier.assertHasEntryNameStartingWith("WEB-INF/lib/jar-0.0.1.BUILD-SNAPSHOT.jar") - verifier.assertHasNoEntryNameStartingWith("WEB-INF/lib/jar.jar") - } -}.verify() - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war-reactor/war/src/main/java/com/example/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war-reactor/war/src/main/java/com/example/SampleApplication.java deleted file mode 100644 index 672d96a2a604..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war-reactor/war/src/main/java/com/example/SampleApplication.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war-with-unpack/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war-with-unpack/pom.xml deleted file mode 100644 index 9238324238c5..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war-with-unpack/pom.xml +++ /dev/null @@ -1,63 +0,0 @@ - - - 4.0.0 - org.springframework.boot.maven.it - war - 0.0.1.BUILD-SNAPSHOT - war - - UTF-8 - @java.version@ - @java.version@ - - - - - @project.groupId@ - @project.artifactId@ - @project.version@ - - - - repackage - - - - - org.springframework - spring-core - - - - - - - - org.apache.maven.plugins - maven-war-plugin - @maven-war-plugin.version@ - - - - Foo - - - - - - - - - org.springframework - spring-context - @spring-framework.version@ - - - jakarta.servlet - jakarta.servlet-api - @jakarta-servlet.version@ - provided - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war-with-unpack/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war-with-unpack/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index 672d96a2a604..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war-with-unpack/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war-with-unpack/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war-with-unpack/verify.groovy deleted file mode 100644 index d1c8e547fb65..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war-with-unpack/verify.groovy +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2012-2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import java.io.*; -import org.springframework.boot.maven.*; - -File f = new File(basedir, "target/war-0.0.1.BUILD-SNAPSHOT.war") -new Verify.WarArchiveVerification(f) { - @Override - protected void verifyZipEntries(Verify.ArchiveVerifier verifier) throws Exception { - super.verifyZipEntries(verifier) - verifier.assertHasUnpackEntry("WEB-INF/lib/spring-core-") - verifier.assertHasNonUnpackEntry("WEB-INF/lib/spring-context-") - } -}.verify() - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war/src/main/java/org/test/SampleApplication.java deleted file mode 100644 index 672d96a2a604..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war/src/main/java/org/test/SampleApplication.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.test; - -public class SampleApplication { - - public static void main(String[] args) { - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war/verify.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war/verify.groovy deleted file mode 100644 index e0c145c834c4..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/it/war/verify.groovy +++ /dev/null @@ -1,7 +0,0 @@ -import java.io.*; -import org.springframework.boot.maven.*; - -Verify.verifyWar( - new File(basedir, "target/war-0.0.1.BUILD-SNAPSHOT.war") -) - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/groovy/generateGoalsDocumentation.groovy b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/groovy/generateGoalsDocumentation.groovy new file mode 100644 index 000000000000..a7542748de85 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/groovy/generateGoalsDocumentation.groovy @@ -0,0 +1,129 @@ +import groovy.util.XmlSlurper + +private String format(String input) { + input.replace("", "`") + .replace("", "`") + .replace("<", "<") + .replace(">", ">") + .replace("
    ", " ") + .replace("\n", " ") + .replace(""", '"') + .replaceAll('\\{@code (.*?)\\}', '`$1`') + .replaceAll('\\{@link (.*?)\\}', '`$1`') + .replaceAll('\\{@literal (.*?)\\}', '`$1`') + .replaceAll('(.*?)', '\$1[\$2]') +} + +private writeParametersTable(PrintWriter writer, def goal, def parameters, def configuration) { + writer.println '[cols="3,2,3"]' + writer.println '|===' + writer.println '| Name | Type | Default' + writer.println() + parameters.each { parameter -> + def name = parameter.name.text() + writer.println("| <>") + def type = parameter.type.text() + if (type.lastIndexOf('.') >= 0) { + type = type.substring(type.lastIndexOf('.') + 1) + } + writer.println("| `$type`") + def defaultValue = "${configuration[name].@'default-value'}" + if (defaultValue) { + writer.println("| `$defaultValue`") + } + else { + writer.println("|") + } + writer.println() + } + writer.println '|===' +} + +private writeParameterDetails(PrintWriter writer, def parameters, def configuration, def sectionId) { + parameters.each { parameter -> + def name = parameter.name.text() + writer.println "[[$sectionId-$name]]" + writer.println "==== `$name`" + writer.println(format(parameter.description.text())) + writer.println() + writer.println '[cols="10h,90"]' + writer.println '|===' + writer.println() + writer.println '| Name' + writer.println "| `$name`" + writer.println '| Type' + def type = parameter.type.text() + if (type.lastIndexOf('.') >= 0) { + type = type.substring(type.lastIndexOf('.') + 1) + } + writer.println("| `$type`") + def defaultValue = "${configuration[name].@'default-value'}" + if (defaultValue) { + writer.println '| Default value' + writer.println("| `$defaultValue`") + } + def userProperty = "${configuration[name].text().replace('${', '`').replace('}', '`')}" + writer.println '| User property' + userProperty ? writer.println("| ${userProperty}") : writer.println("|") + writer.println '| Since' + def since = parameter.since.text() + since ? writer.println("| `${since}`") : writer.println("|") + writer.println '| Required' + writer.println "| ${parameter.required.text()}" + writer.println() + writer.println '|===' + } +} + +def plugin = new XmlSlurper().parse("${project.build.outputDirectory}/META-INF/maven/plugin.xml" as File) +String goalPrefix = plugin.goalPrefix.text() +File goalsDir = new File(project.build.directory, "generated-resources/goals/") +goalsDir.mkdirs() + +new File(goalsDir, "overview.adoc").withPrintWriter { writer -> + writer.println '[cols="1,3"]' + writer.println '|===' + writer.println '| Goal | Description' + writer.println() + plugin.mojos.mojo.each { mojo -> + def goal = mojo.goal.text() + writer.println "| <>" + writer.println "| ${format(mojo.description.text())}" + writer.println() + } + writer.println '|===' +} + +plugin.mojos.mojo.each { mojo -> + String goal = mojo.goal.text() + new File(goalsDir, "${goal}.adoc").withPrintWriter { writer -> + def sectionId = "goals-$goal" + writer.println() + writer.println("[[$sectionId]]") + writer.println("== `$goalPrefix:$goal`") + writer.println("`${plugin.groupId.text()}:${plugin.artifactId.text()}:${plugin.version.text()}:${mojo.goal.text()}`") + writer.println() + writer.println(format(mojo.description.text())) + writer.println() + def parameters = mojo.parameters.parameter.findAll { it.editable.text() == 'true' } + def requiredParameters = parameters.findAll { it.required.text() == 'true' } + if (requiredParameters.size()) { + writer.println("[[$sectionId-parameters-required]]") + writer.println("=== Required parameters") + writeParametersTable(writer, goal, requiredParameters, mojo.configuration) + writer.println() + } + def optionalParameters = parameters.findAll { it.required.text() == 'false' } + if (optionalParameters.size()) { + writer.println("[[$sectionId-parameters-optional]]") + writer.println("=== Optional parameters") + writeParametersTable(writer, goal, optionalParameters, mojo.configuration) + writer.println() + } + def detailsSectionId = "$sectionId-parameters-details" + writer.println("[[$detailsSectionId]]") + writer.println("=== Parameter details") + writeParameterDetails(writer, parameters, mojo.configuration, detailsSectionId) + writer.println() + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractDependencyFilterMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractDependencyFilterMojo.java index fb2a08c5c659..c2074afb6be6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractDependencyFilterMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractDependencyFilterMojo.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,7 +40,8 @@ public abstract class AbstractDependencyFilterMojo extends AbstractMojo { /** * Collection of artifact definitions to include. The {@link Include} element defines - * a {@code groupId} and {@code artifactId} mandatory properties and an optional + * mandatory {@code groupId} and {@code artifactId} properties and an optional + * mandatory {@code groupId} and {@code artifactId} properties and an optional * {@code classifier} property. * @since 1.2.0 */ @@ -49,7 +50,7 @@ public abstract class AbstractDependencyFilterMojo extends AbstractMojo { /** * Collection of artifact definitions to exclude. The {@link Exclude} element defines - * a {@code groupId} and {@code artifactId} mandatory properties and an optional + * mandatory {@code groupId} and {@code artifactId} properties and an optional * {@code classifier} property. * @since 1.1.0 */ @@ -75,7 +76,7 @@ protected void setExcludeGroupIds(String excludeGroupIds) { this.excludeGroupIds = excludeGroupIds; } - protected Set filterDependencies(Set dependencies, FilterArtifacts filters) + protected final Set filterDependencies(Set dependencies, FilterArtifacts filters) throws MojoExecutionException { try { Set filtered = new LinkedHashSet<>(dependencies); @@ -104,6 +105,7 @@ protected final FilterArtifacts getFilters(ArtifactsFilter... additionalFilters) if (this.excludes != null && !this.excludes.isEmpty()) { filters.addFilter(new ExcludeFilter(this.excludes)); } + filters.addFilter(new JarTypeFilter()); return filters; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java new file mode 100644 index 000000000000..3ce19bf4c105 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java @@ -0,0 +1,283 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.maven; + +import java.io.File; +import java.io.FileInputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +import org.apache.maven.artifact.Artifact; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.model.Dependency; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.Component; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; +import org.apache.maven.project.MavenProjectHelper; +import org.apache.maven.shared.artifact.filter.collection.ArtifactsFilter; +import org.apache.maven.shared.artifact.filter.collection.ScopeFilter; +import org.w3c.dom.Document; +import org.xml.sax.InputSource; + +import org.springframework.boot.loader.tools.Layout; +import org.springframework.boot.loader.tools.LayoutFactory; +import org.springframework.boot.loader.tools.Layouts.Expanded; +import org.springframework.boot.loader.tools.Layouts.Jar; +import org.springframework.boot.loader.tools.Layouts.None; +import org.springframework.boot.loader.tools.Layouts.War; +import org.springframework.boot.loader.tools.Libraries; +import org.springframework.boot.loader.tools.Packager; +import org.springframework.boot.loader.tools.layer.CustomLayers; + +/** + * Abstract base class for classes that work with an {@link Packager}. + * + * @author Phillip Webb + * @author Scott Frederick + * @since 2.3.0 + */ +public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo { + + private static final org.springframework.boot.loader.tools.Layers IMPLICIT_LAYERS = org.springframework.boot.loader.tools.Layers.IMPLICIT; + + /** + * The Maven project. + * @since 1.0.0 + */ + @Parameter(defaultValue = "${project}", readonly = true, required = true) + protected MavenProject project; + + /** + * The Maven session. + * @since 2.4.0 + */ + @Parameter(defaultValue = "${session}", readonly = true, required = true) + protected MavenSession session; + + /** + * Maven project helper utils. + * @since 1.0.0 + */ + @Component + protected MavenProjectHelper projectHelper; + + /** + * The name of the main class. If not specified the first compiled class found that + * contains a {@code main} method will be used. + * @since 1.0.0 + */ + @Parameter + private String mainClass; + + /** + * Exclude Spring Boot devtools from the repackaged archive. + * @since 1.3.0 + */ + @Parameter(property = "spring-boot.repackage.excludeDevtools", defaultValue = "true") + private boolean excludeDevtools = true; + + /** + * Include system scoped dependencies. + * @since 1.4.0 + */ + @Parameter(defaultValue = "false") + public boolean includeSystemScope; + + /** + * Layer configuration with options to disable layer creation, exclude layer tools + * jar, and provide a custom layers configuration file. + * @since 2.3.0 + */ + @Parameter + private Layers layers; + + /** + * Return the type of archive that should be packaged by this MOJO. + * @return {@code null}, indicating a layout type will be chosen based on the original + * archive type + */ + protected LayoutType getLayout() { + return null; + } + + /** + * Return the layout factory that will be used to determine the {@link LayoutType} if + * no explicit layout is set. + * @return {@code null}, indicating a default layout factory will be chosen + */ + protected LayoutFactory getLayoutFactory() { + return null; + } + + /** + * Return a {@link Packager} configured for this MOJO. + * @param

    the packager type + * @param supplier a packager supplier + * @return a configured packager + */ + protected

    P getConfiguredPackager(Supplier

    supplier) { + P packager = supplier.get(); + packager.setLayoutFactory(getLayoutFactory()); + packager.addMainClassTimeoutWarningListener(new LoggingMainClassTimeoutWarningListener(this::getLog)); + packager.setMainClass(this.mainClass); + LayoutType layout = getLayout(); + if (layout != null) { + getLog().info("Layout: " + layout); + packager.setLayout(layout.layout()); + } + if (this.layers == null) { + packager.setLayers(IMPLICIT_LAYERS); + } + else if (this.layers.isEnabled()) { + packager.setLayers((this.layers.getConfiguration() != null) + ? getCustomLayers(this.layers.getConfiguration()) : IMPLICIT_LAYERS); + packager.setIncludeRelevantJarModeJars(this.layers.isIncludeLayerTools()); + } + return packager; + } + + private CustomLayers getCustomLayers(File configuration) { + try { + Document document = getDocumentIfAvailable(configuration); + return new CustomLayersProvider().getLayers(document); + } + catch (Exception ex) { + throw new IllegalStateException( + "Failed to process custom layers configuration " + configuration.getAbsolutePath(), ex); + } + } + + private Document getDocumentIfAvailable(File xmlFile) throws Exception { + InputSource inputSource = new InputSource(new FileInputStream(xmlFile)); + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + return builder.parse(inputSource); + } + + /** + * Return {@link Libraries} that the packager can use. + * @param unpacks any libraries that require unpack + * @return the libraries to use + * @throws MojoExecutionException on execution error + */ + protected final Libraries getLibraries(Collection unpacks) throws MojoExecutionException { + Set artifacts = this.project.getArtifacts(); + Set includedArtifacts = filterDependencies(artifacts, getFilters(getAdditionalFilters())); + return new ArtifactsLibraries(artifacts, includedArtifacts, this.session.getProjects(), unpacks, getLog()); + } + + private ArtifactsFilter[] getAdditionalFilters() { + List filters = new ArrayList<>(); + if (this.excludeDevtools) { + Exclude exclude = new Exclude(); + exclude.setGroupId("org.springframework.boot"); + exclude.setArtifactId("spring-boot-devtools"); + ExcludeFilter filter = new ExcludeFilter(exclude); + filters.add(filter); + } + if (!this.includeSystemScope) { + filters.add(new ScopeFilter(null, Artifact.SCOPE_SYSTEM)); + } + return filters.toArray(new ArtifactsFilter[0]); + } + + /** + * Return the source {@link Artifact} to repackage. If a classifier is specified and + * an artifact with that classifier exists, it is used. Otherwise, the main artifact + * is used. + * @param classifier the artifact classifier + * @return the source artifact to repackage + */ + protected Artifact getSourceArtifact(String classifier) { + Artifact sourceArtifact = getArtifact(classifier); + return (sourceArtifact != null) ? sourceArtifact : this.project.getArtifact(); + } + + private Artifact getArtifact(String classifier) { + if (classifier != null) { + for (Artifact attachedArtifact : this.project.getAttachedArtifacts()) { + if (classifier.equals(attachedArtifact.getClassifier()) && attachedArtifact.getFile() != null + && attachedArtifact.getFile().isFile()) { + return attachedArtifact; + } + } + } + return null; + } + + protected File getTargetFile(String finalName, String classifier, File targetDirectory) { + String classifierSuffix = (classifier != null) ? classifier.trim() : ""; + if (!classifierSuffix.isEmpty() && !classifierSuffix.startsWith("-")) { + classifierSuffix = "-" + classifierSuffix; + } + if (!targetDirectory.exists()) { + targetDirectory.mkdirs(); + } + return new File(targetDirectory, + finalName + classifierSuffix + "." + this.project.getArtifact().getArtifactHandler().getExtension()); + } + + /** + * Archive layout types. + */ + public enum LayoutType { + + /** + * Jar Layout. + */ + JAR(new Jar()), + + /** + * War Layout. + */ + WAR(new War()), + + /** + * Zip Layout. + */ + ZIP(new Expanded()), + + /** + * Directory Layout. + */ + DIR(new Expanded()), + + /** + * No Layout. + */ + NONE(new None()); + + private final Layout layout; + + LayoutType(Layout layout) { + this.layout = layout; + } + + public Layout layout() { + return this.layout; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java index 013e4accc533..83ec65ed16d6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,15 +30,20 @@ import java.util.stream.Collectors; import org.apache.maven.artifact.Artifact; +import org.apache.maven.execution.MavenSession; import org.apache.maven.model.Resource; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.project.MavenProject; import org.apache.maven.shared.artifact.filter.collection.AbstractArtifactFeatureFilter; import org.apache.maven.shared.artifact.filter.collection.FilterArtifacts; +import org.apache.maven.toolchain.Toolchain; +import org.apache.maven.toolchain.ToolchainManager; import org.springframework.boot.loader.tools.FileUtils; +import org.springframework.boot.loader.tools.JavaExecutable; import org.springframework.boot.loader.tools.MainClassFinder; /** @@ -64,6 +69,20 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { @Parameter(defaultValue = "${project}", readonly = true, required = true) private MavenProject project; + /** + * The current Maven session. This is used for toolchain manager API calls. + * @since 2.3.0 + */ + @Parameter(defaultValue = "${session}", readonly = true) + private MavenSession session; + + /** + * The toolchain manager to use to locate a custom JDK. + * @since 2.3.0 + */ + @Component + private ToolchainManager toolchainManager; + /** * Add maven resources to the classpath directly, this allows live in-place editing of * resources. Duplicate resources are removed from {@code target/classes} to prevent @@ -75,15 +94,6 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { @Parameter(property = "spring-boot.run.addResources", defaultValue = "false") private boolean addResources = false; - /** - * Path to agent jar. NOTE: a forked process is required to use this feature. - * @since 1.0.0 - * @deprecated since 2.2.0 in favor of {@code agents} - */ - @Deprecated - @Parameter(property = "spring-boot.run.agent") - private File[] agent; - /** * Path to agent jars. NOTE: a forked process is required to use this feature. * @since 2.2.0 @@ -133,13 +143,21 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { private Map environmentVariables; /** - * Arguments that should be passed to the application. On command line use commas to - * separate multiple arguments. + * Arguments that should be passed to the application. * @since 1.0.0 */ - @Parameter(property = "spring-boot.run.arguments") + @Parameter private String[] arguments; + /** + * Arguments from the command line that should be passed to the application. Use + * spaces to separate multiple arguments and make sure to wrap multiple values between + * quotes. When specified, takes precedence over {@link #arguments}. + * @since 2.2.3 + */ + @Parameter(property = "spring-boot.run.arguments") + private String commandlineArguments; + /** * The spring profiles to activate. Convenience shortcut of specifying the * 'spring.profiles.active' argument. On command line use commas to separate multiple @@ -158,12 +176,12 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { private String mainClass; /** - * Additional folders besides the classes directory that should be added to the + * Additional directories besides the classes directory that should be added to the * classpath. * @since 1.0.0 */ - @Parameter(property = "spring-boot.run.folders") - private String[] folders; + @Parameter(property = "spring-boot.run.directories") + private String[] directories; /** * Directory containing the classes and resource files that should be packaged into @@ -213,20 +231,8 @@ protected boolean isFork() { return this.fork; } - /** - * Specify if fork should be enabled by default. - * @return {@code true} if fork should be enabled by default - * @see #logDisabledFork() - * @deprecated as of 2.2.0 in favour of enabling forking by default. - */ - @Deprecated - protected boolean enableForkByDefault() { - return hasAgent() || hasJvmArgs() || hasEnvVariables() || hasWorkingDirectorySet(); - } - private boolean hasAgent() { - File[] configuredAgents = determineAgents(); - return (configuredAgents != null && configuredAgents.length > 0); + return (this.agents != null && this.agents.length > 0); } private boolean hasJvmArgs() { @@ -234,10 +240,6 @@ private boolean hasJvmArgs() { || (this.systemPropertyVariables != null && !this.systemPropertyVariables.isEmpty()); } - private boolean hasEnvVariables() { - return (this.environmentVariables != null && !this.environmentVariables.isEmpty()); - } - private boolean hasWorkingDirectorySet() { return this.workingDirectory != null; } @@ -311,11 +313,22 @@ protected abstract void runWithMavenJvm(String startClassName, String... argumen * @return a {@link RunArguments} defining the application arguments */ protected RunArguments resolveApplicationArguments() { - RunArguments runArguments = new RunArguments(this.arguments); + RunArguments runArguments = (this.arguments != null) ? new RunArguments(this.arguments) + : new RunArguments(this.commandlineArguments); addActiveProfileArgument(runArguments); return runArguments; } + /** + * Provides access to the java binary executable, regardless of OS. + * @return the java executable + */ + protected String getJavaExecutable() { + Toolchain toolchain = this.toolchainManager.getToolchainFromBuildContext("jdk", this.session); + String javaExecutable = (toolchain != null) ? toolchain.findTool("java") : null; + return (javaExecutable != null) ? javaExecutable : new JavaExecutable().toString(); + } + /** * Resolve the environment variables to use. * @return an {@link EnvVariables} defining the environment variables @@ -327,7 +340,7 @@ protected EnvVariables resolveEnvVariables() { private void addArgs(List args) { RunArguments applicationArguments = resolveApplicationArguments(); Collections.addAll(args, applicationArguments.asArray()); - logArguments("Application argument(s): ", this.arguments); + logArguments("Application argument(s): ", applicationArguments.asArray()); } private Map determineEnvironmentVariables() { @@ -360,12 +373,11 @@ private void addJvmArgs(List args) { } private void addAgents(List args) { - File[] configuredAgents = determineAgents(); - if (configuredAgents != null) { + if (this.agents != null) { if (getLog().isInfoEnabled()) { - getLog().info("Attaching agents: " + Arrays.asList(configuredAgents)); + getLog().info("Attaching agents: " + Arrays.asList(this.agents)); } - for (File agent : configuredAgents) { + for (File agent : this.agents) { args.add("-javaagent:" + agent); } } @@ -374,10 +386,6 @@ private void addAgents(List args) { } } - private File[] determineAgents() { - return (this.agents != null) ? this.agents : this.agent; - } - private void addActiveProfileArgument(RunArguments arguments) { if (this.profiles.length > 0) { StringBuilder arg = new StringBuilder("--spring.profiles.active="); @@ -432,7 +440,7 @@ private String getStartClass() throws MojoExecutionException { protected URL[] getClassPathUrls() throws MojoExecutionException { try { List urls = new ArrayList<>(); - addUserDefinedFolders(urls); + addUserDefinedDirectories(urls); addResources(urls); addProjectClasses(urls); addDependencies(urls); @@ -443,10 +451,10 @@ protected URL[] getClassPathUrls() throws MojoExecutionException { } } - private void addUserDefinedFolders(List urls) throws MalformedURLException { - if (this.folders != null) { - for (String folder : this.folders) { - urls.add(new File(folder).toURI().toURL()); + private void addUserDefinedDirectories(List urls) throws MalformedURLException { + if (this.directories != null) { + for (String directory : this.directories) { + urls.add(new File(directory).toURI().toURL()); } } } @@ -547,7 +555,7 @@ public void run() { Thread thread = Thread.currentThread(); ClassLoader classLoader = thread.getContextClassLoader(); try { - Class startClass = classLoader.loadClass(this.startClassName); + Class startClass = Class.forName(this.startClassName, false, classLoader); Method mainMethod = startClass.getMethod("main", String[].class); if (!mainMethod.isAccessible()) { mainMethod.setAccessible(true); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArtifactsLibraries.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArtifactsLibraries.java index d9da896a1904..3e9b5764040a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArtifactsLibraries.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArtifactsLibraries.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.boot.maven; +import java.io.File; import java.io.IOException; import java.util.Collection; import java.util.Collections; @@ -27,10 +28,12 @@ import org.apache.maven.artifact.Artifact; import org.apache.maven.model.Dependency; import org.apache.maven.plugin.logging.Log; +import org.apache.maven.project.MavenProject; import org.springframework.boot.loader.tools.Libraries; import org.springframework.boot.loader.tools.Library; import org.springframework.boot.loader.tools.LibraryCallback; +import org.springframework.boot.loader.tools.LibraryCoordinates; import org.springframework.boot.loader.tools.LibraryScope; /** @@ -39,6 +42,7 @@ * @author Phillip Webb * @author Andy Wilkinson * @author Stephane Nicoll + * @author Scott Frederick * @since 1.0.0 */ public class ArtifactsLibraries implements Libraries { @@ -56,12 +60,56 @@ public class ArtifactsLibraries implements Libraries { private final Set artifacts; + private final Set includedArtifacts; + + private final Collection localProjects; + private final Collection unpacks; private final Log log; + /** + * Creates a new {@code ArtifactsLibraries} from the given {@code artifacts}. + * @param artifacts the artifacts to represent as libraries + * @param unpacks artifacts that should be unpacked on launch + * @param log the log + * @deprecated since 2.4.0 for removal in 2.6.0 in favor of + * {@link #ArtifactsLibraries(Set, Collection, Collection, Log)} + */ + @Deprecated public ArtifactsLibraries(Set artifacts, Collection unpacks, Log log) { + this(artifacts, Collections.emptyList(), unpacks, log); + } + + /** + * Creates a new {@code ArtifactsLibraries} from the given {@code artifacts}. + * @param artifacts the artifacts to represent as libraries + * @param localProjects projects for which {@link Library#isLocal() local} libraries + * should be created + * @param unpacks artifacts that should be unpacked on launch + * @param log the log + * @since 2.4.0 + */ + public ArtifactsLibraries(Set artifacts, Collection localProjects, + Collection unpacks, Log log) { + this(artifacts, artifacts, localProjects, unpacks, log); + } + + /** + * Creates a new {@code ArtifactsLibraries} from the given {@code artifacts}. + * @param artifacts all artifacts that can be represented as libraries + * @param includedArtifacts the actual artifacts to include in the fat jar + * @param localProjects projects for which {@link Library#isLocal() local} libraries + * should be created + * @param unpacks artifacts that should be unpacked on launch + * @param log the log + * @since 2.4.8 + */ + public ArtifactsLibraries(Set artifacts, Set includedArtifacts, + Collection localProjects, Collection unpacks, Log log) { this.artifacts = artifacts; + this.includedArtifacts = includedArtifacts; + this.localProjects = localProjects; this.unpacks = unpacks; this.log = log; } @@ -70,16 +118,22 @@ public ArtifactsLibraries(Set artifacts, Collection unpack public void doWithLibraries(LibraryCallback callback) throws IOException { Set duplicates = getDuplicates(this.artifacts); for (Artifact artifact : this.artifacts) { + String name = getFileName(artifact); + File file = artifact.getFile(); LibraryScope scope = SCOPES.get(artifact.getScope()); - if (scope != null && artifact.getFile() != null) { - String name = getFileName(artifact); - if (duplicates.contains(name)) { - this.log.debug("Duplicate found: " + name); - name = artifact.getGroupId() + "-" + name; - this.log.debug("Renamed to: " + name); - } - callback.library(new Library(name, artifact.getFile(), scope, isUnpackRequired(artifact))); + if (scope == null || file == null) { + continue; + } + if (duplicates.contains(name)) { + this.log.debug("Duplicate found: " + name); + name = artifact.getGroupId() + "-" + name; + this.log.debug("Renamed to: " + name); } + LibraryCoordinates coordinates = new ArtifactLibraryCoordinates(artifact); + boolean unpackRequired = isUnpackRequired(artifact); + boolean local = isLocal(artifact); + boolean included = this.includedArtifacts.contains(artifact); + callback.library(new Library(name, file, scope, coordinates, unpackRequired, local, included)); } } @@ -107,6 +161,20 @@ private boolean isUnpackRequired(Artifact artifact) { return false; } + private boolean isLocal(Artifact artifact) { + for (MavenProject localProject : this.localProjects) { + if (localProject.getArtifact().equals(artifact)) { + return true; + } + for (Artifact attachedArtifact : localProject.getAttachedArtifacts()) { + if (attachedArtifact.equals(artifact)) { + return true; + } + } + } + return false; + } + private String getFileName(Artifact artifact) { StringBuilder sb = new StringBuilder(); sb.append(artifact.getArtifactId()).append("-").append(artifact.getBaseVersion()); @@ -118,4 +186,37 @@ private String getFileName(Artifact artifact) { return sb.toString(); } + /** + * {@link LibraryCoordinates} backed by a Maven {@link Artifact}. + */ + private static class ArtifactLibraryCoordinates implements LibraryCoordinates { + + private final Artifact artifact; + + ArtifactLibraryCoordinates(Artifact artifact) { + this.artifact = artifact; + } + + @Override + public String getGroupId() { + return this.artifact.getGroupId(); + } + + @Override + public String getArtifactId() { + return this.artifact.getArtifactId(); + } + + @Override + public String getVersion() { + return this.artifact.getBaseVersion(); + } + + @Override + public String toString() { + return this.artifact.toString(); + } + + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java new file mode 100644 index 000000000000..33b9891b7808 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java @@ -0,0 +1,427 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.maven; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.time.Duration; +import java.util.Collections; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.zip.ZipEntry; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; +import org.apache.commons.compress.archivers.tar.TarConstants; +import org.apache.maven.artifact.Artifact; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.logging.Log; +import org.apache.maven.plugins.annotations.Execute; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; + +import org.springframework.boot.buildpack.platform.build.AbstractBuildLog; +import org.springframework.boot.buildpack.platform.build.BuildLog; +import org.springframework.boot.buildpack.platform.build.BuildRequest; +import org.springframework.boot.buildpack.platform.build.Builder; +import org.springframework.boot.buildpack.platform.build.Creator; +import org.springframework.boot.buildpack.platform.build.PullPolicy; +import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration; +import org.springframework.boot.buildpack.platform.io.Owner; +import org.springframework.boot.buildpack.platform.io.TarArchive; +import org.springframework.boot.loader.tools.EntryWriter; +import org.springframework.boot.loader.tools.ImagePackager; +import org.springframework.boot.loader.tools.LayoutFactory; +import org.springframework.boot.loader.tools.Libraries; +import org.springframework.util.StringUtils; + +/** + * Package an application into a OCI image using a buildpack. + * + * @author Phillip Webb + * @author Scott Frederick + * @since 2.3.0 + */ +@Mojo(name = "build-image", defaultPhase = LifecyclePhase.PACKAGE, requiresProject = true, threadSafe = true, + requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, + requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME) +@Execute(phase = LifecyclePhase.PACKAGE) +public class BuildImageMojo extends AbstractPackagerMojo { + + private static final String BUILDPACK_JVM_VERSION_KEY = "BP_JVM_VERSION"; + + static { + System.setProperty("org.slf4j.simpleLogger.log.org.apache.http.wire", "ERROR"); + } + + /** + * Directory containing the source archive. + * @since 2.3.0 + */ + @Parameter(defaultValue = "${project.build.directory}", required = true) + private File sourceDirectory; + + /** + * Name of the source archive. + * @since 2.3.0 + */ + @Parameter(defaultValue = "${project.build.finalName}", readonly = true) + private String finalName; + + /** + * Skip the execution. + * @since 2.3.0 + */ + @Parameter(property = "spring-boot.build-image.skip", defaultValue = "false") + private boolean skip; + + /** + * Classifier used when finding the source archive. + * @since 2.3.0 + */ + @Parameter + private String classifier; + + /** + * Image configuration, with {@code builder}, {@code runImage}, {@code name}, + * {@code env}, {@code cleanCache}, {@code verboseLogging}, {@code pullPolicy}, and + * {@code publish} options. + * @since 2.3.0 + */ + @Parameter + private Image image; + + /** + * Alias for {@link Image#name} to support configuration via command-line property. + * @since 2.3.0 + */ + @Parameter(property = "spring-boot.build-image.imageName", readonly = true) + String imageName; + + /** + * Alias for {@link Image#builder} to support configuration via command-line property. + * @since 2.3.0 + */ + @Parameter(property = "spring-boot.build-image.builder", readonly = true) + String imageBuilder; + + /** + * Alias for {@link Image#runImage} to support configuration via command-line + * property. + * @since 2.3.1 + */ + @Parameter(property = "spring-boot.build-image.runImage", readonly = true) + String runImage; + + /** + * Alias for {@link Image#cleanCache} to support configuration via command-line + * property. + * @since 2.4.0 + */ + @Parameter(property = "spring-boot.build-image.cleanCache", readonly = true) + Boolean cleanCache; + + /** + * Alias for {@link Image#pullPolicy} to support configuration via command-line + * property. + */ + @Parameter(property = "spring-boot.build-image.pullPolicy", readonly = true) + PullPolicy pullPolicy; + + /** + * Alias for {@link Image#publish} to support configuration via command-line property. + */ + @Parameter(property = "spring-boot.build-image.publish", readonly = true) + Boolean publish; + + /** + * Docker configuration options. + * @since 2.4.0 + */ + @Parameter + private Docker docker; + + /** + * The type of archive (which corresponds to how the dependencies are laid out inside + * it). Possible values are {@code JAR}, {@code WAR}, {@code ZIP}, {@code DIR}, + * {@code NONE}. Defaults to a guess based on the archive type. + * @since 2.3.11 + */ + @Parameter + private LayoutType layout; + + /** + * The layout factory that will be used to create the executable archive if no + * explicit layout is set. Alternative layouts implementations can be provided by 3rd + * parties. + * @since 2.3.11 + */ + @Parameter + private LayoutFactory layoutFactory; + + /** + * Return the type of archive that should be used when buiding the image. + * @return the value of the {@code layout} parameter, or {@code null} if the parameter + * is not provided + */ + @Override + protected LayoutType getLayout() { + return this.layout; + } + + /** + * Return the layout factory that will be used to determine the + * {@link AbstractPackagerMojo.LayoutType} if no explicit layout is set. + * @return the value of the {@code layoutFactory} parameter, or {@code null} if the + * parameter is not provided + */ + @Override + protected LayoutFactory getLayoutFactory() { + return this.layoutFactory; + } + + @Override + public void execute() throws MojoExecutionException { + if (this.project.getPackaging().equals("pom")) { + getLog().debug("build-image goal could not be applied to pom project."); + return; + } + if (this.skip) { + getLog().debug("skipping build-image as per configuration."); + return; + } + buildImage(); + } + + private void buildImage() throws MojoExecutionException { + Libraries libraries = getLibraries(Collections.emptySet()); + try { + DockerConfiguration dockerConfiguration = (this.docker != null) ? this.docker.asDockerConfiguration() + : null; + BuildRequest request = getBuildRequest(libraries); + Builder builder = new Builder(new MojoBuildLog(this::getLog), dockerConfiguration); + builder.build(request); + } + catch (IOException ex) { + throw new MojoExecutionException(ex.getMessage(), ex); + } + } + + private BuildRequest getBuildRequest(Libraries libraries) throws MojoExecutionException { + ImagePackager imagePackager = new ImagePackager(getArchiveFile(), getBackupFile()); + Function content = (owner) -> getApplicationContent(owner, libraries, imagePackager); + Image image = (this.image != null) ? this.image : new Image(); + if (image.name == null && this.imageName != null) { + image.setName(this.imageName); + } + if (image.builder == null && this.imageBuilder != null) { + image.setBuilder(this.imageBuilder); + } + if (image.runImage == null && this.runImage != null) { + image.setRunImage(this.runImage); + } + if (image.cleanCache == null && this.cleanCache != null) { + image.setCleanCache(this.cleanCache); + } + if (image.pullPolicy == null && this.pullPolicy != null) { + image.setPullPolicy(this.pullPolicy); + } + if (image.publish == null && this.publish != null) { + image.setPublish(this.publish); + } + if (image.publish != null && image.publish && publishRegistryNotConfigured()) { + throw new MojoExecutionException("Publishing an image requires docker.publishRegistry to be configured"); + } + return customize(image.getBuildRequest(this.project.getArtifact(), content)); + } + + private boolean publishRegistryNotConfigured() { + return this.docker == null || this.docker.getPublishRegistry() == null + || this.docker.getPublishRegistry().isEmpty(); + } + + private TarArchive getApplicationContent(Owner owner, Libraries libraries, ImagePackager imagePackager) { + ImagePackager packager = getConfiguredPackager(() -> imagePackager); + return new PackagedTarArchive(owner, libraries, packager); + } + + private File getArchiveFile() { + // We can use 'project.getArtifact().getFile()' because that was done in a + // forked lifecycle and is now null + File archiveFile = getTargetFile(this.finalName, this.classifier, this.sourceDirectory); + if (!archiveFile.exists()) { + archiveFile = getSourceArtifact(this.classifier).getFile(); + } + if (!archiveFile.exists()) { + throw new IllegalStateException("A jar or war file is required for building image"); + } + return archiveFile; + } + + /** + * Return the {@link File} to use to backup the original source. + * @return the file to use to backup the original source + */ + private File getBackupFile() { + Artifact source = getSourceArtifact(null); + if (this.classifier != null && !this.classifier.equals(source.getClassifier())) { + return source.getFile(); + } + return null; + } + + private BuildRequest customize(BuildRequest request) { + request = customizeEnvironment(request); + request = customizeCreator(request); + return request; + } + + private BuildRequest customizeEnvironment(BuildRequest request) { + if (!request.getEnv().containsKey(BUILDPACK_JVM_VERSION_KEY)) { + JavaCompilerPluginConfiguration compilerConfiguration = new JavaCompilerPluginConfiguration(this.project); + String targetJavaVersion = compilerConfiguration.getTargetMajorVersion(); + if (StringUtils.hasText(targetJavaVersion)) { + return request.withEnv(BUILDPACK_JVM_VERSION_KEY, targetJavaVersion + ".*"); + } + } + return request; + } + + private BuildRequest customizeCreator(BuildRequest request) { + String springBootVersion = VersionExtractor.forClass(BuildImageMojo.class); + if (StringUtils.hasText(springBootVersion)) { + request = request.withCreator(Creator.withVersion(springBootVersion)); + } + return request; + } + + /** + * {@link BuildLog} backed by Mojo logging. + */ + private static class MojoBuildLog extends AbstractBuildLog { + + private static final long THRESHOLD = Duration.ofSeconds(2).toMillis(); + + private final Supplier log; + + MojoBuildLog(Supplier log) { + this.log = log; + } + + @Override + protected void log(String message) { + this.log.get().info(message); + } + + @Override + protected Consumer getProgressConsumer(String message) { + return new ProgressLog(message); + } + + private class ProgressLog implements Consumer { + + private final String message; + + private long last; + + ProgressLog(String message) { + this.message = message; + this.last = System.currentTimeMillis(); + } + + @Override + public void accept(TotalProgressEvent progress) { + log(progress.getPercent()); + } + + private void log(int percent) { + if (percent == 100 || (System.currentTimeMillis() - this.last) > THRESHOLD) { + MojoBuildLog.this.log.get().info(this.message + " " + percent + "%"); + this.last = System.currentTimeMillis(); + } + } + + } + + } + + /** + * Adapter class to expose the packaged jar as a {@link TarArchive}. + */ + static class PackagedTarArchive implements TarArchive { + + static final long NORMALIZED_MOD_TIME = TarArchive.NORMALIZED_TIME.toEpochMilli(); + + private final Owner owner; + + private final Libraries libraries; + + private final ImagePackager packager; + + PackagedTarArchive(Owner owner, Libraries libraries, ImagePackager packager) { + this.owner = owner; + this.libraries = libraries; + this.packager = packager; + } + + @Override + public void writeTo(OutputStream outputStream) throws IOException { + TarArchiveOutputStream tar = new TarArchiveOutputStream(outputStream); + tar.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); + try { + this.packager.packageImage(this.libraries, (entry, entryWriter) -> write(entry, entryWriter, tar)); + } + catch (RuntimeException ex) { + outputStream.close(); + throw new RuntimeException("Error packaging archive for image", ex); + } + } + + private void write(ZipEntry jarEntry, EntryWriter entryWriter, TarArchiveOutputStream tar) { + try { + TarArchiveEntry tarEntry = convert(jarEntry); + tar.putArchiveEntry(tarEntry); + if (tarEntry.isFile()) { + entryWriter.write(tar); + } + tar.closeArchiveEntry(); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + private TarArchiveEntry convert(ZipEntry entry) { + byte linkFlag = (entry.isDirectory()) ? TarConstants.LF_DIR : TarConstants.LF_NORMAL; + TarArchiveEntry tarEntry = new TarArchiveEntry(entry.getName(), linkFlag, true); + tarEntry.setUserId(this.owner.getUid()); + tarEntry.setGroupId(this.owner.getGid()); + tarEntry.setModTime(NORMALIZED_MOD_TIME); + if (!entry.isDirectory()) { + tarEntry.setSize(entry.getSize()); + } + return tarEntry; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildInfoMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildInfoMojo.java index ea6d9d16b606..0b88685f70aa 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildInfoMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildInfoMojo.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,7 +37,7 @@ import org.springframework.boot.loader.tools.BuildPropertiesWriter.ProjectDetails; /** - * Generate a {@code build-info.properties} file based the content of the current + * Generate a {@code build-info.properties} file based on the content of the current * {@link MavenProject}. * * @author Stephane Nicoll @@ -62,7 +62,7 @@ public class BuildInfoMojo extends AbstractMojo { private MavenProject project; /** - * The location of the generated build-info.properties. + * The location of the generated {@code build-info.properties} file. */ @Parameter(defaultValue = "${project.build.outputDirectory}/META-INF/build-info.properties") private File outputFile; @@ -77,8 +77,8 @@ public class BuildInfoMojo extends AbstractMojo { private String time; /** - * Additional properties to store in the build-info.properties. Each entry is prefixed - * by {@code build.} in the generated build-info.properties. + * Additional properties to store in the {@code build-info.properties} file. Each + * entry is prefixed by {@code build.} in the generated {@code build-info.properties}. */ @Parameter private Map additionalProperties; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CustomLayersProvider.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CustomLayersProvider.java new file mode 100644 index 000000000000..5d01b34caa95 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CustomLayersProvider.java @@ -0,0 +1,139 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.maven; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import org.springframework.boot.loader.tools.Layer; +import org.springframework.boot.loader.tools.Library; +import org.springframework.boot.loader.tools.layer.ApplicationContentFilter; +import org.springframework.boot.loader.tools.layer.ContentFilter; +import org.springframework.boot.loader.tools.layer.ContentSelector; +import org.springframework.boot.loader.tools.layer.CustomLayers; +import org.springframework.boot.loader.tools.layer.IncludeExcludeContentSelector; +import org.springframework.boot.loader.tools.layer.LibraryContentFilter; + +/** + * Produces a {@link CustomLayers} based on the given {@link Document}. + * + * @author Madhura Bhave + * @author Phillip Webb + */ +class CustomLayersProvider { + + CustomLayers getLayers(Document document) { + Element root = document.getDocumentElement(); + List> applicationSelectors = getApplicationSelectors(root); + List> librarySelectors = getLibrarySelectors(root); + List layers = getLayers(root); + return new CustomLayers(layers, applicationSelectors, librarySelectors); + } + + private List> getApplicationSelectors(Element root) { + return getSelectors(root, "application", (element) -> getSelector(element, ApplicationContentFilter::new)); + } + + private List> getLibrarySelectors(Element root) { + return getSelectors(root, "dependencies", (element) -> getLibrarySelector(element, LibraryContentFilter::new)); + } + + private List getLayers(Element root) { + Element layerOrder = getChildElement(root, "layerOrder"); + if (layerOrder == null) { + return Collections.emptyList(); + } + return getChildNodeTextContent(layerOrder, "layer").stream().map(Layer::new).collect(Collectors.toList()); + } + + private List> getSelectors(Element root, String elementName, + Function> selectorFactory) { + Element element = getChildElement(root, elementName); + if (element == null) { + return Collections.emptyList(); + } + List> selectors = new ArrayList<>(); + NodeList children = element.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + if (child instanceof Element) { + ContentSelector selector = selectorFactory.apply((Element) child); + selectors.add(selector); + } + } + return selectors; + } + + private ContentSelector getSelector(Element element, Function> filterFactory) { + Layer layer = new Layer(element.getAttribute("layer")); + List includes = getChildNodeTextContent(element, "include"); + List excludes = getChildNodeTextContent(element, "exclude"); + return new IncludeExcludeContentSelector<>(layer, includes, excludes, filterFactory); + } + + private ContentSelector getLibrarySelector(Element element, + Function> filterFactory) { + Layer layer = new Layer(element.getAttribute("layer")); + List includes = getChildNodeTextContent(element, "include"); + List excludes = getChildNodeTextContent(element, "exclude"); + Element includeModuleDependencies = getChildElement(element, "includeModuleDependencies"); + Element excludeModuleDependencies = getChildElement(element, "excludeModuleDependencies"); + List> includeFilters = includes.stream().map(filterFactory).collect(Collectors.toList()); + if (includeModuleDependencies != null) { + includeFilters = new ArrayList<>(includeFilters); + includeFilters.add(Library::isLocal); + } + List> excludeFilters = excludes.stream().map(filterFactory).collect(Collectors.toList()); + if (excludeModuleDependencies != null) { + excludeFilters = new ArrayList<>(excludeFilters); + excludeFilters.add(Library::isLocal); + } + return new IncludeExcludeContentSelector<>(layer, includeFilters, excludeFilters); + } + + private List getChildNodeTextContent(Element element, String tagName) { + List patterns = new ArrayList<>(); + NodeList nodes = element.getElementsByTagName(tagName); + for (int i = 0; i < nodes.getLength(); i++) { + Node node = nodes.item(i); + if (node instanceof Element) { + patterns.add(node.getTextContent()); + } + } + return patterns; + } + + private Element getChildElement(Element element, String tagName) { + NodeList nodes = element.getElementsByTagName(tagName); + if (nodes.getLength() == 0) { + return null; + } + if (nodes.getLength() > 1) { + throw new IllegalStateException("Multiple '" + tagName + "' nodes found"); + } + return (Element) nodes.item(0); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/DependencyFilter.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/DependencyFilter.java index d5b0c8ab2b78..a0cb0a30cc98 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/DependencyFilter.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/DependencyFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,11 +46,10 @@ public DependencyFilter(List dependencies) { } @Override - @SuppressWarnings({ "rawtypes", "unchecked" }) - public Set filter(Set artifacts) throws ArtifactFilterException { - Set result = new HashSet(); - for (Object artifact : artifacts) { - if (!filter((Artifact) artifact)) { + public Set filter(Set artifacts) throws ArtifactFilterException { + Set result = new HashSet<>(); + for (Artifact artifact : artifacts) { + if (!filter(artifact)) { result.add(artifact); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Docker.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Docker.java new file mode 100644 index 000000000000..637de7d14833 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Docker.java @@ -0,0 +1,266 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.maven; + +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration; + +/** + * Docker configuration options. + * + * @author Wei Jiang + * @author Scott Frederick + * @since 2.4.0 + */ +public class Docker { + + private String host; + + private boolean tlsVerify; + + private String certPath; + + private DockerRegistry builderRegistry; + + private DockerRegistry publishRegistry; + + /** + * The host address of the Docker daemon. + * @return the Docker host + */ + public String getHost() { + return this.host; + } + + void setHost(String host) { + this.host = host; + } + + /** + * Whether the Docker daemon requires TLS communication. + * @return {@code true} to enable TLS + */ + public boolean isTlsVerify() { + return this.tlsVerify; + } + + void setTlsVerify(boolean tlsVerify) { + this.tlsVerify = tlsVerify; + } + + /** + * The path to TLS certificate and key files required for TLS communication with the + * Docker daemon. + * @return the TLS certificate path + */ + public String getCertPath() { + return this.certPath; + } + + void setCertPath(String certPath) { + this.certPath = certPath; + } + + /** + * Configuration of the Docker registry where builder and run images are stored. + * @return the registry configuration + */ + DockerRegistry getBuilderRegistry() { + return this.builderRegistry; + } + + /** + * Sets the {@link DockerRegistry} that configures authentication to the builder + * registry. + * @param builderRegistry the registry configuration + */ + void setBuilderRegistry(DockerRegistry builderRegistry) { + this.builderRegistry = builderRegistry; + } + + /** + * Configuration of the Docker registry where the generated image will be published. + * @return the registry configuration + */ + DockerRegistry getPublishRegistry() { + return this.publishRegistry; + } + + /** + * Sets the {@link DockerRegistry} that configures authentication to the publishing + * registry. + * @param builderRegistry the registry configuration + */ + void setPublishRegistry(DockerRegistry builderRegistry) { + this.publishRegistry = builderRegistry; + } + + /** + * Returns this configuration as a {@link DockerConfiguration} instance. This method + * should only be called when the configuration is complete and will no longer be + * changed. + * @return the Docker configuration + */ + DockerConfiguration asDockerConfiguration() { + DockerConfiguration dockerConfiguration = new DockerConfiguration(); + dockerConfiguration = customizeHost(dockerConfiguration); + dockerConfiguration = customizeBuilderAuthentication(dockerConfiguration); + dockerConfiguration = customizePublishAuthentication(dockerConfiguration); + return dockerConfiguration; + } + + private DockerConfiguration customizeHost(DockerConfiguration dockerConfiguration) { + if (this.host != null) { + return dockerConfiguration.withHost(this.host, this.tlsVerify, this.certPath); + } + return dockerConfiguration; + } + + private DockerConfiguration customizeBuilderAuthentication(DockerConfiguration dockerConfiguration) { + if (this.builderRegistry == null || this.builderRegistry.isEmpty()) { + return dockerConfiguration; + } + if (this.builderRegistry.hasTokenAuth() && !this.builderRegistry.hasUserAuth()) { + return dockerConfiguration.withBuilderRegistryTokenAuthentication(this.builderRegistry.getToken()); + } + if (this.builderRegistry.hasUserAuth() && !this.builderRegistry.hasTokenAuth()) { + return dockerConfiguration.withBuilderRegistryUserAuthentication(this.builderRegistry.getUsername(), + this.builderRegistry.getPassword(), this.builderRegistry.getUrl(), this.builderRegistry.getEmail()); + } + throw new IllegalArgumentException( + "Invalid Docker builder registry configuration, either token or username/password must be provided"); + } + + private DockerConfiguration customizePublishAuthentication(DockerConfiguration dockerConfiguration) { + if (this.publishRegistry == null || this.publishRegistry.isEmpty()) { + return dockerConfiguration; + } + if (this.publishRegistry.hasTokenAuth() && !this.publishRegistry.hasUserAuth()) { + return dockerConfiguration.withPublishRegistryTokenAuthentication(this.publishRegistry.getToken()); + } + if (this.publishRegistry.hasUserAuth() && !this.publishRegistry.hasTokenAuth()) { + return dockerConfiguration.withPublishRegistryUserAuthentication(this.publishRegistry.getUsername(), + this.publishRegistry.getPassword(), this.publishRegistry.getUrl(), this.publishRegistry.getEmail()); + } + throw new IllegalArgumentException( + "Invalid Docker publish registry configuration, either token or username/password must be provided"); + } + + /** + * Encapsulates Docker registry authentication configuration options. + */ + public static class DockerRegistry { + + private String username; + + private String password; + + private String url; + + private String email; + + private String token; + + public DockerRegistry() { + } + + public DockerRegistry(String username, String password, String url, String email) { + this.username = username; + this.password = password; + this.url = url; + this.email = email; + } + + public DockerRegistry(String token) { + this.token = token; + } + + /** + * The username that will be used for user authentication to the registry. + * @return the username + */ + public String getUsername() { + return this.username; + } + + void setUsername(String username) { + this.username = username; + } + + /** + * The password that will be used for user authentication to the registry. + * @return the password + */ + public String getPassword() { + return this.password; + } + + void setPassword(String password) { + this.password = password; + } + + /** + * The email address that will be used for user authentication to the registry. + * @return the email address + */ + public String getEmail() { + return this.email; + } + + void setEmail(String email) { + this.email = email; + } + + /** + * The URL of the registry. + * @return the registry URL + */ + String getUrl() { + return this.url; + } + + void setUrl(String url) { + this.url = url; + } + + /** + * The token that will be used for token authentication to the registry. + * @return the authentication token + */ + public String getToken() { + return this.token; + } + + void setToken(String token) { + this.token = token; + } + + boolean isEmpty() { + return this.username == null && this.password == null && this.url == null && this.email == null + && this.token == null; + } + + boolean hasTokenAuth() { + return this.token != null; + } + + boolean hasUserAuth() { + return this.username != null && this.password != null; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java new file mode 100644 index 000000000000..0ce0afd83b49 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java @@ -0,0 +1,196 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.maven; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.apache.maven.artifact.Artifact; + +import org.springframework.boot.buildpack.platform.build.BuildRequest; +import org.springframework.boot.buildpack.platform.build.BuildpackReference; +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; +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.util.CollectionUtils; +import org.springframework.util.StringUtils; + +/** + * Image configuration options. + * + * @author Phillip Webb + * @author Scott Frederick + * @since 2.3.0 + */ +public class Image { + + String name; + + String builder; + + String runImage; + + Map env; + + Boolean cleanCache; + + boolean verboseLogging; + + PullPolicy pullPolicy; + + Boolean publish; + + List buildpacks; + + List bindings; + + /** + * The name of the created image. + * @return the image name + */ + public String getName() { + return this.name; + } + + void setName(String name) { + this.name = name; + } + + /** + * The name of the builder image to use to create the image. + * @return the builder image name + */ + public String getBuilder() { + return this.builder; + } + + void setBuilder(String builder) { + this.builder = builder; + } + + /** + * The name of the run image to use to create the image. + * @return the builder image name + */ + public String getRunImage() { + return this.runImage; + } + + void setRunImage(String runImage) { + this.runImage = runImage; + } + + /** + * Environment properties that should be passed to the builder. + * @return the environment properties + */ + public Map getEnv() { + return this.env; + } + + /** + * If the cache should be cleaned before building. + * @return {@code true} if the cache should be cleaned + */ + public Boolean getCleanCache() { + return this.cleanCache; + } + + void setCleanCache(Boolean cleanCache) { + this.cleanCache = cleanCache; + } + + /** + * If verbose logging is required. + * @return {@code true} for verbose logging + */ + public boolean isVerboseLogging() { + return this.verboseLogging; + } + + /** + * If images should be pulled from a remote repository during image build. + * @return the pull policy + */ + public PullPolicy getPullPolicy() { + return this.pullPolicy; + } + + void setPullPolicy(PullPolicy pullPolicy) { + this.pullPolicy = pullPolicy; + } + + /** + * If the built image should be pushed to a registry. + * @return {@code true} if the image should be published + */ + public Boolean getPublish() { + return this.publish; + } + + void setPublish(Boolean publish) { + this.publish = publish; + } + + BuildRequest getBuildRequest(Artifact artifact, Function applicationContent) { + return customize(BuildRequest.of(getOrDeduceName(artifact), applicationContent)); + } + + private ImageReference getOrDeduceName(Artifact artifact) { + if (StringUtils.hasText(this.name)) { + return ImageReference.of(this.name); + } + ImageName imageName = ImageName.of(artifact.getArtifactId()); + return ImageReference.of(imageName, artifact.getVersion()); + } + + private BuildRequest customize(BuildRequest request) { + if (StringUtils.hasText(this.builder)) { + request = request.withBuilder(ImageReference.of(this.builder)); + } + if (StringUtils.hasText(this.runImage)) { + request = request.withRunImage(ImageReference.of(this.runImage)); + } + if (this.env != null && !this.env.isEmpty()) { + request = request.withEnv(this.env); + } + if (this.cleanCache != null) { + request = request.withCleanCache(this.cleanCache); + } + request = request.withVerboseLogging(this.verboseLogging); + if (this.pullPolicy != null) { + request = request.withPullPolicy(this.pullPolicy); + } + if (this.publish != null) { + request = request.withPublish(this.publish); + } + if (!CollectionUtils.isEmpty(this.buildpacks)) { + request = request + .withBuildpacks(this.buildpacks.stream().map(BuildpackReference::of).collect(Collectors.toList())); + } + if (!CollectionUtils.isEmpty(this.bindings)) { + request = request.withBindings(this.bindings.stream().map(Binding::of).collect(Collectors.toList())); + } + return request; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/JarTypeFilter.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/JarTypeFilter.java new file mode 100644 index 000000000000..a05f3f8ada88 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/JarTypeFilter.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.maven; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.jar.JarFile; +import java.util.jar.Manifest; + +import org.apache.maven.artifact.Artifact; + +/** + * A {@link DependencyFilter} that filters dependencies based on the jar type declared in + * their manifest. + * + * @author Andy Wilkinson + */ +class JarTypeFilter extends DependencyFilter { + + private static final Set EXCLUDED_JAR_TYPES = Collections + .unmodifiableSet(new HashSet<>(Arrays.asList("annotation-processor", "dependencies-starter"))); + + JarTypeFilter() { + super(Collections.emptyList()); + } + + @Override + protected boolean filter(Artifact artifact) { + try (JarFile jarFile = new JarFile(artifact.getFile())) { + Manifest manifest = jarFile.getManifest(); + if (manifest != null) { + String jarType = manifest.getMainAttributes().getValue("Spring-Boot-Jar-Type"); + if (jarType != null && EXCLUDED_JAR_TYPES.contains(jarType)) { + return true; + } + } + } + catch (IOException ex) { + // Continue + } + return false; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/JavaCompilerPluginConfiguration.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/JavaCompilerPluginConfiguration.java new file mode 100644 index 000000000000..b44be9a06d3d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/JavaCompilerPluginConfiguration.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.maven; + +import java.util.Arrays; + +import org.apache.maven.model.Plugin; +import org.apache.maven.project.MavenProject; +import org.codehaus.plexus.util.xml.Xpp3Dom; + +/** + * Provides access to the Maven Java Compiler plugin configuration. + * + * @author Scott Frederick + */ +class JavaCompilerPluginConfiguration { + + private final MavenProject project; + + JavaCompilerPluginConfiguration(MavenProject project) { + this.project = project; + } + + String getSourceMajorVersion() { + String version = getConfigurationValue("source"); + + if (version == null) { + version = getPropertyValue("maven.compiler.source"); + } + + return majorVersionFor(version); + } + + String getTargetMajorVersion() { + String version = getConfigurationValue("target"); + + if (version == null) { + version = getPropertyValue("maven.compiler.target"); + } + + return majorVersionFor(version); + } + + private String getConfigurationValue(String propertyName) { + Plugin plugin = this.project.getPlugin("org.apache.maven.plugins:maven-compiler-plugin"); + if (plugin != null) { + Object pluginConfiguration = plugin.getConfiguration(); + if (pluginConfiguration instanceof Xpp3Dom) { + Xpp3Dom dom = (Xpp3Dom) pluginConfiguration; + return getNodeValue(dom, propertyName); + } + } + return null; + } + + private String getPropertyValue(String propertyName) { + if (this.project.getProperties().containsKey(propertyName)) { + return this.project.getProperties().get(propertyName).toString(); + } + return null; + } + + private String getNodeValue(Xpp3Dom dom, String... childNames) { + Xpp3Dom childNode = dom.getChild(childNames[0]); + + if (childNode == null) { + return null; + } + + if (childNames.length > 1) { + return getNodeValue(childNode, Arrays.copyOfRange(childNames, 1, childNames.length)); + } + + return childNode.getValue(); + } + + private String majorVersionFor(String version) { + if (version != null && version.startsWith("1.")) { + return version.substring("1.".length()); + } + return version; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Layers.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Layers.java new file mode 100644 index 000000000000..ce981b4248f5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Layers.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.maven; + +import java.io.File; + +/** + * Layer configuration options. + * + * @author Madhura Bhave + * @since 2.3.0 + */ +public class Layers { + + private boolean enabled = true; + + private boolean includeLayerTools = true; + + private File configuration; + + /** + * Whether a {@code layers.idx} file should be added to the jar. + * @return true if a {@code layers.idx} file should be added. + */ + public boolean isEnabled() { + return this.enabled; + } + + /** + * Whether to include the layer tools jar. + * @return true if layer tools should be included + */ + public boolean isIncludeLayerTools() { + return this.includeLayerTools; + } + + /** + * The location of the layers configuration file. If no file is provided, a default + * configuration is used with four layers: {@code application}, {@code resources}, + * {@code snapshot-dependencies} and {@code dependencies}. + * @return the layers configuration file + */ + public File getConfiguration() { + return this.configuration; + } + + public void setConfiguration(File configuration) { + this.configuration = configuration; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/LoggingMainClassTimeoutWarningListener.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/LoggingMainClassTimeoutWarningListener.java new file mode 100644 index 000000000000..7e2b8d475618 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/LoggingMainClassTimeoutWarningListener.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.maven; + +import java.util.function.Supplier; + +import org.apache.maven.plugin.logging.Log; + +import org.springframework.boot.loader.tools.Packager.MainClassTimeoutWarningListener; + +/** + * {@link MainClassTimeoutWarningListener} backed by a supplied Maven {@link Log}. + * + * @author Phillip Webb + */ +class LoggingMainClassTimeoutWarningListener implements MainClassTimeoutWarningListener { + + private final Supplier log; + + LoggingMainClassTimeoutWarningListener(Supplier log) { + this.log = log; + } + + @Override + public void handleTimeoutWarning(long duration, String mainMethod) { + this.log.get().warn("Searching for the main-class is taking some time, " + + "consider using the mainClass configuration parameter"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/PropertiesMergingResourceTransformer.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/PropertiesMergingResourceTransformer.java index 3d5141f15583..a553fc8298fb 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/PropertiesMergingResourceTransformer.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/PropertiesMergingResourceTransformer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ import java.util.jar.JarOutputStream; import org.apache.maven.plugins.shade.relocation.Relocator; -import org.apache.maven.plugins.shade.resource.ResourceTransformer; +import org.apache.maven.plugins.shade.resource.ReproducibleResourceTransformer; /** * Extension for the Maven @@ -35,13 +35,15 @@ * @author Andy Wilkinson * @since 1.0.0 */ -public class PropertiesMergingResourceTransformer implements ResourceTransformer { +public class PropertiesMergingResourceTransformer implements ReproducibleResourceTransformer { // Set this in pom configuration with ... private String resource; private final Properties data = new Properties(); + private long time; + /** * Return the data the properties being merged. * @return the data @@ -56,12 +58,22 @@ public boolean canTransformResource(String resource) { } @Override + @Deprecated public void processResource(String resource, InputStream inputStream, List relocators) throws IOException { + processResource(resource, inputStream, relocators, 0); + } + + @Override + public void processResource(String resource, InputStream inputStream, List relocators, long time) + throws IOException { Properties properties = new Properties(); properties.load(inputStream); inputStream.close(); properties.forEach((name, value) -> process((String) name, (String) value)); + if (time > this.time) { + this.time = time; + } } private void process(String name, String value) { @@ -76,7 +88,9 @@ public boolean hasTransformedResource() { @Override public void modifyOutputStream(JarOutputStream os) throws IOException { - os.putNextEntry(new JarEntry(this.resource)); + JarEntry jarEntry = new JarEntry(this.resource); + jarEntry.setTime(this.time); + os.putNextEntry(jarEntry); this.data.store(os, "Merged by PropertiesMergingResourceTransformer"); os.flush(); this.data.clear(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java index d57f51d3db4b..2e77b5eb8189 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,40 +18,30 @@ import java.io.File; import java.io.IOException; -import java.util.ArrayList; +import java.nio.file.attribute.FileTime; +import java.time.OffsetDateTime; import java.util.List; import java.util.Properties; -import java.util.Set; +import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; import org.apache.maven.artifact.Artifact; import org.apache.maven.model.Dependency; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; -import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; -import org.apache.maven.project.MavenProject; -import org.apache.maven.project.MavenProjectHelper; -import org.apache.maven.shared.artifact.filter.collection.ArtifactsFilter; -import org.apache.maven.shared.artifact.filter.collection.ScopeFilter; import org.springframework.boot.loader.tools.DefaultLaunchScript; import org.springframework.boot.loader.tools.LaunchScript; -import org.springframework.boot.loader.tools.Layout; import org.springframework.boot.loader.tools.LayoutFactory; -import org.springframework.boot.loader.tools.Layouts.Expanded; -import org.springframework.boot.loader.tools.Layouts.Jar; -import org.springframework.boot.loader.tools.Layouts.None; -import org.springframework.boot.loader.tools.Layouts.War; import org.springframework.boot.loader.tools.Libraries; import org.springframework.boot.loader.tools.Repackager; -import org.springframework.boot.loader.tools.Repackager.MainClassTimeoutWarningListener; /** - * Repackages existing JAR and WAR archives so that they can be executed from the command + * Repackage existing JAR and WAR archives so that they can be executed from the command * line using {@literal java -jar}. With layout=NONE can also be used simply * to package a JAR with nested dependencies (and no main class, so not executable). * @@ -59,29 +49,16 @@ * @author Dave Syer * @author Stephane Nicoll * @author Björn Lindström + * @author Scott Frederick * @since 1.0.0 */ @Mojo(name = "repackage", defaultPhase = LifecyclePhase.PACKAGE, requiresProject = true, threadSafe = true, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME) -public class RepackageMojo extends AbstractDependencyFilterMojo { +public class RepackageMojo extends AbstractPackagerMojo { private static final Pattern WHITE_SPACE_PATTERN = Pattern.compile("\\s+"); - /** - * The Maven project. - * @since 1.0.0 - */ - @Parameter(defaultValue = "${project}", readonly = true, required = true) - private MavenProject project; - - /** - * Maven project helper utils. - * @since 1.0.0 - */ - @Component - private MavenProjectHelper projectHelper; - /** * Directory containing the generated archive. * @since 1.0.0 @@ -112,45 +89,24 @@ public class RepackageMojo extends AbstractDependencyFilterMojo { * attached as a supplemental artifact with that classifier. Attaching the artifact * allows to deploy it alongside to the original one, see the maven documentation for more details. + * >the Maven documentation for more details. * @since 1.0.0 */ @Parameter private String classifier; /** - * Attach the repackaged archive to be installed and deployed. + * Attach the repackaged archive to be installed into your local Maven repository or + * deployed to a remote repository. If no classifier has been configured, it will + * replace the normal jar. If a {@code classifier} has been configured such that the + * normal jar and the repackaged jar are different, it will be attached alongside the + * normal jar. When the property is set to {@code false}, the repackaged archive will + * not be installed or deployed. * @since 1.4.0 */ @Parameter(defaultValue = "true") private boolean attach = true; - /** - * The name of the main class. If not specified the first compiled class found that - * contains a 'main' method will be used. - * @since 1.0.0 - */ - @Parameter - private String mainClass; - - /** - * The type of archive (which corresponds to how the dependencies are laid out inside - * it). Possible values are JAR, WAR, ZIP, DIR, NONE. Defaults to a guess based on the - * archive type. - * @since 1.0.0 - */ - @Parameter(property = "spring-boot.repackage.layout") - private LayoutType layout; - - /** - * The layout factory that will be used to create the executable archive if no - * explicit layout is set. Alternative layouts implementations can be provided by 3rd - * parties. - * @since 1.5.0 - */ - @Parameter - private LayoutFactory layoutFactory; - /** * A list of the libraries that must be unpacked from fat jars in order to run. * Specify each library as a {@code } with a {@code } and a @@ -190,18 +146,52 @@ public class RepackageMojo extends AbstractDependencyFilterMojo { private Properties embeddedLaunchScriptProperties; /** - * Exclude Spring Boot devtools from the repackaged archive. - * @since 1.3.0 + * Timestamp for reproducible output archive entries, either formatted as ISO 8601 + * (yyyy-MM-dd'T'HH:mm:ssXXX) or an {@code int} representing seconds + * since the epoch. + * @since 2.3.0 */ - @Parameter(property = "spring-boot.repackage.excludeDevtools", defaultValue = "true") - private boolean excludeDevtools = true; + @Parameter(defaultValue = "${project.build.outputTimestamp}") + private String outputTimestamp; /** - * Include system scoped dependencies. - * @since 1.4.0 + * The type of archive (which corresponds to how the dependencies are laid out inside + * it). Possible values are {@code JAR}, {@code WAR}, {@code ZIP}, {@code DIR}, + * {@code NONE}. Defaults to a guess based on the archive type. + * @since 1.0.0 */ - @Parameter(defaultValue = "false") - public boolean includeSystemScope; + @Parameter(property = "spring-boot.repackage.layout") + private LayoutType layout; + + /** + * The layout factory that will be used to create the executable archive if no + * explicit layout is set. Alternative layouts implementations can be provided by 3rd + * parties. + * @since 1.5.0 + */ + @Parameter + private LayoutFactory layoutFactory; + + /** + * Return the type of archive that should be packaged by this MOJO. + * @return the value of the {@code layout} parameter, or {@code null} if the parameter + * is not provided + */ + @Override + protected LayoutType getLayout() { + return this.layout; + } + + /** + * Return the layout factory that will be used to determine the + * {@link AbstractPackagerMojo.LayoutType} if no explicit layout is set. + * @return the value of the {@code layoutFactory} parameter, or {@code null} if the + * parameter is not provided + */ + @Override + protected LayoutFactory getLayoutFactory() { + return this.layoutFactory; + } @Override public void execute() throws MojoExecutionException, MojoFailureException { @@ -217,14 +207,13 @@ public void execute() throws MojoExecutionException, MojoFailureException { } private void repackage() throws MojoExecutionException { - Artifact source = getSourceArtifact(); - File target = getTargetFile(); + Artifact source = getSourceArtifact(this.classifier); + File target = getTargetFile(this.finalName, this.classifier, this.outputDirectory); Repackager repackager = getRepackager(source.getFile()); - Set artifacts = filterDependencies(this.project.getArtifacts(), getFilters(getAdditionalFilters())); - Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack, getLog()); + Libraries libraries = getLibraries(this.requiresUnpack); try { LaunchScript launchScript = getLaunchScript(); - repackager.repackage(target, libraries, launchScript); + repackager.repackage(target, libraries, launchScript, parseOutputTimestamp()); } catch (IOException ex) { throw new MojoExecutionException(ex.getMessage(), ex); @@ -232,65 +221,26 @@ private void repackage() throws MojoExecutionException { updateArtifact(source, target, repackager.getBackupFile()); } - /** - * Return the source {@link Artifact} to repackage. If a classifier is specified and - * an artifact with that classifier exists, it is used. Otherwise, the main artifact - * is used. - * @return the source artifact to repackage - */ - private Artifact getSourceArtifact() { - Artifact sourceArtifact = getArtifact(this.classifier); - return (sourceArtifact != null) ? sourceArtifact : this.project.getArtifact(); - } - - private Artifact getArtifact(String classifier) { - if (classifier != null) { - for (Artifact attachedArtifact : this.project.getAttachedArtifacts()) { - if (classifier.equals(attachedArtifact.getClassifier()) && attachedArtifact.getFile() != null - && attachedArtifact.getFile().isFile()) { - return attachedArtifact; - } - } + private FileTime parseOutputTimestamp() { + // Maven ignore a single-character timestamp as it is "useful to override a full + // value during pom inheritance" + if (this.outputTimestamp == null || this.outputTimestamp.length() < 2) { + return null; } - return null; + return FileTime.from(getOutputTimestampEpochSeconds(), TimeUnit.SECONDS); } - private File getTargetFile() { - String classifier = (this.classifier != null) ? this.classifier.trim() : ""; - if (!classifier.isEmpty() && !classifier.startsWith("-")) { - classifier = "-" + classifier; + private long getOutputTimestampEpochSeconds() { + try { + return Long.parseLong(this.outputTimestamp); } - if (!this.outputDirectory.exists()) { - this.outputDirectory.mkdirs(); + catch (NumberFormatException ex) { + return OffsetDateTime.parse(this.outputTimestamp).toInstant().getEpochSecond(); } - return new File(this.outputDirectory, - this.finalName + classifier + "." + this.project.getArtifact().getArtifactHandler().getExtension()); } private Repackager getRepackager(File source) { - Repackager repackager = new Repackager(source, this.layoutFactory); - repackager.addMainClassTimeoutWarningListener(new LoggingMainClassTimeoutWarningListener()); - repackager.setMainClass(this.mainClass); - if (this.layout != null) { - getLog().info("Layout: " + this.layout); - repackager.setLayout(this.layout.layout()); - } - return repackager; - } - - private ArtifactsFilter[] getAdditionalFilters() { - List filters = new ArrayList<>(); - if (this.excludeDevtools) { - Exclude exclude = new Exclude(); - exclude.setGroupId("org.springframework.boot"); - exclude.setArtifactId("spring-boot-devtools"); - ExcludeFilter filter = new ExcludeFilter(exclude); - filters.add(filter); - } - if (!this.includeSystemScope) { - filters.add(new ScopeFilter(null, Artifact.SCOPE_SYSTEM)); - } - return filters.toArray(new ArtifactsFilter[0]); + return getConfiguredPackager(() -> new Repackager(source)); } private LaunchScript getLaunchScript() throws IOException { @@ -355,56 +305,4 @@ private void attachArtifact(Artifact source, File target) { } } - private class LoggingMainClassTimeoutWarningListener implements MainClassTimeoutWarningListener { - - @Override - public void handleTimeoutWarning(long duration, String mainMethod) { - getLog().warn("Searching for the main-class is taking some time, " - + "consider using the mainClass configuration parameter"); - } - - } - - /** - * Archive layout types. - */ - public enum LayoutType { - - /** - * Jar Layout. - */ - JAR(new Jar()), - - /** - * War Layout. - */ - WAR(new War()), - - /** - * Zip Layout. - */ - ZIP(new Expanded()), - - /** - * Dir Layout. - */ - DIR(new Expanded()), - - /** - * No Layout. - */ - NONE(new None()); - - private final Layout layout; - - LayoutType(Layout layout) { - this.layout = layout; - } - - public Layout layout() { - return this.layout; - } - - } - } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java index 510ae049a9a1..959dbc48721f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,11 +30,10 @@ import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; -import org.springframework.boot.loader.tools.JavaExecutable; import org.springframework.boot.loader.tools.RunProcess; /** - * Run an executable archive application. + * Run an application in place. * * @author Phillip Webb * @author Dmytro Nosan @@ -63,12 +62,6 @@ public class RunMojo extends AbstractRunMojo { @Parameter(property = "spring-boot.run.optimizedLaunch", defaultValue = "true") private boolean optimizedLaunch; - @Override - @Deprecated - protected boolean enableForkByDefault() { - return super.enableForkByDefault() || hasDevtools(); - } - @Override protected void logDisabledFork() { super.logDisabledFork(); @@ -111,7 +104,7 @@ protected void runWithForkedJvm(File workingDirectory, List args, Map args, Map environmentVariables) throws MojoExecutionException { try { - RunProcess runProcess = new RunProcess(workingDirectory, new JavaExecutable().toString()); + RunProcess runProcess = new RunProcess(workingDirectory, getJavaExecutable()); Runtime.getRuntime().addShutdownHook(new Thread(new RunProcessKiller(runProcess))); return runProcess.run(true, args, environmentVariables); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StartMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StartMojo.java index 240d9d41a7cb..0ae146fd4207 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StartMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StartMojo.java @@ -37,12 +37,11 @@ import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; -import org.springframework.boot.loader.tools.JavaExecutable; import org.springframework.boot.loader.tools.RunProcess; /** * Start a spring application. Contrary to the {@code run} goal, this does not block and - * allows other goal to operate on the application. This goal is typically used in + * allows other goals to operate on the application. This goal is typically used in * integration test scenario where the application is started before a test suite and * stopped after. * @@ -104,7 +103,7 @@ protected void runWithForkedJvm(File workingDirectory, List args, Map args, Map environmentVariables) throws MojoExecutionException { try { - RunProcess runProcess = new RunProcess(workingDirectory, new JavaExecutable().toString()); + RunProcess runProcess = new RunProcess(workingDirectory, getJavaExecutable()); runProcess.run(false, args, environmentVariables); return runProcess; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StopMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StopMojo.java index 52aae26fd7f2..5afb6b8f305c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StopMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StopMojo.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,8 +32,8 @@ import org.apache.maven.project.MavenProject; /** - * Stop a spring application that has been started by the "start" goal. Typically invoked - * once a test suite has completed. + * Stop an application that has been started by the "start" goal. Typically invoked once a + * test suite has completed. * * @author Stephane Nicoll * @since 1.3.0 @@ -49,9 +49,10 @@ public class StopMojo extends AbstractMojo { private MavenProject project; /** - * Flag to indicate if process to stop was forked. By default, the value is inherited - * from the {@link MavenProject}. If it is set, it must match the value used to - * {@link StartMojo start} the process. + * Flag to indicate if the process to stop was forked. By default, the value is + * inherited from the {@link MavenProject} with a fallback on the default fork value + * ({@code true}). If it is set, it must match the value used to {@link StartMojo + * start} the process. * @since 1.3.0 */ @Parameter(property = "spring-boot.stop.fork") @@ -103,8 +104,11 @@ private boolean isForked() { if (this.fork != null) { return this.fork; } - String property = this.project.getProperties().getProperty("_spring.boot.fork.enabled"); - return Boolean.parseBoolean(property); + String forkFromStart = this.project.getProperties().getProperty("_spring.boot.fork.enabled"); + if (forkFromStart != null) { + return Boolean.parseBoolean(forkFromStart); + } + return true; } private void stopForkedProcess() throws IOException, MojoFailureException, MojoExecutionException { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/VersionExtractor.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/VersionExtractor.java new file mode 100644 index 000000000000..41462ebf5ad1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/VersionExtractor.java @@ -0,0 +1,67 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.maven; + +import java.io.File; +import java.io.IOException; +import java.net.JarURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.util.jar.Attributes; +import java.util.jar.JarFile; + +/** + * Extracts version information for a Class. + * + * @author Andy Wilkinson + * @author Scott Frederick + */ +final class VersionExtractor { + + private VersionExtractor() { + } + + /** + * Return the version information for the provided {@link Class}. + * @param cls the Class to retrieve the version for + * @return the version, or {@code null} if a version can not be extracted + */ + static String forClass(Class cls) { + String implementationVersion = cls.getPackage().getImplementationVersion(); + if (implementationVersion != null) { + return implementationVersion; + } + URL codeSourceLocation = cls.getProtectionDomain().getCodeSource().getLocation(); + try { + URLConnection connection = codeSourceLocation.openConnection(); + if (connection instanceof JarURLConnection) { + return getImplementationVersion(((JarURLConnection) connection).getJarFile()); + } + try (JarFile jarFile = new JarFile(new File(codeSourceLocation.toURI()))) { + return getImplementationVersion(jarFile); + } + } + catch (Exception ex) { + return null; + } + } + + private static String getImplementationVersion(JarFile jarFile) throws IOException { + return jarFile.getManifest().getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/xsd/layers-2.3.xsd b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/xsd/layers-2.3.xsd new file mode 100644 index 000000000000..01eca01439c9 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/xsd/layers-2.3.xsd @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/xsd/layers-2.4.xsd b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/xsd/layers-2.4.xsd new file mode 100644 index 000000000000..57eac0d6b9f5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/xsd/layers-2.4.xsd @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/xsd/layers-2.5.xsd b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/xsd/layers-2.5.xsd new file mode 100644 index 000000000000..57eac0d6b9f5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/xsd/layers-2.5.xsd @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/maven/resources/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/maven/resources/pom.xml new file mode 100644 index 000000000000..559dd6235eee --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/maven/resources/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + org.springframework.boot + spring-boot-maven-plugin + {{version}} + maven-plugin + Spring Boot Maven Plugin + https://projects.spring.io/spring-boot/# + + UTF-8 + + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0 + + + + https://github.com/spring-projects/spring-boot + scm:git:git://github.com/spring-projects/spring-boot.git + scm:git:ssh://git@github.com/spring-projects/spring-boot.git + + + GitHub + https://github.com/spring-projects/spring-boot/issues + + + Pivotal Software, Inc. + https://spring.io + + + + + org.apache.maven.plugins + maven-plugin-plugin + 3.6.0 + + org.springframework.boot.maven + + + + + \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/build-info.apt.vm b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/build-info.apt.vm deleted file mode 100644 index 359a55cabe7d..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/build-info.apt.vm +++ /dev/null @@ -1,56 +0,0 @@ - ----- - Generate build information - ----- - Stephane Nicoll - ----- - 2016-03-17 - ----- - - Spring Boot Actuator displays build-related information if a <<>> - file is present. The <<>> goal generates such file with the coordinates of the project - and the build time. It also allows you to add an arbitrary number of additional properties: - ---- - - ... - - ... - - ... - - ${project.groupId} - ${project.artifactId} - ${project.version} - - - - build-info - - - - UTF-8 - UTF-8 - ${maven.compiler.source} - ${maven.compiler.target} - - - - - ... - - ... - - ... - - ... - ---- - - This configuration will generate a <<>> at the expected location with - four additional keys. Note that <<>> and <<>> - are expected to be regular properties available in the project. They will be interpolated as - you would expect. - - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/custom-layout.apt b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/custom-layout.apt deleted file mode 100644 index 2cffd8f02959..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/custom-layout.apt +++ /dev/null @@ -1,59 +0,0 @@ - ----- - Use a custom layout - ----- - Dave Syer - ----- - 2016-10-30 - ----- - - Spring Boot repackages the jar file for this project using a custom layout factory - defined in the additional jar file, provided as a dependency to the build plugin: - ---- - - ... - - ... - - ... - - ${project.groupId} - ${project.artifactId} - ${project.version} - - - repackage - - repackage - - - - value - - - - - - - com.example - custom-layout - 0.0.1.BUILD-SNAPSHOT - - - ... - - ... - - ... - - ... - ---- - - The layout factory is provided as an implementation of <<>> (from - spring-boot-loader-tools) explicitly specified in the pom. If there is only one custom - <<>> on the plugin classpath and it is listed in - <<>> then it is unnecessary to explicitly set it in the - plugin configuration. - - Layout factories are always ignored if an explicit <> is set. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/exclude-dependency.apt.vm b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/exclude-dependency.apt.vm deleted file mode 100644 index a99283699146..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/exclude-dependency.apt.vm +++ /dev/null @@ -1,84 +0,0 @@ - ----- - Exclude a dependency - ----- - Stephane Nicoll - ----- - 2014-05-06 - ----- - - By default, both the <<>> and the <<>> goals will include any <<>> - dependencies that are defined in the project. A Spring Boot project should consider - <<>> dependencies as <> dependencies that are required to run - the application. - - Some of these dependencies may not be required at all and should be excluded from the - executable jar. For consistency, they should not be present either when running the - application. - - There are two ways one can exclude a dependency from being packaged/used at runtime - - * Exclude a specific artifact identified by <<>> and <<>> - (optionally with a <<>> if needed) - - * Exclude any artifact belonging to a given <<>> - - [] - - The following excludes <<>> (and only that artifact) - ---- - - ... - - ... - - ... - - ${project.groupId} - ${project.artifactId} - ${project.version} - - - - com.foo - bar - - - - ... - - ... - - ... - - ... - ---- - - This example excludes any artifact belonging to the <<>> group - - ---- - - ... - - ... - - ... - - ${project.groupId} - ${project.artifactId} - ${project.version} - - com.foo - - ... - - ... - - ... - - ... - ---- - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/it-random-port.apt.vm b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/it-random-port.apt.vm deleted file mode 100644 index 8635e87c3bb3..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/it-random-port.apt.vm +++ /dev/null @@ -1,82 +0,0 @@ - ----- - Random port for integration tests - ----- - Stephane Nicoll - ----- - 2015-04-16 - ----- - - One nice feature of the Spring Boot test integration is that it can allocate a free - port for the web application. When the <<>> goal of the plugin is used, the - Spring Boot application is started separately, making it difficult to pass the actual - port to the integration test itself. - - The example below showcases how you could achieve the same feature using the - {{{https://www.mojohaus.org/build-helper-maven-plugin/}build-helper-plugin}}: - ---- - - ... - - ... - - ... - - org.codehaus.mojo - build-helper-maven-plugin - - - reserve-tomcat-port - - reserve-network-port - - process-resources - - - tomcat.http.port - - - - - - - ${project.groupId} - ${project.artifactId} - ${project.version} - - - pre-integration-test - - start - - - - --server.port=${tomcat.http.port} - - - - - post-integration-test - - stop - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - - ${tomcat.http.port} - - - - ... - - ... - ---- - - You can now retrieve the <<>> system property in any of your integration test - to create a proper <<>> to the server. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/it-skip.apt.vm b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/it-skip.apt.vm deleted file mode 100644 index f846bca20c5c..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/it-skip.apt.vm +++ /dev/null @@ -1,63 +0,0 @@ - ----- - Skip integration tests - ----- - Stephane Nicoll - ----- - 2016-11-25 - ----- - - The <<>> property allows to skip the execution of the Spring Boot maven plugin - altogether. This example shows how you can skip integration tests with a command-line - property and still make sure that the <> goal runs: - ---- - - - false - ... - - ... - - ... - - ... - - ${project.groupId} - ${project.artifactId} - ${project.version} - - - pre-integration-test - - start - - - ${skip.it} - - - - post-integration-test - - stop - - - ${skip.it} - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - ${skip.it} - - - ... - - ... - ---- - - By default, the integration tests will run but this setup allows you to easily disable - them on the command-line as follows: <<>>. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/repackage-classifier.apt.vm b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/repackage-classifier.apt.vm deleted file mode 100644 index d64dc2ea6438..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/repackage-classifier.apt.vm +++ /dev/null @@ -1,187 +0,0 @@ - ----- - Repackage classifier - ----- - Stephane Nicoll - ----- - 2014-05-02 - ----- - - By default, the <<>> goal will replace the original artifact with the - repackaged one. That's a sane behavior for modules that represent an app but if - your module is used as a dependency of another module, you need to provide a - classifier for the repackaged one. - - The reason for that is that application classes are packaged in <<>> - so that the dependent module cannot load a repackaged jar's classes. If that is the - case or if you prefer to keep the original artifact and attach the repackaged one - with a different classifier, configure the plugin as follows: - ---- - - ... - - ... - - ... - - ${project.groupId} - ${project.artifactId} - ${project.version} - - - repackage - - repackage - - - exec - - - - ... - - ... - - ... - - ... - ---- - - If you are using `spring-boot-starter-parent`, the `repackage` goal is executed - automatically in an execution with id `repackage`. In that setup, only the configuration - should be specified as shown in the following example: - ---- - - ... - - ... - - ... - - ${project.groupId} - ${project.artifactId} - - - repackage - - exec - - - - ... - - ... - - ... - - ... - ---- - - This configuration will generate two artifacts: the original one and the repackaged - counter part produced by the repackage goal. Both will be installed/deployed - transparently. - - You can also use the same configuration if you want to repackage a secondary artifact - the same way the main artifact is replaced. The following configuration installs/deploys - a single <<>> classified artifact with the repackaged app: - ---- - - ... - - ... - - ... - - org.apache.maven.plugins - maven-jar-plugin - @maven-jar-plugin.version@ - - - - jar - - package - - task - - - - - - ${project.groupId} - ${project.artifactId} - ${project.version} - - - repackage - - repackage - - - task - - - - ... - - ... - - ... - - ... - ---- - - As both the <<>> and the <<>> runs at the - same phase, it is important that the jar plugin is defined first (so that it runs before - the repackage goal). - - Again, if you are using `spring-boot-starter-parent`, this can be simplified as follows: - ---- - - ... - - ... - - ... - - org.apache.maven.plugins - maven-jar-plugin - - - default-jar - - task - - - - - - ${project.groupId} - ${project.artifactId} - - - repackage - - task - - - - ... - - ... - - ... - - ... - ---- - - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/repackage-disable-attach.apt.vm b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/repackage-disable-attach.apt.vm deleted file mode 100644 index dcd4dfa142c2..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/repackage-disable-attach.apt.vm +++ /dev/null @@ -1,50 +0,0 @@ - ----- - Local repackaged artifact - ----- - Stephane Nicoll - ----- - 2016-03-01 - ----- - - By default, the <<>> goal will replace the original artifact with the - executable one. If you need to only deploy the original jar and yet be able to - run your app with the regular file name, configure the plugin as follows: - ---- - - ... - - ... - - ... - - ${project.groupId} - ${project.artifactId} - ${project.version} - - - repackage - - repackage - - - false - - - - ... - - ... - - ... - - ... - ---- - - This configuration will generate two artifacts: the original one and the executable counter - part produced by the repackage goal. Only the original one will be installed/deployed. - - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/repackage-name.apt.vm b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/repackage-name.apt.vm deleted file mode 100644 index 9a21dd9ff88d..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/repackage-name.apt.vm +++ /dev/null @@ -1,42 +0,0 @@ - ----- - Repackage classifier - ----- - Stephane Nicoll - ----- - 2019-03-29 - ----- - - If you need the repackaged jar to have a different local name than the one defined by - the <<>> attribute of the project, simply use the standard <<>> - as shown in the following example: - ---- - - ... - - my-app - - ... - - ${project.groupId} - ${project.artifactId} - ${project.version} - - - repackage - - repackage - - - - ... - - ... - - ... - - ... - ---- - - This configuration will generate the repackaged artifact in <<>>. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/run-debug.apt.vm b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/run-debug.apt.vm deleted file mode 100644 index d8d5d0261bac..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/run-debug.apt.vm +++ /dev/null @@ -1,47 +0,0 @@ - ----- - Debug the application - ----- - Stephane Nicoll - ----- - 2014-05-14 - ----- - - By default, the <<>> goal runs your application in a forked process. If you need to - debug it, you should add the necessary JVM arguments to enable remote debugging. The - following configuration suspend the process until a debugger has joined on port 5005: - ---- - - ... - - ... - - ... - - ${project.groupId} - ${project.artifactId} - ${project.version} - - - -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005 - - - ... - - ... - - ... - - ... - ---- - - These arguments can be specified on the command line as well, make sure to wrap that - properly, that is: - ---- -mvn spring-boot:run -Dspring-boot.run.jvmArguments="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005" ---- - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/run-env-variables.apt.vm b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/run-env-variables.apt.vm deleted file mode 100644 index 7a3b7e560aa5..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/run-env-variables.apt.vm +++ /dev/null @@ -1,51 +0,0 @@ - ----- - Using environment variables - ----- -Dmytro Nosan - ----- - 2018-04-08 - ----- - - Environment variables can be specified using the <<>> attribute. - The following sets the 'ENV1', 'ENV2', 'ENV3', 'ENV4' env variables: - ---- - - ... - - ... - - ... - - ${project.groupId} - ${project.artifactId} - ${project.version} - - - 5000 - Some Text - - - - - ... - - ... - - ... - - ... - ---- - - If the value is empty or not defined (i.e. <<<>>>), the env variable is set - with an empty String as the value. Maven trims values specified in the pom so it is - not possible to specify an env variable which needs to start or end with a space. - - Any String typed Maven variable can be passed as system properties. Any attempt to pass - any other Maven variable type (e.g. a <<>> or a <<>> variable) will cause the - variable expression to be passed literally (unevaluated). - - Environment variables defined this way take precedence over existing values. - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/run-profiles.apt.vm b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/run-profiles.apt.vm deleted file mode 100644 index 203add200eb9..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/run-profiles.apt.vm +++ /dev/null @@ -1,47 +0,0 @@ - ----- - Specify active profiles - ----- - Stephane Nicoll - ----- - 2014-07-07 - ----- - - The active profiles to use for a particular application can be specified using the <<>> - argument. The following configuration enables the foo and bar profiles: - ---- - - ... - - ... - - ... - - ${project.groupId} - ${project.artifactId} - ${project.version} - - - foo - bar - - - ... - - ... - - ... - - ... - ---- - - The profiles to enable can be specified on the command line as well, make sure to separate them with - a comma, that is: - ---- -mvn spring-boot:run -Dspring-boot.run.profiles=foo,bar ---- - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/run-system-properties.apt.vm b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/run-system-properties.apt.vm deleted file mode 100644 index 0483f58f9dff..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/run-system-properties.apt.vm +++ /dev/null @@ -1,61 +0,0 @@ - ----- - Using System Properties - ----- - Stephane Nicoll - ----- - 2018-04-24 - ----- - - System properties can be specified using the <<>> attribute. - The following sets <<>> to <<>> and <<>> to 42: - ---- - - ... - - - 42 - - ... - - ... - - ${project.groupId} - ${project.artifactId} - ${project.version} - - - test - ${my.value} - - - ... - - ... - - ... - - ... - ---- - - If the value is empty or not defined (i.e. <<<>>>), the system property - is set with an empty String as the value. Maven trims values specified in the pom so it - is not possible to specify a System property which needs to start or end with a space via - this mechanism: consider using <<>> instead. - - Any String typed Maven variable can be passed as system properties. Any attempt to pass - any other Maven variable type (e.g. a <<>> or a <<>> variable) will cause the - variable expression to be passed literally (unevaluated). - - The <<>> parameter takes precedence over system properties defined with - the mechanism above. In the following example, the value for <<>> is - <<>>: - - ---- -mvn spring-boot:run -Dspring-boot.run.jvmArguments="-Dproperty1=overridden" ---- - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/index.apt b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/index.apt deleted file mode 100644 index 64cd7a049a6d..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/index.apt +++ /dev/null @@ -1,74 +0,0 @@ - ----- - Spring Boot - ----- - Stephane Nicoll - ----- - 2014-05-02 - ----- - -Spring Boot Maven Plugin - - The Spring Boot Maven Plugin provides Spring Boot support in Maven, allowing you to package executable - jar or war archives and run an application “in-placeâ€. - -* Goals Overview - - The Spring Boot Plugin has the following goals. - - * {{{./run-mojo.html}spring-boot:run}} runs your Spring Boot application. - - * {{{./repackage-mojo.html}spring-boot:repackage}} repackages your jar/war to be executable. - - * {{{./start-mojo.html}spring-boot:start}} and {{{./stop-mojo.html}spring-boot:stop}} to manage - the lifecycle of your Spring Boot application (i.e. for integration tests). - - * {{{./build-info-mojo.html}spring-boot:build-info}} generates build information that can be used - by the Actuator. - - - -* Usage - - General instructions on how to use the Spring Boot Plugin can be found on the {{{./usage.html}usage page}}. Some - more specific use cases are described in the examples given below. - - In case you still have questions regarding the plugin's usage, please have a look at the existing - {{{https://stackoverflow.com/questions/tagged/spring-boot}stack overflow issue}}. If you still don't get an - answer, feel free to create a new thread with the <<<#spring-boot>>> tag. - - If you feel like the plugin is missing a feature or has a defect, you can fill a feature request or bug report - in our {{{./issue-tracking.html}issue tracker}}. - -* Examples - - To provide you with better understanding of some usages of Spring Boot, you can take a look into - the following examples: - - * {{{./examples/repackage-classifier.html}Custom repackage classifier}} - - * {{{./examples/repackage-name.html}Custom repackage name}} - - * {{{./examples/repackage-disable-attach.html}Local repackaged artifact}} - - * {{{./examples/exclude-dependency.html}Exclude a dependency}} - - * {{{./examples/run-debug.html}Debug the application}} - - * {{{./examples/run-system-properties.html}Using system properties}} - - * {{{./examples/run-env-variables.html}Using environment variables}} - - * {{{./examples/it-random-port.html}Random port for integration tests}} - - * {{{./examples/it-skip.html}Skip integration tests}} - - * {{{./examples/run-profiles.html}Specify active profiles}} - - * {{{./examples/build-info.html}Generate build information}} - - * {{{./examples/custom-layout.html}Custom layout}} - - [] - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/usage.apt.vm b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/usage.apt.vm deleted file mode 100644 index fb70a9769758..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/usage.apt.vm +++ /dev/null @@ -1,314 +0,0 @@ - - ----- - Usage - ----- - Stephane Nicoll - ----- - 2014-05-06 - ----- - -Usage - - The plugin provides several goals to work with a Spring Boot application: - - * <<>>: create a jar or war file that is auto-executable. It can replace the regular artifact or can be - attached to the build lifecycle with a separate <>. - - * <<>>: run your Spring Boot application with several options to pass parameters to it. - - * <<>> and <<>>: integrate your Spring Boot application to the <<>> phase so that - the application starts before it. - - * <<>>: generate a build information that can be used by the Actuator. - - [] - - Each goal is further described below. - -* Repackaging an application - - In order to repackage your application, you simply need to add a reference to the - plugin in your <<>>: - ---- - - ... - - ... - - ${project.groupId} - ${project.artifactId} - ${project.version} - - - - repackage - - - - - ... - - ... - ---- - - The example above repackages a jar or war that is built during the package phase of the Maven lifecycle, - including any <<>> dependencies that are defined in the project. If some of these dependencies - need to be excluded, you can use one of the exclude options, - see {{{./examples/exclude-dependency.html}Exclude a dependency}} for more details. - Please note that the <<>> feature of the <<>> - is currently not supported. - - Devtools is automatically excluded by default (you can control that using the - <<>> property). In order to make that work with <<>> packaging, - the <<>> dependency must be set as <<>> or with the - <<>> scope. - - The original (i.e. non executable) artifact is renamed to <<<.original>>> by default but it is also - possible to keep the original artifact using a custom classifier. - - The plugin rewrites your manifest, and in particular it manages the <> and <> - entries, so if the defaults don't work you have to configure those there (not in the jar plugin). The - <> in the manifest is actually controlled by the <> property of the boot plugin, e.g. - ---- - - ... - - ... - - ${project.groupId} - ${project.artifactId} - ${project.version} - - ${start-class} - ZIP - - - - - repackage - - - - - ... - - ... - ---- - - The <<>> property defaults to a guess based on the archive type (<<>> or <<>>). The - following layouts are available: - - * <<>>: regular executable JAR layout. - - * <<>>: executable WAR layout. <<>> dependencies are placed in <<>> - to avoid any clash when the <<>> is deployed in a servlet container. - - * <<>> (alias to <<

    >>): similar to the <<>> layout using <<>>. - - * <<>>: Bundle all dependencies and project resources. Does not bundle a bootstrap loader. - - [] - - For more detailed examples of how to configure this goal see: - - * {{{./examples/repackage-classifier.html}Custom repackage classifier}} - - * {{{./examples/exclude-dependency.html}Exclude a dependency}} - - [] - -* Running the application - - The plugin includes a run goal which can be used to launch your application from the command - line: - ---- -mvn spring-boot:run ---- - - By default the application is executed in a forked process and setting properties on the - command-line will not affect the application. If you need to specify some JVM arguments - (i.e. for debugging purposes), you can use the <<>> parameter, see - {{{./examples/run-debug.html}Debug the application}} for more details. There is also - explicit support for {{{./examples/run-system-properties.html}system properties}} and - {{{./examples/run-env-variables.html}environment variables}}. - - As enabling a profile is quite common, there is dedicated <<>> property that - offers a shortcut for - <<<-Dspring-boot.run.jvmArguments="-Dspring.profiles.active=dev">>>, - see {{{./examples/run-profiles.html}Specify active profiles}}. - - Although this is not recommended, it is possible to execute the application directly - from the Maven JVM by disabling the <<>> property. Doing so means that the - <<>>, <<>>, <<>> and - <<>> options are ignored. - - Spring Boot 1.3 has introduced <<>>, a module to improve the development-time - experience when working on Spring Boot applications. To enable it, just add the following - dependency to your project: - ---- - - - org.springframework.boot - spring-boot-devtools - ${project.version} - true - - ---- - - When <<>> is running, it detects change 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: - ---- -spring.devtools.remote.restart.enabled=false ---- - - Prior to <<>>, the plugin supported hot refreshing of resources by default which has - now be disabled in favour of the solution described above. You can restore it at any time by - configuring your project: - ---- - - ... - - ... - - ${project.groupId} - ${project.artifactId} - ${project.version} - - true - - - ... - - ... - ---- - - When <<>> is enabled, any <> folder will be added to - the application classpath when you run the application and any duplicate found in - <> will be removed. This allows hot refreshing of resources which can be very - useful when developing web applications. For example, you can work on HTML, CSS or JavaScript - files and see your changes immediately without recompiling your application. It is also a helpful - way of allowing your front end developers to work without needing to download and install a Java - IDE. - - Note that a side effect of using this feature is that filtering of resources at build time will - not work. - - In order to be consistent with the <<>> goal, the <<>> goal builds the classpath - in such a way that any dependency that is excluded in the plugin's configuration gets excluded - from the classpath as well. See {{{./examples/exclude-dependency.html}Exclude a dependency}} for - more details. - - Sometimes it is useful to include test dependencies when running the application. For example, - if you want to run your application in a test mode that uses stub classes. If you wish to do this, - you can set the <<>> parameter to true. Note that this is only applied when you - run an application: the <<>> goal will not add test dependencies to the resulting JAR/WAR. - -* Working with integration tests - - While you may start your Spring Boot application very easily from your test (or test suite) itself, - it may be desirable to handle that in the build itself. To make sure that the lifecycle of your Spring - Boot application is properly managed your integration tests, you can use the <<>> and - <<>> goals as described below: - ---- - - ... - - ... - - ${project.groupId} - ${project.artifactId} - ${project.version} - - - pre-integration-test - - start - - - - post-integration-test - - stop - - - - - ... - - ... - ---- - - Such setup can now use the {{{https://maven.apache.org/surefire/maven-failsafe-plugin/}failsafe-plugin}} to - run your integration tests as you would expect. - - You could also configure a more advanced setup to skip the integration tests when a specific property has - been set: - ---- - - false - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - ${it.skip} - - - - ${project.groupId} - ${project.artifactId} - ${project.version} - - - pre-integration-test - - start - - - ${it.skip} - - - - post-integration-test - - stop - - - ${it.skip} - - - - - - ---- - - If you run <<>> your integration tests will be skipped altogether. - - - For more detailed examples of how to configure this goal see: - - * {{{./examples/it-random-port.html}Random port for integration tests}} - - [] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/site.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/site.xml deleted file mode 100644 index 6711b55c15cb..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/site/site.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - org.apache.maven.skins - maven-fluido-skin - 1.6 - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ArtifactsLibrariesTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ArtifactsLibrariesTests.java index 9243bf26d3c8..1e4eceed03c8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ArtifactsLibrariesTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ArtifactsLibrariesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.boot.maven; import java.io.File; +import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashSet; @@ -26,12 +27,14 @@ import org.apache.maven.artifact.handler.ArtifactHandler; import org.apache.maven.model.Dependency; import org.apache.maven.plugin.logging.Log; +import org.apache.maven.project.MavenProject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.boot.loader.tools.Library; import org.springframework.boot.loader.tools.LibraryCallback; @@ -39,15 +42,16 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; /** * Tests for {@link ArtifactsLibraries}. * * @author Phillip Webb */ +@ExtendWith(MockitoExtension.class) class ArtifactsLibrariesTests { @Mock @@ -70,20 +74,18 @@ class ArtifactsLibrariesTests { @BeforeEach void setup() { - MockitoAnnotations.initMocks(this); this.artifacts = Collections.singleton(this.artifact); - this.libs = new ArtifactsLibraries(this.artifacts, null, mock(Log.class)); - given(this.artifact.getFile()).willReturn(this.file); + this.libs = new ArtifactsLibraries(this.artifacts, Collections.emptyList(), null, mock(Log.class)); given(this.artifactHandler.getExtension()).willReturn("jar"); - given(this.artifact.getArtifactHandler()).willReturn(this.artifactHandler); } @Test void callbackForJars() throws Exception { - given(this.artifact.getType()).willReturn("jar"); + given(this.artifact.getFile()).willReturn(this.file); + given(this.artifact.getArtifactHandler()).willReturn(this.artifactHandler); given(this.artifact.getScope()).willReturn("compile"); this.libs.doWithLibraries(this.callback); - verify(this.callback).library(this.libraryCaptor.capture()); + then(this.callback).should().library(this.libraryCaptor.capture()); Library library = this.libraryCaptor.getValue(); assertThat(library.getFile()).isEqualTo(this.file); assertThat(library.getScope()).isEqualTo(LibraryScope.COMPILE); @@ -92,16 +94,18 @@ void callbackForJars() throws Exception { @Test void callbackWithUnpack() throws Exception { + given(this.artifact.getFile()).willReturn(this.file); + given(this.artifact.getArtifactHandler()).willReturn(this.artifactHandler); given(this.artifact.getGroupId()).willReturn("gid"); given(this.artifact.getArtifactId()).willReturn("aid"); - given(this.artifact.getType()).willReturn("jar"); given(this.artifact.getScope()).willReturn("compile"); Dependency unpack = new Dependency(); unpack.setGroupId("gid"); unpack.setArtifactId("aid"); - this.libs = new ArtifactsLibraries(this.artifacts, Collections.singleton(unpack), mock(Log.class)); + this.libs = new ArtifactsLibraries(this.artifacts, Collections.emptyList(), Collections.singleton(unpack), + mock(Log.class)); this.libs.doWithLibraries(this.callback); - verify(this.callback).library(this.libraryCaptor.capture()); + then(this.callback).should().library(this.libraryCaptor.capture()); assertThat(this.libraryCaptor.getValue().isUnpackRequired()).isTrue(); } @@ -109,14 +113,12 @@ void callbackWithUnpack() throws Exception { void renamesDuplicates() throws Exception { Artifact artifact1 = mock(Artifact.class); Artifact artifact2 = mock(Artifact.class); - given(artifact1.getType()).willReturn("jar"); given(artifact1.getScope()).willReturn("compile"); given(artifact1.getGroupId()).willReturn("g1"); given(artifact1.getArtifactId()).willReturn("artifact"); given(artifact1.getBaseVersion()).willReturn("1.0"); given(artifact1.getFile()).willReturn(new File("a")); given(artifact1.getArtifactHandler()).willReturn(this.artifactHandler); - given(artifact2.getType()).willReturn("jar"); given(artifact2.getScope()).willReturn("compile"); given(artifact2.getGroupId()).willReturn("g2"); given(artifact2.getArtifactId()).willReturn("artifact"); @@ -124,11 +126,75 @@ void renamesDuplicates() throws Exception { given(artifact2.getFile()).willReturn(new File("a")); given(artifact2.getArtifactHandler()).willReturn(this.artifactHandler); this.artifacts = new LinkedHashSet<>(Arrays.asList(artifact1, artifact2)); - this.libs = new ArtifactsLibraries(this.artifacts, null, mock(Log.class)); + this.libs = new ArtifactsLibraries(this.artifacts, Collections.emptyList(), null, mock(Log.class)); this.libs.doWithLibraries(this.callback); - verify(this.callback, times(2)).library(this.libraryCaptor.capture()); + then(this.callback).should(times(2)).library(this.libraryCaptor.capture()); assertThat(this.libraryCaptor.getAllValues().get(0).getName()).isEqualTo("g1-artifact-1.0.jar"); assertThat(this.libraryCaptor.getAllValues().get(1).getName()).isEqualTo("g2-artifact-1.0.jar"); } + @Test + void libraryCoordinatesVersionUsesBaseVersionOfArtifact() throws IOException { + Artifact snapshotArtifact = mock(Artifact.class); + given(snapshotArtifact.getScope()).willReturn("compile"); + given(snapshotArtifact.getArtifactId()).willReturn("artifact"); + given(snapshotArtifact.getBaseVersion()).willReturn("1.0-SNAPSHOT"); + given(snapshotArtifact.getFile()).willReturn(new File("a")); + given(snapshotArtifact.getArtifactHandler()).willReturn(this.artifactHandler); + this.artifacts = Collections.singleton(snapshotArtifact); + new ArtifactsLibraries(this.artifacts, Collections.emptyList(), null, mock(Log.class)) + .doWithLibraries((library) -> { + assertThat(library.isIncluded()).isTrue(); + assertThat(library.isLocal()).isFalse(); + assertThat(library.getCoordinates().getVersion()).isEqualTo("1.0-SNAPSHOT"); + }); + } + + @Test + void artifactForLocalProjectProducesLocalLibrary() throws IOException { + Artifact artifact = mock(Artifact.class); + given(artifact.getScope()).willReturn("compile"); + given(artifact.getArtifactId()).willReturn("artifact"); + given(artifact.getBaseVersion()).willReturn("1.0-SNAPSHOT"); + given(artifact.getFile()).willReturn(new File("a")); + given(artifact.getArtifactHandler()).willReturn(this.artifactHandler); + MavenProject mavenProject = mock(MavenProject.class); + given(mavenProject.getArtifact()).willReturn(artifact); + this.artifacts = Collections.singleton(artifact); + new ArtifactsLibraries(this.artifacts, Collections.singleton(mavenProject), null, mock(Log.class)) + .doWithLibraries((library) -> assertThat(library.isLocal()).isTrue()); + } + + @Test + void attachedArtifactForLocalProjectProducesLocalLibrary() throws IOException { + MavenProject mavenProject = mock(MavenProject.class); + Artifact artifact = mock(Artifact.class); + given(mavenProject.getArtifact()).willReturn(artifact); + Artifact attachedArtifact = mock(Artifact.class); + given(attachedArtifact.getScope()).willReturn("compile"); + given(attachedArtifact.getArtifactId()).willReturn("attached-artifact"); + given(attachedArtifact.getBaseVersion()).willReturn("1.0-SNAPSHOT"); + given(attachedArtifact.getFile()).willReturn(new File("a")); + given(attachedArtifact.getArtifactHandler()).willReturn(this.artifactHandler); + given(mavenProject.getAttachedArtifacts()).willReturn(Collections.singletonList(attachedArtifact)); + this.artifacts = Collections.singleton(attachedArtifact); + new ArtifactsLibraries(this.artifacts, Collections.singleton(mavenProject), null, mock(Log.class)) + .doWithLibraries((library) -> assertThat(library.isLocal()).isTrue()); + } + + @Test + void nonIncludedArtifact() throws IOException { + Artifact artifact = mock(Artifact.class); + given(artifact.getScope()).willReturn("compile"); + given(artifact.getArtifactId()).willReturn("artifact"); + given(artifact.getBaseVersion()).willReturn("1.0-SNAPSHOT"); + given(artifact.getFile()).willReturn(new File("a")); + given(artifact.getArtifactHandler()).willReturn(this.artifactHandler); + MavenProject mavenProject = mock(MavenProject.class); + given(mavenProject.getArtifact()).willReturn(artifact); + this.artifacts = Collections.singleton(artifact); + new ArtifactsLibraries(this.artifacts, Collections.emptySet(), Collections.singleton(mavenProject), null, + mock(Log.class)).doWithLibraries((library) -> assertThat(library.isIncluded()).isFalse()); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/CustomLayersProviderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/CustomLayersProviderTests.java new file mode 100644 index 000000000000..5e5bceaea24f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/CustomLayersProviderTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.maven; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.xml.sax.InputSource; + +import org.springframework.boot.loader.tools.Library; +import org.springframework.boot.loader.tools.LibraryCoordinates; +import org.springframework.boot.loader.tools.layer.CustomLayers; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link CustomLayersProvider}. + * + * @author Madhura Bhave + * @author Scott Frederick + */ +class CustomLayersProviderTests { + + private CustomLayersProvider customLayersProvider; + + @BeforeEach + void setup() { + this.customLayersProvider = new CustomLayersProvider(); + } + + @Test + void getLayerResolverWhenDocumentValid() throws Exception { + CustomLayers layers = this.customLayersProvider.getLayers(getDocument("layers.xml")); + assertThat(layers).extracting("name").containsExactly("my-deps", "my-dependencies-name", + "snapshot-dependencies", "my-resources", "configuration", "application"); + Library snapshot = mockLibrary("test-SNAPSHOT.jar", "org.foo", "1.0.0-SNAPSHOT"); + Library groupId = mockLibrary("my-library", "com.acme", null); + Library otherDependency = mockLibrary("other-library", "org.foo", null); + Library localSnapshotDependency = mockLibrary("local-library", "org.foo", "1.0-SNAPSHOT"); + given(localSnapshotDependency.isLocal()).willReturn(true); + assertThat(layers.getLayer(snapshot).toString()).isEqualTo("snapshot-dependencies"); + assertThat(layers.getLayer(groupId).toString()).isEqualTo("my-deps"); + assertThat(layers.getLayer(otherDependency).toString()).isEqualTo("my-dependencies-name"); + assertThat(layers.getLayer(localSnapshotDependency).toString()).isEqualTo("application"); + assertThat(layers.getLayer("META-INF/resources/test.css").toString()).isEqualTo("my-resources"); + assertThat(layers.getLayer("application.yml").toString()).isEqualTo("configuration"); + assertThat(layers.getLayer("test").toString()).isEqualTo("application"); + } + + private Library mockLibrary(String name, String groupId, String version) { + Library library = mock(Library.class); + given(library.getName()).willReturn(name); + given(library.getCoordinates()).willReturn(LibraryCoordinates.of(groupId, null, version)); + return library; + } + + @Test + void getLayerResolverWhenDocumentContainsLibraryLayerWithNoFilters() throws Exception { + CustomLayers layers = this.customLayersProvider.getLayers(getDocument("dependencies-layer-no-filter.xml")); + Library library = mockLibrary("my-library", "com.acme", null); + assertThat(layers.getLayer(library).toString()).isEqualTo("my-deps"); + assertThatIllegalStateException().isThrownBy(() -> layers.getLayer("application.yml")) + .withMessageContaining("match any layer"); + } + + @Test + void getLayerResolverWhenDocumentContainsResourceLayerWithNoFilters() throws Exception { + CustomLayers layers = this.customLayersProvider.getLayers(getDocument("application-layer-no-filter.xml")); + Library library = mockLibrary("my-library", "com.acme", null); + assertThat(layers.getLayer("application.yml").toString()).isEqualTo("my-layer"); + assertThatIllegalStateException().isThrownBy(() -> layers.getLayer(library)) + .withMessageContaining("match any layer"); + } + + private Document getDocument(String resourceName) throws Exception { + ClassPathResource resource = new ClassPathResource(resourceName); + InputSource inputSource = new InputSource(resource.getInputStream()); + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder documentBuilder = factory.newDocumentBuilder(); + return documentBuilder.parse(inputSource); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/DependencyFilterMojoTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/DependencyFilterMojoTests.java index d09eb1728478..d32b34e1c8bf 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/DependencyFilterMojoTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/DependencyFilterMojoTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,17 +16,25 @@ package org.springframework.boot.maven; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Path; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import java.util.UUID; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; import org.apache.maven.artifact.Artifact; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.shared.artifact.filter.collection.ArtifactsFilter; import org.apache.maven.shared.artifact.filter.collection.ScopeFilter; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @@ -39,6 +47,9 @@ */ class DependencyFilterMojoTests { + @TempDir + static Path temp; + @Test void filterDependencies() throws MojoExecutionException { TestableDependencyFilterMojo mojo = new TestableDependencyFilterMojo(Collections.emptyList(), "com.foo"); @@ -97,20 +108,50 @@ void filterExcludeKeepOrder() throws MojoExecutionException { assertThat(artifacts).containsExactly(one, three, four); } + @Test + void excludeByJarType() throws MojoExecutionException { + TestableDependencyFilterMojo mojo = new TestableDependencyFilterMojo(Collections.emptyList(), ""); + Artifact one = createArtifact("com.foo", "one", null, "dependencies-starter"); + Artifact two = createArtifact("com.bar", "two"); + Set artifacts = mojo.filterDependencies(one, two); + assertThat(artifacts).containsExactly(two); + } + private static Artifact createArtifact(String groupId, String artifactId) { return createArtifact(groupId, artifactId, null); } private static Artifact createArtifact(String groupId, String artifactId, String scope) { + return createArtifact(groupId, artifactId, scope, null); + } + + private static Artifact createArtifact(String groupId, String artifactId, String scope, String jarType) { Artifact a = mock(Artifact.class); given(a.getGroupId()).willReturn(groupId); given(a.getArtifactId()).willReturn(artifactId); if (scope != null) { given(a.getScope()).willReturn(scope); } + given(a.getFile()).willReturn(createArtifactFile(jarType)); return a; } + private static File createArtifactFile(String jarType) { + Path jarPath = temp.resolve(UUID.randomUUID().toString() + ".jar"); + Manifest manifest = new Manifest(); + manifest.getMainAttributes().putValue("Manifest-Version", "1.0"); + if (jarType != null) { + manifest.getMainAttributes().putValue("Spring-Boot-Jar-Type", jarType); + } + try { + new JarOutputStream(new FileOutputStream(jarPath.toFile()), manifest).close(); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + return jarPath.toFile(); + } + private static final class TestableDependencyFilterMojo extends AbstractDependencyFilterMojo { private final ArtifactsFilter[] additionalFilters; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/DockerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/DockerTests.java new file mode 100644 index 000000000000..eb91cdc90140 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/DockerTests.java @@ -0,0 +1,123 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.maven; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerHost; +import org.springframework.util.Base64Utils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link Docker}. + * + * @author Wei Jiang + * @author Scott Frederick + */ +class DockerTests { + + @Test + void asDockerConfigurationWithDefaults() { + Docker docker = new Docker(); + assertThat(docker.asDockerConfiguration().getHost()).isNull(); + assertThat(docker.asDockerConfiguration().getBuilderRegistryAuthentication()).isNull(); + assertThat(docker.asDockerConfiguration().getPublishRegistryAuthentication()).isNull(); + } + + @Test + void asDockerConfigurationWithHostConfiguration() { + Docker docker = new Docker(); + docker.setHost("docker.example.com"); + docker.setTlsVerify(true); + docker.setCertPath("/tmp/ca-cert"); + DockerConfiguration dockerConfiguration = docker.asDockerConfiguration(); + DockerHost host = dockerConfiguration.getHost(); + assertThat(host.getAddress()).isEqualTo("docker.example.com"); + assertThat(host.isSecure()).isEqualTo(true); + assertThat(host.getCertificatePath()).isEqualTo("/tmp/ca-cert"); + assertThat(docker.asDockerConfiguration().getBuilderRegistryAuthentication()).isNull(); + assertThat(docker.asDockerConfiguration().getPublishRegistryAuthentication()).isNull(); + } + + @Test + void asDockerConfigurationWithUserAuth() { + Docker docker = new Docker(); + docker.setBuilderRegistry( + new Docker.DockerRegistry("user1", "secret1", "https://docker1.example.com", "docker1@example.com")); + docker.setPublishRegistry( + new Docker.DockerRegistry("user2", "secret2", "https://docker2.example.com", "docker2@example.com")); + DockerConfiguration dockerConfiguration = docker.asDockerConfiguration(); + assertThat(decoded(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())) + .contains("\"username\" : \"user1\"").contains("\"password\" : \"secret1\"") + .contains("\"email\" : \"docker1@example.com\"") + .contains("\"serveraddress\" : \"https://docker1.example.com\""); + assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader())) + .contains("\"username\" : \"user2\"").contains("\"password\" : \"secret2\"") + .contains("\"email\" : \"docker2@example.com\"") + .contains("\"serveraddress\" : \"https://docker2.example.com\""); + } + + @Test + void asDockerConfigurationWithIncompleteBuilderUserAuthFails() { + Docker docker = new Docker(); + docker.setBuilderRegistry( + new Docker.DockerRegistry("user", null, "https://docker.example.com", "docker@example.com")); + assertThatIllegalArgumentException().isThrownBy(docker::asDockerConfiguration) + .withMessageContaining("Invalid Docker builder registry configuration"); + } + + @Test + void asDockerConfigurationWithIncompletePublishUserAuthFails() { + Docker docker = new Docker(); + docker.setPublishRegistry( + new Docker.DockerRegistry("user", null, "https://docker.example.com", "docker@example.com")); + assertThatIllegalArgumentException().isThrownBy(docker::asDockerConfiguration) + .withMessageContaining("Invalid Docker publish registry configuration"); + } + + @Test + void asDockerConfigurationWithTokenAuth() { + Docker docker = new Docker(); + docker.setBuilderRegistry(new Docker.DockerRegistry("token1")); + docker.setPublishRegistry(new Docker.DockerRegistry("token2")); + DockerConfiguration dockerConfiguration = docker.asDockerConfiguration(); + assertThat(decoded(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())) + .contains("\"identitytoken\" : \"token1\""); + assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader())) + .contains("\"identitytoken\" : \"token2\""); + } + + @Test + void asDockerConfigurationWithUserAndTokenAuthFails() { + Docker.DockerRegistry dockerRegistry = new Docker.DockerRegistry(); + dockerRegistry.setUsername("user"); + dockerRegistry.setPassword("secret"); + dockerRegistry.setToken("token"); + Docker docker = new Docker(); + docker.setBuilderRegistry(dockerRegistry); + assertThatIllegalArgumentException().isThrownBy(docker::asDockerConfiguration) + .withMessageContaining("Invalid Docker builder registry configuration"); + } + + String decoded(String value) { + return new String(Base64Utils.decodeFromString(value)); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java new file mode 100644 index 000000000000..15ee4db41051 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java @@ -0,0 +1,158 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.maven; + +import java.util.Arrays; +import java.util.Collections; +import java.util.function.Function; + +import org.apache.maven.artifact.Artifact; +import org.apache.maven.artifact.DefaultArtifact; +import org.apache.maven.artifact.handler.DefaultArtifactHandler; +import org.apache.maven.artifact.versioning.VersionRange; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.build.BuildRequest; +import org.springframework.boot.buildpack.platform.build.BuildpackReference; +import org.springframework.boot.buildpack.platform.build.PullPolicy; +import org.springframework.boot.buildpack.platform.docker.type.Binding; +import org.springframework.boot.buildpack.platform.io.Owner; +import org.springframework.boot.buildpack.platform.io.TarArchive; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +/** + * Tests for {@link Image}. + * + * @author Phillip Webb + * @author Scott Frederick + */ +class ImageTests { + + @Test + void getBuildRequestWhenNameIsNullDeducesName() { + BuildRequest request = new Image().getBuildRequest(createArtifact(), mockApplicationContent()); + assertThat(request.getName().toString()).isEqualTo("docker.io/library/my-app:0.0.1-SNAPSHOT"); + } + + @Test + void getBuildRequestWhenNameIsSetUsesName() { + Image image = new Image(); + image.name = "demo"; + BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); + assertThat(request.getName().toString()).isEqualTo("docker.io/library/demo:latest"); + } + + @Test + void getBuildRequestWhenNoCustomizationsUsesDefaults() { + BuildRequest request = new Image().getBuildRequest(createArtifact(), mockApplicationContent()); + assertThat(request.getName().toString()).isEqualTo("docker.io/library/my-app:0.0.1-SNAPSHOT"); + assertThat(request.getBuilder().toString()).contains("paketobuildpacks/builder"); + assertThat(request.getRunImage()).isNull(); + assertThat(request.getEnv()).isEmpty(); + assertThat(request.isCleanCache()).isFalse(); + assertThat(request.isVerboseLogging()).isFalse(); + assertThat(request.getPullPolicy()).isEqualTo(PullPolicy.ALWAYS); + assertThat(request.getBuildpacks()).isEmpty(); + assertThat(request.getBindings()).isEmpty(); + } + + @Test + void getBuildRequestWhenHasBuilderUsesBuilder() { + Image image = new Image(); + image.builder = "springboot/builder:2.2.x"; + BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); + assertThat(request.getBuilder().toString()).isEqualTo("docker.io/springboot/builder:2.2.x"); + } + + @Test + void getBuildRequestWhenHasRunImageUsesRunImage() { + Image image = new Image(); + image.runImage = "springboot/run:latest"; + BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); + assertThat(request.getRunImage().toString()).isEqualTo("docker.io/springboot/run:latest"); + } + + @Test + void getBuildRequestWhenHasEnvUsesEnv() { + Image image = new Image(); + image.env = Collections.singletonMap("test", "test"); + BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); + assertThat(request.getEnv()).containsExactly(entry("test", "test")); + } + + @Test + void getBuildRequestWhenHasCleanCacheUsesCleanCache() { + Image image = new Image(); + image.cleanCache = true; + BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); + assertThat(request.isCleanCache()).isTrue(); + } + + @Test + void getBuildRequestWhenHasVerboseLoggingUsesVerboseLogging() { + Image image = new Image(); + image.verboseLogging = true; + BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); + assertThat(request.isVerboseLogging()).isTrue(); + } + + @Test + void getBuildRequestWhenHasPullPolicyUsesPullPolicy() { + Image image = new Image(); + image.setPullPolicy(PullPolicy.NEVER); + BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); + assertThat(request.getPullPolicy()).isEqualTo(PullPolicy.NEVER); + } + + @Test + void getBuildRequestWhenHasPublishUsesPublish() { + Image image = new Image(); + image.publish = true; + BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); + assertThat(request.isPublish()).isTrue(); + } + + @Test + void getBuildRequestWhenHasBuildpacksUsesBuildpacks() { + Image image = new Image(); + image.buildpacks = Arrays.asList("example/buildpack1@0.0.1", "example/buildpack2@0.0.2"); + BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); + assertThat(request.getBuildpacks()).containsExactly(BuildpackReference.of("example/buildpack1@0.0.1"), + BuildpackReference.of("example/buildpack2@0.0.2")); + } + + @Test + void getBuildRequestWhenHasBindingsUsesBindings() { + Image image = new Image(); + image.bindings = Arrays.asList("host-src:container-dest:ro", "volume-name:container-dest:rw"); + BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); + assertThat(request.getBindings()).containsExactly(Binding.of("host-src:container-dest:ro"), + Binding.of("volume-name:container-dest:rw")); + } + + private Artifact createArtifact() { + return new DefaultArtifact("com.example", "my-app", VersionRange.createFromVersion("0.0.1-SNAPSHOT"), "compile", + "jar", null, new DefaultArtifactHandler()); + } + + private Function mockApplicationContent() { + return (owner) -> null; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/JarTypeFilterTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/JarTypeFilterTests.java new file mode 100644 index 000000000000..fd40ec8a4dbc --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/JarTypeFilterTests.java @@ -0,0 +1,101 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.maven; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; + +import org.apache.maven.artifact.Artifact; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link JarTypeFilter}. + * + * @author Andy Wilkinson + */ +class JarTypeFilterTests { + + @TempDir + Path temp; + + @Test + void whenArtifactHasNoJarTypeThenItIsIncluded() { + assertThat(new JarTypeFilter().filter(createArtifact(null))).isFalse(); + } + + @Test + void whenArtifactHasJarTypeThatIsNotExcludedThenItIsIncluded() { + assertThat(new JarTypeFilter().filter(createArtifact("something-included"))).isFalse(); + } + + @Test + void whenArtifactHasDependenciesStarterJarTypeThenItIsExcluded() { + assertThat(new JarTypeFilter().filter(createArtifact("dependencies-starter"))).isTrue(); + } + + @Test + void whenArtifactHasAnnotationProcessorJarTypeThenItIsExcluded() { + assertThat(new JarTypeFilter().filter(createArtifact("annotation-processor"))).isTrue(); + } + + @Test + void whenArtifactHasNoManifestFileThenItIsIncluded() { + assertThat(new JarTypeFilter().filter(createArtifactWithNoManifest())).isFalse(); + } + + private Artifact createArtifact(String springBootJarType) { + Path jarPath = this.temp.resolve("test.jar"); + Manifest manifest = new Manifest(); + manifest.getMainAttributes().putValue("Manifest-Version", "1.0"); + if (springBootJarType != null) { + manifest.getMainAttributes().putValue("Spring-Boot-Jar-Type", springBootJarType); + } + try { + new JarOutputStream(new FileOutputStream(jarPath.toFile()), manifest).close(); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + return mockArtifact(jarPath); + } + + private Artifact createArtifactWithNoManifest() { + Path jarPath = this.temp.resolve("test.jar"); + try { + new JarOutputStream(new FileOutputStream(jarPath.toFile())).close(); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + return mockArtifact(jarPath); + } + + private Artifact mockArtifact(Path jarPath) { + Artifact artifact = mock(Artifact.class); + given(artifact.getFile()).willReturn(jarPath.toFile()); + return artifact; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/JavaCompilerPluginConfigurationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/JavaCompilerPluginConfigurationTests.java new file mode 100644 index 000000000000..0bcd407642d3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/JavaCompilerPluginConfigurationTests.java @@ -0,0 +1,94 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.maven; + +import java.io.IOException; +import java.io.StringReader; +import java.util.Arrays; +import java.util.Properties; + +import org.apache.maven.model.Plugin; +import org.apache.maven.project.MavenProject; +import org.codehaus.plexus.util.xml.Xpp3Dom; +import org.codehaus.plexus.util.xml.Xpp3DomBuilder; +import org.codehaus.plexus.util.xml.pull.XmlPullParserException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link JavaCompilerPluginConfiguration}. + * + * @author Scott Frederick + */ +class JavaCompilerPluginConfigurationTests { + + private MavenProject project; + + private Plugin plugin; + + @BeforeEach + void setUp() { + this.project = mock(MavenProject.class); + this.plugin = mock(Plugin.class); + given(this.project.getPlugin(anyString())).willReturn(this.plugin); + } + + @Test + void versionsAreNullWithNoConfiguration() { + given(this.plugin.getConfiguration()).willReturn(null); + given(this.project.getProperties()).willReturn(new Properties()); + JavaCompilerPluginConfiguration configuration = new JavaCompilerPluginConfiguration(this.project); + assertThat(configuration.getSourceMajorVersion()).isNull(); + assertThat(configuration.getTargetMajorVersion()).isNull(); + } + + @Test + void versionsAreReturnedFromConfiguration() throws IOException, XmlPullParserException { + Xpp3Dom dom = buildConfigurationDom("1.9", "11"); + given(this.plugin.getConfiguration()).willReturn(dom); + Properties properties = new Properties(); + properties.setProperty("maven.compiler.source", "1.8"); + properties.setProperty("maven.compiler.target", "10"); + given(this.project.getProperties()).willReturn(properties); + JavaCompilerPluginConfiguration configuration = new JavaCompilerPluginConfiguration(this.project); + assertThat(configuration.getSourceMajorVersion()).isEqualTo("9"); + assertThat(configuration.getTargetMajorVersion()).isEqualTo("11"); + } + + @Test + void versionsAreReturnedFromProperties() { + given(this.plugin.getConfiguration()).willReturn(null); + Properties properties = new Properties(); + properties.setProperty("maven.compiler.source", "1.8"); + properties.setProperty("maven.compiler.target", "11"); + given(this.project.getProperties()).willReturn(properties); + JavaCompilerPluginConfiguration configuration = new JavaCompilerPluginConfiguration(this.project); + assertThat(configuration.getSourceMajorVersion()).isEqualTo("8"); + assertThat(configuration.getTargetMajorVersion()).isEqualTo("11"); + } + + private Xpp3Dom buildConfigurationDom(String... properties) throws IOException, XmlPullParserException { + return Xpp3DomBuilder + .build(new StringReader("" + Arrays.toString(properties) + "")); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/PropertiesMergingResourceTransformerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/PropertiesMergingResourceTransformerTests.java index f221c5fad522..ad0dc6831c32 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/PropertiesMergingResourceTransformerTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/PropertiesMergingResourceTransformerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,10 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; import java.util.jar.JarOutputStream; import org.junit.jupiter.api.Test; @@ -36,28 +40,38 @@ class PropertiesMergingResourceTransformerTests { @Test void testProcess() throws Exception { assertThat(this.transformer.hasTransformedResource()).isFalse(); - this.transformer.processResource("foo", new ByteArrayInputStream("foo=bar".getBytes()), null); + this.transformer.processResource("foo", new ByteArrayInputStream("foo=bar".getBytes()), null, 0); assertThat(this.transformer.hasTransformedResource()).isTrue(); } @Test void testMerge() throws Exception { - this.transformer.processResource("foo", new ByteArrayInputStream("foo=bar".getBytes()), null); - this.transformer.processResource("bar", new ByteArrayInputStream("foo=spam".getBytes()), null); + this.transformer.processResource("foo", new ByteArrayInputStream("foo=bar".getBytes()), null, 0); + this.transformer.processResource("bar", new ByteArrayInputStream("foo=spam".getBytes()), null, 0); assertThat(this.transformer.getData().getProperty("foo")).isEqualTo("bar,spam"); } @Test void testOutput() throws Exception { this.transformer.setResource("foo"); - this.transformer.processResource("foo", new ByteArrayInputStream("foo=bar".getBytes()), null); + long time = 1592911068000L; + this.transformer.processResource("foo", new ByteArrayInputStream("foo=bar".getBytes()), null, time); ByteArrayOutputStream out = new ByteArrayOutputStream(); JarOutputStream os = new JarOutputStream(out); this.transformer.modifyOutputStream(os); os.flush(); os.close(); - assertThat(out.toByteArray()).isNotNull(); - assertThat(out.toByteArray().length > 0).isTrue(); + byte[] bytes = out.toByteArray(); + assertThat(bytes).hasSizeGreaterThan(0); + List entries = new ArrayList<>(); + try (JarInputStream is = new JarInputStream(new ByteArrayInputStream(bytes))) { + JarEntry entry; + while ((entry = is.getNextJarEntry()) != null) { + entries.add(entry); + } + } + assertThat(entries).hasSize(1); + assertThat(entries.get(0).getTime()).isEqualTo(time); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/Verify.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/Verify.java deleted file mode 100644 index c69a060bb434..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/Verify.java +++ /dev/null @@ -1,308 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.maven; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.Map; -import java.util.Properties; -import java.util.jar.Manifest; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -import org.springframework.core.io.FileSystemResource; -import org.springframework.core.io.support.PropertiesLoaderUtils; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.contentOf; - -/** - * Verification utility for use with maven-invoker-plugin verification scripts. - * - * @author Phillip Webb - * @author Andy Wilkinson - * @author Stephane Nicoll - */ -public final class Verify { - - public static final String SAMPLE_APP = "org.test.SampleApplication"; - - private Verify() { - } - - public static void verifyJar(File file) throws Exception { - new JarArchiveVerification(file, SAMPLE_APP).verify(); - } - - public static void verifyJar(File file, String main, String... scriptContents) throws Exception { - verifyJar(file, main, true, scriptContents); - } - - public static void verifyJar(File file, String main, boolean executable, String... scriptContents) - throws Exception { - new JarArchiveVerification(file, main).verify(executable, scriptContents); - } - - public static void verifyWar(File file) throws Exception { - new WarArchiveVerification(file).verify(); - } - - public static void verifyZip(File file) throws Exception { - new ZipArchiveVerification(file).verify(); - } - - public static void verifyModule(File file) throws Exception { - new ModuleArchiveVerification(file).verify(); - } - - public static Properties verifyBuildInfo(File file, String group, String artifact, String name, String version) - throws IOException { - FileSystemResource resource = new FileSystemResource(file); - Properties properties = PropertiesLoaderUtils.loadProperties(resource); - assertThat(properties.get("build.group")).isEqualTo(group); - assertThat(properties.get("build.artifact")).isEqualTo(artifact); - assertThat(properties.get("build.name")).isEqualTo(name); - assertThat(properties.get("build.version")).isEqualTo(version); - return properties; - } - - public static class ArchiveVerifier { - - private final ZipFile zipFile; - - private final Map content; - - public ArchiveVerifier(ZipFile zipFile) { - this.zipFile = zipFile; - Enumeration entries = zipFile.entries(); - this.content = new HashMap<>(); - while (entries.hasMoreElements()) { - ZipEntry zipEntry = entries.nextElement(); - this.content.put(zipEntry.getName(), zipEntry); - } - } - - public void assertHasEntryNameStartingWith(String entry) { - for (String name : this.content.keySet()) { - if (name.startsWith(entry)) { - return; - } - } - throw new IllegalStateException("Expected entry starting with " + entry); - } - - public void assertHasNoEntryNameStartingWith(String entry) { - for (String name : this.content.keySet()) { - if (name.startsWith(entry)) { - throw new IllegalStateException("Entry starting with " + entry + " should not have been found"); - } - } - } - - public void assertHasNonUnpackEntry(String entryName) { - assertThat(hasNonUnpackEntry(entryName)).as("Entry starting with " + entryName + " was an UNPACK entry") - .isTrue(); - } - - public void assertHasUnpackEntry(String entryName) { - assertThat(hasUnpackEntry(entryName)).as("Entry starting with " + entryName + " was not an UNPACK entry") - .isTrue(); - } - - private boolean hasNonUnpackEntry(String entryName) { - return !hasUnpackEntry(entryName); - } - - private boolean hasUnpackEntry(String entryName) { - String comment = getEntryStartingWith(entryName).getComment(); - return comment != null && comment.startsWith("UNPACK:"); - } - - private ZipEntry getEntryStartingWith(String entryName) { - return this.content.entrySet().stream().filter((entry) -> entry.getKey().startsWith(entryName)) - .map(Map.Entry::getValue).findFirst() - .orElseThrow(() -> new IllegalStateException("Unable to find entry starting with " + entryName)); - } - - public boolean hasEntry(String entry) { - return this.content.containsKey(entry); - } - - public ZipEntry getEntry(String entry) { - return this.content.get(entry); - } - - public InputStream getEntryContent(String entry) throws IOException { - ZipEntry zipEntry = getEntry(entry); - if (zipEntry == null) { - throw new IllegalArgumentException("No entry with name [" + entry + "]"); - } - return this.zipFile.getInputStream(zipEntry); - } - - } - - public abstract static class AbstractArchiveVerification { - - private final File file; - - AbstractArchiveVerification(File file) { - this.file = file; - } - - public void verify() throws Exception { - verify(true); - } - - public void verify(boolean executable, String... scriptContents) throws Exception { - assertThat(this.file).exists().isFile(); - - if (scriptContents.length > 0 && executable) { - String contents = contentOf(this.file); - contents = contents.substring(0, contents.indexOf(new String(new byte[] { 0x50, 0x4b, 0x03, 0x04 }))); - for (String content : scriptContents) { - assertThat(contents).contains(content); - } - } - - if (!executable) { - String contents = contentOf(this.file); - assertThat(contents).as("Is executable").startsWith(new String(new byte[] { 0x50, 0x4b, 0x03, 0x04 })); - } - - try (ZipFile zipFile = new ZipFile(this.file)) { - ArchiveVerifier verifier = new ArchiveVerifier(zipFile); - verifyZipEntries(verifier); - } - } - - protected void verifyZipEntries(ArchiveVerifier verifier) throws Exception { - verifyManifest(verifier); - } - - private void verifyManifest(ArchiveVerifier verifier) throws Exception { - Manifest manifest = new Manifest(verifier.getEntryContent("META-INF/MANIFEST.MF")); - verifyManifest(manifest); - } - - protected abstract void verifyManifest(Manifest manifest) throws Exception; - - } - - public static class JarArchiveVerification extends AbstractArchiveVerification { - - private final String main; - - public JarArchiveVerification(File file, String main) { - super(file); - this.main = main; - } - - @Override - protected void verifyZipEntries(ArchiveVerifier verifier) throws Exception { - super.verifyZipEntries(verifier); - verifier.assertHasEntryNameStartingWith("BOOT-INF/lib/spring-context"); - verifier.assertHasEntryNameStartingWith("BOOT-INF/lib/spring-core"); - verifier.assertHasEntryNameStartingWith("BOOT-INF/lib/jakarta.servlet-api-4"); - assertThat(verifier.hasEntry("org/springframework/boot/loader/JarLauncher.class")) - .as("Unpacked launcher classes").isTrue(); - assertThat(verifier.hasEntry("BOOT-INF/classes/org/test/SampleApplication.class")).as("Own classes") - .isTrue(); - } - - @Override - protected void verifyManifest(Manifest manifest) throws Exception { - assertThat(manifest.getMainAttributes().getValue("Main-Class")) - .isEqualTo("org.springframework.boot.loader.JarLauncher"); - assertThat(manifest.getMainAttributes().getValue("Start-Class")).isEqualTo(this.main); - assertThat(manifest.getMainAttributes().getValue("Not-Used")).isEqualTo("Foo"); - } - - } - - public static class WarArchiveVerification extends AbstractArchiveVerification { - - public WarArchiveVerification(File file) { - super(file); - } - - @Override - protected void verifyZipEntries(ArchiveVerifier verifier) throws Exception { - super.verifyZipEntries(verifier); - verifier.assertHasEntryNameStartingWith("WEB-INF/lib/spring-context"); - verifier.assertHasEntryNameStartingWith("WEB-INF/lib/spring-core"); - verifier.assertHasEntryNameStartingWith("WEB-INF/lib-provided/jakarta.servlet-api-4"); - assertThat(verifier.hasEntry("org/springframework/boot/loader/JarLauncher.class")) - .as("Unpacked launcher classes").isTrue(); - assertThat(verifier.hasEntry("WEB-INF/classes/org/test/SampleApplication.class")).as("Own classes") - .isTrue(); - assertThat(verifier.hasEntry("index.html")).as("Web content").isTrue(); - } - - @Override - protected void verifyManifest(Manifest manifest) throws Exception { - assertThat(manifest.getMainAttributes().getValue("Main-Class")) - .isEqualTo("org.springframework.boot.loader.WarLauncher"); - assertThat(manifest.getMainAttributes().getValue("Start-Class")).isEqualTo("org.test.SampleApplication"); - assertThat(manifest.getMainAttributes().getValue("Not-Used")).isEqualTo("Foo"); - } - - } - - private static class ZipArchiveVerification extends AbstractArchiveVerification { - - ZipArchiveVerification(File file) { - super(file); - } - - @Override - protected void verifyManifest(Manifest manifest) throws Exception { - assertThat(manifest.getMainAttributes().getValue("Main-Class")) - .isEqualTo("org.springframework.boot.loader.PropertiesLauncher"); - assertThat(manifest.getMainAttributes().getValue("Start-Class")).isEqualTo("org.test.SampleApplication"); - assertThat(manifest.getMainAttributes().getValue("Not-Used")).isEqualTo("Foo"); - } - - } - - private static class ModuleArchiveVerification extends AbstractArchiveVerification { - - ModuleArchiveVerification(File file) { - super(file); - } - - @Override - protected void verifyZipEntries(ArchiveVerifier verifier) throws Exception { - super.verifyZipEntries(verifier); - verifier.assertHasEntryNameStartingWith("lib/spring-context"); - verifier.assertHasEntryNameStartingWith("lib/spring-core"); - verifier.assertHasNoEntryNameStartingWith("lib/jakarta.servlet-api"); - assertThat(verifier.hasEntry("org/springframework/boot/loader/JarLauncher.class")) - .as("Unpacked launcher classes").isFalse(); - assertThat(verifier.hasEntry("org/test/SampleModule.class")).as("Own classes").isTrue(); - } - - @Override - protected void verifyManifest(Manifest manifest) throws Exception { - } - - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/application-layer-no-filter.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/application-layer-no-filter.xml new file mode 100644 index 000000000000..7dafca31411a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/application-layer-no-filter.xml @@ -0,0 +1,11 @@ + + + + + + my-layer + + \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/dependencies-layer-no-filter.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/dependencies-layer-no-filter.xml new file mode 100644 index 000000000000..a4040cd0b8a5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/dependencies-layer-no-filter.xml @@ -0,0 +1,11 @@ + + + + + + my-deps + + \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/layers.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/layers.xml new file mode 100644 index 000000000000..a3908ccbb0a1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/layers.xml @@ -0,0 +1,36 @@ + + + + META-INF/resources/** + *.properties + + + **/application*.* + + + + + + *:*:*-SNAPSHOT + + + + + + + com.acme:* + + + + + my-deps + my-dependencies-name + snapshot-dependencies + my-resources + configuration + application + + \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/resource-layer-no-filter.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/resource-layer-no-filter.xml new file mode 100644 index 000000000000..3640b70b6d5f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/resource-layer-no-filter.xml @@ -0,0 +1,11 @@ + + + + + + my-layer + + \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-test-support/build.gradle new file mode 100644 index 000000000000..a185547f10d6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/build.gradle @@ -0,0 +1,47 @@ +plugins { + id "java-library" + id "org.springframework.boot.conventions" +} + +description = "Spring Boot Testing Support" + +dependencies { + api(platform(project(path: ":spring-boot-project:spring-boot-parent"))) + + compileOnly("com.datastax.oss:java-driver-core") { + exclude(group: "org.slf4j", module: "jcl-over-slf4j") + } + compileOnly("jakarta.servlet:jakarta.servlet-api") + compileOnly("junit:junit") + compileOnly("org.elasticsearch:elasticsearch") + compileOnly("org.junit.jupiter:junit-jupiter") + compileOnly("org.junit.platform:junit-platform-engine") + compileOnly("org.junit.platform:junit-platform-launcher") + compileOnly("org.mockito:mockito-core") + compileOnly("org.springframework:spring-context") + compileOnly("org.springframework.data:spring-data-redis") + compileOnly("org.testcontainers:cassandra") + compileOnly("org.testcontainers:testcontainers") + + implementation("jakarta.inject:jakarta.inject-api") + implementation("org.apache.maven.resolver:maven-resolver-connector-basic") + implementation("org.apache.maven.resolver:maven-resolver-impl") + implementation("org.apache.maven:maven-resolver-provider") { + exclude(group: "javax.inject", module: "javax.inject") + } + implementation("org.apache.maven.resolver:maven-resolver-transport-http") { + exclude group: "org.slf4j", module: "jcl-over-slf4j" + } + implementation("org.assertj:assertj-core") + implementation("org.hamcrest:hamcrest-core") + implementation("org.hamcrest:hamcrest-library") + implementation("org.springframework:spring-core") + implementation("org.springframework:spring-test") + + testImplementation("jakarta.servlet:jakarta.servlet-api") + testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.springframework:spring-context") + + testRuntimeOnly("org.hibernate.validator:hibernate-validator") + testRuntimeOnly("org.mockito:mockito-core") +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-test-support/pom.xml deleted file mode 100644 index 580c05156d1b..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/pom.xml +++ /dev/null @@ -1,149 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-tools - ${revision} - - spring-boot-test-support - Spring Boot Testing Support - Spring Boot Testing Support - - ${basedir}/../../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - - org.apache.maven.resolver - maven-resolver-connector-basic - - - org.apache.maven.resolver - maven-resolver-impl - - - org.apache.maven - maven-resolver-provider - - - com.google.guava - guava - - - - - org.apache.maven.resolver - maven-resolver-transport-http - - - jcl-over-slf4j - org.slf4j - - - - - org.springframework - spring-core - - - org.assertj - assertj-core - - - - com.datastax.cassandra - cassandra-driver-core - true - - - jakarta.servlet - jakarta.servlet-api - true - - - org.mockito - mockito-core - true - - - org.neo4j - neo4j-ogm-core - true - - - org.springframework.data - spring-data-redis - true - - - org.springframework - spring-context - true - - - org.testcontainers - testcontainers - true - - - javax.activation - javax.activation-api - - - javax.annotation - javax.annotation-api - - - javax.xml.bind - jaxb-api - - - - - org.testcontainers - junit-jupiter - true - - - - junit - junit - provided - - - org.hamcrest - hamcrest-core - - - - - org.junit.jupiter - junit-jupiter - provided - - - - org.hibernate.validator - hibernate-validator - test - - - javax.validation - validation-api - - - - - jakarta.validation - jakarta.validation-api - test - - - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/BuildOutput.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/BuildOutput.java index a0e1c3bd5b6e..f283aeebfb49 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/BuildOutput.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/BuildOutput.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,7 +40,9 @@ public BuildOutput(Class testClass) { public File getTestClassesLocation() { try { File location = new File(this.testClass.getProtectionDomain().getCodeSource().getLocation().toURI()); - if (location.getPath().endsWith(path("target", "test-classes"))) { + if (location.getPath().endsWith(path("bin", "test")) || location.getPath().endsWith(path("bin", "intTest")) + || location.getPath().endsWith(path("build", "classes", "java", "test")) + || location.getPath().endsWith(path("build", "classes", "java", "intTest"))) { return location; } throw new IllegalStateException("Unexpected test classes location '" + location + "'"); @@ -56,9 +58,16 @@ public File getTestClassesLocation() { */ public File getTestResourcesLocation() { File testClassesLocation = getTestClassesLocation(); - if (testClassesLocation.getPath().endsWith(path("target", "test-classes"))) { + if (testClassesLocation.getPath().endsWith(path("bin", "test")) + || testClassesLocation.getPath().endsWith(path("bin", "intTest"))) { return testClassesLocation; } + if (testClassesLocation.getPath().endsWith(path("build", "classes", "java", "test"))) { + return new File(testClassesLocation.getParentFile().getParentFile().getParentFile(), "resources/test"); + } + if (testClassesLocation.getPath().endsWith(path("build", "classes", "java", "intTest"))) { + return new File(testClassesLocation.getParentFile().getParentFile().getParentFile(), "resources/intTest"); + } throw new IllegalStateException( "Cannot determine test resources location from classes location '" + testClassesLocation + "'"); } @@ -68,7 +77,7 @@ public File getTestResourcesLocation() { * @return root location */ public File getRootLocation() { - return getTestClassesLocation().getParentFile(); + return new File("build"); } private String path(String... components) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ForkedClassPath.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ForkedClassPath.java new file mode 100644 index 000000000000..30168fa77faa --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ForkedClassPath.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.testsupport.classpath; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Annotation used to fork the classpath. This can be helpful where neither + * {@link ClassPathExclusions} or {@link ClassPathOverrides} are needed, but just a copy + * of the classpath. + * + * @author Christoph Dreis + * @since 2.4.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Documented +@ExtendWith(ModifiedClassPathExtension.class) +public @interface ForkedClassPath { + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathClassLoader.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathClassLoader.java index ec9ef0f6cff7..adae5409fb75 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathClassLoader.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathClassLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -66,6 +66,8 @@ final class ModifiedClassPathClassLoader extends URLClassLoader { private static final Pattern INTELLIJ_CLASSPATH_JAR_PATTERN = Pattern.compile(".*classpath(\\d+)?\\.jar"); + private static final int MAX_RESOLUTION_ATTEMPTS = 5; + private final ClassLoader junitLoader; ModifiedClassPathClassLoader(URL[] urls, ClassLoader parent, ClassLoader junitLoader) { @@ -75,8 +77,9 @@ final class ModifiedClassPathClassLoader extends URLClassLoader { @Override public Class loadClass(String name) throws ClassNotFoundException { - if (name.startsWith("org.junit") || name.startsWith("org.hamcrest")) { - return this.junitLoader.loadClass(name); + if (name.startsWith("org.junit") || name.startsWith("org.hamcrest") + || name.startsWith("io.netty.internal.tcnative")) { + return Class.forName(name, false, this.junitLoader); } return super.loadClass(name); } @@ -87,7 +90,14 @@ static ModifiedClassPathClassLoader get(Class testClass) { private static ModifiedClassPathClassLoader compute(Class testClass) { ClassLoader classLoader = testClass.getClassLoader(); - return new ModifiedClassPathClassLoader(processUrls(extractUrls(classLoader), testClass), + MergedAnnotations annotations = MergedAnnotations.from(testClass, + MergedAnnotations.SearchStrategy.TYPE_HIERARCHY); + if (annotations.isPresent(ForkedClassPath.class) && (annotations.isPresent(ClassPathOverrides.class) + || annotations.isPresent(ClassPathExclusions.class))) { + throw new IllegalStateException("@ForkedClassPath is redundant in combination with either " + + "@ClassPathOverrides or @ClassPathExclusions"); + } + return new ModifiedClassPathClassLoader(processUrls(extractUrls(classLoader), annotations), classLoader.getParent(), classLoader); } @@ -122,11 +132,7 @@ private static URL toURL(String entry) { } private static boolean isManifestOnlyJar(URL url) { - return isSurefireBooterJar(url) || isShortenedIntelliJJar(url); - } - - private static boolean isSurefireBooterJar(URL url) { - return url.getPath().contains("surefirebooter"); + return isShortenedIntelliJJar(url); } private static boolean isShortenedIntelliJJar(URL url) { @@ -168,13 +174,10 @@ private static Attributes getManifestMainAttributesFromUrl(URL url) throws Excep } } - private static URL[] processUrls(URL[] urls, Class testClass) { - MergedAnnotations annotations = MergedAnnotations.from(testClass, - MergedAnnotations.SearchStrategy.TYPE_HIERARCHY); + private static URL[] processUrls(URL[] urls, MergedAnnotations annotations) { ClassPathEntryFilter filter = new ClassPathEntryFilter(annotations.get(ClassPathExclusions.class)); - List processedUrls = new ArrayList<>(); List additionalUrls = getAdditionalUrls(annotations.get(ClassPathOverrides.class)); - processedUrls.addAll(additionalUrls); + List processedUrls = new ArrayList<>(additionalUrls); for (URL url : urls) { if (!filter.isExcluded(url)) { processedUrls.add(url); @@ -191,29 +194,34 @@ private static List getAdditionalUrls(MergedAnnotation } private static List resolveCoordinates(String[] coordinates) { + Exception latestFailure = null; DefaultServiceLocator serviceLocator = MavenRepositorySystemUtils.newServiceLocator(); serviceLocator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class); serviceLocator.addService(TransporterFactory.class, HttpTransporterFactory.class); RepositorySystem repositorySystem = serviceLocator.getService(RepositorySystem.class); DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession(); LocalRepository localRepository = new LocalRepository(System.getProperty("user.home") + "/.m2/repository"); + RemoteRepository remoteRepository = new RemoteRepository.Builder("central", "default", + "https://repo.maven.apache.org/maven2").build(); session.setLocalRepositoryManager(repositorySystem.newLocalRepositoryManager(session, localRepository)); - CollectRequest collectRequest = new CollectRequest(null, Arrays.asList( - new RemoteRepository.Builder("central", "default", "https://repo.maven.apache.org/maven2").build())); - - collectRequest.setDependencies(createDependencies(coordinates)); - DependencyRequest dependencyRequest = new DependencyRequest(collectRequest, null); - try { - DependencyResult result = repositorySystem.resolveDependencies(session, dependencyRequest); - List resolvedArtifacts = new ArrayList<>(); - for (ArtifactResult artifact : result.getArtifactResults()) { - resolvedArtifacts.add(artifact.getArtifact().getFile().toURI().toURL()); + for (int i = 0; i < MAX_RESOLUTION_ATTEMPTS; i++) { + CollectRequest collectRequest = new CollectRequest(null, Arrays.asList(remoteRepository)); + collectRequest.setDependencies(createDependencies(coordinates)); + DependencyRequest dependencyRequest = new DependencyRequest(collectRequest, null); + try { + DependencyResult result = repositorySystem.resolveDependencies(session, dependencyRequest); + List resolvedArtifacts = new ArrayList<>(); + for (ArtifactResult artifact : result.getArtifactResults()) { + resolvedArtifacts.add(artifact.getArtifact().getFile().toURI().toURL()); + } + return resolvedArtifacts; + } + catch (Exception ex) { + latestFailure = ex; } - return resolvedArtifacts; - } - catch (Exception ignored) { - return Collections.emptyList(); } + throw new IllegalStateException("Resolution failed after " + MAX_RESOLUTION_ATTEMPTS + " attempts", + latestFailure); } private static List createDependencies(String[] allCoordinates) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathExtension.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathExtension.java index 9099ee738558..90a6d6c33446 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathExtension.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,30 +16,33 @@ package org.springframework.boot.testsupport.classpath; -import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.URLClassLoader; -import java.util.concurrent.atomic.AtomicBoolean; import org.junit.jupiter.api.extension.Extension; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.InvocationInterceptor; import org.junit.jupiter.api.extension.ReflectiveInvocationContext; import org.junit.platform.engine.discovery.DiscoverySelectors; +import org.junit.platform.launcher.Launcher; +import org.junit.platform.launcher.LauncherDiscoveryRequest; +import org.junit.platform.launcher.TestPlan; +import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; +import org.junit.platform.launcher.core.LauncherFactory; +import org.junit.platform.launcher.listeners.SummaryGeneratingListener; +import org.junit.platform.launcher.listeners.TestExecutionSummary; -import org.springframework.boot.testsupport.junit.platform.Launcher; -import org.springframework.boot.testsupport.junit.platform.LauncherDiscoveryRequest; -import org.springframework.boot.testsupport.junit.platform.LauncherDiscoveryRequestBuilder; -import org.springframework.boot.testsupport.junit.platform.SummaryGeneratingListener; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; /** * A custom {@link Extension} that runs tests using a modified class path. Entries are * excluded from the class path using {@link ClassPathExclusions @ClassPathExclusions} and - * overridden using {@link ClassPathOverrides @ClassPathOverrides} on the test class. A - * class loader is created with the customized class path and is used both to load the - * test class and as the thread context class loader while the test is being run. + * overridden using {@link ClassPathOverrides @ClassPathOverrides} on the test class. For + * an unchanged copy of the class path {@link ForkedClassPath @ForkedClassPath} can be + * used. A class loader is created with the customized class path and is used both to load + * the test class and as the thread context class loader while the test is being run. * * @author Christoph Dreis */ @@ -76,12 +79,12 @@ public void interceptTestMethod(Invocation invocation, ReflectiveInvocatio invocation.proceed(); return; } - fakeInvocation(invocation); + invocation.skip(); runTestWithModifiedClassPath(invocationContext, extensionContext); } private void runTestWithModifiedClassPath(ReflectiveInvocationContext invocationContext, - ExtensionContext extensionContext) throws ClassNotFoundException, Throwable { + ExtensionContext extensionContext) throws Throwable { Class testClass = extensionContext.getRequiredTestClass(); Method testMethod = invocationContext.getExecutable(); ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader(); @@ -95,19 +98,19 @@ private void runTestWithModifiedClassPath(ReflectiveInvocationContext in } } - private void runTest(ClassLoader classLoader, String testClassName, String testMethodName) - throws ClassNotFoundException, Throwable { + private void runTest(ClassLoader classLoader, String testClassName, String testMethodName) throws Throwable { Class testClass = classLoader.loadClass(testClassName); Method testMethod = findMethod(testClass, testMethodName); - LauncherDiscoveryRequest request = new LauncherDiscoveryRequestBuilder(classLoader) + LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request() .selectors(DiscoverySelectors.selectMethod(testClass, testMethod)).build(); - Launcher launcher = new Launcher(classLoader); - SummaryGeneratingListener listener = new SummaryGeneratingListener(classLoader); + Launcher launcher = LauncherFactory.create(); + TestPlan testPlan = launcher.discover(request); + SummaryGeneratingListener listener = new SummaryGeneratingListener(); launcher.registerTestExecutionListeners(listener); - launcher.execute(request); - Throwable failure = listener.getSummary().getFailure(); - if (failure != null) { - throw failure; + launcher.execute(testPlan); + TestExecutionSummary summary = listener.getSummary(); + if (!CollectionUtils.isEmpty(summary.getFailures())) { + throw summary.getFailures().get(0).getException(); } } @@ -121,7 +124,7 @@ private Method findMethod(Class testClass, String testMethodName) { } } } - Assert.state(method != null, "Unable to find " + testClass + "." + testMethodName); + Assert.state(method != null, () -> "Unable to find " + testClass + "." + testMethodName); return method; } @@ -130,17 +133,7 @@ private void intercept(Invocation invocation, ExtensionContext extensionCo invocation.proceed(); return; } - fakeInvocation(invocation); - } - - private void fakeInvocation(Invocation invocation) { - try { - Field field = ReflectionUtils.findField(invocation.getClass(), "invoked"); - ReflectionUtils.makeAccessible(field); - ReflectionUtils.setField(field, invocation, new AtomicBoolean(true)); - } - catch (Throwable ex) { - } + invocation.skip(); } private boolean isModifiedClassPathClassLoader(ExtensionContext extensionContext) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/package-info.java index b4a6ea0d3795..9c4624e5d0e7 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/package-info.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,6 @@ */ /** - * Custom JUnit runner to change the classpath. + * Custom JUnit extension to change the classpath. */ package org.springframework.boot.testsupport.classpath; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/compiler/TestCompiler.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/compiler/TestCompiler.java index 3bf0002f00fa..532174dbdea6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/compiler/TestCompiler.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/compiler/TestCompiler.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import java.io.IOException; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import javax.annotation.processing.Processor; import javax.tools.JavaCompiler; @@ -40,9 +41,9 @@ public class TestCompiler { /** - * The default source folder. + * The default source directory. */ - public static final File SOURCE_FOLDER = new File("src/test/java"); + public static final File SOURCE_DIRECTORY = new File("src/test/java"); private final JavaCompiler compiler; @@ -59,7 +60,7 @@ public TestCompiler(JavaCompiler compiler, File outputLocation) throws IOExcepti this.fileManager = compiler.getStandardFileManager(null, null, null); this.outputLocation = outputLocation; this.outputLocation.mkdirs(); - Iterable temp = Arrays.asList(this.outputLocation); + Iterable temp = Collections.singletonList(this.outputLocation); this.fileManager.setLocation(StandardLocation.CLASS_OUTPUT, temp); this.fileManager.setLocation(StandardLocation.SOURCE_OUTPUT, temp); } @@ -92,15 +93,15 @@ private Iterable getJavaFileObjects(Class... types) } protected File getFile(Class type) { - return new File(getSourceFolder(), sourcePathFor(type)); + return new File(getSourceDirectory(), sourcePathFor(type)); } public static String sourcePathFor(Class type) { return type.getName().replace('.', '/') + ".java"; } - protected File getSourceFolder() { - return SOURCE_FOLDER; + protected File getSourceDirectory() { + return SOURCE_DIRECTORY; } /** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/DisabledOnOs.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/DisabledOnOs.java new file mode 100644 index 000000000000..d691eac3a810 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/DisabledOnOs.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.testsupport.junit; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.condition.OS; +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Improves JUnit5's {@link org.junit.jupiter.api.condition.DisabledOnOs} by adding an + * architecture check. + * + * @author Moritz Halbritter + * @since 2.5.11 + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(DisabledOnOsCondition.class) +public @interface DisabledOnOs { + + /** + * See {@link org.junit.jupiter.api.condition.DisabledOnOs#value()}. + * @return os + */ + OS os(); + + /** + * Architecture of the operating system. + * @return architecture + */ + String architecture(); + + /** + * See {@link org.junit.jupiter.api.condition.DisabledOnOs#disabledReason()}. + * @return disabled reason + */ + String disabledReason() default ""; + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/DisabledOnOsCondition.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/DisabledOnOsCondition.java new file mode 100644 index 000000000000..3b644b2006af --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/DisabledOnOsCondition.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.testsupport.junit; + +import java.util.Optional; + +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.platform.commons.util.AnnotationUtils; + +/** + * Evaluates {@link DisabledOnOs}. + * + * @author Moritz Halbritter + */ +class DisabledOnOsCondition implements ExecutionCondition { + + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { + Optional annotation = AnnotationUtils.findAnnotation(context.getElement(), DisabledOnOs.class); + if (!annotation.isPresent()) { + return ConditionEvaluationResult.enabled("No @DisabledOnOs found"); + } + return evaluate(annotation.get()); + } + + private ConditionEvaluationResult evaluate(DisabledOnOs annotation) { + String architecture = System.getProperty("os.arch"); + String os = System.getProperty("os.name"); + if (annotation.os().isCurrentOs() && annotation.architecture().equals(architecture)) { + String reason = annotation.disabledReason().isEmpty() + ? String.format("Disabled on OS = %s, architecture = %s", os, architecture) + : annotation.disabledReason(); + return ConditionEvaluationResult.disabled(reason); + } + return ConditionEvaluationResult + .enabled(String.format("Enabled on OS = %s, architecture = %s", os, architecture)); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/platform/Launcher.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/platform/Launcher.java deleted file mode 100644 index 7ae970f0e9f8..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/platform/Launcher.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.testsupport.junit.platform; - -import java.lang.reflect.Array; - -/** - * Reflective mirror of JUnit 5's {@code Launcher}. - * - * @author Phillip Webb - * @since 2.2.0 - */ -public class Launcher extends ReflectiveWrapper { - - private final Class testExecutionListenerType; - - private final Object instance; - - public Launcher(ClassLoader classLoader) throws Throwable { - super(classLoader, "org.junit.platform.launcher.Launcher"); - this.testExecutionListenerType = loadClass("org.junit.platform.launcher.TestExecutionListener"); - Class factoryClass = loadClass("org.junit.platform.launcher.core.LauncherFactory"); - this.instance = factoryClass.getMethod("create").invoke(null); - } - - public void registerTestExecutionListeners(SummaryGeneratingListener listener) throws Throwable { - Object listeners = Array.newInstance(this.testExecutionListenerType, 1); - Array.set(listeners, 0, listener.instance); - this.type.getMethod("registerTestExecutionListeners", listeners.getClass()).invoke(this.instance, listeners); - } - - public void execute(LauncherDiscoveryRequest request) throws Throwable { - Object listeners = Array.newInstance(this.testExecutionListenerType, 0); - this.type.getMethod("execute", request.type, listeners.getClass()).invoke(this.instance, request.instance, - listeners); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/platform/LauncherDiscoveryRequest.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/platform/LauncherDiscoveryRequest.java deleted file mode 100644 index c71732b7e5bb..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/platform/LauncherDiscoveryRequest.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.testsupport.junit.platform; - -/** - * Reflective mirror of JUnit 5's {@code LauncherDiscoveryRequest}. - * - * @author Phillip Webb - * @since 2.2.0 - */ -public class LauncherDiscoveryRequest extends ReflectiveWrapper { - - final Object instance; - - LauncherDiscoveryRequest(ClassLoader classLoader, Object instance) throws Throwable { - super(classLoader, "org.junit.platform.launcher.LauncherDiscoveryRequest"); - this.instance = instance; - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/platform/LauncherDiscoveryRequestBuilder.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/platform/LauncherDiscoveryRequestBuilder.java deleted file mode 100644 index a029c8ee4e10..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/platform/LauncherDiscoveryRequestBuilder.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.testsupport.junit.platform; - -import java.lang.reflect.Method; - -import org.junit.platform.engine.DiscoverySelector; - -/** - * Reflective mirror of JUnit 5's {@code LauncherDiscoveryRequestBuilder}. - * - * @author Phillip Webb - * @since 2.2.0 - */ -public class LauncherDiscoveryRequestBuilder extends ReflectiveWrapper { - - final Object instance; - - public LauncherDiscoveryRequestBuilder(ClassLoader classLoader) throws Throwable { - super(classLoader, "org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder"); - this.instance = this.type.getMethod("request").invoke(null); - } - - LauncherDiscoveryRequestBuilder(ClassLoader classLoader, Class type, Object instance) throws Throwable { - super(classLoader, type); - this.instance = instance; - } - - public LauncherDiscoveryRequestBuilder selectors(DiscoverySelector... selectors) throws Throwable { - Class[] parameterTypes = { DiscoverySelector[].class }; - Method method = this.type.getMethod("selectors", parameterTypes); - return new LauncherDiscoveryRequestBuilder(getClassLoader(), this.type, - method.invoke(this.instance, new Object[] { selectors })); - } - - public LauncherDiscoveryRequest build() throws Throwable { - return new LauncherDiscoveryRequest(getClassLoader(), this.type.getMethod("build").invoke(this.instance)); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/platform/ReflectiveWrapper.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/platform/ReflectiveWrapper.java deleted file mode 100644 index 8bd468a3e69b..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/platform/ReflectiveWrapper.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.testsupport.junit.platform; - -import org.springframework.util.ClassUtils; - -/** - * Base class for all reflective wrappers. - * - * @author Phillip Webb - */ -class ReflectiveWrapper { - - final ClassLoader classLoader; - - final Class type; - - ReflectiveWrapper(ClassLoader classLoader, String type) throws Throwable { - this.classLoader = classLoader; - this.type = loadClass(type); - } - - protected ReflectiveWrapper(ClassLoader classLoader, Class type) throws Throwable { - this.classLoader = classLoader; - this.type = type; - } - - protected final ClassLoader getClassLoader() { - return this.classLoader; - } - - protected final Class loadClass(String type) throws ClassNotFoundException, LinkageError { - return ClassUtils.forName(type, this.classLoader); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/platform/SummaryGeneratingListener.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/platform/SummaryGeneratingListener.java deleted file mode 100644 index 918df638afee..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/platform/SummaryGeneratingListener.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.testsupport.junit.platform; - -/** - * Reflective mirror of JUnit 5's {@code SummaryGeneratingListener}. - * - * @author Phillip Webb - * @since 2.2.0 - */ -public class SummaryGeneratingListener extends ReflectiveWrapper { - - final Object instance; - - public SummaryGeneratingListener(ClassLoader classLoader) throws Throwable { - super(classLoader, "org.junit.platform.launcher.listeners.SummaryGeneratingListener"); - this.instance = this.type.newInstance(); - } - - public TestExecutionSummary getSummary() throws Throwable { - return new TestExecutionSummary(getClassLoader(), this.type.getMethod("getSummary").invoke(this.instance)); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/platform/TestExecutionSummary.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/platform/TestExecutionSummary.java deleted file mode 100644 index b56d960c6089..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/platform/TestExecutionSummary.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.testsupport.junit.platform; - -import java.util.List; - -import org.springframework.util.CollectionUtils; - -/** - * Reflective mirror of JUnit 5's {@code TestExecutionSummary}. - * - * @author Phillip Webb - * @since 2.2.0 - */ -public class TestExecutionSummary extends ReflectiveWrapper { - - private final Class failureType; - - private final Object instance; - - TestExecutionSummary(ClassLoader classLoader, Object instance) throws Throwable { - super(classLoader, "org.junit.platform.launcher.listeners.TestExecutionSummary"); - this.failureType = loadClass("org.junit.platform.launcher.listeners.TestExecutionSummary$Failure"); - this.instance = instance; - } - - public Throwable getFailure() throws Throwable { - List failures = (List) this.type.getMethod("getFailures").invoke(this.instance); - if (!CollectionUtils.isEmpty(failures)) { - Object failure = failures.get(0); - return (Throwable) this.failureType.getMethod("getException").invoke(failure); - } - return null; - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/platform/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/platform/package-info.java deleted file mode 100644 index f516d737447c..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/platform/package-info.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Reflective mirror of JUnit 5 classes required to workaround surefire bug - * {@code SUREFIRE-1679}. - */ -package org.springframework.boot.testsupport.junit.platform; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/system/OutputCapture.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/system/OutputCapture.java index 9c8d16b7daee..4a60add574f0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/system/OutputCapture.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/system/OutputCapture.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -194,7 +194,7 @@ PrintStream getParent() { private static PrintStream getSystemStream(PrintStream printStream) { while (printStream instanceof PrintStreamCapture) { - return ((PrintStreamCapture) printStream).getParent(); + printStream = ((PrintStreamCapture) printStream).getParent(); } return printStream; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/CassandraContainer.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/CassandraContainer.java new file mode 100644 index 000000000000..adf9c451a3a2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/CassandraContainer.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.testsupport.testcontainers; + +import java.time.Duration; + +/** + * Custom {@link org.testcontainers.containers.CassandraContainer} tuned for stability in + * heavily contended environments such as CI. + * + * @author Andy Wilkinson + * @since 2.4.10 + */ +public class CassandraContainer extends org.testcontainers.containers.CassandraContainer { + + public CassandraContainer() { + super(DockerImageNames.cassandra()); + withStartupTimeout(Duration.ofMinutes(10)); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DisabledIfDockerUnavailable.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DisabledIfDockerUnavailable.java new file mode 100644 index 000000000000..d148b50047c4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DisabledIfDockerUnavailable.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.testsupport.testcontainers; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Disables test execution if Docker is unavailable. + * + * @author Andy Wilkinson + * @author Phillip Webb + * @since 2.3.0 + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@ExtendWith(DisabledIfDockerUnavailableCondition.class) +public @interface DisabledIfDockerUnavailable { + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DisabledIfDockerUnavailableCondition.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DisabledIfDockerUnavailableCondition.java new file mode 100644 index 000000000000..4b6b33253805 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DisabledIfDockerUnavailableCondition.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.testsupport.testcontainers; + +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.testcontainers.DockerClientFactory; + +/** + * An {@link ExecutionCondition} that disables execution if Docker is unavailable. + * + * @author Andy Wilkinson + * @author Phillip Webb + */ +class DisabledIfDockerUnavailableCondition implements ExecutionCondition { + + private static final String SILENCE_PROPERTY = "visibleassertions.silence"; + + private static final ConditionEvaluationResult ENABLED = ConditionEvaluationResult.enabled("Docker available"); + + private static final ConditionEvaluationResult DISABLED = ConditionEvaluationResult.disabled("Docker unavailable"); + + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { + String originalSilenceValue = System.getProperty(SILENCE_PROPERTY); + try { + DockerClientFactory.instance().client(); + return ENABLED; + } + catch (Throwable ex) { + return DISABLED; + } + finally { + if (originalSilenceValue != null) { + System.setProperty(SILENCE_PROPERTY, originalSilenceValue); + } + else { + System.clearProperty(SILENCE_PROPERTY); + } + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java new file mode 100644 index 000000000000..4df12a9a3bdd --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java @@ -0,0 +1,113 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.testsupport.testcontainers; + +import org.testcontainers.utility.DockerImageName; + +/** + * Create {@link DockerImageName} instances for services used in integration tests. + * + * @author Stephane Nicoll + * @since 2.3.6 + */ +public final class DockerImageNames { + + private static final String CASSANDRA_VERSION = "3.11.10"; + + private static final String COUCHBASE_VERSION = "6.5.1"; + + private static final String MONGO_VERSION = "4.0.23"; + + private static final String NEO4J_VERSION = "4.0"; + + private static final String POSTGRESQL_VERSION = "9.6.21"; + + private static final String REDIS_VERSION = "4.0.14"; + + private static final String REGISTRY_VERSION = "2.7.1"; + + private DockerImageNames() { + } + + /** + * Return a {@link DockerImageName} suitable for running Cassandra. + * @return a docker image name for running cassandra + */ + public static DockerImageName cassandra() { + return DockerImageName.parse("cassandra").withTag(CASSANDRA_VERSION); + } + + /** + * Return a {@link DockerImageName} suitable for running Couchbase. + * @return a docker image name for running couchbase + */ + public static DockerImageName couchbase() { + return DockerImageName.parse("couchbase/server").withTag(COUCHBASE_VERSION); + } + + /** + * Return a {@link DockerImageName} suitable for running Elasticsearch according to + * the version available on the classpath. + * @return a docker image name for running elasticsearch + */ + public static DockerImageName elasticsearch() { + String version = org.elasticsearch.Version.CURRENT.toString(); + return DockerImageName.parse("docker.elastic.co/elasticsearch/elasticsearch").withTag(version); + } + + /** + * Return a {@link DockerImageName} suitable for running Mongo. + * @return a docker image name for running mongo + */ + public static DockerImageName mongo() { + return DockerImageName.parse("mongo").withTag(MONGO_VERSION); + } + + /** + * Return a {@link DockerImageName} suitable for running Neo4j. + * @return a docker image name for running neo4j + */ + public static DockerImageName neo4j() { + return DockerImageName.parse("neo4j").withTag(NEO4J_VERSION); + } + + /** + * Return a {@link DockerImageName} suitable for running PostgreSQL. + * @return a docker image name for running postgresql + */ + public static DockerImageName postgresql() { + return DockerImageName.parse("postgres").withTag(POSTGRESQL_VERSION); + } + + /** + * Return a {@link DockerImageName} suitable for running Redis. + * @return a docker image name for running redis + */ + public static DockerImageName redis() { + return DockerImageName.parse("redis").withTag(REDIS_VERSION); + } + + /** + * Return a {@link DockerImageName} suitable for running a Docker registry. + * @return a docker image name for running a registry + * @since 2.4.0 + */ + public static DockerImageName registry() { + return DockerImageName.parse("registry").withTag(REGISTRY_VERSION); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/RedisContainer.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/RedisContainer.java index cc521bf43dc8..bc53a13135fc 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/RedisContainer.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/RedisContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ public class RedisContainer extends GenericContainer { public RedisContainer() { - super("redis:4.0.6"); + super(DockerImageNames.redis()); addExposedPorts(6379); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/web/servlet/MockServletWebServer.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/web/servlet/MockServletWebServer.java index f9d9846f4c5a..7c8d35bd0369 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/web/servlet/MockServletWebServer.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/web/servlet/MockServletWebServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,15 +25,18 @@ import javax.servlet.Filter; import javax.servlet.FilterRegistration; -import javax.servlet.RequestDispatcher; import javax.servlet.Servlet; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletRegistration; +import javax.servlet.SessionCookieConfig; + +import org.springframework.mock.web.MockSessionCookieConfig; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; /** @@ -66,27 +69,28 @@ public MockServletWebServer(Initializer[] initializers, int port) { private void initialize() { try { this.servletContext = mock(ServletContext.class); - given(this.servletContext.addServlet(anyString(), any(Servlet.class))).willAnswer((invocation) -> { + lenient().doAnswer((invocation) -> { RegisteredServlet registeredServlet = new RegisteredServlet(invocation.getArgument(1)); MockServletWebServer.this.registeredServlets.add(registeredServlet); return registeredServlet.getRegistration(); - }); - given(this.servletContext.addFilter(anyString(), any(Filter.class))).willAnswer((invocation) -> { + }).when(this.servletContext).addServlet(anyString(), any(Servlet.class)); + lenient().doAnswer((invocation) -> { RegisteredFilter registeredFilter = new RegisteredFilter(invocation.getArgument(1)); MockServletWebServer.this.registeredFilters.add(registeredFilter); return registeredFilter.getRegistration(); - }); + }).when(this.servletContext).addFilter(anyString(), any(Filter.class)); + final SessionCookieConfig sessionCookieConfig = new MockSessionCookieConfig(); + given(this.servletContext.getSessionCookieConfig()).willReturn(sessionCookieConfig); final Map initParameters = new HashMap<>(); - given(this.servletContext.setInitParameter(anyString(), anyString())).will((invocation) -> { + lenient().doAnswer((invocation) -> { initParameters.put(invocation.getArgument(0), invocation.getArgument(1)); return null; - }); + }).when(this.servletContext).setInitParameter(anyString(), anyString()); given(this.servletContext.getInitParameterNames()) .willReturn(Collections.enumeration(initParameters.keySet())); - given(this.servletContext.getInitParameter(anyString())) - .willAnswer((invocation) -> initParameters.get(invocation.getArgument(0))); + lenient().doAnswer((invocation) -> initParameters.get(invocation.getArgument(0))).when(this.servletContext) + .getInitParameter(anyString()); given(this.servletContext.getAttributeNames()).willReturn(Collections.emptyEnumeration()); - given(this.servletContext.getNamedDispatcher("default")).willReturn(mock(RequestDispatcher.class)); for (Initializer initializer : this.initializers) { initializer.onStartup(this.servletContext); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathExtensionExclusionsTests.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathExtensionExclusionsTests.java index 25e7206f2717..dba1a03e7734 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathExtensionExclusionsTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathExtensionExclusionsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ @ClassPathExclusions("hibernate-validator-*.jar") class ModifiedClassPathExtensionExclusionsTests { - private static final String EXCLUDED_RESOURCE = "META-INF/services/" + "javax.validation.spi.ValidationProvider"; + private static final String EXCLUDED_RESOURCE = "META-INF/services/javax.validation.spi.ValidationProvider"; @Test void entriesAreFilteredFromTestClassClassLoader() { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathExtensionForkTests.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathExtensionForkTests.java new file mode 100644 index 000000000000..fed8ad860a66 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathExtensionForkTests.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.testsupport.classpath; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ForkedClassPath @ForkedClassPath}. + * + * @author Christoph Dreis + */ +@ForkedClassPath +class ModifiedClassPathExtensionForkTests { + + @Test + void modifiedClassLoaderIsUsed() { + ClassLoader classLoader = getClass().getClassLoader(); + assertThat(classLoader.getClass().getName()).isEqualTo(ModifiedClassPathClassLoader.class.getName()); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/web/servlet/MockServletWebServerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/web/servlet/MockServletWebServerTests.java new file mode 100644 index 000000000000..1c78f1170180 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/web/servlet/MockServletWebServerTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.testsupport.web.servlet; + +import org.junit.jupiter.api.Test; + +import org.springframework.mock.web.MockSessionCookieConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MockServletWebServer}. + * + * @author Stephane Nicoll + */ +class MockServletWebServerTests { + + @Test + void servletContextIsConfigured() { + MockServletWebServer server = TestMockServletWebServer.create(); + assertThat(server.getServletContext()).isNotNull(); + } + + @Test + void servletContextHasSessionCookieConfigConfigured() { + MockServletWebServer server = TestMockServletWebServer.create(); + assertThat(server.getServletContext().getSessionCookieConfig()).isNotNull() + .isInstanceOf(MockSessionCookieConfig.class); + } + + private static final class TestMockServletWebServer extends MockServletWebServer { + + private TestMockServletWebServer(Initializer[] initializers, int port) { + super(initializers, port); + } + + static MockServletWebServer create(Initializer... initializers) { + return new TestMockServletWebServer(initializers, 8080); + } + + } + +} diff --git a/spring-boot-project/spring-boot/build.gradle b/spring-boot-project/spring-boot/build.gradle new file mode 100644 index 000000000000..fb0e69543b25 --- /dev/null +++ b/spring-boot-project/spring-boot/build.gradle @@ -0,0 +1,167 @@ +plugins { + id "java-library" + id "org.jetbrains.kotlin.jvm" + id "org.springframework.boot.conventions" + id "org.springframework.boot.configuration-properties" + id "org.springframework.boot.deployed" + id "org.springframework.boot.optional-dependencies" +} + +description = "Spring Boot" + +def tomcatConfigProperties = "$buildDir/tomcat-config-properties" + +configurations { + tomcatDistribution +} + +dependencies { + annotationProcessor("org.apache.logging.log4j:log4j-core") + + api("org.springframework:spring-core") + api("org.springframework:spring-context") + + optional("ch.qos.logback:logback-classic") + optional("com.atomikos:transactions-jdbc") + optional("com.atomikos:transactions-jms") + optional("com.atomikos:transactions-jta") + optional("com.fasterxml.jackson.core:jackson-databind") + optional("com.h2database:h2") + optional("com.google.code.gson:gson") + optional("com.oracle.database.jdbc:ucp") + optional("com.oracle.database.jdbc:ojdbc8") + optional("com.samskivert:jmustache") + optional("com.zaxxer:HikariCP") + optional("io.netty:netty-tcnative-boringssl-static") + optional("io.projectreactor:reactor-tools") + optional("io.projectreactor.netty:reactor-netty-http") + optional("io.r2dbc:r2dbc-pool") + optional("io.rsocket:rsocket-core") + optional("io.rsocket:rsocket-transport-netty") + optional("io.undertow:undertow-servlet") { + exclude group: "org.jboss.spec.javax.annotation", module: "jboss-annotations-api_1.3_spec" + exclude group: "org.jboss.spec.javax.servlet", module: "jboss-servlet-api_4.0_spec" + } + optional("jakarta.jms:jakarta.jms-api") + optional("jakarta.persistence:jakarta.persistence-api") + optional("jakarta.servlet:jakarta.servlet-api") + optional("jakarta.transaction:jakarta.transaction-api") + optional("junit:junit") + optional("org.apache.commons:commons-dbcp2") { + exclude(group: "commons-logging", module: "commons-logging") + } + optional("org.apache.httpcomponents:httpclient") { + exclude(group: "commons-logging", module: "commons-logging") + } + optional("org.apache.httpcomponents.client5:httpclient5") + optional("org.apache.logging.log4j:log4j-api") + optional("org.apache.logging.log4j:log4j-core") + optional("org.apache.tomcat.embed:tomcat-embed-core") + optional("org.apache.tomcat.embed:tomcat-embed-jasper") + optional("org.apache.tomcat:tomcat-jdbc") + optional("org.assertj:assertj-core") + optional("org.codehaus.groovy:groovy") + optional("org.codehaus.groovy:groovy-xml") + optional("org.eclipse.jetty:jetty-servlets") + optional("org.eclipse.jetty:jetty-util") + optional("org.eclipse.jetty:jetty-webapp") { + exclude(group: "javax.servlet", module: "javax.servlet-api") + } + optional("org.eclipse.jetty:jetty-alpn-conscrypt-server") { + exclude(group: "javax.servlet", module: "javax.servlet-api") + } + optional("org.eclipse.jetty.http2:http2-server") { + exclude(group: "javax.servlet", module: "javax.servlet-api") + } + optional("org.flywaydb:flyway-core") + optional("org.hamcrest:hamcrest-library") + optional("org.hibernate:hibernate-core") { + exclude(group: "javax.activation", module: "javax.activation-api") + exclude(group: "javax.persistence", module: "javax.persistence-api") + exclude(group: "javax.xml.bind", module: "jaxb-api") + exclude(group: "org.jboss.spec.javax.transaction", module: "jboss-transaction-api_1.2_spec") + } + optional("org.hibernate.validator:hibernate-validator") + optional("org.jooq:jooq") { + exclude(group: "javax.xml.bind", module: "jaxb-api") + } + optional("org.liquibase:liquibase-core") { + exclude(group: "javax.xml.bind", module: "jaxb-api") + } + optional("org.postgresql:postgresql") + optional("org.slf4j:jul-to-slf4j") + optional("org.slf4j:slf4j-api") + optional("org.springframework:spring-messaging") + optional("org.springframework:spring-orm") + optional("org.springframework:spring-oxm") + optional("org.springframework:spring-r2dbc") + optional("org.springframework:spring-test") + optional("org.springframework:spring-web") + optional("org.springframework:spring-webflux") + optional("org.springframework:spring-webmvc") + optional("org.springframework.security:spring-security-web") + optional("org.springframework.ws:spring-ws-core") + optional("org.yaml:snakeyaml") + optional("org.jetbrains.kotlin:kotlin-reflect") + optional("org.jetbrains.kotlin:kotlin-stdlib") + + testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) + testImplementation("com.google.appengine:appengine-api-1.0-sdk") { + exclude group: "javax.inject", module: "javax.inject" + } + testImplementation("com.ibm.db2:jcc") + testImplementation("com.jayway.jsonpath:json-path") + testImplementation("com.microsoft.sqlserver:mssql-jdbc") + testImplementation("com.squareup.okhttp3:okhttp") + testImplementation("com.sun.xml.messaging.saaj:saaj-impl") + testImplementation("io.projectreactor:reactor-test") + testImplementation("io.r2dbc:r2dbc-h2") + testImplementation("jakarta.inject:jakarta.inject-api") + testImplementation("jakarta.persistence:jakarta.persistence-api") + testImplementation("jakarta.xml.ws:jakarta.xml.ws-api") + testImplementation("mysql:mysql-connector-java") + testImplementation("net.sourceforge.jtds:jtds") + testImplementation("org.apache.derby:derby") + testImplementation("org.awaitility:awaitility") + testImplementation("org.eclipse.jetty:jetty-client") + testImplementation("org.eclipse.jetty.http2:http2-client") + testImplementation("org.eclipse.jetty.http2:http2-http-client-transport") + testImplementation("org.firebirdsql.jdbc:jaybird-jdk18") { + exclude group: "javax.resource", module: "connector-api" + } + testImplementation("org.hsqldb:hsqldb") + testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.mariadb.jdbc:mariadb-java-client") + testImplementation("org.mockito:mockito-core") + testImplementation("org.mockito:mockito-junit-jupiter") + testImplementation("org.springframework:spring-context-support") + testImplementation("org.springframework.data:spring-data-redis") + testImplementation("org.springframework.data:spring-data-r2dbc") + testImplementation("org.xerial:sqlite-jdbc") + + testRuntimeOnly("org.testcontainers:jdbc") { + exclude group: "javax.annotation", module: "javax.annotation-api" + exclude group: "javax.xml.bind", module: "jaxb-api" + } + + tomcatDistribution("org.apache.tomcat:tomcat:${tomcatVersion}@zip") +} + +task extractTomcatConfigProperties(type: Sync) { + destinationDir = file(tomcatConfigProperties) + from { + zipTree(configurations.tomcatDistribution.incoming.files.singleFile).matching { + include '**/conf/catalina.properties' + }.singleFile + } +} + +sourceSets { + test { + output.dir(tomcatConfigProperties, builtBy: "extractTomcatConfigProperties") + } +} + +toolchain { + testJvmArgs.add("--add-opens=java.base/java.net=ALL-UNNAMED") +} diff --git a/spring-boot-project/spring-boot/pom.xml b/spring-boot-project/spring-boot/pom.xml deleted file mode 100644 index cb3d81a582d6..000000000000 --- a/spring-boot-project/spring-boot/pom.xml +++ /dev/null @@ -1,545 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-parent - ${revision} - ../spring-boot-parent - - spring-boot - Spring Boot - Spring Boot - - ${basedir}/../.. - - - ${git.url} - ${git.connection} - ${git.developerConnection} - - - - - org.springframework - spring-core - - - org.springframework - spring-context - - - - ch.qos.logback - logback-classic - true - - - com.atomikos - transactions-jdbc - true - - - com.atomikos - transactions-jms - true - - - com.atomikos - transactions-jta - true - - - com.fasterxml.jackson.core - jackson-databind - true - - - com.google.code.gson - gson - true - - - com.oracle.ojdbc - ojdbc8 - true - - - com.samskivert - jmustache - true - - - com.sendgrid - sendgrid-java - true - - - com.zaxxer - HikariCP - true - - - io.projectreactor.netty - reactor-netty - true - - - io.netty - netty-tcnative-boringssl-static - true - - - io.rsocket - rsocket-core - true - - - io.rsocket - rsocket-transport-netty - true - - - io.undertow - undertow-servlet - true - - - jakarta.jms - jakarta.jms-api - true - - - jakarta.servlet - jakarta.servlet-api - true - - - jakarta.validation - jakarta.validation-api - true - - - junit - junit - true - - - org.hamcrest - hamcrest-core - - - - - org.apache.commons - commons-dbcp2 - true - - - org.apache.httpcomponents - httpclient - true - - - org.apache.logging.log4j - log4j-api - true - - - org.apache.logging.log4j - log4j-core - true - - - org.apache.tomcat.embed - tomcat-embed-core - true - - - org.apache.tomcat.embed - tomcat-embed-jasper - true - - - org.apache.tomcat - tomcat-jdbc - true - - - org.assertj - assertj-core - true - - - org.codehaus.btm - btm - true - - - javax.transaction - jta - - - - - org.codehaus.groovy - groovy - true - - - org.codehaus.groovy - groovy-xml - true - - - org.eclipse.jetty - jetty-servlets - true - - - org.eclipse.jetty - jetty-util - true - - - org.eclipse.jetty - jetty-webapp - true - - - org.eclipse.jetty - jetty-alpn-conscrypt-server - true - - - org.eclipse.jetty.http2 - http2-server - true - - - javax.servlet - javax.servlet-api - - - - - org.hamcrest - hamcrest - true - - - org.hibernate - hibernate-core - true - - - javax.activation - javax.activation-api - - - javax.persistence - javax.persistence-api - - - javax.xml.bind - jaxb-api - - - - - org.hibernate.validator - hibernate-validator - true - - - javax.validation - validation-api - - - - - org.jboss - jboss-transaction-spi - true - - - org.liquibase - liquibase-core - true - - - org.neo4j - neo4j-ogm-core - true - - - org.slf4j - jul-to-slf4j - true - - - org.slf4j - slf4j-api - true - - - org.springframework - spring-messaging - true - - - org.springframework - spring-orm - true - - - org.springframework - spring-oxm - true - - - org.springframework - spring-test - true - - - org.springframework - spring-web - true - - - org.springframework - spring-webflux - true - - - org.springframework - spring-webmvc - true - - - org.springframework.security - spring-security-web - true - - - org.springframework.ws - spring-ws-core - true - - - org.yaml - snakeyaml - true - - - org.jetbrains.kotlin - kotlin-reflect - true - - - org.jetbrains.kotlin - kotlin-stdlib - true - - - - org.springframework.boot - spring-boot-configuration-processor - true - - - - org.springframework.boot - spring-boot-test-support - test - - - com.google.appengine - appengine-api-1.0-sdk - test - - - com.h2database - h2 - test - - - com.ibm.db2 - jcc - test - - - com.jayway.jsonpath - json-path - test - - - com.microsoft.sqlserver - mssql-jdbc - test - - - com.squareup.okhttp3 - okhttp - test - - - com.sun.xml.messaging.saaj - saaj-impl - test - - - io.projectreactor - reactor-test - test - - - jakarta.persistence - jakarta.persistence-api - test - - - jakarta.xml.ws - jakarta.xml.ws-api - test - - - mysql - mysql-connector-java - test - - - net.sourceforge.jtds - jtds - test - - - org.apache.derby - derby - test - - - org.apache.httpcomponents - httpasyncclient - test - - - org.awaitility - awaitility - test - - - org.firebirdsql.jdbc - jaybird-jdk18 - test - - - javax.resource - connector-api - - - - - org.hsqldb - hsqldb - test - - - org.mariadb.jdbc - mariadb-java-client - test - - - org.mockito - mockito-core - test - - - org.postgresql - postgresql - test - - - org.springframework - spring-context-support - test - - - org.springframework.data - spring-data-redis - test - - - org.xerial - sqlite-jdbc - test - - - - - - org.jetbrains.kotlin - kotlin-maven-plugin - - - compile - compile - - compile - - - - ${project.basedir}/src/main/kotlin - ${project.basedir}/src/main/java - - - - - test-compile - test-compile - - test-compile - - - - ${project.basedir}/src/test/kotlin - ${project.basedir}/src/test/java - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - - default-compile - none - - - default-testCompile - none - - - java-compile - compile - - compile - - - - java-test-compile - test-compile - - testCompile - - - - - - - diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ApplicationContextFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ApplicationContextFactory.java new file mode 100644 index 000000000000..d17ee20d31d6 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ApplicationContextFactory.java @@ -0,0 +1,91 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot; + +import java.util.function.Supplier; + +import org.springframework.beans.BeanUtils; +import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext; +import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +/** + * Strategy interface for creating the {@link ConfigurableApplicationContext} used by a + * {@link SpringApplication}. Created contexts should be returned in their default form, + * with the {@code SpringApplication} responsible for configuring and refreshing the + * context. + * + * @author Andy Wilkinson + * @author Phillip Webb + * @since 2.4.0 + */ +@FunctionalInterface +public interface ApplicationContextFactory { + + /** + * A default {@link ApplicationContextFactory} implementation that will create an + * appropriate context for the {@link WebApplicationType}. + */ + ApplicationContextFactory DEFAULT = (webApplicationType) -> { + try { + switch (webApplicationType) { + case SERVLET: + return new AnnotationConfigServletWebServerApplicationContext(); + case REACTIVE: + return new AnnotationConfigReactiveWebServerApplicationContext(); + default: + return new AnnotationConfigApplicationContext(); + } + } + catch (Exception ex) { + throw new IllegalStateException("Unable create a default ApplicationContext instance, " + + "you may need a custom ApplicationContextFactory", ex); + } + }; + + /** + * Creates the {@link ConfigurableApplicationContext application context} for a + * {@link SpringApplication}, respecting the given {@code webApplicationType}. + * @param webApplicationType the web application type + * @return the newly created application context + */ + ConfigurableApplicationContext create(WebApplicationType webApplicationType); + + /** + * Creates an {@code ApplicationContextFactory} that will create contexts by + * instantiating the given {@code contextClass} via its primary constructor. + * @param contextClass the context class + * @return the factory that will instantiate the context class + * @see BeanUtils#instantiateClass(Class) + */ + static ApplicationContextFactory ofContextClass(Class contextClass) { + return of(() -> BeanUtils.instantiateClass(contextClass)); + } + + /** + * Creates an {@code ApplicationContextFactory} that will create contexts by calling + * the given {@link Supplier}. + * @param supplier the context supplier, for example + * {@code AnnotationConfigApplicationContext::new} + * @return the factory that will instantiate the context class + */ + static ApplicationContextFactory of(Supplier supplier) { + return (webApplicationType) -> supplier.get(); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ApplicationEnvironment.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ApplicationEnvironment.java new file mode 100644 index 000000000000..c948a8d4925a --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ApplicationEnvironment.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot; + +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; +import org.springframework.core.env.ConfigurablePropertyResolver; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.StandardEnvironment; + +/** + * {@link StandardEnvironment} for typical use in a typical {@link SpringApplication}. + * + * @author Phillip Webb + */ +class ApplicationEnvironment extends StandardEnvironment { + + @Override + protected String doGetActiveProfilesProperty() { + return null; + } + + @Override + protected String doGetDefaultProfilesProperty() { + return null; + } + + @Override + protected ConfigurablePropertyResolver createPropertyResolver(MutablePropertySources propertySources) { + return ConfigurationPropertySources.createPropertyResolver(propertySources); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ApplicationReactiveWebEnvironment.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ApplicationReactiveWebEnvironment.java new file mode 100644 index 000000000000..6f0324e65f0a --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ApplicationReactiveWebEnvironment.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot; + +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; +import org.springframework.boot.web.reactive.context.StandardReactiveWebEnvironment; +import org.springframework.core.env.ConfigurablePropertyResolver; +import org.springframework.core.env.MutablePropertySources; + +/** + * {@link StandardReactiveWebEnvironment} for typical use in a typical + * {@link SpringApplication}. + * + * @author Phillip Webb + */ +class ApplicationReactiveWebEnvironment extends StandardReactiveWebEnvironment { + + @Override + protected String doGetActiveProfilesProperty() { + return null; + } + + @Override + protected String doGetDefaultProfilesProperty() { + return null; + } + + @Override + protected ConfigurablePropertyResolver createPropertyResolver(MutablePropertySources propertySources) { + return ConfigurationPropertySources.createPropertyResolver(propertySources); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ApplicationServletEnvironment.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ApplicationServletEnvironment.java new file mode 100644 index 000000000000..22c3eedd319e --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ApplicationServletEnvironment.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot; + +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; +import org.springframework.core.env.ConfigurablePropertyResolver; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.web.context.support.StandardServletEnvironment; + +/** + * {@link StandardServletEnvironment} for typical use in a typical + * {@link SpringApplication}. + * + * @author Phillip Webb + */ +class ApplicationServletEnvironment extends StandardServletEnvironment { + + @Override + protected String doGetActiveProfilesProperty() { + return null; + } + + @Override + protected String doGetDefaultProfilesProperty() { + return null; + } + + @Override + protected ConfigurablePropertyResolver createPropertyResolver(MutablePropertySources propertySources) { + return ConfigurationPropertySources.createPropertyResolver(propertySources); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/BeanDefinitionLoader.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/BeanDefinitionLoader.java index 760a436ac7be..93fdf1658f03 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/BeanDefinitionLoader.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/BeanDefinitionLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.boot; import java.io.IOException; +import java.lang.reflect.Constructor; import java.util.HashSet; import java.util.Set; @@ -25,14 +26,14 @@ import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader; +import org.springframework.beans.factory.support.AbstractBeanDefinitionReader; import org.springframework.beans.factory.support.BeanDefinitionReader; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; import org.springframework.context.annotation.AnnotatedBeanDefinitionReader; import org.springframework.context.annotation.ClassPathBeanDefinitionScanner; -import org.springframework.core.annotation.MergedAnnotations; -import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; +import org.springframework.core.SpringProperties; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; @@ -41,9 +42,9 @@ import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.core.type.filter.AbstractTypeHierarchyTraversingFilter; import org.springframework.core.type.filter.TypeFilter; -import org.springframework.stereotype.Component; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** @@ -53,17 +54,22 @@ * {@link SpringApplication} for the types of sources that are supported. * * @author Phillip Webb + * @author Vladislav Kisel + * @author Sebastien Deleuze * @see #setBeanNameGenerator(BeanNameGenerator) */ class BeanDefinitionLoader { + // Static final field to facilitate code removal by Graal + private static final boolean XML_ENABLED = !SpringProperties.getFlag("spring.xml.ignore"); + private final Object[] sources; private final AnnotatedBeanDefinitionReader annotatedReader; - private final XmlBeanDefinitionReader xmlReader; + private final AbstractBeanDefinitionReader xmlReader; - private BeanDefinitionReader groovyReader; + private final BeanDefinitionReader groovyReader; private final ClassPathBeanDefinitionScanner scanner; @@ -80,10 +86,8 @@ class BeanDefinitionLoader { Assert.notEmpty(sources, "Sources must not be empty"); this.sources = sources; this.annotatedReader = new AnnotatedBeanDefinitionReader(registry); - this.xmlReader = new XmlBeanDefinitionReader(registry); - if (isGroovyPresent()) { - this.groovyReader = new GroovyBeanDefinitionReader(registry); - } + this.xmlReader = (XML_ENABLED ? new XmlBeanDefinitionReader(registry) : null); + this.groovyReader = (isGroovyPresent() ? new GroovyBeanDefinitionReader(registry) : null); this.scanner = new ClassPathBeanDefinitionScanner(registry); this.scanner.addExcludeFilter(new ClassExcludeFilter(sources)); } @@ -94,8 +98,10 @@ class BeanDefinitionLoader { */ void setBeanNameGenerator(BeanNameGenerator beanNameGenerator) { this.annotatedReader.setBeanNameGenerator(beanNameGenerator); - this.xmlReader.setBeanNameGenerator(beanNameGenerator); this.scanner.setBeanNameGenerator(beanNameGenerator); + if (this.xmlReader != null) { + this.xmlReader.setBeanNameGenerator(beanNameGenerator); + } } /** @@ -104,8 +110,10 @@ void setBeanNameGenerator(BeanNameGenerator beanNameGenerator) { */ void setResourceLoader(ResourceLoader resourceLoader) { this.resourceLoader = resourceLoader; - this.xmlReader.setResourceLoader(resourceLoader); this.scanner.setResourceLoader(resourceLoader); + if (this.xmlReader != null) { + this.xmlReader.setResourceLoader(resourceLoader); + } } /** @@ -114,103 +122,107 @@ void setResourceLoader(ResourceLoader resourceLoader) { */ void setEnvironment(ConfigurableEnvironment environment) { this.annotatedReader.setEnvironment(environment); - this.xmlReader.setEnvironment(environment); this.scanner.setEnvironment(environment); + if (this.xmlReader != null) { + this.xmlReader.setEnvironment(environment); + } } /** * Load the sources into the reader. - * @return the number of loaded beans */ - int load() { - int count = 0; + void load() { for (Object source : this.sources) { - count += load(source); + load(source); } - return count; } - private int load(Object source) { + private void load(Object source) { Assert.notNull(source, "Source must not be null"); if (source instanceof Class) { - return load((Class) source); + load((Class) source); + return; } if (source instanceof Resource) { - return load((Resource) source); + load((Resource) source); + return; } if (source instanceof Package) { - return load((Package) source); + load((Package) source); + return; } if (source instanceof CharSequence) { - return load((CharSequence) source); + load((CharSequence) source); + return; } throw new IllegalArgumentException("Invalid source type " + source.getClass()); } - private int load(Class source) { + private void load(Class source) { if (isGroovyPresent() && GroovyBeanDefinitionSource.class.isAssignableFrom(source)) { // Any GroovyLoaders added in beans{} DSL can contribute beans here GroovyBeanDefinitionSource loader = BeanUtils.instantiateClass(source, GroovyBeanDefinitionSource.class); - load(loader); + ((GroovyBeanDefinitionReader) this.groovyReader).beans(loader.getBeans()); } - if (isComponent(source)) { + if (isEligible(source)) { this.annotatedReader.register(source); - return 1; } - return 0; - } - - private int load(GroovyBeanDefinitionSource source) { - int before = this.xmlReader.getRegistry().getBeanDefinitionCount(); - ((GroovyBeanDefinitionReader) this.groovyReader).beans(source.getBeans()); - int after = this.xmlReader.getRegistry().getBeanDefinitionCount(); - return after - before; } - private int load(Resource source) { + private void load(Resource source) { if (source.getFilename().endsWith(".groovy")) { if (this.groovyReader == null) { throw new BeanDefinitionStoreException("Cannot load Groovy beans without Groovy on classpath"); } - return this.groovyReader.loadBeanDefinitions(source); + this.groovyReader.loadBeanDefinitions(source); + } + else { + if (this.xmlReader == null) { + throw new BeanDefinitionStoreException("Cannot load XML bean definitions when XML support is disabled"); + } + this.xmlReader.loadBeanDefinitions(source); } - return this.xmlReader.loadBeanDefinitions(source); } - private int load(Package source) { - return this.scanner.scan(source.getName()); + private void load(Package source) { + this.scanner.scan(source.getName()); } - private int load(CharSequence source) { - String resolvedSource = this.xmlReader.getEnvironment().resolvePlaceholders(source.toString()); + private void load(CharSequence source) { + String resolvedSource = this.scanner.getEnvironment().resolvePlaceholders(source.toString()); // Attempt as a Class try { - return load(ClassUtils.forName(resolvedSource, null)); + load(ClassUtils.forName(resolvedSource, null)); + return; } catch (IllegalArgumentException | ClassNotFoundException ex) { // swallow exception and continue } - // Attempt as resources - Resource[] resources = findResources(resolvedSource); - int loadCount = 0; - boolean atLeastOneResourceExists = false; - for (Resource resource : resources) { - if (isLoadCandidate(resource)) { - atLeastOneResourceExists = true; - loadCount += load(resource); - } - } - if (atLeastOneResourceExists) { - return loadCount; + // Attempt as Resources + if (loadAsResources(resolvedSource)) { + return; } // Attempt as package Package packageResource = findPackage(resolvedSource); if (packageResource != null) { - return load(packageResource); + load(packageResource); + return; } throw new IllegalArgumentException("Invalid source '" + resolvedSource + "'"); } + private boolean loadAsResources(String resolvedSource) { + boolean foundCandidate = false; + Resource[] resources = findResources(resolvedSource); + for (Resource resource : resources) { + if (isLoadCandidate(resource)) { + foundCandidate = true; + load(resource); + } + } + return foundCandidate; + } + private boolean isGroovyPresent() { return ClassUtils.isPresent("groovy.lang.MetaClass", null); } @@ -273,16 +285,23 @@ private Package findPackage(CharSequence source) { return Package.getPackage(source.toString()); } - private boolean isComponent(Class type) { - // This has to be a bit of a guess. The only way to be sure that this type is - // eligible is to make a bean definition out of it and try to instantiate it. - if (MergedAnnotations.from(type, SearchStrategy.TYPE_HIERARCHY).isPresent(Component.class)) { - return true; - } - // Nested anonymous classes are not eligible for registration, nor are groovy - // closures - return !type.getName().matches(".*\\$_.*closure.*") && !type.isAnonymousClass() - && type.getConstructors() != null && type.getConstructors().length != 0; + /** + * Check whether the bean is eligible for registration. + * @param type candidate bean type + * @return true if the given bean type is eligible for registration, i.e. not a groovy + * closure nor an anonymous class + */ + private boolean isEligible(Class type) { + return !(type.isAnonymousClass() || isGroovyClosure(type) || hasNoConstructors(type)); + } + + private boolean isGroovyClosure(Class type) { + return type.getName().matches(".*\\$_.*closure.*"); + } + + private boolean hasNoConstructors(Class type) { + Constructor[] constructors = type.getDeclaredConstructors(); + return ObjectUtils.isEmpty(constructors); } /** diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/BootstrapContext.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/BootstrapContext.java new file mode 100644 index 000000000000..197620f1b374 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/BootstrapContext.java @@ -0,0 +1,87 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot; + +import java.util.function.Supplier; + +import org.springframework.context.ApplicationContext; +import org.springframework.core.env.Environment; + +/** + * A simple bootstrap context that is available during startup and {@link Environment} + * post-processing up to the point that the {@link ApplicationContext} is prepared. + *

    + * Provides lazy access to singletons that may be expensive to create, or need to be + * shared before the {@link ApplicationContext} is available. + * + * @author Phillip Webb + * @since 2.4.0 + */ +public interface BootstrapContext { + + /** + * Return an instance from the context if the type has been registered. The instance + * will be created it if it hasn't been accessed previously. + * @param the instance type + * @param type the instance type + * @return the instance managed by the context + * @throws IllegalStateException if the type has not been registered + */ + T get(Class type) throws IllegalStateException; + + /** + * Return an instance from the context if the type has been registered. The instance + * will be created it if it hasn't been accessed previously. + * @param the instance type + * @param type the instance type + * @param other the instance to use if the type has not been registered + * @return the instance + */ + T getOrElse(Class type, T other); + + /** + * Return an instance from the context if the type has been registered. The instance + * will be created it if it hasn't been accessed previously. + * @param the instance type + * @param type the instance type + * @param other a supplier for the instance to use if the type has not been registered + * @return the instance + */ + T getOrElseSupply(Class type, Supplier other); + + /** + * Return an instance from the context if the type has been registered. The instance + * will be created it if it hasn't been accessed previously. + * @param the instance type + * @param the exception to throw if the type is not registered + * @param type the instance type + * @param exceptionSupplier the supplier which will return the exception to be thrown + * @return the instance managed by the context + * @throws X if the type has not been registered + * @throws IllegalStateException if the type has not been registered + */ + T getOrElseThrow(Class type, Supplier exceptionSupplier) throws X; + + /** + * Return if a registration exists for the given type. + * @param the instance type + * @param type the instance type + * @return {@code true} if the type has already been registered + */ + boolean isRegistered(Class type); + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/BootstrapContextClosedEvent.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/BootstrapContextClosedEvent.java new file mode 100644 index 000000000000..2be023e4731c --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/BootstrapContextClosedEvent.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot; + +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ConfigurableApplicationContext; + +/** + * {@link ApplicationEvent} published by a {@link BootstrapContext} when it's closed. + * + * @author Phillip Webb + * @since 2.4.0 + * @see BootstrapRegistry#addCloseListener(org.springframework.context.ApplicationListener) + */ +public class BootstrapContextClosedEvent extends ApplicationEvent { + + private final ConfigurableApplicationContext applicationContext; + + BootstrapContextClosedEvent(BootstrapContext source, ConfigurableApplicationContext applicationContext) { + super(source); + this.applicationContext = applicationContext; + } + + /** + * Return the {@link BootstrapContext} that was closed. + * @return the bootstrap context + */ + public BootstrapContext getBootstrapContext() { + return (BootstrapContext) this.source; + } + + /** + * Return the prepared application context. + * @return the application context + */ + public ConfigurableApplicationContext getApplicationContext() { + return this.applicationContext; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/BootstrapRegistry.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/BootstrapRegistry.java new file mode 100644 index 000000000000..c3637c652c15 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/BootstrapRegistry.java @@ -0,0 +1,185 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot; + +import java.util.function.Supplier; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; +import org.springframework.core.env.Environment; +import org.springframework.util.Assert; + +/** + * A simple object registry that is available during startup and {@link Environment} + * post-processing up to the point that the {@link ApplicationContext} is prepared. + *

    + * Can be used to register instances that may be expensive to create, or need to be shared + * before the {@link ApplicationContext} is available. + *

    + * The registry uses {@link Class} as a key, meaning that only a single instance of a + * given type can be stored. + *

    + * The {@link #addCloseListener(ApplicationListener)} method can be used to add a listener + * that can perform actions when {@link BootstrapContext} has been closed and the + * {@link ApplicationContext} is fully prepared. For example, an instance may choose to + * register itself as a regular Spring bean so that it is available for the application to + * use. + * + * @author Phillip Webb + * @since 2.4.0 + * @see BootstrapContext + * @see ConfigurableBootstrapContext + */ +public interface BootstrapRegistry { + + /** + * Register a specific type with the registry. If the specified type has already been + * registered and has not been obtained as a {@link Scope#SINGLETON singleton}, it + * will be replaced. + * @param the instance type + * @param type the instance type + * @param instanceSupplier the instance supplier + */ + void register(Class type, InstanceSupplier instanceSupplier); + + /** + * Register a specific type with the registry if one is not already present. + * @param the instance type + * @param type the instance type + * @param instanceSupplier the instance supplier + */ + void registerIfAbsent(Class type, InstanceSupplier instanceSupplier); + + /** + * Return if a registration exists for the given type. + * @param the instance type + * @param type the instance type + * @return {@code true} if the type has already been registered + */ + boolean isRegistered(Class type); + + /** + * Return any existing {@link InstanceSupplier} for the given type. + * @param the instance type + * @param type the instance type + * @return the registered {@link InstanceSupplier} or {@code null} + */ + InstanceSupplier getRegisteredInstanceSupplier(Class type); + + /** + * Add an {@link ApplicationListener} that will be called with a + * {@link BootstrapContextClosedEvent} when the {@link BootstrapContext} is closed and + * the {@link ApplicationContext} has been prepared. + * @param listener the listener to add + */ + void addCloseListener(ApplicationListener listener); + + /** + * Supplier used to provide the actual instance when needed. + * + * @param the instance type + * @see Scope + */ + @FunctionalInterface + interface InstanceSupplier { + + /** + * Factory method used to create the instance when needed. + * @param context the {@link BootstrapContext} which may be used to obtain other + * bootstrap instances. + * @return the instance + */ + T get(BootstrapContext context); + + /** + * Return the scope of the supplied instance. + * @return the scope + * @since 2.4.2 + */ + default Scope getScope() { + return Scope.SINGLETON; + } + + /** + * Return a new {@link InstanceSupplier} with an updated {@link Scope}. + * @param scope the new scope + * @return a new {@link InstanceSupplier} instance with the new scope + * @since 2.4.2 + */ + default InstanceSupplier withScope(Scope scope) { + Assert.notNull(scope, "Scope must not be null"); + InstanceSupplier parent = this; + return new InstanceSupplier() { + + @Override + public T get(BootstrapContext context) { + return parent.get(context); + } + + @Override + public Scope getScope() { + return scope; + } + + }; + } + + /** + * Factory method that can be used to create an {@link InstanceSupplier} for a + * given instance. + * @param the instance type + * @param instance the instance + * @return a new {@link InstanceSupplier} + */ + static InstanceSupplier of(T instance) { + return (registry) -> instance; + } + + /** + * Factory method that can be used to create an {@link InstanceSupplier} from a + * {@link Supplier}. + * @param the instance type + * @param supplier the supplier that will provide the instance + * @return a new {@link InstanceSupplier} + */ + static InstanceSupplier from(Supplier supplier) { + return (registry) -> (supplier != null) ? supplier.get() : null; + } + + } + + /** + * The scope of a instance. + * @since 2.4.2 + */ + enum Scope { + + /** + * A singleton instance. The {@link InstanceSupplier} will be called only once and + * the same instance will be returned each time. + */ + SINGLETON, + + /** + * A prototype instance. The {@link InstanceSupplier} will be called whenver an + * instance is needed. + */ + PROTOTYPE + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/BootstrapRegistryInitializer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/BootstrapRegistryInitializer.java new file mode 100644 index 000000000000..496341f0052c --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/BootstrapRegistryInitializer.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot; + +/** + * Callback interface that can be used to initialize a {@link BootstrapRegistry} before it + * is used. + * + * @author Phillip Webb + * @since 2.4.5 + * @see SpringApplication#addBootstrapRegistryInitializer(BootstrapRegistryInitializer) + * @see BootstrapRegistry + */ +@FunctionalInterface +public interface BootstrapRegistryInitializer { + + /** + * Initialize the given {@link BootstrapRegistry} with any required registrations. + * @param registry the registry to initialize + */ + void initialize(BootstrapRegistry registry); + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/Bootstrapper.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/Bootstrapper.java new file mode 100644 index 000000000000..b6776fc362d1 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/Bootstrapper.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot; + +/** + * Callback interface that can be used to initialize a {@link BootstrapRegistry} before it + * is used. + * + * @author Phillip Webb + * @since 2.4.0 + * @see SpringApplication#addBootstrapper(Bootstrapper) + * @see BootstrapRegistry + * @deprecated since 2.4.5 for removal in 2.6 in favor of + * {@link BootstrapRegistryInitializer} + */ +@Deprecated +public interface Bootstrapper { + + /** + * Initialize the given {@link BootstrapRegistry} with any required registrations. + * @param registry the registry to initialize + * @since 2.4.4 + */ + default void initialize(BootstrapRegistry registry) { + intitialize(registry); + } + + /** + * Initialize the given {@link BootstrapRegistry} with any required registrations. + * @param registry the registry to initialize + * @deprecated since 2.4.4 for removal in 2.6 in favor of + * {@link Bootstrapper#initialize(BootstrapRegistry)} + */ + @Deprecated + void intitialize(BootstrapRegistry registry); + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ConfigurableBootstrapContext.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ConfigurableBootstrapContext.java new file mode 100644 index 000000000000..432c1a707841 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ConfigurableBootstrapContext.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot; + +/** + * A {@link BootstrapContext} that also provides configuration methods via the + * {@link BootstrapRegistry} interface. + * + * @author Phillip Webb + * @since 2.4.0 + * @see BootstrapRegistry + * @see BootstrapContext + * @see DefaultBootstrapContext + */ +public interface ConfigurableBootstrapContext extends BootstrapRegistry, BootstrapContext { + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/DefaultBootstrapContext.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/DefaultBootstrapContext.java new file mode 100644 index 000000000000..eec1b2a714cd --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/DefaultBootstrapContext.java @@ -0,0 +1,136 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.event.ApplicationEventMulticaster; +import org.springframework.context.event.SimpleApplicationEventMulticaster; +import org.springframework.util.Assert; + +/** + * Default {@link ConfigurableBootstrapContext} implementation. + * + * @author Phillip Webb + * @since 2.4.0 + */ +public class DefaultBootstrapContext implements ConfigurableBootstrapContext { + + private final Map, InstanceSupplier> instanceSuppliers = new HashMap<>(); + + private final Map, Object> instances = new HashMap<>(); + + private final ApplicationEventMulticaster events = new SimpleApplicationEventMulticaster(); + + @Override + public void register(Class type, InstanceSupplier instanceSupplier) { + register(type, instanceSupplier, true); + } + + @Override + public void registerIfAbsent(Class type, InstanceSupplier instanceSupplier) { + register(type, instanceSupplier, false); + } + + private void register(Class type, InstanceSupplier instanceSupplier, boolean replaceExisting) { + Assert.notNull(type, "Type must not be null"); + Assert.notNull(instanceSupplier, "InstanceSupplier must not be null"); + synchronized (this.instanceSuppliers) { + boolean alreadyRegistered = this.instanceSuppliers.containsKey(type); + if (replaceExisting || !alreadyRegistered) { + Assert.state(!this.instances.containsKey(type), () -> type.getName() + " has already been created"); + this.instanceSuppliers.put(type, instanceSupplier); + } + } + } + + @Override + public boolean isRegistered(Class type) { + synchronized (this.instanceSuppliers) { + return this.instanceSuppliers.containsKey(type); + } + } + + @Override + @SuppressWarnings("unchecked") + public InstanceSupplier getRegisteredInstanceSupplier(Class type) { + synchronized (this.instanceSuppliers) { + return (InstanceSupplier) this.instanceSuppliers.get(type); + } + } + + @Override + public void addCloseListener(ApplicationListener listener) { + this.events.addApplicationListener(listener); + } + + @Override + public T get(Class type) throws IllegalStateException { + return getOrElseThrow(type, () -> new IllegalStateException(type.getName() + " has not been registered")); + } + + @Override + public T getOrElse(Class type, T other) { + return getOrElseSupply(type, () -> other); + } + + @Override + public T getOrElseSupply(Class type, Supplier other) { + synchronized (this.instanceSuppliers) { + InstanceSupplier instanceSupplier = this.instanceSuppliers.get(type); + return (instanceSupplier != null) ? getInstance(type, instanceSupplier) : other.get(); + } + } + + @Override + public T getOrElseThrow(Class type, Supplier exceptionSupplier) throws X { + synchronized (this.instanceSuppliers) { + InstanceSupplier instanceSupplier = this.instanceSuppliers.get(type); + if (instanceSupplier == null) { + throw exceptionSupplier.get(); + } + return getInstance(type, instanceSupplier); + } + } + + @SuppressWarnings("unchecked") + private T getInstance(Class type, InstanceSupplier instanceSupplier) { + T instance = (T) this.instances.get(type); + if (instance == null) { + instance = (T) instanceSupplier.get(this); + if (instanceSupplier.getScope() == Scope.SINGLETON) { + this.instances.put(type, instance); + } + } + return instance; + } + + /** + * Method to be called when {@link BootstrapContext} is closed and the + * {@link ApplicationContext} is prepared. + * @param applicationContext the prepared context + */ + public void close(ConfigurableApplicationContext applicationContext) { + this.events.multicastEvent(new BootstrapContextClosedEvent(this, applicationContext)); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/DefaultPropertiesPropertySource.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/DefaultPropertiesPropertySource.java new file mode 100644 index 000000000000..c7c47a115080 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/DefaultPropertiesPropertySource.java @@ -0,0 +1,131 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySource; +import org.springframework.util.CollectionUtils; + +/** + * {@link MapPropertySource} containing default properties contributed directly to a + * {@code SpringApplication}. By convention, the {@link DefaultPropertiesPropertySource} + * is always the last property source in the {@link Environment}. + * + * @author Phillip Webb + * @since 2.4.0 + */ +public class DefaultPropertiesPropertySource extends MapPropertySource { + + /** + * The name of the 'default properties' property source. + */ + public static final String NAME = "defaultProperties"; + + /** + * Create a new {@link DefaultPropertiesPropertySource} with the given {@code Map} + * source. + * @param source the source map + */ + public DefaultPropertiesPropertySource(Map source) { + super(NAME, source); + } + + /** + * Return {@code true} if the given source is named 'defaultProperties'. + * @param propertySource the property source to check + * @return {@code true} if the name matches + */ + public static boolean hasMatchingName(PropertySource propertySource) { + return (propertySource != null) && propertySource.getName().equals(NAME); + } + + /** + * Create a consume a new {@link DefaultPropertiesPropertySource} instance if the + * provided source is not empty. + * @param source the {@code Map} source + * @param action the action used to consume the + * {@link DefaultPropertiesPropertySource} + */ + public static void ifNotEmpty(Map source, Consumer action) { + if (!CollectionUtils.isEmpty(source) && action != null) { + action.accept(new DefaultPropertiesPropertySource(source)); + } + } + + /** + * Add a new {@link DefaultPropertiesPropertySource} or merge with an existing one. + * @param source the {@code Map} source + * @param sources the existing sources + * @since 2.4.4 + */ + public static void addOrMerge(Map source, MutablePropertySources sources) { + if (!CollectionUtils.isEmpty(source)) { + Map resultingSource = new HashMap<>(); + DefaultPropertiesPropertySource propertySource = new DefaultPropertiesPropertySource(resultingSource); + if (sources.contains(NAME)) { + mergeIfPossible(source, sources, resultingSource); + sources.replace(NAME, propertySource); + } + else { + resultingSource.putAll(source); + sources.addLast(propertySource); + } + } + } + + @SuppressWarnings("unchecked") + private static void mergeIfPossible(Map source, MutablePropertySources sources, + Map resultingSource) { + PropertySource existingSource = sources.get(NAME); + if (existingSource != null) { + Object underlyingSource = existingSource.getSource(); + if (underlyingSource instanceof Map) { + resultingSource.putAll((Map) underlyingSource); + } + resultingSource.putAll(source); + } + } + + /** + * Move the 'defaultProperties' property source so that it's the last source in the + * given {@link ConfigurableEnvironment}. + * @param environment the environment to update + */ + public static void moveToEnd(ConfigurableEnvironment environment) { + moveToEnd(environment.getPropertySources()); + } + + /** + * Move the 'defaultProperties' property source so that it's the last source in the + * given {@link MutablePropertySources}. + * @param propertySources the property sources to update + */ + public static void moveToEnd(MutablePropertySources propertySources) { + PropertySource propertySource = propertySources.remove(NAME); + if (propertySource != null) { + propertySources.addLast(propertySource); + } + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/EnvironmentConverter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/EnvironmentConverter.java index 1bd0eb8864ba..0a79361a084e 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/EnvironmentConverter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/EnvironmentConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -87,7 +87,7 @@ private StandardEnvironment convertEnvironment(ConfigurableEnvironment environme private StandardEnvironment createEnvironment(Class type) { try { - return type.newInstance(); + return type.getDeclaredConstructor().newInstance(); } catch (Exception ex) { return new StandardEnvironment(); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/LazyInitializationBeanFactoryPostProcessor.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/LazyInitializationBeanFactoryPostProcessor.java index 6eb7a509bb08..668697d4c709 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/LazyInitializationBeanFactoryPostProcessor.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/LazyInitializationBeanFactoryPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,12 @@ package org.springframework.boot; +import java.util.ArrayList; import java.util.Collection; import org.springframework.beans.BeansException; import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; @@ -30,6 +32,11 @@ * {@link BeanFactoryPostProcessor} to set lazy-init on bean definitions that are not * {@link LazyInitializationExcludeFilter excluded} and have not already had a value * explicitly set. + *

    + * Note that {@link SmartInitializingSingleton SmartInitializingSingletons} are + * automatically excluded from lazy initialization to ensure that their + * {@link SmartInitializingSingleton#afterSingletonsInstantiated() callback method} is + * invoked. * * @author Andy Wilkinson * @author Madhura Bhave @@ -42,9 +49,7 @@ public final class LazyInitializationBeanFactoryPostProcessor implements BeanFac @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { - // Take care not to force the eager init of factory beans when getting filters - Collection filters = beanFactory - .getBeansOfType(LazyInitializationExcludeFilter.class, false, false).values(); + Collection filters = getFilters(beanFactory); for (String beanName : beanFactory.getBeanDefinitionNames()) { BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName); if (beanDefinition instanceof AbstractBeanDefinition) { @@ -53,6 +58,14 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) } } + private Collection getFilters(ConfigurableListableBeanFactory beanFactory) { + // Take care not to force the eager init of factory beans when getting filters + ArrayList filters = new ArrayList<>( + beanFactory.getBeansOfType(LazyInitializationExcludeFilter.class, false, false).values()); + filters.add(LazyInitializationExcludeFilter.forBeanTypes(SmartInitializingSingleton.class)); + return filters; + } + private void postProcess(ConfigurableListableBeanFactory beanFactory, Collection filters, String beanName, AbstractBeanDefinition beanDefinition) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/LazyInitializationExcludeFilter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/LazyInitializationExcludeFilter.java index 55893ad02031..253f465a5dfd 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/LazyInitializationExcludeFilter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/LazyInitializationExcludeFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,13 +29,12 @@ * beans). Adding an instance of this filter to the application context can be used for * these edge cases. *

    - * A typical example would be something like this: - *

    - *

    
    + * A typical example would be something like this: 
      * @Bean
      * public static LazyInitializationExcludeFilter integrationLazyInitializationExcludeFilter() {
      *   return LazyInitializationExcludeFilter.forBeanTypes(IntegrationFlow.class);
    - * }
    + * } + *
    *

    * NOTE: Beans of this type will be instantiated very early in the spring application * lifecycle so they should generally be declared static and not have any dependencies. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java index 5ca1aa695eb3..643d724915bc 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,18 +17,17 @@ package org.springframework.boot; import java.lang.reflect.Constructor; -import java.security.AccessControlException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; +import java.util.stream.Collectors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -47,7 +46,8 @@ import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.boot.convert.ApplicationConversionService; -import org.springframework.boot.web.reactive.context.StandardReactiveWebEnvironment; +import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext; +import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ApplicationListener; @@ -60,13 +60,10 @@ import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.GenericTypeResolver; import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.support.ConfigurableConversionService; import org.springframework.core.env.CommandLinePropertySource; import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; -import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertySource; import org.springframework.core.env.SimpleCommandLinePropertySource; @@ -74,6 +71,7 @@ import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.core.metrics.ApplicationStartup; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -81,7 +79,6 @@ import org.springframework.util.ReflectionUtils; import org.springframework.util.StopWatch; import org.springframework.util.StringUtils; -import org.springframework.web.context.support.StandardServletEnvironment; /** * Class that can be used to bootstrap and launch a Spring application from a Java main @@ -163,21 +160,30 @@ public class SpringApplication { /** * The class name of application context that will be used by default for non-web * environments. + * @deprecated since 2.4.0 for removal in 2.6.0 in favor of using a + * {@link ApplicationContextFactory} */ + @Deprecated public static final String DEFAULT_CONTEXT_CLASS = "org.springframework.context." + "annotation.AnnotationConfigApplicationContext"; /** * The class name of application context that will be used by default for web * environments. + * @deprecated since 2.4.0 for removal in 2.6.0 in favor of using an + * {@link ApplicationContextFactory} */ + @Deprecated public static final String DEFAULT_SERVLET_WEB_CONTEXT_CLASS = "org.springframework.boot." + "web.servlet.context.AnnotationConfigServletWebServerApplicationContext"; /** * The class name of application context that will be used by default for reactive web * environments. + * @deprecated since 2.4.0 for removal in 2.6.0 in favor of using an + * {@link ApplicationContextFactory} */ + @Deprecated public static final String DEFAULT_REACTIVE_WEB_CONTEXT_CLASS = "org.springframework." + "boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext"; @@ -195,6 +201,8 @@ public class SpringApplication { private static final Log logger = LogFactory.getLog(SpringApplication.class); + static final SpringApplicationShutdownHook shutdownHook = new SpringApplicationShutdownHook(); + private Set> primarySources; private Set sources = new LinkedHashSet<>(); @@ -217,8 +225,6 @@ public class SpringApplication { private ConfigurableEnvironment environment; - private Class applicationContextClass; - private WebApplicationType webApplicationType; private boolean headless = true; @@ -231,7 +237,9 @@ public class SpringApplication { private Map defaultProperties; - private Set additionalProfiles = new HashSet<>(); + private List bootstrapRegistryInitializers; + + private Set additionalProfiles = Collections.emptySet(); private boolean allowBeanDefinitionOverriding; @@ -239,6 +247,12 @@ public class SpringApplication { private boolean lazyInitialization = false; + private String environmentPrefix; + + private ApplicationContextFactory applicationContextFactory = ApplicationContextFactory.DEFAULT; + + private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT; + /** * Create a new {@link SpringApplication} instance. The application context will load * beans from the specified primary sources (see {@link SpringApplication class-level} @@ -269,11 +283,22 @@ public SpringApplication(ResourceLoader resourceLoader, Class... primarySourc Assert.notNull(primarySources, "PrimarySources must not be null"); this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources)); this.webApplicationType = WebApplicationType.deduceFromClasspath(); + this.bootstrapRegistryInitializers = getBootstrapRegistryInitializersFromSpringFactories(); setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); this.mainApplicationClass = deduceMainApplicationClass(); } + @SuppressWarnings("deprecation") + private List getBootstrapRegistryInitializersFromSpringFactories() { + ArrayList initializers = new ArrayList<>(); + getSpringFactoriesInstances(Bootstrapper.class).stream() + .map((bootstrapper) -> ((BootstrapRegistryInitializer) bootstrapper::initialize)) + .forEach(initializers::add); + initializers.addAll(getSpringFactoriesInstances(BootstrapRegistryInitializer.class)); + return initializers; + } + private Class deduceMainApplicationClass() { try { StackTraceElement[] stackTrace = new RuntimeException().getStackTrace(); @@ -298,20 +323,19 @@ private Class deduceMainApplicationClass() { public ConfigurableApplicationContext run(String... args) { StopWatch stopWatch = new StopWatch(); stopWatch.start(); + DefaultBootstrapContext bootstrapContext = createBootstrapContext(); ConfigurableApplicationContext context = null; - Collection exceptionReporters = new ArrayList<>(); configureHeadlessProperty(); SpringApplicationRunListeners listeners = getRunListeners(args); - listeners.starting(); + listeners.starting(bootstrapContext, this.mainApplicationClass); try { ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); - ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); + ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments); configureIgnoreBeanInfo(environment); Banner printedBanner = printBanner(environment); context = createApplicationContext(); - exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class, - new Class[] { ConfigurableApplicationContext.class }, context); - prepareContext(context, environment, listeners, applicationArguments, printedBanner); + context.setApplicationStartup(this.applicationStartup); + prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner); refreshContext(context); afterRefresh(context, applicationArguments); stopWatch.stop(); @@ -322,7 +346,7 @@ public ConfigurableApplicationContext run(String... args) { callRunners(context, applicationArguments); } catch (Throwable ex) { - handleRunFailure(context, ex, exceptionReporters, listeners); + handleRunFailure(context, ex, listeners); throw new IllegalStateException(ex); } @@ -330,45 +354,69 @@ public ConfigurableApplicationContext run(String... args) { listeners.running(context); } catch (Throwable ex) { - handleRunFailure(context, ex, exceptionReporters, null); + handleRunFailure(context, ex, null); throw new IllegalStateException(ex); } return context; } + private DefaultBootstrapContext createBootstrapContext() { + DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext(); + this.bootstrapRegistryInitializers.forEach((initializer) -> initializer.initialize(bootstrapContext)); + return bootstrapContext; + } + private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, - ApplicationArguments applicationArguments) { + DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) { // Create and configure the environment ConfigurableEnvironment environment = getOrCreateEnvironment(); configureEnvironment(environment, applicationArguments.getSourceArgs()); ConfigurationPropertySources.attach(environment); - listeners.environmentPrepared(environment); + listeners.environmentPrepared(bootstrapContext, environment); + DefaultPropertiesPropertySource.moveToEnd(environment); + Assert.state(!environment.containsProperty("spring.main.environment-prefix"), + "Environment prefix cannot be set via properties."); bindToSpringApplication(environment); if (!this.isCustomEnvironment) { - environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment, - deduceEnvironmentClass()); + environment = convertEnvironment(environment); } ConfigurationPropertySources.attach(environment); return environment; } + /** + * Convert the given {@link ConfigurableEnvironment environment} to an application + * environment that doesn't attempt to resolve profile properties directly. + * @param environment the environment to convert + * @return the converted environment + * @since 2.5.7 + * @deprecated since 2.5.8 for removal in 2.7.0 + */ + @Deprecated + public StandardEnvironment convertEnvironment(ConfigurableEnvironment environment) { + return new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment, + deduceEnvironmentClass()); + } + private Class deduceEnvironmentClass() { switch (this.webApplicationType) { case SERVLET: - return StandardServletEnvironment.class; + return ApplicationServletEnvironment.class; case REACTIVE: - return StandardReactiveWebEnvironment.class; + return ApplicationReactiveWebEnvironment.class; default: - return StandardEnvironment.class; + return ApplicationEnvironment.class; } } - private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment, - SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) { + private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context, + ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, + ApplicationArguments applicationArguments, Banner printedBanner) { context.setEnvironment(environment); postProcessApplicationContext(context); applyInitializers(context); listeners.contextPrepared(context); + bootstrapContext.close(context); if (this.logStartupInfo) { logStartupInfo(context.getParent() == null); logStartupProfileInfo(context); @@ -394,15 +442,10 @@ private void prepareContext(ConfigurableApplicationContext context, Configurable } private void refreshContext(ConfigurableApplicationContext context) { - refresh(context); if (this.registerShutdownHook) { - try { - context.registerShutdownHook(); - } - catch (AccessControlException ex) { - // Not allowed in some environments. - } + shutdownHook.registerApplicationContext(context); } + refresh(context); } private void configureHeadlessProperty() { @@ -413,7 +456,8 @@ private void configureHeadlessProperty() { private SpringApplicationRunListeners getRunListeners(String[] args) { Class[] types = new Class[] { SpringApplication.class, String[].class }; return new SpringApplicationRunListeners(logger, - getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args)); + getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args), + this.applicationStartup); } private Collection getSpringFactoriesInstances(Class type) { @@ -454,11 +498,11 @@ private ConfigurableEnvironment getOrCreateEnvironment() { } switch (this.webApplicationType) { case SERVLET: - return new StandardServletEnvironment(); + return new ApplicationServletEnvironment(); case REACTIVE: - return new StandardReactiveWebEnvironment(); + return new ApplicationReactiveWebEnvironment(); default: - return new StandardEnvironment(); + return new ApplicationEnvironment(); } } @@ -475,8 +519,7 @@ private ConfigurableEnvironment getOrCreateEnvironment() { */ protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) { if (this.addConversionService) { - ConversionService conversionService = ApplicationConversionService.getSharedInstance(); - environment.setConversionService((ConfigurableConversionService) conversionService); + environment.setConversionService(new ApplicationConversionService()); } configurePropertySources(environment, args); configureProfiles(environment, args); @@ -491,8 +534,8 @@ protected void configureEnvironment(ConfigurableEnvironment environment, String[ */ protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) { MutablePropertySources sources = environment.getPropertySources(); - if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) { - sources.addLast(new MapPropertySource("defaultProperties", this.defaultProperties)); + if (!CollectionUtils.isEmpty(this.defaultProperties)) { + DefaultPropertiesPropertySource.addOrMerge(this.defaultProperties, sources); } if (this.addCommandLineProperties && args.length > 0) { String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME; @@ -520,9 +563,6 @@ protected void configurePropertySources(ConfigurableEnvironment environment, Str * @see org.springframework.boot.context.config.ConfigFileApplicationListener */ protected void configureProfiles(ConfigurableEnvironment environment, String[] args) { - Set profiles = new LinkedHashSet<>(this.additionalProfiles); - profiles.addAll(Arrays.asList(environment.getActiveProfiles())); - environment.setActiveProfiles(StringUtils.toStringArray(profiles)); } private void configureIgnoreBeanInfo(ConfigurableEnvironment environment) { @@ -550,7 +590,7 @@ private Banner printBanner(ConfigurableEnvironment environment) { return null; } ResourceLoader resourceLoader = (this.resourceLoader != null) ? this.resourceLoader - : new DefaultResourceLoader(getClassLoader()); + : new DefaultResourceLoader(null); SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(resourceLoader, this.banner); if (this.bannerMode == Mode.LOG) { return bannerPrinter.print(environment, this.mainApplicationClass, logger); @@ -560,32 +600,14 @@ private Banner printBanner(ConfigurableEnvironment environment) { /** * Strategy method used to create the {@link ApplicationContext}. By default this - * method will respect any explicitly set application context or application context - * class before falling back to a suitable default. + * method will respect any explicitly set application context class or factory before + * falling back to a suitable default. * @return the application context (not yet refreshed) * @see #setApplicationContextClass(Class) + * @see #setApplicationContextFactory(ApplicationContextFactory) */ protected ConfigurableApplicationContext createApplicationContext() { - Class contextClass = this.applicationContextClass; - if (contextClass == null) { - try { - switch (this.webApplicationType) { - case SERVLET: - contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS); - break; - case REACTIVE: - contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS); - break; - default: - contextClass = Class.forName(DEFAULT_CONTEXT_CLASS); - } - } - catch (ClassNotFoundException ex) { - throw new IllegalStateException( - "Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex); - } - } - return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass); + return this.applicationContextFactory.create(this.webApplicationType); } /** @@ -607,7 +629,7 @@ protected void postProcessApplicationContext(ConfigurableApplicationContext cont } } if (this.addConversionService) { - context.getBeanFactory().setConversionService(ApplicationConversionService.getSharedInstance()); + context.getBeanFactory().setConversionService(context.getEnvironment().getConversionService()); } } @@ -645,19 +667,26 @@ protected void logStartupInfo(boolean isRoot) { protected void logStartupProfileInfo(ConfigurableApplicationContext context) { Log log = getApplicationLog(); if (log.isInfoEnabled()) { - String[] activeProfiles = context.getEnvironment().getActiveProfiles(); + List activeProfiles = quoteProfiles(context.getEnvironment().getActiveProfiles()); if (ObjectUtils.isEmpty(activeProfiles)) { - String[] defaultProfiles = context.getEnvironment().getDefaultProfiles(); - log.info("No active profile set, falling back to default profiles: " - + StringUtils.arrayToCommaDelimitedString(defaultProfiles)); + List defaultProfiles = quoteProfiles(context.getEnvironment().getDefaultProfiles()); + String message = String.format("%s default %s: ", defaultProfiles.size(), + (defaultProfiles.size() <= 1) ? "profile" : "profiles"); + log.info("No active profile set, falling back to " + message + + StringUtils.collectionToDelimitedString(defaultProfiles, ", ")); } else { - log.info("The following profiles are active: " - + StringUtils.arrayToCommaDelimitedString(activeProfiles)); + String message = (activeProfiles.size() == 1) ? "1 profile is active: " + : activeProfiles.size() + " profiles are active: "; + log.info("The following " + message + StringUtils.collectionToDelimitedString(activeProfiles, ", ")); } } } + private List quoteProfiles(String[] profiles) { + return Arrays.stream(profiles).map((profile) -> "\"" + profile + "\"").collect(Collectors.toList()); + } + /** * Returns the {@link Log} for the application. By default will be deduced. * @return the application log @@ -742,9 +771,8 @@ protected BeanDefinitionLoader createBeanDefinitionLoader(BeanDefinitionRegistry * Refresh the underlying {@link ApplicationContext}. * @param applicationContext the application context to refresh */ - protected void refresh(ApplicationContext applicationContext) { - Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext); - ((AbstractApplicationContext) applicationContext).refresh(); + protected void refresh(ConfigurableApplicationContext applicationContext) { + applicationContext.refresh(); } /** @@ -789,7 +817,7 @@ private void callRunner(CommandLineRunner runner, ApplicationArguments args) { } private void handleRunFailure(ConfigurableApplicationContext context, Throwable exception, - Collection exceptionReporters, SpringApplicationRunListeners listeners) { + SpringApplicationRunListeners listeners) { try { try { handleExitCode(context, exception); @@ -798,9 +826,10 @@ private void handleRunFailure(ConfigurableApplicationContext context, Throwable } } finally { - reportFailure(exceptionReporters, exception); + reportFailure(getExceptionReporters(context), exception); if (context != null) { context.close(); + shutdownHook.deregisterFailedApplicationContext(context); } } } @@ -810,6 +839,16 @@ private void handleRunFailure(ConfigurableApplicationContext context, Throwable ReflectionUtils.rethrowRuntimeException(exception); } + private Collection getExceptionReporters(ConfigurableApplicationContext context) { + try { + return getSpringFactoriesInstances(SpringBootExceptionReporter.class, + new Class[] { ConfigurableApplicationContext.class }, context); + } + catch (Throwable ex) { + return Collections.emptyList(); + } + } + private void reportFailure(Collection exceptionReporters, Throwable failure) { try { for (SpringBootExceptionReporter reporter : exceptionReporters) { @@ -966,6 +1005,7 @@ public void setHeadless(boolean headless) { * registered. Defaults to {@code true} to ensure that JVM shutdowns are handled * gracefully. * @param registerShutdownHook if the shutdown hook should be registered + * @see #getShutdownHandlers() */ public void setRegisterShutdownHook(boolean registerShutdownHook) { this.registerShutdownHook = registerShutdownHook; @@ -1017,6 +1057,31 @@ public void setAddConversionService(boolean addConversionService) { this.addConversionService = addConversionService; } + /** + * Adds a {@link Bootstrapper} that can be used to initialize the + * {@link BootstrapRegistry}. + * @param bootstrapper the bootstraper + * @since 2.4.0 + * @deprecated since 2.4.5 for removal in 2.6 in favor of + * {@link #addBootstrapRegistryInitializer(BootstrapRegistryInitializer)} + */ + @Deprecated + public void addBootstrapper(Bootstrapper bootstrapper) { + Assert.notNull(bootstrapper, "Bootstrapper must not be null"); + this.bootstrapRegistryInitializers.add(bootstrapper::initialize); + } + + /** + * Adds {@link BootstrapRegistryInitializer} instances that can be used to initialize + * the {@link BootstrapRegistry}. + * @param bootstrapRegistryInitializer the bootstrap registry initializer to add + * @since 2.4.5 + */ + public void addBootstrapRegistryInitializer(BootstrapRegistryInitializer bootstrapRegistryInitializer) { + Assert.notNull(bootstrapRegistryInitializer, "BootstrapRegistryInitializer must not be null"); + this.bootstrapRegistryInitializers.addAll(Arrays.asList(bootstrapRegistryInitializer)); + } + /** * Set default environment properties which will be used in addition to those in the * existing {@link Environment}. @@ -1043,7 +1108,15 @@ public void setDefaultProperties(Properties defaultProperties) { * @param profiles the additional profiles to set */ public void setAdditionalProfiles(String... profiles) { - this.additionalProfiles = new LinkedHashSet<>(Arrays.asList(profiles)); + this.additionalProfiles = Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(profiles))); + } + + /** + * Return an immutable set of any additional profiles in use. + * @return the additional profiles + */ + public Set getAdditionalProfiles() { + return this.additionalProfiles; } /** @@ -1137,16 +1210,54 @@ public void setResourceLoader(ResourceLoader resourceLoader) { this.resourceLoader = resourceLoader; } + /** + * Return a prefix that should be applied when obtaining configuration properties from + * the system environment. + * @return the environment property prefix + * @since 2.5.0 + */ + public String getEnvironmentPrefix() { + return this.environmentPrefix; + } + + /** + * Set the prefix that should be applied when obtaining configuration properties from + * the system environment. + * @param environmentPrefix the environment property prefix to set + * @since 2.5.0 + */ + public void setEnvironmentPrefix(String environmentPrefix) { + this.environmentPrefix = environmentPrefix; + } + /** * Sets the type of Spring {@link ApplicationContext} that will be created. If not * specified defaults to {@link #DEFAULT_SERVLET_WEB_CONTEXT_CLASS} for web based * applications or {@link AnnotationConfigApplicationContext} for non web based * applications. * @param applicationContextClass the context class to set + * @deprecated since 2.4.0 for removal in 2.6.0 in favor of + * {@link #setApplicationContextFactory(ApplicationContextFactory)} */ + @Deprecated public void setApplicationContextClass(Class applicationContextClass) { - this.applicationContextClass = applicationContextClass; this.webApplicationType = WebApplicationType.deduceFromApplicationContext(applicationContextClass); + this.applicationContextFactory = ApplicationContextFactory.ofContextClass(applicationContextClass); + } + + /** + * Sets the factory that will be called to create the application context. If not set, + * defaults to a factory that will create + * {@link AnnotationConfigServletWebServerApplicationContext} for servlet web + * applications, {@link AnnotationConfigReactiveWebServerApplicationContext} for + * reactive web applications, and {@link AnnotationConfigApplicationContext} for + * non-web applications. + * @param applicationContextFactory the factory for the context + * @since 2.4.0 + */ + public void setApplicationContextFactory(ApplicationContextFactory applicationContextFactory) { + this.applicationContextFactory = (applicationContextFactory != null) ? applicationContextFactory + : ApplicationContextFactory.DEFAULT; } /** @@ -1204,6 +1315,34 @@ public Set> getListeners() { return asUnmodifiableOrderedSet(this.listeners); } + /** + * Set the {@link ApplicationStartup} to use for collecting startup metrics. + * @param applicationStartup the application startup to use + * @since 2.4.0 + */ + public void setApplicationStartup(ApplicationStartup applicationStartup) { + this.applicationStartup = (applicationStartup != null) ? applicationStartup : ApplicationStartup.DEFAULT; + } + + /** + * Returns the {@link ApplicationStartup} used for collecting startup metrics. + * @return the application startup + * @since 2.4.0 + */ + public ApplicationStartup getApplicationStartup() { + return this.applicationStartup; + } + + /** + * Return a {@link SpringApplicationShutdownHandlers} instance that can be used to add + * or remove handlers that perform actions before the JVM is shutdown. + * @return a {@link SpringApplicationShutdownHandlers} instance + * @since 2.5.1 + */ + public static SpringApplicationShutdownHandlers getShutdownHandlers() { + return shutdownHook.getHandlers(); + } + /** * Static helper that can be used to run a {@link SpringApplication} from the * specified source using default settings. @@ -1250,7 +1389,7 @@ public static void main(String[] args) throws Exception { * {@link ExitCodeGenerator}. In the case of multiple exit codes the highest value * will be used (or if all values are negative, the lowest value will be used) * @param context the context to close if possible - * @param exitCodeGenerators exist code generators + * @param exitCodeGenerators exit code generators * @return the outcome (0 if successful) */ public static int exit(ApplicationContext context, ExitCodeGenerator... exitCodeGenerators) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationBannerPrinter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationBannerPrinter.java index 34da198a7e8e..a9502aef6a91 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationBannerPrinter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationBannerPrinter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.boot; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.PrintStream; import java.io.UnsupportedEncodingException; import java.util.ArrayList; @@ -88,8 +89,13 @@ private Banner getBanner(Environment environment) { private Banner getTextBanner(Environment environment) { String location = environment.getProperty(BANNER_LOCATION_PROPERTY, DEFAULT_BANNER_LOCATION); Resource resource = this.resourceLoader.getResource(location); - if (resource.exists()) { - return new ResourceBanner(resource); + try { + if (resource.exists() && !resource.getURL().toExternalForm().contains("liquibase-core")) { + return new ResourceBanner(resource); + } + } + catch (IOException ex) { + // Ignore } return null; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListener.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListener.java index 325febe884ae..70f8092e941c 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListener.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,15 +38,41 @@ public interface SpringApplicationRunListener { /** * Called immediately when the run method has first started. Can be used for very * early initialization. + * @param bootstrapContext the bootstrap context */ + default void starting(ConfigurableBootstrapContext bootstrapContext) { + starting(); + } + + /** + * Called immediately when the run method has first started. Can be used for very + * early initialization. + * @deprecated since 2.4.0 for removal in 2.6.0 in favor of + * {@link #starting(ConfigurableBootstrapContext)} + */ + @Deprecated default void starting() { } + /** + * Called once the environment has been prepared, but before the + * {@link ApplicationContext} has been created. + * @param bootstrapContext the bootstrap context + * @param environment the environment + */ + default void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, + ConfigurableEnvironment environment) { + environmentPrepared(environment); + } + /** * Called once the environment has been prepared, but before the * {@link ApplicationContext} has been created. * @param environment the environment + * @deprecated since 2.4.0 for removal in 2.6.0 in favor of + * {@link #environmentPrepared(ConfigurableBootstrapContext, ConfigurableEnvironment)} */ + @Deprecated default void environmentPrepared(ConfigurableEnvironment environment) { } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListeners.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListeners.java index 55f520dac889..21c26ef8fa5a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListeners.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListeners.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,11 +19,14 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.function.Consumer; import org.apache.commons.logging.Log; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.metrics.ApplicationStartup; +import org.springframework.core.metrics.StartupStep; import org.springframework.util.ReflectionUtils; /** @@ -37,51 +40,51 @@ class SpringApplicationRunListeners { private final List listeners; - SpringApplicationRunListeners(Log log, Collection listeners) { + private final ApplicationStartup applicationStartup; + + SpringApplicationRunListeners(Log log, Collection listeners, + ApplicationStartup applicationStartup) { this.log = log; this.listeners = new ArrayList<>(listeners); + this.applicationStartup = applicationStartup; } - void starting() { - for (SpringApplicationRunListener listener : this.listeners) { - listener.starting(); - } + void starting(ConfigurableBootstrapContext bootstrapContext, Class mainApplicationClass) { + doWithListeners("spring.boot.application.starting", (listener) -> listener.starting(bootstrapContext), + (step) -> { + if (mainApplicationClass != null) { + step.tag("mainApplicationClass", mainApplicationClass.getName()); + } + }); } - void environmentPrepared(ConfigurableEnvironment environment) { - for (SpringApplicationRunListener listener : this.listeners) { - listener.environmentPrepared(environment); - } + void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) { + doWithListeners("spring.boot.application.environment-prepared", + (listener) -> listener.environmentPrepared(bootstrapContext, environment)); } void contextPrepared(ConfigurableApplicationContext context) { - for (SpringApplicationRunListener listener : this.listeners) { - listener.contextPrepared(context); - } + doWithListeners("spring.boot.application.context-prepared", (listener) -> listener.contextPrepared(context)); } void contextLoaded(ConfigurableApplicationContext context) { - for (SpringApplicationRunListener listener : this.listeners) { - listener.contextLoaded(context); - } + doWithListeners("spring.boot.application.context-loaded", (listener) -> listener.contextLoaded(context)); } void started(ConfigurableApplicationContext context) { - for (SpringApplicationRunListener listener : this.listeners) { - listener.started(context); - } + doWithListeners("spring.boot.application.started", (listener) -> listener.started(context)); } void running(ConfigurableApplicationContext context) { - for (SpringApplicationRunListener listener : this.listeners) { - listener.running(context); - } + doWithListeners("spring.boot.application.running", (listener) -> listener.running(context)); } void failed(ConfigurableApplicationContext context, Throwable exception) { - for (SpringApplicationRunListener listener : this.listeners) { - callFailedListener(listener, context, exception); - } + doWithListeners("spring.boot.application.failed", + (listener) -> callFailedListener(listener, context, exception), (step) -> { + step.tag("exception", exception.getClass().toString()); + step.tag("message", exception.getMessage()); + }); } private void callFailedListener(SpringApplicationRunListener listener, ConfigurableApplicationContext context, @@ -104,4 +107,18 @@ private void callFailedListener(SpringApplicationRunListener listener, Configura } } + private void doWithListeners(String stepName, Consumer listenerAction) { + doWithListeners(stepName, listenerAction, null); + } + + private void doWithListeners(String stepName, Consumer listenerAction, + Consumer stepAction) { + StartupStep step = this.applicationStartup.start(stepName); + this.listeners.forEach(listenerAction); + if (stepAction != null) { + stepAction.accept(step); + } + step.end(); + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationShutdownHandlers.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationShutdownHandlers.java new file mode 100644 index 000000000000..882d1debf1b8 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationShutdownHandlers.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot; + +import org.springframework.context.ApplicationContext; + +/** + * Interface that can be used to add or remove code that should run when the JVM is + * shutdown. Shutdown handers are similar to JVM {@link Runtime#addShutdownHook(Thread) + * shutdown hooks} except that they run sequentially rather than concurrently. + *

    + * Shutdown handlers are guaranteed to be called only after registered + * {@link ApplicationContext} instances have been closed and are no longer active. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @since 2.5.1 + * @see SpringApplication#getShutdownHandlers() + * @see SpringApplication#setRegisterShutdownHook(boolean) + */ +public interface SpringApplicationShutdownHandlers { + + /** + * Add an action to the handlers that will be run when the JVM exits. + * @param action the action to add + */ + void add(Runnable action); + + /** + * Remove a previously added an action so that it no longer runs when the JVM exits. + * @param action the action to remove + */ + void remove(Runnable action); + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationShutdownHook.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationShutdownHook.java new file mode 100644 index 000000000000..274ca9d476bb --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationShutdownHook.java @@ -0,0 +1,222 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot; + +import java.security.AccessControlException; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.util.Assert; + +/** + * A {@link Runnable} to be used as a {@link Runtime#addShutdownHook(Thread) shutdown + * hook} to perform graceful shutdown of Spring Boot applications. This hook tracks + * registered application contexts as well as any actions registered via + * {@link SpringApplication#getShutdownHandlers()}. + * + * @author Andy Wilkinson + * @author Phillip Webb + * @author Brian Clozel + */ +class SpringApplicationShutdownHook implements Runnable { + + private static final int SLEEP = 50; + + private static final long TIMEOUT = TimeUnit.MINUTES.toMillis(10); + + private static final Log logger = LogFactory.getLog(SpringApplicationShutdownHook.class); + + private final Handlers handlers = new Handlers(); + + private final Set contexts = new LinkedHashSet<>(); + + private final Set closedContexts = Collections.newSetFromMap(new WeakHashMap<>()); + + private final ApplicationContextClosedListener contextCloseListener = new ApplicationContextClosedListener(); + + private final AtomicBoolean shutdownHookAdded = new AtomicBoolean(false); + + private boolean inProgress; + + SpringApplicationShutdownHandlers getHandlers() { + return this.handlers; + } + + void registerApplicationContext(ConfigurableApplicationContext context) { + addRuntimeShutdownHookIfNecessary(); + synchronized (SpringApplicationShutdownHook.class) { + assertNotInProgress(); + context.addApplicationListener(this.contextCloseListener); + this.contexts.add(context); + } + } + + private void addRuntimeShutdownHookIfNecessary() { + if (this.shutdownHookAdded.compareAndSet(false, true)) { + addRuntimeShutdownHook(); + } + } + + void addRuntimeShutdownHook() { + try { + Runtime.getRuntime().addShutdownHook(new Thread(this, "SpringApplicationShutdownHook")); + } + catch (AccessControlException ex) { + // Not allowed in some environments + } + } + + void deregisterFailedApplicationContext(ConfigurableApplicationContext applicationContext) { + synchronized (SpringApplicationShutdownHook.class) { + Assert.state(!applicationContext.isActive(), "Cannot unregister active application context"); + SpringApplicationShutdownHook.this.contexts.remove(applicationContext); + } + } + + @Override + public void run() { + Set contexts; + Set closedContexts; + Set actions; + synchronized (SpringApplicationShutdownHook.class) { + this.inProgress = true; + contexts = new LinkedHashSet<>(this.contexts); + closedContexts = new LinkedHashSet<>(this.closedContexts); + actions = new LinkedHashSet<>(this.handlers.getActions()); + } + contexts.forEach(this::closeAndWait); + closedContexts.forEach(this::closeAndWait); + actions.forEach(Runnable::run); + } + + boolean isApplicationContextRegistered(ConfigurableApplicationContext context) { + synchronized (SpringApplicationShutdownHook.class) { + return this.contexts.contains(context); + } + } + + void reset() { + synchronized (SpringApplicationShutdownHook.class) { + this.contexts.clear(); + this.closedContexts.clear(); + this.handlers.getActions().clear(); + this.inProgress = false; + } + } + + /** + * Call {@link ConfigurableApplicationContext#close()} and wait until the context + * becomes inactive. We can't assume that just because the close method returns that + * the context is actually inactive. It could be that another thread is still in the + * process of disposing beans. + * @param context the context to clean + */ + private void closeAndWait(ConfigurableApplicationContext context) { + if (!context.isActive()) { + return; + } + context.close(); + try { + int waited = 0; + while (context.isActive()) { + if (waited > TIMEOUT) { + throw new TimeoutException(); + } + Thread.sleep(SLEEP); + waited += SLEEP; + } + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + logger.warn("Interrupted waiting for application context " + context + " to become inactive"); + } + catch (TimeoutException ex) { + logger.warn("Timed out waiting for application context " + context + " to become inactive", ex); + } + } + + private void assertNotInProgress() { + Assert.state(!SpringApplicationShutdownHook.this.inProgress, "Shutdown in progress"); + } + + /** + * The handler actions for this shutdown hook. + */ + private class Handlers implements SpringApplicationShutdownHandlers { + + private final Set actions = Collections.newSetFromMap(new IdentityHashMap<>()); + + @Override + public void add(Runnable action) { + Assert.notNull(action, "Action must not be null"); + synchronized (SpringApplicationShutdownHook.class) { + assertNotInProgress(); + this.actions.add(action); + } + } + + @Override + public void remove(Runnable action) { + Assert.notNull(action, "Action must not be null"); + synchronized (SpringApplicationShutdownHook.class) { + assertNotInProgress(); + this.actions.remove(action); + } + } + + Set getActions() { + return this.actions; + } + + } + + /** + * {@link ApplicationListener} to track closed contexts. + */ + private class ApplicationContextClosedListener implements ApplicationListener { + + @Override + public void onApplicationEvent(ContextClosedEvent event) { + // The ContextClosedEvent is fired at the start of a call to {@code close()} + // and if that happens in a different thread then the context may still be + // active. Rather than just removing the context, we add it to a {@code + // closedContexts} set. This is weak set so that the context can be GC'd once + // the {@code close()} method returns. + synchronized (SpringApplicationShutdownHook.class) { + ApplicationContext applicationContext = event.getApplicationContext(); + SpringApplicationShutdownHook.this.contexts.remove(applicationContext); + SpringApplicationShutdownHook.this.closedContexts + .add((ConfigurableApplicationContext) applicationContext); + } + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringBootConfiguration.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringBootConfiguration.java index b5684bd98a32..b888fe04e23a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringBootConfiguration.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringBootConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.AliasFor; +import org.springframework.stereotype.Indexed; /** * Indicates that a class provides Spring Boot application @@ -44,6 +45,7 @@ @Retention(RetentionPolicy.RUNTIME) @Documented @Configuration +@Indexed public @interface SpringBootConfiguration { /** diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/StartupInfoLogger.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/StartupInfoLogger.java index 73dcf08b4b9f..b6ac1ad9b25b 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/StartupInfoLogger.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/StartupInfoLogger.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -67,6 +67,7 @@ private CharSequence getStartingMessage() { message.append("Starting "); appendApplicationName(message); appendVersion(message, this.sourceClass); + appendJavaVersion(message); appendOn(message); appendPid(message); appendContext(message); @@ -147,6 +148,10 @@ private void appendContext(StringBuilder message) { } } + private void appendJavaVersion(StringBuilder message) { + append(message, "using Java ", () -> System.getProperty("java.version")); + } + private void append(StringBuilder message, String prefix, Callable call) { append(message, prefix, call, ""); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiColors.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiColors.java index dc6384c1e08b..24a8afcbec61 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiColors.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiColors.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -130,17 +130,6 @@ private AnsiElement findClosest(LabColor color) { return closest; } - /** - * Get the closest {@link AnsiColor ANSI color} to the given AWT {@link Color}. - * @param color the color to find - * @return the closest color - * @deprecated since 2.2.0 in favor of {@link #findClosest(Color)} - */ - @Deprecated - public static AnsiColor getClosest(Color color) { - return (AnsiColor) new AnsiColors(BitDepth.FOUR).findClosest(color); - } - /** * Represents a color stored in LAB form. */ diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiPropertySource.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiPropertySource.java index 4c0cce7d1800..1553ead56d57 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiPropertySource.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiPropertySource.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -162,7 +162,7 @@ private boolean containsOnlyDigits(String postfix) { return false; } } - return postfix.length() > 0; + return !postfix.isEmpty(); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ApplicationAvailability.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ApplicationAvailability.java new file mode 100644 index 000000000000..3ab16c164ec4 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ApplicationAvailability.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.availability; + +import org.springframework.context.ApplicationContext; + +/** + * Provides {@link AvailabilityState availability state} information for the application. + *

    + * Components can inject this class to get the current state information. To update the + * state of the application an {@link AvailabilityChangeEvent} should be + * {@link ApplicationContext#publishEvent published} to the application context with + * directly or via {@link AvailabilityChangeEvent#publish}. + * + * @author Brian Clozel + * @author Phillip Webb + * @since 2.3.0 + */ +public interface ApplicationAvailability { + + /** + * Return the {@link LivenessState} of the application. + * @return the liveness state + */ + default LivenessState getLivenessState() { + return getState(LivenessState.class, LivenessState.BROKEN); + } + + /** + * Return the {@link ReadinessState} of the application. + * @return the readiness state + */ + default ReadinessState getReadinessState() { + return getState(ReadinessState.class, ReadinessState.REFUSING_TRAFFIC); + } + + /** + * Return {@link AvailabilityState} information for the application. + * @param the state type + * @param stateType the state type + * @param defaultState the default state to return if no event of the given type has + * been published yet (must not be {@code null}. + * @return the readiness state + * @see #getState(Class) + */ + S getState(Class stateType, S defaultState); + + /** + * Return {@link AvailabilityState} information for the application. + * @param the state type + * @param stateType the state type + * @return the readiness state or {@code null} if no event of the given type has been + * published yet + * @see #getState(Class, AvailabilityState) + */ + S getState(Class stateType); + + /** + * Return the last {@link AvailabilityChangeEvent} received for a given state type. + * @param the state type + * @param stateType the state type + * @return the readiness state or {@code null} if no event of the given type has been + * published yet + */ + AvailabilityChangeEvent getLastChangeEvent(Class stateType); + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ApplicationAvailabilityBean.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ApplicationAvailabilityBean.java new file mode 100644 index 000000000000..f65bf5dc7213 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ApplicationAvailabilityBean.java @@ -0,0 +1,105 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.availability; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationListener; +import org.springframework.util.Assert; + +/** + * Bean that provides an {@link ApplicationAvailability} implementation by listening for + * {@link AvailabilityChangeEvent change events}. + * + * @author Brian Clozel + * @author Phillip Webb + * @since 2.3.0 + * @see ApplicationAvailability + */ +public class ApplicationAvailabilityBean + implements ApplicationAvailability, ApplicationListener> { + + private final Map, AvailabilityChangeEvent> events = new HashMap<>(); + + private final Log logger; + + public ApplicationAvailabilityBean() { + this(LogFactory.getLog(ApplicationAvailabilityBean.class)); + } + + ApplicationAvailabilityBean(Log logger) { + this.logger = logger; + } + + @Override + public S getState(Class stateType, S defaultState) { + Assert.notNull(stateType, "StateType must not be null"); + Assert.notNull(defaultState, "DefaultState must not be null"); + S state = getState(stateType); + return (state != null) ? state : defaultState; + } + + @Override + public S getState(Class stateType) { + AvailabilityChangeEvent event = getLastChangeEvent(stateType); + return (event != null) ? event.getState() : null; + } + + @Override + @SuppressWarnings("unchecked") + public AvailabilityChangeEvent getLastChangeEvent(Class stateType) { + return (AvailabilityChangeEvent) this.events.get(stateType); + } + + @Override + public void onApplicationEvent(AvailabilityChangeEvent event) { + Class type = getStateType(event.getState()); + if (this.logger.isDebugEnabled()) { + this.logger.debug(getLogMessage(type, event)); + } + this.events.put(type, event); + } + + private Object getLogMessage(Class type, AvailabilityChangeEvent event) { + AvailabilityChangeEvent lastChangeEvent = getLastChangeEvent(type); + StringBuilder message = new StringBuilder( + "Application availability state " + type.getSimpleName() + " changed"); + message.append((lastChangeEvent != null) ? " from " + lastChangeEvent.getState() : ""); + message.append(" to " + event.getState()); + message.append(getSourceDescription(event.getSource())); + return message; + } + + private String getSourceDescription(Object source) { + if (source == null || source instanceof ApplicationEventPublisher) { + return ""; + } + return ": " + ((source instanceof Throwable) ? source : source.getClass().getName()); + } + + @SuppressWarnings("unchecked") + private Class getStateType(AvailabilityState state) { + Class type = (state instanceof Enum) ? ((Enum) state).getDeclaringClass() : state.getClass(); + return (Class) type; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/AvailabilityChangeEvent.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/AvailabilityChangeEvent.java new file mode 100644 index 000000000000..9c6fa1e8796c --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/AvailabilityChangeEvent.java @@ -0,0 +1,95 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.availability; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.PayloadApplicationEvent; +import org.springframework.core.ResolvableType; +import org.springframework.util.Assert; + +/** + * {@link ApplicationEvent} sent when the {@link AvailabilityState} of the application + * changes. + *

    + * Any application component can send such events to update the state of the application. + * + * @param the availability state type + * @author Brian Clozel + * @author Phillip Webb + * @since 2.3.0 + */ +public class AvailabilityChangeEvent extends PayloadApplicationEvent { + + /** + * Create a new {@link AvailabilityChangeEvent} instance. + * @param source the source of the event + * @param state the availability state (never {@code null}) + */ + public AvailabilityChangeEvent(Object source, S state) { + super(source, state); + } + + /** + * Return the changed availability state. + * @return the availability state + */ + public S getState() { + return getPayload(); + } + + @Override + public ResolvableType getResolvableType() { + return ResolvableType.forClassWithGenerics(getClass(), getStateType()); + } + + private Class getStateType() { + S state = getState(); + if (state instanceof Enum) { + return ((Enum) state).getDeclaringClass(); + } + return state.getClass(); + } + + /** + * Convenience method that can be used to publish an {@link AvailabilityChangeEvent} + * to the given application context. + * @param the availability state type + * @param context the context used to publish the event + * @param state the changed availability state + */ + public static void publish(ApplicationContext context, S state) { + Assert.notNull(context, "Context must not be null"); + publish(context, context, state); + } + + /** + * Convenience method that can be used to publish an {@link AvailabilityChangeEvent} + * to the given application context. + * @param the availability state type + * @param publisher the publisher used to publish the event + * @param source the source of the event + * @param state the changed availability state + */ + public static void publish(ApplicationEventPublisher publisher, Object source, + S state) { + Assert.notNull(publisher, "Publisher must not be null"); + publisher.publishEvent(new AvailabilityChangeEvent<>(source, state)); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/AvailabilityState.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/AvailabilityState.java new file mode 100644 index 000000000000..b78daab83b7d --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/AvailabilityState.java @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.availability; + +/** + * Tagging interface used on {@link ApplicationAvailability} states. This interface is + * usually implemented on an {@code enum} type. + * + * @author Phillip Webb + * @since 2.3.0 + * @see LivenessState + * @see ReadinessState + */ +public interface AvailabilityState { + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/LivenessState.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/LivenessState.java new file mode 100644 index 000000000000..885d7c27bfb0 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/LivenessState.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.availability; + +/** + * "Liveness" state of the application. + *

    + * An application is considered live when it's running with a correct internal state. + * "Liveness" failure means that the internal state of the application is broken and we + * cannot recover from it. As a result, the platform should restart the application. + * + * @author Brian Clozel + * @since 2.3.0 + */ +public enum LivenessState implements AvailabilityState { + + /** + * The application is running and its internal state is correct. + */ + CORRECT, + + /** + * The application is running but its internal state is broken. + */ + BROKEN + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ReadinessState.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ReadinessState.java new file mode 100644 index 000000000000..6c372f0f4457 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/ReadinessState.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.availability; + +/** + * "Readiness" state of the application. + *

    + * An application is considered ready when it's {@link LivenessState live} and willing to + * accept traffic. "Readiness" failure means that the application is not able to accept + * traffic and that the infrastructure should stop routing requests to it. + * + * @author Brian Clozel + * @since 2.3.0 + */ +public enum ReadinessState implements AvailabilityState { + + /** + * The application is ready to receive traffic. + */ + ACCEPTING_TRAFFIC, + + /** + * The application is not willing to receive traffic. + */ + REFUSING_TRAFFIC + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/package-info.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/package-info.java new file mode 100644 index 000000000000..5bacc7b1f318 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/availability/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Support for describing the availability of Spring Boot applications. + */ +package org.springframework.boot.availability; diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java index 5f7efcd0f5e1..c6dba2a96a85 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,10 @@ import java.util.concurrent.atomic.AtomicBoolean; import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.boot.ApplicationContextFactory; import org.springframework.boot.Banner; +import org.springframework.boot.BootstrapRegistry; +import org.springframework.boot.BootstrapRegistryInitializer; import org.springframework.boot.SpringApplication; import org.springframework.boot.WebApplicationType; import org.springframework.boot.convert.ApplicationConversionService; @@ -39,6 +42,7 @@ import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; +import org.springframework.core.metrics.ApplicationStartup; import org.springframework.util.StringUtils; /** @@ -75,7 +79,7 @@ public class SpringApplicationBuilder { private SpringApplicationBuilder parent; - private final AtomicBoolean running = new AtomicBoolean(false); + private final AtomicBoolean running = new AtomicBoolean(); private final Set> sources = new LinkedHashSet<>(); @@ -94,9 +98,8 @@ public SpringApplicationBuilder(Class... sources) { } /** - * Creates a new {@link org.springframework.boot.SpringApplication} instances from the - * given sources. Subclasses may override in order to provide a custom subclass of - * {@link org.springframework.boot.SpringApplication} + * Creates a new {@link SpringApplication} instance from the given sources. Subclasses + * may override in order to provide a custom subclass of {@link SpringApplication}. * @param sources the sources * @return the {@link org.springframework.boot.SpringApplication} instance * @since 1.1.0 @@ -272,12 +275,26 @@ public SpringApplicationBuilder sibling(Class[] sources, String... args) { * Explicitly set the context class to be used. * @param cls the context class to use * @return the current builder + * @deprecated since 2.4.0 for removal in 2.6.0 in favor of + * {@link #contextFactory(ApplicationContextFactory)} */ + @Deprecated public SpringApplicationBuilder contextClass(Class cls) { this.application.setApplicationContextClass(cls); return this; } + /** + * Explicitly set the factory used to create the application context. + * @param factory the factory to use + * @return the current builder + * @since 2.4.0 + */ + public SpringApplicationBuilder contextFactory(ApplicationContextFactory factory) { + this.application.setApplicationContextFactory(factory); + return this; + } + /** * Add more sources (configuration classes and components) to this application. * @param sources the sources to add @@ -382,13 +399,31 @@ public SpringApplicationBuilder setAddConversionService(boolean addConversionSer } /** - * Default properties for the environment in the form {@code key=value} or - * {@code key:value}. - * @param defaultProperties the properties to set. + * Adds a {@link org.springframework.boot.Bootstrapper} that can be used to initialize + * the {@link BootstrapRegistry}. + * @param bootstrapper the bootstraper * @return the current builder + * @since 2.4.0 + * @deprecated since 2.4.5 for removal in 2.6 in favor of + * {@link #addBootstrapRegistryInitializer(BootstrapRegistryInitializer)} */ - public SpringApplicationBuilder properties(String... defaultProperties) { - return properties(getMapFromKeyValuePairs(defaultProperties)); + @Deprecated + public SpringApplicationBuilder addBootstrapper(org.springframework.boot.Bootstrapper bootstrapper) { + this.application.addBootstrapper(bootstrapper); + return this; + } + + /** + * Adds {@link BootstrapRegistryInitializer} instances that can be used to initialize + * the {@link BootstrapRegistry}. + * @param bootstrapRegistryInitializer the bootstrap registry initializer to add + * @return the current builder + * @since 2.4.5 + */ + public SpringApplicationBuilder addBootstrapRegistryInitializer( + BootstrapRegistryInitializer bootstrapRegistryInitializer) { + this.application.addBootstrapRegistryInitializer(bootstrapRegistryInitializer); + return this; } /** @@ -402,6 +437,19 @@ public SpringApplicationBuilder lazyInitialization(boolean lazyInitialization) { return this; } + /** + * Default properties for the environment in the form {@code key=value} or + * {@code key:value}. Multiple calls to this method are cumulative and will not clear + * any previously set properties. + * @param defaultProperties the properties to set. + * @return the current builder + * @see SpringApplicationBuilder#properties(Properties) + * @see SpringApplicationBuilder#properties(Map) + */ + public SpringApplicationBuilder properties(String... defaultProperties) { + return properties(getMapFromKeyValuePairs(defaultProperties)); + } + private Map getMapFromKeyValuePairs(String[] properties) { Map map = new HashMap<>(); for (String property : properties) { @@ -425,10 +473,12 @@ private int lowestIndexOf(String property, String... candidates) { } /** - * Default properties for the environment in the form {@code key=value} or - * {@code key:value}. + * Default properties for the environment.Multiple calls to this method are cumulative + * and will not clear any previously set properties. * @param defaultProperties the properties to set. * @return the current builder + * @see SpringApplicationBuilder#properties(String...) + * @see SpringApplicationBuilder#properties(Map) */ public SpringApplicationBuilder properties(Properties defaultProperties) { return properties(getMapFromProperties(defaultProperties)); @@ -444,10 +494,11 @@ private Map getMapFromProperties(Properties properties) { /** * Default properties for the environment. Multiple calls to this method are - * cumulative. + * cumulative and will not clear any previously set properties. * @param defaults the default properties * @return the current builder * @see SpringApplicationBuilder#properties(String...) + * @see SpringApplicationBuilder#properties(Properties) */ public SpringApplicationBuilder properties(Map defaults) { this.defaultProperties.putAll(defaults); @@ -498,6 +549,18 @@ public SpringApplicationBuilder environment(ConfigurableEnvironment environment) return this; } + /** + * Prefix that should be applied when obtaining configuration properties from the + * system environment. + * @param environmentPrefix the environment property prefix to set + * @return the current builder + * @since 2.5.0 + */ + public SpringApplicationBuilder environmentPrefix(String environmentPrefix) { + this.application.setEnvironmentPrefix(environmentPrefix); + return this; + } + /** * {@link ResourceLoader} for the application context. If a custom class loader is * needed, this is where it would be added. @@ -533,4 +596,16 @@ public SpringApplicationBuilder listeners(ApplicationListener... listeners) { return this; } + /** + * Configure the {@link ApplicationStartup} to be used with the + * {@link ApplicationContext} for collecting startup metrics. + * @param applicationStartup the application startup to use + * @return the current builder + * @since 2.4.0 + */ + public SpringApplicationBuilder applicationStartup(ApplicationStartup applicationStartup) { + this.application.setApplicationStartup(applicationStartup); + return this; + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/cloud/CloudFoundryVcapEnvironmentPostProcessor.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/cloud/CloudFoundryVcapEnvironmentPostProcessor.java index f038288f26f2..8a12031f5cfd 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/cloud/CloudFoundryVcapEnvironmentPostProcessor.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/cloud/CloudFoundryVcapEnvironmentPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,13 +23,15 @@ import java.util.Properties; import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.springframework.boot.SpringApplication; -import org.springframework.boot.context.config.ConfigFileApplicationListener; +import org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor; +import org.springframework.boot.context.event.ApplicationPreparedEvent; import org.springframework.boot.env.EnvironmentPostProcessor; import org.springframework.boot.json.JsonParser; import org.springframework.boot.json.JsonParserFactory; +import org.springframework.boot.logging.DeferredLog; +import org.springframework.context.ApplicationListener; import org.springframework.core.Ordered; import org.springframework.core.env.CommandLinePropertySource; import org.springframework.core.env.ConfigurableEnvironment; @@ -89,16 +91,39 @@ * @author Andy Wilkinson * @since 1.3.0 */ -public class CloudFoundryVcapEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered { - - private static final Log logger = LogFactory.getLog(CloudFoundryVcapEnvironmentPostProcessor.class); +public class CloudFoundryVcapEnvironmentPostProcessor + implements EnvironmentPostProcessor, Ordered, ApplicationListener { private static final String VCAP_APPLICATION = "VCAP_APPLICATION"; private static final String VCAP_SERVICES = "VCAP_SERVICES"; + private final Log logger; + + private final boolean switchableLogger; + // Before ConfigFileApplicationListener so values there can use these ones - private int order = ConfigFileApplicationListener.DEFAULT_ORDER - 1; + private int order = ConfigDataEnvironmentPostProcessor.ORDER - 1; + + /** + * Create a new {@link CloudFoundryVcapEnvironmentPostProcessor} instance. + * @deprecated since 2.4.0 for removal in 2.6.0 in favor of + * {@link #CloudFoundryVcapEnvironmentPostProcessor(Log)} + */ + @Deprecated + public CloudFoundryVcapEnvironmentPostProcessor() { + this.logger = new DeferredLog(); + this.switchableLogger = true; + } + + /** + * Create a new {@link CloudFoundryVcapEnvironmentPostProcessor} instance. + * @param logger the logger to use + */ + public CloudFoundryVcapEnvironmentPostProcessor(Log logger) { + this.logger = logger; + this.switchableLogger = false; + } public void setOrder(int order) { this.order = order; @@ -127,6 +152,19 @@ public void postProcessEnvironment(ConfigurableEnvironment environment, SpringAp } } + /** + * Event listener used to switch logging. + * @deprecated since 2.4.0 for removal in 2.6.0 in favor of only using + * {@link EnvironmentPostProcessor} callbacks + */ + @Deprecated + @Override + public void onApplicationEvent(ApplicationPreparedEvent event) { + if (this.switchableLogger) { + ((DeferredLog) this.logger).switchTo(CloudFoundryVcapEnvironmentPostProcessor.class); + } + } + private void addWithPrefix(Properties properties, Properties other, String prefix) { for (String key : other.stringPropertyNames()) { String prefixed = prefix + key; @@ -142,7 +180,7 @@ private Properties getPropertiesFromApplication(Environment environment, JsonPar extractPropertiesFromApplication(properties, map); } catch (Exception ex) { - logger.error("Could not parse VCAP_APPLICATION", ex); + this.logger.error("Could not parse VCAP_APPLICATION", ex); } return properties; } @@ -155,7 +193,7 @@ private Properties getPropertiesFromServices(Environment environment, JsonParser extractPropertiesFromServices(properties, map); } catch (Exception ex) { - logger.error("Could not parse VCAP_SERVICES", ex); + this.logger.error("Could not parse VCAP_SERVICES", ex); } return properties; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/cloud/CloudPlatform.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/cloud/CloudPlatform.java index 2db1b48e561d..674a8ff490c4 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/cloud/CloudPlatform.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/cloud/CloudPlatform.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,10 @@ package org.springframework.boot.cloud; +import java.util.Arrays; +import java.util.List; + +import org.springframework.boot.context.properties.bind.Binder; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.EnumerablePropertySource; import org.springframework.core.env.Environment; @@ -23,22 +27,35 @@ import org.springframework.core.env.StandardEnvironment; /** - * Simple detection for well known cloud platforms. For more advanced cloud provider - * integration consider the Spring Cloud project. + * Simple detection for well known cloud platforms. Detection can be forced using the + * {@code "spring.main.cloud-platform"} configuration property. * * @author Phillip Webb + * @author Brian Clozel + * @author Nguyen Sach * @since 1.3.0 - * @see "https://cloud.spring.io" */ public enum CloudPlatform { + /** + * No Cloud platform. Useful when false-positives are detected. + */ + NONE { + + @Override + public boolean isDetected(Environment environment) { + return false; + } + + }, + /** * Cloud Foundry platform. */ CLOUD_FOUNDRY { @Override - public boolean isActive(Environment environment) { + public boolean isDetected(Environment environment) { return environment.containsProperty("VCAP_APPLICATION") || environment.containsProperty("VCAP_SERVICES"); } @@ -50,7 +67,7 @@ public boolean isActive(Environment environment) { HEROKU { @Override - public boolean isActive(Environment environment) { + public boolean isDetected(Environment environment) { return environment.containsProperty("DYNO"); } @@ -62,7 +79,7 @@ public boolean isActive(Environment environment) { SAP { @Override - public boolean isActive(Environment environment) { + public boolean isDetected(Environment environment) { return environment.containsProperty("HC_LANDSCAPE"); } @@ -73,28 +90,38 @@ public boolean isActive(Environment environment) { */ KUBERNETES { + private static final String KUBERNETES_SERVICE_HOST = "KUBERNETES_SERVICE_HOST"; + + private static final String KUBERNETES_SERVICE_PORT = "KUBERNETES_SERVICE_PORT"; + private static final String SERVICE_HOST_SUFFIX = "_SERVICE_HOST"; private static final String SERVICE_PORT_SUFFIX = "_SERVICE_PORT"; @Override - public boolean isActive(Environment environment) { + public boolean isDetected(Environment environment) { if (environment instanceof ConfigurableEnvironment) { - return isActive((ConfigurableEnvironment) environment); + return isAutoDetected((ConfigurableEnvironment) environment); } return false; } - private boolean isActive(ConfigurableEnvironment environment) { + private boolean isAutoDetected(ConfigurableEnvironment environment) { PropertySource environmentPropertySource = environment.getPropertySources() .get(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME); - if (environmentPropertySource instanceof EnumerablePropertySource) { - return isActive((EnumerablePropertySource) environmentPropertySource); + if (environmentPropertySource != null) { + if (environmentPropertySource.containsProperty(KUBERNETES_SERVICE_HOST) + && environmentPropertySource.containsProperty(KUBERNETES_SERVICE_PORT)) { + return true; + } + if (environmentPropertySource instanceof EnumerablePropertySource) { + return isAutoDetected((EnumerablePropertySource) environmentPropertySource); + } } return false; } - private boolean isActive(EnumerablePropertySource environmentPropertySource) { + private boolean isAutoDetected(EnumerablePropertySource environmentPropertySource) { for (String propertyName : environmentPropertySource.getPropertyNames()) { if (propertyName.endsWith(SERVICE_HOST_SUFFIX)) { String serviceName = propertyName.substring(0, @@ -107,14 +134,69 @@ private boolean isActive(EnumerablePropertySource environmentPropertySource) return false; } + }, + + /** + * Azure App Service platform. + */ + AZURE_APP_SERVICE { + + private final List azureEnvVariables = Arrays.asList("WEBSITE_SITE_NAME", "WEBSITE_INSTANCE_ID", + "WEBSITE_RESOURCE_GROUP", "WEBSITE_SKU"); + + @Override + public boolean isDetected(Environment environment) { + return this.azureEnvVariables.stream().allMatch(environment::containsProperty); + } + }; + private static final String PROPERTY_NAME = "spring.main.cloud-platform"; + /** * Determines if the platform is active (i.e. the application is running in it). * @param environment the environment * @return if the platform is active. */ - public abstract boolean isActive(Environment environment); + public boolean isActive(Environment environment) { + String platformProperty = environment.getProperty(PROPERTY_NAME); + return isEnforced(platformProperty) || (platformProperty == null && isDetected(environment)); + } + + /** + * Determines if the platform is enforced by looking at the + * {@code "spring.main.cloud-platform"} configuration property. + * @param environment the environment + * @return if the platform is enforced + * @since 2.3.0 + */ + public boolean isEnforced(Environment environment) { + return isEnforced(environment.getProperty(PROPERTY_NAME)); + } + + /** + * Determines if the platform is enforced by looking at the + * {@code "spring.main.cloud-platform"} configuration property. + * @param binder the binder + * @return if the platform is enforced + * @since 2.4.0 + */ + public boolean isEnforced(Binder binder) { + return isEnforced(binder.bind(PROPERTY_NAME, String.class).orElse(null)); + } + + private boolean isEnforced(String platform) { + return name().equalsIgnoreCase(platform); + } + + /** + * Determines if the platform is detected by looking for platform-specific environment + * variables. + * @param environment the environment + * @return if the platform is auto-detected. + * @since 2.3.0 + */ + public abstract boolean isDetected(Environment environment); /** * Returns if the platform is behind a load balancer and uses @@ -126,7 +208,7 @@ public boolean isUsingForwardHeaders() { } /** - * Returns the active {@link CloudPlatform} or {@code null} if one cannot be deduced. + * Returns the active {@link CloudPlatform} or {@code null} if one is not active. * @param environment the environment * @return the {@link CloudPlatform} or {@code null} */ diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/ApplicationPidFileWriter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/ApplicationPidFileWriter.java index 1fa0b65ae39e..488daf6c7ea9 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/ApplicationPidFileWriter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/ApplicationPidFileWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -86,7 +86,7 @@ public class ApplicationPidFileWriter implements ApplicationListener> propertySources; + + private final PropertySourceOptions propertySourceOptions; + + /** + * A {@link ConfigData} instance that contains no data. + */ + public static final ConfigData EMPTY = new ConfigData(Collections.emptySet()); + + /** + * Create a new {@link ConfigData} instance with the same options applied to each + * source. + * @param propertySources the config data property sources in ascending priority + * order. + * @param options the config data options applied to each source + * @see #ConfigData(Collection, PropertySourceOptions) + */ + public ConfigData(Collection> propertySources, Option... options) { + this(propertySources, PropertySourceOptions.always(Options.of(options))); + } + + /** + * Create a new {@link ConfigData} instance with specific property source options. + * @param propertySources the config data property sources in ascending priority + * order. + * @param propertySourceOptions the property source options + * @since 2.4.5 + */ + public ConfigData(Collection> propertySources, + PropertySourceOptions propertySourceOptions) { + Assert.notNull(propertySources, "PropertySources must not be null"); + Assert.notNull(propertySourceOptions, "PropertySourceOptions must not be null"); + this.propertySources = Collections.unmodifiableList(new ArrayList<>(propertySources)); + this.propertySourceOptions = propertySourceOptions; + } + + /** + * Return the configuration data property sources in ascending priority order. If the + * same key is contained in more than one of the sources, then the later source will + * win. + * @return the config data property sources + */ + public List> getPropertySources() { + return this.propertySources; + } + + /** + * Return a set of {@link Option config data options} for this source. + * @return the config data options + * @deprecated since 2.4.5 in favor of {@link #getOptions(PropertySource)} + */ + @Deprecated + public Set

    + * The initial locations can be influenced via the {@link #LOCATION_PROPERTY}, + * {@value #ADDITIONAL_LOCATION_PROPERTY} and {@value #IMPORT_PROPERTY} properties. If no + * explicit properties are set, the {@link #DEFAULT_SEARCH_LOCATIONS} will be used. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +class ConfigDataEnvironment { + + /** + * Property used override the imported locations. + */ + static final String LOCATION_PROPERTY = "spring.config.location"; + + /** + * Property used to provide additional locations to import. + */ + static final String ADDITIONAL_LOCATION_PROPERTY = "spring.config.additional-location"; + + /** + * Property used to provide additional locations to import. + */ + static final String IMPORT_PROPERTY = "spring.config.import"; + + /** + * Property used to determine what action to take when a + * {@code ConfigDataNotFoundAction} is thrown. + * @see ConfigDataNotFoundAction + */ + static final String ON_NOT_FOUND_PROPERTY = "spring.config.on-not-found"; + + /** + * Default search locations used if not {@link #LOCATION_PROPERTY} is found. + */ + static final ConfigDataLocation[] DEFAULT_SEARCH_LOCATIONS; + static { + List locations = new ArrayList<>(); + locations.add(ConfigDataLocation.of("optional:classpath:/;optional:classpath:/config/")); + locations.add(ConfigDataLocation.of("optional:file:./;optional:file:./config/;optional:file:./config/*/")); + DEFAULT_SEARCH_LOCATIONS = locations.toArray(new ConfigDataLocation[0]); + } + + private static final ConfigDataLocation[] EMPTY_LOCATIONS = new ConfigDataLocation[0]; + + private static final Bindable CONFIG_DATA_LOCATION_ARRAY = Bindable + .of(ConfigDataLocation[].class); + + private static final Bindable> STRING_LIST = Bindable.listOf(String.class); + + private static final BinderOption[] ALLOW_INACTIVE_BINDING = {}; + + private static final BinderOption[] DENY_INACTIVE_BINDING = { BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE }; + + private final DeferredLogFactory logFactory; + + private final Log logger; + + private final ConfigDataNotFoundAction notFoundAction; + + private final ConfigurableBootstrapContext bootstrapContext; + + private final ConfigurableEnvironment environment; + + private final ConfigDataLocationResolvers resolvers; + + private final Collection additionalProfiles; + + private final ConfigDataEnvironmentUpdateListener environmentUpdateListener; + + private final ConfigDataLoaders loaders; + + private final ConfigDataEnvironmentContributors contributors; + + /** + * Create a new {@link ConfigDataEnvironment} instance. + * @param logFactory the deferred log factory + * @param bootstrapContext the bootstrap context + * @param environment the Spring {@link Environment}. + * @param resourceLoader {@link ResourceLoader} to load resource locations + * @param additionalProfiles any additional profiles to activate + * @param environmentUpdateListener optional + * {@link ConfigDataEnvironmentUpdateListener} that can be used to track + * {@link Environment} updates. + */ + ConfigDataEnvironment(DeferredLogFactory logFactory, ConfigurableBootstrapContext bootstrapContext, + ConfigurableEnvironment environment, ResourceLoader resourceLoader, Collection additionalProfiles, + ConfigDataEnvironmentUpdateListener environmentUpdateListener) { + Binder binder = Binder.get(environment); + UseLegacyConfigProcessingException.throwIfRequested(binder); + this.logFactory = logFactory; + this.logger = logFactory.getLog(getClass()); + this.notFoundAction = binder.bind(ON_NOT_FOUND_PROPERTY, ConfigDataNotFoundAction.class) + .orElse(ConfigDataNotFoundAction.FAIL); + this.bootstrapContext = bootstrapContext; + this.environment = environment; + this.resolvers = createConfigDataLocationResolvers(logFactory, bootstrapContext, binder, resourceLoader); + this.additionalProfiles = additionalProfiles; + this.environmentUpdateListener = (environmentUpdateListener != null) ? environmentUpdateListener + : ConfigDataEnvironmentUpdateListener.NONE; + this.loaders = new ConfigDataLoaders(logFactory, bootstrapContext, resourceLoader.getClassLoader()); + this.contributors = createContributors(binder); + } + + protected ConfigDataLocationResolvers createConfigDataLocationResolvers(DeferredLogFactory logFactory, + ConfigurableBootstrapContext bootstrapContext, Binder binder, ResourceLoader resourceLoader) { + return new ConfigDataLocationResolvers(logFactory, bootstrapContext, binder, resourceLoader); + } + + private ConfigDataEnvironmentContributors createContributors(Binder binder) { + this.logger.trace("Building config data environment contributors"); + MutablePropertySources propertySources = this.environment.getPropertySources(); + List contributors = new ArrayList<>(propertySources.size() + 10); + PropertySource defaultPropertySource = null; + for (PropertySource propertySource : propertySources) { + if (DefaultPropertiesPropertySource.hasMatchingName(propertySource)) { + defaultPropertySource = propertySource; + } + else { + this.logger.trace(LogMessage.format("Creating wrapped config data contributor for '%s'", + propertySource.getName())); + contributors.add(ConfigDataEnvironmentContributor.ofExisting(propertySource)); + } + } + contributors.addAll(getInitialImportContributors(binder)); + if (defaultPropertySource != null) { + this.logger.trace("Creating wrapped config data contributor for default property source"); + contributors.add(ConfigDataEnvironmentContributor.ofExisting(defaultPropertySource)); + } + return createContributors(contributors); + } + + protected ConfigDataEnvironmentContributors createContributors( + List contributors) { + return new ConfigDataEnvironmentContributors(this.logFactory, this.bootstrapContext, contributors); + } + + ConfigDataEnvironmentContributors getContributors() { + return this.contributors; + } + + private List getInitialImportContributors(Binder binder) { + List initialContributors = new ArrayList<>(); + addInitialImportContributors(initialContributors, bindLocations(binder, IMPORT_PROPERTY, EMPTY_LOCATIONS)); + addInitialImportContributors(initialContributors, + bindLocations(binder, ADDITIONAL_LOCATION_PROPERTY, EMPTY_LOCATIONS)); + addInitialImportContributors(initialContributors, + bindLocations(binder, LOCATION_PROPERTY, DEFAULT_SEARCH_LOCATIONS)); + return initialContributors; + } + + private ConfigDataLocation[] bindLocations(Binder binder, String propertyName, ConfigDataLocation[] other) { + return binder.bind(propertyName, CONFIG_DATA_LOCATION_ARRAY).orElse(other); + } + + private void addInitialImportContributors(List initialContributors, + ConfigDataLocation[] locations) { + for (int i = locations.length - 1; i >= 0; i--) { + initialContributors.add(createInitialImportContributor(locations[i])); + } + } + + private ConfigDataEnvironmentContributor createInitialImportContributor(ConfigDataLocation location) { + this.logger.trace(LogMessage.format("Adding initial config data import from location '%s'", location)); + return ConfigDataEnvironmentContributor.ofInitialImport(location); + } + + /** + * Process all contributions and apply any newly imported property sources to the + * {@link Environment}. + */ + void processAndApply() { + ConfigDataImporter importer = new ConfigDataImporter(this.logFactory, this.notFoundAction, this.resolvers, + this.loaders); + registerBootstrapBinder(this.contributors, null, DENY_INACTIVE_BINDING); + ConfigDataEnvironmentContributors contributors = processInitial(this.contributors, importer); + ConfigDataActivationContext activationContext = createActivationContext( + contributors.getBinder(null, BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE)); + contributors = processWithoutProfiles(contributors, importer, activationContext); + activationContext = withProfiles(contributors, activationContext); + contributors = processWithProfiles(contributors, importer, activationContext); + applyToEnvironment(contributors, activationContext, importer.getLoadedLocations(), + importer.getOptionalLocations()); + } + + private ConfigDataEnvironmentContributors processInitial(ConfigDataEnvironmentContributors contributors, + ConfigDataImporter importer) { + this.logger.trace("Processing initial config data environment contributors without activation context"); + contributors = contributors.withProcessedImports(importer, null); + registerBootstrapBinder(contributors, null, DENY_INACTIVE_BINDING); + return contributors; + } + + private ConfigDataActivationContext createActivationContext(Binder initialBinder) { + this.logger.trace("Creating config data activation context from initial contributions"); + try { + return new ConfigDataActivationContext(this.environment, initialBinder); + } + catch (BindException ex) { + if (ex.getCause() instanceof InactiveConfigDataAccessException) { + throw (InactiveConfigDataAccessException) ex.getCause(); + } + throw ex; + } + } + + private ConfigDataEnvironmentContributors processWithoutProfiles(ConfigDataEnvironmentContributors contributors, + ConfigDataImporter importer, ConfigDataActivationContext activationContext) { + this.logger.trace("Processing config data environment contributors with initial activation context"); + contributors = contributors.withProcessedImports(importer, activationContext); + registerBootstrapBinder(contributors, activationContext, DENY_INACTIVE_BINDING); + return contributors; + } + + private ConfigDataActivationContext withProfiles(ConfigDataEnvironmentContributors contributors, + ConfigDataActivationContext activationContext) { + this.logger.trace("Deducing profiles from current config data environment contributors"); + Binder binder = contributors.getBinder(activationContext, + (contributor) -> !contributor.hasConfigDataOption(ConfigData.Option.IGNORE_PROFILES), + BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE); + try { + Set additionalProfiles = new LinkedHashSet<>(this.additionalProfiles); + additionalProfiles.addAll(getIncludedProfiles(contributors, activationContext)); + Profiles profiles = new Profiles(this.environment, binder, additionalProfiles); + return activationContext.withProfiles(profiles); + } + catch (BindException ex) { + if (ex.getCause() instanceof InactiveConfigDataAccessException) { + throw (InactiveConfigDataAccessException) ex.getCause(); + } + throw ex; + } + } + + private Collection getIncludedProfiles(ConfigDataEnvironmentContributors contributors, + ConfigDataActivationContext activationContext) { + PlaceholdersResolver placeholdersResolver = new ConfigDataEnvironmentContributorPlaceholdersResolver( + contributors, activationContext, null, true); + Set result = new LinkedHashSet<>(); + for (ConfigDataEnvironmentContributor contributor : contributors) { + ConfigurationPropertySource source = contributor.getConfigurationPropertySource(); + if (source != null && !contributor.hasConfigDataOption(ConfigData.Option.IGNORE_PROFILES)) { + Binder binder = new Binder(Collections.singleton(source), placeholdersResolver); + binder.bind(Profiles.INCLUDE_PROFILES, STRING_LIST).ifBound((includes) -> { + if (!contributor.isActive(activationContext)) { + InactiveConfigDataAccessException.throwIfPropertyFound(contributor, Profiles.INCLUDE_PROFILES); + InactiveConfigDataAccessException.throwIfPropertyFound(contributor, + Profiles.INCLUDE_PROFILES.append("[0]")); + } + result.addAll(includes); + }); + } + } + return result; + } + + private ConfigDataEnvironmentContributors processWithProfiles(ConfigDataEnvironmentContributors contributors, + ConfigDataImporter importer, ConfigDataActivationContext activationContext) { + this.logger.trace("Processing config data environment contributors with profile activation context"); + contributors = contributors.withProcessedImports(importer, activationContext); + registerBootstrapBinder(contributors, activationContext, ALLOW_INACTIVE_BINDING); + return contributors; + } + + private void registerBootstrapBinder(ConfigDataEnvironmentContributors contributors, + ConfigDataActivationContext activationContext, BinderOption... binderOptions) { + this.bootstrapContext.register(Binder.class, InstanceSupplier + .from(() -> contributors.getBinder(activationContext, binderOptions)).withScope(Scope.PROTOTYPE)); + } + + private void applyToEnvironment(ConfigDataEnvironmentContributors contributors, + ConfigDataActivationContext activationContext, Set loadedLocations, + Set optionalLocations) { + checkForInvalidProperties(contributors); + checkMandatoryLocations(contributors, activationContext, loadedLocations, optionalLocations); + MutablePropertySources propertySources = this.environment.getPropertySources(); + applyContributor(contributors, activationContext, propertySources); + DefaultPropertiesPropertySource.moveToEnd(propertySources); + Profiles profiles = activationContext.getProfiles(); + this.logger.trace(LogMessage.format("Setting default profiles: %s", profiles.getDefault())); + this.environment.setDefaultProfiles(StringUtils.toStringArray(profiles.getDefault())); + this.logger.trace(LogMessage.format("Setting active profiles: %s", profiles.getActive())); + this.environment.setActiveProfiles(StringUtils.toStringArray(profiles.getActive())); + this.environmentUpdateListener.onSetProfiles(profiles); + } + + private void applyContributor(ConfigDataEnvironmentContributors contributors, + ConfigDataActivationContext activationContext, MutablePropertySources propertySources) { + this.logger.trace("Applying config data environment contributions"); + for (ConfigDataEnvironmentContributor contributor : contributors) { + PropertySource propertySource = contributor.getPropertySource(); + if (contributor.getKind() == ConfigDataEnvironmentContributor.Kind.BOUND_IMPORT && propertySource != null) { + if (!contributor.isActive(activationContext)) { + this.logger.trace( + LogMessage.format("Skipping inactive property source '%s'", propertySource.getName())); + } + else { + this.logger + .trace(LogMessage.format("Adding imported property source '%s'", propertySource.getName())); + propertySources.addLast(propertySource); + this.environmentUpdateListener.onPropertySourceAdded(propertySource, contributor.getLocation(), + contributor.getResource()); + } + } + } + } + + private void checkForInvalidProperties(ConfigDataEnvironmentContributors contributors) { + for (ConfigDataEnvironmentContributor contributor : contributors) { + InvalidConfigDataPropertyException.throwOrWarn(this.logger, contributor); + } + } + + private void checkMandatoryLocations(ConfigDataEnvironmentContributors contributors, + ConfigDataActivationContext activationContext, Set loadedLocations, + Set optionalLocations) { + Set mandatoryLocations = new LinkedHashSet<>(); + for (ConfigDataEnvironmentContributor contributor : contributors) { + if (contributor.isActive(activationContext)) { + mandatoryLocations.addAll(getMandatoryImports(contributor)); + } + } + for (ConfigDataEnvironmentContributor contributor : contributors) { + if (contributor.getLocation() != null) { + mandatoryLocations.remove(contributor.getLocation()); + } + } + mandatoryLocations.removeAll(loadedLocations); + mandatoryLocations.removeAll(optionalLocations); + if (!mandatoryLocations.isEmpty()) { + for (ConfigDataLocation mandatoryLocation : mandatoryLocations) { + this.notFoundAction.handle(this.logger, new ConfigDataLocationNotFoundException(mandatoryLocation)); + } + } + } + + private Set getMandatoryImports(ConfigDataEnvironmentContributor contributor) { + List imports = contributor.getImports(); + Set mandatoryLocations = new LinkedHashSet<>(imports.size()); + for (ConfigDataLocation location : imports) { + if (!location.isOptional()) { + mandatoryLocations.add(location); + } + } + return mandatoryLocations; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributor.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributor.java new file mode 100644 index 000000000000..b7347a60b511 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributor.java @@ -0,0 +1,567 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.config; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.bind.PlaceholdersResolver; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.core.env.Environment; +import org.springframework.core.env.PropertySource; +import org.springframework.util.CollectionUtils; + +/** + * A single element that may directly or indirectly contribute configuration data to the + * {@link Environment}. There are several different {@link Kind kinds} of contributor, all + * are immutable and will be replaced with new versions as imports are processed. + *

    + * Contributors may provide a set of imports that should be processed and ultimately + * turned into children. There are two distinct import phases: + *

      + *
    • {@link ImportPhase#BEFORE_PROFILE_ACTIVATION Before} profiles have been + * activated.
    • + *
    • {@link ImportPhase#AFTER_PROFILE_ACTIVATION After} profiles have been + * activated.
    • + *
    + * In each phase all imports will be resolved before they are loaded. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +class ConfigDataEnvironmentContributor implements Iterable { + + private static final ConfigData.Options EMPTY_LOCATION_OPTIONS = ConfigData.Options + .of(ConfigData.Option.IGNORE_IMPORTS); + + private final ConfigDataLocation location; + + private final ConfigDataResource resource; + + private final boolean fromProfileSpecificImport; + + private final PropertySource propertySource; + + private final ConfigurationPropertySource configurationPropertySource; + + private final ConfigDataProperties properties; + + private final ConfigData.Options configDataOptions; + + private final Map> children; + + private final Kind kind; + + /** + * Create a new {@link ConfigDataEnvironmentContributor} instance. + * @param kind the contributor kind + * @param location the location of this contributor + * @param resource the resource that contributed the data or {@code null} + * @param fromProfileSpecificImport if the contributor is from a profile specific + * import + * @param propertySource the property source for the data or {@code null} + * @param configurationPropertySource the configuration property source for the data + * or {@code null} + * @param properties the config data properties or {@code null} + * @param configDataOptions any config data options that should apply + * @param children the children of this contributor at each {@link ImportPhase} + */ + ConfigDataEnvironmentContributor(Kind kind, ConfigDataLocation location, ConfigDataResource resource, + boolean fromProfileSpecificImport, PropertySource propertySource, + ConfigurationPropertySource configurationPropertySource, ConfigDataProperties properties, + ConfigData.Options configDataOptions, Map> children) { + this.kind = kind; + this.location = location; + this.resource = resource; + this.fromProfileSpecificImport = fromProfileSpecificImport; + this.properties = properties; + this.propertySource = propertySource; + this.configurationPropertySource = configurationPropertySource; + this.configDataOptions = (configDataOptions != null) ? configDataOptions : ConfigData.Options.NONE; + this.children = (children != null) ? children : Collections.emptyMap(); + } + + /** + * Return the contributor kind. + * @return the kind of contributor + */ + Kind getKind() { + return this.kind; + } + + ConfigDataLocation getLocation() { + return this.location; + } + + /** + * Return if this contributor is currently active. + * @param activationContext the activation context + * @return if the contributor is active + */ + boolean isActive(ConfigDataActivationContext activationContext) { + if (this.kind == Kind.UNBOUND_IMPORT) { + return false; + } + return this.properties == null || this.properties.isActive(activationContext); + } + + /** + * Return the resource that contributed this instance. + * @return the resource or {@code null} + */ + ConfigDataResource getResource() { + return this.resource; + } + + /** + * Return if the contributor is from a profile specific import. + * @return if the contributor is profile specific + */ + boolean isFromProfileSpecificImport() { + return this.fromProfileSpecificImport; + } + + /** + * Return the property source for this contributor. + * @return the property source or {@code null} + */ + PropertySource getPropertySource() { + return this.propertySource; + } + + /** + * Return the configuration property source for this contributor. + * @return the configuration property source or {@code null} + */ + ConfigurationPropertySource getConfigurationPropertySource() { + return this.configurationPropertySource; + } + + /** + * Return if the contributor has a specific config data option. + * @param option the option to check + * @return {@code true} if the option is present + */ + boolean hasConfigDataOption(ConfigData.Option option) { + return this.configDataOptions.contains(option); + } + + ConfigDataEnvironmentContributor withoutConfigDataOption(ConfigData.Option option) { + return new ConfigDataEnvironmentContributor(this.kind, this.location, this.resource, + this.fromProfileSpecificImport, this.propertySource, this.configurationPropertySource, this.properties, + this.configDataOptions.without(option), this.children); + } + + /** + * Return any imports requested by this contributor. + * @return the imports + */ + List getImports() { + return (this.properties != null) ? this.properties.getImports() : Collections.emptyList(); + } + + /** + * Return true if this contributor has imports that have not yet been processed in the + * given phase. + * @param importPhase the import phase + * @return if there are unprocessed imports + */ + boolean hasUnprocessedImports(ImportPhase importPhase) { + if (getImports().isEmpty()) { + return false; + } + return !this.children.containsKey(importPhase); + } + + /** + * Return children of this contributor for the given phase. + * @param importPhase the import phase + * @return a list of children + */ + List getChildren(ImportPhase importPhase) { + return this.children.getOrDefault(importPhase, Collections.emptyList()); + } + + /** + * Returns a {@link Stream} that traverses this contributor and all its children in + * priority order. + * @return the stream + */ + Stream stream() { + return StreamSupport.stream(spliterator(), false); + } + + /** + * Returns an {@link Iterator} that traverses this contributor and all its children in + * priority order. + * @return the iterator + * @see java.lang.Iterable#iterator() + */ + @Override + public Iterator iterator() { + return new ContributorIterator(); + } + + /** + * Create an new {@link ConfigDataEnvironmentContributor} with bound + * {@link ConfigDataProperties}. + * @param contributors the contributors used for binding + * @param activationContext the activation context + * @return a new contributor instance + */ + ConfigDataEnvironmentContributor withBoundProperties(Iterable contributors, + ConfigDataActivationContext activationContext) { + Iterable sources = Collections.singleton(getConfigurationPropertySource()); + PlaceholdersResolver placeholdersResolver = new ConfigDataEnvironmentContributorPlaceholdersResolver( + contributors, activationContext, this, true); + Binder binder = new Binder(sources, placeholdersResolver, null, null, null); + UseLegacyConfigProcessingException.throwIfRequested(binder); + ConfigDataProperties properties = ConfigDataProperties.get(binder); + if (properties != null && this.configDataOptions.contains(ConfigData.Option.IGNORE_IMPORTS)) { + properties = properties.withoutImports(); + } + return new ConfigDataEnvironmentContributor(Kind.BOUND_IMPORT, this.location, this.resource, + this.fromProfileSpecificImport, this.propertySource, this.configurationPropertySource, properties, + this.configDataOptions, null); + } + + /** + * Create a new {@link ConfigDataEnvironmentContributor} instance with a new set of + * children for the given phase. + * @param importPhase the import phase + * @param children the new children + * @return a new contributor instance + */ + ConfigDataEnvironmentContributor withChildren(ImportPhase importPhase, + List children) { + Map> updatedChildren = new LinkedHashMap<>(this.children); + updatedChildren.put(importPhase, children); + if (importPhase == ImportPhase.AFTER_PROFILE_ACTIVATION) { + moveProfileSpecific(updatedChildren); + } + return new ConfigDataEnvironmentContributor(this.kind, this.location, this.resource, + this.fromProfileSpecificImport, this.propertySource, this.configurationPropertySource, this.properties, + this.configDataOptions, updatedChildren); + } + + private void moveProfileSpecific(Map> children) { + List before = children.get(ImportPhase.BEFORE_PROFILE_ACTIVATION); + if (!hasAnyProfileSpecificChildren(before)) { + return; + } + List updatedBefore = new ArrayList<>(before.size()); + List updatedAfter = new ArrayList<>(); + for (ConfigDataEnvironmentContributor contributor : before) { + updatedBefore.add(moveProfileSpecificChildren(contributor, updatedAfter)); + } + updatedAfter.addAll(children.getOrDefault(ImportPhase.AFTER_PROFILE_ACTIVATION, Collections.emptyList())); + children.put(ImportPhase.BEFORE_PROFILE_ACTIVATION, updatedBefore); + children.put(ImportPhase.AFTER_PROFILE_ACTIVATION, updatedAfter); + } + + private ConfigDataEnvironmentContributor moveProfileSpecificChildren(ConfigDataEnvironmentContributor contributor, + List removed) { + for (ImportPhase importPhase : ImportPhase.values()) { + List children = contributor.getChildren(importPhase); + List updatedChildren = new ArrayList<>(children.size()); + for (ConfigDataEnvironmentContributor child : children) { + if (child.hasConfigDataOption(ConfigData.Option.PROFILE_SPECIFIC)) { + removed.add(child.withoutConfigDataOption(ConfigData.Option.PROFILE_SPECIFIC)); + } + else { + updatedChildren.add(child); + } + } + contributor = contributor.withChildren(importPhase, updatedChildren); + } + return contributor; + } + + private boolean hasAnyProfileSpecificChildren(List contributors) { + if (CollectionUtils.isEmpty(contributors)) { + return false; + } + for (ConfigDataEnvironmentContributor contributor : contributors) { + for (ImportPhase importPhase : ImportPhase.values()) { + if (contributor.getChildren(importPhase).stream() + .anyMatch((child) -> child.hasConfigDataOption(ConfigData.Option.PROFILE_SPECIFIC))) { + return true; + } + } + } + return false; + } + + /** + * Create a new {@link ConfigDataEnvironmentContributor} instance where a existing + * child is replaced. + * @param existing the existing node that should be replaced + * @param replacement the replacement node that should be used instead + * @return a new {@link ConfigDataEnvironmentContributor} instance + */ + ConfigDataEnvironmentContributor withReplacement(ConfigDataEnvironmentContributor existing, + ConfigDataEnvironmentContributor replacement) { + if (this == existing) { + return replacement; + } + Map> updatedChildren = new LinkedHashMap<>( + this.children.size()); + this.children.forEach((importPhase, contributors) -> { + List updatedContributors = new ArrayList<>(contributors.size()); + for (ConfigDataEnvironmentContributor contributor : contributors) { + updatedContributors.add(contributor.withReplacement(existing, replacement)); + } + updatedChildren.put(importPhase, Collections.unmodifiableList(updatedContributors)); + }); + return new ConfigDataEnvironmentContributor(this.kind, this.location, this.resource, + this.fromProfileSpecificImport, this.propertySource, this.configurationPropertySource, this.properties, + this.configDataOptions, updatedChildren); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + buildToString("", builder); + return builder.toString(); + } + + private void buildToString(String prefix, StringBuilder builder) { + builder.append(prefix); + builder.append(this.kind); + builder.append(" "); + builder.append(this.location); + builder.append(" "); + builder.append(this.resource); + builder.append(" "); + builder.append(this.configDataOptions); + builder.append("\n"); + for (ConfigDataEnvironmentContributor child : this.children.getOrDefault(ImportPhase.BEFORE_PROFILE_ACTIVATION, + Collections.emptyList())) { + child.buildToString(prefix + " ", builder); + } + for (ConfigDataEnvironmentContributor child : this.children.getOrDefault(ImportPhase.AFTER_PROFILE_ACTIVATION, + Collections.emptyList())) { + child.buildToString(prefix + " ", builder); + } + } + + /** + * Factory method to create a {@link Kind#ROOT root} contributor. + * @param contributors the immediate children of the root + * @return a new {@link ConfigDataEnvironmentContributor} instance + */ + static ConfigDataEnvironmentContributor of(List contributors) { + Map> children = new LinkedHashMap<>(); + children.put(ImportPhase.BEFORE_PROFILE_ACTIVATION, Collections.unmodifiableList(contributors)); + return new ConfigDataEnvironmentContributor(Kind.ROOT, null, null, false, null, null, null, null, children); + } + + /** + * Factory method to create a {@link Kind#INITIAL_IMPORT initial import} contributor. + * This contributor is used to trigger initial imports of additional contributors. It + * does not contribute any properties itself. + * @param initialImport the initial import location (with placeholders resolved) + * @return a new {@link ConfigDataEnvironmentContributor} instance + */ + static ConfigDataEnvironmentContributor ofInitialImport(ConfigDataLocation initialImport) { + List imports = Collections.singletonList(initialImport); + ConfigDataProperties properties = new ConfigDataProperties(imports, null); + return new ConfigDataEnvironmentContributor(Kind.INITIAL_IMPORT, null, null, false, null, null, properties, + null, null); + } + + /** + * Factory method to create a contributor that wraps an {@link Kind#EXISTING existing} + * property source. The contributor provides access to existing properties, but + * doesn't actively import any additional contributors. + * @param propertySource the property source to wrap + * @return a new {@link ConfigDataEnvironmentContributor} instance + */ + static ConfigDataEnvironmentContributor ofExisting(PropertySource propertySource) { + return new ConfigDataEnvironmentContributor(Kind.EXISTING, null, null, false, propertySource, + ConfigurationPropertySource.from(propertySource), null, null, null); + } + + /** + * Factory method to create an {@link Kind#UNBOUND_IMPORT unbound import} contributor. + * This contributor has been actively imported from another contributor and may itself + * import further contributors later. + * @param location the location of this contributor + * @param resource the config data resource + * @param profileSpecific if the contributor is from a profile specific import + * @param configData the config data + * @param propertySourceIndex the index of the property source that should be used + * @return a new {@link ConfigDataEnvironmentContributor} instance + */ + static ConfigDataEnvironmentContributor ofUnboundImport(ConfigDataLocation location, ConfigDataResource resource, + boolean profileSpecific, ConfigData configData, int propertySourceIndex) { + PropertySource propertySource = configData.getPropertySources().get(propertySourceIndex); + ConfigData.Options options = configData.getOptions(propertySource); + ConfigurationPropertySource configurationPropertySource = ConfigurationPropertySource.from(propertySource); + return new ConfigDataEnvironmentContributor(Kind.UNBOUND_IMPORT, location, resource, profileSpecific, + propertySource, configurationPropertySource, null, options, null); + } + + /** + * Factory method to create an {@link Kind#EMPTY_LOCATION empty location} contributor. + * @param location the location of this contributor + * @param profileSpecific if the contributor is from a profile specific import + * @return a new {@link ConfigDataEnvironmentContributor} instance + */ + static ConfigDataEnvironmentContributor ofEmptyLocation(ConfigDataLocation location, boolean profileSpecific) { + return new ConfigDataEnvironmentContributor(Kind.EMPTY_LOCATION, location, null, profileSpecific, null, null, + null, EMPTY_LOCATION_OPTIONS, null); + } + + /** + * The various kinds of contributor. + */ + enum Kind { + + /** + * A root contributor used contain the initial set of children. + */ + ROOT, + + /** + * An initial import that needs to be processed. + */ + INITIAL_IMPORT, + + /** + * An existing property source that contributes properties but no imports. + */ + EXISTING, + + /** + * A contributor with {@link ConfigData} imported from another contributor but not + * yet bound. + */ + UNBOUND_IMPORT, + + /** + * A contributor with {@link ConfigData} imported from another contributor that + * has been. + */ + BOUND_IMPORT, + + /** + * A valid location that contained nothing to load. + */ + EMPTY_LOCATION; + + } + + /** + * Import phases that can be used when obtaining imports. + */ + enum ImportPhase { + + /** + * The phase before profiles have been activated. + */ + BEFORE_PROFILE_ACTIVATION, + + /** + * The phase after profiles have been activated. + */ + AFTER_PROFILE_ACTIVATION; + + /** + * Return the {@link ImportPhase} based on the given activation context. + * @param activationContext the activation context + * @return the import phase + */ + static ImportPhase get(ConfigDataActivationContext activationContext) { + if (activationContext != null && activationContext.getProfiles() != null) { + return AFTER_PROFILE_ACTIVATION; + } + return BEFORE_PROFILE_ACTIVATION; + } + + } + + /** + * Iterator that traverses the contributor tree. + */ + private final class ContributorIterator implements Iterator { + + private ImportPhase phase; + + private Iterator children; + + private Iterator current; + + private ConfigDataEnvironmentContributor next; + + private ContributorIterator() { + this.phase = ImportPhase.AFTER_PROFILE_ACTIVATION; + this.children = getChildren(this.phase).iterator(); + this.current = Collections.emptyIterator(); + } + + @Override + public boolean hasNext() { + return fetchIfNecessary() != null; + } + + @Override + public ConfigDataEnvironmentContributor next() { + ConfigDataEnvironmentContributor next = fetchIfNecessary(); + if (next == null) { + throw new NoSuchElementException(); + } + this.next = null; + return next; + } + + private ConfigDataEnvironmentContributor fetchIfNecessary() { + if (this.next != null) { + return this.next; + } + if (this.current.hasNext()) { + this.next = this.current.next(); + return this.next; + } + if (this.children.hasNext()) { + this.current = this.children.next().iterator(); + return fetchIfNecessary(); + } + if (this.phase == ImportPhase.AFTER_PROFILE_ACTIVATION) { + this.phase = ImportPhase.BEFORE_PROFILE_ACTIVATION; + this.children = getChildren(this.phase).iterator(); + return fetchIfNecessary(); + } + if (this.phase == ImportPhase.BEFORE_PROFILE_ACTIVATION) { + this.phase = null; + this.next = ConfigDataEnvironmentContributor.this; + return this.next; + } + return null; + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributorPlaceholdersResolver.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributorPlaceholdersResolver.java new file mode 100644 index 000000000000..8ddf031706d7 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributorPlaceholdersResolver.java @@ -0,0 +1,94 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.config; + +import org.springframework.boot.context.config.ConfigDataEnvironmentContributor.Kind; +import org.springframework.boot.context.properties.bind.PlaceholdersResolver; +import org.springframework.boot.origin.Origin; +import org.springframework.boot.origin.OriginLookup; +import org.springframework.core.env.PropertySource; +import org.springframework.util.PropertyPlaceholderHelper; +import org.springframework.util.SystemPropertyUtils; + +/** + * {@link PlaceholdersResolver} backed by one or more + * {@link ConfigDataEnvironmentContributor} instances. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +class ConfigDataEnvironmentContributorPlaceholdersResolver implements PlaceholdersResolver { + + private final Iterable contributors; + + private final ConfigDataActivationContext activationContext; + + private final boolean failOnResolveFromInactiveContributor; + + private final PropertyPlaceholderHelper helper; + + private final ConfigDataEnvironmentContributor activeContributor; + + ConfigDataEnvironmentContributorPlaceholdersResolver(Iterable contributors, + ConfigDataActivationContext activationContext, ConfigDataEnvironmentContributor activeContributor, + boolean failOnResolveFromInactiveContributor) { + this.contributors = contributors; + this.activationContext = activationContext; + this.activeContributor = activeContributor; + this.failOnResolveFromInactiveContributor = failOnResolveFromInactiveContributor; + this.helper = new PropertyPlaceholderHelper(SystemPropertyUtils.PLACEHOLDER_PREFIX, + SystemPropertyUtils.PLACEHOLDER_SUFFIX, SystemPropertyUtils.VALUE_SEPARATOR, true); + } + + @Override + public Object resolvePlaceholders(Object value) { + if (value instanceof String) { + return this.helper.replacePlaceholders((String) value, this::resolvePlaceholder); + } + return value; + } + + private String resolvePlaceholder(String placeholder) { + Object result = null; + for (ConfigDataEnvironmentContributor contributor : this.contributors) { + PropertySource propertySource = contributor.getPropertySource(); + Object value = (propertySource != null) ? propertySource.getProperty(placeholder) : null; + if (value != null && !isActive(contributor)) { + if (this.failOnResolveFromInactiveContributor) { + ConfigDataResource resource = contributor.getResource(); + Origin origin = OriginLookup.getOrigin(propertySource, placeholder); + throw new InactiveConfigDataAccessException(propertySource, resource, placeholder, origin); + } + value = null; + } + result = (result != null) ? result : value; + } + return (result != null) ? String.valueOf(result) : null; + } + + private boolean isActive(ConfigDataEnvironmentContributor contributor) { + if (contributor == this.activeContributor) { + return true; + } + if (contributor.getKind() != Kind.UNBOUND_IMPORT) { + return contributor.isActive(this.activationContext); + } + return contributor.withBoundProperties(this.contributors, this.activationContext) + .isActive(this.activationContext); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributors.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributors.java new file mode 100644 index 000000000000..5ead7593ddaf --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributors.java @@ -0,0 +1,331 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.config; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.apache.commons.logging.Log; + +import org.springframework.boot.ConfigurableBootstrapContext; +import org.springframework.boot.context.config.ConfigDataEnvironmentContributor.ImportPhase; +import org.springframework.boot.context.config.ConfigDataEnvironmentContributor.Kind; +import org.springframework.boot.context.properties.bind.BindContext; +import org.springframework.boot.context.properties.bind.BindHandler; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.bind.PlaceholdersResolver; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.boot.logging.DeferredLogFactory; +import org.springframework.core.log.LogMessage; +import org.springframework.util.ObjectUtils; + +/** + * An immutable tree structure of {@link ConfigDataEnvironmentContributors} used to + * process imports. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +class ConfigDataEnvironmentContributors implements Iterable { + + private static final Predicate NO_CONTRIBUTOR_FILTER = (contributor) -> true; + + private final Log logger; + + private final ConfigDataEnvironmentContributor root; + + private final ConfigurableBootstrapContext bootstrapContext; + + /** + * Create a new {@link ConfigDataEnvironmentContributors} instance. + * @param logFactory the log factory + * @param bootstrapContext the bootstrap context + * @param contributors the initial set of contributors + */ + ConfigDataEnvironmentContributors(DeferredLogFactory logFactory, ConfigurableBootstrapContext bootstrapContext, + List contributors) { + this.logger = logFactory.getLog(getClass()); + this.bootstrapContext = bootstrapContext; + this.root = ConfigDataEnvironmentContributor.of(contributors); + } + + private ConfigDataEnvironmentContributors(Log logger, ConfigurableBootstrapContext bootstrapContext, + ConfigDataEnvironmentContributor root) { + this.logger = logger; + this.bootstrapContext = bootstrapContext; + this.root = root; + } + + /** + * Processes imports from all active contributors and return a new + * {@link ConfigDataEnvironmentContributors} instance. + * @param importer the importer used to import {@link ConfigData} + * @param activationContext the current activation context or {@code null} if the + * context has not get been created + * @return a {@link ConfigDataEnvironmentContributors} instance with all relevant + * imports have been processed + */ + ConfigDataEnvironmentContributors withProcessedImports(ConfigDataImporter importer, + ConfigDataActivationContext activationContext) { + ImportPhase importPhase = ImportPhase.get(activationContext); + this.logger.trace(LogMessage.format("Processing imports for phase %s. %s", importPhase, + (activationContext != null) ? activationContext : "no activation context")); + ConfigDataEnvironmentContributors result = this; + int processed = 0; + while (true) { + ConfigDataEnvironmentContributor contributor = getNextToProcess(result, activationContext, importPhase); + if (contributor == null) { + this.logger.trace(LogMessage.format("Processed imports for of %d contributors", processed)); + return result; + } + if (contributor.getKind() == Kind.UNBOUND_IMPORT) { + ConfigDataEnvironmentContributor bound = contributor.withBoundProperties(result, activationContext); + result = new ConfigDataEnvironmentContributors(this.logger, this.bootstrapContext, + result.getRoot().withReplacement(contributor, bound)); + continue; + } + ConfigDataLocationResolverContext locationResolverContext = new ContributorConfigDataLocationResolverContext( + result, contributor, activationContext); + ConfigDataLoaderContext loaderContext = new ContributorDataLoaderContext(this); + List imports = contributor.getImports(); + this.logger.trace(LogMessage.format("Processing imports %s", imports)); + Map imported = importer.resolveAndLoad(activationContext, + locationResolverContext, loaderContext, imports); + this.logger.trace(LogMessage.of(() -> getImportedMessage(imported.keySet()))); + ConfigDataEnvironmentContributor contributorAndChildren = contributor.withChildren(importPhase, + asContributors(imported)); + result = new ConfigDataEnvironmentContributors(this.logger, this.bootstrapContext, + result.getRoot().withReplacement(contributor, contributorAndChildren)); + processed++; + } + } + + private CharSequence getImportedMessage(Set results) { + if (results.isEmpty()) { + return "Nothing imported"; + } + StringBuilder message = new StringBuilder(); + message.append("Imported " + results.size() + " resource" + ((results.size() != 1) ? "s " : " ")); + message.append(results.stream().map(ConfigDataResolutionResult::getResource).collect(Collectors.toList())); + return message; + } + + protected final ConfigurableBootstrapContext getBootstrapContext() { + return this.bootstrapContext; + } + + private ConfigDataEnvironmentContributor getNextToProcess(ConfigDataEnvironmentContributors contributors, + ConfigDataActivationContext activationContext, ImportPhase importPhase) { + for (ConfigDataEnvironmentContributor contributor : contributors.getRoot()) { + if (contributor.getKind() == Kind.UNBOUND_IMPORT + || isActiveWithUnprocessedImports(activationContext, importPhase, contributor)) { + return contributor; + } + } + return null; + } + + private boolean isActiveWithUnprocessedImports(ConfigDataActivationContext activationContext, + ImportPhase importPhase, ConfigDataEnvironmentContributor contributor) { + return contributor.isActive(activationContext) && contributor.hasUnprocessedImports(importPhase); + } + + private List asContributors( + Map imported) { + List contributors = new ArrayList<>(imported.size() * 5); + imported.forEach((resolutionResult, data) -> { + ConfigDataLocation location = resolutionResult.getLocation(); + ConfigDataResource resource = resolutionResult.getResource(); + boolean profileSpecific = resolutionResult.isProfileSpecific(); + if (data.getPropertySources().isEmpty()) { + contributors.add(ConfigDataEnvironmentContributor.ofEmptyLocation(location, profileSpecific)); + } + else { + for (int i = data.getPropertySources().size() - 1; i >= 0; i--) { + contributors.add(ConfigDataEnvironmentContributor.ofUnboundImport(location, resource, + profileSpecific, data, i)); + } + } + }); + return Collections.unmodifiableList(contributors); + } + + /** + * Returns the root contributor. + * @return the root contributor. + */ + ConfigDataEnvironmentContributor getRoot() { + return this.root; + } + + /** + * Return a {@link Binder} backed by the contributors. + * @param activationContext the activation context + * @param options binder options to apply + * @return a binder instance + */ + Binder getBinder(ConfigDataActivationContext activationContext, BinderOption... options) { + return getBinder(activationContext, NO_CONTRIBUTOR_FILTER, options); + } + + /** + * Return a {@link Binder} backed by the contributors. + * @param activationContext the activation context + * @param filter a filter used to limit the contributors + * @param options binder options to apply + * @return a binder instance + */ + Binder getBinder(ConfigDataActivationContext activationContext, Predicate filter, + BinderOption... options) { + return getBinder(activationContext, filter, asBinderOptionsSet(options)); + } + + private Set asBinderOptionsSet(BinderOption... options) { + return ObjectUtils.isEmpty(options) ? EnumSet.noneOf(BinderOption.class) + : EnumSet.copyOf(Arrays.asList(options)); + } + + private Binder getBinder(ConfigDataActivationContext activationContext, + Predicate filter, Set options) { + boolean failOnInactiveSource = options.contains(BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE); + Iterable sources = () -> getBinderSources( + filter.and((contributor) -> failOnInactiveSource || contributor.isActive(activationContext))); + PlaceholdersResolver placeholdersResolver = new ConfigDataEnvironmentContributorPlaceholdersResolver(this.root, + activationContext, null, failOnInactiveSource); + BindHandler bindHandler = !failOnInactiveSource ? null : new InactiveSourceChecker(activationContext); + return new Binder(sources, placeholdersResolver, null, null, bindHandler); + } + + private Iterator getBinderSources(Predicate filter) { + return this.root.stream().filter(this::hasConfigurationPropertySource).filter(filter) + .map(ConfigDataEnvironmentContributor::getConfigurationPropertySource).iterator(); + } + + private boolean hasConfigurationPropertySource(ConfigDataEnvironmentContributor contributor) { + return contributor.getConfigurationPropertySource() != null; + } + + @Override + public Iterator iterator() { + return this.root.iterator(); + } + + /** + * {@link ConfigDataLocationResolverContext} for a contributor. + */ + private static class ContributorDataLoaderContext implements ConfigDataLoaderContext { + + private final ConfigDataEnvironmentContributors contributors; + + ContributorDataLoaderContext(ConfigDataEnvironmentContributors contributors) { + this.contributors = contributors; + } + + @Override + public ConfigurableBootstrapContext getBootstrapContext() { + return this.contributors.getBootstrapContext(); + } + + } + + /** + * {@link ConfigDataLocationResolverContext} for a contributor. + */ + private static class ContributorConfigDataLocationResolverContext implements ConfigDataLocationResolverContext { + + private final ConfigDataEnvironmentContributors contributors; + + private final ConfigDataEnvironmentContributor contributor; + + private final ConfigDataActivationContext activationContext; + + private volatile Binder binder; + + ContributorConfigDataLocationResolverContext(ConfigDataEnvironmentContributors contributors, + ConfigDataEnvironmentContributor contributor, ConfigDataActivationContext activationContext) { + this.contributors = contributors; + this.contributor = contributor; + this.activationContext = activationContext; + } + + @Override + public Binder getBinder() { + Binder binder = this.binder; + if (binder == null) { + binder = this.contributors.getBinder(this.activationContext); + this.binder = binder; + } + return binder; + } + + @Override + public ConfigDataResource getParent() { + return this.contributor.getResource(); + } + + @Override + public ConfigurableBootstrapContext getBootstrapContext() { + return this.contributors.getBootstrapContext(); + } + + } + + private class InactiveSourceChecker implements BindHandler { + + private final ConfigDataActivationContext activationContext; + + InactiveSourceChecker(ConfigDataActivationContext activationContext) { + this.activationContext = activationContext; + } + + @Override + public Object onSuccess(ConfigurationPropertyName name, Bindable target, BindContext context, + Object result) { + for (ConfigDataEnvironmentContributor contributor : ConfigDataEnvironmentContributors.this) { + if (!contributor.isActive(this.activationContext)) { + InactiveConfigDataAccessException.throwIfPropertyFound(contributor, name); + } + } + return result; + } + + } + + /** + * Binder options that can be used with + * {@link ConfigDataEnvironmentContributors#getBinder(ConfigDataActivationContext, BinderOption...)}. + */ + enum BinderOption { + + /** + * Throw an exception if an inactive contributor contains a bound value. + */ + FAIL_ON_BIND_TO_INACTIVE_SOURCE; + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentPostProcessor.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentPostProcessor.java new file mode 100644 index 000000000000..7df92a25482e --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentPostProcessor.java @@ -0,0 +1,218 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.config; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.function.Supplier; + +import org.apache.commons.logging.Log; + +import org.springframework.boot.ConfigurableBootstrapContext; +import org.springframework.boot.DefaultBootstrapContext; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.boot.logging.DeferredLogFactory; +import org.springframework.core.Ordered; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.log.LogMessage; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +/** + * {@link EnvironmentPostProcessor} that loads and applies {@link ConfigData} to Spring's + * {@link Environment}. + * + * @author Phillip Webb + * @author Madhura Bhave + * @author Nguyen Bao Sach + * @since 2.4.0 + */ +public class ConfigDataEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered { + + /** + * The default order for the processor. + */ + public static final int ORDER = Ordered.HIGHEST_PRECEDENCE + 10; + + /** + * Property used to determine what action to take when a + * {@code ConfigDataLocationNotFoundException} is thrown. + * @see ConfigDataNotFoundAction + */ + public static final String ON_LOCATION_NOT_FOUND_PROPERTY = ConfigDataEnvironment.ON_NOT_FOUND_PROPERTY; + + private final DeferredLogFactory logFactory; + + private final Log logger; + + private final ConfigurableBootstrapContext bootstrapContext; + + private final ConfigDataEnvironmentUpdateListener environmentUpdateListener; + + public ConfigDataEnvironmentPostProcessor(DeferredLogFactory logFactory, + ConfigurableBootstrapContext bootstrapContext) { + this(logFactory, bootstrapContext, null); + } + + public ConfigDataEnvironmentPostProcessor(DeferredLogFactory logFactory, + ConfigurableBootstrapContext bootstrapContext, + ConfigDataEnvironmentUpdateListener environmentUpdateListener) { + this.logFactory = logFactory; + this.logger = logFactory.getLog(getClass()); + this.bootstrapContext = bootstrapContext; + this.environmentUpdateListener = environmentUpdateListener; + } + + @Override + public int getOrder() { + return ORDER; + } + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + postProcessEnvironment(environment, application.getResourceLoader(), application.getAdditionalProfiles()); + } + + void postProcessEnvironment(ConfigurableEnvironment environment, ResourceLoader resourceLoader, + Collection additionalProfiles) { + try { + this.logger.trace("Post-processing environment to add config data"); + resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader(); + getConfigDataEnvironment(environment, resourceLoader, additionalProfiles).processAndApply(); + } + catch (UseLegacyConfigProcessingException ex) { + this.logger.debug(LogMessage.format("Switching to legacy config file processing [%s]", + ex.getConfigurationProperty())); + configureAdditionalProfiles(environment, additionalProfiles); + postProcessUsingLegacyApplicationListener(environment, resourceLoader); + } + } + + ConfigDataEnvironment getConfigDataEnvironment(ConfigurableEnvironment environment, ResourceLoader resourceLoader, + Collection additionalProfiles) { + return new ConfigDataEnvironment(this.logFactory, this.bootstrapContext, environment, resourceLoader, + additionalProfiles, this.environmentUpdateListener); + } + + private void configureAdditionalProfiles(ConfigurableEnvironment environment, + Collection additionalProfiles) { + if (!CollectionUtils.isEmpty(additionalProfiles)) { + Set profiles = new LinkedHashSet<>(additionalProfiles); + profiles.addAll(Arrays.asList(environment.getActiveProfiles())); + environment.setActiveProfiles(StringUtils.toStringArray(profiles)); + } + } + + private void postProcessUsingLegacyApplicationListener(ConfigurableEnvironment environment, + ResourceLoader resourceLoader) { + getLegacyListener().addPropertySources(environment, resourceLoader); + } + + @SuppressWarnings("deprecation") + LegacyConfigFileApplicationListener getLegacyListener() { + return new LegacyConfigFileApplicationListener(this.logFactory.getLog(ConfigFileApplicationListener.class)); + } + + /** + * Apply {@link ConfigData} post-processing to an existing {@link Environment}. This + * method can be useful when working with an {@link Environment} that has been created + * directly and not necessarily as part of a {@link SpringApplication}. + * @param environment the environment to apply {@link ConfigData} to + */ + public static void applyTo(ConfigurableEnvironment environment) { + applyTo(environment, null, null, Collections.emptyList()); + } + + /** + * Apply {@link ConfigData} post-processing to an existing {@link Environment}. This + * method can be useful when working with an {@link Environment} that has been created + * directly and not necessarily as part of a {@link SpringApplication}. + * @param environment the environment to apply {@link ConfigData} to + * @param resourceLoader the resource loader to use + * @param bootstrapContext the bootstrap context to use or {@code null} to use a + * throw-away context + * @param additionalProfiles any additional profiles that should be applied + */ + public static void applyTo(ConfigurableEnvironment environment, ResourceLoader resourceLoader, + ConfigurableBootstrapContext bootstrapContext, String... additionalProfiles) { + applyTo(environment, resourceLoader, bootstrapContext, Arrays.asList(additionalProfiles)); + } + + /** + * Apply {@link ConfigData} post-processing to an existing {@link Environment}. This + * method can be useful when working with an {@link Environment} that has been created + * directly and not necessarily as part of a {@link SpringApplication}. + * @param environment the environment to apply {@link ConfigData} to + * @param resourceLoader the resource loader to use + * @param bootstrapContext the bootstrap context to use or {@code null} to use a + * throw-away context + * @param additionalProfiles any additional profiles that should be applied + */ + public static void applyTo(ConfigurableEnvironment environment, ResourceLoader resourceLoader, + ConfigurableBootstrapContext bootstrapContext, Collection additionalProfiles) { + DeferredLogFactory logFactory = Supplier::get; + bootstrapContext = (bootstrapContext != null) ? bootstrapContext : new DefaultBootstrapContext(); + ConfigDataEnvironmentPostProcessor postProcessor = new ConfigDataEnvironmentPostProcessor(logFactory, + bootstrapContext); + postProcessor.postProcessEnvironment(environment, resourceLoader, additionalProfiles); + } + + /** + * Apply {@link ConfigData} post-processing to an existing {@link Environment}. This + * method can be useful when working with an {@link Environment} that has been created + * directly and not necessarily as part of a {@link SpringApplication}. + * @param environment the environment to apply {@link ConfigData} to + * @param resourceLoader the resource loader to use + * @param bootstrapContext the bootstrap context to use or {@code null} to use a + * throw-away context + * @param additionalProfiles any additional profiles that should be applied + * @param environmentUpdateListener optional + * {@link ConfigDataEnvironmentUpdateListener} that can be used to track + * {@link Environment} updates. + */ + public static void applyTo(ConfigurableEnvironment environment, ResourceLoader resourceLoader, + ConfigurableBootstrapContext bootstrapContext, Collection additionalProfiles, + ConfigDataEnvironmentUpdateListener environmentUpdateListener) { + DeferredLogFactory logFactory = Supplier::get; + bootstrapContext = (bootstrapContext != null) ? bootstrapContext : new DefaultBootstrapContext(); + ConfigDataEnvironmentPostProcessor postProcessor = new ConfigDataEnvironmentPostProcessor(logFactory, + bootstrapContext, environmentUpdateListener); + postProcessor.postProcessEnvironment(environment, resourceLoader, additionalProfiles); + } + + @SuppressWarnings("deprecation") + static class LegacyConfigFileApplicationListener extends ConfigFileApplicationListener { + + LegacyConfigFileApplicationListener(Log logger) { + super(logger); + } + + @Override + public void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) { + super.addPropertySources(environment, resourceLoader); + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentUpdateListener.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentUpdateListener.java new file mode 100644 index 000000000000..f32f0af784a0 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentUpdateListener.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.config; + +import java.util.EventListener; + +import org.springframework.core.env.Environment; +import org.springframework.core.env.PropertySource; + +/** + * {@link EventListener} to listen to {@link Environment} updates triggered by the + * {@link ConfigDataEnvironmentPostProcessor}. + * + * @author Phillip Webb + * @since 2.4.2 + */ +public interface ConfigDataEnvironmentUpdateListener extends EventListener { + + /** + * A {@link ConfigDataEnvironmentUpdateListener} that does nothing. + */ + ConfigDataEnvironmentUpdateListener NONE = new ConfigDataEnvironmentUpdateListener() { + }; + + /** + * Called when a new {@link PropertySource} is added to the {@link Environment}. + * @param propertySource the {@link PropertySource} that was added + * @param location the original {@link ConfigDataLocation} of the source. + * @param resource the {@link ConfigDataResource} of the source. + */ + default void onPropertySourceAdded(PropertySource propertySource, ConfigDataLocation location, + ConfigDataResource resource) { + } + + /** + * Called when {@link Environment} profiles are set. + * @param profiles the profiles being set + */ + default void onSetProfiles(Profiles profiles) { + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataException.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataException.java new file mode 100644 index 000000000000..a9d21e08ab67 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataException.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.config; + +/** + * Abstract base class for configuration data exceptions. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.4.0 + */ +public abstract class ConfigDataException extends RuntimeException { + + /** + * Create a new {@link ConfigDataException} instance. + * @param message the exception message + * @param cause the exception cause + */ + protected ConfigDataException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataImporter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataImporter.java new file mode 100644 index 000000000000..0970b4f6a605 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataImporter.java @@ -0,0 +1,165 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.config; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.logging.Log; + +import org.springframework.boot.logging.DeferredLogFactory; + +/** + * Imports {@link ConfigData} by {@link ConfigDataLocationResolver resolving} and + * {@link ConfigDataLoader loading} locations. {@link ConfigDataResource resources} are + * tracked to ensure that they are not imported multiple times. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +class ConfigDataImporter { + + private final Log logger; + + private final ConfigDataLocationResolvers resolvers; + + private final ConfigDataLoaders loaders; + + private final ConfigDataNotFoundAction notFoundAction; + + private final Set loaded = new HashSet<>(); + + private final Set loadedLocations = new HashSet<>(); + + private final Set optionalLocations = new HashSet<>(); + + /** + * Create a new {@link ConfigDataImporter} instance. + * @param logFactory the log factory + * @param notFoundAction the action to take when a location cannot be found + * @param resolvers the config data location resolvers + * @param loaders the config data loaders + */ + ConfigDataImporter(DeferredLogFactory logFactory, ConfigDataNotFoundAction notFoundAction, + ConfigDataLocationResolvers resolvers, ConfigDataLoaders loaders) { + this.logger = logFactory.getLog(getClass()); + this.resolvers = resolvers; + this.loaders = loaders; + this.notFoundAction = notFoundAction; + } + + /** + * Resolve and load the given list of locations, filtering any that have been + * previously loaded. + * @param activationContext the activation context + * @param locationResolverContext the location resolver context + * @param loaderContext the loader context + * @param locations the locations to resolve + * @return a map of the loaded locations and data + */ + Map resolveAndLoad(ConfigDataActivationContext activationContext, + ConfigDataLocationResolverContext locationResolverContext, ConfigDataLoaderContext loaderContext, + List locations) { + try { + Profiles profiles = (activationContext != null) ? activationContext.getProfiles() : null; + List resolved = resolve(locationResolverContext, profiles, locations); + return load(loaderContext, resolved); + } + catch (IOException ex) { + throw new IllegalStateException("IO error on loading imports from " + locations, ex); + } + } + + private List resolve(ConfigDataLocationResolverContext locationResolverContext, + Profiles profiles, List locations) { + List resolved = new ArrayList<>(locations.size()); + for (ConfigDataLocation location : locations) { + resolved.addAll(resolve(locationResolverContext, profiles, location)); + } + return Collections.unmodifiableList(resolved); + } + + private List resolve(ConfigDataLocationResolverContext locationResolverContext, + Profiles profiles, ConfigDataLocation location) { + try { + return this.resolvers.resolve(locationResolverContext, location, profiles); + } + catch (ConfigDataNotFoundException ex) { + handle(ex, location, null); + return Collections.emptyList(); + } + } + + private Map load(ConfigDataLoaderContext loaderContext, + List candidates) throws IOException { + Map result = new LinkedHashMap<>(); + for (int i = candidates.size() - 1; i >= 0; i--) { + ConfigDataResolutionResult candidate = candidates.get(i); + ConfigDataLocation location = candidate.getLocation(); + ConfigDataResource resource = candidate.getResource(); + if (resource.isOptional()) { + this.optionalLocations.add(location); + } + if (this.loaded.contains(resource)) { + this.loadedLocations.add(location); + } + else { + try { + ConfigData loaded = this.loaders.load(loaderContext, resource); + if (loaded != null) { + this.loaded.add(resource); + this.loadedLocations.add(location); + result.put(candidate, loaded); + } + } + catch (ConfigDataNotFoundException ex) { + handle(ex, location, resource); + } + } + } + return Collections.unmodifiableMap(result); + } + + private void handle(ConfigDataNotFoundException ex, ConfigDataLocation location, ConfigDataResource resource) { + if (ex instanceof ConfigDataResourceNotFoundException) { + ex = ((ConfigDataResourceNotFoundException) ex).withLocation(location); + } + getNotFoundAction(location, resource).handle(this.logger, ex); + } + + private ConfigDataNotFoundAction getNotFoundAction(ConfigDataLocation location, ConfigDataResource resource) { + if (location.isOptional() || (resource != null && resource.isOptional())) { + return ConfigDataNotFoundAction.IGNORE; + } + return this.notFoundAction; + } + + Set getLoadedLocations() { + return this.loadedLocations; + } + + Set getOptionalLocations() { + return this.optionalLocations; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLoader.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLoader.java new file mode 100644 index 000000000000..eaea00ca3a22 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLoader.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.config; + +import java.io.IOException; + +import org.apache.commons.logging.Log; + +import org.springframework.boot.BootstrapContext; +import org.springframework.boot.BootstrapRegistry; +import org.springframework.boot.ConfigurableBootstrapContext; +import org.springframework.boot.logging.DeferredLogFactory; + +/** + * Strategy class that can be used used to load {@link ConfigData} for a given + * {@link ConfigDataResource}. Implementations should be added as a + * {@code spring.factories} entries. The following constructor parameter types are + * supported: + *
      + *
    • {@link Log} or {@link DeferredLogFactory} - if the loader needs deferred + * logging
    • + *
    • {@link ConfigurableBootstrapContext} - A bootstrap context that can be used to + * store objects that may be expensive to create, or need to be shared + * ({@link BootstrapContext} or {@link BootstrapRegistry} may also be used).
    • + *
    + *

    + * Multiple loaders cannot claim the same resource. + * + * @param the resource type + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.4.0 + */ +public interface ConfigDataLoader { + + /** + * Returns if the specified resource can be loaded by this instance. + * @param context the loader context + * @param resource the resource to check. + * @return if the resource is supported by this loader + */ + default boolean isLoadable(ConfigDataLoaderContext context, R resource) { + return true; + } + + /** + * Load {@link ConfigData} for the given resource. + * @param context the loader context + * @param resource the resource to load + * @return the loaded config data or {@code null} if the location should be skipped + * @throws IOException on IO error + * @throws ConfigDataResourceNotFoundException if the resource cannot be found + */ + ConfigData load(ConfigDataLoaderContext context, R resource) + throws IOException, ConfigDataResourceNotFoundException; + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLoaderContext.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLoaderContext.java new file mode 100644 index 000000000000..50e0bf45de8d --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLoaderContext.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.config; + +import org.springframework.boot.ConfigurableBootstrapContext; +import org.springframework.boot.env.EnvironmentPostProcessor; + +/** + * Context provided to {@link ConfigDataLoader} methods. + * + * @author Phillip Webb + * @since 2.4.0 + */ +public interface ConfigDataLoaderContext { + + /** + * Provides access to the {@link ConfigurableBootstrapContext} shared across all + * {@link EnvironmentPostProcessor EnvironmentPostProcessors}. + * @return the bootstrap context + */ + ConfigurableBootstrapContext getBootstrapContext(); + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLoaders.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLoaders.java new file mode 100644 index 000000000000..c9636ed4ccb8 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLoaders.java @@ -0,0 +1,130 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.config; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.logging.Log; + +import org.springframework.boot.BootstrapContext; +import org.springframework.boot.BootstrapRegistry; +import org.springframework.boot.ConfigurableBootstrapContext; +import org.springframework.boot.logging.DeferredLogFactory; +import org.springframework.boot.util.Instantiator; +import org.springframework.core.ResolvableType; +import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.core.log.LogMessage; +import org.springframework.util.Assert; + +/** + * A collection of {@link ConfigDataLoader} instances loaded via {@code spring.factories}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +class ConfigDataLoaders { + + private final Log logger; + + private final List> loaders; + + private final List> resourceTypes; + + /** + * Create a new {@link ConfigDataLoaders} instance. + * @param logFactory the deferred log factory + * @param bootstrapContext the bootstrap context + * @param classLoader the class loader used when loading + */ + ConfigDataLoaders(DeferredLogFactory logFactory, ConfigurableBootstrapContext bootstrapContext, + ClassLoader classLoader) { + this(logFactory, bootstrapContext, classLoader, + SpringFactoriesLoader.loadFactoryNames(ConfigDataLoader.class, classLoader)); + } + + /** + * Create a new {@link ConfigDataLoaders} instance. + * @param logFactory the deferred log factory + * @param bootstrapContext the bootstrap context + * @param classLoader the class loader used when loading + * @param names the {@link ConfigDataLoader} class names instantiate + */ + ConfigDataLoaders(DeferredLogFactory logFactory, ConfigurableBootstrapContext bootstrapContext, + ClassLoader classLoader, List names) { + this.logger = logFactory.getLog(getClass()); + Instantiator> instantiator = new Instantiator<>(ConfigDataLoader.class, + (availableParameters) -> { + availableParameters.add(Log.class, logFactory::getLog); + availableParameters.add(DeferredLogFactory.class, logFactory); + availableParameters.add(ConfigurableBootstrapContext.class, bootstrapContext); + availableParameters.add(BootstrapContext.class, bootstrapContext); + availableParameters.add(BootstrapRegistry.class, bootstrapContext); + }); + this.loaders = instantiator.instantiate(classLoader, names); + this.resourceTypes = getResourceTypes(this.loaders); + } + + private List> getResourceTypes(List> loaders) { + List> resourceTypes = new ArrayList<>(loaders.size()); + for (ConfigDataLoader loader : loaders) { + resourceTypes.add(getResourceType(loader)); + } + return Collections.unmodifiableList(resourceTypes); + } + + private Class getResourceType(ConfigDataLoader loader) { + return ResolvableType.forClass(loader.getClass()).as(ConfigDataLoader.class).resolveGeneric(); + } + + /** + * Load {@link ConfigData} using the first appropriate {@link ConfigDataLoader}. + * @param the resource type + * @param context the loader context + * @param resource the resource to load + * @return the loaded {@link ConfigData} + * @throws IOException on IO error + */ + ConfigData load(ConfigDataLoaderContext context, R resource) throws IOException { + ConfigDataLoader loader = getLoader(context, resource); + this.logger.trace(LogMessage.of(() -> "Loading " + resource + " using loader " + loader.getClass().getName())); + return loader.load(context, resource); + } + + @SuppressWarnings("unchecked") + private ConfigDataLoader getLoader(ConfigDataLoaderContext context, R resource) { + ConfigDataLoader result = null; + for (int i = 0; i < this.loaders.size(); i++) { + ConfigDataLoader candidate = this.loaders.get(i); + if (this.resourceTypes.get(i).isInstance(resource)) { + ConfigDataLoader loader = (ConfigDataLoader) candidate; + if (loader.isLoadable(context, resource)) { + if (result != null) { + throw new IllegalStateException("Multiple loaders found for resource '" + resource + "' [" + + candidate.getClass().getName() + "," + result.getClass().getName() + "]"); + } + result = loader; + } + } + } + Assert.state(result != null, () -> "No loader found for resource '" + resource + "'"); + return result; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocation.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocation.java new file mode 100644 index 000000000000..f1df92af3e6b --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocation.java @@ -0,0 +1,172 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.config; + +import org.springframework.boot.origin.Origin; +import org.springframework.boot.origin.OriginProvider; +import org.springframework.util.StringUtils; + +/** + * A user specified location that can be {@link ConfigDataLocationResolver resolved} to + * one or more {@link ConfigDataResource config data resources}. A + * {@link ConfigDataLocation} is a simple wrapper around a {@link String} value. The exact + * format of the value will depend on the underlying technology, but is usually a URL like + * syntax consisting of a prefix and path. For example, {@code crypt:somehost/somepath}. + *

    + * Locations can be mandatory or {@link #isOptional() optional}. Optional locations are + * prefixed with {@code optional:}. + * + * @author Phillip Webb + * @since 2.4.0 + */ +public final class ConfigDataLocation implements OriginProvider { + + /** + * Prefix used to indicate that a {@link ConfigDataResource} is optional. + */ + public static final String OPTIONAL_PREFIX = "optional:"; + + private final boolean optional; + + private final String value; + + private final Origin origin; + + private ConfigDataLocation(boolean optional, String value, Origin origin) { + this.value = value; + this.optional = optional; + this.origin = origin; + } + + /** + * Return the the location is optional and should ignore + * {@link ConfigDataNotFoundException}. + * @return if the location is optional + */ + public boolean isOptional() { + return this.optional; + } + + /** + * Return the value of the location (always excluding any user specified + * {@code optional:} prefix. + * @return the location value + */ + public String getValue() { + return this.value; + } + + /** + * Return if {@link #getValue()} has the specified prefix. + * @param prefix the prefix to check + * @return if the value has the prefix + */ + public boolean hasPrefix(String prefix) { + return this.value.startsWith(prefix); + } + + /** + * Return {@link #getValue()} with the specified prefix removed. If the location does + * not have the given prefix then the {@link #getValue()} is returned unchanged. + * @param prefix the prefix to check + * @return the value with the prefix removed + */ + public String getNonPrefixedValue(String prefix) { + if (hasPrefix(prefix)) { + return this.value.substring(prefix.length()); + } + return this.value; + } + + @Override + public Origin getOrigin() { + return this.origin; + } + + /** + * Return an array of {@link ConfigDataLocation} elements built by splitting this + * {@link ConfigDataLocation} around a delimiter of {@code ";"}. + * @return the split locations + * @since 2.4.7 + */ + public ConfigDataLocation[] split() { + return split(";"); + } + + /** + * Return an array of {@link ConfigDataLocation} elements built by splitting this + * {@link ConfigDataLocation} around the specified delimiter. + * @param delimiter the delimiter to split on + * @return the split locations + * @since 2.4.7 + */ + public ConfigDataLocation[] split(String delimiter) { + String[] values = StringUtils.delimitedListToStringArray(toString(), delimiter); + ConfigDataLocation[] result = new ConfigDataLocation[values.length]; + for (int i = 0; i < values.length; i++) { + result[i] = of(values[i]).withOrigin(getOrigin()); + } + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ConfigDataLocation other = (ConfigDataLocation) obj; + return this.value.equals(other.value); + } + + @Override + public int hashCode() { + return this.value.hashCode(); + } + + @Override + public String toString() { + return (!this.optional) ? this.value : OPTIONAL_PREFIX + this.value; + } + + /** + * Create a new {@link ConfigDataLocation} with a specific {@link Origin}. + * @param origin the origin to set + * @return a new {@link ConfigDataLocation} instance. + */ + ConfigDataLocation withOrigin(Origin origin) { + return new ConfigDataLocation(this.optional, this.value, origin); + } + + /** + * Factory method to create a new {@link ConfigDataLocation} from a string. + * @param location the location string + * @return a {@link ConfigDataLocation} instance or {@code null} if no location was + * provided + */ + public static ConfigDataLocation of(String location) { + boolean optional = location != null && location.startsWith(OPTIONAL_PREFIX); + String value = (!optional) ? location : location.substring(OPTIONAL_PREFIX.length()); + if (!StringUtils.hasText(value)) { + return null; + } + return new ConfigDataLocation(optional, value, null); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationBindHandler.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationBindHandler.java new file mode 100644 index 000000000000..138f5ef0a741 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationBindHandler.java @@ -0,0 +1,75 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.config; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.springframework.boot.context.properties.bind.AbstractBindHandler; +import org.springframework.boot.context.properties.bind.BindContext; +import org.springframework.boot.context.properties.bind.BindHandler; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.origin.Origin; + +/** + * {@link BindHandler} to set the {@link Origin} of bound {@link ConfigDataLocation} + * objects. + * + * @author Phillip Webb + * @author Scott Frederick + */ +class ConfigDataLocationBindHandler extends AbstractBindHandler { + + @Override + @SuppressWarnings("unchecked") + public Object onSuccess(ConfigurationPropertyName name, Bindable target, BindContext context, Object result) { + if (result instanceof ConfigDataLocation) { + return withOrigin(context, (ConfigDataLocation) result); + } + if (result instanceof List) { + List list = ((List) result).stream().filter(Objects::nonNull).collect(Collectors.toList()); + for (int i = 0; i < list.size(); i++) { + Object element = list.get(i); + if (element instanceof ConfigDataLocation) { + list.set(i, withOrigin(context, (ConfigDataLocation) element)); + } + } + return list; + } + if (result instanceof ConfigDataLocation[]) { + ConfigDataLocation[] locations = Arrays.stream((ConfigDataLocation[]) result).filter(Objects::nonNull) + .toArray(ConfigDataLocation[]::new); + for (int i = 0; i < locations.length; i++) { + locations[i] = withOrigin(context, locations[i]); + } + return locations; + } + return result; + } + + private ConfigDataLocation withOrigin(BindContext context, ConfigDataLocation result) { + if (result.getOrigin() != null) { + return result; + } + Origin origin = Origin.from(context.getConfigurationProperty()); + return result.withOrigin(origin); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationNotFoundException.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationNotFoundException.java new file mode 100644 index 000000000000..62bd68dc528b --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationNotFoundException.java @@ -0,0 +1,89 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.config; + +import org.springframework.boot.origin.Origin; +import org.springframework.util.Assert; + +/** + * {@link ConfigDataNotFoundException} thrown when a {@link ConfigDataLocation} cannot be + * found. + * + * @author Phillip Webb + * @since 2.4.0 + */ +public class ConfigDataLocationNotFoundException extends ConfigDataNotFoundException { + + private final ConfigDataLocation location; + + /** + * Create a new {@link ConfigDataLocationNotFoundException} instance. + * @param location the location that could not be found + */ + public ConfigDataLocationNotFoundException(ConfigDataLocation location) { + this(location, null); + } + + /** + * Create a new {@link ConfigDataLocationNotFoundException} instance. + * @param location the location that could not be found + * @param cause the exception cause + */ + public ConfigDataLocationNotFoundException(ConfigDataLocation location, Throwable cause) { + this(location, getMessage(location), cause); + } + + /** + * Create a new {@link ConfigDataLocationNotFoundException} instance. + * @param location the location that could not be found + * @param message the exception message + * @param cause the exception cause + * @since 2.4.7 + */ + public ConfigDataLocationNotFoundException(ConfigDataLocation location, String message, Throwable cause) { + super(message, cause); + Assert.notNull(location, "Location must not be null"); + this.location = location; + } + + /** + * Return the location that could not be found. + * @return the location + */ + public ConfigDataLocation getLocation() { + return this.location; + } + + @Override + public Origin getOrigin() { + return Origin.from(this.location); + } + + @Override + public String getReferenceDescription() { + return getReferenceDescription(this.location); + } + + private static String getMessage(ConfigDataLocation location) { + return String.format("Config data %s cannot be found", getReferenceDescription(location)); + } + + private static String getReferenceDescription(ConfigDataLocation location) { + return String.format("location '%s'", location); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationResolver.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationResolver.java new file mode 100644 index 000000000000..08bfa76821b7 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationResolver.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.config; + +import java.util.Collections; +import java.util.List; + +import org.apache.commons.logging.Log; + +import org.springframework.boot.BootstrapContext; +import org.springframework.boot.BootstrapRegistry; +import org.springframework.boot.ConfigurableBootstrapContext; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.logging.DeferredLogFactory; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ResourceLoader; + +/** + * Strategy interface used to resolve {@link ConfigDataLocation locations} into one or + * more {@link ConfigDataResource resources}. Implementations should be added as a + * {@code spring.factories} entries. The following constructor parameter types are + * supported: + *
      + *
    • {@link Log} or {@link DeferredLogFactory} - if the resolver needs deferred + * logging
    • + *
    • {@link Binder} - if the resolver needs to obtain values from the initial + * {@link Environment}
    • + *
    • {@link ResourceLoader} - if the resolver needs a resource loader
    • + *
    • {@link ConfigurableBootstrapContext} - A bootstrap context that can be used to + * store objects that may be expensive to create, or need to be shared + * ({@link BootstrapContext} or {@link BootstrapRegistry} may also be used).
    • + *
    + *

    + * Resolvers may implement {@link Ordered} or use the {@link Order @Order} annotation. The + * first resolver that supports the given location will be used. + * + * @param the location type + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.4.0 + */ +public interface ConfigDataLocationResolver { + + /** + * Returns if the specified location address can be resolved by this resolver. + * @param context the location resolver context + * @param location the location to check. + * @return if the location is supported by this resolver + */ + boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDataLocation location); + + /** + * Resolve a {@link ConfigDataLocation} into one or more {@link ConfigDataResource} + * instances. + * @param context the location resolver context + * @param location the location that should be resolved + * @return a list of {@link ConfigDataResource resources} in ascending priority order. + * @throws ConfigDataLocationNotFoundException on a non-optional location that cannot + * be found + * @throws ConfigDataResourceNotFoundException if a resolved resource cannot be found + */ + List resolve(ConfigDataLocationResolverContext context, ConfigDataLocation location) + throws ConfigDataLocationNotFoundException, ConfigDataResourceNotFoundException; + + /** + * Resolve a {@link ConfigDataLocation} into one or more {@link ConfigDataResource} + * instances based on available profiles. This method is called once profiles have + * been deduced from the contributed values. By default this method returns an empty + * list. + * @param context the location resolver context + * @param location the location that should be resolved + * @param profiles profile information + * @return a list of resolved locations in ascending priority order. + * @throws ConfigDataLocationNotFoundException on a non-optional location that cannot + * be found + */ + default List resolveProfileSpecific(ConfigDataLocationResolverContext context, ConfigDataLocation location, + Profiles profiles) throws ConfigDataLocationNotFoundException { + return Collections.emptyList(); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationResolverContext.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationResolverContext.java new file mode 100644 index 000000000000..2ba29d0b297d --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationResolverContext.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.config; + +import org.springframework.boot.ConfigurableBootstrapContext; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.env.EnvironmentPostProcessor; + +/** + * Context provided to {@link ConfigDataLocationResolver} methods. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.4.0 + */ +public interface ConfigDataLocationResolverContext { + + /** + * Provides access to a binder that can be used to obtain previously contributed + * values. + * @return a binder instance + */ + Binder getBinder(); + + /** + * Provides access to the parent {@link ConfigDataResource} that triggered the resolve + * or {@code null} if there is no available parent. + * @return the parent location + */ + ConfigDataResource getParent(); + + /** + * Provides access to the {@link ConfigurableBootstrapContext} shared across all + * {@link EnvironmentPostProcessor EnvironmentPostProcessors}. + * @return the bootstrap context + */ + ConfigurableBootstrapContext getBootstrapContext(); + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationResolvers.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationResolvers.java new file mode 100644 index 000000000000..bfdc0ac5fe3b --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationResolvers.java @@ -0,0 +1,154 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.config; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +import org.apache.commons.logging.Log; + +import org.springframework.boot.BootstrapContext; +import org.springframework.boot.BootstrapRegistry; +import org.springframework.boot.ConfigurableBootstrapContext; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.logging.DeferredLogFactory; +import org.springframework.boot.util.Instantiator; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.SpringFactoriesLoader; + +/** + * A collection of {@link ConfigDataLocationResolver} instances loaded via + * {@code spring.factories}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +class ConfigDataLocationResolvers { + + private final List> resolvers; + + /** + * Create a new {@link ConfigDataLocationResolvers} instance. + * @param logFactory a {@link DeferredLogFactory} used to inject {@link Log} instances + * @param bootstrapContext the bootstrap context + * @param binder a binder providing values from the initial {@link Environment} + * @param resourceLoader {@link ResourceLoader} to load resource locations + */ + ConfigDataLocationResolvers(DeferredLogFactory logFactory, ConfigurableBootstrapContext bootstrapContext, + Binder binder, ResourceLoader resourceLoader) { + this(logFactory, bootstrapContext, binder, resourceLoader, SpringFactoriesLoader + .loadFactoryNames(ConfigDataLocationResolver.class, resourceLoader.getClassLoader())); + } + + /** + * Create a new {@link ConfigDataLocationResolvers} instance. + * @param logFactory a {@link DeferredLogFactory} used to inject {@link Log} instances + * @param bootstrapContext the bootstrap context + * @param binder {@link Binder} providing values from the initial {@link Environment} + * @param resourceLoader {@link ResourceLoader} to load resource locations + * @param names the {@link ConfigDataLocationResolver} class names + */ + ConfigDataLocationResolvers(DeferredLogFactory logFactory, ConfigurableBootstrapContext bootstrapContext, + Binder binder, ResourceLoader resourceLoader, List names) { + Instantiator> instantiator = new Instantiator<>(ConfigDataLocationResolver.class, + (availableParameters) -> { + availableParameters.add(Log.class, logFactory::getLog); + availableParameters.add(DeferredLogFactory.class, logFactory); + availableParameters.add(Binder.class, binder); + availableParameters.add(ResourceLoader.class, resourceLoader); + availableParameters.add(ConfigurableBootstrapContext.class, bootstrapContext); + availableParameters.add(BootstrapContext.class, bootstrapContext); + availableParameters.add(BootstrapRegistry.class, bootstrapContext); + }); + this.resolvers = reorder(instantiator.instantiate(resourceLoader.getClassLoader(), names)); + } + + private List> reorder(List> resolvers) { + List> reordered = new ArrayList<>(resolvers.size()); + StandardConfigDataLocationResolver resourceResolver = null; + for (ConfigDataLocationResolver resolver : resolvers) { + if (resolver instanceof StandardConfigDataLocationResolver) { + resourceResolver = (StandardConfigDataLocationResolver) resolver; + } + else { + reordered.add(resolver); + } + } + if (resourceResolver != null) { + reordered.add(resourceResolver); + } + return Collections.unmodifiableList(reordered); + } + + List resolve(ConfigDataLocationResolverContext context, ConfigDataLocation location, + Profiles profiles) { + if (location == null) { + return Collections.emptyList(); + } + for (ConfigDataLocationResolver resolver : getResolvers()) { + if (resolver.isResolvable(context, location)) { + return resolve(resolver, context, location, profiles); + } + } + throw new UnsupportedConfigDataLocationException(location); + } + + private List resolve(ConfigDataLocationResolver resolver, + ConfigDataLocationResolverContext context, ConfigDataLocation location, Profiles profiles) { + List resolved = resolve(location, false, () -> resolver.resolve(context, location)); + if (profiles == null) { + return resolved; + } + List profileSpecific = resolve(location, true, + () -> resolver.resolveProfileSpecific(context, location, profiles)); + return merge(resolved, profileSpecific); + } + + private List resolve(ConfigDataLocation location, boolean profileSpecific, + Supplier> resolveAction) { + List resources = nonNullList(resolveAction.get()); + List resolved = new ArrayList<>(resources.size()); + for (ConfigDataResource resource : resources) { + resolved.add(new ConfigDataResolutionResult(location, resource, profileSpecific)); + } + return resolved; + } + + @SuppressWarnings("unchecked") + private List nonNullList(List list) { + return (list != null) ? (List) list : Collections.emptyList(); + } + + private List merge(List list1, List list2) { + List merged = new ArrayList<>(list1.size() + list2.size()); + merged.addAll(list1); + merged.addAll(list2); + return merged; + } + + /** + * Return the resolvers managed by this object. + * @return the resolvers + */ + List> getResolvers() { + return this.resolvers; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataNotFoundAction.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataNotFoundAction.java new file mode 100644 index 000000000000..74f92d14a48d --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataNotFoundAction.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.config; + +import org.apache.commons.logging.Log; + +import org.springframework.core.log.LogMessage; + +/** + * Action to take when an uncaught {@link ConfigDataNotFoundException} is thrown. + * + * @author Phillip Webb + * @since 2.4.0 + */ +public enum ConfigDataNotFoundAction { + + /** + * Throw the exception to fail startup. + */ + FAIL { + + @Override + void handle(Log logger, ConfigDataNotFoundException ex) { + throw ex; + } + + }, + + /** + * Ignore the exception and continue processing the remaining locations. + */ + IGNORE { + + @Override + void handle(Log logger, ConfigDataNotFoundException ex) { + logger.trace(LogMessage.format("Ignoring missing config data %s", ex.getReferenceDescription())); + } + + }; + + /** + * Handle the given exception. + * @param logger the logger used for output {@code ConfigDataLocation}) + * @param ex the exception to handle + */ + abstract void handle(Log logger, ConfigDataNotFoundException ex); + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataNotFoundException.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataNotFoundException.java new file mode 100644 index 000000000000..2accaeb6158f --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataNotFoundException.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.config; + +import org.springframework.boot.origin.OriginProvider; + +/** + * {@link ConfigDataNotFoundException} thrown when a {@link ConfigData} cannot be found. + * + * @author Phillip Webb + * @since 2.4.0 + */ +public abstract class ConfigDataNotFoundException extends ConfigDataException implements OriginProvider { + + /** + * Create a new {@link ConfigDataNotFoundException} instance. + * @param message the exception message + * @param cause the exception cause + */ + ConfigDataNotFoundException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Return a description of actual referenced item that could not be found. + * @return a description of the referenced items + */ + public abstract String getReferenceDescription(); + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataNotFoundFailureAnalyzer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataNotFoundFailureAnalyzer.java new file mode 100644 index 000000000000..2202d0fb9f4e --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataNotFoundFailureAnalyzer.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.config; + +import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; +import org.springframework.boot.diagnostics.FailureAnalysis; +import org.springframework.boot.origin.Origin; + +/** + * An implementation of {@link AbstractFailureAnalyzer} to analyze failures caused by + * {@link ConfigDataNotFoundException}. + * + * @author Michal Mlak + * @author Phillip Webb + */ +class ConfigDataNotFoundFailureAnalyzer extends AbstractFailureAnalyzer { + + @Override + protected FailureAnalysis analyze(Throwable rootFailure, ConfigDataNotFoundException cause) { + ConfigDataLocation location = getLocation(cause); + Origin origin = Origin.from(location); + String message = String.format("Config data %s does not exist", cause.getReferenceDescription()); + StringBuilder action = new StringBuilder("Check that the value "); + if (location != null) { + action.append(String.format("'%s' ", location)); + } + if (origin != null) { + action.append(String.format("at %s ", origin)); + } + action.append("is correct"); + if (location != null && !location.isOptional()) { + action.append(String.format(", or prefix it with '%s'", ConfigDataLocation.OPTIONAL_PREFIX)); + } + return new FailureAnalysis(message, action.toString(), cause); + } + + private ConfigDataLocation getLocation(ConfigDataNotFoundException cause) { + if (cause instanceof ConfigDataLocationNotFoundException) { + return ((ConfigDataLocationNotFoundException) cause).getLocation(); + } + if (cause instanceof ConfigDataResourceNotFoundException) { + return ((ConfigDataResourceNotFoundException) cause).getLocation(); + } + return null; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataProperties.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataProperties.java new file mode 100644 index 000000000000..e9337c70f1ad --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataProperties.java @@ -0,0 +1,187 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.config; + +import java.util.Collections; +import java.util.List; +import java.util.function.Predicate; + +import org.springframework.boot.cloud.CloudPlatform; +import org.springframework.boot.context.properties.bind.BindContext; +import org.springframework.boot.context.properties.bind.BindHandler; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.bind.Name; +import org.springframework.boot.context.properties.source.ConfigurationProperty; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.util.ObjectUtils; + +/** + * Bound properties used when working with {@link ConfigData}. + * + * @author Phillip Webb + * @author Madhura Bhave + */ +class ConfigDataProperties { + + private static final ConfigurationPropertyName NAME = ConfigurationPropertyName.of("spring.config"); + + private static final ConfigurationPropertyName LEGACY_PROFILES_NAME = ConfigurationPropertyName + .of("spring.profiles"); + + private static final Bindable BINDABLE_PROPERTIES = Bindable.of(ConfigDataProperties.class); + + private static final Bindable BINDABLE_STRING_ARRAY = Bindable.of(String[].class); + + private final List imports; + + private final Activate activate; + + /** + * Create a new {@link ConfigDataProperties} instance. + * @param imports the imports requested + * @param activate the activate properties + */ + ConfigDataProperties(@Name("import") List imports, Activate activate) { + this.imports = (imports != null) ? imports : Collections.emptyList(); + this.activate = activate; + } + + /** + * Return any additional imports requested. + * @return the requested imports + */ + List getImports() { + return this.imports; + } + + /** + * Return {@code true} if the properties indicate that the config data property source + * is active for the given activation context. + * @param activationContext the activation context + * @return {@code true} if the config data property source is active + */ + boolean isActive(ConfigDataActivationContext activationContext) { + return this.activate == null || this.activate.isActive(activationContext); + } + + /** + * Return a new variant of these properties without any imports. + * @return a new {@link ConfigDataProperties} instance + */ + ConfigDataProperties withoutImports() { + return new ConfigDataProperties(null, this.activate); + } + + ConfigDataProperties withLegacyProfiles(String[] legacyProfiles, ConfigurationProperty property) { + if (this.activate != null && !ObjectUtils.isEmpty(this.activate.onProfile)) { + throw new InvalidConfigDataPropertyException(property, false, NAME.append("activate.on-profile"), null); + } + return new ConfigDataProperties(this.imports, new Activate(this.activate.onCloudPlatform, legacyProfiles)); + } + + /** + * Factory method used to create {@link ConfigDataProperties} from the given + * {@link Binder}. + * @param binder the binder used to bind the properties + * @return a {@link ConfigDataProperties} instance or {@code null} + */ + static ConfigDataProperties get(Binder binder) { + LegacyProfilesBindHandler legacyProfilesBindHandler = new LegacyProfilesBindHandler(); + String[] legacyProfiles = binder.bind(LEGACY_PROFILES_NAME, BINDABLE_STRING_ARRAY, legacyProfilesBindHandler) + .orElse(null); + ConfigDataProperties properties = binder.bind(NAME, BINDABLE_PROPERTIES, new ConfigDataLocationBindHandler()) + .orElse(null); + if (!ObjectUtils.isEmpty(legacyProfiles)) { + properties = (properties != null) + ? properties.withLegacyProfiles(legacyProfiles, legacyProfilesBindHandler.getProperty()) + : new ConfigDataProperties(null, new Activate(null, legacyProfiles)); + } + return properties; + } + + /** + * {@link BindHandler} used to check for legacy processing properties. + */ + private static class LegacyProfilesBindHandler implements BindHandler { + + private ConfigurationProperty property; + + @Override + public Object onSuccess(ConfigurationPropertyName name, Bindable target, BindContext context, + Object result) { + this.property = context.getConfigurationProperty(); + return result; + } + + ConfigurationProperty getProperty() { + return this.property; + } + + } + + /** + * Activate properties used to determine when a config data property source is active. + */ + static class Activate { + + private final CloudPlatform onCloudPlatform; + + private final String[] onProfile; + + /** + * Create a new {@link Activate} instance. + * @param onCloudPlatform the cloud platform required for activation + * @param onProfile the profile expression required for activation + */ + Activate(CloudPlatform onCloudPlatform, String[] onProfile) { + this.onProfile = onProfile; + this.onCloudPlatform = onCloudPlatform; + } + + /** + * Return {@code true} if the properties indicate that the config data property + * source is active for the given activation context. + * @param activationContext the activation context + * @return {@code true} if the config data property source is active + */ + boolean isActive(ConfigDataActivationContext activationContext) { + if (activationContext == null) { + return false; + } + boolean activate = true; + activate = activate && isActive(activationContext.getCloudPlatform()); + activate = activate && isActive(activationContext.getProfiles()); + return activate; + } + + private boolean isActive(CloudPlatform cloudPlatform) { + return this.onCloudPlatform == null || this.onCloudPlatform == cloudPlatform; + } + + private boolean isActive(Profiles profiles) { + return ObjectUtils.isEmpty(this.onProfile) + || (profiles != null && matchesActiveProfiles(profiles::isAccepted)); + } + + private boolean matchesActiveProfiles(Predicate activeProfiles) { + return org.springframework.core.env.Profiles.of(this.onProfile).matches(activeProfiles); + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataResolutionResult.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataResolutionResult.java new file mode 100644 index 000000000000..d7c35255f4ec --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataResolutionResult.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.config; + +/** + * Result returned from {@link ConfigDataLocationResolvers} containing both the + * {@link ConfigDataResource} and the original {@link ConfigDataLocation}. + * + * @author Phillip Webb + */ +class ConfigDataResolutionResult { + + private final ConfigDataLocation location; + + private final ConfigDataResource resource; + + private final boolean profileSpecific; + + ConfigDataResolutionResult(ConfigDataLocation location, ConfigDataResource resource, boolean profileSpecific) { + this.location = location; + this.resource = resource; + this.profileSpecific = profileSpecific; + } + + ConfigDataLocation getLocation() { + return this.location; + } + + ConfigDataResource getResource() { + return this.resource; + } + + boolean isProfileSpecific() { + return this.profileSpecific; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataResource.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataResource.java new file mode 100644 index 000000000000..45505150b877 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataResource.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.config; + +/** + * A single resource from which {@link ConfigData} can be loaded. Implementations must + * implement a valid {@link #equals(Object) equals}, {@link #hashCode() hashCode} and + * {@link #toString() toString} methods. + * + * @author Phillip Webb + * @author Madhura Bhave + * @since 2.4.0 + */ +public abstract class ConfigDataResource { + + private final boolean optional; + + /** + * Create a new non-optional {@link ConfigDataResource} instance. + */ + public ConfigDataResource() { + this(false); + } + + /** + * Create a new {@link ConfigDataResource} instance. + * @param optional if the resource is optional + * @since 2.4.6 + */ + protected ConfigDataResource(boolean optional) { + this.optional = optional; + } + + boolean isOptional() { + return this.optional; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataResourceNotFoundException.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataResourceNotFoundException.java new file mode 100644 index 000000000000..523b46423889 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataResourceNotFoundException.java @@ -0,0 +1,148 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.config; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.springframework.boot.origin.Origin; +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; + +/** + * {@link ConfigDataNotFoundException} thrown when a {@link ConfigDataResource} cannot be + * found. + * + * @author Phillip Webb + * @since 2.4.0 + */ +public class ConfigDataResourceNotFoundException extends ConfigDataNotFoundException { + + private final ConfigDataResource resource; + + private final ConfigDataLocation location; + + /** + * Create a new {@link ConfigDataResourceNotFoundException} instance. + * @param resource the resource that could not be found + */ + public ConfigDataResourceNotFoundException(ConfigDataResource resource) { + this(resource, null); + } + + /** + * Create a new {@link ConfigDataResourceNotFoundException} instance. + * @param resource the resource that could not be found + * @param cause the exception cause + */ + public ConfigDataResourceNotFoundException(ConfigDataResource resource, Throwable cause) { + this(resource, null, cause); + } + + private ConfigDataResourceNotFoundException(ConfigDataResource resource, ConfigDataLocation location, + Throwable cause) { + super(getMessage(resource, location), cause); + Assert.notNull(resource, "Resource must not be null"); + this.resource = resource; + this.location = location; + } + + /** + * Return the resource that could not be found. + * @return the resource + */ + public ConfigDataResource getResource() { + return this.resource; + } + + /** + * Return the original location that was resolved to determine the resource. + * @return the location or {@code null} if no location is availble + */ + public ConfigDataLocation getLocation() { + return this.location; + } + + @Override + public Origin getOrigin() { + return Origin.from(this.location); + } + + @Override + public String getReferenceDescription() { + return getReferenceDescription(this.resource, this.location); + } + + /** + * Create a new {@link ConfigDataResourceNotFoundException} instance with a location. + * @param location the location to set + * @return a new {@link ConfigDataResourceNotFoundException} instance + */ + ConfigDataResourceNotFoundException withLocation(ConfigDataLocation location) { + return new ConfigDataResourceNotFoundException(this.resource, location, getCause()); + } + + private static String getMessage(ConfigDataResource resource, ConfigDataLocation location) { + return String.format("Config data %s cannot be found", getReferenceDescription(resource, location)); + } + + private static String getReferenceDescription(ConfigDataResource resource, ConfigDataLocation location) { + String description = String.format("resource '%s'", resource); + if (location != null) { + description += String.format(" via location '%s'", location); + } + return description; + } + + /** + * Throw a {@link ConfigDataNotFoundException} if the specified {@link Path} does not + * exist. + * @param resource the config data resource + * @param pathToCheck the path to check + */ + public static void throwIfDoesNotExist(ConfigDataResource resource, Path pathToCheck) { + throwIfDoesNotExist(resource, Files.exists(pathToCheck)); + } + + /** + * Throw a {@link ConfigDataNotFoundException} if the specified {@link File} does not + * exist. + * @param resource the config data resource + * @param fileToCheck the file to check + */ + public static void throwIfDoesNotExist(ConfigDataResource resource, File fileToCheck) { + throwIfDoesNotExist(resource, fileToCheck.exists()); + } + + /** + * Throw a {@link ConfigDataNotFoundException} if the specified {@link Resource} does + * not exist. + * @param resource the config data resource + * @param resourceToCheck the resource to check + */ + public static void throwIfDoesNotExist(ConfigDataResource resource, Resource resourceToCheck) { + throwIfDoesNotExist(resource, resourceToCheck.exists()); + } + + private static void throwIfDoesNotExist(ConfigDataResource resource, boolean exists) { + if (!exists) { + throw new ConfigDataResourceNotFoundException(resource); + } + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigFileApplicationListener.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigFileApplicationListener.java index b33937b87a72..9250ef4e5c50 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigFileApplicationListener.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigFileApplicationListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,14 @@ package org.springframework.boot.context.config; +import java.io.File; import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; @@ -28,15 +32,19 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.function.BiConsumer; +import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.commons.logging.Log; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.DefaultPropertiesPropertySource; import org.springframework.boot.SpringApplication; import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; import org.springframework.boot.context.event.ApplicationPreparedEvent; @@ -53,15 +61,17 @@ import org.springframework.context.annotation.ConfigurationClassPostProcessor; import org.springframework.context.event.SmartApplicationListener; import org.springframework.core.Ordered; -import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.env.AbstractEnvironment; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.Profiles; import org.springframework.core.env.PropertySource; import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -75,6 +85,7 @@ * 'application.properties' and/or 'application.yml' files in the following locations: *